如同分析vuex源码我们首先通过一个简单例子进行了解vue-router是如何使用的,然后在分析在源码中是如何实现的
示例
下面示例来自于example/basica/app.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const Home = { template: 'home' }
const Foo = { template: 'foo' }
const Bar = { template: 'bar' }
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
new Vue({
router,
template: `
`
}).$mount('#app')
首先调用Vue.use(VueRouter),Vue.use()方法是Vue用来进行插件安装的方法,这里主要用来安装VueRouter。然后实例化了VueRouter,我们来看看VueRouter这个构造函数到底做了什么。
从源码入口文件src/index.js开始看
import type { Matcher } from './create-matcher'
export default class VueRouter {
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
init (app: any /* Vue component instance */) {
this.apps.push(app)
// main app already initialized.
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
getMatchedComponents (to?: RawLocation | Route): Array {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
}))
}
}
代码一步步看,先从constructor函数的实现,首先进行初始化我们来看看这些初始化条件分别代表的是什么
- this.app表示当前Vue实例
- this.apps表示所有app组件
- this.options表示传入的VueRouter的选项
- this.resolveHooks表示resolve钩子回调函数的数组,resolve用于解析目标位置
- this.matcher创建匹配函数
代码中有createMatcher()函数,来看看他的实现
function createMatcher (
routes,
router
) {
var ref = createRouteMap(routes);
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}
function match (
raw,
currentRoute,
redirectedFrom
) {
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;
// 命名路由处理
if (name) {
// nameMap[name]的路由记录
var record = nameMap[name];
...
location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
// _createRoute用于创建路由
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
// 普通路由处理
}
// no match
return _createRoute(null, location)
}
return {
match: match,
addRoutes: addRoutes
}
}
createMatcher()有两个参数routes表示创建VueRouter传入的routes配置信息,router表示VueRouter实例。createMatcher()的作用就是传入的routes通过createRouteMap创建对应的map,和一个创建map的方法。
我们先来看看createRouteMap()方法的定义
function createRouteMap (
routes,
oldPathList,
oldPathMap,
oldNameMap
) {
// 用于控制匹配优先级
var pathList = oldPathList || [];
// name 路由 map
var pathMap = oldPathMap || Object.create(null);
// name 路由 map
var nameMap = oldNameMap || Object.create(null);
// 遍历路由配置对象增加路由记录
routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});
// 确保通配符总是在pathList的最后,保证最后匹配
for (var i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0]);
l--;
i--;
}
}
return {
pathList: pathList,
pathMap: pathMap,
nameMap: nameMap
}
}
createRouteMap()有4个参数:routes代表的配置信息,oldPathList包含所有路径的数组用于匹配优先级,oldNameMap表示name map,oldPathMap表示path map。createRouteMap就是更新pathList,nameMap和pathMap。nameMap到底代表的是什么呢?它是包含路由记录的一个对象,每个属性值名是每个记录的path属性值,属性值就是具有这个path属性值的路由记录。这儿有一个叫路由记录的东西,这是什么意思呢?路由记录就是 routes 配置数组中的对象副本(还有在 children 数组),路由记录都是包含在matched属性中例如
const router = new VueRouter({
routes: [
// 下面的对象就是 route record
{ path: '/foo', component: Foo,
children: [
// 这也是个 route record
{ path: 'bar', component: Bar }
]
}
]
})
在上面代码中用一段代码用于给每个route添加路由记录,那么路由记录的实现是如何的呢,下面是addRouteReord()的实现
function addRouteRecord (
pathList,
pathMap,
nameMap,
route,
parent,
matchAs
) {
var path = route.path;
var name = route.name;
var normalizedPath = normalizePath(
path,
parent
);
var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name: name,
parent: parent,
matchAs: matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
};
if (route.children) {
route.children.forEach(function (child) {
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}
if (route.alias !== undefined) {
// 如果有别名的情况
}
if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}
}
addRouteRecord()这个函数的参数我都懒得说什么意思了,新增的parent也表示路由记录,首先获取path,name。然后通过normalizePath()规范格式,然后就是record这个对象的建立,然后遍历routes的子元素添加路由记录如果有别名的情况还需要考虑别名的情况然后更新path Map。
History
我们在回到VueRouter的构造函数中,往下看是模式的选择,一共这么几种模式一种history,hash和abstract三种。· 默认hash: 使用URL hash值作为路由,支持所有浏览器
· history: 依赖HTML5 History API和服务器配置
· abstract:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
默认是hash,路由通过“#”隔开,但是如果工程中有锚链接或者路由中有hash值,原先的“#”就会对页面跳转产生影响;所以就需要使用history模式。
在应用中我们常用的基本都是history模式,下面我们来看看HashHistory的构造函数
var History = function History (router, base) {
this.router = router;
this.base = normalizeBase(base);
this.current = START;
this.pending = null;
this.ready = false;
this.readyCbs = [];
this.readyErrorCbs = [];
this.errorCbs = [];
};
因为hash和history有一些相同的地方,所以HashHistory会在History构造函数上进行扩展下面是各个属性所代表的意义:
- this.router表示VueRouter实例
- this.base表示应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为
"/app/"。normalizeBase()用于格式化base - this.current开始时的route,route使用createRoute()创建
- this.pending表示进行时的route
- this.ready表示准备状态
- this.readyCbs表示准备回调函数
creatRoute()在文件src/util/route.js中,下面是他的实现
function createRoute (
record,
location,
redirectedFrom,
router
) {
var stringifyQuery$$1 = router && router.options.stringifyQuery;
var query = location.query || {};
try {
query = clone(query);
} catch (e) {}
var route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery$$1),
matched: record ? formatMatch(record) : []
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery$$1);
}
return Object.freeze(route)
}
createRoute有三个参数,record表示路由记录,location,redirectedFrom表示url地址信息对象,router表示VueRouter实例对象。通过传入的参数,返回一个冻结的route对象,route对象里边包含了一些有关location的属性。History包含了一些基本的方法,例如比较重要的方法有transitionTo(),下面是transitionTo()的具体实现。
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
var this$1 = this;
var route = this.router.match(location, this.current);
this.confirmTransition(route, function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();
// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) { cb(route); });
}
}, function (err) {
if (onAbort) {
onAbort(err);
}
if (err && !this$1.ready) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
}
});
};
首先match得到匹配的route对象,route对象在之前已经提到过。然后使用confirmTransition()确认过渡,更新route,ensureURL()的作用就是更新URL。如果ready为false,更改ready的值,然后对readyCbs数组进行遍历回调。下面来看看HTML5History的构造函数
var HTML5History = (function (History$$1) {
function HTML5History (router, base) {
var this$1 = this;
History$$1.call(this, router, base);
var initLocation = getLocation(this.base);
window.addEventListener('popstate', function (e) {
var current = this$1.current;
var location = getLocation(this$1.base);
if (this$1.current === START && location === initLocation) {
return
}
});
}
if ( History$$1 ) HTML5History.__proto__ = History$$1;
HTML5History.prototype = Object.create( History$$1 && History$$1.prototype );
HTML5History.prototype.constructor = HTML5History;
HTML5History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
HTML5History.prototype.replace = function replace (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
replaceState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
return HTML5History;
}(History))
在HTML5History()中代码多次用到了getLocation()那我们来看看他的具体实现吧
function getLocation (base) {
var path = window.location.pathname;
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length);
}
return (path || '/') + window.location.search + window.location.hash
}
用一个简单的地址来解释代码中各个部分的含义。例如http://example.com:1234/test/test.htm#part2?a=123,window.location.pathname=>/test/test.htm=>?a=123,window.location.hash=>#part2。
把我们继续回到HTML5History()中,首先继承history构造函数。然后监听popstate事件。当活动记录条目更改时,将触发popstate事件。需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。我们来看看HTML5History的push方法。location表示url信息,onComplete表示成功后的回调函数,onAbort表示失败的回调函数。首先获取current属性值,replaceState和pushState用于更新url,然后处理滚动。模式的选择就大概讲完了,我们回到入口文件,看看init()方法,app代表的是Vue的实例,现将app存入this.apps中,如果this.app已经存在就返回,如果不是就赋值。this.history是三种的实例对象,然后分情况进行transtionTo()操作,history方法就是给history.cb赋值穿进去的回调函数。
下面看getMatchedComponents(),唯一需要注意的就是我们多次提到的route.matched是路由记录的数据,最终返回的是每个路由记录的components属性值的值。
Router-View
最后讲讲router-view
var View = {
name: 'router-view',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render: function render (_, ref) {
var props = ref.props;
var children = ref.children;
var parent = ref.parent;
var data = ref.data;
// 解决嵌套深度问题
data.routerView = true;
var h = parent.$createElement;
var name = props.name;
// route
var route = parent.$route;
// 缓存
var cache = parent._routerViewCache || (parent._routerViewCache = {});
// 组件的嵌套深度
var depth = 0;
// 用于设置class值
var inactive = false;
// 组件的嵌套深度
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
if (parent._inactive) {
inactive = true;
}
parent = parent.$parent;
}
data.routerViewDepth = depth;
if (inactive) {
return h(cache[name], data, children)
}
var matched = route.matched[depth];
if (!matched) {
cache[name] = null;
return h()
}
var component = cache[name] = matched.components[name];
data.registerRouteInstance = function (vm, val) {
// val could be undefined for unregistration
var current = matched.instances[name];
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val;
}
}
;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
matched.instances[name] = vnode.componentInstance;
};
var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
if (propsToPass) {
propsToPass = data.props = extend({}, propsToPass);
var attrs = data.attrs = data.attrs || {};
for (var key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key];
delete propsToPass[key];
}
}
}
return h(component, data, children)
}
};
router-view比较简单,functional为true使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。props表示接受属性,下面来看看render函数,首先获取数据,然后缓存,_inactive用于处理keep-alive情况,获取路由记录,注册Route实例,h()用于渲染。很简单我也懒得一一再说。
小结
文章由入口文件入手,推导出本篇文章。由于篇幅限制,代码进行了一定的省略,将一些比较简单的代码进行了省略。