matcher 相关的实现都在 src/create-matcher.js 中
export type Matcher = {match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;addRoutes: (routes: Array<RouteConfig>) => void;addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;getRoutes: () => Array<RouteRecord>;};
路由中重要的 2 个概念
Loaction 和 Route的数据结构定义在 flow/declarations.js 中
Location
declare type Location = {_normalized?: boolean;name?: string;path?: string;hash?: string;query?: Dictionary<string>;params?: Dictionary<string>;append?: boolean;replace?: boolean;}
和浏览器提供的 window.location 部分结构有点类似,它们都是对 url 的结构化描述
例子
/abc?foo=bar&baz=qux#hellopath /abcquery {foo:'bar',baz:'qux'}
Route
declare type Route = {path: string;name: ?string;hash: string;query: Dictionary<string>;params: Dictionary<string>;fullPath: string;matched: Array<RouteRecord>;redirectedFrom?: string;meta?: any;}
Route 表示的是路由中的一条线路,它除了描述了类似 Loctaion 的 path、query、hash 这些概念,还有 matched 表示匹配到的所有的 RouteRecord
createMatcher
export function createMatcher (routes: Array<RouteConfig>, // 用户定义的路由配置router: VueRouter // new VueRouter返回的实例): Matcher {// createRouteMap创建路由映射表const { pathList, pathMap, nameMap } = createRouteMap(routes)function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap)}function addRoute (parentOrRoute, route) {const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined// $flow-disable-linecreateRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)// add aliases of parentif (parent && parent.alias.length) {createRouteMap(// $flow-disable-line route is defined if parent isparent.alias.map(alias => ({ path: alias, children: [route] })),pathList,pathMap,nameMap,parent)}}function getRoutes () {return pathList.map(path => pathMap[path])}function match (raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location): Route {const location = normalizeLocation(raw, currentRoute, false, router)const { name } = locationif (name) {const record = nameMap[name]if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`)}if (!record) return _createRoute(null, location)const paramNames = record.regex.keys.filter(key => !key.optional).map(key => key.name)if (typeof location.params !== 'object') {location.params = {}}if (currentRoute && typeof currentRoute.params === 'object') {for (const key in currentRoute.params) {if (!(key in location.params) && paramNames.indexOf(key) > -1) {location.params[key] = currentRoute.params[key]}}}location.path = fillParams(record.path, location.params, `named route "${name}"`)return _createRoute(record, location, redirectedFrom)} else if (location.path) {location.params = {}for (let i = 0; i < pathList.length; i++) {const path = pathList[i]const record = pathMap[path]if (matchRoute(record.regex, location.path, location.params)) {return _createRoute(record, location, redirectedFrom)}}}// no matchreturn _createRoute(null, location)}function redirect (record: RouteRecord,location: Location): Route {const originalRedirect = record.redirectlet redirect = typeof originalRedirect === 'function'? originalRedirect(createRoute(record, location, null, router)): originalRedirectif (typeof redirect === 'string') {redirect = { path: redirect }}if (!redirect || typeof redirect !== 'object') {if (process.env.NODE_ENV !== 'production') {warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)}return _createRoute(null, location)}const re: Object = redirectconst { name, path } = relet { query, hash, params } = locationquery = re.hasOwnProperty('query') ? re.query : queryhash = re.hasOwnProperty('hash') ? re.hash : hashparams = re.hasOwnProperty('params') ? re.params : paramsif (name) {// resolved named directconst targetRecord = nameMap[name]if (process.env.NODE_ENV !== 'production') {assert(targetRecord, `redirect failed: named route "${name}" not found.`)}return match({_normalized: true,name,query,hash,params}, undefined, location)} else if (path) {// 1. resolve relative redirectconst rawPath = resolveRecordPath(path, record)// 2. resolve paramsconst resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)// 3. rematch with existing query and hashreturn match({_normalized: true,path: resolvedPath,query,hash}, undefined, location)} else {if (process.env.NODE_ENV !== 'production') {warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)}return _createRoute(null, location)}}function alias (record: RouteRecord,location: Location,matchAs: string): Route {const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)const aliasedMatch = match({_normalized: true,path: aliasedPath})if (aliasedMatch) {const matched = aliasedMatch.matchedconst aliasedRecord = matched[matched.length - 1]location.params = aliasedMatch.paramsreturn _createRoute(aliasedRecord, location)}return _createRoute(null, location)}function _createRoute (record: ?RouteRecord,location: Location,redirectedFrom?: Location): Route {if (record && record.redirect) {return redirect(record, redirectedFrom || location)}if (record && record.matchAs) {return alias(record, location, record.matchAs)}return createRoute(record, location, redirectedFrom, router)}// matcher是一个对象,对外暴露了match和addRoutes方法return {match,addRoute,getRoutes,addRoutes}}
createRouteMap定义在 src/create-route-map 中
// 把用户的路由配置转换成一张路由映射表export function createRouteMap (routes: Array<RouteConfig>,oldPathList?: Array<string>,oldPathMap?: Dictionary<RouteRecord>,oldNameMap?: Dictionary<RouteRecord>,parentRoute?: RouteRecord): {pathList: Array<string>,pathMap: Dictionary<RouteRecord>,nameMap: Dictionary<RouteRecord>} {// the path list is used to control path matching priority// pathList存储所有的pathconst pathList: Array<string> = oldPathList || []// $flow-disable-line// pathMap表示一个path到RouteRecord的映射关系const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)// $flow-disable-line// nameMap表示name到RouteRecord的映射关系const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)// 遍历routes为每一个route执行addRouteRecord方法生成一条记录routes.forEach(route => {addRouteRecord(pathList, pathMap, nameMap, route, parentRoute) // 不断给pathList, pathMap, nameMap添加数据})// pathList是为了记录路由配置中的所有path// pathMap, nameMap都是为了通过path和name能快速查找到对应的RouteRecord// ensure wildcard routes are always at the endfor (let i = 0, l = pathList.length; i < l; i++) {if (pathList[i] === '*') {pathList.push(pathList.splice(i, 1)[0])l--i--}}if (process.env.NODE_ENV === 'development') {// warn if routes do not include leading slashesconst found = pathList// check for missing leading slash.filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')if (found.length > 0) {const pathNames = found.map(path => `- ${path}`).join('\n')warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)}}return {pathList,pathMap,nameMap}}
RouteRecord的数据结构
declare type RouteRecord = {path: string;alias: Array<string>;regex: RouteRegExp;components: Dictionary<any>;instances: Dictionary<any>;enteredCbs: Dictionary<Array<Function>>;name: ?string;parent: ?RouteRecord;redirect: ?RedirectOption;matchAs: ?string;beforeEnter: ?NavigationGuard;meta: any;props: boolean | Object | Function | Dictionary<boolean | Object | Function>;}
addRouteRecord
function addRouteRecord (pathList: Array<string>,pathMap: Dictionary<RouteRecord>,nameMap: Dictionary<RouteRecord>,route: RouteConfig,parent?: RouteRecord,matchAs?: string) {const { path, name } = routeif (process.env.NODE_ENV !== 'production') {assert(path != null, `"path" is required in a route configuration.`)assert(typeof route.component !== 'string',`route config "component" for path: ${String(path || name)} cannot be a ` + `string id. Use an actual component instead.`)warn(// eslint-disable-next-line no-control-regex!/[^\u0000-\u007F]+/.test(path),`Route with path "${path}" contains unencoded characters, make sure ` +`your path is correctly encoded before passing it to the router. Use ` +`encodeURI to encode static segments of your path.`)}const pathToRegexpOptions: PathToRegexpOptions =route.pathToRegexpOptions || {}const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)if (typeof route.caseSensitive === 'boolean') {pathToRegexpOptions.sensitive = route.caseSensitive}// 1.创建RouteRecordconst record: RouteRecord = {path: normalizedPath, // path是规范化后的路径 - 会根据parent的path做计算regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 一个正则表达式的扩展 - 利用了path-to-regexp这个工具库,把path解析成一个正则表达式的扩展components: route.components || { default: route.component }, // components是一个对象,通常在配置中写的component实际上会被转换成{components: route.component}alias: route.alias? typeof route.alias === 'string'? [route.alias]: route.alias: [],instances: {}, // 表示组件的实例,也是一个对象类型enteredCbs: {},name,parent, // 表示父的RouteRecord,配置时会配置子路由,所以整个RouteRecord也就是一个树型结构matchAs,redirect: route.redirect,beforeEnter: route.beforeEnter,meta: route.meta || {},props:route.props == null? {}: route.components? route.props: { default: route.props }}// 如果配置了childrenif (route.children) {// Warn if route is named, does not redirect and has a default child route.// If users navigate to this route by name, the default child will// not be rendered (GH Issue #629)if (process.env.NODE_ENV !== 'production') {if (route.name &&!route.redirect &&route.children.some(child => /^\/?$/.test(child.path))) {warn(false,`Named Route '${route.name}' has a default child route. ` +`When navigating to this named route (:to="{name: '${route.name}'"), ` +`the default child route will not be rendered. Remove the name from ` +`this route and use the name of the default child route for named ` +`links instead.`)}}// 递归执行addRouteRecord方法,并把当前的record作为parent传入,通过这样的深度遍历就可以拿到一个route下的完整记录route.children.forEach(child => {const childMatchAs = matchAs? cleanPath(`${matchAs}/${child.path}`): undefinedaddRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)})}// 为pathList和pathMap各添加一条记录if (!pathMap[record.path]) {pathList.push(record.path)pathMap[record.path] = record}if (route.alias !== undefined) {const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]for (let i = 0; i < aliases.length; ++i) {const alias = aliases[i]if (process.env.NODE_ENV !== 'production' && alias === path) {warn(false,`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`)// skip in dev to make it workcontinue}const aliasRoute = {path: alias,children: route.children}addRouteRecord(pathList,pathMap,nameMap,aliasRoute,parent,record.path || '/' // matchAs)}}// 如果在路由配置中配置了name,则给nameMap添加一条记录if (name) {if (!nameMap[name]) {nameMap[name] = record} else if (process.env.NODE_ENV !== 'production' && !matchAs) {warn(false,`Duplicate named routes definition: ` +`{ name: "${name}", path: "${record.path}" }`)}}}
path-to-regexp例子
var keys = []var re = pathToRegexp('/foo/:bar', keys)// re = /^\/foo\/([^\/]+?)\/?$/i// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
addRoutes
addRoutes 方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由,所以 Vue-Router 也提供了这一接口
function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap)}
addRoutes 的方法十分简单,再次调用 createRouteMap 即可,传入新的 routes 配置,由于 pathList、pathMap、nameMap 都是引用类型,执行 addRoutes 后会修改它们的值
match
function match (raw: RawLocation, // RawLocation类型,可以是url字符串,也可以是Location对象currentRoute?: Route, // Route类型,表示当前的路径redirectedFrom?: Location // 和重定向相关): Route {//const location = normalizeLocation(raw, currentRoute, false, router)const { name } = locationif (name) {const record = nameMap[name]if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`)}if (!record) return _createRoute(null, location)const paramNames = record.regex.keys.filter(key => !key.optional).map(key => key.name)if (typeof location.params !== 'object') {location.params = {}}if (currentRoute && typeof currentRoute.params === 'object') {for (const key in currentRoute.params) {if (!(key in location.params) && paramNames.indexOf(key) > -1) {location.params[key] = currentRoute.params[key]}}}location.path = fillParams(record.path, location.params, `named route "${name}"`)return _createRoute(record, location, redirectedFrom)} else if (location.path) {location.params = {}for (let i = 0; i < pathList.length; i++) {const path = pathList[i]const record = pathMap[path]if (matchRoute(record.regex, location.path, location.params)) {return _createRoute(record, location, redirectedFrom)}}}// no match// 返回的是一个路径,根据传入的raw和当前的路径currentRoute计算出一个新的路径并返回return _createRoute(null, location)}
normalizeLocation定义在 src/util/location.js 中
// 根据raw和current计算出新的locationexport function normalizeLocation (raw: RawLocation,current: ?Route,append: ?boolean,router: ?VueRouter): Location {let next: Location = typeof raw === 'string' ? { path: raw } : raw// named targetif (next._normalized) {return next} else if (next.name) {next = extend({}, raw)const params = next.paramsif (params && typeof params === 'object') {next.params = extend({}, params)}return next}// relative paramsif (!next.path && next.params && current) {next = extend({}, next)next._normalized = trueconst params: any = extend(extend({}, current.params), next.params)if (current.name) {next.name = current.namenext.params = params} else if (current.matched.length) {const rawPath = current.matched[current.matched.length - 1].pathnext.path = fillParams(rawPath, params, `path ${current.path}`)} else if (process.env.NODE_ENV !== 'production') {warn(false, `relative params navigation requires a current route.`)}return next}const parsedPath = parsePath(next.path || '')const basePath = (current && current.path) || '/'const path = parsedPath.path? resolvePath(parsedPath.path, basePath, append || next.append): basePathconst query = resolveQuery(parsedPath.query,next.query,router && router.options.parseQuery)let hash = next.hash || parsedPath.hashif (hash && hash.charAt(0) !== '#') {hash = `#${hash}`}return {_normalized: true,path,query,hash}}
normalizeLocation主要处理2种情况,一种是有 params 且没有 path,一种是有 path 的
对于第一种情况,如果 current 有 name,则计算出的 location 也有 name
计算出新的 location 后,对 location 的 name 和 path 的两种情况做了处理
name
有 name 的情况下就根据 nameMap 匹配到 record,它就是一个 RouterRecord 对象,如果 record 不存在,则匹配失败,返回一个空路径;然后拿到 record 对应的 paramNames,再对比 currentRoute 中的 params,把交集部分的 params 添加到 location 中,然后在通过 fillParams 方法根据 record.path 和 location.path 计算出 location.path,最后调用 _createRoute(record, location, redirectedFrom) 去生成一条新路径
path
通过 name 可以很快的找到 record,但是通过 path 并不能,因为计算后的 location.path 是一个真实路径,而 record 中的 path 可能会有 param,因此需要对所有的 pathList 做顺序遍历, 然后通过 matchRoute 方法根据 record.regex、location.path、location.params 匹配,如果匹配到则也通过 _createRoute(record, location, redirectedFrom) 去生成一条新路径。因为是顺序遍历,所以书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配
_createRoute
// 先不考虑 record.redirect 和 record.matchAs 的情况,最终会调用 createRoute 方法function _createRoute (record: ?RouteRecord,location: Location,redirectedFrom?: Location): Route {if (record && record.redirect) {return redirect(record, redirectedFrom || location)}if (record && record.matchAs) {return alias(record, location, record.matchAs)}return createRoute(record, location, redirectedFrom, router)}return {match,addRoute,getRoutes,addRoutes}
createRoute定义在 src/uitl/route.js 中
// 根据record和location创建出来,最终返回的是一条Route路径export function createRoute (record: ?RouteRecord,location: Location,redirectedFrom?: ?Location,router?: VueRouter): Route {const stringifyQuery = router && router.options.stringifyQuerylet query: any = location.query || {}try {query = clone(query)} catch (e) {}const route: Route = {name: location.name || (record && record.name),meta: (record && record.meta) || {},path: location.path || '/',hash: location.hash || '',query,params: location.params || {},fullPath: getFullPath(location, stringifyQuery),matched: record ? formatMatch(record) : []}if (redirectedFrom) {route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)}return Object.freeze(route)}
在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,并且它最后是不可以被外部修改的。Route 对象中有一个非常重要属性是 matched,它通过 formatMatch(record) 计算而来
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {const res = []while (record) {res.unshift(record)record = record.parent}return res}
可以看它是通过 record 循环向上找 parent,直到找到最外层,并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,它记录了一条线路上的所有 record
matched 属性非常有用,它为之后渲染组件提供了依据
了解了 Location、Route、RouteRecord 等概念,并通过 matcher 的 match 方法,找到匹配的路径 Route,这个对 Route 的切换,组件的渲染都有非常重要的指导意义
