Vue Router 实现原理
Vue Router使用步骤
基础路由
接下来我会通过代码的形式为大家展现路由的基本用法
- 创建router对象,router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 路由组件
import index from '@/views/index'
// 组成插件
Vue.use(VueRouter)
// 路由规则
const routes = [
    {
        name: 'index',
        path: '/',
        component: index
    }
]
// 路由对象
const router = new VueRouter({
    routes
})
export default router- 注册注册router对象,在main.js文件内操作
import router from './router'
new Vue({
render: h => h(App),
router
}).$mount('#app')- 创建路由占位, 在需要用到路由的文件中,例如App.vue文件
<router-view></router-view>
- 最后一步创建链接
<router-link to="./">首页</router-link>
<router-link :to="{name: 'index'}">首页</router-link>动态路由
<template>
    <div>
        // 方式一: 通过路由规则获取的数据
        通过当前路由规则匹配获取: {{$route.params.id}}
        // 方式二: 不仅可以通过路由,还有通过父组件传递(推荐)
        通过开启 props 获取: {{id}}
    </div>
</template>
<script>
export default {
    name: 'Detail',
    props: ['id']
}
</script>const routes = [
    {
        name: 'Detail',
        path: '/detail/:id', // :id是占位符,传入对应的id,
        // 开启props会把url中的参数传递给组件,在组件内通过props来接收url参数
        props: true,
        // 路由懒加载,当需要访问该路由时加载组件
        // webpackChunkName 这个参数在我 前端模块打包工具Webpack中有介绍(魔法注释,自定义打包名)
        component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
    }
]嵌套路由
const routes = [
    {
        name: 'Login',
        path: '/login',
        component: () => import('@/views/Login.vue')
    },
    // 嵌套路由
    {
        path: '/',
        component: () => import('@/components/Layout.vue'), // 公共组件
        children: [
            {
                name: 'index',
                path: '', // 如果为空则访问父路径('/')时会默认将父组件合并加载该组件
                component: () => import('@/views/Index.vue'), // 跟上边公共组件合并
            },
            {
                name: 'detail',
                path: 'detail/:id', // 相对路径
                props: true,
                component: () => import('@/view/Detail.vue'), // 跟上边公共组件合并
            }
        ]
    }
]编程式导航
- 
跳转路由但不记录历史 this.$router.replace('/login')
- 
会记录历史跳转 this.$router.push('/')
- 
会记录历史跳转切传递参数id为1 this.$router.push({name: 'Detail', params: { id: 1 }})
- 
后退到上上一次页面,后退2次 this.$router.go(-2)
Hash模式和History模式
表现形式区别
- 
Hash模式 https://xuanhe.com/#/detail?age=18
- 
History模式 https://xuanhe.com/detail/18
原理的区别
- Hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,当地址发生变化后,发生onhashchange事件
- History模式是基于HTML5中的History API
- history.pushState() 不会向服务端发送请求,只会改变浏览器地址,并且将地址记录在历史记录中
- history.replaceState() 同上,但是不会保存记录
- 监听popstate事件监听浏览器的变化,但是调用history.pushState()和history.replaceState()不会触发该事件,只有在点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)才会出发
 
关于popstate的用法详情 popstate
注意: History模式需要服务器的支持
- 
单页应用中,服务端不存在的地址会返回找不到该页面(刷新页面的操作会向服务器发送请求) 
- 
在服务器端要配置除了静态资源外都返回单页应用的index.html 
- 
node中使用一个插件处理history模式 
connect-history-api-fallback
- 
nginx中配置 server { listen 80; # 端口号 server_name localhost; # 域名(一般是线上地址) #charset koi8-r; #access_log logs/host.access.log main; location / { root html; # 根目录 index index.html index.htm; # 默认首页(从根目录里找) try_files $uri $uri/ /index.html; # $uri就是请求的路径,会去找($url这个文件, 或者是$uri目录下的index配置的页面,或者直接访问根文件的index.html,可以根据实际情况进行配置) } }
模拟实现Vue Router
回顾核心代码
// router/index.js
// 注册插件
// vue.use这个方法可以接收一个函数或者一个对象,如果接收的函数则内部直接调用该函数,如果传入的是一个对象,会调用对象的install方法
Vue.use(VueRouter)
// 创建路由对象 VueRouter是一个类,内部应该具备install方法
const router = new VueRouter({
    routes: [
        { name: 'home', path: '/', component: homeComponent }
    ]
})
// main.js
// 创建vue实例,注册router对象
new Vue({
    router,
    render: h => h(app)
}).$mount('#app')install 方法的创建
- 判断插件是否已经安装
- 把vue的构造函数记录的全局变量中
- 把创建vue实例时候传入的router对象注入的vue实例上
let _vue = null; export default class VueRouter { static install(vue) { // 1.判断插件是否已经安装 if (VueRouter.install.installed) { return; } VueRouter.install.installed = true; // 2.把vue的构造函数记录的全局变量中 _vue = vue; // 3.把创建vue实例时候传入的router对象注入的vue实例上 // 混入,这里混入所有的vue实例都会有 _vue.mixin({ beforeCreate() { // 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行 if (this.$options.router) { _vue.prototype.$router = this.$optins.router; } }, }); } }
构造函数实现
构造函数主要初始化三个参数.
- options
- data (响应式对象)
- routeMap (键值对形式存储,键是路由地址,值是对应的组件)
 constructor(options) {
    this.options = options;
    this.data = _vue.observable({
        current: '/'
    });
    // 解析routes解析路由规则,键值对形式
    this.routeMap = {};
  }createRouteMap方法实现
- 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  // 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  createRouteMap () {
    // 遍历routes把path和component存到routerMap对象中;
    this.options.routes.forEach((route) => {
      this.routeMap[route.path] = route.component
    })
  }initComponents的router-link组件
  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      template: '<a :href="to"><slot></slot></a>'
    })
  }初步router插件已经完成,但是这样有一个问题,就是运行时版本不支持tempalte选项,必须使用render函数来创建组件,如果直接使用会报错
Vue 构建版本
- 运行时版: 不支持template模板,需要打包的时候提前编译
- 完整版: 包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数
要开启完整版只需要在vue.config.js文件中设置runtimeCompiler: true
module.exports = {
    runtimeCompiler: true
}运行时版不带编译器,不支持template选项
单文件组件中的template在打包时编译成了render函数,所以单文件中的template在运行时版可以生效
- 不使用编译器来解决组件template的方法
将tempalte替换为render函数,initComponents方法
initComponents的router-view组件和router-link组件的修改
- router-view相当于一个占位符,在该组件内部根据当前路由地址获取到对应的路由组件,渲染到router-view中
- 其他细节均在代码注释中体现
  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        // 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
        return h('a', {
          // 添加dom对象的属性写在attrs中
          attrs: {
            href: this.to
          },
          // 注册dom对象的事件
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default]) // 获取子元素默认插槽
      },
      methods: {
        // 事件处理函数
        clickHandler(e) {
          // 1.调用pushState方法改变浏览器的地址
          // pushState接收三个参数,1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
          history.pushState({}, '', this.to)
          // 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例),因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
          this.$router.data.current = this.to // 下面方式使用self也行,这个也行
          // 阻止默认行为
          e.preventDefault() 
        }
      }
      // 运行时版本不支持template,只支持render函数
      // template: '<a :href="to"><slot></slot></a>'
    })
    // 创建router-view组件
    // 组件内部的this是vue实例,这里需要用到router实例
    const self = this
    _vue.component('router-view',{
      // 利用h参数创建虚拟dom
      render(h) {
        // 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以接收一个组件转换成一个虚拟dom
        return h(component)
      }
    })
  }initEvent解决浏览器前进后退
现在基本上已经完成了一个基础的router了,但是会有一个小问题,在点击浏览器的前进后退按钮时地址栏发生变化,但是组件没有发生变化,没有重新加载地址对应的组件
- 解决浏览器前进后退渲染不更新的情况
 // 解决浏览器前进后退不重新加载组件的方法
  initEvent(){
    window.addEventListener('popstate', () => {
        // 将响应式的保存路径的值设置成 浏览器地址中路径部分的值即可
        this.data.current = window.location.pathname
    })
  }init方法封装
- 主要就是封装方法的调用
    // 封装初始化的一些方法
  init() {
    // 调用保存地址和组件映射的方法
    this.createRouteMap()
    // 调用创建router-link和router-view组件的方法
    this.initComponents()
    // 调用解决浏览器前进后退不重新加载组件的方法
    this.initEvent()
  }简版的完整代码
那么现在,我们就实现了一个简单的Vue Router,完整代码如下:
let _vue = null
export default class VueRouter {
  static install (vue) {
    // 1.判断插件是否已经安装
    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    // 2.把vue的构造函数记录的全局变量中
    _vue = vue
    // 3.把创建vue实例时候传入的router对象注入的vue实例上
    // 混入,这里混入所有的vue实例都会有
    _vue.mixin({
      beforeCreate () {
        // 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行
        if (this.$options.router) {
          _vue.prototype.$router = this.$options.router
        }
      }
    })
  }
  constructor (options) {
    this.options = options
    this.data = _vue.observable({
      current: '/'
    })
    // 解析routes解析路由规则,键值对形式
    this.routeMap = {}
    this.init()
  }
  // 封装初始化的一些方法
  init () {
    // 调用保存地址和组件映射的方法
    this.createRouteMap()
    // 调用创建router-link和router-view组件的方法
    this.initComponents()
    // 调用解决浏览器前进后退不重新加载组件的方法
    this.initEvent()
  }
  // 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  createRouteMap () {
    // 遍历routes把path和component存到routerMap对象中;
    this.options.routes.forEach((route) => {
      this.routeMap[route.path] = route.component
    })
  }
  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        // 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
        return h('a', {
          // 添加dom对象的属性写在attrs中
          attrs: {
            href: this.to
          },
          // 注册dom对象的事件
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default]) // 获取子元素默认插槽
      },
      methods: {
        // 事件处理函数
        clickHandler (e) {
          // 1.调用pushState方法改变浏览器的地址,pushState接收三个参数
          // 1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
          history.pushState({}, '', this.to)
          // 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例)
          // 因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
          this.$router.data.current = this.to
          e.preventDefault()
        }
      }
      // 运行时版本不支持template,只支持render函数
      // template: '<a :href="to"><slot></slot></a>'
    })
    // 创建router-view组件
    const self = this
    _vue.component('router-view', {
      // 利用h参数创建虚拟dom
      render (h) {
        // 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以接收一个组件转换成一个虚拟dom
        return h(component)
      }
    })
  }
  // 解决浏览器前进后退不重新加载组件的方法
  initEvent () {
    window.addEventListener('popstate', () => {
      // 将响应式的保存路径的值设置成浏览器地址中路径部分的值即可
      this.data.current = window.location.pathname
    })
  }
}希望大家对Router的理解更加深刻,开发中更加效率!
