一.Vue-Router基本应用
通过Vue路由的基本配置来探索Vue-Router
import Vue from 'vue'import Router from 'vue-router'import Home from './views/Home.vue'import About from './views/About.vue'Vue.use(Router);// 使用Vue-Router插件export default new Router({ // 创建Vue-router实例,将实例注入到main.js中routes: [{path: '/',name: 'home',component: Home},{path: '/about',name: 'about',component: About,children: [{path: 'a', component: {render(h) {return <h1>about A</h1>}}},{path: 'b', component: {render(h) {return <h1>about B</h1>}}}]}]})new Vue({router, // 在根实例中注入router实例render: h => h(App)}).$mount('#app')
这里我们不难发现核心方法就是Vue.use(Router),在就是new Router产生router实例
二.编写Vue-Router
从这里开始我们自己来实现一个Vue-router插件,这里先来标注一下整体目录结构
├─vue-router│ ├─components # 存放vue-router的两个核心组件│ │ ├─link.js│ │ └─view.js│ ├─history # 存放浏览器跳转相关逻辑│ │ ├─base.js│ │ └─hash.js│ ├─create-matcher.js # 创建匹配器│ ├─create-route-map.js # 创建路由映射表│ ├─index.js # 引用时的入口文件│ ├─install.js # install方法
默认我们引用Vue-Router使用的是index.js文件,use方法默认会调用当前返回对象的install方法
import install from './install'export default class VueRouter{}VueRouter.install = install; // 提供的install方法
2.1 编写install方法
export let _Vue;export default function install(Vue) {_Vue = Vue;Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法beforeCreate() {if (this.$options.router) { // 如果有router属性说明是根实例this._routerRoot = this; // 将根实例挂载在_routerRoot属性上this._router = this.$options.router; // 将当前router实例挂载在_router上this._router.init(this); // 初始化路由,这里的this指向的是根实例} else { // 父组件渲染后会渲染子组件this._routerRoot = this.$parent && this.$parent._routerRoot;// 保证所有子组件都拥有_routerRoot 属性,指向根实例// 保证所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象}}})}
这里我们应该在Vue-Router上增加一个init方法,主要目的就是初始化功能
这里在强调下,什么是路由? 路由就是匹配到对应路径显示对应的组件!
import createMatcher from './create-matcher'import install from './install'export default class VueRouter{constructor(options){// 根据用户传递的routes创建匹配关系,this.matcher需要提供两个方法// match:match方法用来匹配规则// addRoutes:用来动态添加路由this.matcher = createMatcher(options.routes || []);}init(app){}}VueRouter.install = install;
2.2 编写createMatcher方法
import createRouteMap from './create-route-map'export default function createMatcher(routes) {// 收集所有的路由路径, 收集路径的对应渲染关系// pathList = ['/','/about','/about/a','/about/b']// pathMap = {'/':'/的记录','/about':'/about记录'...}let {pathList,pathMap} = createRouteMap(routes);// 这个方法就是动态加载路由的方法function addRoutes(routes){// 将新增的路由追加到pathList和pathMap中createRouteMap(routes,pathList,pathMap);}function match(){} // 稍后根据路径找到对应的记录return {addRoutes,match}}
这里需要创建映射关系,需要createRouteMap方法
2.3 编写createRouteMap方法
export default function createRouteMap(routes,oldPathList,oldPathMap){// 当第一次加载的时候没有 pathList 和 pathMaplet pathList = oldPathList || [];let pathMap = oldPathMap || Object.create(null);routes.forEach(route=>{// 添加到路由记录,用户配置可能是无限层级,稍后要递归调用此方法addRouteRecord(route,pathList,pathMap);});return { // 导出映射关系pathList,pathMap}}// 将当前路由存储到pathList和pathMap中function addRouteRecord(route,pathList,pathMap,parent){// 如果是子路由记录 需要增加前缀let path = parent?`${parent.path}/${route.path}`:route.path;let record = { // 提取需要的信息path,component:route.component,parent}if(!pathMap[path]){pathList.push(path);pathMap[path] = record;}if(route.children){ // 递归添加子路由route.children.forEach(r=>{// 这里需要标记父亲是谁addRouteRecord(r,pathList,pathMap,route);})}}
该方法主要是处理路径和不同路径对应的记录
matcher我们先写到这,稍后在来补全match方法的实现
2.4 编写浏览器历史相关代码
import HashHistory from './history/hash'constructor(options){this.matcher = createMatcher(options.routes || []);// vue路由有三种模式 hash / h5api /abstract ,为了保证调用时方法一致。我们需要提供一个base类,在分别实现子类,不同模式下通过父类调用对应子类的方法this.history = new HashHistory(this);}
这里我们以hash路由为主,创建hash路由实例
import History from './base'// hash路由export default class HashHistory extends History{constructor(router){super(router);}}// 路由的基类export default class History {constructor(router){this.router = router;}}
如果是hash路由,打开网站如果没有hash默认应该添加#/
import History from './base';function ensureSlash(){if(window.location.hash){return}window.location.hash = '/'}export default class HashHistory extends History{constructor(router){super(router);ensureSlash(); // 确保有hash}}
稍后我们在继续扩展路由相关代码,我们先把焦点转向初始化逻辑
init(app){const history = this.history;// 初始化时,应该先拿到当前路径,进行匹配逻辑// 让路由系统过度到某个路径const setupHashListener = ()=> {history.setupListener(); // 监听路径变化}history.transitionTo( // 父类提供方法负责跳转history.getCurrentLocation(), // 子类获取对应的路径// 跳转成功后注册路径监听,为视图更新做准备setupHashListener)}
这里我们要分别实现 transitionTo(基类方法)、 getCurrentLocation 、setupListener
getCurrentLocation实现
function getHash(){return window.location.hash.slice(1);}export default class HashHistory extends History{// ...getCurrentLocation(){return getHash();}}
setupListener实现
export default class HashHistory extends History{// ...setupListener(){window.addEventListener('hashchange', ()=> {// 根据当前hash值 过度到对应路径this.transitionTo(getHash());})}}
可以看到最核心的还是transitionTo方法
TransitionTo实现
export function createRoute(record, location) { // {path:'/',matched:[record,record]}let res = [];if (record) { // 如果有记录while(record){res.unshift(record); // 就将当前记录的父亲放到前面record = record.parent}}return {...location,matched: res}}export default class History {constructor(router) {this.router = router;// 根据记录和路径返回对象,稍后会用于router-view的匹配this.current = createRoute(null, {path: '/'})}// 核心逻辑transitionTo(location, onComplete) {// 去匹配路径let route = this.router.match(location);// 相同路径不必过渡if(location === route.path &&route.matched.length === this.current.matched.length){return}this.updateRoute(route); // 更新路由即可onComplete && onComplete();}updateRoute(route){ // 跟新current属性this.current =route;}}
export default class VueRouter{// ...match(location){return this.matcher.match(location);}}
终于这回可以完善一下刚才没有写完的match方法
function match(location){ // 稍后根据路径找到对应的记录let record = pathMap[location]if (record) { // 根据记录创建对应的路由return createRoute(record,{path:location})}// 找不到则返回空匹配return createRoute(null, {path: location})}
我们不难发现路径变化时都会更改current属性,我们可以把current属性变成响应式的,每次current变化刷新视图即可
export let _Vue;export default function install(Vue) {_Vue = Vue;Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法beforeCreate() {if (this.$options.router) { // 如果有router属性说明是根实例// ...Vue.util.defineReactive(this,'_route',this._router.history.current);}// ...}});// 仅仅是为了更加方便Object.defineProperty(Vue.prototype,'$route',{ // 每个实例都可以获取到$route属性get(){return this._routerRoot._route;}});Object.defineProperty(Vue.prototype,'$router',{ // 每个实例都可以获取router实例get(){return this._routerRoot._router;}})}
Vue.util.defineReactive 这个方法是vue中响应式数据变化的核心
当路径变化时需要执行此回调更新_route属性, 在init方法中增加监听函数
history.listen((route) => { // 需要更新_route属性app._route = route});
export default class History {constructor(router) {// ...this.cb = null;}listen(cb){this.cb = cb; // 注册函数}updateRoute(route){this.current =route;this.cb && this.cb(route); // 更新current后 更新_route属性}}
三.编写Router-Link及Router-View组件
3.1 router-view组件
export default {functional:true,render(h,{parent,data}){let route = parent.$route;let depth = 0;data.routerView = true;while(parent){ // 根据matched 渲染对应的router-viewif (parent.$vnode && parent.$vnode.data.routerView){depth++;}parent = parent.$parent;}let record = route.matched[depth];if(!record){return h();}return h(record.component, data);}}
3.2 router-link组件
export default {props:{to:{type:String,required:true},tag:{type:String}},render(h){let tag = this.tag || 'a';let handler = ()=>{this.$router.push(this.to);}return <tag onClick={handler}>{this.$slots.default}</tag>}}
四.beforeEach实现
this.beforeHooks = [];beforeEach(fn){ // 将fn注册到队列中this.beforeHooks.push(fn);}
将用户函数注册到数组中
function runQueue(queue, iterator,cb) { // 迭代queuefunction step(index){if(index >= queue.length){cb();}else{let hook = queue[index];iterator(hook,()=>{ // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入step(index+1)})}}step(0)}export default class History {transitionTo(location, onComplete) {// 跳转到这个路径let route = this.router.match(location);if (location === this.current.path && route.matched.length === this.current.matched.length) {return}let queue = [].concat(this.router.beforeHooks);const iterator = (hook, next) => {hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数next();});}runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由this.updateRoute(route);onComplete && onComplete();});}updateRoute(route) {this.current = route;this.cb && this.cb(route);}listen(cb) {this.cb = cb;}}
