看完拉勾前端训练营关于Vue-Router的实现,干货满满,但Vue-Router的实现实在是绕,所以做一下笔记,确认以及加深自己的了解。进了拉勾前端训练营两个多月,收获还是挺多的,群里不少大牛,还有美女班主任,导师及时回答学员的疑问,幽默风趣,真是群里一席谈,胜读四年本科(literally true,四年本科的课程真的水=_=)。
实现的功能
实现前,看一下实现的功能:
- 基本路由功能
- 子路由功能
- History及Hash功能
创建一个项目。首先肯定是要创建Vue Router的类,在根目录下创建index.js文件:
export default class VueRouter {
constructor (option) {
this._routes = options.routes || []
}
init () {}
}
我们平时创建路由实例时,会传入一个对象,像这样:
const router = new VueRouter({
routes
})
所以构造函数应该要有一个对象,如果里面有路由routes,赋值给this._routes,否则给它一个空数组。options里当然有其他属性,但先不管,之后再实现。 还有一个init方法,用来初始化设定。
install
由于Vue Router是插件,要想使用它,必须通过Vue.use方法。该方法会判定传入的参数是对象还函数,如果是对象,则调用里面的install方法,函数的话则直接调用。 Vue Router是一个对象,所以要有install方法。实现install之前,看一下Vue.use的源码,这样可以更好理解怎样实现install:
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
首先Vue.use会先判定Vue有没有一个属性叫_installedPlugins,有则引用,没有就为Vue添加属性_installedPlugins,它是一个空数组,再去引用它。_installedPlugins是记录安装过的插件。接下来判定_installedPlugins里有没有传入的插件,有则不用安装。 把传入的参数从第二个开始,变成数组,把Vue放入数组首位。如果插件是对象,则调用它的install方法,插件方法里的上下文this依然是它自身,传入刚才变成数组的参数。函数的话,不用考虑上下文,直接调用。最后记录该插件是安装过的。
现在简单把install方法实现,在根目录下新建install.js:
export let _Vue = null
export default function install (Vue) {
_Vue = Vue
_Vue.mixin({
beforeCreate () {
if (this.$options.router) {
this._router = this.$options.router
this._routerRoot = this
// 初始化 router 对象
this._router.init(this)
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
全局变量_Vue是为了方便其他Vue Router模块的引用,不然的话其他模式需要引入Vue,比较麻烦。mixin是把Vue中某些功能抽取出来,方便在不同地方複用,这里的用法是全局挂载鈎子函数。
先判断是否为根实例,如果是根实例,会有路由传入,所以会$options.router存在。根实例的话则添加两个私有属性,其中_routerRoot是为了方便根实例以下的组件引用,然后初始化router。如果是根实例下的组件,去找一下有没有父组件,有就引用它的_routerRoot,这样可以通过_routerRoot.router来引用路由。
挂载函数基本完成。当我们使用Vue Router,还有两个组件挂载:Router Link和Router View。在根目录下创建文件夹components,创建文件link.js和view.js。先把Router Link实现:
export default {
name: 'RouterLink',
props: {
to: {
type: String,
required: true
}
},
render (h) {
return h('a', { attrs: { href: '#' + this.to } }, [this.$slots.default])
}
}
RouterLink接收一个参数to,类型是字符串。这里不使用template,是因为运行版本的vue没有编译器,把模板转为渲染函数,要直接用渲染函数。
简单讲一下渲染函数的用法,第一个参数是标签类型,第二个是标签的属性,第三是内容。详细可以看vue文档。
我们要实现的其实是<a :href="{{ '#' + this.to }}"><slot name="default"></slot></a>
。所以第一个参数是a,第二个它的连接,第三个之所以要用数组,是因为标签的内容是一个slot标签节点,子节点要用数组包起来。
至于RouterView,现在不知道它的实现,大概写一下:
export default {
name: 'RouterView',
render (h) {
return h ()
}
}
在install里把两个组件注册:
import Link from './components/link'
import View from './components/view'
export default function install (Vue) {
...
_Vue.component(Link.name, Link)
_Vue.component(View.name, View)
}
createMatcher
接下来要创建create-matcher,它是用来生成匹配器,主要返回两个方法:match和addRoutes。前者是匹配输入路径,获取路由表相关资料,后者是手动添加路由规则到路由表。这两个方法都是要依赖路由表,所以我们还要实现路由表生成器:create-router-map,它接收路由规则,返回一个路由表,它是对象,里面有两个属性,一个是pathList,它是一个数组,存有所有路由表的路径,另一个是pathMap,是一个字典,键是路径,而值的路径相应的资料。 在项目根目录下创建create-router-map.js:
export default function createRouteMap (routes) {
// 存储所有的路由地址
const pathList = []
// 路由表,路径和组件的相关信息
const pathMap = {}
return {
pathList,
pathMap
}
}
我们需要遍历路由规则,在这过程中做两件事:
- 把所有路径存入pathList
- 把路由和资料对应关係放入pathMap
这里的难点是有子路由,所以要用递归,但现在先不要考虑这问题,简单把功能实现:
function addRouteRecord (route, pathList, pathMap, parentRecord) {
const path = route.path
const record = {
path: path,
component: route.component,
parentRecord: parentRecord
// ...
}
// 判断当前路径,是否已经存储在路由表中了
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
}
现在考虑一下子路由的问题。首先要先有判定路由是否有子路由,有的话遍历子路由,递归处理,还要考虑路径名称问题,如果是子路由,path应该是父子路径合并,所以这里要判定是否存有父路由。
function addRouteRecord (route, pathList, pathMap, parentRecord) {
const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path
const record = {
path: path,
component: route.component,
parentRecord: parentRecord
// ...
}
// 判断当前路径,是否已经存储在路由表中了
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
// 判断当前的route是否有子路由
if (route.children) {
route.children.forEach(childRoute => {
addRouteRecord(childRoute, pathList, pathMap, route)
})
}
}
如果有传入父路由资料,path是父子路径合并。
最后把addRouteRecord添加到createRouteMap:
export default function createRouteMap (routes) {
// 存储所有的路由地址
const pathList = []
// 路由表,路径和组件的相关信息
const pathMap = {}
// 遍历所有的路由规则 routes
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap)
})
return {
pathList,
pathMap
}
}
createRouteMap实现了,可以把create-matcher的路由表创建和addRoute实现:
import createRouteMap from './create-route-map'
export default function createMatcher (routes) {
const { pathList, pathMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap)
}
return {
match,
addRoutes
}
}
最后要实现match了,它接收一个路径,然后返回路径相关资料,相关资料不仅仅是它自身的,还有它的父路径的资料。这里先实现一个工具类函数,它是专门创建路由的,就是返回路径以及它的相关资料。创建util/route.js:
export default function createRoute (record, path) {
// 创建路由数据对象
// route ==> { matched, path } matched ==> [record1, record2]
const matched = []
while (record) {
matched.unshift(record)
record = record.parentRecord
}
return {
matched,
path
}
其实功能很简单,就是不断获取上一级的资料,放进数组首位。配上createRoute,match基本就实现了:
import createRoute from './util/route'
function match (path) {
const record = pathMap[path]
if (record) {
// 创建路由数据对象
// route ==> { matched, path } matched ==> [record1, record2]
return createRoute(record, path)
}
return createRoute(null, path)
}
在VueRouter的构造函数里把matcher加上:
import createMatcher from './create-matcher'
export default class VueRouter {
constructor (options) {
this._routes = options.routes || []
this.matcher = createMatcher(this._routes)
...
History历史管理
matcher做好后,开始实现History类吧,它的目的是根据用户设定的模式,管理路径,通知 RouterView把路径对应的组件渲染出来。
在项目根目录新建history/base.js:
import createRoute from '../util/route'
export default class History {
constructor (router) {
this.router = router
// 记录当前路径对应的 route 对象 { matched, path }
this.current = createRoute(null, '/')
}
transitionTo (path, onComplete) {
this.current = this.router.matcher.match(path)
onComplete && onComplete()
}
}
创建时当时路径先默认为根路径,current是路由对象,属性有路径名和相关资料,transitionTo是路径跳转时调用的方法,它更改current和调用回调函数。 之后不同模式(如hash或history)的类都是继承History。这里只实现HashHistory:
import History from './base'
export default class HashHistory extends History {
constructor (router) {
super(router)
// 保证首次访问的时候 #/
ensureSlash()
}
getCurrentLocation () {
return window.location.hash.slice(1)
}
setUpListener () {
window.addEventListener('hashchange', () => {
this.transitionTo(this.getCurrentLocation())
})
}
}
function ensureSlash () {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
HashHistory基本是围绕window.location.hash,所以先讲一下它。简单来说,它会返回#后面的路径名。如果对它赋值,它会在最前面加上#。明白window.location.hash后,其他方法都不难理解。setUpListener注册一个hashchange事件,表示当哈希路径(#后的路径)发生变化,调用注册的函数。
html5模式不实现了,继承HashHistory算了:
import History from './base'
export default class HTML5History extends History {
}
History的类基本实现了,但是现在还不是响应式的,意味着即使实例发生变化,视图不会变化。这问题后解决。
回到VueRouter的构造函数:
constructor(options)
...
const mode = this.mode = options.mode || 'hash'
switch (mode) {
case 'hash':
this.history = new HashHistory(this)
break
case 'history':
this.history = new HTML5History(this)
break
default:
throw new Error('mode error')
}
}
这里使用了简单工厂模式 (Simple Factory Pattern),就是设计模式中工厂模式的简易版。它存有不同的类,这些类都是继承同一类的,它通过传入的参数进行判断,创建相应的实例返回。简单工厂模式的好处是用户不用考虑创建实例的细节,他要做的是导入工厂,往工厂传入参数,就可获得实例。
init
之前的History有一个问题,就是它不是响应式的,也就是说,路径发生变化,浏覧器不会有任何反应,要想为响应式,可以给它一个回调函数:
import createRoute from '../util/route'
export default class History {
constructor (router) {
...
this.cb = null
}
...
listen (cb) {
this.cb = cb
}
transitionTo (path, onComplete) {
this.current = this.router.matcher.match(path)
this.cb && this.cb(this.current)
onComplete && onComplete()
}
}
加上listen方法,为History添加回调函数,当路径发生转变时调用。
把之前的初始化方法init补上:
init (app) {
// app 是 Vue 的实例
const history = this.history
history.listen(current => {
app._route = current
})
history.transitionTo(
history.getCurrentLocation(),
history.setUpListener
)
}
给history的回调函数是路径发生变化,把路由传给vue实例,然后是转换至当前路径,完成时调用history.setUpListener。不过直接把history.setUpListener放进去有一个问题,因为这等于是仅仅把setUpListener放进去,里面的this指向window,所以要用箭头函数封装,这样的话,就会调用history.setUpListener,this指向history。
init (app) {
// app 是 Vue 的实例
const history = this.history
const setUpListener = () => {
history.setUpListener()
}
history.listen(current => {
app._route = current
})
history.transitionTo(
history.getCurrentLocation(),
setUpListener
)
}
用箭头函数把history.setUpListener封装一下,this就指向history。
install补完
init完成实现,回来把install的剩馀地方实现了。当初始化完成后,把vue实例的路由(不是路由表)变成响应式,可以使用 Vue.util.defineReactive(this, '_route', this._router.history.current)
,就是为vue实例添加一个属性_route,它的值是this._router.history.current,最后添加router和route。
完整代码如下:
import Link from './components/link'
import View from './components/view'
export let _Vue = null
export default function install (Vue) {
// 判断该插件是否注册略过,可以参考源码
_Vue = Vue
// Vue.prototype.xx
_Vue.mixin({
beforeCreate () {
// 给所有 Vue 实例,增加 router 的属性
// 根实例
// 以及所有的组件增加 router 属性
if (this.$options.router) {
this._router = this.$options.router
this._routerRoot = this
// 初始化 router 对象
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
// this.$parent
// this.$children
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
_Vue.component(Link.name, Link)
_Vue.component(View.name, View)
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
}
现在就可以如平时开发一样,使用router和route。
RouterView
最后把RouterView实现。其实它也没什么,就是获取当取路径,从路径中得到组件,然后渲染出来。问题是要考虑父子组件的问题。把思想整理一下,当有父组件时,肯定是父组件已经渲染出来,子组件是从父组件的RouterView组件渲染,还有是$route有的是当前路径和匹配的资料的数组,即包括父组件的数组,所以可遍历获得要渲染的组件:
export default {
name: 'RouterView',
render (h) {
const route = this.$route
let depth = 0
//routerView表示已经完成渲染了
this.routerView = true
let parent = this.$parent
while (parent) {
if (parent.routerView) {
depth++
}
parent = parent.$parent
}
const record = route.matched[depth]
if (record) {
return h(record.component)
}
return h()
}
}
if (parent.routerView)
是因为是确认父组件是否已经渲染,如果渲染,它的routerView为true,用depth来记录有多少父路由,然后通过它获取matched的资料,有的话则渲染获取的组件。
总结
Vue Router的代码量不多,但实在是绕,简单总结一下比较好。先看一下项目结构:
用一张表把所有的文件作用简述一遍:
文件 | 作用 | index.js | 存放VueRouter类 | install.js | 插件类必须要有的函数,用来给Vue.use调用 | create-route-map.js | 生成路由表,它输出一个对象,有pathList和pathMap属性,前者是存有所有路径的数组,后者是字典,把路径和它的资料对应 | util/route.js | 一个函数接收路径为参数,返回路由对象,存有matched和path属性,matched是匹配到的路径的资料和父路径资料,它是一个数组,path是路径本身 | create-matcher.js | 利用create-route-map创建路由表,且返回两个函数,一个是用util/route匹配路由,另一个是手动把路由规则转变成路由 | history/base.js | History类文件,用来作历史管理,存有当前路径的路由,以及转换路径的方法 | history/hash.js | HashHistory类文件,继承至History,用作hash模式下的历史管理 | components/link.js | Router-Link的组件 | components/view.js | Router-View的组件 |
---|
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!