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 中,拿到匹配的路由對象,進行路由跳轉

其他

系列文章列表
個人博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章