路由模式
Hash 模式(默认)
- http://localhost:8080/aaa/#/bbb
- http://localhost:8080/aaa#bbb
原理:依靠浏览器提供的 onhashchange 事件:
// onhashchange 事件触发条件:当一个窗口的 URL 中 # 后面的部分改变时
window.onhashchange = function (event) {
// http://localhost:8080/aaa/#/bbb -> http://localhost:8080/aaa/#/ccc
console.log(event.oldURL, event.newURL);
let hash = location.hash //通过 location 对象获取 hash 地址
console.log(hash) // #/ccc
}
- 当 URL 改变时,页面不会重新加载;
- Hash 变化的 URL 会被浏览器所记录,可以通过浏览器的“前进、后退”进行页面的切换和刷新;
onhashchange 中,仅能够改变 ‘#’ 之后的 url 片段;
History 模式
-
原理:依靠浏览器提供的 History API 实现
hashchange 仅能够改变 ‘#’ 之后的 url 片段,而 history api 提供了完全的自由;
- 具有浏览器历史记录,可以进行“前进”和“后退”的页面切换;
- 当路径变化时,可以通过监听浏览器的路径变化;
- History 模式可以进行“前进、后退”操作,但页面 F5 刷新时,就会报出页面 404;
这时因为 F5 的页面刷新操作,是会真正到服务器请求页面的; 在 hash 模式下,前端路由修改的 # 部分,浏览器请求时不会携带,所以不会有问题; 但 history 模式下,服务器上没有相关的响应或资源时,会报错 404;
路由插件 install
在执行 Vue.use(Router) 之后,会调用 Router 插件的 install:
// router.js
import Vue from 'vue';
import Router from './vue-router';
Vue.use(Router);
export default new Router({
// 路由配置...
})
- vue-router 插件最终的导出结果是一个类 Router;
- Router 实例化时,可以传入一个路由配置对象;
- Router 类能够被 Vue.use(),说明 Router 类上包含了 install 方法; ```typescript // 导入路由安装逻辑 import install from “./install”;
class VueRouter { constructor(options) { // 传入路由配置对象 console.log(‘options >>’, options); } }
// 当 Vue.use 时,会自动执行插件上的 install VueRouter.install = install;
export default VueRouter;
<a name="iKMtT"></a>
## install 逻辑实现
- 插件安装时,指定插件依赖的 Vue 版本,并导出提供 vue-router 插件其他逻辑使用;
- 在 Vue 全局上注册两个组件:<router-link> 和 <router-view>;
- 在 Vue 原型上添加两个属性:$router 和 $route;
- $route:包含路由相关的属性,如 name、hash、meta 等
- $router:路由相关的方法,如 history API (push,replace,go)
- 通过 Vue.mixin 为每个组件混入根组件上的 router 实例
```typescript
// 用于存储插件安装时传入的 Vue 并向外抛出,提供给插件中的其他文件使用
// export 的特点:如果导出的值发生变化,外部会取得变化后的新值;
export let _Vue;
/**
* 插件安装入口 install 逻辑
* @param {*} Vue Vue 的构造函数
* @param {*} options 插件的选项
*/
export default function install(Vue, options) {
_Vue = Vue; // 存储插件安装时使用的 Vue
// 通过生命周期,为所有组件混入 router 属性
Vue.mixin({
beforeCreate() { // this 指向当前组件实例
// 将 new Vue 时传入的 router 实例共享给所有子组件
if (this.$options.router) { // 根组件才有 router
this._routerRoot = this; // 为根组件添加 _routerRoot 属性指向根组件自己
this._router = this.$options.router; // this._router 指向 this.$options.router
} else { // 子组件
// 如果是子组件,就去找父亲上的_routerRoot属性,并继续传递给儿子
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
// 这样,所有组件都能够通过 this._routerRoot._router 获取到同一个 router 实例;
},
})
// 在 Vue 全局注册两个组件:`<router-link>` 和 `<router-view>`;
Vue.component('router-link', {
render: h => h('a', {}, '')
});
Vue.component('router-view', {
render: h => h('div', {}, '')
});
// 在 Vue 原型上添加两个属性: `$router` 和 `$route`;
Vue.prototype.$route = {};
Vue.prototype.$router = {};
}
路由映射表
路由插件初始化
- 在 VueRouter 实例化时,接收外部传入的路由配置 options:new Router(options),构造函数内部通过 createMatcher 路由匹配器对其进行处理
- 在路由匹配器 createMatcher 中,将路由配置(嵌套数组)处理为便于匹配的扁平化结构对象 matcher;
- 在路由匹配器中创建路由插件的两个核心方法:match 和 addRoutes;
- match:用于通过路由规则匹配到对应的组件
- addRoutes:用于动态的添加路由匹配规则
- 在 VueRouter 类中,创建路由初始化方法 init,当执行Vue.use安装路由插件在 install 方法中处理根组件时,执行路由的 init 初始化操作,将根实例 Vue.app 传入 init 方法中; ```typescript import createRouteMap from “./create-route-map”;
/**
- 路由匹配器函数
- 对路由配置进行扁平化处理
- addRoutes:动态添加路由匹配规则
- match:根据路径进行路由匹配
- @param {*} routes
@returns 返回路由匹配器的两个核心方法 addRoutes、match */ export default function createMatcher(routes) { // 将嵌套数组的路由配置,处理为便于匹配的扁平结构 // 创建 match 方法:根据路径进行路由匹配 // 创建 addRoutes 方法:动态添加路由匹配规则
// 路由配置的扁平化处理 let { pathMap } = createRouteMap(routes);
// 根据路径进行路由匹配 function match(location) {
let record = pathMap[location];
return record;
}
/**
- 动态添加路由匹配规则
将追加的路由规则进行扁平化处理 */ function addRoutes(routes) { createRouteMap(routes,pathMap); }
return { addRoutes, // 添加路由 match // 用于匹配路径 } }
- 路由配置扁平化处理
- 支持初始化和追加两种情况
- @param {*} routes 路由实例中的路由配置
- @param {*} oldPathMap 路由规则映射表(扁平化结构)
@returns 新的路由规则映射表(扁平化结构) */ export default function createRouteMap(routes, oldPathMap) {
// 拿到当前已有的映射关系 let pathMap = oldPathMap || Object.create(null);
// 将路由配置 routes 依次加入到 pathMap 路由规则的扁平化映射表 routes.forEach(route => {
addRouteRecord(route, pathMap);
});
return {
pathMap
} }
/**
- 添加一个路由记录(递归当前的树形路由配置)
- 先序深度遍历:先把当前路由放进去,再处理他的子路由
- @param {*} route 原始路由记录
- @param {*} pathMap 路由规则的扁平化映射表
- @param {} parent 当前路由所属的父路由对象 /
function addRouteRecord(route, pathMap, parent) {
// 处理子路由时,需要做路径拼接
let path = parent ? (parent.path + '/' + route.path) : route.path;
// 构造路由记录对象(还包含其他属性:path、component、parent、name、props、meta、redirect...)
let record = {
path,
component: route.component,
parent // 标识当前组件的父路由记录对象
};
// 查重:路由定义不能重复,否则仅第一个生效
if (!pathMap[path]) {
pathMap[path] = record;
}
// 递归处理当前路由的子路由
if (route.children) {
route.children.forEach(childRoute => {
addRouteRecord(childRoute, pathMap, record);
})
}
}
<a name="ZfOGm"></a>
# 两种路由模式的设计
公共逻辑抽离到父类中实现:
- transitionTo 根据路由进行匹配跳转
```typescript
/**
* 通过路由记录,逐层进行路由匹配
* @param {*} record 路由记录
* @param {*} location 路径
* @returns 逐层匹配后的全部匹配结果
*/
export function createRoute(record, location) {
let res = []; //[/user /user/info]
if (record) {
while(record) {
res.unshift(record);
record = record.parent;
}
}
return {
...location,
matched: res
}
}
class History {
constructor(router) {
this.router = router; // 存储子类传入的 router 实例
// {'/': Record1, '/user': Record2, '/user/info': Record3 }
this.current = createRoute(null, {
path: '/'
});
}
/**
* 路由跳转方法:
* 每次跳转时都需要知道 from 和 to
* 响应式数据:当路径变化时,视图刷新
* @param {*}} location
* @param {*} onComplete
*/
transitionTo(location, onComplete) {
// 根据路径进行路由匹配
let route = this.router.match(location);
// 查重:如果前后两次路径相同,且路由匹配的结果也相同,那么本次无需进行任何操作
if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
return
}
// 使用当前路由route更新current,并执行其他回调
this.updateRoute(route);
onComplete && onComplete();
}
listen(cb) {
// 存储路由变化时的更新回调函数,即 app._route = route;
this.cb = cb;
}
/**
* 路由变化时的相关操作:
* 1,更新 current;
* 2,触发_route的响应式更新;
* @param {*} route 当前匹配到的路由结果
*/
updateRoute(route) {
// 每次路由切换时,都会更改current属性
this.current = route;
// 调用保存的更新回调,触发app._route的响应式更新
this.cb && this.cb(route);
}
}
export {
History
}
- Hash 模式获取当前路径 hash 值
- 监听 hashchange 事件 ```typescript import { History } from ‘./base’;
function ensureSlash() { // location.hash 存在兼容性问题,可根据完整 URL 判断是否包含’/‘ if (window.location.hash) { return; } window.location.hash = ‘/‘; // 如果当前路径没有hash,默认为 / }
class HashHistory extends History { constructor(router) { super(router); this.router = router; // Hash 模式下,对URL路径进行处理,确保包含’/‘ ensureSlash(); }
getCurrentLocation() {
// 获取路径的 hash 值
return getHash();
}
setupListener() {
// 当 hash 值变化时,获取新的 hash 值,并进行匹配跳转
window.addEventListener('hashchange', () => {
this.transitionTo(getHash());
})
}
} export default HashHistory
- history 模式获取当前路径 path 值
- 监听 popState 事件
```typescript
import { History } from './base';
class BrowserHistory extends History {
constructor(router) {
super(router); // 调用父类构造方法,并将 router 实例传给父类
this.router = router; // 存储 router 实例,共内部使用
}
setupListener(){
// 当路径变化时,拿到新的 hash 值,并进行匹配跳转
window.addEventListener('popState',()=>{
this.transitionTo(getHash());
})
}
}
export default BrowserHistory;
路由的响应式实现
使用 Vue.util.defineReactive 将 this._router.history.current 定义为响应式数据
// 通过生命周期,为所有组件混入 router 属性
Vue.mixin({
beforeCreate() { // this 指向当前组件实例
// 将 new Vue 时传入的 router 实例共享给所有子组件
if (this.$options.router) { // 根组件才有 router
this._routerRoot = this; // 为根组件添加 _routerRoot 属性指向根组件自己
this._router = this.$options.router; // this._router 指向 this.$options.router
// 在根组件中,调用路由实例上的 init 方法,完成插件的初始化
this._router.init(this); // this 为根实例
// 目标:让 this._router.history.current 成为响应式数据;
// 作用:current用于渲染时会进行依赖收集,当current更新时可以触发视图更新;
// 方案:在根组件实例上定义响应式数据 _route,将this._router.history.current对象中的属性依次代理到 _route 上;
// 优势:当current对象中的任何属性发生变化时,都会触发响应式更新;
// Vue.util.defineReactive: Vue 构造函数中提供的工具方法,用于定义响应式数据
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else { // 子组件
// 如果是子组件,就去找父亲上的_routerRoot属性,并继续传递给儿子
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
// 这样,所有组件都能够通过 this._routerRoot._router 获取到同一个 router 实例;
},
})
$route、$router 与 router-link 组件实现
定义原型方法
/**
* 在 Vue 原型上添加 $route 属性 -> current 对象
* $route:包含了路由相关的属性
*/
Object.defineProperty(Vue.prototype, '$route', {
get() {
// this指向当前实例;所有实例上都可以拿到_routerRoot;
// 所以,this._routerRoot._route 就是根实例上的 _router
// 即:处理根实例时,定义的响应式数据 -> this.current 对象
return this._routerRoot._route; // 包含:path、matched等路由相关属性
}
});
/**
* 在 Vue 原型上添加 $router 属性 -> router 实例
* $router:包含了路由相关的方法
*/
Object.defineProperty(Vue.prototype, '$router', {
get() {
// this._routerRoot._router 就是当前 router 实例;
// router 实例中,包含 matcher、push、go、repace 等方法;
return this._routerRoot._router;
}
});
组件 - 每次点击
组件时,都会进行 hash 值的切换; 组件可以接收来自外部的传参; 组件渲染后返回的内容包括:元素标签 + 点击跳转事件 + 插槽等; export default {
// 组件名称
name: 'routerLink',
// 接收来自外部传入的属性
props: {
to: { // 目标路径
type: String,
require: true
},
tag: { // 标签名,默认 a
type: String,
default: 'a'
}
},
methods: {
handler(to) {
// 路由跳转:内部调用 history.push
this.$router.push(to);
}
},
render() {
let { tag, to } = this;
// JSX:标签 + 点击跳转事件 + 插槽
return <tag onClick={this.handler.bind(this, to)}>{this.$slots.default}</tag>
}
}
- 每次点击
this.$router.push 方法
class VueRouter {
push(to) {
this.history.push(to); // 子类对应的push实现
}
}
class HashHistory extends History {
push(location) {
// 跳转路径,并在跳转完成后更新 hash 值;
// transitionTo内部会查重:hash 值变化虽会再次跳转,但不会更新current属性;
this.transitionTo(location, () => {
window.location.hash = location; // 更新 hash 值
})
}
/**
* 路由跳转方法:
* 每次跳转时都需要知道 from 和 to
* 响应式数据:当路径变化时,视图刷新
* @param {*}} location
* @param {*} onComplete
*/
transitionTo(location, onComplete) {
// 根据路径进行路由匹配;route :当前匹配结果
let route = this.router.match(location);
// 查重:如果前后两次路径相同,且路由匹配的结果也相同,那么本次无需进行任何操作
if (location == this.current.path && route.matched.length == this.current.matched.length) {
return;
}
// 使用当前路由route更新current,并执行其他回调
this.updateRoute(route);
onComplete && onComplete();
}
}
router-view 组件
获取渲染记录
- 标记 router-view 层级深度,在当前组件向父组件追溯,进而确定当前层级
- 根据深度进行 router-view 渲染 ```typescript
// 普通组件,使用组件需要先进行实例化再挂载:new Ctor().$mount(); // 函数式组件,无需创建实例即可直接使用(相当于 react 中的函数组件); // 他们之间唯一的区别就是 render 函数中没有 this,即没有组件状态(没有 data,props 等)
/**
- router-view 函数式组件,多层级渲染
函数式组件特点:性能高,无需创建实例,没有 this */ export default { name: ‘routerView’, functional: true, // true 表示函数式组件
// 需要通过记录深度 depth 对应到每一层 router-view 要渲染的内容 render(h, { parent, data }) {
// 获取当前需要渲染的相关路由记录,即:this.current
let route = parent.$route;
let depth = 0; // 记录等级深度
// 在当前层级(第一层)的 data 属性中,添加自定义属性
data.routerView = true;
// App.vue渲染组件时,调用render函数,此时的父亲中没有 data.routerView 属性
// 在渲染第一层时,添加routerView=true标识
while(parent) { // parent 为 router-view 的父标签
// parent.$vnode:代表占位符的vnode;即:组件标签名的虚拟节点;
// parent._vnode 指组件的内容;即:实际要渲染的虚拟节点;
if(parent.$vnote && parent.$vnote.data.routerView) {
depth++;
}
parent = parent.$parent; // 更新父组件,用于循环的下一次处理
}
// 根据匹配结果与层级深度 depth,进行渲染
// 第一层router-view 渲染第一个record 第二个router-view渲染第二个
let record = route.matched[depth]; // 获取对应层级的记录
// 未匹配到路由记录,渲染空虚拟节点(empty-vnode),也叫作注释节点
if (!record) {
return h();
}
// h(record.component):渲染当前组件,当组件渲染时,传入 data 数据,其中 data 包含了之前标识的 routerView 属性;
return h(record.component, data);
全局钩子函数
- 钩子函数的订阅
发布订阅模式
class VueRouter {
constructor(options) { // 传入配置对象
// 定义一个存放钩子函数的数组
this.beforeHooks = [];
}}
// 在router.beforeEach时,依次执行注册的钩子函数
beforeEach(fn){
this.beforeHooks.push(fn);
}
}
export default VueRouter;
- 钩子的执行时机
eforeEach 钩子的执行时机:路由已经开始切换,但还没有更新之前
- 执行注册的钩子函数
```typescript
/**
- 递归执行钩子函数
- @param {*} queue 钩子函数队列
- @param {*} iterator 执行钩子函数的迭代器
- @param {} cb 全部执行完成后调用 / function runQueue(queue, iterator, cb) { // 异步迭代 function step(index) { // 结束条件:队列全部执行完成,执行回调函数 cb 更新路由 if (index >= queue.length) return cb(); let hook = queue[index]; // 先执行第一个 将第二个hook执行的逻辑当做参数传入 iterator(hook, () => step(index + 1)); } step(0); }
class History { constructor(router) { this.router = router; }
/**
- 路由跳转方法:
- 每次跳转时都需要知道 from 和 to
- 响应式数据:当路径变化时,视图刷新
- @param {*}} location
- @param {} onComplete / transitionTo(location, onComplete) { let route = this.router.match(location); if (location == this.current.path && route.matched.length == this.current.matched.length) { return } // 获取到注册的回调方法 let queue = [].concat(this.router.beforeHooks); const iterator = (hook, next) => { hook(this.current, route, () => { next(); }) } runQueue(queue, iterator, () => { // 将最后的两步骤放到回调中,确保执行顺序 // 1,使用当前路由route更新current,并执行其他回调 this.updateRoute(route); // 根据路径加载不同的组件 this.router.matcher.match(location) 组件 // 2,渲染组件 onComplete && onComplete(); }) } }