Vue-router 原理实现

407 阅读1分钟

Vue-router基础回顾

使用步骤

  1. 创建routeR对象 router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
//路由组件
import Index from '../views/Index.vue'
// 1. 注册路由插件
Vue.use(VueRouter)

// 路由规则
const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  },
]
// 2. 创建 router 对象
const router = new VueRouter({
  routes
})

export default router
  1. 注册 router 对象 main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  // 3. 注册 router 对象
  router,
  render: h => h(App)
}).$mount('#app')
  1. 创建路由组件站位 App.vue
    <router-view/>
  1. 创建连接
    <router-link to="/">Index</router-link> |
    <router-link to="/blog">Blog</router-link> |

动态路由传参

  1. 通过当前路由规则获取 $route.params.id
const routes = [
  {
  //路径中携带参数
    path: "/detail/:id",
    name: "Detail",
    component: Detail
  }
]
//detail 组件中接受路由参数
//$route.params.id

  • 主意:通过路由规则来获取参数,强依赖路由,使用组件时候必须传相应的参数
  1. 通过开启 props 获取
const routes = [
  {
  //路径中携带参数
    path: "/detail/:id",
    name: "Detail",
    component: Detail,
    //开启props把url里面的参数传递给组件
    //在组件中通过props来接受URL参数
    props:true
  }
]
//detail 组件中接受路由参数
export default {
    props:["id"],
    template:'<div>Detail ID: {{ id }}</div>
}

嵌套路由

  • index组件和details组件嵌套layout组件
{
    path: '/',
    component: Layout,
    children: [
      {
        name: 'index',
        path: '',
        component: Index
      },
      {
        name: 'detail',
        path: 'detail/:id',
        props: true,
        component: () => import('@/views/Detail.vue')
      }
    ]
  }

编程式导航

// 跳转到指定路径 
router.push('/login') 
// 命名的路由 
router.push({ name: 'user', params: { id: '5' }}) 
//replace方法不会记录本次历史
router.replace() 
router.go()

Hash 模式和 History 模式的区别

  1. 两种模式都是客户端路由的实现方式,当路径发生变化的之后,不会像服务器发送请求,使用js监视路径的变化,根据不同的地址渲染不同的内容。如果需要服务器端内容,发送ajax请求来获取。
  • 表现形式的区别
  • 原理的区别
    • Hash 模式
      • Vue Router 默认使用的是 hash 模式,使用 hash 来模拟一个完整的URL,通过onhashchange 监听路径的变化
    • History 模式
      history.pushState() 
      history.replaceState() 
      history.go()
      
  • 开启 History 模式
const router = new VueRouter({
 mode: 'history',
 routes
})
  • HTML5 History 模式的使用
  1. History 需要服务器的支持
  2. 单页面应用中,服务端不存在www.testurl.com/login 这样的地址会返回找不到该页面
  3. 在服务端应该除了静态资源外都返回单页应用的 index.html

Vue Router 模拟实现

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

Hash 模式

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

History 模式

  • 通过history.pushState()方法改变地址栏
  • 监听popstate事件 可以监听到浏览器地址操作的变化,在popstate事件处理函数中可以记录改变后的地址,需要注意的是,当调用pushState或者replaceState不会触发该事件,只有点击浏览器的前进和后退按钮的时候,或者调用history的back 和 forward方法的时候才会触发该事件
  • 根据当前路由地址找到对应的组件重新渲染

回顾核心代码

截屏2021-06-21 下午5.05.04.png

类图

  • VueRouter属性分别有 options data routeMap 属性
  1. options的作用是记录构造函数中传入的对象
  2. routeMap的作用记录路由地址和组件对应的关系
  3. data属性里面有一个current记录当前路由地址的,data是响应式的对象,因为当路由地址发生变化之后,对应的组件也要相应的更新

创建 VueRouter 插件

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.mixin({
        beforeCreate () {
            // 判断router 对象是否已经挂载了 Vue 实例上个
            if(this.$options.router){
                // 把router 对象注入到Vue实例上
                _Vue.prototype.$router = this.$options.router
                _Vue.prototype.$router.init()
            }
        }
    })
   }
}

实现 VueRouter 类的构造函数

constructor (options) {
       this.options = options
     // 记录路径和对应的组件
       this.routeMap = {}
       this.data = _Vue.observable({
           // 当前的默认路径
           current: "/"
       })
 }

实现 VueRouter 类 createRouteMap

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

实现 VueRouter 类 - 注册事件

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

实现 VueRouter 类 - router-link 和 router-view 组件

initComponents (Vue) {
      Vue.component('router-link', {
          props: {
              to: String
          },
          // 需要带编译器版本的 Vue.js 
          // template: "<a :href='\"#\" + to'><slot></slot></a>"
          // 使用运行时版本的 Vue.js
          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] 
            return h(component)
         }
      })
   }

Vue的构建版本

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

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

  • 注意:

    • vue-cli 创建的项目默认使用的是 运行时版本的 Vue.js
    • 如果想切换成带编译器版本的 Vue.js,需要修改 vue-cli 配置
      • 项目根目录创建 vue.config.js 文件,添加 runtimeCompiler
      module.exports = {
         runtimeCompiler: true
      }
      

实现 VueRouter 类 - init()

   init(){
       this.createRouteMap()
       this.initComponents(_Vue)
       this.initEvents()
   }
   // 插件的 install() 方法中调用 init() 初始化 
   if (this.$options.router) {
       _Vue.prototype.$router = this.$options.router 
       // 初始化插件的时候,调用 init 
       this.$options.router.init() 
   }