前面我們講過,在使用 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
中,拿到匹配的路由對象,進行路由跳轉