vue-router源码解析(四)路由匹配规则
前面我们讲过,在使用 vue-router
的时候,主要有以下几个步骤:
// 1. 安装 插件 Vue.use(VueRouter); // 2. 创建router对象 const router = new VueRouter({ routes // 路由列表 eg: [{ path: '/foo', component: Foo }] }); // 3. 挂载router const app = new Vue({ router }).$mount('#app');
然后再进行路由跳转的时候,我们会有以下几种使用方式 。 详细使用请查看官方文档
// 字符串 router.push('home'); // 对象 router.push({ path: 'home' }); // 命名的路由 router.push({ name: 'user', params: { userId: '123' } }); // 带查询参数,变成 /register?plan=private router.push({ path: 'register', query: { plan: 'private' } });
那么,你有没有想过, push
进去的对象是如何与我们之前定义的 routes
相对应的 ??
接下来,我们一步步来进行探个究竟吧!
匹配路由入口
之前我们说过 push 方法的具体实现, 里面主要是通过 transitionTo 来实现路由匹配并切换
// src/history/hash.js // 跳转到 push(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) }
所以我们来看看 transitionTo
// src/history/base.js // 切换路由 transitionTo(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { // 匹配路由 // 根据路径获取到匹配的路径 const route = this.router.match(location, this.current) // 跳转路由 this.confirmTransition(route, () => { // ...more }, err => { // ...more }) }
这里看到, transitionTo 主要处理两件事
- 匹配路由
- 将匹配到的路由作为参数,调用 confirmTransition 进行跳转
我们来看看具体如何匹配路由的 , 这里直接调用了匹配器的 match 方法
// 获取匹配的路由对象 match( raw: RawLocation, current ? : Route, redirectedFrom ? : Location ): Route { // 直接调用match方法 return this.matcher.match(raw, current, redirectedFrom) }
匹配器
export default class VueRouter { constructor() { // ...more // 创建匹配器 this.matcher = createMatcher(options.routes || [], this); // ...more } }
创建匹配器
在 VueRouter 实例化的时候, 会通过我们之前设置的 routers , 以及 createMatcher 创建一个匹配器, 匹配器包含一个 match 方法,用于匹配路由
// 文件位置: src/create-matcher.js // 创建匹配 export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher { // 创建 路由映射的关系 ,返回对应的关系 const { pathList, pathMap, nameMap } = createRouteMap(routes); // 添加 路由 function addRoutes(routes) { createRouteMap(routes, pathList, pathMap, nameMap); } // 匹配规则 function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 路径 const location = normalizeLocation(raw, currentRoute, false, router); const { name } = location; // 如果存在 name if (name) { // 找出匹配的 const record = nameMap[name]; if (!record) return _createRoute(null, location); // ...more if (record) { 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 return _createRoute(null, location); } // 创建路由 function _createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { // ...more return createRoute(record, location, redirectedFrom, router); } return { match, addRoutes }; }
获取路由映射关系 createRouteMap
export function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { // the path list is used to control path matching priority // 数组,包括所有的 path const pathList: Array<string> = oldPathList || []; // $flow-disable-line // 对象 , key 为 path , 值为 路由对象 const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null); // $flow-disable-line // 对象 , key 为 name , 值为 路由对象 const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null); // 循环遍历 routes ,添加路由记录 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route); }); // ensure wildcard routes are always at the end // 确保 * 匹配符放到最后面 for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]); l--; i--; } } return { pathList, pathMap, nameMap }; }
addRouteRecord 主要完成了几项工作
- 生成 normalizedPath 复制给 record.path
- 通过 compileRouteRegex 生成 record.regex , 用于后期的路由匹配
- 将 record 分别加入到 pathMap 、 pathList、nameMap 里面
// 添加路由记录对象 function addRouteRecord( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route; // ... const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}; const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ); if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive; } // 路由记录对象 const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }; // ... if (!pathMap[record.path]) { pathList.push(record.path); pathMap[record.path] = record; } if (name) { if (!nameMap[name]) { nameMap[name] = record; } // ... } }
创建路由对象
// 文件位置: src/util/route.js // 创建路由对象 export function createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route { const stringifyQuery = router && router.options.stringifyQuery; // 请求参数 let 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); }createRoute 生成的对象,便是是我们经常用到的路由对象。 当前激活的路由信息对象则是
this.$route
路由匹配规则
路由是否匹配 , 主要是通过 path-to-regexp , 来创建一个正则表达式 , 然后 , 通过这个正则来检查是否匹配
import Regexp from 'path-to-regexp'; // ...more // 编译路径,返回一个正则 function compileRouteRegex( path: string, pathToRegexpOptions: PathToRegexpOptions ): RouteRegExp { const regex = Regexp(path, [], pathToRegexpOptions); // ...more return regex; }
关于 path-to-regexp ,这里主要讲几个例子。
import Regexp from 'path-to-regexp'; // 假如我们页面 path 为 /about let reg = Regexp('/about', [], {}); // reg ==> /^\/about(?:\/(?=$))?$/i '/about'.match(reg); // ["/about", index: 0, input: "/about", groups: undefined] '/home'.match(reg); // null // 假如我们页面 path 为 /about/:id let reg = Regexp('/about/:id', [], {}); // reg ==> /^\/about\/((?:[^\/]+?))(?:\/(?=$))?$/i '/about'.match(reg); // null '/about/123'.match(reg); //["/about/123", "123", index: 0, input: "/about/123", groups: undefined]
具体文档可参照这里 : path-to-regexp
最后通过正则检查路由是否匹配, 匹配结果非 null 则表示路由符合预先设定的规则
// 匹配路由规则 function matchRoute(regex: RouteRegExp, path: string, params: Object): boolean { const m = path.match(regex); if (!m) { return false; } else if (!params) { // 没参数直接返回true return true; } // ...more, 这里对参数做了一些处理 return true; }
总结
最后,对路由匹配做一个总结 。 路由匹配具体的步骤有:
- 实例化的时候,创建匹配器 ,并生成路由的映射关系 。匹配器中包含
match
方法 push
的时候,调用到match
方法match
方法里面,从路由的映射关系里面,通过编译好的正则来判定是否匹配,返回最终匹配的路由对象transitionTo
中,拿到匹配的路由对象,进行路由跳转
其他
系列文章列表
个人博客
相关推荐
前端小白 2020-07-19
bowean 2020-07-05
80437700 2020-05-15
RainyX 2020-05-16
85423468 2020-05-05
wangxuekuan 2020-11-24
xuedabao 2020-11-19
hjr 2020-10-21
sjun0 2020-11-12
bestallen 2020-08-17
80143853 2020-08-17
Tplinkly 2020-08-15
dingyahui 2020-08-12
JiangMengYa 2020-07-30
BingGoGo技术 2020-07-25
JudeJoo 2020-07-14