VueRouter原理分析与实现

266 阅读4分钟

VueRouter基础回顾

文章内容输出来源:拉勾教育大前端高薪训练营 P.S.此处手动@雪姨

使用步骤

  1. 创建Router对象,router/index.js

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Index from '../views/Index.vue'
    // 1. 注册路由插件
    // Vue.use是用来注册组件,他会调用传入对象的install方法
    Vue.use(VueRouter)
    // 路由规则
    const routes = [
      {
        path: '/',
        name: 'Index',
        component: Index
      }
    ]
    // 2. 创建 router 对象
    const router = new VueRouter({
      routes
    })
    export default router
    
  2. 注册router对象,main.js

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    
    new Vue({
      // 3. 注册 router 对象
      router,
      render: h => h(App)
    }).$mount('#app')
    
  3. 创建路由组件的占位,App.vue

    <router-view/>
    
  4. 创建链接

    <router-link to="/">Index</router-link>
    <router-link to="/blog">Blog</router-link>
    

动态路由

const routes = [
    {
        name: 'detail',
        // 路径中携带参数
        path: '/detail/:id',
        // 路由懒加载
        component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
    }
]
// detail 组件中接收路由参数
$route.params.id

推荐使用以下方式传递参数

const routes = [
    {
        name: 'detail',
        // 路径中携带参数
        path: '/detail/:id',
        component: detail,
        props: true
    }
]
// detail 组件中接收路由参数
const detail = {
    props: ['id'],
    template: '<div>Detail ID: {{ id }}</div>'
}

嵌套路由

image-20210309220338797.png

编程式导航

// 跳转到指定路径
router.push('/login')
// 命名的路由
router.push({ name: 'user', params: { id: '5' }})
router.replace()//不记录本次历史
router.go()//跳转到历史中的某一次,如后退两次或者前进

Hash和History模式

两种模式的区别

  • 表现形式的区别

  • 原理的区别

    • hash模式

      hash模式基于锚点,以及onhashchange事件

      vue Router默认使用的是hash模式,使用hash来模拟一个完整的URL,通过onhashchange监听路径的变化

    • history模式是基于HTML5的HistoryAPI

      history.pushState() //IE10以后才支持
      history.replaceState()
      history.go()
      
  • 开启History模式

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

HTML5 History 模式的使用

  • History 需要服务器的支持
  • 单页应用中,服务端不存在 www.testurl.com/login 这样的地址会返回找不到该页面
  • 在服务端应该除了静态资源外都返回单页应用的 index.html
Node.js环境
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-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')
})
nginx环境配置
  • 从官网下载 nginx 的压缩包
  • 把压缩包解压到 c 盘根目录,c:\nginx-1.18.0 文件夹
  • 修改 conf\nginx.conf 文件
location / {
    root html;
    index index.html index.htm;
    #新添加内容
    #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页
    #如果都获取不到返回根目录中的 index.html
    try_files $uri $uri/ /index.html;
}
  • 打开命令行,切换到目录 c:\nginx-1.18.0
  • nginx 启动、重启和停止
# 启动
start nginx
# 重启
nginx -s reload
# 停止
nginx -s stop

模拟实现自己的Vue Router

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

Hash模式

  • URL中#后面的内容作为路径地址,当#后面的路径发生变化的时候,不会去请求服务器
  • 监听hashchange事件
  • 在hashchange事件中,根据当前的路由地址找到对应的组件并重新渲染

History模式

  • 通过history.pushState()方法改变地址栏,把当前地址记录到浏览器的访问地址中,浏览器不会向服务器发送请求
  • 监听popstate事件,可以监听到浏览器历史操作的变化,在popstate事件的处理函数中,可以记录改变后的地址;调用pushState和replaceState方法不会出发该事件,点击浏览器前进后退按钮的时候或者调用history的back或forward方法的时候才会触发
  • 根据当前路由地址找到对应组件并重新渲染

模拟实现Vue Router

Vue Router的核心代码
// 注册插件
// Vue.use() 内部调用传入对象的 install 方法
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
    routes: [
    	{ name: 'home', path: '/', component: homeComponent }
    ]
})
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')

Vue.use()函数可以传入对象也可以传入方法

  • 传入方法会直接执行这个方法
  • 传入对象会调用对象内部的install方法
Vue Router的类图

image-20210310102905582.png

实现思路
  • 创建 LVueRouter 插件,静态方法 install
    • 判断插件是否已经被加载
    • 当 Vue 加载的时候把传入的 router 对象挂载到 Vue 实例上(注意:只执行一次)
  • 创建 LVueRouter 类
    • 初始化,options、routeMap、app(简化操作,创建 Vue 实例作为响应式数据记录当前路径)
    • initRouteMap() 遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
    • 注册 popstate 事件,当路由地址发生变化,重新记录当前的路径
    • 创建 router-link 和 router-view 组件
    • 当路径改变的时候通过当前路径在 routerMap 对象中找到对应的组件,渲染 router-view
代码实现
  • 创建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.prototype.$router =
        // 混入
        _Vue.mixin({
          beforeCreate () {
            if (this.$options.router) {
              _Vue.prototype.$router = this.$options.router
              this.$options.router.init()
            }
          }
        })
      }
    }
    
  • 实现VueRouter类的构造函数

    constructor (options) {
      this.options = options
      this.routeMap = {}
      this.data = _Vue.observable({
        current: '/'
      })
    }
    
  • 实现VueRouter类的路由映射函数

    createRouteMap () {
      // 遍历所有路由规则,解析成键值对,存储到routeMap
      this.options.routes.forEach(route => {
        // 记录路径和组件的映射关系
        this.routeMap[route.path] = route.component
      })
    }
    
  • 实现 LVueRouter 类 - router-link 和 router-view 组件

    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 _this = this
      Vue.component('router-view', {
        render (h) {
          let component = _this.routeMap[_this.data.current]
          // 若该路由没有匹配到相应的组件,则默认匹配到404页面
          if (!component) {
            component = _this.routeMap['*']
          }
          return h(component)
        }
      })
    }
    

    注意:

    • vue-cli创建项目默认使用的是运行时版本的vue.js

    • 如果想切换成带编译器版本的Vue.js,需要修改vue-cli配置

      • 项目根目录创建vue.config.js文件,添加runtimeCompoler
      module.exports = {
         runtimeCompiler: true
      }
      
  • 实现VueRouter类的初始化方法init()

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

参考资料

Vue完成版和运行时