VueRouter?原理解讀之初始化流程
1.1 核心概念
官方介紹
Vue Router 是 Vue.js 的官方路由。它與 Vue.js 核心深度集成,讓用 Vue.js 構(gòu)建單頁應(yīng)用變得輕而易舉。功能包括:
- 嵌套路由映射
- 動態(tài)路由選擇
- 模塊化、基于組件的路由配置
- 路由參數(shù)、查詢、通配符
- 展示由 Vue.js 的過渡系統(tǒng)提供的過渡效果
- 細(xì)致的導(dǎo)航控制
- 自動激活 CSS 類的鏈接
- HTML5 history 模式或 hash 模式
- 可定制的滾動行為
- URL 的正確編碼
使用與閱讀源碼的必要性
現(xiàn)代工程化的前端項目只要使用到 Vue.js 框架,基本都是逃離不了如何對 SPA 路由跳轉(zhuǎn)的處理,而 VueRouter 作為一個成熟、優(yōu)秀的前端路由管理庫也是被業(yè)界廣泛推薦和使用,因此對 Vue 開發(fā)者來講,深入底層的了解使用 VueRouter ,學(xué)習(xí)其實現(xiàn)原理是很有必要的。隨著不斷對 VueRouter 的深度使用,一方面就是在實踐當(dāng)中可能遇到一些需要額外定制化處理的場景,像捕獲一些異常上報、處理路由緩存等場景需要我們對 VueRouter 有著一定程度的熟悉才能更好的處理;另一方面則是業(yè)余外的學(xué)習(xí)、拓展自身能力的一個渠道,通過對源碼的閱讀理解能夠不斷開拓自己的知識面以及提升自己的 CR 水平還有潛移默化當(dāng)中的一些技術(shù)設(shè)計、架構(gòu)能力等。
1.2 基本使用
路由配置與項目引入
// 1. 定義相關(guān)路由視圖組件 const Home = { template: '<div>Home</div>' } const About = { template: '<div>About</div>' } // 2. 定義相關(guān)路由路徑等路由配置 const routes = [ { path: '/', component: Home }, { path: '/about', component: About }, ] // 3. 通過 createRouter 方法傳入路由配置參數(shù)進(jìn)行路由對象創(chuàng)建 const router = VueRouter.createRouter({ // 4. 選擇路由能力的模式進(jìn)行初始化創(chuàng)建 history: VueRouter.createWebHashHistory(), routes, }) // 5. 調(diào)用 Vue.js 對象的 use 方法來對 VueRouter 路由對象進(jìn)行初始化安裝處理 const app = Vue.createApp({}) app.use(router) app.mount('#app')
路由組件使用
<router-view class="view left-sidebar" name="LeftSidebar"></router-view> <router-view class="view main-content"> <router-link to="/" reaplace>Home</router-link> </router-view> <router-view class="view right-sidebar" name="RightSidebar"></router-view>
跳轉(zhuǎn) api 調(diào)用
export default { methods: { redirectHome() { this.$router.replace('/') }, goToAbout() { this.$router.push('/about') }, }, }
當(dāng)然 VueRouter 的能力和相關(guān)的 api 肯定不僅僅是這塊基礎(chǔ)使用這么簡單,具體其他相關(guān)更高級、深層次的 api 與用法請參考官方文檔等。因為是 VueRouter 源碼分析和原理解析的系列文章,受眾最好是有一定的使用經(jīng)驗的開發(fā)者甚至是深度使用者更好,因此可能會存在一點門檻,這塊需要閱讀者自行斟酌。
2.1 createRouter 初始化入口分析
大致流程
將createRouter
這個方法的代碼簡單化后如下:能夠看到createRouter
方法在內(nèi)部定義了一個 router 對象并在這個對象上掛載一些屬性與方法,最后將這個 router 對象作為函數(shù)返回值進(jìn)行返回。
// vuejs:router/packages/router/src/router.ts export function createRouter(options: RouterOptions): Router { const matcher = createRouterMatcher(options.routes, options) const parseQuery = options.parseQuery || originalParseQuery const stringifyQuery = options.stringifyQuery || originalStringifyQuery const routerHistory = options.history const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const afterGuards = useCallbacks<NavigationHookAfter>() const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(START_LOCATION_NORMALIZED) let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED function addRoute(parentOrRoute: RouteRecordName | RouteRecordRaw, route?: RouteRecordRaw) { // ··· ··· } function removeRoute(name: RouteRecordName) { // ··· ··· } function getRoutes() { // ··· ··· } function hasRoute(name: RouteRecordName): boolean { // ··· ··· } function resolve(rawLocation: Readonly<RouteLocationRaw>, currentLocation?: RouteLocationNormalizedLoaded): RouteLocation & { href: string } { // ··· ··· } function push(to: RouteLocationRaw) { // ··· ··· } function replace(to: RouteLocationRaw) { // ··· ··· } let readyHandlers = useCallbacks<OnReadyCallback>() let errorHandlers = useCallbacks<_ErrorHandler>() function isReady(): Promise<void> { // ··· ··· } const go = (delta: number) => routerHistory.go(delta) const router: Router = { // ··· ··· } return router }
Router 對象的定義:
從上面的createRouter
方法定義當(dāng)中能夠知道,返回的是一個 Router 的對象,我們首先來看下 Router 對象的屬性定義:返回項 Router 是創(chuàng)建出來的全局路由對象,包含了路由的實例和常用的內(nèi)置操作跳轉(zhuǎn)、獲取信息等方法。
// vuejs:router/packages/router/src/router.ts export interface Router { // 當(dāng)前路由 readonly currentRoute: Ref<RouteLocationNormalizedLoaded> // VueRouter 路由配置項 readonly options: RouterOptions // 是否監(jiān)聽中 listening: boolean // 動態(tài)增加路由項 addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void addRoute(route: RouteRecordRaw): () => void // 動態(tài)刪除路由項 removeRoute(name: RouteRecordName): void // 根據(jù)路由配置的 name 判斷是否有該路由 hasRoute(name: RouteRecordName): boolean // 獲取當(dāng)前所有路由數(shù)據(jù) getRoutes(): RouteRecord[] // 當(dāng)前網(wǎng)頁的標(biāo)準(zhǔn)路由 URL 地址 resolve( to: RouteLocationRaw, currentLocation?: RouteLocationNormalizedLoaded ): RouteLocation & { href: string } // 路由導(dǎo)航跳轉(zhuǎn)操作方法 push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined> replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined> back(): ReturnType<Router['go']> forward(): ReturnType<Router['go']> go(delta: number): void // 全局守衛(wèi) beforeEach(guard: NavigationGuardWithThis<undefined>): () => void beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void afterEach(guard: NavigationHookAfter): () => void // 路由錯誤回調(diào)處理 onError(handler: _ErrorHandler): () => void // 路由是否已經(jīng)完成初始化導(dǎo)航 isReady(): Promise<void> // 2.x 版本的 Vue.js 引入 VueRouter 時候自動調(diào)用 install(app: App): void }
創(chuàng)建路由流程概括
在createRouter
的方法當(dāng)中,我們能夠看到該方法其實主要是做了三件事情,
- 使用
createRouterMatcher
創(chuàng)建頁面路由匹配器; - 創(chuàng)建和處理守衛(wèi)相關(guān)方法;
- 定義其他相關(guān)的 router 對象的屬性和內(nèi)置的方法。
接下來我們來具體分析里面的三個大步驟到底分別處理做了些什么事情呢。
2.2 創(chuàng)建頁面路由匹配器
在前面的簡單分析當(dāng)中,在createRouter
的第一步就是根據(jù)配置的路由 options 配置調(diào)用createRouterMacher
方法創(chuàng)建頁面路由匹配器matcher
對象。
// vuejs:router/packages/router/src/matcher/index.ts export function createRouterMatcher( routes: Readonly<RouteRecordRaw[]>, globalOptions: PathParserOptions ): RouterMatcher { const matchers: RouteRecordMatcher[] = [] const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>() globalOptions = mergeOptions( { strict: false, end: true, sensitive: false } as PathParserOptions, globalOptions ) function getRecordMatcher(name: RouteRecordName) { return matcherMap.get(name) } function addRoute( record: RouteRecordRaw, parent?: RouteRecordMatcher, originalRecord?: RouteRecordMatcher ) { // ··· ··· } function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) { // ··· ··· } function getRoutes() { // ··· ··· } function resolve(location: Readonly<MatcherLocationRaw>, currentLocation: Readonly<MatcherLocation>): MatcherLocation { // ··· ··· } routes.forEach(route => addRoute(route)) return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher } }
對于 VueRouter 整個庫來看這個頁面路由匹配器matcher
對象是占據(jù)了比較大的一個模塊,這篇文章主要是對路由初始化這部分流程邏輯進(jìn)行分析;也因為篇幅的原因,這里就先簡單從較上帝的視角來看看這個大概的流程。
- 方法內(nèi)聲明了
matchers
與matcherMap
兩個內(nèi)部變量存放經(jīng)過解析的路由配置信息; - 創(chuàng)建相關(guān)的路由匹配器的操作方法:addRoute, resolve, removeRoute, getRoutes, getRecordMatcher;
- 根據(jù)調(diào)用
createRouter
方法傳入的參數(shù)遍歷調(diào)用addRoute
初始化路由匹配器數(shù)據(jù); - 最后方法返回一個對象,并且將該些操作方法掛載到該對象屬性當(dāng)中;
2.3 創(chuàng)建初始化導(dǎo)航守衛(wèi)
useCallbacks 實現(xiàn)訂閱發(fā)布中心
// vuejs:router/packages/router/src/utils/callbacks.ts export function useCallbacks<T>() { let handlers: T[] = [] function add(handler: T): () => void { handlers.push(handler) return () => { const i = handlers.indexOf(handler) if (i > -1) handlers.splice(i, 1) } } function reset() { handlers = [] } return { add, list: () => handlers, reset, } }
這里首先簡單分析下useCallback
hooks 的方法,其實就是利用閉包創(chuàng)建一個內(nèi)部的回調(diào)函數(shù)數(shù)組變量,然后再創(chuàng)建和返回一個對象,對象有三個屬性方法,分別是add
添加一個回調(diào)執(zhí)行函數(shù)并且返回一個清除當(dāng)前回調(diào)函數(shù)的一個函數(shù),list
獲取回調(diào)函數(shù)數(shù)組,reset
清空當(dāng)前所有回調(diào)方法。是一個簡單的標(biāo)準(zhǔn)的發(fā)布訂閱中心處理的實現(xiàn)。
創(chuàng)建相關(guān)的導(dǎo)航守衛(wèi)
// vuejs:router/packages/router/src/router.ts import { useCallbacks } from './utils/callbacks' export function createRouter(options: RouterOptions): Router { const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const afterGuards = useCallbacks<NavigationHookAfter>() // ··· ··· const router: Router = { // ··· ··· beforeEach: beforeGuards.add, beforeResolve: beforeResolveGuards.add, afterEach: afterGuards.add, // ··· ··· } return router }
通過上面經(jīng)過節(jié)選的相關(guān)導(dǎo)航守衛(wèi)處理的部分代碼,能夠看到其實 VueRouter 在createRouter
里面對于全局導(dǎo)航守衛(wèi)的處理還是比較簡單通俗易懂的,通過useCallbacks
hooks 方法分別創(chuàng)建了beforeEach
、beforeResolve
、afterEach
三個對應(yīng)的全局導(dǎo)航守衛(wèi)的回調(diào)處理對象(這里主要是初始化創(chuàng)建相關(guān)的訂閱發(fā)布的發(fā)布者對象);
- beforeEach:在任何導(dǎo)航路由之前執(zhí)行;
- beforeResolve:在導(dǎo)航路由解析確認(rèn)之前執(zhí)行;
- afterEach:在任何導(dǎo)航路由確認(rèn)跳轉(zhuǎn)之后執(zhí)行;
因為篇幅問題,VueRouter 的守衛(wèi)其實不僅僅這些,后面會梳理整理相關(guān)的守衛(wèi)處理,訂閱回調(diào)的發(fā)布執(zhí)行等相關(guān)邏輯作為一篇守衛(wèi)相關(guān)的文章單獨編寫,這里就不講述過多的東西了。
2.4 定義掛載相關(guān) Router 方法
在 router 對象上面還掛載了不少方法,接下來我們來簡單分析下這些方法的實現(xiàn)邏輯。
路由配置相關(guān) addRoute、removeRoute、hasRoute、getRoutes
// vuejs:router/packages/router/src/router.ts export function createRouter(options: RouterOptions): Router { // ··· ··· // 添加路由項 - 兼容處理參數(shù)后使用 addRoute 進(jìn)行添加路由項 function addRoute( parentOrRoute: RouteRecordName | RouteRecordRaw, route?: RouteRecordRaw ) { let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined let record: RouteRecordRaw if (isRouteName(parentOrRoute)) { parent = matcher.getRecordMatcher(parentOrRoute) record = route! } else { record = parentOrRoute } return matcher.addRoute(record, parent) } // 刪除路由項 - 根據(jù)路由名 name 調(diào)用 getRecordMatcher 獲取路由項,如果找到記錄則調(diào)用 removeRoute 刪除該路由項 function removeRoute(name: RouteRecordName) { const recordMatcher = matcher.getRecordMatcher(name) if (recordMatcher) { matcher.removeRoute(recordMatcher) } } // 獲取當(dāng)前所有路由項 - function getRoutes() { return matcher.getRoutes().map(routeMatcher => routeMatcher.record) } // 是否含有路由 - 根據(jù)路由名 name 調(diào)用 getRecordMatcher 獲取路由項 function hasRoute(name: RouteRecordName): boolean { return !!matcher.getRecordMatcher(name) } const router: Router = { addRoute, removeRoute, hasRoute, getRoutes, } return router }
這部分是對路由配置的操作方法的實現(xiàn),但是看下來邏輯并不難,都是比較清晰。在前面的章節(jié)當(dāng)中我們對頁面路由匹配器matcher
進(jìn)行了簡單的分析,知道了在createRouterMacher
方法返回的這個對象包含著 addRoute, resolve, removeRoute, getRoutes, getRecordMatcher 這些操作方法,并且內(nèi)部維護(hù)著路由匹配器的信息。
這部分路由操作方法就是利用這個createRouterMacher
所創(chuàng)建的頁面路由匹配器matcher
掛載的方法來實現(xiàn)的。
路由操作相關(guān) push、replace、go、back、forward
// vuejs:router/packages/router/src/router.ts export function createRouter(options: RouterOptions): Router { const routerHistory = options.history function push(to: RouteLocationRaw) { return pushWithRedirect(to) } function replace(to: RouteLocationRaw) { return push(assign(locationAsObject(to), { replace: true })) } const go = (delta: number) => routerHistory.go(delta) const router: Router = { push, replace, go, back: () => go(-1), forward: () => go(1), } return router }
先來看比較簡單的go
、back
、forward
這幾個方法,這幾個方法都是直接調(diào)用路由歷史對象的go
方法,底層其實就是調(diào)用瀏覽器的 history 提供的 go 跳轉(zhuǎn) api,這個路由跳轉(zhuǎn)的會在另外的專門講解路由模式的文章當(dāng)中講述,這里就不展開詳細(xì)講述了。
而另外的push
與replace
方法能從上面看到replace
其實就是調(diào)用push
的方法,都是使用pushWithRedirect
處理跳轉(zhuǎn),僅一個 replace 的參數(shù)不同。接著我們來分析pushWithRedirect
這個方法。
// vuejs:router/packages/router/src/router.ts function pushWithRedirect( to: RouteLocationRaw | RouteLocation, redirectedFrom?: RouteLocation ): Promise<NavigationFailure | void | undefined> { // 定義相關(guān)的路由變量屬性 const targetLocation: RouteLocation = (pendingLocation = resolve(to)) const from = currentRoute.value const data: HistoryState | undefined = (to as RouteLocationOptions).state const force: boolean | undefined = (to as RouteLocationOptions).force const replace = (to as RouteLocationOptions).replace === true // 調(diào)用 handleRedirectRecord 判斷目標(biāo)跳轉(zhuǎn)是否需要重定向 -- 就是這個 to 要跳轉(zhuǎn)的路由的 redirect 屬性是否為 true const shouldRedirect = handleRedirectRecord(targetLocation) // 若需要重定向則遞歸調(diào)用 pushWithRedirect 方法 if (shouldRedirect) return pushWithRedirect( assign(locationAsObject(shouldRedirect), { state: typeof shouldRedirect === 'object' ? assign({}, data, shouldRedirect.state) : data, force, replace, }), redirectedFrom || targetLocation ) // 后續(xù)邏輯是非重定向的路由 const toLocation = targetLocation as RouteLocationNormalized toLocation.redirectedFrom = redirectedFrom let failure: NavigationFailure | void | undefined // 不設(shè)置強(qiáng)制跳轉(zhuǎn)并且目標(biāo)跳轉(zhuǎn)路由地址與當(dāng)前路由地址一樣的情況下定義相關(guān)的跳轉(zhuǎn)異常以及頁面的滾動,后續(xù)使用 Promise.resolve 處理異常 if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) { failure = createRouterError<NavigationFailure>( ErrorTypes.NAVIGATION_DUPLICATED, { to: toLocation, from } ) handleScroll(from, from, true, false) } // 判斷前面的執(zhí)行邏輯是否存在跳轉(zhuǎn)異?;蛘咤e誤,如果沒有跳轉(zhuǎn)異常錯誤則執(zhí)行 navigate 這個Promise方法 return (failure ? Promise.resolve(failure) : navigate(toLocation, from)) .catch((error: NavigationFailure | NavigationRedirectError) => { // 處理跳轉(zhuǎn)中出現(xiàn)異常后捕獲相關(guān)的錯誤并對不同錯誤進(jìn)行處理 isNavigationFailure(error) ? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) ? error : markAsReady(error) : triggerError(error, toLocation, from) }).then((failure: NavigationFailure | NavigationRedirectError | void) => { // 跳轉(zhuǎn)調(diào)用 navigate 后的處理 if (failure) { // 處理跳轉(zhuǎn)和執(zhí)行navigate過程當(dāng)中的錯誤異常 } else { failure = finalizeNavigation( toLocation as RouteLocationNormalizedLoaded, from, true, replace, data ) } triggerAfterEach(toLocation as RouteLocationNormalizedLoaded, from, failure) return failure }) }
在對pushWithRedirect
方法進(jìn)行分析后知道這個方法是對頁面的重定向進(jìn)行專門處理,處理完成后會調(diào)用navigate
這個 Promise 方法。
在pushWithRedirect
方法的邏輯末尾中,一系列的邏輯處理完成后才會調(diào)用finalizeNavigation
與triggerAfterEach
進(jìn)行導(dǎo)航切換路由的確認(rèn)與相關(guān)導(dǎo)航守衛(wèi)鉤子的收尾執(zhí)行。
我們先來看下這個navigate
方法的邏輯:
// vuejs:router/packages/router/src/router.ts function navigate( to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded ): Promise<any> { let guards: Lazy<any>[] // extractChangingRecords 方法會根據(jù)(to/目標(biāo)跳轉(zhuǎn)路由)和(from/離開的路由)到路由匹配器matcher里匹配對應(yīng)的路由項并且將結(jié)果存到3個數(shù)組中 // leavingRecords:當(dāng)前即將離開的路由 // updatingRecords:要更新的路由 // enteringRecords:要跳轉(zhuǎn)的目標(biāo)路由 const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from) // extractComponentsGuards 方法用于提取不同的路由鉤子,第二個參數(shù)可傳值:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave guards = extractComponentsGuards( leavingRecords.reverse(), // 這里因為Vue組件銷毀順序是從子到父,因此使用reverse反轉(zhuǎn)數(shù)組保證子路由鉤子順序在前 'beforeRouteLeave', to, from ) // 將失活組件的 onBeforeRouteLeave 導(dǎo)航守衛(wèi)都提取并且添加到 guards 里 for (const record of leavingRecords) { record.leaveGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) }) } // 檢查當(dāng)前正在處理的目標(biāo)跳轉(zhuǎn)路由和 to 是否相同路由,如果不是的話則拋除 Promise 異常 const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from) guards.push(canceledNavigationCheck) return ( runGuardQueue(guards) // 作為啟動 Promise 開始執(zhí)行失活組件的 beforeRouteLeave 鉤子 .then(() => { // 執(zhí)行全局 beforeEach 鉤子 guards = [] for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // 執(zhí)行重用組件的 beforeRouteUpdate 鉤子 guards = extractComponentsGuards( updatingRecords, 'beforeRouteUpdate', to, from ) for (const record of updatingRecords) { record.updateGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) }) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // 執(zhí)行全局 beforeEnter 鉤子 guards = [] for (const record of to.matched) { if (record.beforeEnter && !from.matched.includes(record)) { if (isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)) } else { guards.push(guardToPromiseFn(record.beforeEnter, to, from)) } } } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // 清除已經(jīng)存在的 enterCallbacks, 因為這些已經(jīng)在 extractComponentsGuards 里面添加 to.matched.forEach(record => (record.enterCallbacks = {})) // 執(zhí)行被激活組件的 beforeRouteEnter 鉤子 guards = extractComponentsGuards( enteringRecords, 'beforeRouteEnter', to, from ) guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // 執(zhí)行全局 beforeResolve 鉤子 guards = [] for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .catch(err => // 處理在過程當(dāng)中拋除的異常或者取消導(dǎo)航跳轉(zhuǎn)操作 isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) ) ) }
這個navigate
方法主要就是使用runGuardQueue
封裝即將要執(zhí)行的相關(guān)的一系列導(dǎo)航守衛(wèi)的鉤子回調(diào),這塊封裝的內(nèi)部處理邏輯還是比較復(fù)雜的,這里因為篇幅問題我們這塊還是以了解知道在這塊主要的流程邏輯,后續(xù)也會對路由守衛(wèi)這塊專門詳細(xì)的進(jìn)行源碼閱讀分析并且編寫相關(guān)的文章。
接著我們再來看下finalizeNavigation
是如何進(jìn)行前端路由跳轉(zhuǎn)的:
// vuejs:router/packages/router/src/router.ts function finalizeNavigation( toLocation: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, isPush: boolean, replace?: boolean, data?: HistoryState ): NavigationFailure | void { // 檢查是否需要取消目標(biāo)路由的跳轉(zhuǎn) -- 判斷目標(biāo)跳轉(zhuǎn)的路由和當(dāng)前處理的跳轉(zhuǎn)路由是否不同,不同則取消路由跳轉(zhuǎn) const error = checkCanceledNavigation(toLocation, from) if (error) return error const isFirstNavigation = from === START_LOCATION_NORMALIZED const state = !isBrowser ? {} : history.state if (isPush) { // 處理路由跳轉(zhuǎn),判斷根據(jù) replace 參數(shù)判斷使用 replace 還是 push 的跳轉(zhuǎn)形式 if (replace || isFirstNavigation) routerHistory.replace( toLocation.fullPath, assign({ scroll: isFirstNavigation && state && state.scroll, }, data) ) else routerHistory.push(toLocation.fullPath, data) } currentRoute.value = toLocation // 處理設(shè)置頁面的滾動 handleScroll(toLocation, from, isPush, isFirstNavigation) markAsReady() }
在邏輯當(dāng)中能夠看到調(diào)用 VueRouter 的push
和replace
方法進(jìn)行跳轉(zhuǎn)時候會調(diào)用這個routerHistory
路由歷史對象對應(yīng)的同名 api 進(jìn)行跳轉(zhuǎn)處理,但是受限于文章的篇幅,這塊先劇透這里底層路由跳轉(zhuǎn)邏輯使用的是瀏覽器的history
的pushState
與replaceState
這兩個 api,后續(xù)很快就會推出相關(guān)的前端路由能力實現(xiàn)原理的剖析文章,大家敬請關(guān)注。
鉤子相關(guān) beforeEach、beforeResolve、afterEach、onError
// vuejs:router/packages/router/src/router.ts import { useCallbacks } from './utils/callbacks' export function createRouter(options: RouterOptions): Router { const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const afterGuards = useCallbacks<NavigationHookAfter>() let errorHandlers = useCallbacks<_ErrorHandler>() // ··· ··· const router: Router = { // ··· ··· // 拋出相關(guān)導(dǎo)航守衛(wèi)或鉤子對應(yīng)的新增訂閱回調(diào)事件 beforeEach: beforeGuards.add, beforeResolve: beforeResolveGuards.add, afterEach: afterGuards.add, onError: errorHandlers.add, // ··· ··· } return router }
這塊鉤子的大部分的邏輯已經(jīng)在前面創(chuàng)建初始化導(dǎo)航守衛(wèi)這個小章節(jié)里面已經(jīng)講述了,因此這塊定義掛載拋出鉤子事件的邏輯其實也較明朗了:
- 使用
useCallbacks
方法定義相關(guān)鉤子的訂閱發(fā)布中心對象; createRouter
方法返回的 router 對象定義增加對應(yīng)的訂閱事件add
,這樣子在定義路由時候配置傳入的鉤子回調(diào)函數(shù)則自動被添加到對應(yīng)鉤子的訂閱回調(diào)列表當(dāng)中。
注冊安裝 install 方法:
熟悉 Vue.js 技術(shù)棧的同學(xué)都基本知道這個install
方法會在插件庫引入 Vue 項目當(dāng)中的Vue.use
方法當(dāng)中被調(diào)用,這里 VueRouter 的 install 也不例外,同樣會在下面的 use 方法的調(diào)用當(dāng)中被 Vue.js 內(nèi)部所調(diào)用到。
const app = Vue.createApp({}) app.use(router)
接下來我們真正進(jìn)入到對install
方法的分析當(dāng)中去:
// vuejs:router/packages/router/src/router.ts install(app: App) { const router = this // 注冊 VueRouter 的路由視圖和鏈接組件為 Vue 全局組件 app.component('RouterLink', RouterLink) app.component('RouterView', RouterView) // 在全局 Vue this 對象上掛載 $router 屬性為路由對象 app.config.globalProperties.$router = router Object.defineProperty(app.config.globalProperties, '$route', { enumerable: true, get: () => unref(currentRoute), }) // 判斷瀏覽器環(huán)境并且還沒執(zhí)行初始化路由跳轉(zhuǎn)時候先進(jìn)行一次 VueRouter 的 push 路由跳轉(zhuǎn) if ( isBrowser && !started && currentRoute.value === START_LOCATION_NORMALIZED ) { started = true push(routerHistory.location).catch(err => { if (__DEV__) warn('Unexpected error when starting the router:', err) }) } // 使用 computed 計算屬性來創(chuàng)建一個記錄當(dāng)前已經(jīng)被激活過的路由的對象 reactiveRoute const reactiveRoute = {} as { [k in keyof RouteLocationNormalizedLoaded]: ComputedRef< RouteLocationNormalizedLoaded[k] > } for (const key in START_LOCATION_NORMALIZED) { reactiveRoute[key] = computed(() => currentRoute.value[key]) } // 全局注入相關(guān)的一些路由相關(guān)的變量 app.provide(routerKey, router) app.provide(routeLocationKey, reactive(reactiveRoute)) app.provide(routerViewLocationKey, currentRoute) // 重寫覆蓋 Vue 項目的卸載鉤子函數(shù) - 執(zhí)行相關(guān)屬性的卸載并且調(diào)用原本 Vue.js 的卸載 unmount 事件 const unmountApp = app.unmount installedApps.add(app) app.unmount = function () { installedApps.delete(app) if (installedApps.size < 1) { pendingLocation = START_LOCATION_NORMALIZED removeHistoryListener && removeHistoryListener() removeHistoryListener = null currentRoute.value = START_LOCATION_NORMALIZED started = false ready = false } unmountApp() // 執(zhí)行原本 Vue 項目當(dāng)中設(shè)置的 unmount 鉤子函數(shù) } }
在install
方法當(dāng)中主要邏輯還是對一些全局的屬性和相關(guān)的組件、變量以及鉤子事件進(jìn)行一個初始化處理操作:
這塊的邏輯可能有些操作在一開始初始化時候可能看不太懂為啥要這樣處理,后面會繼續(xù)推出 VueRouter 系列的源碼解析,到時候會回來回顧這塊的一些 install 引入安裝路由庫時候里面的一些操作與源碼邏輯。
總結(jié):
至此,VueRouter 這個前端路由庫的初始化流程createRouter
就簡單的分析完成了,這篇初始化的源碼解析的文章更多的像是領(lǐng)入門的流程概述簡析。
雖然說初始化主要做了前面講述的三個步驟:創(chuàng)建頁面路由匹配器、導(dǎo)航守衛(wèi)、初始化 router 對象并且返回。但是這三件事情當(dāng)中其實還是有著不少的處理細(xì)節(jié),里面還牽涉了不少其他功能模塊的實現(xiàn),一開始可能還只能大概通過上帝模式去俯瞰這個初始化的流程,可能僅僅留有個印象(這里因為篇幅問題未能夠各個方面都進(jìn)行很詳細(xì)的講解,后續(xù)也會沿著這些伏筆線索不斷推出相關(guān)的源碼原理解析文章),開篇反而可能存在著一定程度的心智負(fù)擔(dān)。但是相信跟隨著后面的一系列文章,應(yīng)該能夠?qū)⑾到y(tǒng)串聯(lián)起來,對 VueRouter 的實現(xiàn)有更完整的認(rèn)知。
相關(guān)的參考資料
- Vue Router 官方文檔:router.vuejs.org/
以上就是VueRouter 原理解讀 - 初始化流程的詳細(xì)內(nèi)容,更多關(guān)于VueRouter 初始化流程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue2.0在沒有dev-server.js下的本地數(shù)據(jù)配置方法
這篇文章主要介紹了vue2.0在沒有dev-server.js下的本地數(shù)據(jù)配置方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-02-02Vue實現(xiàn)預(yù)覽文件(Word/Excel/PDF)功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何通過Vue實現(xiàn)預(yù)覽文件(Word/Excel/PDF)的功能,文中的實現(xiàn)步驟講解詳細(xì),需要的小伙伴可以參考一下2023-03-03利用Vue模擬實現(xiàn)element-ui的分頁器效果
這篇文章主要為大家詳細(xì)介紹了如何利用Vue模擬實現(xiàn)element-ui的分頁器效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動手嘗試一下2022-11-11Vue.js 利用v-for中的index值實現(xiàn)隔行變色
這篇文章主要介紹了Vue.js 利用v-for中的index值實現(xiàn)隔行變色效果,首先定義好樣式,利用v-for中的index值,然后綁定樣式來實現(xiàn)隔行變色,需要的朋友可以參考下2018-08-08Vue+thinkphp5.1+axios實現(xiàn)文件上傳
這篇文章主要為大家詳細(xì)介紹了Vue+thinkphp5.1+axios實現(xiàn)文件上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-05-05Vue中computed(計算屬性)和watch(監(jiān)聽屬性)的用法及區(qū)別說明
這篇文章主要介紹了Vue中computed(計算屬性)和watch(監(jiān)聽屬性)的用法及區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07