The Future Depends on You
首页/Vue/手写 Vue-router 核心功能/
手写 Vue-router 核心功能
上次更新时间:2021-1-22 文章分类:Vue 阅读人数:16

目录结构

|--vue-router

  |--components
    |--router-link.js
    |--router-view.js

  |--history
    |--base.js
    |--browsorHistory.js
    |--hashHistory.js

  |--create-matcher.js
  |--create-route-map.js
  |--index.js
  |--install.js

初始化流程

  1. VueRouter 类的构造函数将用户输入的 routes 树形结构扁平化处理;
  2. 根据用户配置的模式初始化 history 实例(hash模式或者history模式,默认hash模式);
  3. 添加 VueRouter.install 方法,编写 Vue 插件需要提供的一个方法;
  4. 调用 init 方法监听路径;

扁平化处理

首先在 VueRouter 类的构造函数中将用户输入的 routes 树形结构扁平化处理。即:

let routes = [
  {
    path: '/',
    component: Home,
  },
  {
    path: '/about',
    component: About,
    children: [
      {
        path: 'a',
        component: About_a
      },
      {
        path: 'b',
        component: About_b
      }
    ]
  }
]

转化成:

/: {path: "/", component: {Home}, parent: undefined}
/about: {path: "/about", component: {About}, parent: undefined}
/about/a: {path: "/about/a", component: {About_a}, parent: {About}}
/about/b: {path: "/about/b", component: {About_b}, parent: {About}}

在 vue-router -> create-matcher.js 中调用 createRouteMap 方法进行扁平化处理,同时添加动态添加路由的方法。

import { createRouteMap } from './create-route-map';

export default function createMatcher(routes) {

  // routes 是用户自己匹配的 但是用起来不方便

  // pathList 会把所有的路由 组成一个数组 ['/', '/about', '/about/a', '/about/b']
  // pathMap {/: {}, /about: {}, /about/a: {}}
  let { pathList, pathMap } = createRouteMap(routes);

  // 动态添加路由
  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap)
  }

  return {
    match,
    addRoutes
  }
}

vue-router -> create-route-map.js

const addRouteRecord = (route, pathList, pathMap, parentRoute) => {
  // 根据当前路由产生一个记录 path/component
  let path = parentRoute ? `${parentRoute.path}/${route.path}` : route.path;
  let record = {
    path,
    component: route.component,
    parent: parentRoute
  }

  // 防止用户重复编写路由
  if (!pathMap[path]) {
    pathMap[path] = record;
    pathList.push(path);
  }

  // 将子路由也放到对应的pathMap和pathList中
  if (route.children) {
    route.children.forEach(r => {
      addRouteRecord(r, pathList, pathMap, route);
    })
  }
}

export function createRouteMap(routes, oldPathList, oldPathMap) {

  let pathList = oldPathList || [];
  let pathMap = oldPathMap || {};

  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap);
  });
  return {
    pathList,
    pathMap
  }
}

初始化history实例

根据用户配置的 mode 初始化实例

import HashHistory from './history/hashHistory';
import BrowserHistory from './history/browsorHistory';

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

switch (this.mode) {
  case "hash":
    this.history = new HashHistory(this)
    break;
  case 'history':
    this.history = new BrowserHistory(this)
    break;
}

其中HashHistoryBrowserHistoryHistory 类中继承了一些公共方法。

History 类中 transitionTo 方法用于在路径发生变化后匹配路径和更新组件的作用。createRoute 方法返回 与当前理由相匹配的全部路由。即:

vue-router -> history -> base.js

// 根据匹配到的记录 计算匹配到的所有记录
export const createRoute = (record, location) => {
  let matched = [];

  if (record) {
    while (record) {
      matched.unshift(record);
      // 通过当前的记录找到所有的父亲
      record = record.parent;
    }
  }

  return {
    ...location,
    matched
  }
}

// 这个current就是一个普通的变量 this.current ? 希望current变化了可以更新视图
export default class History {
  constructor(router) {
    this.router = router;

    // current 代表当前路径匹配的记录
    // / {path: '/', component:home}
    // /about/a {path: '/about', component: about}, {path: '/about/A', component: A}
    this.current = createRoute(null, {
      path: '/'
    })
    // this.current = {path: '/', matched: []}
  }

  transitionTo(location, complete) {
    // 获取当前路径匹配对应的记录,当路径变化时获取对应的记录 => 渲染页面(router-view实现)

    // 通过路径拿到对应的记录,有了记录之后就可以找到对象的匹配
    let current = this.router.match(location);

    console.log('current', current)
    // 防止重复点击 不需要再次渲染
    // 匹配到的个数和路径都是相同的 就不需要再次跳转了
    if (this.current.path == location && this.current.matched.length === current.matched.length) {
      return;
    }

    // 用最新的匹配到的结果,去更新视图
    // 这个current只是响应式的,他的变化不会更新 _route
    this.current = current;
    this.cb && this.cb(current);

    // 当路径变化后,current属性会进行更新操作
    complete && complete();
  }
  // 保存回调函数
  listen(cb) {
    this.cb = cb;
  }
}

初始化 BrowserHistory:

vue-router -> history -> browsorHistory.js

import History from './base';

export default class BrowserHistory extends History {

  constructor(router) {
    super(router);
    this.router = router;
  }

  getCurrentLocation() {
    return window.location.pathname;
  }

  setupListener() {
    // 监听路由的前后跳转
    window.addEventListener('popstate', () => {
      this.transitionTo(this.getCurrentLocation());
    })
  }
}

初始化 HashHistory:

vue-router -> history -> hashHistory.js

import History from './base';

const ensureSlash = () => {
  if (window.location.hash) {
    return;
  }
  window.location.hash = '/'
}

export default class HashHistory extends History {
  constructor(router) {
    super(router);
    this.router = router;

    // 如果使用hashHistory 默认如果没有hash 应该跳转到 首页 #/
    ensureSlash();
  }

  getCurrentLocation() {
    return window.location.hash.slice(1); // #/about -> /about
  }

  setupListener() {
    window.addEventListener('hashchange', () => {
      // 再次执行匹配操作
      this.transitionTo(this.getCurrentLocation())
    })
  }
}

install 方法

在用户使用 Vue.use(VueRouter) 时,会调用 VueRouter.install 方法。

在 install 中

  1. 注册两个全局组件 router-linkrouter-view
  2. 使用 Vue.mixin 给每个组件添加 beforeCreate 生命周期函数,使用 Vue.util.defineReactive 将路由变量声明为响应式数据;
  3. 在 Vue 的原型上添加 $route$router,供使用者调用。

vue-router -> install.js

import routerLink from './components/router-link';
import routerView from './components/router-view';

export let Vue;

const install = function (_Vue) {
  // install 方法内部一般会用他来定义一些全局的内容:指令、全局组件、给原型扩展方法
  Vue = _Vue;

  Vue.component('router-link', routerLink);
  Vue.component('router-view', routerView);

  // 用户将router属性注册到了new Vue
  // 希望每个子组件 都可以获取到router属性
  Vue.mixin({
    // mixin 可以给beforeCreate 这个生命周期增加合并的方法
    beforeCreate() {
      // 如果有 router 说明在根实例上增加了router 当前这个实例是根实例
      // 渲染流程先父后子,渲染完毕 先子后父
      if (this.$options.router) {
        // 根
        this._routerRoot = this; // 将当前根实例放到了 _routerRoot
        this._router = this.$options.router;
        // 当前用户的router属性
        this._router.init(this); // 调用插件中的init 方法
        // 如果用户更改了 current 是没有效果的, 需要把 _route 也进行更新
        Vue.util.defineReactive(this, "_route", this._router.history.current)
      } else {
        // 子
        // 这里所有的组件都拥有了 this._routerRoot 属性
        this._routerRoot = this.$options && this.$parent._routerRoot;
      }
    }
  });

  Object.defineProperty(Vue.prototype, '$route', { // 存放的都是属性 path matched
    get() {
      // 取current
      return this._routerRoot && this._routerRoot._route;
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot && this._routerRoot._router;
    }
  })
}

export default install;

vue-router -> components -> router-link.js

export default {
  // this 指代的是当前组件(插槽 分为具名插槽和默认插槽)
  name: 'router-link',
  props: {
    to: {
      type: String,
      required: true
    },
    tag: {
      type: String
    }
  },
  methods: {
    // 指定跳转的方法
    clickHandler() {
      // 调用$router 中的 push 方法进行跳转
      this.$router.push(this.to);
    }
  },
  render(h) {
    let tag = this.tag || 'a';
    return h(tag, {
      on: {
        click: this.clickHandler
      }
    }, this.$slots.default)
  }
};

vue-router -> components -> router-view.js

export default {
  name: 'router-view',
  functional: true, // 函数式组件 函数不用new 没有this 没有生命周期 没有data
  render(h, { parent, data }) {
    // this.$route 有matched属性 这个属性有几个就依次将它赋值到对应的router-view上
    // parent 是当前父组件
    // data 是这个组件上的一些表示

    let route = parent.$route;
    let depth = 0;

    data.routerView = true; // 标识路由属性

    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      parent = parent.$parent;
    }

    let record = route.matched[depth];

    if (!record) {
      // 渲染一个空元素
      return h();
    }
    return h(record.component, data)
  }
}

init 方法

init 方法调用 history.transitionTo 监听路由的变化 并改变响应式数据 current 触发组件更新。

init(app) {
  // 需要根据用户配置,做出一个映射表
  // 需要根据当前路径 实现页面跳转的逻辑
  console.log('app', app)
  const history = this.history;

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

  // transitionTo 跳转逻辑 hash、browser都有
  // getCurrentLocation hash 和 browser实现不一样
  // setupListener hash 监听
  let setupHashListener = () => {
    history.setupListener(); // hashchange
  }

  // 跳转路径 进行监控 根据路径获取对应的记录
  history.transitionTo(history.getCurrentLocation(), setupHashListener);

  // 初始化时 都需要调用更新 _route的方法
  // 只要current发生变化 就触发此函数
  history.listen((route) => {
    // 更新视图的操作 当current变化后再次更新 _route属性
    app._route = route;
  });
}

路由监听

当用户点击 router-link 时,调用 this.$router.push(this.to) 。在 push 方法中,我们将 hash 模式和 history 模式分开讨论。

vue-router -> index.js

push(location) {
  const history = history;
  if (this.mode === 'hash') {
    window.location.hash = location;
  }
  else if (this.mode === 'history') {
    window.history.pushState({}, "", location);
    this.history.transitionTo(location)
  }
}

生命周期

生命周期是路由中的一个重要环节。在这里我们实现一下 beforeEach

将所有的 beforeEach 保存下来:

vue-router -> index.js

beforeEach(fn) { this.beforeHooks.push(fn); } 在 transitionTo 中处理

transitionTo(location, complete) {

  let current = this.router.match(location);
  // ----- 添加部分
  // 需要调用 钩子函数
  let queue = this.router.beforeHooks;
  // -----
  if (this.current.path == location && this.current.matched.length === current.matched.length) {
    return;
  }
  // ----- 添加部分
  const iterator = (hook, next) => {
    hook(current, this.current, next)
  }
  // -----
  runQueue(queue, iterator, () => {
    this.current = current;
    this.cb && this.cb(current);
    complete && complete();
  });
}

// ----- 添加部分
const runQueue = (queue, iterator, complete) => {
  function next(index) {
    if (index >= queue.length) {
      return complete();
    }
    let hook = queue[index]
    iterator(hook, () => {
      next(index + 1)
    });
  }
  next(0)
}
// -----

测试

import Vue from 'vue';
import VueRouter from '../vue-router/index';

import Home from '../views/home.vue';
import About from '../views/about.vue';
import About_a from '../views/about_a.vue';
import About_b from '../views/about_b.vue'

Vue.use(VueRouter);

let routes = [
  {
    path: '/',
    component: Home,
  },
  {
    path: '/about',
    component: About,
    children: [
      {
        path: 'a',
        component: About_a
      },
      {
        path: 'b',
        component: About_b
      }
    ]
  }
]
let router = new VueRouter({
  routes,
  mode: '设置模式'
});
export default router;

hash 模式:

yctang

history 模式:
yctang

源码地址:https://github.com/Tangyincheng/vur-router-core

  • 完结.