最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 基于vue2手写一个简易的Vue-router

    正文概述 掘金(小桂summer)   2021-01-26   363

    前言

    • 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文件

    <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>
    
    
    

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

    正题

    install和main文件

    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实例

    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
    

    扁平化路由表

    // 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路径处理方式

    // 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实现

    /**
     * @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)
        },
    }
    


    起源地下载网 » 基于vue2手写一个简易的Vue-router

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元