基于vue2手写一个简易的Vue-router

587 阅读5分钟

前言

  • vue-router官方使用说明文档
  • vue-router源码地址
  • 本文主要讲vue-router二种获取路径模式historyhash
  • 路由守卫的实现原理(这里主要以beforeEach为例)
  • router-linkrouter-view实现原理
  • 大致实现的思想
  • 将用户填写的路由表 变成扁平化的映射表
  • 在根组件初始化响应劫持, 当前路径的映射表(用的是Vue.util.defineReactive内部响应式API)
  • 监听路径变化, 在router-view去渲染当前路径的所有组件

项目的目录结构

* 基于vue2项目开发的vue-router
├── public
│   └── index.html
├── src
│   ├── router
│   │   └── index.js
│   ├── views
│   │   ├── About.vue
│   │   └── Home.vue
│   ├── vue-router
│   │   ├── components
│   │   │   ├── link.js
│   │   │   └── view.js
│   │   └── history
│   │   │   ├── base.js
│   │   │   ├── hash.js
│   │   │   └── html5.js
│   │   ├── create-matcher.js
│   │   ├── create-router-map.js
│   │   ├── index.js
│   │   └── install.js
├── App.vue
└── main.js

示例

src/router/index.js

import Vue from 'vue'
import VueRouter from '@/vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    children:[
      {
        path:'a',
        component: {
          render: (h) => <h1>about a page</h1>
        }
      },
      {
        path:'b',
        component: {
          render: (h) => <h1>about b page</h1>
        }
      }
    ]
  }
]

/**
 * @description hash    丑    兼容性好
 * @description history 好看  但是需要服务端支持 在开发环境内部提供了historyFallback插件 所以不会出现404
 */
const router = new VueRouter({
  mode:'history',
  routes
})

router.beforeEach((to, from, next)=>{
  console.log('beforeEach111--->', JSON.stringify(to.path), JSON.stringify(from.path))

  // 异步一秒之后 在执行下一个钩子函数
  setTimeout(() => { next() }, 1000)
})

router.beforeEach((to, from, next) => {
  console.log('beforeEach222--->', JSON.stringify(to.path), JSON.stringify(from.path))

  next()
})

export default router

.vue文件

src/App.vue
src/views/About.vue
src/views/Home.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">首页</router-link>
      -----
      <router-link to="/about">关于页面</router-link>
    </div>
    <hr>
    <!-- 匹配路径后对应的组件会显示到router-view中 -->
    <router-view />
  </div>
</template>

<style>
  #app #nav {
    color:blue;
    font-size: 24px;
  }
</style>
<template>
  <div class="about">
    <h1>This is an about page</h1>


    <router-link to="/about/a">about - a</router-link>  | 
    <router-link to="/about/b">about - b</router-link>
    <router-view></router-view>

  </div>
</template>

<template>
  <div class="home">
   <h1>This is a home page</h1>
  </div>
</template>


正题

install和main文件

src/main.js src/vue-router/install.js 安装router和注入store实例

let vm = new Vue({
  name: 'root',
  router, // 注入了router实例
  render: h => h(App)
}).$mount('#app')

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

export let Vue

/**
 * @description 给每个组件安装router, 前提是根组件有router
 * @description 在Vue实例链上挂载劫持的$router, $route
 * @description 给Vue全局注册router-view, router-link组件
 */
export default function install(_Vue) {
    Vue = _Vue

    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                // 只执行一次
                // 根组件
                this._router = this.$options.router
                this._routerRoot = this

                // 初始化路由逻辑
                this._router.init(this)

                // 将路径对应的映射表 变成响应式
                Vue.util.defineReactive(this, '_route', this._router.history.current)
            } else {
                // 子组件
                this._routerRoot = this.$parent && this.$parent._routerRoot
            }
        },
    })
    
    /**
     * @description VueRouter实例
     */
    Object.defineProperty(Vue.prototype, '$router', {
        get() {
            return this._routerRoot._router
        }
    })

    /**
     * @description 当前路径对应的映射表
     */
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })

    Vue.component('router-link', RouterLink)
    Vue.component('router-view', RouterView)
}

创建VueRouter实例

src/vue-router/index.js

import install, { Vue } from './install'
import { createMatcher } from './create-matcher'
import Hash from './history/hash'
import HTML5History from './history/html5'

class VueRouter {
    constructor(options = {}) {
        const routes = options.routes

        this.mode = options.mode || 'hash'

        // 路由钩子函数数组
        this.beforeHooks = []

        /** 将路由数据 做扁平化处理 创建映射表 */
        /** 该方法 后续也可动态加载路由 addRoutes */
        this.matcher = createMatcher(options.routes || [])

        /** mode模式 */
        switch(this.mode) {
            case 'hash':
                this.history = new Hash(this)
                break
            case 'history':
                this.history = new HTML5History(this)
                break

        }
                
    }

    /**
     * @descript 根据路径 查找映射表
     */
    match(location) {
        return this.matcher.match(location)
    }

    /**
     * @descript 改变路径 $router.push
     */
    push(location) {
        // history浏览器可以监听到, 代码操作无法监听到
        this.history.transitionTo(location, () => {
            this.history.pushState(location)
        })
    }

    /**
     * @descript 初始化操作
     */
    init(app) {
        const history = this.history

        // 监听路径 叠片
        const setUpListener = () => {
            history.setUpListener()
        }

        // 路径监听
        history.transitionTo(
            history.getCurrentLocation(),
            setUpListener
        )

        // 改变响应式_route方法传给history
        // 主要是找跟根组件_route 将当前路径的映射表赋值给_route
        history.listen((route) => {
            app._route = route
        })

    }

    /**
     * @description 路由前置钩子
     */
    beforeEach(fn){
        this.beforeHooks.push(fn);
    }

}

VueRouter.install = install


export default VueRouter

扁平化路由表

src/vue-router/create-matcher.js src/vue-router/create-route-map.js

// create-matcher 文件
import { createRouteMap } from './create-route-map'

/**
 * @description 路由表 -> 映射表
 * @returns {function} match        通过路径查找对应的记录
 * @returns {function} addRoutes    动态添加路由方法
 */
export function createMatcher(routes) {
    /** 创建映射表 */
    const { pathMap } = createRouteMap(routes)
    
    /** 查寻匹配 pathMap对应的记录 */
    /** 参数是路径 如'/about/a' */
    function match(path) {
        return pathMap[path]
    }

    /** 添加新的路由表 */
    /** 动态路由 如做权限方面 动态添加路由 */
    function addRoutes(newRoutes) {
        return createRouteMap(newRoutes, pathMap)
    }

    return {
        match,
        addRoutes,
    }
    
}
// create-route-map 文件
/**
 * @description 映射表
 * @description 最终的样子
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * {
 *   /        : { path: '/',         componet: {...}, props: {}, parent: {} },
 *   /about   : { path: '/about',    componet: {...}, props: {}, parent: {} },
 *   /about/a : { path: '/about/a',  componet: {...}, props: {}, parent: {path: 'about'...} },
 *   /about/b : { path: '/about/b',  componet: {...}, props: {}, parent: {path: 'about'...} },
 * }
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 */

export function createRouteMap(routes, oldPathMap) {
    // 创建的格式 最终存储的变量 将层级扁平化处理
    let pathMap = oldPathMap || {}

    routes.forEach(route => {
        addRouteRecord(route, pathMap)
    })

    return { pathMap }
}

/**
 * @description 核心方法
 * @description 递归 创建映射表
 * @param {object} route    用户每个路由组件对应的信息
 * @param {object} pathMap  存储的地方
 * @param {object} parent   是否存在父级
 */
function addRouteRecord(route, pathMap, parent) {
    let path = parent ? `${parent.path}/${route.path}` : route.path

    let record = {
        path,
        component: route.component,
        props: route.props || {},
        parent
    }

    pathMap[path] = record

    route.children && route.children.forEach(childRoute => {
        addRouteRecord(childRoute, pathMap, record)
    })


}

hash和hsitory路径处理方式

src/vue-router/history/base.js
src/vue-router/history/html5.js
src/vue-router/history/hash.js
通过路径的不同去匹配当前路径的映射表
在将映射表赋予_route让其响应式发生变化从而更新组件(就是router-view获取的$route)
有些方法是一致 放在base.js文件
主要的方法是base.js里的transitionTo方法

// base文件
/**
 * @description 根据当前路径 查找所有的父组件
 * @param {object} record 当前路径对应的映射表 
 * @param {object} location 当前路径
 */
function createRoute(record, location) {
    const matched = []
    if (record) {
        while(record) {
            matched.unshift(record)
            record = record.parent
        }
    }

    return {
        ...location,
        matched
    }
    
}

/**
 * @description 路由钩子队列逻辑 这里以前置钩子为例
 * @description 思想就是将前置钩子都放在一个钩子里 
 * @description 最后在依次调用 如: [ ...beforeEach, ...beforeEnter, ...beforeRouteEnter]
 * @param {array}     queue     存放钩子的集合
 * @param {function}  iterator  执行钩子函数 并next() 时看集合中是否还有钩子 有钩子继续执行
 * @param {function}  cb        依次执行完钩子函数后 更改响应式_route(就是获取的this.$route) 更新组件
 */

function runQueue(queue, iterator, cb) {
    const step = index => {
        if (index >= queue.length) {
            cb()

        } else {
            if (queue[index]) {
                // 第一个参数 执行的钩子
                // 第二个参数 next
                iterator(queue[index], () => { step(index + 1) })
            } else {
                step(index + 1)
            }
        }
    }

    step(0)
}

export default class History {
    constructor(router) {
        this.router = router

        // 保存路径的变化
        // 默认{path: '/', matched: []}
        this.current = createRoute(null, {path: '/'})
    }

    /**
     * @description 改变_route响应式值方法
     */
    listen(cb) {
        this.cb = cb
    }

    /**
     * @description 根据当前的路径 去获取对应的映射表 
     * @description 并改变_route响应式的值
     */
    transitionTo(path, onComplete) {
        let record = this.router.match(path)
        let route = createRoute(record, { path })

        // 判断当前路由是否一致 一致返回
        // 保证跳转的路径 和 当前路径一致
        // 匹配的记录个数 应该和 当前的匹配个数一致 如第一次打开路径都是'/' 但是匹配的映射表不-样 [] 和 [{...}]
        if (path ===  this.current.path && route.matched.length === this.current.matched.length) {
            return
        }

        // 执行钩子函数的方法 方便迭代
        const iterator = (hook, next) => {
            hook(route, this.current, next)
        }
        

        // 全部的前置钩子函数
        let queue = this.router.beforeHooks
       
        runQueue(queue,iterator,() => {
            this.updateRoute(route)

            // 开启路径监听 执行一次
            onComplete && onComplete() // 默认第一次cb是 监听 hashchange || popstate

            // TODO... 后置的钩子可以放在这里 还是调用runQueue
        })

    }
    
    /**
     * @description 更新组件 为 router-view
     */
    updateRoute(route) {
        // 更改老的映射表
        this.current = route

        // change _route
        this.cb && this.cb(route)
    }
    
}
// hash 文件
import History from './base'

/**
 * @description 确保路径是hash
 */
function ensureHash() {
    if(!window.location.hash) {
        window.location.hash = '/'
    }
}

/**
 * @description 获取hash
 */
function getHash() {
    return window.location.hash.slice(1)
}

export default class Hash extends History {
    constructor(router) {
        super(router)

        ensureHash()
    }

    /**
     * @description 得到当前的hash
     */
    getCurrentLocation() {
        return getHash()
    }

    /**
     * @description 监听hash路径变化
     */
    setUpListener() {
        window.addEventListener('hashchange', () => {
            // hash 变化 去渲染组件
            this.transitionTo(getHash())
        })
    }

    /**
     * @description 改变路径变化 前进
     */
    pushState(location) {
        window.location.hash = location
    }

    
}
// html5文件
import History from './base'

export default class HTML5History extends History {
    constructor(router) {
        super(router)
    }

    /**
     * @description 获取路径
     */
    getCurrentLocation() {
        return window.location.pathname
    }

    /**
     * @description 监听html5路径变化
     */
    setUpListener() {
        window.addEventListener('popstate', () => {
            this.transitionTo(this.getCurrentLocation())
        })
    }

    /**
     * @description 改变路径 前进
     */
    pushState(location) {
        history.pushState({}, null, location)
    }


}

router-linkrouter-view实现

src/vue-router/components/link.js
src/vue-router/components/view.js

/**
 * @description router-link 组件
 * @description 函数式组件写法
 */
export default {
    functional: true,
    props: {
        to: {
            type: String,
            required: true
        }
    },

    render(h, { props, slots, parent }) {
        const click = () => {
            parent.$router.push(props.to)
        }

        // jsx写法
        return <a onClick = { click }>{ slots().default }</a>
    },
}
/**
 * @description router-view 组件
 * @description 函数式组件
 */
export default {
    functional: true,

    render(h, { parent,data }) {
        // 获取当前路径对应的映射表(current)
        let route = parent.$route

        let depth = 0
        while (parent) {
            if(parent.$vnode && parent.$vnode.data.routerView ){
                depth++
            }
            parent = parent.$parent
        }

        // 有两个router-view  [/about  /about/a]
        // parent.$vnode.data.routerView 没有先渲染/about -> [/about: {routerView: true}, /about/a]
        // parent.$vnode.data.routerView 有再渲染/about/a -> [/about: {routerView: true}, /about/a: {routerView: true}]
        let record = route.matched[depth]

        if(!record){
            return h()
        }

        data.routerView = true

        // $vnode是描述组件的
        // _vnode是描述组件的标签的
        // <router-view routeView=true></router-view>
        return h(record.component, data)
    },
}