vue-router 原理及手写实现

148 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

vue-router 原理

原理其实很简单,主要是vue的响应性和路由监听事件。

  1. 监听postStatehashChange

history路由监听即postState事件 hash路由即hashChange事件
在路由监听回调里面会给route的current赋值

this.route.current = window.location.pathname
  1. 观测routecurrent 变化
  2. 更新route-view

因为route-view用到了route参数,响应式特性会update用到的dom,在route-view中会根据route获取对应的component对象也就是组件本身,替换route-view显示

postStatehashChange什么时候会监听到?

postStatehashChange 在地址发生变化时都会触发

值得注意的 在不请求情况下点击相同的route-link postState会触发,hashChange不会。 hashChange只会在#后面的地址变化触发 另外调用history.pushState()history.replaceState()不会触发popstate事件。

所以,a标签href 如果是#号事件会触发,如果不带#号,页面会请求,浏览器刷新的。 所以我们要重写a标签的click事件,以防止请求事件触发 #号实际上就是锚点,一般用在同页面跳到指定标题,带#的改变地址,不请求页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
<div>
    <a href="ceshi" onClick="tagAClick(event)">测试1</a>
    <a href="#ddddd">测试2</a>
    <a href="#erwe">测试3</a>
    <button onclick="jumpPage()">按钮跳转</button>
</div>

<script>
    window.addEventListener('hashchange', () => {
        console.log('hashchange监听 \n' + 'pathname:' + window.location.pathname + ' \nhash: ' + window.location.hash)
    })

    window.addEventListener('popstate', () => {
        console.log('popstate被监听到了 \n' + 'pathname:' + window.location.pathname + ' \nhash: ' + window.location.hash)

    })

    function jumpPage(e) {
        history.pushState({}, '', 'button')
        console.log('button点击跳转 \n' + 'pathname:' + window.location.pathname + ' \nhash: ' + window.location.hash)
    }

    function tagAClick(e) {
        history.pushState({}, '', 'ceshi')
        console.log('a标签跳转 \n' + 'pathname:' + window.location.pathname + ' \nhash: ' + window.location.hash)
        e.preventDefault();
    }

</script>
</body>
</html>

在线运行上述代码

点击运行结果为 点击 测试1

"a标签跳转 
pathname:/cpe/boomboom/ceshi 
hash: "

点击 测试2

"popstate被监听到了 
pathname:/cpe/boomboom/ceshi 
hash: #ddddd"
"hashchange监听 
pathname:/cpe/boomboom/ceshi 
hash: #ddddd"

点击 测试3

"popstate被监听到了 
pathname:/cpe/boomboom/ceshi 
hash: #erwe"
"hashchange监听 
pathname:/cpe/boomboom/ceshi 
hash: #erwe"

点击 button

"button点击跳转 
pathname:/cpe/boomboom/button 
hash: "

手写router-view

router-使用

完整的使用大致分为以下五个步骤

// 1. router.use  也就是调用intall方法
Vue.use(VueRouter)
// 2. 配置路由对象
const routes = [
    {
        path: '/',
        name: 'Index',
        component: Index
    },
    {
        path: '/detail',
        name: 'detail',
        component: Detail
    },
    {
        path: '/about',
        name: 'about',
        component: About
    }
]
// 3.导出router对象,并将路由配置穿进构造方法里面
export default new VueRouter({routers})
// 4. 将router对象传进Vue实例中
new Vue({
    router,
    render: h => h(App)
}).$mount('#app')
// 5. 配置route-link跳转, 和component占位元素 route-view
<template>
    <div class="app">
        <div class="nav">
            <router-link to="/">Index</router-link> |
            <router-link to="/about">About</router-link> |
            <router-link to="/detail">Detail</router-link>
        </div>
        <router-view />
    </div>
</template>

根据使用来写vue-router

install方法

install方法对应的是 Vue.use(VueRouter)类名直接调用所以是静态方法。
install 主要作用就是让所有页面都有router对象,同时拥有route对象

let _Vue 
class VueRouter {
    static install(Vue) {
        _Vue = Vue
        Vue.mixin({
            // 在mixin情况下会先调用mixin里面的生命周期方法(beforeCreate),后调用组件自身的生命周期方法(beforeCreate)
            beforeCreate() {
                if (this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                    // this.$router = this.$options.router
                }
            }
        })
    }
    
}

export default VueRouter

构造方法

构造方法主要作用初始变量,观察route属性等

  • 1.保存options属性,保存mode属性
  • 2.生成routeMap{ url:component } )对象
  • 3.添加路由监听。
  • 4.定义route-view route-link组件
hash模式的实现

// VueRouter.js文件

let _Vue 
class VueRouter {
    static install(Vue) {
        _Vue = Vue
        Vue.mixin({
            // 在mixin情况下会先调用mixin里面的生命周期方法(beforeCreate),后调用组件自身的生命周期方法(beforeCreate)
            beforeCreate() {
                if (this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                    // this.$router = this.$options.router
                }
            }
        })
    }
    
    constructor(options){
        // 保存options属性
        this.options = options
        // 如果不传mode,默认为hash
        this.mode = options.mode || 'hash'
        // 生命routeMap对象
        this.routeMap = {}
        // 监听route.current对象
        this.route = _Vue.observable({
            current: '/'
        })
        this.init()
    }
    
    init() {
        //生成routeMap
        this.initRouteMap()
        //添加路由监听。
        this.initEvent()
        // 定义组件route-view 和route-link
        this.initComponent(_Vue)
    }
    
    /**
     * 生成routeMap
     * */
    initRouteMap() {
        this.options.routes.forEach(item => {
            /**
             *   {
             *        path: '/about',
             *        name: 'about',
             *        component: About
             *    }
             * */
            
            this.routeMap[item.path] = item.component
        })
    }
    
    /**
     * 添加路由监听事件,在监听事件里面改变route.current的值
     * */
    initEvent() {
        window.addEventListener('hashchange', () => {
            console.log('hashchange被监听到了===========')
            // hash为 #about 所以通过 slice(1) 得到 /about
            that.route.current = window.location.hash.slice(1)
        })
    }
    /**
     * 定义route-view 和route-link组件
     *  
     *  route-link实际上就是<a href='path'></a>
     *  
     *  routeView 实际上就是根据不同的path展示不同的component组件
     *  
     *  扩展:h函数
     *  h函数实际上就是createElement()方法
     *
     *  //@param1  tags(标签名)、组件名称等
     *  //@param2  tagPropsObject 标签对应的属性数据
     *  //@param3  childNode 子级虚拟节点,也是需要createElement构建
     *  createElement(tags, tagPropsObject, childNode) 函数接受三个参数,分别是:
       eg: h('div', {class: 'title'}, '我是标题') === <div class='title'>我是标题</div>
     * 
     * 
     * */
    initComponent(Vue) {
        const that = this
        Vue.component('route-link', {
            props: {
                to: String
            },
            render(h) {
                return h('a',{
                    domprops:{
                        to: '#' + this.to
                    }
                }, [this.$slots.default])
            }
            
        })
        Vue.component('route-view', {
            render(h){
                // routeMap根据路径获取当前component
                return h(that.routeMap(that.route.current))
            }
        })
    }
    
}

export default VueRouter

history的实现

由于a标签跳转的时候会请求页面,浏览器刷新,为了防止请求页面,同时实现#号的去除,我们需要重写a标签的click事件,并且阻止传播

实际开发中为防止页面刷新请求我们需要后台配合,将所有请求重定向到根页面走我们根页面判断逻辑以实现route-view替换当前component的逻辑

// VueRouter.js文件

...省略代码...
    initEvent() {
        //-------------------------新增代码----------------
        /**
         * 注意添加history路由变化popstate监听的目的是监听浏览器后退,前进事件。因为pushState不会          * 进此回调方法
         */
        window.addEventListener('popstate', () => {
          that.route.current = window.location.pathname
        })
        //-------------------------
        
        window.addEventListener('hashchange', () => {
            console.log('hashchange被监听到了===========')
            // hash为 #about 所以通过 slice(1) 得到 /about
            that.route.current = window.location.hash.slice(1)
        })
    }
    
  
    // *** history模式下面防止浏览器请求页面,我们需要重新 a 标签的click事件。同时阻止事件传播。然后手动pushState到新页面 ***
    initComponent(Vue){
        Vue.component("router-link",{
            props:{
                to:String
            },
            render(h){
                return h("a",{
                    attrs:{
                        href:this.to
                    },
                    on:{
                        click:this.clickhander
                    }
                },[this.$slots.default])
            },
            methods:{
                clickhander(e){
                    history.pushState({},"",this.to)
                    this.$router.data.current=this.to
                    e.preventDefault()
                }
            }
            // template:"<a :href='to'><slot></slot><>"
        })
      ...省略代码...
    }

...省略代码...