vue-router原理解析并实现简单的vue-router

vue-router原理解析并实现简单的vue-router

vue-router的用法

当我们使用vue-router插件时会有两步

  1. Vue.use(VueRouter) 注册组件
  2. new VueRouter() 实例一个 VueRouter 类,同时把路由信息传给 Vue 实例
import Vue from 'vue'
import VueRouter from '../vuerouter'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]
const router = new VueRouter({
  mode: 'hash',
  routes
})
export default router;

当执行 Vue.use(VueRouter) 时会自动调用 VueRouter 类的 install 方法,安装 vue-router 插件。所以我们在实现 vue-router 时应该定义 VueRouter 类,并且其上声明 install 方法。

开始实现

install 方法

开始撸代码前我们先来捋一捋 vue-router 有哪些特点,首先他有两个内置组件 router-link 和 router-view,其次可以通过 router 拿到 vue-router 实例,最后还可以通过this.$router拿到 vue-router 实例(包括了路由的跳转方法,钩子函数等),通过this.$route拿到路由对象(包括path,params,hash,query,fullPath,matched,name等路由信息参数)。所以我们在 install 方法中要做如下工作:

  1. 注册 router-link 和 router-view 全局组件
  2. 调用 Vue.mixin() 方法为每个组件混入 router 属性获取 vue-router 实例,这里是重点难点,具体看代码注释。
  3. 在Vue对象原型上定义 响应式的$router 和 $route 属性,便于全局进行路由操作。

install.js

import routerLink from "./components/router-link";
import routerView from "./components/router-view";
export let Vue = null;
export const install = function (_Vue) {
  Vue = _Vue;
  // 注册组件
  Vue.component('router-link', routerLink)
  Vue.component('router-view', routerView)
  // 每个子组件都获取router属性
  Vue.mixin({
    // 如果有router 说明你在根实例增加了router
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this;//将当前根实例Vue放到了_routerRoot
        this._router = this.$options.router; // 当前VueRouter实例
        // 调用初始化函数
        this._router.init(this);
        // 把路由_route属性变成响应式的
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 将根组件的_routerRoot赋值个子组件的_routerRoot
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    }
  })
  // vue原型上添加两个响应式属性
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route;//取current
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router;
    }
  })
}

VueRouter类

我们再来想一想 vue-router 的核心功能是什么?当然是根据路由匹配对应的记录(记录就是组件加上一些其他信息的对象)啦! 那么就首先需要有路由到记录的映射关系,其次需要有一个方法能返回路由对应记录

  1. 在构造函数中定义 matcher 属性,它的值是参数是配置的路由信息的 createMatcher 方法的返回值,createMatcher 函数有两个重要功能:
    • 调用 createRouteMap 方法创建路径与组件的映射关系
    • 返回两个方法,功能分别是1、匹配路径对应的记录,2、添加新的路由
  2. 定义 match 方法,返回路径对应的记录,实际上调用的是 createMatcher 函数返回的匹配路径的方法。
  3. 声明 init 方法,声明监听 hash 变化的方法,跳转方法,注意这里是难点要点。

index.js

import { install } from './install';
import createMatcher from './create-matcher'
import BrowsHistory from './browsHistory';
import HashHistory from './hashHistory';
class VueRouter {
  constructor(options) {
    // 本函数两个功能:匹配路径对应记录功能match, 添加匹配功能addRoutes
    this.matcher = createMatcher(options.routes || []);//获取用户的整个路由配置
    // 创建历史管理 路由两种模式, 默认是hash
    this.mode = options.mode || 'hash';
    switch (this.mode) {
      case 'history':
        this.history = new BrowsHistory(this);
        break;
      case 'hash':
        this.history = new HashHistory(this);
        break;
    }
  }
  // 返回路径对应的记录
  match(location) {
    return this.matcher.match(location);
  }
  init(app) {//app指代最外层的Vue
    const history = this.history;
  	// 设置onHashChange,监听hash变化
    let setupHashLisener = () => {
      history.setupHashLisener();
    }
     // 跳转路径,会进行匹配操作根据路径获取对应的记录
    history.transitionTo(history.getCurrentLocation(), setupHashLisener);
    // 当跳转后更新_route,将回调函数传入
    history.listen(route => {
      app._route = route;//current更新后在更新_route
    })
  }
}
VueRouter.install = install;
export default VueRouter;

create-matcher.js
addRoutes 是动态添加路由函数,它的参数是路由信息(可以是一个数组),内部会调用 createRouteMap 方法将传入的路由信息格式化成一个路径到记录的映射关系表。

import createRouteMap from './create-router-map';
import { createRoute } from './base';
export default function createMatcher(routes) {
  // pathList保存所有的路由路径,同时获得所有路径及其对应记录的映射关系表
  let { pathList, pathMap } = createRouteMap(routes);

  function match(location) {//通过用户输入的路径获取对应的匹配记录
    let record = pathMap[location];
    return createRoute(record, {
      path:location
    })
  }
  function addRoutes(routes) {//动态添加路由
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes
  }
}

create-router-map.js

需要注意

  1. 将嵌套路由的父路由与当前路由拼接,例路由 /a 的子路由是 /b ,那么 path 就是 /a/b。
  2. 一旦监测到有路由嵌套,就循环递归处理子路由
function addRouteRecord(route, pathList, pathMap, parentRecord) {
  // 将嵌套路由的父路由与当前路由拼接 路由/a的子路由是/b,那么放入path时就是['/a/b']
  let path = parentRecord ? `${parentRecord.path}/${route.path}`: route.path;
  let record = {//当前路由产生一个记录
    path,
    component: route.component,
    parent: parentRecord
  }
  if (!pathMap[path]) {//若映射表中没有当前路径
    pathMap[path] = record;
    pathList.push(path);
  }
  // 将子路由也放到对应的pathmap和pathlist,循环递归处理子路由
  if (route.children) {
    route.foreach(child => {
      addRouteRecord(child, pathList, pathMap, record);
    })
  }
}
// 将所有路径放入字符串,且构造路径记录的映射关系
export default function createRouteMap(routes, oldPathList, oldPathMap) {
  let pathList = oldPathList || [];
  let pathMap = oldPathMap || {};
  // 循环处理当前层路由
  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap);
  });
  return {
    pathList,
    pathMap
  }
}

初始化函数 init

vue-router 有三种模式,分别是 history、hash、abstract,它们的特点如下:

  1. hash:使用URL的 hash 来模拟一个完整的URL,当 hash 值发生改变时,页面不会重新加载,其显示的网络路径中会有 # 号,兼容所有的浏览器和服务器。可以使用 JavaScript 来对 loaction.hash 进行赋值,改变URL的 hash 值; 通过 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

  2. history:美化后的 hash 模式,路径中会去掉 #。依赖于 html5 的历史记录调用栈,所以要担心IE9及以下的版本,并且还包括back、forward、go三个方法,对应浏览器的前进、后退、跳转操作。

    • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
    • 使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
    • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
  3. abstract:适用于所有JavaScript环境,例如服务器端使用Node.js。如果没有浏览器API,路由器将自动被强制进入此模式

接下来就来聊聊这个 init 方法,在VueRouter构造函数内部给对象定义了 history 属性,代表的是路由模式,init 函数内部首先拿到了这个属性的值,可以调用该类的路由处理方法。

因为路由的两种模式有很多同样的处理逻辑,所以我们可以定义一个父类,然后再让两种模式继承该父类,这样就提高了代码的复用能力。父类中构造函数先保存 VueRouter 实例,同时匹配根路径对应的所有记录。然后定义路由跳转方法。

还需注意路由匹配逻辑
由于在构建映射表的时候,一个记录里有它的父级路由,例路由 /a/b/c,先将对应记录放入结果数组,那么它的父级路由就是 /a/b,再将对应记录放入结果数组,并且要放在子路由对应记录的前边,再将父路由 /a 对应记录放入结果数组。
base.js

export const createRoute = (record, location) => {//根据路径记录匹配到的所有组件
  // 存放匹配到的记录
  let matched = [];
  // 如果record存在就将其及其所有父组件存入数组中,父组件存放位置在前
  if (record) {
    while (record) {
      matched.unshift(record);
      record = record.parent;
    }
  }
  return {
    ...location,
    matched
  }
}
export default class Base{
  // 父类初始化
  constructor(router) {
    this.router = router;
      //搜集 '/' 对应的所有记录
    this.current = createRoute(null, {
      path: '/'
    })
  }
  // 路由跳转方法
  transitionTo(location, complete) {
    // 路径变化,currnt更新
    let current = this.router.match(location);
    // 判断本次要跳转的路径是否和当前路径相同
    if (this.current.path === location && this.current.matched.length === current.matched.length) return;
    // 用最新的匹配结果更新视图
    this.current = current;//这个current只是响应式的,它的变化不会更新_route,所以需要在 init方法中手动更新保持数据同步
    // 调用回调函数更新_route
    this.cb && this.cb(current);
  }
  // 记录传入的回调函数,该回调函数目的在于更新_route
  listen(cb) {
    this.cb = cb;
  }
}

接下来说说 hash 子类和 history 子类,首先 hash 子类做的是很简单,就是定义了两个方法获取当前最新的 hash 值,监听 hash 变化的函数供外部调用,

hashHistory.js

import History from './base'
// 判断是否有hash值
const ensureSlash = () => {
  if (window.location.hash) {
    return;
  }
  window.location.hash = '/';
}
// 继承父类
export default class HashHistory extends History{
  constructor(router) {
    super(router);
    this.router = router;
    // 如果使用hash默认如果没有hash应该跳转到首页
    ensureSlash();
  } 
  // 获取当前hash值
  getCurrentLocation() {
    // 得到的hash值带#号所以应该截取一下
    return window.location.hash.slice(1);
  }
  // 定义监听hash变化的函数
  setupHashLisener() {
    window.addEventListener('hashchange', () => {
      // 有变化则跳转路由
      this.transitionTo(this.getCurrentLocation())
    })
  }
}

browsHistory.js
由于 history 模式比较简单。本项目未完善 history 模式子类。

import History from './base'
export default class BrowsHistory extends History {
}

总结

vue-router 的核心功能就是根据路由匹配对应的记录,所以我们就需要在实例化 vue-router 类时调用 createMatcher 方法让他继续处理构建路由到记录的映射表,同时还要返回一个函数用来匹配路由对应路径,最后由于有三种模式可选且所有模式有一些共用的处理逻辑,所以可以声明一个基础类封装一些共用逻辑,再声明三个类继承基础类并且封装各个模式特有的逻辑。到此一个简单的 vue-router 就实现了。

好了,这篇博客就到这里了,我是孤城浪人,一个正在前端路上摸爬滚打的菜鸟,此项目已开源到github。
vue-router原理解析并实现简单的vue-router_第1张图片

你可能感兴趣的:(#,vue,vue.js,vue-ruter)