Vue-Router的实现原理

923 阅读6分钟

vue的基础结构

el

 new Vue({
    el: '#app',
    data: {
        company: {
            name: '掘金',
            address: '上海自由世纪广场'
        }
    }
})
vue中会把data中的数据填充到el指向的模板中,并把模板渲染到浏览器

render

new Vue({
    data: {
        company: {
            name: '掘金',
            address: '上海自由世纪广场'
        }
    },
    render(h) {
        return h('div', [
            h('p', '公司名称:' + this.company.name),
            h('p', '公司地址:' + this.company.address)
        ])
    }
}).$mount('#app')
h函数的作用是创建虚拟dom,render是把h函数创建的虚拟dom返回,$mount方法的作用是把虚拟dom转化为真实dom,渲染到浏览器

vue的生命周期

  1. 初始化---初始化事件,生命周期相关成员,包括h函数
  2. beforeCreate
  3. 初始化注入---会把props,data,method等注入到vue的实例上
  4. created---可以访问到上面注入的成员
  5. 接下来是把模板编译成render函数---首先判断选项中是否设置了el选项,如果没有设置就调用$mount方法,把el转换成template,接下来把模板变成render函数,接下里判断是否设置了模板,如果没有设置的话会把el的外层html作为模板,然后把template模板编译到渲染函数中,渲染函数就是用来生成虚拟dom的
  6. beforeMount---挂载之前,无法获取新元素的内容
  7. 挂载dom,把新的结构渲染到页面上
  8. mounted---可以访问新的dom
  9. 当调用destroy()方法的时候,首先会触发beforeDestory,
  10. 然后执行清理的工作---解除绑定,销毁子组件和事件监听等 11.destoryed

如果我们使用单文件组件的时候,模板编译是在打包和构建的时候完成的,不在运行期间去编译模板

组件---可复用的vue实例

vue-router的原理实现

使用步骤

  1. 创建视图组件
  2. 使用Vue.use(VueRouter)注册路由插件,Vue.use()用来注册插件,接收一个参数,参数是函数的话,会自动调用这个函数,是一个对象的话,会调用对象中的install方法来注册插件
  3. 定义路由数组,路由规则
  4. 创建router对象const router = new VueRouter({ routes }),创建的时候,需要把路由规则传递进来,然后导出
  5. main.js中导入,创建实例的时候需要把这个导入的路由对象注册上去,当做参数传递进vue实例中,传入router的作用是给vue实例注入$route$router两个属性

$route$router

$route路由规则,存储的是路由的数据,包括参数,路由路径等
$routervue-router的实例,就是路由对象,他(的原型上)提供了一些路由相关的方法,包括pushreplacego等,路由模式等

关于路由的路径解释

  1. 嵌套路由中,父组件中一定会有router-view占位
  2. 路由的相对路径和绝对路径的区别就是相对路径(不带‘/’)会拼接父级路由,绝对路径(带‘/’)是配置的完整路由
routes: [
    {
      path: '/vue',  //父级路径
      component: Info,
      children: [
        { path: 'home', component: Home },  //相对路径
        { path: '/about', component: About }  //绝对路径
      ]
    }
  ]
 

相对路径:地址栏最后的路径显示为localhost:8080/#/vue/home
绝对路径:地址栏最后的路径显示为localhost:8080/#/about

编程式导航

this.$router.push() this.$router.replace() this.$router.go()类似这样的页面跳转的方法,就叫做编程式导航

hash和history模式的区别

表现形式区别 一个带#一个没有
原理的区别

Hash模式是基于锚点以及onhashchange事件 history模式基于H5的 History API来实现的主要依靠的是history.pushState()history.replaceState()来实现的

history.pushState()是ie10以后才支持的
然后history.pushState()history.push()的区别是当调用history.push()路径发生变化,需要向服务器发送请求,而history.pushState()不会发送请求,只会改变浏览器的地址,并且把这个地址记录到历史记录中去,从而实现客户端路由

history模式的使用

  • history需要服务取得支持
  • 单页面应用中只有一个页面index.html,服务端不存在http://xxxxxxx/login这样的页面,正常访问单页应用不会有任何问题,但是当前浏览器地址是这个的时候,同时我们刷新浏览器的时候,会像服务器去请求login这个页面,而服务器不存在这个页面,于是返回404
  • 所以在服务端应该除了静态资源都返回单页应用的index.html

vue-cli自带的服务器已经处理好了这个问题,所以在本地开启的服务中,是没有这样的问题的

node服务器配置

如果在nodejs服务器中处理history模式的问题的时候我们需要导入connect-history-api-fallack模块,把这个模块注册到node服务器中,如下

const path = require('path')
const history = require('connect-history-api-fallack')	// 导入处理history模式的模块
const express = require('express')

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

app.listen(3000, () => {
	console.log('服务器开启了')
})

我们再尝试下再nginx服务其中配置处理history模式

nginx服务器的配置
  • 从官网下载nginx的压缩包,解压,目录不能有中文
  • 打开命令行,切换到nginx目录
  • 启动nginxstart nginx 启动nginx -s reload 重启nginx -s stop 停止
  • 把打包好的前端代码拷贝到nginx文件下的html文件夹下面,之后我们需要修改的文件在conf目录下的nginx.conf文件
// 找到http对象下的server下的location
location / {
	root html;
    index index.html index.htm;
    try_files $uri	$uri/	/index.html;
}
// try_files的意思是试着访问一下这个文件,$uri当前浏览器请求的路径所对应的文件,找到了就直接返回,找不到就继续往后找
// 找不到,再把这个请求的路径当做一个目录($uri/),再去寻找这个目录下的默认首页index.html或者index.htm,找到直接返回,找不到继续往后找
// 没找到,因为我们访问的是单页应用,前两次寻找没找到,意味着请求路径不存在,所以需要返回单页面应用的首页,也就是根目录文件夹下的index.html

VueRouter实现原理

hash模式

url中的#后面的内容作为路径地址,直接通过location.url切换浏览器中地址,只改变了后面的url不会向服务器请求地址,只会把地址记录到浏览器的访问历史中 监听hashchange事件,hash发生改变的时候,触发这个事件 根据当前的路由地址找到对应的组件,进行重新渲染 History模式

  • 通过history.pushState()方法改变地址栏,并把当前地址记录到浏览器的访问历史中,仅仅是改变地址栏,并不会跳转路径
  • 通过监听popstate事件,可以监听到浏览器历史操作的变化,popstate事件的处理函数中,可以记录改变后的地址,点击浏览器的前进后退按钮,或者调用history的back和forward方法的时候,这个事件才会被触发
  • 根据当前的路由地址找到对应的组件,进行重新渲染

关于vue这里需要普及一个版本问题

Runtime-only版本字面意思是只包含运行时版本,是在构建时通过webpackvue-loader工具将模板预编译成Javascript,也就是进行了预编译,在最终打好的包里实际上是已经编译好的,在浏览器中可以直接运行 Runtime+complier字面意思是运行时+编译器,是不在打包时进行编译的,是在客户端(浏览器)运行时进行编译的,所以要使用带编译器的完整版本

// Runtime+complier
new Vue({
  router,
  components: { App },
  template: '<App/>'
}).$mount('#app')

// Runtime-only版本
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

手写一个支持history模式的Vue-router

let _Vue = null
export default class VueRouter {
    static install (Vue) {
        // 判断是否已安装
        if(VueRouter.install.installed) return
        VueRouter.install.installed = true
        // 把Vue构造函数记录到全局变量
        _Vue = Vue
        // 把创建Vue实例传入的router对象注入到Vue实例上,new Vue()那边
        // _Vue.prototype.$router = this.options.router
        _Vue.mixin({
            beforeCreated() {
                if(this.$options.router){
                    _Vue.prototype.$router = this.options.router    // this.options 创建vue实例,初始化的时候传入的参数
                    // mixin中this指向的就是vue实例

                    this.$options.install.init()
                }
            }
        })
    }

    constructor(options) {
        this.options = options // 记录构造函数中传入的options
        this.routeMap = {}     // 存储 路由地址:对应组件的键值对
        this.data = _Vue.observable({   // 使用vue提供的observable方法创建响应式对象,可以直接用在渲染函数和计算属性里面
            current: '/'      // 记录当前路由地址
        })
    }

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

    createrouteMap() {
        // 路由树中处理成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        })
    }

    initComponents (Vue) {
        Vue.component('router-link', {
            props: {
                to: String
            },
            // template: '<a :href="to"><slot></slot></a>'  // vue-cli 默认使用运行时版本的vue,没有编译器
            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 // data是响应式的对象,值改变之后,会重新加载对应的组件,重新渲染
                    e.preventDefault() // 阻止默认行为 使浏览器不跳转不刷新
                }
            }
        })
        const self = this
        Vue.component('router-view', {
            render (h) {    // h函数的作用就是创建虚拟dom
                // 找到当前的路由地址 self.data.current
                const component =  self.routeMap[self.data.current]
                return h(component)  // h函数可以直接把一个组件转换成虚拟dom
            }
        })
    }

    initEvent() {   // 处理浏览器后退前进
        window.addEventListener('popstate', () => { 
            // 取出当前地址栏中的地址,我们要的仅仅是路径部分,第一个/后面的部分
            this.data.current = window.location.pathname
        })
    }
}