Vue-Router实现原理

379 阅读6分钟

Vue.js框架基础回顾

  • 前端路由是实现单页面应用的基础

前端:你要懂的单页面应用和多页面应用

单页面应用:只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站

Vue基础结构

e.g.1:基础vue实例,传入eldata选项,vue内部会把data数据填充到el指向的模板上,并把模板渲染到浏览器。

e.g.2:使用了render选项和$mount方法,render方法接收一个参数h(),h函数创建了虚拟DOM,而$mount把虚拟DOM转换为真实DOM,渲染到浏览器。

两者运行有区别

Vue的生命周期

Vue.js语法和概念

语法

  • 差值表达式:{{data}}中成员显示在模板的任何位置,内容中有html字符串,会把内容解析为普通文本,html的内容会被转义,可以使用v-html指令
  • 指令:14个
  • 计算属性和侦听器:计算属性会被缓存
  • Class和Style绑定:Class样式可以重用
  • 条件渲染/列表渲染
  • 表单输入绑定(双向绑定)

概念

  • 组件:可复用的vue实例,一个组件封装了html、css、JavaScript,可以实现页面上的一个功能区域,可以无限次地被重用
  • 插槽:在自定义组件中挖坑,在使用组件时填坑,让组件更灵活
  • 插件
  • 混入mixin:相同的选项合并,代码可以进行重用
  • 深入响应式原理
  • 不同构件版本的Vue:(不)带编译器

报告:

  1. this.$router

  • this.$route是当前的路由信息,没有push、back等方法

  • this.$router可以通过currentRoute获取到当前路由对象中信息,e.g:path、parm as、query等

  1. router-link组件的exact-active-class用来设置地址精确匹配的元素的样式

Vue Router实现原理

基础回顾

Ⅰ、创建路由

  1. 注册路由插件,制定路由规则

  2. 创建router对象,导出

  3. 注册router对象。创建Vue实例的时候,传入router对象,Vue实例中被注入两个属性:

    $route:路由规则

    $router:路由相关的方法

  4. 创建路由组件的占位<router-view/>

  5. 创建链接<router-link/>

Ⅱ、动态路由传参

  • 路由懒加载
{
    path:'xxx/:id',
	//props:true,
    //开启props,会把URL中的参数传递给组件
    //在组件中通过props来接收URL参数
    name:'xxx',
    //命名式导航
	commponent:() => import('../xxx/xxx.vue')
}
  1. 通过当前路由规则,获取数据。强依赖路由。

    {{$route.params.id}}
    
  2. 父子组件传值的方式:路由规则中开启props传参。不依赖路由。

    {{id}}
    
    export default {
        props: ['id']
    }
    

Ⅲ、嵌套路由

layout.vue嵌套到index.vue/ details.vue中输出,使之拥有相同的头和尾

//index.js
//配置路由

import Layout from '@/commponents/Layout.vue'

const routes = [
    //嵌套路由:先加载Layout组件,再加载Index/Detail组件
    {
        path:'/',
        component:Layout,
        children: [
            //首页
            {
            	name: 'index',
            	path: '',//相对路径'/'
            	component: Index
            },
            //详情页
            {
                name: 'detail',
                path: 'detail/:id',
                props: true,
                component: () => import('@/views/Detail.vue')//'/Detail.vue'
            }
        ]
    }
]

Ⅳ、编程式导航

this.$router.push('/')
this.$router.push({name:'Home'})
this.$router.replace('/login')
//不会记录当前历史
this.$router.go(-2)

Hash模式和History模式

  • 都是客户端路由的实现方式

Ⅰ、区别

表现形式:

  • Hash:http://xxx.com/#/xxx?id=11很丑
  • History:http://xxx.com/xxx/222

原理:

  • Hash:基于锚点,以及onhashchange事件

    调用push方法会先判断当前浏览器是否支持window.pushState,再调用pushState改变地址栏;

    否则通过window.loaction改变地址栏

  • History:基于HTML5中的HistoryAPI

    • history.pushState()IE10以后才支持

    • history.replaceState()

    此模式下调用router.push(url)方法时,push方法内部会直接调用window.history.pushState,把url设置到浏览器的地址栏

    window.history.pushState不能触发popstate事件,当历史状态被激活的时候才会触发popstate

两者都是客户端修改URL,性能相差不大

Ⅱ、History模式

  • History模式是基于浏览器的History API实现的
  • 需要服务器支持
  • 单页应用中,服务端不存在http://www.xx.com/login会返回找不到页面
  • 在服务端应该除了静态资源外都返回单页面应用的index.html
//index.js
const routes = [
    {
        path:'*',
        name:'404',
        compontent: () => import('../xxx/404.vue')
    }
]

const router = new VueRouter({
    mode: 'history',
    routes
})

Ⅲ、node.js服务器配置

node配置的服务器app.js

//app.js
const path = require('path')//可以合并两个路径
//导入处理hitory模式的模块
const history = require('connect-history-fallback')
//导入express
const express = require('express')

const app = express()
//注册处理history模式的中间件
//app.use(history())
//处理静态资源的中间件,网站根目录../web
app.use(express.static(path.join(__dirname,'../web')))//处理静态资源

//开启服务器,端口是3000
app.listen(3000, () => {
    console.log("服务器开启,端口:3000")
})

中间件?

  • 服务端不存在http://www.xx.com/login会返回找不到页面,所以要在服务端配置支持
app.use(history())

node app.js

Ⅳ、nginx服务器配置

(不能有中文)

start nginx
nginx -s reload
nginx -s stop

实现自己的Vue Router

  • 前置知识:插件、混入、Vue.observable()、插槽、render函数、运行时和完整版的Vue

Vue.observable():可以使用Vue.observable()把任意对象转换成响应式对象,Vue.observable()返回的对象没有办法在模板中直接使用,但可以在h()函数中使用

Ⅰ、实现原理

Hash模式:

  1. URL中#后面的内容作为路径地址
  2. 监听hashchange事件
  3. 根据当前路由地址找到对应组件重新渲染

History模式:

  1. 通过history.pushState()方法改变地址栏
  2. 监听popstate事件
  3. 根据当前路由地址找到对应组件重新渲染

Ⅱ、分析

类图:

Ⅲ、代码实现

install

//./vuerouter/index.js
let _Vue = null

export default class VueRouter {
    static install() {
        if(VueRouter.install.installed){
            return 
        }
        VueRouter.install.installed = true//当前插件被安装了
        
        _Vue = Vue
        
        //_Vue.prototype.$router = this.$options.router
        //混入
        _Vue.mixin({
            beforeCreate(){
                //组件orvue实例
               if(this.$options.router) {
                  _Vue.prototype.$router = this.$options.router//只需要执行一次
                   this.$options.router.init()
               }
            }
        })
    }
}

/**
 * 1.判断当前插件是否重复被安装
 * 2.install静态方法接收了静态函数,将来要在vue的实例方法中使用这个函数,要把vue构造函数记录到全局变量中
 * 3.把创建vue实例时候传入的router对象注入到vue实例上
 */

构造函数

constructor (options) {
    this.options = options
    this.routeMap = {}
    this.data = _Vue.observable({
        current: '/'//存储当前路由地址
    })
    //三个所需要的属性
}

createRouteMap:把构造函数传过来的选项中的rules(路由规则)转换为键值对的形式存储到routerMap.

routerMap中存储的键的值就是路由中对应的组件。如果路由发生变化,很容易根据地址在routeMap中找到组件,并且渲染到视图上。

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

initComponents

  1. 实现router-link组件
//参数vue为了减少和外部的依赖
initComponents (Vue) {
    vue.component('router-link',{
        props: {
            to: String,
        },
        template: '<a :href = "to"><slot></slot></a>'
    })
}

init:包装initComponentscreateRouteMap方法让外部使用方便

init() {
	this.createRouteMap()
    this.initComponents(_Vue)//传入vue的构造函数
}
  • 完整版本的Vue

Vue的构建版本

运行时版:不支持template模板,需要打包的时候提前编译

完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数

完整版性能不如运行时版本

vuecli默认使用运行时版本

Vue-cli中方法,runtimeCompiler:是否使用包含运行时编译器的Vue构建版本。设置为true后可以在Vue组件中使用template选项

创建./vue.config.js

module.exports = {
    runtimeCompiler: true,
}
  • 运行版本的Vue

关注:render函数怎么写

因为运行版本不支持template选项,需要修改initComponents

initComponents(Vue){
    Vue.component('router-link', {
        props: {
            to: String
        }
    },
    //h创建虚拟DOM
    render (h) {
        return h('a', {
            attrs: {
                herf: this.to//history模式
            }//设置DOM属性
        }, [this.$slots.default])
        //获取默认插槽设置到a标签里
    }
    //template:...
    )
}
  1. 实现router-view组件,并且使路由转跳都在客户端操作,页面不刷新,但地址栏发生变化
initComponents (Vue) {
    Vue.component('router-link', {
        props: {
            to: String
        }
    },
    render (h) {
        return h('a', {
            attrs: {
                herf: this.to
            },
            on: {
                click: this.clickHandler
            },//给a标签注册一个跳转功能
        }, [this.$slots.default])
        methods:{
            clickHander (e) {
                //1.改变浏览器的地址栏
                history.pushState({}, '', this.to)
               //2.把当前路径记录到data.current里
                this.$router.data.current = this.to
                e.preventDefault()
                //事件处理函数,调用默认行为
            }
        }
    }
    )
    
    const self = this
    Vue.component('route-view', {
        render (h) {
            //在render内部首先要找到路由地址,再根据当前路由的地址,在routeMap对象中找到路由地址对应的组件,再调用h函数帮这个组件转换成虚拟DOM,然后返回
            const component = self.routeMap[self.data.current]//获取路由地址
            return h(component)
        }
    })
}

initEvent:当地址变化的时候,改变页面元素的逻辑

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

initEvent () {
    window.addEventListener('popstate', ()=> {
        this.data.current = window.location.pathname
    })
}


本文首发于我的GitHub博客,其它博客同步更新。