手写简易 vue-router

200 阅读6分钟

image.png

序章: 1、坦白从宽

不装了,作为一名 21 世纪的无为好青年。学习了一波只有自己能看懂的 vue-router 然后呢,把它以 文章的形式展示出来。一是锻炼自己的写作能力二是.....

1.1、欲练此功,必先 install

古有葵花宝典 xxx,今有 Vue 插件 install。既然选择使用 Vue, 自然要按照规矩办事。install 方法主要做了 以下几点

  1. 混入 beforeCreate
  2. 注册全局组件(页面渲染)
  3. 定义 routerrouter route

2,3 很好理解,那么 mixin beforeCreate 主要做了什么呢?

  1. 赋值根节点
  2. 路由的初始化
  3. 新增响应式数据(响应式更新)

router1.png

let _Vue;
VueRouter.install = function(Vue) {
  _Vue = Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        // 根root
        this._routerRoot = this; // 根
        this._router = this.$options.router; // 路由实例
        this._router.init(this); // 路由初始化
        Vue.util.defineReactive(this, "_route", this._router.history.current); // 新增响应式数据 利用vue的响应式特点,来实现响应式更新
      } else {
        // 非根
        this._routerRoot = this.$parent ? this.$parent._routerRoot : this;
      }
    },
  });

  Vue.component("RouterView", View);
  Vue.component("RouterLink", Link);

  Object.defineProperty(Vue.prototype, "$router", {
    get: function get() {
      return this._routerRoot._router;
    },
  });

  Object.defineProperty(Vue.prototype, "$route", {
    get: function get() {
      return this._routerRoot._route;
    },
  });
};

1.2 '锻'剑重铸之日,VueRouter 归来之时

练剑的功法我有了,接下来自然需要材料。此时我们拿出 2 大材料 routes = [], mode = 'hase'. 小心翼翼的放入 盲盒之中, 只见金光一闪。

我们看到看见了 盲盒(Vuerouter 构造函数)做了 2 件事

  1. 创建匹配器(Matcher),针对 routes 做相应处理(例如 routes 解析, routes 匹配等)
  2. 根据 Mode 使用不同的模式 (主要 监听路由变化, 路由更新等)

router2.png

class VueRouter {
  constructor(options) {
    this.options = options;
    this.mode = options.mode || "hash";
    // this.matcher = createMatcher(options.routes); // 构造器
    switch (this.mode) {
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "history":
        this.history = new HTML5History(this);
        break;
      default:
        break;
    }
  }
  init(app) {
    const history = this.history;
    history.setupListeners(); // 路由监听
    history.listen((route) => {
      // 路由变化的回调函数
      app._route = route;
    });
    history.transitionTo(window.location.hash.slice(1)); // 保证第一次渲染
  }
  match(route) {
    return this.matcher.match(route);
  }
}

class HTML5History {}

class HashHistory {}

export default VueRouter;

1.3 ‘智能’匹配器,更快更稳更高效

当一大堆乱七八糟的材料(routes)进来后,是那么让人不知所措。所以呢,所以呢,我们要 创建一个 匹配器, 用了解析,匹配,查找。

那么流程是啥?

  1. 解析:将 数组形式的 routes 解析成 Map 的形式 (如 {'/home/index': {....}})
  2. 匹配:我们需要根据 当前的 url, 来找到对应的 路由信息(组件信息)
  3. 处理:假设一个场景 父组件:home 子组件: home-child. 此时路由匹配到了 home-child, 我们也找到了 home-child 并将其展示在页面中,那么问题来了 home 的 内容丢失了, 这显然不是我们想看到的结果。因此,需要处理,找到所有节点, 存储在 matched 中

image.png

/**
 *
 * @param {*} routes 路由数组
 */
function createMatcher(routes) {
  const pathList = [];
  const pathMap = {};
  const nameMap = {};
  routes.forEach((route) => {
    addRouteRecord(pathList, pathMap, nameMap, route);
  });
  function match(route) {
    const target = typeof route === "string" ? { path: route } : route;
    const { name, path } = target;
    let record;
    if (name) {
      record = nameMap[name];
    } else if (path) {
      record = pathMap[path];
    }
    if (record) {
      return createRoute(record, target);
    }
    return createRoute(null, target);
  }

  function createRoute(record, location) {
    var route = {
      name: location.name || (record && record.name),
      path: location.path || "/",
      component: record ? record.component : "",
      matched: record ? formatMatch(record) : [],
    };
    return Object.freeze(route);
  }
  return {
    match,
  };
}

// 找到所有匹配的路由
function formatMatch(record) {
  var res = [];
  while (record) {
    res.unshift(record);
    record = record.parent;
  }
  return res;
}

/**
 *
 * @param {*} pathList route list
 * @param {*} pathMap route map
 * @param {*} nameMap name map
 * @param {*} route route
 * @param {*} parent 父级
 */
function addRouteRecord(pathList, pathMap, nameMap, route, parent) {
  const { path, name, component, children } = route;
  const normalizedPath = normalizePath(path, parent);
  const record = {
    path: normalizedPath,
    component,
    name,
    parent,
  };
  // 存在子路由
  if (children) {
    children.forEach((child) => {
      addRouteRecord(pathList, pathMap, nameMap, child, record);
    });
  }

  if (!pathMap[normalizedPath]) {
    pathList.push(normalizedPath);
    pathMap[normalizedPath] = record;
  }

  if (name) {
    if (!nameMap[normalizedPath]) {
      nameMap[normalizedPath] = record;
    }
  }
}

/**
 *
 * @param {*} path 路径
 * @param {*} parent 父级
 */
function normalizePath(path, parent) {
  if (parent == null) {
    return path;
  }
  return cleanPath(parent.path + "/" + path);
}

/**
 * 多余的// 替换
 * @param {*} path 路径
 * @returns
 */
function cleanPath(path) {
  return path.replace(/\/\//g, "/");
}

1.4 以前我没得选,现在我选择 xxx

vue-router 的模式有三种(hash,history,abstract).

正所谓'后宫佳丽三千人,三千宠爱在一身', 尽管有三种模式,但我独爱 hash. 所以这里就以 hash 作为 例子。

那么 hash 作为 vue-router 的 重要模式之一。 它做了什么呢?

  1. 监听路有变化(利用 浏览器 提供的 hashchange)
  2. 路由跳转 (匹配找到当前 路由 对应的路由)
  3. 路由更新
  4. 触发回调

}~LT@5GIL8RWYIS1W_Q7GKU.png

// hash 模式的
// 主要提供 hash的路由跳转的相关操作
class HashHistory {
  constructor(router) {
    this.router = router;
    this.current = {
      path: "/",
    };
    ensureSlash();
  }

  // 监听
  setupListeners() {
    window.addEventListener("hashchange", () => {
      // 传入当前url的hash
      this.transitionTo(window.location.hash.slice(1));
    });
  }

  // 回调
  listen(cb) {
    this.cb = cb;
  }

  // 路由跳转
  transitionTo(location) {
    const route = this.router.match(location); // 寻找
    this.updateRoute(route);
  }

  // 路由更新
  updateRoute(route) {
    this.current = route;
    this.cb && this.cb(route);
  }
}

function ensureSlash() {
  if (window.location.hash) {
    return;
  }
  window.location.hash = "/";
}

export default HashHistory;

1.5 温故而知新

简单来说, vue-router 可以分为 3 大块

  1. install
  2. history
  3. matcher

通过 vue.use 调用 install 声明 赋值 变量, history 监听路由变化,触发更新, matcher 匹配查找 对应路由, 从而更改 _route 值, 触发视图更新!!!!

1.6 人靠衣装马靠鞍,酒香也怕巷子深

纵使你做的再多再好,不显示出来, 别人也是看不到的。所以需要 router-view 组件 用来渲染

image.png

const View = {
  name: "RouterView",
  functional: true,
  props: {
    name: {
      type: String,
      default: "default",
    },
  },
  render: function render(h, { props, children, parent, data }) {
    // used by devtools to display a router-view badge
    data.routerView = true;
    let route = parent.$route || {};
    let depth = 0;
    // 存在父组件
    while (parent && parent._routerRoot !== parent) {
      var vnodeData = parent.$vnode ? parent.$vnode.data : {};
      // 且带view组件
      if (vnodeData.routerView) {
        depth++;
      }
      parent = parent.$parent;
    }
    const matched = route.matched[depth];
    const component = matched && matched.component;
    if (component) {
      return h(component, data);
    } else {
      return h();
    }
  },
};

1.7 想不到啥名了, 那就无名吧

相比较 router-view 组件 router-link 显得就没有 那么秀了, 说简单点, 类似 标签

const Link = {
  name: "RouterLink",
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  render: function render(h) {
    return h(
      "a",
      {
        domProps: {
          href: "#" + this.to,
        },
      },
      [this.$slots.default]
    );
  },
};

1.8 完整版本

在这里强调一个点(虽然前面已经说过), 也就是 createRoute matched, 一开始再 路由匹配的时候, 并没有记录 。也就是在 router-view 渲染时, 直接查找

path 所对应的 component, 并直接渲染。 也就导致 1. 死循环 2. 无法渲染父组件

// 仅最基本功能,暂不考虑 query等
class VueRouter {
  constructor(options) {
    this.$options = options;
    const { routes = [], mode = "hash" } = options;
    this.mode = mode; // 模式
    this.matcher = new createMatcher(routes); // 匹配器
    switch (mode) {
      case "hash":
        this.history = new HashHistory(this); // history
        break;
      case "history":
        break;
    }
  }

  init(app) {
    // 初始化
    this.history.setupListeners(); // 设置监听器
    this.history.listen(route => {
      // 设置路由变化的回调函数
      app._route = route;
    });
    this.history.transitionTo(window.location.hash.slice(1)); // 保证第一次渲染
  }

  // 匹配
  match(route) {
    return this.matcher.match(route);
  }
}

// 匹配器
class createMatcher {
  constructor(routes) {
    this.pathList = [];
    this.pathMap = {};
    this.nameMap = {};
    this.init(routes);
  }
  // 初始化
  init(routes) {
    const pathList = this.pathList;
    const pathMap = this.pathMap;
    const nameMap = this.nameMap;
    routes.forEach(route => {
      this.addRouteRecord(pathList, pathMap, nameMap, route);
    });
  }

  // 根据name 或者 path 找 recode, 然后返回相应route
  match(route) {
    const target = typeof route === "string" ? { path: route } : route;
    const { name, path } = target;
    let record;
    if (name) {
      record = this.nameMap[name];
    } else if (path) {
      record = this.pathMap[path];
    }

    if (record) {
      return this.createRoute(record, target);
    }
    return this.createRoute(null, target);
  }

  // 返回路由 name path component matched 匹配到的所有组件
  createRoute(record, current) {
    const route = {
      name: current.name || (record && record.name),
      path: current.path || "/",
      component: record ? record.component : "",
      matched: record ? this.formatMatch(record) : []
    };
    return Object.freeze(route);
  }

  // 匹配所有 祖宗路由
  formatMatch(record) {
    const res = [];
    while (record) {
      res.unshift(record);
      record = record.parent;
    }
    return res;
  }
  /**
   *
   * @param {*} pathList pathlist
   * @param {*} pathMap pathMap
   * @param {*} nameMap nameMap
   * @param {*} route 路由
   * @param {*} parent 父路由
   */
  addRouteRecord(pathList, pathMap, nameMap, route, parent) {
    const { path, name, component, children } = route;
    const normalizedPath = this.normalizedPath(path, parent);
    const record = {
      path: normalizedPath,
      name,
      parent,
      component
    };

    // 存在子元素
    if (children) {
      children.forEach(child => {
        this.addRouteRecord(pathList, pathMap, nameMap, child, route);
      });
    }

    if (!pathMap[normalizedPath]) {
      // 不存在,添加
      pathMap[normalizedPath] = record;
      pathList.push(normalizedPath);
    }

    if (name && !nameMap[normalizedPath]) {
      nameMap[normalizedPath] = record;
    }
  }

  /**
   * 拼接path
   * @param {*} path 路径
   * @param {*} parent 父级
   */
  normalizedPath(path, parent) {
    if (!parent) {
      return path;
    }
    return this.cleanPath(parent.path + "/" + path);
  }

  /**
   * 多余的// 替换
   * @param {*} path
   */
  cleanPath(path) {
    return path.replace(/\/\//g, "/");
  }
}

// hash
class HashHistory {
  constructor(router) {
    this.router = router;
  }

  // 监听后的回调函数
  listen(cb) {
    this.cb = cb;
  }

  // 监听
  setupListeners() {
    window.addEventListener("hashchange", () => {
      this.transitionTo(window.location.hash.slice(1));
    });
  }

  // 路由跳转
  transitionTo(location) {
    const route = this.router.match(location);
    this.updateRoute(route);
  }

  // 路由更新
  updateRoute(route) {
    this.current = route;
    this.cb && this.cb(route);
  }
}

const View = {
  name: "routerView",
  functional: true,
  prors: {
    name: {
      type: String,
      default: "default"
    }
  },
  render(h, { parent, data }) {
    data.routerView = true;
    const { $route: route } = parent; // 指向history current 也就是当前路由
    let depth = 0; // 深度
    while (parent && parent._routerRoot !== parent) {
      // 存在且非根
      const vnodeData = parent.$vnode ? parent.$vnode.data : {};
      if (vnodeData.routerView) {
        // 带 routerView
        depth++;
      }
      parent = parent.$parent; // 继续找父级
    }
    const matched = route.matched[depth];
    const component = matched && matched.component;
    if (component) {
      return h(component, data);
    } else {
      return h();
    }
  }
};

const Link = {
  name: "RouterLink",
  props: {
    to: {
      type: String,
      required: true
    }
  },
  render(h) {
    return h(
      "a",
      {
        domProps: {
          href: "#" + this.to
        }
      },
      [this.$slots.default]
    );
  }
};

let _Vue;
VueRouter.install = function(Vue) {
  _Vue = Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        // 根
        this._routerRoot = this; // router 根, 利用获取 router route
        this._router = this.$options.router;
        this._router.init(this); // 初始化, 监听路由等
        Vue.util.defineReactive(this, "_route", this._router.history.current); // 响应式数据
      } else {
        this._routerRoot = this.$parent?._routerRoot || this;
      }
    }
  });

  Vue.component("routerView", View);
  Vue.component("routerLink", Link);

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

  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route;
    }
  });
};

export default VueRouter;