源码 -VueRouter3

308 阅读3分钟

一、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插件 讲vueRouter实例 挂到Vue的原型对象上
    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 <h3>about A</h3>}
              }
            },
            {
              path: 'b', component: {
                render(h) {return <h3>about B</h3>}
              }
            }
          ]
        }
      ]
    })
    
new Vue({
  router, // 在根实例中注入router实例
  render: h => h(App)
}).$mount('#app')

二.手写 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方法

1、 index.js 文件

use方法默认会调用当前返回对象的install方法

// index.js
import install from './install'
import HashHistory from './history/hash'
import createMatcher from './creat-matcher'
export default class VueRouter {
  constructor (options) {
    this.matcher = createMatcher(options.routes || [])
    // vue路由有三种模式 hash / h5api /abstract ,为了保证调用时方法一致。
    // 我们需要提供一个base类,在分别实现子类,不同模式下通过父类调用对应子类的方法
    this.history = new HashHistory(this)
    // beforeEach钩子集合 
    this.beforeHooks = [];
  }
  beforeEach(fn){ // 将fn注册到队列中
        this.beforeHooks.push(fn);
  }
  match (location) {
    return this.matcher.match(location)
  }
  // 方便页面上函数式跳转
  push (url) {
    this.history.push(url)
  }
  init (app) {
    const history = this.history
    // 初始化时,应该先拿到当前路径,进行匹配逻辑

    // 让路由系统过度到某个路径
    const setupHashListener = () => {
      history.setupListener() // 监听路径变化
    }
    history.transitionTo(
      // 父类提供方法负责跳转
      history.getCurrentLocation(), // 子类获取对应的路径
      // 跳转成功后注册路径监听,为视图更新做准备
      setupHashListener
    )
    history.listen(route => {
      // 需要更新_route属性
      app._route = route
    })
  }
}
VueRouter.install = install // 提供的install方法

2、install.js中 编写install方法

import RouterView from './components/view'
import RouterLink from './components/link'

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指向的是根实例
        Vue.util.defineReactive(this, '_route', this._router.history.current)
        // console.log('我是根组件',this)
      } else {
        // 父组件渲染后会渲染子组件
        this._routerRoot = this.$parent && this.$parent._routerRoot
        // 保证所有子组件都拥有_routerRoot 属性,指向根实例
        // 保证所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象
        // console.log('我是子组件组件',this)
      }
    }
  })
  Vue.component('router-view', RouterView)
  Vue.component('router-link', RouterLink)
  // 仅仅是为了更加方便
  Object.defineProperty(Vue.prototype, '$route', {
    // 每个实例都可以获取到$route属性
    get () {
      return this._routerRoot._route
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    // 每个实例都可以获取router实例
    get () {
      return this._routerRoot._router
    }
  })
}

2、create-matcher.js 页面上所有的router-view组件 用一个适配器 对应上

import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
    // 收集所有的路由路径, 收集路径的对应渲染关系
    // pathList = ['/','/about','/about/a','/about/b']
    // pathMap = {'/':{path:'/',component,parent},'/about':{path:'/',component,parent}...}
    let {pathList,pathMap} = createRouteMap(routes);
    
    // 这个方法就是动态加载路由的方法
    function addRoutes(routes){
        // 将新增的路由追加到pathList和pathMap中
        createRouteMap(routes,pathList,pathMap);
    }   
    function match(location){ // 稍后根据路径找到对应的记录
        let record = pathMap[location]
        if (record) { // 根据记录创建对应的路由
            return createRoute(record,{
                path:location
            })
        }
        // 找不到则返回空匹配
        return createRoute(null, {
            path: location
        })
    }
    return {
        addRoutes,
        match
    }
}

3、create-route-map.js 需要创建路由映射关系

export default function createRouteMap(routes,oldPathList,oldPathMap){
    // 当第一次加载的时候没有 pathList 和 pathMap
    let 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);
        })
    }
}

4.与浏览器相关代码

写一个基本的功能的history 方便几种路由扩展

// history/base.js
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
  }
}
// 钩子迭代器
function runQueue(queue, iterator,cb) { 
    function 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 {
  constructor (router) {
    this.router = router
    // 根据记录和路径返回对象,稍后会用于router-view的匹配
    this.current = createRoute(null, {
      path: '/'
    })
    this.cb = null
  }
  listen (cb) {
    this.cb = cb // 注册函数
  }
  // 核心逻辑
  transitionTo (location, onComplete) {
    // 去匹配路径
    let route = this.router.match(location)
    // 相同路径不必过渡
    if (location === this.current.path && route.matched.length === this.current.matched.length) {
      return
    }
    this.updateRoute(route) // 更新路由即可
    // 不走钩子 onComplete && onComplete()
    // 走钩子
    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) // 更新current后 更新_route属性
  }
}




hash路由为主,创建hash路由实例

// history/hash.js
import History from './base'
如果是`hash`路由,打开网站如果没有`hash`默认应该添加`#/`
function ensureSlash () {

  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}
// hash路由
function getHash () {
  return window.location.hash.slice(1)
}
export default class HashHistory extends History {
  constructor (router) {
    super(router)
    ensureSlash() // 确保有hash
  }
push(url){
    window.location.hash = url
}
  getCurrentLocation () {
    return getHash()
  }
  setupListener () {
    window.addEventListener('hashchange', () => {
      // 根据当前hash值 过度到对应路径
      // console.log(getHash())
      this.transitionTo(getHash())
    })
  }
}

三、编写Router-Link及Router-View组件

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-view
            if (parent.$vnode && parent.$vnode.data.routerView){
                depth++;
            }
            parent = parent.$parent;
        }
        let record = route.matched[depth];
        if(!record){
            return h();
        }
        return h(record.component, data);
    }
}

2、 router-link组件

export default {
    props:{
        to:{
            type:String,
            required:true
        },
        tag:{
            type:Stringdefault:'a'
        }
    },
    render(h){
        let handler = ()=>{
            this.$router.push(this.to);
        }
        return <a onClick={handler}>{this.$slots.default}</a>
    }
}