背景
这两天在学习vue-router的源码,通过造轮子,把学到的东西进行巩固吧。
Router类的设计
在使用vue-router的时候我们通常会传入这样的参数:
new Router({
base: '/example',
mode: 'hash',
routes: [
{
path: '/',
name: 'home',
component: '<div>home</div>'
}
]
})
很显然Router是一个类,传入的参数就是构造函数的参数因此我们的构造函数设计如下:
class Router {
constructor (options) {
this.base = options.base
this.routes = options.routes
this.mode = options.mode || 'hash'
this.init()
}
init() {}
}
hash和history两种模式基类
设计完了Router类,我们还需要根据不同的mode采取不同的处理方式,即hash模式和history模式。但是这两种模式都有一些共同的属性,例如:path,query,params,name,fullPath, route等属性,还有对路由匹配的处理,因此我们将这些通用方法设计成一个基类。
class Base {
constructor (router) {
this.router = router
this.current = {
path: '/',
query: {},
params: {},
name: '',
fullPath: '/',
route: {}
}
}
// 这里的taregt就是浏览器中获取path,如:/foo /bar
transitionTo(target, cb) {
// 通过对比传入的 routes 获取匹配到的 targetRoute 对象
const targetRoute = match(target, this.router.routes)
this.confirmTransition(targetRoute, () => {
this.current.route = targetRoute
this.current.name = targetRoute.name
this.current.path = targetRoute.path
this.current.query = targetRoute.query || getQuery()
this.current.fullPath = getFullPath(this.current)
cb && cb()
})
}
confirmTransition (route, cb) {
cb()
}
}
function getFullPath ({ path, query = {}, hash = '' }, _stringifyQuery){
const stringify = _stringifyQuery || stringifyQuery
return (path || '/') + stringify(query) + hash
}
// 用这些path从而筛选出对应的route
export function match(path, routeMap) {
let match = {}
if (typeof path === 'string' || path.name === undefined) {
for(let route of routeMap) {
if (route.path === path || route.path === path.path) {
match = route
break;
}
}
} else {
for(let route of routeMap) {
if (route.name === path.name) {
match = route
if (path.query) {
match.query = path.query
}
break;
}
}
}
return match
}
// 获取url中的参数
export function getQuery() {
const hash = location.hash
const queryStr = hash.indexOf('?') !== -1 ? hash.substring(hash.indexOf('?') + 1) : ''
const queryArray = queryStr ? queryStr.split('&') : []
let query = {}
queryArray.forEach((q) => {
let qArray = q.split('=')
query[qArray[0]] = qArray[1]
})
return query
}
function stringifyQuery (obj) {
const res = obj ? Object.keys(obj).map(key => {
const val = obj[key]
if (val === undefined) {
return ''
}
if (val === null) {
return key
}
if (Array.isArray(val)) {
const result = []
val.forEach(val2 => {
if (val2 === undefined) {
return
}
if (val2 === null) {
result.push(key)
} else {
result.push(key + '=' + val2)
}
})
return result.join('&')
}
return key + '=' + val
}).filter(x => x.length > 0).join('&') : null
return res ? `?${res}` : ''
}
HashHistory实现
class HashHistory extends Base {
constructor (router) {
super(router)
this.ensureSlash()
// 监听hashchange事件
window.addEventListener('hashchange', () => {
this.transitionTo(this.getCurrentLocation())
})
}
push (location) {
const targetRoute = match(location, this.router.routes)
this.transitionTo(targetRoute, () => {
changeUrl(this.current.fullPath.substring(1))
})
}
replaceState (location) {
const targetRoute = match(location, this.router.routes)
this.transitionTo(targetRoute, () => {
changeUrl(this.current.fullPath.substring(1), true)
})
}
ensureSlash () {
const path = this.getCurrentLocation()
if (path.charAt(0) === '/') {
return true
}
changeUrl(path)
return false
}
// 获取当前路由
getCurrentLocation() {
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
}
// 处理浏览器路由跳转变化
// 这里使用了:
// window.history.replaceState({}, '', url)
// window.history.pushState({}, '', url)
function changeUrl(path, replace) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
if (replace) {
window.history.replaceState({}, '', `${base}#/${path}`)
} else {
window.history.pushState({}, '', `${base}#/${path}`)
}
}
window.history.replaceState和window.history.pushState的区别
Html5History实现
class HTML5History extends Base {
constructor (router) {
super(router)
window.addEventListener('popstate', () => {
this.transitionTo(getLocation())
})
}
push (location) {
const targetRoute = match(location, this.router.routes)
this.transitionTo(targetRoute, () => {
changeUrl(this.router.base, this.current.fullPath)
})
}
getCurrentLocation () {
return getLocation(this.router.base)
}
}
function getLocation (base = ''){
let path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
function changeUrl(base, path, replace) {
if (replace) {
window.history.replaceState({}, '', (base + path).replace(/\/\//g, '/'))
} else {
window.history.pushState({}, '', (base + path).replace(/\/\//g, '/'))
}
}
整合Router
前面我聊了Router类,hash模式下如何处理,history模式下如何处理,但是如何与我们的构造函数Router类整合呢?或者说Router中对这两种路由模式如何处理呢?
import { HTML5History } from './history/HTML5History'
import { HashHistory } from './history/HashHistory'
class Router {
constructor (options) {
// 省略之前代码
this.mode = options.mode || 'hash'
this.history = this.mode === 'hash' ?
new HashHistory(options) :
new HTML5History(options)
}
}
这样就实现了针对不同的mode对应不同的路由解析办法, 但是还有一个问题,我们上面的代码实现了可以动态切换路由,但是切换路由的同时视图如何跟着渲染呢?我们可以利用vue的双向绑定,从而实现当路由切换的同时视图也跟着切换
import { Watcher } from './utils/Watcher'
class Router {
constructor (options) {
// 省略代码
this.init()
this.history = this.mode === 'hash' ?
new HashHistory(options) :
new HTML5History(options)
}
// 简单搞个render()说明问题就好
render () {
let i
if ((i = this.history.current) && (i = i.route) && (i = i.component)) {
document.getElementById(this.container).innerHTML = i
}
}
init () {
const history = this.history
observer.call(this, this.history.current)
new Watcher(this.history.current, 'route', this.render.bind(this))
history.transitionTo(history.getCurrentLocation())
}
}
接下来我们来依次实现这几个函数,顺便也学习了vue中的双向绑定
- observer函数实现
class Observer {
constructor (value) {
this.walk(value)
}
walk (obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
this.walk(obj[key])
}
defineRective(obj, key, obj[key])
})
}
}
function defineRective (obj, key, value) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get () {
if (dep.target) {
dep.add()
}
return value
},
set (newVal) {
// 注意这两个顺序
value = newVal
dep.notify()
}
})
}
- Dep类的实现
class Dep {
constructor () {
this.listeners = []
}
add () {
this.listeners.push(Dep.target)
}
notify () {
this.listeners.forEach(listen => listen.update())
}
}
export function setTarget (target) {
Dep.target = target
}
export function cleanTarget() {
Dep.target = null
}
- watcher函数实现
import {setTarget, cleanTarget} from './dep'
export class Watcher {
constructor (vm, expression, callback) {
this.vm = vm
this.callbacks = []
this.expression = expression
this.callbacks.push(callback)
this.value = this.getVal()
}
getVal () {
setTarget(this)
let val = this.vm
this.expression.split('.').forEach((key) => {
val = val[key]
})
cleanTarget()
return val
}
update () {
this.callbacks.forEach((cb) => {
cb()
})
}
}
参考资料: