vue-router 实现源码

141 阅读8分钟

路由是什么

路由的定义,维基是这样定义的;路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。路由引导分组转送,经过一些中间的节点后,到它们最后的目的地。作成硬件的话,则称为路由器。路由通常根据路由表——一个存储到各个目的地的最佳路径的表——来引导分组转送 上面的定义可能很官方,但是我们可以抽出一些重点

路由是一种活动,负责将信息从源地址传输到目的地址; 要完成这样一个活动需要一个很重要的东西路由表-源地址和目标地址的映射表

前端路由是什么

其实前端路由是针对 spa说的,在spa出现之前,页面的跳转(导航)都是通过服务端控制的,并且跳转存在一个明显白屏跳转过程;spa出现后,为了更好的体验,就没有再让服务端控制跳转了,于是前端路由出现了,前端可以自由控制组件的渲染,来模拟页面跳转

前端路由的两种模式

  1. hash模式 hash 模式 ##a ##b 通过# 后边的路径方式进行切换 // window.location.hash = '/about' // window.onhashchange = function () {} // hash 变化渲染对应的路径组件

// window.history.pushState() // 实现增添路径 但是强制刷新还是有问题, (服务端来解决这个问题 // window.onpopstate = function () {} // 监控浏览器路径的变化

// vue-router 源码中 在hash模式下, 如果支持onpopstate 会优先使用,否则使用onhashchange

  1. history 模式

history 实现 history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新 history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

通过浏览器前进后退改变 URL 时会触发 popstate 事件 通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。 好在我们可以拦截 pushState/replaceState的调用和标签的点击事件来检测 URL 变化 通过js 调用history的back,go,forward方法课触发该事件

所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

hash 和history模式的区别是什么

hash: 兼容所有浏览器,包括不支持 HTML5 History Api 的浏览器,例 www.abc.com/#/index,has… hash的改变会触发hashchange事件,通过监听hashchange事件来完成操作实现前端路由。hash值变化不会让浏览器向服务器请求。// 监听hash变化,点击浏览器的前进后退会触发

window.addEventListener('hashchange', function(event){ 
    let newURL = event.newURL; // hash 改变后的新 url
    let oldURL = event.oldURL; // hash 改变前的旧 url
},false)

history: 兼容能支持 HTML5 History Api 的浏览器,依赖HTML5 History API来实现前端路由。没有#,路由地址跟正常的url一样,但是初次访问或者刷新都会向服务器请求,如果没有请求到对应的资源就会返回404,所以路由地址匹配不到任何静态资源,则应该返回同一个index.html 页面,需要在nginx中配置。 abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

实现思路

1. 要明白的几个概念

### 路由规则、配置对象(RouteConfig)

### 路由记录(RouteRecord)
### 路由对象(Route)
### 位置(Location)
### 路由组件(RouteComponent)

2. 主要的思路

从第一节的知识我门可以得知 要实现一个前端路由,需要三个部分 路由映射表: 一个能表达url和组件关系的映射表,可以使用Map、对象字面量来实现 匹配器: 负责在访问url时,进行匹配,找出对应的组件 历史记录栈: 浏览器平台,已经原生支持,无需实现,直接调用接口

3. 目录结构

4. 初始化的一些东西

1. Vue.use(VueRouter) install 方法

上面也提到vue-router的入口文件在src/index.js中,那我们去index.js中找找install方法

```js
// src/index.js


import { install } from './install' // 导入安装方法
// ...

// VueRouter类
export default class VueRouter {
  // ... 
}

// ...

VueRouter.install = install // 挂载安装方法,Vue.use时,自动调用install方法
VueRouter.version = '__VERSION__'

// 浏览器环境,自动安装VueRouter
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
```

可以看到,开头导入了install方法,并将其做为静态方法直接挂载到VueRouter上,这样,在Vue.use(VueRouter)时,install方法就会被调用; 可以看到,如果在浏览器环境,并且通过script标签的形式引入Vue时(会在window上挂载Vue全局变量),会尝试自动使用VueRouter 我们接下来看看install.js中是什么

install方法

 import View from './components/view'
 import Link from './components/link'

 export let _Vue // 用来避免将Vue做为依赖打包进来

 // install方法
 export function install (Vue) {
   if (install.installed && _Vue === Vuereturn // 避免重复安装
   install.installed = true

   _Vue = Vue // 保留Vue引用

   const isDef = v => v !== undefined

   // 为router-view组件关联路由组件
   const registerInstance = (vm, callVal) => {
     let i = vm.$options._parentVnode
     // 调用vm.$options._parentVnode.data.registerRouteInstance方法
     // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js @71行)
     // 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件
     if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
       i(vm, callVal)
     }
   }

   // 注册全局混入
   Vue.mixin({
     beforeCreate () {
       // this === new Vue({router:router}) === Vue根实例

       // 判断是否使用了vue-router插件
       if (isDef(this.$options.router)) {
         // 在Vue根实例上保存一些信息
         this._routerRoot = this // 保存挂载VueRouter的Vue实例,此处为根实例
         this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
         // beforeCreate hook被触发时,调用
         this._router.init(this// 初始化VueRouter实例,并传入Vue根实例
         // 响应式定义_route属性,保证_route发生变化时,组件(router-view)会重新渲染
         Vue.util.defineReactive(this'_route'this._router.history.current)
       } else {
         // 回溯查找_routerRoot
         this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
       }

       // 为router-view组件关联路由组件
       registerInstance(thisthis)
     },
     destroyed () {
       // destroyed hook触发时,取消router-view和路由组件的关联
       registerInstance(this)
     }
   })

   // 在原型上注入$router、$route属性,方便快捷访问
   Object.defineProperty(Vue.prototype'$router', {
     get () { return this._routerRoot._router }
   })
   Object.defineProperty(Vue.prototype'$route', {
     // 每个组件访问到的$route,其实最后访问的都是Vue根实例的_route
     get () { return this._routerRoot._route }
   })

   // 注册router-view、router-link全局组件
   Vue.component('RouterView'View)
   Vue.component('RouterLink'Link)

   const strats = Vue.config.optionMergeStrategies
   // use the same hook merging strategy for route hooks
   strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
 }

2. 初始化流程 _init() 方法

import install from "./install"

import createMatcher from './create-matcher';
import HashHistory from "./history/hashHistory";
import BrowserHistory from "./history/browserHistory";

class VueRouter {
    constructor (options) {

        // 创建了一个匹配器 1. 匹配功能 match  2. 可以添加匹配, 动态路由添加 addRoutes 权限

        this.matcher = createMatcher(options.routes || []) // 获取用户的整个配置

        // 创建历史管理 (路由两种模式 hash 和浏览器API)
        this.mode = options.mode || 'hash'

        switch (this.mode) {
            case 'hash':
                this.history = new HashHistory(this) // this是当前的实例
                break;
            case 'history':
                this.history = new BrowserHistory(this) // this 是当前的实例
                break;
        }
        this.beforeHooks


    }
    match (location) { // 做一个中转 把 matcher中的match方法 提出来
        return this.matcher.match(location)
    }
    init (app) { // 目前这个app 指代就是 最外层的 new Vue
        // 需要根据用户的配置 作出一个映射表
        
        // 需要根据当前路径 实现下一个页面跳转的逻辑

        // 跳转的路径 会进行匹配操作 ,根据路径获取对应的记录

        const history = this.history

        // transitionTo 跳转逻辑 hash browser都有
        // getCurrentLocation  获取当前路径 hash 和browser 实现不一样 
        // setupListener  hash 监听

        // 初始化时 都需要调用 更新_route 的方法来获取current更新
        
        let setUpHashListener = () => {
            history.setupListener()
        }

        history.transitionTo(history.getCurrentLocation(), setUpHashListener)

        // this.current 变换就触发这个方法
        history.listen((route) => {
            app._route = route // 更新视图的操作, 当current变化 更新
        })

    }
    beforeEach(fn) {
        this.beforeHooks.push(fn)
    }
}

VueRouter.install = install

export default VueRouter

接收RouterOptions

RouterOptions定义了VueRouter所能接收的所有选项;

我们重点关注一下下面的几个选项值

routes是路由配置规则列表,这个主要用来后续生成路由映射表的;

它是一个数组,每一项都是一个路由配置规则(RouteConfig),关于RouteConfig,可以回看术语那一节;

mode、fallback是跟路由模式相关的

后面会详细讲VueRouter的路由模式

属性赋初值

对一些属性赋予了初值,例如,对接收全局导航守卫(beforeEach、beforeResolve、afterEach)的数组做了初始化

创建matcher

通过createMatcher生成了matcher 这个matcher对象就是最初聊的匹配器,负责url匹配,它接收了routes和router实例;createMatcher里面不光创建了matcher,还创建了路由映射表RouteMap,我们后面细看

确定路由模式

三种路由模式我们后面细讲 现在只需要知道VueRouter是如何确定路由模式的 VueRouter会根据options.mode、options.fallback、supportsPushState、inBrowser来确定最终的路由模式 先确定fallback,fallback只有在用户设置了mode:history并且当前环境不支持pushState且用户主动声明了需要回退,此时fallback才为true 当fallback为true时会使用hash模式; 如果最后发现处于非浏览器环境,则会强制使用abstract模式

作者:光辉GuangHui 链接:juejin.cn/post/688052… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

根据路由模式生成不同的History实例

后续看 histroy文件夹

3. 设置监听方法 setUpHashListener

setupListeners

上面说到,在初始化时,会根据history类型,调用transitionTo跳转到不同的初始页面 为什么要跳转初始页面?

因为在初始化时,url可能指向其它页面,此时需要调用getCurrentLocation方法,从当前url上解析出路由,然后跳转之

可以看到HTML5History类和HashHistory类调用transitionTo方法的参数不太一样

前者只传入了一个参数 后者传入了三个参数

我们看下transitionTo方法的方法签名

this.$route 和 this.$router 区别