mini vue router 要干啥
- 实现一个插件:包含有VueRouter class & 必须的 install 方法;
- 实现2个全局组件:router-link & router-view;
- 监听 url 变化,实现 hash 模式进行跳转;
- 实现嵌套子路由跳转显示;
开始实现
插件实现
使用 vue-router 时,我们使用 Vue.use(VueRouter)来注册路由,并且通过 new VueRouter(routes)接收路由配置,构造路由器实例 router 并挂载到根实例选项中,使得所有组件内都可以通过 :this.$router
访问路由器
let Vue
// 保存路由配置选项
class VueRouter { // new VueRouter(routes)
constructor(options) {
this.$options = options
// todo
}
}
VueRouter.install = function (_Vue) { //
Vue = _Vue // 接受宿主环境的 Vue
Vue.mixin({
beforeCreate() {
// 将根组件上的 router 实例挂载到 Vue 原型,那么所有的组件实例上都会有 $router
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
// todo
}
export default VueRouter
实现router-link
本轮子使用hash模式,因此url中会带有#
号。router-link
组件目的是点击进行跳转,可以将思路简化为:
=> {this.$slots.default}
上代码:
const Link = Vue.extend({
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
}
}, [this.$slots.default]
)
}
})
// 注册 router-link
Vue.component('router-link', Link)
监听url变化并实现router-view响应视图
- 通过监听
hashchange
事件,获取到更改后的URL标识符,并在路由器中创建响应式的matched数组记录URL变化匹配到的路由; - 深度标记每一个router-view组件,然后从路由器的matched数组中根据当前访问的深度(depth)取出要显示的component,在当前router-view中渲染。思路如下图:
constructor(options) {
// 响应式的matched 按深度存放路由配置
Vue.util.defineReactive(this, 'matched', [])
// this.current 记录的当前的URL标识符
const initPath = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initPath)
// 监听URL变化
window.addEventListener('hashchange', this.onHashChange.bind(this))
// page加载时也要匹配当前URL需要render的组件
window.addEventListener('load', this.onHashChange.bind(this))
this.match()
}
onHashChange() {
this.current = window.location.hash.slice(1) || '/'
this.matched = []
this.match()
}
match(routes) {
routes = routes || this.$options.routes
//递归遍历记录当前URL下所有命中的route
for (const route of routes) {
if (route.path === '/' && this.current === '/') { // 首页
this.matched.push(route)
return
}
// about/info
if (route.path !== '/' && ~this.current.indexOf(route.path)) {
this.matched.push(route)
if (route.children) {
this.match(route.children)
}
return
}
}
}
// 1. 标记深度
const View = Vue.extend({
render(h) {
this.$vnode.data.routerView = true // 标记当前组件是 router-view
let depth = 0
//递归确认 当前 router-view 在组件树中的深度
let parent = this.$parent
while (parent) {
const vnodeData = parent.$vnode && parent.$vnode.data
if (vnodeData) {
if (vnodeData.routerView) { // 说明是一个 router-view
++depth
}
}
parent = parent.$parent
}
let component = null
const { matched } = this.$router
if(matched[depth]){
component = matched[depth].component
}
console.log('当前深度:',depth);
console.log('当前matched:',this.$router.matched);
return h(component)
}
})
Vue.component('router-view', View)
至此,一个简单的 mini vue-router 就实现了,我们可以像使用官方的vue-router一样,引入我们自己的 mini-vue-router在项目中使用。
当然,官方库要远远复杂于本轮子,本轮子旨在理解vue-router核心思想,更深层次研究请阅读官方源码【》》传送门】
本轮子实现效果如下:
扩展与思考
- 如何实现路由守卫
- 如何实现路由缓存
- 如何实现history模式
- 如何实现路由懒加载
附注
本轮子的完整代码:
// 1. 是一个插件 有 VueRouter class 以及 install 方法
// 2. new VueRouter(options) 一个实例 挂载到 根实例上 并且所有组件能通过 this.$router 访问router 实例
// 3. router-link router-view 两个全局组件 router-link 跳转,router-view 显示内容
// 4. 监听 url 变化 监听 haschange || popstate 事件
// 5. 响应最新的 url : 创建一个响应式的属性 current 当它改变的时候获取对应的组件并显示
// 6. 子组件 深度标记 与 macth()
let Vue
// 保存路由配置选项
class VueRouter { // new VueRouter(routes)
constructor(options) {
this.$options = options
// todo 缓存一个路由映射表
// 响应式的matched 按深度存放路由配置
Vue.util.defineReactive(this, 'matched', [])
// this.current 记录的当前的URL标识符
const initPath = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initPath)
// 监听URL变化
window.addEventListener('hashchange', this.onHashChange.bind(this))
// page加载时也要匹配当前URL需要render的组件
window.addEventListener('load', this.onHashChange.bind(this))
this.match()
}
onHashChange() {
this.current = window.location.hash.slice(1) || '/'
this.matched = []
this.match()
}
match(routes) {
routes = routes || this.$options.routes
//递归遍历记录当前URL下所有命中的route
for (const route of routes) {
if (route.path === '/' && this.current === '/') { // 首页
this.matched.push(route)
return
}
// about/info
if (route.path !== '/' && ~this.current.indexOf(route.path)) {
this.matched.push(route)
if (route.children) {
this.match(route.children)
}
return
}
}
}
}
VueRouter.install = function (_Vue) { //
Vue = _Vue // 接受宿主环境的 Vue
Vue.mixin({
beforeCreate() {
// 将根组件上的 router 实例挂载到 Vue 实例原型,那么所有的 组件实例上都会有 $router
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
const Link = Vue.extend({
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
}
}, [this.$slots.default]
)
}
})
// 1. 标记深度
const View = Vue.extend({
render(h) {
this.$vnode.data.routerView = true // 标记当前组件是 router-view
let depth = 0
//递归确认 当前 router-view 在组件树中的深度
let parent = this.$parent
while (parent) {
const vnodeData = parent.$vnode && parent.$vnode.data
if (vnodeData) {
if (vnodeData.routerView) { // 说明是一个 router-view
++depth
}
}
parent = parent.$parent
}
let component = null
const { matched } = this.$router
if (matched[depth]) {
component = matched[depth].component
}
console.log('当前深度:', depth);
console.log('当前matched:', this.$router.matched);
return h(component)
}
})
Vue.component('router-link', Link)
Vue.component('router-view', View)
}
export default VueRouter