Vue-Router原理实现

104 阅读3分钟

1.Vue.js基础回顾


1.1Vue基础结构

<div id="app">
  {{ message }}
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

//el选项
<script>
    new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    })
</script>

//$mount选项
<script>
    new Vue({
      data: {
        message: 'Hello Vue!'
      },
      render(h){
          return h('div',[
              h('p',this.message)
          ])
      }
    }).$mount('#app')
</script>

  • render函数接受一个参数, 这个参数是h函数,h函数的作用是创建虚拟dom, render函数是把h函数创建的虚拟dom返回。
  • $mount方法的作用是把虚拟dom转换为真实dom,渲染到浏览器。
  • 使用el选项和render选项在运行时有什么区别?

1.2Vue的生命周期

生命周期发生的事情
new Vue()初始化事件&生命周期
beforeCreate()初始化注入&校验,会将props, data, methods等成员注入到vue实例上。
created()可以访问到props,data,methods等成员,核心事情就是把模版编译成render函数
beforeMount()无法获取新元素的内容,将新的结构渲染到页面
mounted()可以访问到新的dom结构的内容
beforeUpdate()当data被修改时, 新旧虚拟dom的对比,然后把差异渲染到浏览器中。访问不到新的内容
updated()可以访问修改后的新的内容
beforeDestroy()解除绑定销毁子组建以及事件监听器。
destroyed()销毁完毕

Vue始终推荐提前编译模版,提高性能,不需要在运行期间编译模版。

1.3Vue语法和概念

  • 差值表达式:通过{{}}将data中的成员显示在模版中的任何位置。如果内容中有html字符串, 差值表达式会将内容解析为普通文本, html内容被转译。如果想作为html输出, 可以使用v-html指令。
  • 指令:内置14个指令,可以创建自定义指令。
  • 计算属性和侦听器:计算属性的结果被缓存,从缓存中获取结果,提高性能。监听数据的变化作复杂操作,可使用侦听器。
  • class和style绑定:分别可以绑定数组或者对象
  • 条件渲染和列表渲染: v-if/v-show,v-for,用key跟踪每一个节点的身份,让每一项尽可能重用
  • 表单输入绑定: v-model双向绑定
  • 组件: 无限次被重用
  • 插槽: 在自定义组件时挖坑, 在使用组件时填坑, 让组件更灵活
  • 插件: vue-router, vueX
  • 混入mixin
  • 深入响应式原理
  • 不同构件版本的vue

2. Vue Router实现原理


2.1 Vue Router 基础回顾

路由使用步骤及动态路由

  • 路由组件:Blog.vue, Index.vue, Photo.vue
  • 路由模块:index.js文件夹
import Vue from 'vue'
import Vue Router from 'vue-router'
import Index from '../views/index.vue'
// step1: 注册路由插件
// Vue.use用来注册插件,他会调用传入对象的install方法
Vue.use(VueRouter)

//路由规则
const routes = [
    {
        path:'/',
        name:'Index',
        component:Index
    },{
        path:'/blog',
        name:'Blog',
        component: () => import(/* webpackChunkName: "blog" */ '../views/index.vue')
    },{
        path:'/photo',
        name:'Photo',
        component:() => import(/* webpackChunkName: "photo" */ '../views/photo.vue')
    },{
        //动态路由, 在组件中有2中方式获取id,
        // 1, $route.params.id,强依赖路由。
        // 2, 当props的值设置为true时,会把url的参数传递给组件,只需要通过props接收就OK了, 同父子组件传值。
        path:'/detail/:id', 
        name:'Detail',
        props:true,
        component:() => import(/* webpackChunkName: "detail" */ '../views/detail.vue') //路由懒加载, 访问路由地址时才会加载组件,提供性能。
    },{
        path:'*',
        name:'404',
        component:() => import('../views/404.vue')
    }
]

// step2: 创建路由对象
const router = new VueRouter({
    routes
})


export default router

  • 文件入口:main.js
import Vue from 'vue'
import router from './router'

new Vue({
    //step3: 生成vue实例时,注册router对象,
    router,
    render:h=>h(App)
}).$mount('#app')


打印vue实例对象:
//存贮路由规则
$route{
    fullPath:'/blog',
    hash:'',
    matched:[{}],
    meta:{},
    name:'Blog',
    params:{},
    path:'/blog',
    query:{}
}
//VueRouter实例, 路由对象
$router
相关属性:mode:'hash', currentRoute:[]
相关方法:go(), back(), push(), replace(), forward(), addRoutes(), beforeEach(), afterEach()等等

  • App.vue文件
<template>
<div id="app">
    //创建链接
    <div id="nav">
        <router-link to="/">Index</router-link>
        <router-link to="/blog">Blog</router-link>
        <router-link to="/photo">Photo</router-link>
    </div>
    //step4: 创建路由组建的占位,组件加载进来会替换掉router-view.
    <router-view></router-view>
</div>
</template>

嵌套路由

  • 当首页和详情页公用一个头部和尾部的时候 layout.vue
<template>
<div>
    <div>header</div>
    <router-view></router-view>
    <div>footer</div>
</div>
</template>

router>index.js

cosnt route = [
    //嵌套路由
    {
        path:'/',
        component:Layout,
        children:[
            {
                name:Index,
                path:'',
                component:Index
            },{
                name:Detail,
                path:'detail/:id',
                props:true,
                component:() => import(/* webpackChunkName: "detail" */ '../views/detail.vue')
            }
        ]
    }
]

编程式导航

//参数可以是字符串和对象。
this.$router.push('/detail')
this.$router.push({name:'Detail',params:{id:'123'}})

this.$router.replace()
this.$router.go(-2)
this.$router.back()

2.2 hash和History模式

表现形式&原理的区别

形式:
hash模式
    https://music.163.com/#/playList?id=3102961863
history模式
    https://music.163.com/playList/id=3102961863

原理:
hash模式是基于锚点, 以及onhashchange事件
-- 通过 location.hash = 'foo' 这样的语法来改变,路径就会由 baidu.com 变更为 baidu.com/#foo。
-- 通过 window.addEventListener('hashchange') 这个事件,就可以监听到 hash 值的变化。
history是基于html5中的historyAPI
history.pushState():路径变化,向服务器发送请求
history.replaceState(): 不会发生服务器请求

history模式的使用, 需要服务器的支持

  • 为什么刷新后会 404:本质上是因为刷新以后是带着 baidu.com/foo 这个页面去请求服务端资源的,但是服务端并没有对这个路径进行任何的映射处理,当然会返回 404
  • 解决方法:处理方式是让服务端对于"不认识"的页面,返回 index.html,这样这个包含了前端路由相关js代码的首页,就会加载你的前端路由配置表,并且此时虽然服务端给你的文件是首页文件,但是你的 url 上是 baidu.com/foo,前端路由就会加载 /foo 这个路径相对应的视图,完美的解决了 404 问题.

2.3模拟实现自己的Vue Router

  • Vue.use(VueRouter) // 注册插件,调用VueRouter的install静态方法
  • 创建路由对象,传入一个对象, 对象里面包含路由规则
const router = new VueRouter({
    routes:[
        {path:'',name:'Index',component:homePage}
    ]
})
  • 创建vue实例, 注册router对象
new Vue({
    router,
    render:h => h(App)
}).$mount('#app')
  • VueRouter类图

截屏2021-03-30 下午3.02.19.png

2.3.1 install方法

let _Vue = null
export defualt class VueRouter{
    static install(vue){
    
        //1. 判断插件是否已经安装
        if(VueRouter.install.installed){
            return
        }
        VueRouter.install.installed = true //表示插件被安装了
        
        //2. 把Vue构造函数记录到全局变量
        _Vue = vue
        
        //3. 把创建Vue实例的时候传入的router对象注入到Vue实例上
        // 混入, 给所有的vue实例设置一个选项, 所有的组件会执行我们混入的beforecreat()
        _Vue.mixin({
            beforeCreate(){
                // 直接拿到Vue实例, 
                // 组件不执行(不然会执行很多次), 实例的时候执行一次即可
                // 组件时不会传入options, 实例的时候会传入options
                if(this.$options.router){
                   _Vue.prototype.$router = this.$options.router
                   this.$options.router.init()
                }
            }
        })
        
    }
}

2.3.2构造函数

constructor(options){
    this.$options = options
    this.routeMap = {} //键:路由地址, 值:路由地址对应的路由组件, 根据路由地址找到路由组建,渲染到页面上
    //data里面是响应式的路由地址,这一点特别*重要*
    this.data = _Vue.observable({
        current:'/'
    })
}

2.3.3 init方法

init(){
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
}

2.3.4 createRouteMap方法

  • 作用: 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
    createRouteMap(){
        this.$options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        })
    }

2.3.5 initComponents方法

initComponents(Vue){
    Vue.component('router-link', {
        props:{
            to:String
        },
        // template:'<a :href="to"><slot></slot></a>'
        render(h){
            return h('a', {
                attrs:{
                    href: this.to
                },
                on:{
                    click:this.clickHandler
                }
            },[this.$slots.default])
        },
        methods:{
            clickHandler(e){
                history.pushState({},'网页标题',this.to)
                this.$router.data.current = this.to
                e.preventDefault()
            }
        }
    })
    const self = this
    Vue.component('router-view', {
        render (h) {
            // 拿到当前路由地址,注意this的指向
            const component = self.routeMap[self.data.current]
            //url变化, 浏览器会刷新, 不希望向服务器发送请求, history的pushState(),仅仅改变地址栏的地址, 不会发送请求
            return h(component)
        }
    })
}
template:'<a :href="to"><slot></slot></a>'
此时运行会报错,原因如下:
  • Vue的构建版本:(默认使用运行时版本)
  1. 运行时版:不支持template模版,需要打包的时候提前编译
  2. 完整版:包含运行时和编译器,体积比运行时版本大10k左右,程序运行时把模版转换成render函数,性能不如运行时版本。 如何切换到完整版?
// vue.config.js
module.exports = {
    runtimeCompiler:true
}
//重启终端

2.3.6 initEvent方法

  • 点击浏览器的前进后退时, 没有重新加载路由对应的组件
  • popstate(),历史发生变化时被出发
initEvent(){
    window.addEventListener('popstate', () => {
        this.data.current = window.location.pathname
    })
}

3.模拟vue-router的原理到此就结束了,欢迎交流~