这是我参与「第四届青训营」笔记创作活动的第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 结合路由对象 生成 pathList 和 pathMap.
之后的 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); // 每次循环子节点时都将父路径传入
});
}
}
跳转思路
路由跳转中两种最常见的是 hash 和 history, 接下来例子就用 hash 来实现
首先,我们要分析下我们的 跳转 相关需要哪些API
-
- 获取当前的路径, 这里如果是hash 的话,我们就要获取
#后面的内容, 而这两个API 的实现方法都不同, 就要写到各自的类中
- 获取当前的路径, 这里如果是hash 的话,我们就要获取
-
- 跳转到指定路径, 这里要通过
match方法获取路径涉及到的组件
- 跳转到指定路径, 这里要通过
-
- 设置监听, 那么第一次成功跳转后, 之后的路径变化怎么监听呢?
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 上的 current 和 app._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 的其他内容, 之后弄懂了某一部分内容, 再做更新
此篇文章已收录到我的个人博客