仿vue-router源码实现
前言
vue-router用于处理单页面路由分为H5mode和Hashmode,今天我们就来实现一下
一、基础实现
实现我们需要建一个RouterTable类,来进行路由和组件的关联关系
在Router里我们需要两个值,自己的history和RouterTable
router.js:
import Vue from 'vue'
import H5Mode from './history/h5'
import RouterView from './component/RouterView.vue'
import RouterLink from './component/RouterLink.vue'
Vue.component("RouterView", RouterView)
Vue.component("RouterLink", RouterLink)
class RouterTable { //路由和组件对应的关联关系
constructor(routes) {
this._pathMap = new Map()
this.init(routes)
}
init(routes) {
const addRouter = route => {
console.log(route);
this._pathMap.set(route.path, route)
}
routes.forEach(route => {
addRouter(route)
});
}
match(path) {
let find;
console.log(this._pathMap);
console.log(this._pathMap.keys());
for (const value of this._pathMap.keys()) {
if (path === value) {
find = value
break
}
}
return this._pathMap.get(find)
}
}
export default class Router {
constructor({ routes = [] }) {
this.routerTable = new RouterTable(routes)
this.history = new H5Mode(this)
}
init(app) {
console.log(app);
console.log(this);
const { history } = this
history.listen(route => {
app._route = route
})
history.transitionTo(history.getCurrentLocation()) //app._route = route path: '/home', component
}
push(to) {
this.history.push(to)
}
}
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) //{path: '/home', component: {…}}
}
},
})
}
其中Map是一组键值对的结构,具有极快的查找速度。就相当于一个二维数组。
for-in是ES5标准,遍历的是key(可遍历对象、数组或字符串的key);for-of是ES6标准,遍历的是value(可遍历对象、数组或字符串的value)。
创建监听路由变化的类
因为有H5和Hash两种模式,所以我们先创建一个基本的类
base.js:
export default class HistoryBase {
constructor({ routerTable }) {
this.routerTable = routerTable
}
listen(cb) {
this.cb = cb
}
transitionTo(target) { //pathname:/home
const route = this.routerTable.match(target)
this.current = route
this.cb(this.current)
// route => {
// app._route = route
// }(this.current)
}
}
我们主要采用H5Mode所以创建
h5.js:
import HistoryBase from './base'
export default class H5Mode extends HistoryBase {
constructor(options) {
super(options)
this.initListener()
}
initListener() {
window.addEventListener("popstate", () => {
this.transitionTo(this.getCurrentLocation())
})
}
getCurrentLocation() {
const path = window.location.pathname
return path
}
push(target) {
this.transitionTo(target)
window.history.pushState({ key: +new Date() }, "", target)
}
}
它主要是通过popstate来进行监听的,如果路由变了就通过app._route = route改变,这个时候RouterView.vue又会监听app._route 来返回不同的组件
二、创建RouterView
它的作用是将组件渲染出来,他需要通过this.$root._routerRoot._route拿到component,因为当时我们存的是Vue.util.defineReactive(this, “_route”, this._router.history.current),其中第三个参数就是一个{path: ‘/home’, component: {…}}
<script>
export default {
mounted(){
console.log(this);
},
render(){
const route=this.$root._routerRoot._route;
if(!route){
return
}
const { component }=route
return <component />
}
}
</script>
二、创建RouterLink
我们同时可以通过RouterLink来进行跳转组件
内容我们可以采用slot来占位,然后采用window.history.pushState方法跳转
但是用于window.history.pushState不能触发popstate方法,所以我们需要手动触发 this.transitionTo(target)
<template>
<a @click="jump">
<slot></slot>
</a>
</template>
<script>
export default {
props:{
to:{
type:String,
require:true
}
},
methods: {
jump(){
const router=this.$root._routerRoot._router;
router.push(this.to)
}
},
}
</script>
导航守卫
vue-router提供一个返回的函数,用来销毁钩子函数
首先现在router.js里写
router.beforeEach((to, from, next) => {
console.log("beforeEach", to, from);
next()
})
router.beforeResolve((to, from, next) => {
console.log("beforeResolve", to, from);
next()
})
router.afterEach((to, from) => {
console.log("afterEach", to, from);
})
Router类添加 beforeHooks,resolveHooks ,afterHooks
registerHook会返回一个可以清除Hooks的函数
export default class Router {
constructor({ routes = [] }) {
this.routerTable = new RouterTable(routes)
this.history = new H5Mode(this)
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
}
init(app) {
const { history } = this
history.listen(route => {
app._route = route
})
history.transitionTo(history.getCurrentLocation()) //app._route = route path: '/home', component
}
push(to) {
this.history.push(to)
}
beforeEach(fn) {
return registerHook(this.beforeHooks, fn)
}
beforeResolve(fn) {
return registerHook(this.resolveHooks, fn)
}
afterEach(fn) {
return registerHook(this.afterHooks, fn)
}
}
function registerHook(list, fn) {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) {
list.splice(i, 1)
console.log(list.splice(i, 1));
}
}
}
在base.js修改代码如下,其中runQueue会一直执行Hooks函数,并且最后会执行OnComplete函数也就updateRoute
如果没有next(),那么就不会执行step(index + 1)
import { runQueue } from '../util/async'
export default class HistoryBase {
constructor(router) {
this.router = router
this.routerTable = router.routerTable
}
listen(cb) {
this.cb = cb
}
confirmTransition(route, OnComplete, onAbort) {
if (route === this.current) {
return;
}
const queue = [
...this.router.beforeHooks,
...this.router.resolveHooks,
]
const iterator = (hook, next) => { //queue[index], () => { step(index + 1) }
console.log(hook); //(to, from, next) => {console.log("beforeEach", to, from);next()}
hook(route, this.current, (to) => {
if (to === false) {
onAbort && onAbort(to)
} else {
next(to) // step(index + 1) next
}
})
}
runQueue(queue, iterator, () => { //叠加器函数
OnComplete()
})
}
updateRoute(route) {
const from = this.current
this.current = route
this.cb(this.current)
this.router.afterHooks.forEach(hook => {
hook && hook(route, from)
});
}
transitionTo(target) { //pathname:/home
const route = this.routerTable.match(target)
this.confirmTransition(route, () => {
this.updateRoute(route)
})
}
}
async.js代码如下
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)
}
总结
就是用app._route = route改变来进行组件的渲染
导航守卫就是当地址改变时,依次执行queue里面的hooks,并且递增。