vueRouter 简易实现 | 青训营笔记

75 阅读6分钟

这是我参与「第四届青训营」笔记创作活动的第6天

仿写vueRouter

vueRoutre 简单架构

--vue-router
  - createMatcher.js
  - index.js
  - install.js
  
install.js 是 vue 的插件注册方法

install.js

通过 Vue.mixin给全局所有组件实例 注入 beforeCreate

// 安装插件, 这个插件依赖于 vue
export let _Vue;
export default function install(Vue) {
  _Vue = Vue;
  Vue.mixin({
    beforeCreate() {
      // 根实例
      if (this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;

        //
        this._router.init(this);
      } else {
        // 父组件的实例
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
}

vueRouter 核心 Index.js

主要就是一个 createMatcher 方法, 它返回两个方法

  • match: 负责匹配路径
  • addRoutes: 负责动态添加路由
export default class VueRouter {
  constructor(options) {
    // 1. 根据不同的路径,跳转不同的页面
    // 将用户传递的 routes 转成用户好维护的结构
    // match  负责匹配路径
    // addRoutes 动态添加
    this.matcher = createMatcher(options.routes || []);
  }

  init(app) {
    // app 指代的是根实例
  }
}

// 默认会调用install方法
VueRouter.install = install;

createMatcher

这里面是用 createRouteMap 结合路由对象 生成 pathListpathMap.

之后的 addRoutes 则是通过调用 这个方法, 把新加的配置处理后加到原对象上去

export default function createMatcher(routes) {
  // 核心: 扁平化, 创建路由映射表

  // [/,/about/a,/about/b]
  // {/: , /about: ,}

  let { pathList, pathMap } = createRouteMap(routes); // 初始化配置

  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap); // 添加新配置
  }

  function match() {}

  return {
    match,
    addRoutes,
  };
}

实现 createRouteMap

接下来我们就对传入的路由对象进行转换

{
  '/home': {
    path:'/home',
    component:Home,
    parent: undefined
  },
  '/about':{
    path:'/about',
    component:About,
    parent: undefined,    
  },
  '/about/a':{
    path:'/about/a',
    component:About,
    parent:{
      path:'/about',
      component:About,
      parent: undefined,    
    },
  }
}

export default function createRouteMap(routes, oldPathList, oldPathMap) {
  // 用户传入的数据 进行格式化

  let pathList = oldPathList || [];
  let pathMap = oldPathMap || Object.create(null);

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

  return {
    pathList,
    pathMap,
  };
}

function addRouteRecord(route, pathList, pathMap, parent) {
  let path = parent ? `${parent.path}/${route.path}` : route.path;
  let record = {
    path,
    component: route.component,
    parent,
  };
  if (!pathMap[path]) {
    pathList.push(path); // 路径添加到 pathList中
    pathMap[path] = record;
  }
  if (route.children) {
    route.children.forEach((child) => {
      addRouteRecord(child, pathList, pathMap, route); // 每次循环子节点时都将父路径传入
    });
  }
}

跳转思路

路由跳转中两种最常见的是 hashhistory, 接下来例子就用 hash 来实现

首先,我们要分析下我们的 跳转 相关需要哪些API

    1. 获取当前的路径, 这里如果是hash 的话,我们就要获取 # 后面的内容, 而这两个API 的实现方法都不同, 就要写到各自的类中
    1. 跳转到指定路径, 这里要通过match 方法获取路径涉及到的组件
    1. 设置监听, 那么第一次成功跳转后, 之后的路径变化怎么监听呢?

history/base.js

export default class History {
  constructor(router) {
    // new VueRouer
    this.router = router;
  }

  // 跳转的核心逻辑, onComplete 代表跳转成功后执行的方法
  transitionTo(location, onComplete) {
    this.router.match(location); // 要用当前路径 找出对应的记录

    onComplete && onComplete();
  }
}

history/hash.js

import History from "./base";
function getHash() {
  return window.location.hash.slice(1);
}
export default class HashHistory extends History {
  constructor(router) {
    super(router);
  }
  // 获取当前的路径
  getCurrentLocation() {
    return getHash();
  }

  // 设置监听
  setupHashListener() {
    window.addEventListener("hashchange", () => {
      // 重新跳转路径
      this.transitionTo(getHash());
    });
  }
}

index.js

import install from "./install";
import createMatcher from "./createMatcher";
import HashHistory from "./history/hash";

export default class VueRouter {
  constructor(options) {
    console.log(options);
    // 1. 根据不同的路径,跳转不同的页面
    // 将用户传递的 routes 转成用户好维护的结构
    // match  负责匹配路径
    // addRoutes 动态添加
    this.matcher = createMatcher(options.routes || []);

    // 创建路由系统, 根据模式来创建不同的路由对象
    this.mode = options.mode;

    this.history = new HashHistory(this);
    // History 类, 基类, 基类根据模式掉不同的子类
  }

  // 初始化
  init() {
    // app 指代的是根实例
    // 先根据当前路径, 显示到指定的 组件
    const history = this.history;
    const setupHashListener = () => {
      history.setupHashListener();
    };
    // 先跳转到指定路径, 跳转成功后就设置hash值监听
    // 在 history 的base里调用 router.match 来找
    // router.match 就是用的 matcher 里返回的 match
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener //监听路径变换
    ); // 过渡到某个路径
  }

  // match 方法, 用的是 matcher里的 match方法
  match(location) {
    return this.matcher.match(location);
  }
}

// 默认会调用install方法
VueRouter.install = install;

获取要匹配的路由

我们在History上继续改造, 我们在constructor中添加一个路径, 默认值为 空路径, 这里 我们最后要进行路径匹配的, 就新建一个 createRoute 函数去固定已经匹配好的路由的格式

export function createRoute(record, location) {
  // 创建一个路由
  let res = [];
  if (record) {
    // {path:/about/a, component:xxx,parent}
    while (record) {
      res.unshift(record);
      record = record.parent;
    }
  }
  return {
    ...location,
    matched: res,
  };
}
export default class History {
  constructor(router) {
    // new VueRouer
    this.router = router;
    // 默认路由中应该保存一个当前的路径
    // 后续会更改这个路径
    this.current = createRoute(null, {
      path: "/",
    });
  }

  // 跳转的核心逻辑, onComplete 代表跳转成功后执行的方法
  transitionTo(location, onComplete) {
    // /aoute/a => {path:'/about/a',matched:[about, aboutA]}
    let route = this.router.match(location); // 要用当前路径 找出对应的记录, 并返回
    // route 就是当前路径要匹配的哪些路由
    console.log("route", route);
    onComplete && onComplete();
  }
}

createMatcher/match方法

function match(location) {
    // 找到当前的记录
    let record = pathMap[location];
    let local = {
      path: location,
    };
    // 1. 需要找到对应的记录, 并且根据记录产生一个匹配数组
    if (record) {
      // 找到了记录
      return createRoute(record, local);
    }

    // 没有匹配到
    return createRoute(null, local);
  }

渲染路由

这里我就先只实现一个 router-view 组件

// 函数式组件
// 没有this, 没有状态
export default {
  functional: true,
  render(h, { parent, data }) {
    // matched
    let route = parent.$route;
    let matched = route.matched;
    data.routerView = true; // 当前组件是一个 routerView
    let depth = 0;

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

    let record = matched[depth];
    if (!record) {
      return h();
    }

    let component = record.component;
    return h(component, data);
  },
};

响应式

install.js 里添加 $route$router, 并设置响应式, 这里的响应式是监听 history 上的 currentapp._route 的改变

// 安装插件, 这个插件依赖于 vue
import RouterView from "./components/view";
export let _Vue;
export default function install(Vue) {
  _Vue = Vue;
  Vue.mixin({
    beforeCreate() {
      // 根实例
      if (this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);

        console.log(this);
        // current 变了, 这个 _route 重新赋值
        Vue.util.defineReactive(this, "_route", this._router.history.current);
      } else {
        // 父组件的实例
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route;
    },
  });

  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._routerRoot._router; // 拿到router属性
    },
  });

  Vue.component("RouterView", RouterView);
}

app._route 的改变,我们可以在给 history 设置一个 listen 函数, 当变化就调用这个

init(app) {
  // app 指代的是根实例
  // 先根据当前路径, 显示到指定的 组件
  const history = this.history;
  console.log("1");
  const setupHashListener = () => {
    history.setupHashListener();
  };
  // 先跳转到指定路径, 跳转成功后就设置hash值监听
  // 在 history 的base里调用 router.match 来找
  // router.match 就是用的 matcher 里返回的 match
  history.transitionTo(
    history.getCurrentLocation(),
    setupHashListener //监听路径变换
  ); // 过渡到某个路径

  history.listen((route) => {
    app._route = route; // 视图就可以刷新, 路径变了就调用这个方法
  });
}

// 跳转的核心逻辑, onComplete 代表跳转成功后执行的方法
transitionTo(location, onComplete) {
  // /aoute/a => {path:'/about/a',matched:[about, aboutA]}
  let route = this.router.match(location); // 要用当前路径 找出对应的记录, 并返回
  // route 就是当前路径要匹配的哪些路由

  // 将新的route覆盖掉 current
  if (
    this.current.path === location &&
    route.matched.length === this.current.matched.lenght
  ) {
    return; // 如果是相同路径, 就不调整
  }

  this.updateRoute(route);

  onComplete && onComplete();
}

updateRoute(route) {
  this.current = route;
  this.cb && this.cb(route); // 路径变化会将最新路径传递给 listener
}

listen(cb) {
  this.cb = cb;
}

总结

以上就是 vueRouter 的基本功能的简单实现, 只实现了对 路径 的监听和响应式, 总体来说, 这部分不是很麻烦, 理解后都好弄, 这里我是把路由的属性减少到了几个, 源码中一个路由有十几个属性.....

现在也在探索 router 的其他内容, 之后弄懂了某一部分内容, 再做更新

此篇文章已收录到我的个人博客