根据vue-router的源码,实现一个自己的vue-router。
写在前面
本文将带你手写一个vue-router,实现router-view,实现hash模式,嵌套路由等主要的功能,history模式建议自行看源码,原理就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面,但是需要后端做配合,没有hash直接上手使用来的方便简单,所以本篇将主要将hash的实现,关于history的有兴趣的可以看这里vue-router,本篇完整代码
让我们现在初始化一个项目
vue create my-router cd vuex-test yarn serve
router.js
import Vue from 'vue'
import Router from '../myRouter'
import Home from '../src/components/Home'
import About from '../src/components/About'
Vue.use(Router)
const routes = [
{
path: '/',
component: Home
},
{
path: '/about',
component: About,
children: [
{
path: 'a',
component: {
render() {
return <h1>About A</h1>
}
}
},
{
path: 'b',
component: {
render() {
return <h1>About B</h1>
}
}
}
]
}
]
const router = new Router({
routes
})
export default router
基本的嵌套路由,访问'/'渲染Home组件,访问'/about'渲染About组件,访问'/about/a'渲染About组件和子组件About A,访问'/about/b'渲染About组件和子组件About B。嵌套路由'/about/b'一定是匹配到父组件,然后由父组件去渲染子组件的router-view组件。
install方法
install.js
export let _vue
/**
* 1. 注册全局属性 $route $router
* 2. 注册全局组件 router-view router-link
*/
export default function install(Vue) {
_vue = Vue
Vue.mixin({
beforeCreate () {
if(this.$options.router) {
this._routerRoot = this
this._router = this.$options.router
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
}
我们知道vue-router的用法是vue.use(router),vue会调用install方法进行初始化。初始化分为俩个步骤,1. 注册全局属性 route router,2. 注册全局组件 router-view router-link,我们这次主要讲router-view。 install只要是判断是否为根组件,只有根组件才会传入router实例,根组件我们将_routerRoot指向根实例,_router指向router实例,这样所有的子组件都能通过$parent._routerRoot拿到_router这个router实例。
数据扁平化
class Router
import createMatcher from './create-matcher'
import install from './install'
export default class Router {
constructor(options) {
/**
* 将用户的数据扁平化
* [
* {
* path: '/ss',
* component: SSS
* }
* ]
* => {'/ss': SSS, '/sss/aa': a}
*
* matcher会有俩个方法
* 1. match 用来匹配路径和组件
* 2. addRoutes 用来动态的添加组件
*/
this.matcher = createMatcher(options.routes || [])
}
init(app) { // main vue
const setupHashLinster = () => {
history.setupHashLinstener()
}
history.transitionTo(
// 首次进入的时候跳转到对应的hash
// 回调用来监听hash的改变
history.getCurrentLocation(),
setupHashLinster
)
}
}
Router.install = install
数据扁平化就是将我们的用户传进来的routes给拆成我们想要的数据结构。vue中create-matcher.js单独用来做这个事情。
createMatcher
create-matcher.js
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
/**
* pathList => 路径的一个关系array [/sss, /sss/s, /sss/b]
* pathMap => 路径和组件的关系map {/sss: 'ss', ....}
*/
let { pathList, pathMap } = createRouteMap(routes)
}
create-route-map.js
export default function createRouteMap(routes, oldPathList, oldPathMap) {
let pathList = oldPathList || []
let pathMap = oldPathMap || Object.create(null)
routes.forEach((route) => {
addRouteRecord(route, pathList, pathMap)
})
return {
pathList,
pathMap
}
}
function addRouteRecord(route, pathList, pathMap, parent) {
let path = parent ? `${parent.path}/${route.path}` : route.path
let record = {
path,
component: route.component,
parent
}
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
if (route.children) {
route.children.forEach((child) => {
addRouteRecord(child, pathList, pathMap, route)
})
}
}
createRouteMap创建pathList, pathMap对应关系,将数据扁平话。使用递归addRouteRecord将'/about/a'转化成
{
'/about': {
path: '/about',
component: About,
parent: null
},
'/about/a': {
path,
component: About A,
parent: About // 指父路由
}
}
match方法的作用
create-matcher.js
import { createRoute } from '../history/base'
/**
* 用来匹配路径
*/
function match(location) {
/**
* 更具路径匹配组件并不能直接渲染组件,因该找到所有要匹配的项
* path: 'about/a' => [about, aboutA]
* 只有将父子组件都渲染才能完成工作。
*/
let record = pathMap[location]
let local = {
path: location
}
if (record) {
return createRoute(record, local)
}
return createRoute(null, local)
}
/**
* 动态路由,可以动态添加路由,并将添加的路由放到路由映射表中
*/
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap)
}
return {
match,
addRoutes
};
base.js
export function createRoute(record, location) {
let res = []
if (record) {
while(record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
match是用来将当前的路径跟我们用户初始化参数做匹配的,并将需要渲染的组件给返回,createRoute将传入的匹配数据和当前的地址拼接返回{path: '/about/a', matched: [About, AboutA]}。
History
vue-router有三个模式,hash,history,abstract,每个模式都有对url的操作,共有的方法放在class Base中,自己独有的就放在自己的类中。History要实现路由的改变的监听,并将改变后的数据match出对应的组件。
export default class Base {
constructor(router) {
this.router = router
/**
* 默认匹配项,后续会根据路由改变而替换
* 保存匹配到的组件
*/
this.current = createRoute(null, {
path: '/'
})
}
/**
* location 要跳转的路径
* onComplete 跳转完成之后的回调
*/
transitionTo(location, onComplete) {
/**
* 去匹配当前hash的组件
*/
let route = this.router.match(location)
/**
* 匹配完成,将current给修改掉
* 相同路径就不进行跳转了
*/
if(this.current.path === location && route.matched.length === this.current.matched.length) return
/**
* 有了当前的current,我们的vue各个组件该怎样访问
*/
this.updateRoute(route)
onComplete && onComplete()
}
updateRoute(route) {
this.current = route
this.cb && this.cb(route)
}
linsten(cb) {
this.cb = cb
}
}
我们用this.current保存匹配到的组件,并提供一个方法transitionTo,当url改变时去匹配改变以后的组件并将this.current给修改掉。现在我们需要将class Hash和Base联起来。
实现Hash
hash.js
import Base from './base'
const getHash = () => {
return window.location.hash.slice(1)
}
const ensureSlash = () => {
if (window.location.hash) return
window.location.hash = '/'
}
export default class Hash extends Base {
constructor(router) {
super(router)
// 确保hash是有#/的
ensureSlash()
}
getCurrentLocation() {
return getHash()
}
setupHashLinstener() {
window.addEventListener('hashchange', () => {
this.transitionTo(getHash())
})
}
}
}
setupHashLinstener实际上就是transitionTo(location, onComplete) 的第二个参数,在首次初始化路由路由为/,并完成hashchange的监听,当hash改变在执行transitionTo把当前的hash去match出最新的matched组件,然后修改this.current。
路由是响应式的
上面我们的所有工作都是围绕current来做的,当url改变我们去match最新的组件给current。当current改变的时候就自动更新组件。当current改变我们要将_route这个属性改变,所以就用到base的linsten方法。
install.js
// 调用router的init
this._router.init(this) this指的是根实例
// 怎样让this.current变成响应式的
// Vue.util.defineReactive = vue.set()
Vue.util.defineReactive(this, '_route', this._router.history.current)
class Router
init(app) { // 根实例
const history = this.history
const setupHashLinster = () => {
history.setupHashLinstener()
}
history.transitionTo(
// 首次进入的时候跳转到对应的hash
// 回调用来监听hash的改变
history.getCurrentLocation(),
setupHashLinster
)
history.linsten((route) => {
// current改变会执行这个回调,修改_route
app._route = route
})
}
添加全局的属性
我们需要个每个vue组件添加 router属性。
install.js
/**
* 怎么让所有的组件都能访问到current
*/
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
/**
* 怎么让所有的组件都能访问到router实例
*/
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router
}
})
}
添加全局的组件
routerView函数式组件,render第二个参数是context可以拿到当前组件的状态。我们拿到$route就能拿到当前url对应的组件。 这里解释一下while循环。当我们渲染'/about/a'时,matched=[About, aboutA]俩组件,第一次渲染的是About组件,depth = 0,当渲染aboutA时,parent = About满足条件depth++,然后渲染aboutA组件。
install.js
/**
* 注册全局组件
*/
Vue.component('router-view', routerView)
router-view.js
export default {
functional: true,
render(h, {parent, data}) {
let route = parent.$route
let matched = route.matched
// 组件标示,表示是个routerView组件
data.routerView = true
let depth = 0
while(parent) {
if(parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
parent = parent.$parent
}
let record = matched[depth]
if(record) {
let component = record.component
return h(component, data)
} else {
return h()
}
}
}
vue-router基本完成,谢谢你能看到这里。