新闻资讯
如何实现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基本完成。
回复列表