路由模式
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/#/cccconsole.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.jsimport 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) { // 根组件才有 routerthis._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 // 用于匹配路径 } }
typescript /**
- 路由配置扁平化处理
- 支持初始化和追加两种情况
- @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 事件```typescriptimport { 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) { // 根组件才有 routerthis._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: { // 标签名,默认 atype: String,default: 'a'}},methods: {handler(to) {// 路由跳转:内部调用 history.pushthis.$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.currentlet 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(); }) } }
