Vue-Router核心原理实现

vue-router基本使用

  • 配置路由文件

    // 路由配置文件router/index.js
    // 导入vue和vue-router
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    // 导入首页视图文件
    import Index from '../views/Index.vue'
    
    // 注册路由插件
    // Vue.use()用来注册插件,会调用传入对象的install方法
    Vue.use(VueRouter)
    
    // 定义路由规则,即URL路径与视图的映射关系
    const routes = [
      {
        path: '/',
        name: 'Index',
        // 展示首页
        component: Index
      },
      {
        path: '/blog',
        name: 'Blog',
        // 路由层面的代码分割,为这个路径生成一个独立的代码块
        // 当该路径被访问时实现懒加载功能
        conponent: () => import(/* webpackChunkName: "blog" */'../views/Blog.vue')
      }
      ...
    ]
    
    // 创建路由对象router,传入路由规则
    const router = new VueRouter({
      // 默认模式是hash模式,不需要显示配置
      routes 
    })
    
    // 导出路由对象
    export default router
    
  • 入口文件中导入路由对象,传给Vue构造函数实例化

    • $router用来控制路由,$route作为$router的数据映射仓库使用

      import Vue from 'vue'
      // 导入根组件
      import App from './App.vue'
      // 导入路由对象
      import router from './router'
      
      // 生产环境不出现提示信息
      Vue.config.productionTip = false
      
      // 实例化Vue,挂载应用
      new Vue({
        // 传入router对象,将会给Vue实例注入$route和$router两个属性
        router,
        render: h => h(App)
      }).$mount('#app')
      
  • 在视图文件中,创建路由渲染的占位组件

    // App.vue
    
    <template>
      <div id="app">
         // 路由跳转,相当于<a>标签
         <router-link to="/">Index</router-link>
         <router-link to="/blog">Blog</router-link>
         // 路由渲染的占位组件,当匹配到某个路径时,渲染对应的组件
         <router-view />
      </div> 
    </template>
    <script>
    // ...
    </script>
    <style>
    // ...
    </style>
    

动态路由传参

const routes = [
  {
    // 动态路由中id作为参数,接收从URL传来的数据
    path: '/detail/:id',
    name: 'Detail',
    // 开启props,会把URL中的参数传递给组件,组件通过props来接收
    props: true,
    component: () => import(/* webpackChunkName: "detail" */'../views/Detail.vue')
  }
]

组件代码

// Detai.vue
<template>
  // 方式一:通过当前路由规则获取路由参数
  <div>{{ $route.params.id }}</div>
  // 方式二:通过props接收路由参数
  <div>{{ id }}</div>
</template>
<script>
  export default {
    name: 'Detail',
    // props声明:接收参数id,这与第二种方式接收路由参数相对应
    props: ['id']
  }
</script>

推荐使用props来接收动态路由参数。第一种通过$route.params来接收路由参数,使得组件与路由强耦合,不利于组件的灵活使用与维护。

路由中的查询字符串

使用$route.query来获取到查询字符串对象

嵌套路由

当需要有两个组件嵌套时,可以使用嵌套路由来同时展示。例如,layout.vue组件负责header和footer部分,而中间content部分则嵌套其他组件,根据路由的不同,content嵌套不同的组件。因此,content部分可以使用<router-view >来加载不同的组件。

  • 路径的匹配是由嵌套组件的路径组合来决定的。
  • 当路径匹配时,嵌套组件都会加载,将组件渲染到<router-view >
// 路由配置文件
const routes = [
  ...,
  {
    path: '/',
    component: Layout,
    // 嵌套路由配置,children里的path可以是相对路径,也可以是绝对路径
    children: [
      {
        name: 'index',
        // 当路径为/时,匹配到Index组件
        path: '',
        component: Index
      },
      {
        name: 'detail',
        // 当路径是/detail/:id时,匹配到Detail组件
        path: 'detail/:id',
        // @是src的别名
        component: () => import('@/views/Detail.vue')
      }
    ]
  }
]

编程式导航

调用$router的导航方法,如push、replace、go等等。
这些方法可以接收location对象和回调函数:(location, onComplete?, onAbort) => {}

  • location可以是表示路径的字符串,也可以是更具体的对象

    // location对象
    {
      // 在routes中配置的组件名称
      name,
      // 路径字符串,同时提供path和params时,params不生效,同时path必须是完整的带参数的路径
      path,
      // 动态路由参数
      params: { key: value },
      // 查询字符串
      query: { key: value }
    }
    
  • go方法只需要一个数值型参数即可。

hash与history模式

表现形式

  • hash:基于锚点,路径中带#
  • history:正常的路径,但需要服务器配合

触发事件

  • hash:hashchange事件
  • history:popstate事件监听前进后退操作,pushState和replaceState方法
    • 调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)。

History模式

单页应用中通过JavaScript来访问(pushState之类的方法不会发送请求)没有问题,但如果通过浏览器地址栏来访问时,浏览器会发送请求给服务器,则会造成404情况
因此,服务器出了静态资源外,都返回单页应用的index.html。

配置404页面路由
// 路由配置文件
const routes = [
  ...,
  // 当以上所有路径都不匹配时,是一个未匹配路径,则用*捕获该路径,显示404页面
  {
    path: '*',
    name: '404',
    component: () => import('../views/404.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})
History模式下,node.js服务器的配置
// app.js,express框架开发的服务器
const path = require('path')

// 导入处理history模式的中间件模块
const history = require('connect-history-api-fallback')

const express = require('express')

const app = express()

// 处理单页应用在history模式下的请求
app.use(history())

app.use(express.static(path.join(__dirname, 'web')))

app.listen(3000, () => {
  console.log('监听port:3000');
})
nginx服务器配置

修改nginx配置文件

  server {
    location / {
      root   html;
      index  index.html index.htm
      // 这里要开启history模式对应的配置
      try_files $uri $uri/ /index.html
    }
  }

模拟Vue Router实现

URL变化的几种方式

  • 手动修改URL:浏览器会发送请求,服务端返回index.html,然后重新加载运行脚本,这样就会将当前URL对应的组件渲染出来。
  • 通过链接元素<a>修改URL
    • 在hash模式下触发hashchange事件
    • 在history模式下没有对应的监控URL变化事件,只能拦截<a>元素的点击事件阻止跳转e.preventDefault(),使用history对象的pushState或replaceState方法,然后手动修改路由实例中表示当前路径的data.current,从而触发重新渲染。
  • 通过浏览器的前进后退或history.forward()或history.back()方法修改URL(这些行为通常的请求会使用缓存资源,所以不会和服务端通信),则会触发popstate事件,在该事件内修改路由实例中表示当前路径的data.current,触发渲染。

思路

将URL的变化更新到路由实例的data.current上,由于current属性是响应式的,所以一旦current属性更新,则会引起重新渲染,将current对应的组件渲染到页面。

hash模式

  • URL中#后的内容作为路由地址。
    • 通过location.url改变地址栏
    • #后的内容更改不会发送请求,但会更新到历史记录
  • 监听hashchange事件
  • 根据当前路由地址找到对应组件,重新渲染

history模式

  • 通过history.pushState()或history.replaceState()方法改变地址栏
    • 不会发送请求
    • 更新到历史记录
    • 不触发popstate事件
    • 不触发hashchange事件,即使hash真的有改变
  • 监听popstate事件
    • 浏览器行为会触发popstate事件,如点击后退或前进按钮
    • 调用history.forward()或history.back()才会触发该事件。
  • 根据当前路由地址找到对应组件,重新渲染

核心代码实现

  • 根据Vue-Router的使用情况来确定如何实现
// 导入Vue-Router, 说明Vue-Router需要默认导出一个成员
import VueRouter from 'vue-router'

// Vue.use()方法注册VueRouter插件
// 插件可以是一个函数,也可以是一个对象
Vue.use(VueRouter)

// 创建路由对象,说明默认导出的是一个类
const router = new VueRouter({
  // 注册routes规则表
  routes: []
})

new Vue({
  // 创建Vue实例时,传入router实例
  router,
  render: h => h(App)
}).$mount('#app')
  • Vue.use()接收函数的时候,会直接调用该函数。如果接收的是对象,则会调用对象的install()方法
实现过程
  • 先给出类图,即类中的成员表

    • 中间的是属性,下面的是方法。+表示公共成员,_表示私有成员
      Vue-Router实现的类图
    • options:记录构造函数传入的选项对象
    • routeMap:记录路由地址与组件的映射关系,将routes定义的路由规则表解析后存储到routeMap中。
    • datadata.current定义当前的路由地址,data对象的目的是需要一个响应式的对象,当路由地址发生变化时,组件需要自动更新。使用Vue.observable( object )方法即可。
    • Constructor(Options): VueRouter
    • install(Vue): void:Vue的插件机制调用的
    • init(): void:调用下面的三个方法
    • initEvent(): void:注册popState事件
    • createRouteMap(): void:初始化routeMap
    • initComponents(Vue): void:创建<router-link /><router-view />这两个组件
  • 第一步,实现_install方法

    let _Vue = null // 声明一个变量来接收Vue,因为后面要用到Vue来做很多事
    // 导出一个类,包含一个静态的install方法
    export default class VueRouter {
      // install()接收Vue构造函数和可选的options
      static install (Vue, options) {
        // 判断当前插件是否已经安装,用静态属性来存储安装状态最合适
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        // 2. 把Vue构造函数记录到变量中,因为VueRouter还需要利用Vue的其他方法做些事,如Vue.component()来创建<router-link />和<router-view />组件
        _Vue = Vue
        // 3. 将实现挂载时创建的Vue实例传入的VueRouter实例注入到所有Vue实例上,这样每个Vue实例(组件本来就是Vue实例)的$router属性指向该VueRouter实例
        // 让所有实例都共享的引用,毫无疑问应该定义在原型上,所以这里使用_Vue.prototype.$router
        // $router的值应该是创建Vue实例时传入的options.router,即实例可以用vm.$options.router拿到该值
        // 但是如何获取到挂载时创建的Vue实例呢?
        // 使用全局混入(一般写插件用,业务代码不推荐),使得接下来的所有Vue实例化时都会使用混入的自定义逻辑,比如这里自定义beforeCreate钩子函数
        // 所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法
        _Vue.mixin({
          beforeCreate () {
            // 该方法会在每个Vue实例化(包括组件)都执行一次,但实际上我们只要执行一次就够了,因为是挂载到原型上的
            // 生命周期钩子的this指向实例
            if (this.$options.router) {
              // 这里的含义是,传入选项包含router时,才执行原型挂载,否则不执行,比如组件通常不传入router选项
              _Vue.prototype.$router = this.$options.router
              // 在Vue实例化时才调用router的初始化方法
              this.$options.router.init()
            }
          }
        })
      }
    
    • 首先需要判断插件是否安装
    • 其次,保存Vue构造函数到一个变量中
    • 最后,将router实例注入到每个Vue实例中,这里使用了全局混入的方法来达到目的。
  • 实现构造函数contructor

      constructor (options) {
        // options属性来保存传入的选项对象
        this.options = options
        // routeMap来保存路径与组件的映射关系
        this.routeMap = {}
        // 这里的data属性由于是用Vue.observable()创建,被Vue加入了响应式系统,从而data的更新会导致视图的更新
        // data.current的默认值应该根据当前浏览器路径来判断,而不是首页
        this.data = _Vue.observable({
          // current属性来保存当前浏览器地址栏的路径
          current: window.location.pathname
        })
      }
    
  • 创建createMap方法,遍历routes规则数组,将path与组件映射关系存入routeMap属性

      // 把options.routes转换到this.routeMap,键为path,值为组件
      createMap () {
        this.options.routes.forEach(route => {
          this.routeMap[route.path] = route.component
        })
      }
    
  • 创建initComponent方法,用于创建全局组件<router-link /><router-view />

      initComponents (Vue) {
        const self = this
        // 全局组件
        Vue.component('router-link', {
          props: {
            to: String
          },
          render (h) {
            return h('a', {
              attrs: {
                href: this.to
              },
              // 给<a>添加点击事件,阻止浏览器跳转
              on: {
                click: this.clickHandle
              }
            },
            [this.$slots.default])
          },
          methods: {
            clickHandle (e) {
              // 更新浏览器地址栏和历史记录
              history.pushState({}, '', this.to)
              // 更新当前router的data.current
              this.$router.data.current = this.to
              // 阻止浏览器跳转
              e.preventDefault()
            }
          }
        })
        Vue.component('router-view', {
          render (h) {
            const component = self.routeMap[self.data.current]
            // 异步组件也可以直接传入h()中渲染
            return h(component)
          }
        })
      }
    
  • 创建initEvents方法,用于监听popstate事件,更新data.current

      initEvents () {
        window.addEventListener('popstate', () => {
          this.data.current = window.location.pathname
        })
      }
    
  • 创建init方法,统一调用初始化要用到的方法

      init () {
        this.createMap()
        this.initComponents(_Vue)
        this.initEvents()
      }
    

路由匹配的原理

Vue-Router内部使用了 path-to-regexp库,将路由表中的path字符串转换为正则表达式,然后用正则表达式来匹配URL中的pathname,得到匹配信息与路径参数的值

path-to-regexp的使用

安装path-to-regexp,下面是基本使用的示例代码

const pathToRegexp = require('path-to-regexp').pathToRegexp

// pathToRegexp(path, keys, options),options选项对象可以不传
// keys数组用来记录匹配过程中得到的捕获组的key
const keys = []
// 返回一个正则表达式/^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i
let re = pathToRegexp('/foo/:bar', keys)
/* keys的值,每个对象都记录了原始字符串中的路径参数
[
  {
    name: 'bar',
    prefix: '/',
    suffix: '',
    pattern: '[^\\/#\\?]+?',
    modifier: ''
  }
]
*/
console.log(keys)
/* re.exec('/foo/baz')的执行结果
[
  '/foo/baz',
  'baz',
  index: 0,
  input: '/foo/randal',
  groups: undefined
]
*/
console.log(re.exec('/foo/baz'))

const match = re.exec('/foo/baz')
// url来记录当前被匹配的字符串, values则记录捕获到的值
const [url, ...values] = match
// 将路径参数与捕获到的值存储到params对象中
const params = keys.reduce((cache, key, index) => {
  cache[key.name] = values[index]
  return cache
}, {})
// { bar: 'baz' }
console.log(params)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值