序章: 1、坦白从宽
不装了,作为一名 21 世纪的无为好青年。学习了一波只有自己能看懂的 vue-router 然后呢,把它以 文章的形式展示出来。一是锻炼自己的写作能力二是.....
1.1、欲练此功,必先 install
古有葵花宝典 xxx,今有 Vue 插件 install。既然选择使用 Vue, 自然要按照规矩办事。install 方法主要做了 以下几点
- 混入 beforeCreate
- 注册全局组件(页面渲染)
- 定义 route
2,3 很好理解,那么 mixin beforeCreate 主要做了什么呢?
- 赋值根节点
- 路由的初始化
- 新增响应式数据(响应式更新)
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 件事
- 创建匹配器(Matcher),针对 routes 做相应处理(例如 routes 解析, routes 匹配等)
- 根据 Mode 使用不同的模式 (主要 监听路由变化, 路由更新等)
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)进来后,是那么让人不知所措。所以呢,所以呢,我们要 创建一个 匹配器, 用了解析,匹配,查找。
那么流程是啥?
- 解析:将 数组形式的 routes 解析成 Map 的形式 (如 {'/home/index': {....}})
- 匹配:我们需要根据 当前的 url, 来找到对应的 路由信息(组件信息)
- 处理:假设一个场景 父组件:home 子组件: home-child. 此时路由匹配到了 home-child, 我们也找到了 home-child 并将其展示在页面中,那么问题来了 home 的 内容丢失了, 这显然不是我们想看到的结果。因此,需要处理,找到所有节点, 存储在 matched 中
/**
*
* @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 的 重要模式之一。 它做了什么呢?
- 监听路有变化(利用 浏览器 提供的 hashchange)
- 路由跳转 (匹配找到当前 路由 对应的路由)
- 路由更新
- 触发回调
// 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 大块
- install
- history
- matcher
通过 vue.use 调用 install 声明 赋值 变量, history 监听路由变化,触发更新, matcher 匹配查找 对应路由, 从而更改 _route 值, 触发视图更新!!!!
1.6 人靠衣装马靠鞍,酒香也怕巷子深
纵使你做的再多再好,不显示出来, 别人也是看不到的。所以需要 router-view 组件 用来渲染
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;