前言
个人学习过程中的记录,方便后续的复习,如果有错误或者更好的方法,希望大佬指出
路由跳转过程
页面的url变化(点击浏览器的箭头,直接输入地址,或者根据mode的不同,如history模式
通过window.history.pushState
,hash模式
通过location.hash=xxxx
来改变)被监听器监听到(window.addEventListener("popstate",function(){})或者window.onhashchange=function(){}
),需要注意的是pushState不会触发popstate事件
,然后找到路由表中对应的路由,改变响应式的_route
,最后通过RouterView组件
渲染出来
文件组成
hash和history有共同的方法,所以写一个base类,他们都可以继承base
routes.js是Router类
index.js则是使用router
RouterTable和Routers基础
先将RouterView和RouterLink变成全局组件,因为需要使用Vue.use()所以Router上面需要有一个install方法,RouterTable用来储存路由表,HistoryMode 则是history模式下相关的方法(为了更加直观,就不加上mode判断了)
import HistoryMode from "./mode/history";
import Vue from "vue";
import RouterView from "./components/RouterView.vue";
import RouterLink from "./components/RouterLink.vue";
Vue.component("RouterView", RouterView);
Vue.component("RouterLink", RouterLink);
class RouterTable {
constructor(routes) {
this.pathMap = new Map()
this.init(routes)
}
init(routes) {
const addRoute = (route) => {
this.pathMap.set(route.path, route)
}
routes.forEach(route => addRoute(route))
}
math(path) {
let find = this.pathMap.get(path)
if (find) return find
}
}
export default class Router {
constructor({ routes = [] }) {
this.routerTable = new RouterTable(routes)
this.history = new HistoryMode()
}
}
Router.install = () => {
Vue.mixin({
beforeCreate() {
},
});
};
history和hash模式的异同
搞清楚这两个模式有哪些异同,就知道了base hash history
这三个文件下面大概需要写那些东西,具体就不展开讲了,大概就是监听的方式不一致,获取url的方法不一样,跳转的处理不一样(但是跳转要改变的_route
是一样的)
HistoryMode基础
先着重讲解history,hash只需要把上述不一样的地方改一改就可以了
首先HistoryMode 的实例需要先调用监听方法,如果url变化,则需要获取url,并且调用跳转方法改变_route
transitionTo
继承自BaseMode ,因为上面说了两种模式跳转页面都需要改变_route
,这个方法可以直接写在BaseMode里面
import BaseMode from "./base";
export default class HistoryMode extends BaseMode {
constructor(options) {
super(options);
this.initListener()
}
//监听
initListener() {
window.addEventListener('popstate', () => {
this.transitionTo(this.getCurrentLocation())
})
}
//获取url
getCurrentLocation() {
let path = window.location.pathname
return path + window.location.search + window.location.hash
}
}
BaseMode基础
那么怎么知道url改变的时候要改变那个实例上的_route
呢,可以在install的时候,调用router实例中的init方法,传入一个回调,然后调用transitionTo
的时候,调用回调,改变_route
export default class BaseMode {
//需要在Router中this.history = new HistoryMode()的时候将this传入
//this.history = new HistoryMode(this)
constructor(router) {
this.routerTable = router.routerTable
}
listen(cb) {
this.cb = cb
}
transitionTo(route) {
//跳转之前需要通过path获取路由信息,然后通过回调改变`_route`
this.current =this.routerTable.math(route)
this.cb(this.current)
}
}
Router类中的init方法
init(app) {
const { history } = this
history.listen((route) => {
app._route = route
})
}
install
所有的vue component的_routerRoot、_router 都是一样的
vue router实例调用init方法,初始化
Router.install = () => {
Vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
this._routerRoot = this //可以让组件访问到vue实例以及上面的东西,如_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 && this.$parent._routerRoot || this
}
},
});
};
RouterView
rener函数中获取当年路由信息,来渲染页面
<script>
export default {
name: "RouterView",
render() {
const route = this._routerRoot._route;
if (!route) return;
const hook = {
init(vnode) {
route.instance = vnode.componentInstance;
},
};
const { component } = route;
return <component hook={hook} />;
},
};
</script>
直接改变url展示
这个时候路由已经初具效果,可以通过改变url展示了
RouterLink
接下来要通过RouterLink改变,接下来就要写Router 上面的push方法
<template>
<div>
<a style="cursor: pointer" @click="jump">
<slot></slot>
</a>
</div>
</template>
<script>
export default {
props: {
to: {
type: String,
required: true,
},
},
data() {
return {
test: "123",
};
},
methods: {
jump(to) {
const router = this._routerRoot._router;
router.push(this.to);
},
},
};
</script>
Router补充
push(to) {
this.history.push(to)
}
HistoryMode补充
push(target) {
this.transitionTo(target)
window.history.pushState({ key: +new Date() }, "", target)
}
完整演示
到这儿,history模式的低配版路由就写好了
HashMode
Router 中也需要进行少量的修改,这里就不贴代码了,可以优化成将mode作为参数传入,更具不同的mode进行不同的操作
import BaseHistory from "./base";
export default class HashHistory extends BaseHistory {
constructor(options) {
super(options)
this.initListener()
}
initListener() {
window.onhashchange = () => {
this.transitionTo(this.getCurrentLocation())
}
}
getCurrentLocation() {
let path = decodeURI(window.location.hash) || "#/";
let address = path.split('#')[1]
//console.log(address, 'path')
return address;
}
push(target) {
this.transitionTo(target);
window.location.hash = target
}
}
路由导航守卫
全局守卫
//全局前置守卫
router.beforeEach((to, from, next) => {
console.log("router.beforeEach", to, from);
next();
});
//全局解析守卫
router.beforeResolve((to, from, next) => {
console.log("router.beforeResolve", to, from);
next();
});
//全局后置守卫
router.afterEach((to, from) => {
console.log("router.afterEach", to, from);
});
路由独享守卫 beforeEnter
{
path: "/",
name: "Home",
component: Home,
beforeEnter: (to, from, next) => {
console.log("/home.beforeEnter", to, from);
next();
},
},
组件内守卫
组件类守卫就不写他的源码了
beforeRouteEnter(to, from, next) {
console.log("home-beforeRouteEnter", to, from);
next();
},
beforeRouteUpdate(to, from, next) {
console.log("home-beforeRouteUpdate", to, from);
next();
},
beforeRouteLeave(to, from, next) {
console.log("home-beforeRouteLeave", to, from);
next();
},
Router
写一个函数,触发全局路由守卫的时候调用这个函数,将需要执行的函数存入Hooks中
function registerHook(list, fn) {
list.push(fn);
return () => {
let i = list.indexOf(fn);
if (i > -1) list.splice(i, 1);
};
}
export default class Router {
constructor({ routes = [] }) {
//......
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
}
beforeEach(fn) {
registerHook(this.beforeHooks, fn);
}
beforeResolve(fn) {
registerHook(this.resolveHooks, fn);
}
afterEach(fn) {
registerHook(this.afterHooks, fn);
}
}
BaseMode
因为路由守卫不管那个MODE都会有,所以在BaseMode中写相关方法
跳转的时候,先执行confirmTransition,在路由导航中进行确认,如果通过则执行onComplete,调用updateRoute,更新界面,执行afterEach,否则执行onAbort进行想要的操作
export default class BaseMode {
constructor(router) {
this.routerTable = router.routerTable;
this.router = router;
}
listen(cb) {
this.cb = cb;
}
transitionTo(target) {
const route = this.routerTable.math(target);
this.confirmTransition(route, () => {
this.updateRoute(route);
});
}
confirmTransition(route, onComplete, onAbort) {
if (route == this.current) return;
//由于路由导航传入的参数(函数)需要按顺序执行,所以将他们按执行顺序都放入一个数组中,使用类似迭代器来一次调用
const queue = [
...this.router.beforeHooks,
route.beforeEnter,
...this.router.resolveHooks,
];
runQueue(queue, iterator, () => onComplete());
}
updateRoute(route) {
let from = this.current;
this.current = route;
this.cb(this.current);
this.router.afterHooks.forEach((hook) => {
hook && hook(this.current, from);
});
}
}
runQueue
queue是传入的导航守卫传入的参数(函数)组成的数组,iter是一个类似迭代器的东西,end为queue里面所有函数执行完毕之后执行,更新路由,渲染页面
export function runQueue(queue, iter, end) {
const step = (index) => {
if (index >= queue.length) {
end();
} else {
if (queue[index]) {
iter(queue[index], () => {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
iterator
hook 就是beforeEach传入的参数(函数),to 是route(接下来要跳转的页面),from是this.current(离开的页面,此时this.current还没有更新,所以仍然是上一个页面的路由,next(beforeEach里的next)是一个回调,可以传false,error等参数,这里只演示false)
const iterator = (hook, next) => {
hook(route,this.current, (to) => {
if (to === false) {
onAbort && onAbort();
} else {
next();
}
});
};
完整代码
BaseMode
import { runQueue } from "../../util/async";
export default class BaseMode {
constructor(router) {
this.routerTable = router.routerTable;
this.router = router;
}
listen(cb) {
this.cb = cb;
}
transitionTo(target) {
const route = this.routerTable.math(target);
this.confirmTransition(route, () => {
this.updateRoute(route);
});
}
confirmTransition(route, onComplete, onAbort) {
if (route == this.current) return;
const queue = [
...this.router.beforeHooks,
route.beforeEnter,
...this.router.resolveHooks,
];
const iterator = (hook, next) => {
hook(this.current, route, (to) => {
if (to === false) {
onAbort && onAbort();
} else {
next();
}
});
};
runQueue(queue, iterator, () => onComplete());
}
updateRoute(route) {
let from = this.current;
this.current = route;
this.cb(this.current);
this.router.afterHooks.forEach((hook) => {
hook && hook(this.current, from);
});
}
}
history和hash
import BaseMode from "./base";
export default class HistoryMode extends BaseMode {
constructor(options) {
super(options);
this.initListener()
}
initListener() {
window.addEventListener('popstate', () => {
this.transitionTo(this.getCurrentLocation())
})
}
getCurrentLocation() {
let path = window.location.pathname
return path + window.location.search + window.location.hash
}
push(target) {
this.transitionTo(target)
window.history.pushState({ key: +new Date() }, "", target)
}
}
import BaseHistory from "./base";
export default class HashHistory extends BaseHistory {
constructor(options) {
super(options)
this.initListener()
}
initListener() {
window.onhashchange = () => {
this.transitionTo(this.getCurrentLocation())
}
}
getCurrentLocation() {
let path = decodeURI(window.location.hash) || "#/";
let address = path.split('#')[1]
//console.log(address, 'path')
return address;
}
push(target) {
this.transitionTo(target);
window.location.hash = target
}
}
Router
import HistoryMode from "./mode/history";
import Vue from "vue";
import RouterView from "./components/RouterView.vue";
import RouterLink from "./components/RouterLink.vue";
Vue.component("RouterView", RouterView);
Vue.component("RouterLink", RouterLink);
class RouterTable {
constructor(routes) {
this.pathMap = new Map();
this.init(routes);
}
init(routes) {
const addRoute = (route) => {
this.pathMap.set(route.path, route);
};
routes.forEach((route) => addRoute(route));
}
math(path) {
let find = this.pathMap.get(path);
if (find) return find;
}
}
function registerHook(list, fn) {
list.push(fn);
return () => {
let i = list.indexOf(fn);
if (i > -1) list.splice(i, 1);
};
}
export default class Router {
constructor({ routes = [] }) {
this.routerTable = new RouterTable(routes);
this.history = new HistoryMode(this);
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
}
init(app) {
const { history } = this;
history.listen((route) => {
app._route = route;
});
history.transitionTo(history.getCurrentLocation());
}
push(to) {
this.history.push(to);
}
beforeEach(fn) {
registerHook(this.beforeHooks, fn);
}
beforeResolve(fn) {
registerHook(this.resolveHooks, fn);
}
afterEach(fn) {
registerHook(this.afterHooks, fn);
}
}
Router.install = () => {
Vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, "_route", this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
},
});
};
rouerLink 和routerView
<template>
<div>
<a style="cursor: pointer" @click="jump">
<slot></slot>
</a>
</div>
</template>
<script>
export default {
name: "RouterLink",
props: {
to: {
type: String,
required: true,
},
},
data() {
return {
test: "123",
};
},
methods: {
jump() {
const router = this._routerRoot._router;
router.push(this.to);
},
},
};
</script>
<script>
export default {
name: "RouterView",
render() {
const route = this._routerRoot._route;
if (!route) return;
const { component } = route;
return <component />;
},
};
</script>
效果
结言
低配版的路由源码到此结束,部分是我自己的理解,可能有些许错误,希望看到的大佬不吝赐教