导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


插件注册

代码结构

.
├── index.js
└── install.js

插件需要实现 install 方法,Vue.use() 时需要调用。

index.js

import install, { Vue } from "./install";

class VueRouter {
    constructor (options) {
        // 用户传递的路由配置, 需要对这个配置进行路由映射
        let routes = options.routes;
    }
}

VueRouter.install = install;

export default VueRouter;

install.js

export let Vue;

function install(_Vue) {
    Vue = _Vue; // 将传入的 Vue 的构造函数变为全局的
    Vue.mixin({ // mergeOptions 所有组件初始化都回采用这个方法
        beforeCreate() {
            // 只有 new Vue 的时候的那个根实例才有 router, 根据这个判断是不是根实例
            // 组件渲染是从父到子的
            if (this.$options.router) {
                // 根实例上才传递了 router
                this._routerRoot = this; // 根实例
                this._router = this.$options.router;
            } else {
                this._routerRoot = this.$parent && this.$parent._routerRoot;
            }
            // 这样在组件中都可以用 this._routerRoot._router 拿到 router 实例
        }
    });
    // 通过 defineProperty 而不是 Vue.prototype.$router 去赋值的原因是, 如果有多个 new Vue , 只会给使用了 vue-router 的实例返回 $router; 没有使用的不会有返回值
    Object.defineProperty(Vue.prototype, '$router', {
        get() {
            return this._routerRoot && this._routerRoot._router;
        }
    });

    Vue.component('router-link', {
        render() {
            return <a>{ this.$slots.default }</a>
        }
    });

    Vue.component('router-view', {
        render() {
            return <div>empty</div>
        }
    });
}

export default install;

路由管理

思路:

路由管理工具

导出三个方法分别是 addRoutesaddRoutematch

create-matcher.js

import createRouteMap from "./create-route-map";

function createMatcher(routes) {
  const { pathMap } = createRouteMap(routes);
  function addRoutes(routes) { // 动态添加路由
      createRouteMap(routes, pathMap);
  }
  function addRoute(route) {
      createRouteMap([route], pathMap);
  }
  function match(location) {
      return pathMap[location];
  }
  return {
      addRoutes, // 添加多个路由
      addRoute,  // 添加单个路由
      match,     // 根据路径返回对应路由
  }
}

export default createMatcher;

路由映射工具

将用户配置的 routes 树形路由表,递归的转为路径和记录匹配的路由映射表。

create-route-map.js

function addRouteRecord(route, pathMap, parentRecord) {
  // 有父组件就拼上父组件path
  let path = parentRecord ? `${parentRecord.path === '/' ? '/' : `${parentRecord.path}/`}${route.path}` : route.path;
  let record = {
      path,
      component: route.component,
      props: route.props,
      meta: route.meta,
  };
  if (!pathMap[path]) {
      // 维护路径对应的属性
      pathMap[path] = record; // 路径和记录对应起来
  }
  route.children && route.children.forEach(childRoute => {
      addRouteRecord(childRoute, pathMap, record)
  });
}

export default function createRouteMap(routes, pathMap) { // 根据用户options扁平化路由信息
  pathMap = pathMap || {};

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

  return {
      pathMap,
  }
}

使用路由管理器

index.js 中使用 matcher 路由管理器。创建和管理路由。

index.js

import install, { Vue } from "./install";
import createMatcher from "./create-matcher";

class VueRouter {
    constructor (options) {
        // 用户传递的路由配置, 需要对这个配置进行路由映射
        let routes = options.routes;
        this.matcher = createMatcher(routes);
    }
}

VueRouter.install = install;

export default VueRouter;

路由模式

根据不同的模式创建对应的路由系统。

index.js

import install, { Vue } from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import BrowserHistory from "./history/history";

class VueRouter {
    constructor (options) {
        // 用户传递的路由配置, 需要对这个配置进行路由映射
        let routes = options.routes;
        this.matcher = createMatcher(routes);
        
        // 根据不同的模式创建对应的路由系统
        const mode = options.mode || 'hash';
        switch (mode) {
            case 'hash':
                this.history = new HashHistory(this);
                break;
            case 'history':
                this.history = new BrowserHistory(this);
                break;
            default:
                break;
        }
    }
}

VueRouter.install = install;

export default VueRouter;

创建单独的 history 文件夹维护路由模式,继承基类 base ,公共放 base ,特性放子类实现。

.
├── base.js     # 基类
├── hash.js     # hash 模式
└── history.js  # history 模式

基类 Base

history/base.js

class History {
  constructor(router) {
    this.router = router;
  }
}

export default History;

HashHistory

初始化 hash 路由的时候,要给定一个默认的 hash 路径 /

history/hash.js

import Base from "./base";

function ensureSlash() {
  if (window.location.hash) {
    return true;
  }
  window.location.hash = '/'; // 没有 hash , 就默认一个 / 
}

class HashHistory extends Base {
  constructor(router) {
    super(router);

    // 初始化 hash 路由的时候,要给定一个默认的 hash 路径 /
    ensureSlash();
  }
}

export default HashHistory;

BrowerHistory

history/history.js

import Base from "./base";

class BrowserHistory extends Base {
  constructor(router) {
    super(router);
  }
}

export default BrowserHistory;

监听路由变化

初始化路由

install.js

install 方法中的 beforeCreate 中初始化路由。

export let Vue;

function install(_Vue) {
    Vue = _Vue; // 将传入的 Vue 的构造函数变为全局的
    Vue.mixin({ // mergeOptions 所有组件初始化都回采用这个方法
        beforeCreate() {
            // 只有 new Vue 的时候的那个根实例才有 router, 根据这个判断是不是根实例
            // 组件渲染是从父到子的
            if (this.$options.router) {
                // 根实例上才传递了 router
                this._routerRoot = this; // 根实例
                this._router = this.$options.router;
                this._router.init(this); // this 就是我们整个应用的根实例
            } else {
                this._routerRoot = this.$parent && this.$parent._routerRoot;
            }
            // 这样在组件中都可以用 this._routerRoot._router 拿到 router 实例
        }
    });
    // 通过 defineProperty 而不是 Vue.prototype.$router 去赋值的原因是, 如果有多个 new Vue , 只会给使用了 vue-router 的实例返回 $router; 没有使用的不会有返回值
    Object.defineProperty(Vue.prototype, '$router', {
        get() {
            return this._routerRoot && this._routerRoot._router;
        }
    });

    Vue.component('router-link', {
        render() {
            return <a>{ this.$slots.default }</a>
        }
    });

    Vue.component('router-view', {
        render() {
            return <div>empty</div>
        }
    });
}

export default install;

首次和后续监听路由变化

index.js

init 中先针对当前路径渲染首次加载时的组件;然后监听路由变化渲染之后组件。

import install, { Vue } from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import BrowserHistory from "./history/history";

class VueRouter {
    constructor (options) {
        // 用户传递的路由配置, 需要对这个配置进行路由映射
        let routes = options.routes;
        this.matcher = createMatcher(routes);
        
        // 根据不同的模式创建对应的路由系统
        const mode = options.mode || 'hash';
        switch (mode) {
            case 'hash':
                this.history = new HashHistory(this);
                break;
            case 'history':
                this.history = new BrowserHistory(this);
                break;
            default:
                break;
        }        
    }
    match(location) {
        return this.matcher.match(location);
    }
    push(location) {
        this.history.transitionTo(location);
    }
    init(app) {
        let history = this.history;
        // 根据路径的变化匹配对应的组件来进行渲染,路径变化了,需要更新视图(所以得是响应式的)

        // 在监听之前,先得根据路径匹配到对应的组件渲染,之后监听路由变化
        history.transitionTo(history.getCurrentLocation(), () => {
            history.setupListener(); // 监听路由的变化
        });
    }
}

VueRouter.install = install;

export default VueRouter;

history/base.js

class History {
  constructor(router) {
    this.router = router;
  }
  transitionTo(location, listener) {
    let record = this.router.match(location);
    console.log(record);
    // 当路由切换的时候。也应该调用 transitionTo 方法,再次拿到新的记录

    listener && listener();
  }
}

export default History;

history/hash.js

import Base from "./base";

function ensureSlash() {
  if (window.location.hash) {
    return true;
  }
  window.location.hash = '/'; // 没有 hash , 就默认一个 / 
}
function getHash() {
  return window.location.hash.slice(1);
}

class HashHistory extends Base {
  constructor(router) {
    super(router);

    // 初始化 hash 路由的时候,要给定一个默认的 hash 路径 /
    ensureSlash();
  }
  setupListener() { // 稍后需要调用此方法,监控 hash 值的变化
    window.addEventListener('hashchange', function() {
      console.log(getHash());
    });
  }
  getCurrentLocation() {
    return getHash();
  }
}

export default HashHistory;

history/history.js

import Base from "./base";

class BrowserHistory extends Base {
  constructor(router) {
    super(router);
  }
  setupListener() { // 稍后需要调用此方法,监控 hash 值的变化
    window.addEventListener('popstate', function() {
      console.log(window.location.pathname);
    });
  }
  getCurrentLocation() {
    return window.location.pathname;
  }
}

export default BrowserHistory;

指定路径更改 hash & hash 更改重新渲染

我们需要在主动更改路由后更改 hash ,也需要在 hash 变化后重新渲染对应页面。(双向操作)

index.js

import install, { Vue } from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import BrowserHistory from "./history/history";

class VueRouter {
    constructor (options) {
        // 用户传递的路由配置, 需要对这个配置进行路由映射
        let routes = options.routes;
        this.matcher = createMatcher(routes);
        
        // 根据不同的模式创建对应的路由系统
        const mode = options.mode || 'hash';
        switch (mode) {
            case 'hash':
                this.history = new HashHistory(this);
                break;
            case 'history':
                this.history = new BrowserHistory(this);
                break;
            default:
                break;
        }        
    }
    match(location) {
        return this.matcher.match(location);
    }
    push(location) {
        this.history.transitionTo(location, () => {
            // 指定路径后更改 hash
            window.location.hash = location;
        });
    }
    init(app) {
        let history = this.history;
        // 根据路径的变化匹配对应的组件来进行渲染,路径变化了,需要更新视图(所以得是响应式的)

        // 在监听之前,先得根据路径匹配到对应的组件渲染,之后监听路由变化
        history.transitionTo(history.getCurrentLocation(), () => {
            history.setupListener(); // 监听路由的变化
        });
    }
}

VueRouter.install = install;

export default VueRouter;

history/hash.js

import Base from "./base";

function ensureSlash() {
  if (window.location.hash) {
    return true;
  }
  window.location.hash = '/'; // 没有 hash , 就默认一个 / 
}
function getHash() {
  return window.location.hash.slice(1);
}

class HashHistory extends Base {
  constructor(router) {
    super(router);

    // 初始化 hash 路由的时候,要给定一个默认的 hash 路径 /
    ensureSlash();
  }
  setupListener() { // 稍后需要调用此方法,监控 hash 值的变化
    window.addEventListener('hashchange', () => {
      // hash 变化后需要重新跳转
      this.transitionTo(getHash());
    });
  }
  getCurrentLocation() {
    return getHash();
  }
}

export default HashHistory;

一个路径可能对应不止一个组件

例如 /about/b 其实等同于 /about + /b,我们需要根据匹配的记录找到所有的组件, 根据不同的组件渲染到不同的 router-view, 所以可能一个路由 path 匹配的不止一个组件。

为此我们需要维护一个 matched 数组用于记录当前路由的所有匹配到的记录。

首先在路由映射中维护父子关系:

create-route-map.js

function addRouteRecord(route, pathMap, parentRecord) {
  // 有父组件就拼上父组件path
  let path = parentRecord ? `${parentRecord.path === '/' ? '/' : `${parentRecord.path}/`}${route.path}` : route.path;
  let record = {
      path,
      component: route.component,
      props: route.props,
      meta: route.meta,
      parent: parentRecord,
  };
  if (!pathMap[path]) {
      // 维护路径对应的属性
      pathMap[path] = record; // 路径和记录对应起来
  }
  route.children && route.children.forEach(childRoute => {
      addRouteRecord(childRoute, pathMap, record)
  });
}

export default function createRouteMap(routes, pathMap) { // 根据用户options扁平化路由信息
  pathMap = pathMap || {};

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

  return {
      pathMap,
  }
}

然后在基类 base 中维护一个当前路由记录的对象 current 。初始化和路径变化时都需要动态计算。

history/base.js

function createRoute(record, location) {
  let matched = [];
  if (record) {
    while (record) {
      matched.unshift(record);
      record = record.parent;
    }
  }
  return {
    ...location,
    matched,
  }
}

class History {
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, {
      path: '/'
    });
  }
  transitionTo(location, listener) {
    let record = this.router.match(location);

    // e.g. /about/b = /about + /b: 需要根据匹配的记录找到所有的组件, 根据不同的组件渲染到不同的 router-view, 所以可能一个路由 path 匹配的不止一个组件
    let route = createRoute(record, { path: location });
    
    // 如果路径和匹配都一样,当重复处理
    if (location === this.current.path && route.matched.length === this.current.matched.length) {
      return;
    }
    this.current = route; // 更新当前 current 对象
    console.log(this.current);
    
    // 当路由切换的时候。也应该调用 transitionTo 方法,再次拿到新的记录

    listener && listener();
  }
}

export default History;

现在我们每次切换页面或者刷新页面或者点击浏览器历史记录左右键都能正确更新 current 当前路由对象了。但我们希望 current 变化后能够出发页面的重新渲染,所以我们需要 current 是个响应式的值。

响应式 $route