關于前端路由,網上有很多可以參考,但是感覺都是一些mini實現,對hashchange、pushState、replaceState這幾個api的使用。所以準備梳理下官方github倉庫的的源碼實現,同時給自己也作為閱讀筆記作為學習記錄,在代碼截圖中,為便于理解,會刪除大量干擾閱讀的支線/邊界情況代碼。
看完本篇文章你將理解:
1. Vue Router 的基本實現原理
2. 路徑是如何管理的,路徑和路由組件的渲染是如何映射的
3. 導航守衛是如何執行的
4. 給路由組件傳遞數據,有幾種方式,分別都怎么做的
先把[github源碼](https://github.com/vuejs/vue-router-next) clone 下來。
首先從入口index.js文件開始瀏覽:
```
export { createWebHistory } from './history/html5'
export { createMemoryHistory } from './history/memory'
export { createWebHashHistory } from './history/hash'
export { createRouterMatcher, RouterMatcher } from './matcher'
export {
LocationQuery,
parseQuery,
stringifyQuery,
LocationQueryRaw,
LocationQueryValue,
LocationQueryValueRaw,
} from './query'
export { RouterHistory, HistoryState } from './history/common'
export { RouteRecord, RouteRecordNormalized } from './matcher/types'
export {
PathParserOptions,
_PathParserOptions,
} from './matcher/pathParserRanker'
export {
routeLocationKey,
routerViewLocationKey,
routerKey,
matchedRouteKey,
viewDepthKey,
} from './injectionSymbols'
export {
// route location
_RouteLocationBase,
LocationAsPath,
LocationAsRelativeRaw,
RouteQueryAndHash,
RouteLocationRaw,
RouteLocation,
RouteLocationNormalized,
RouteLocationNormalizedLoaded,
RouteParams,
RouteParamsRaw,
RouteParamValue,
RouteParamValueRaw,
RouteLocationMatched,
RouteLocationOptions,
RouteRecordRedirectOption,
// route records
_RouteRecordBase,
RouteMeta,
START_LOCATION_NORMALIZED as START_LOCATION,
RouteComponent,
// RawRouteComponent,
RouteRecordName,
RouteRecordRaw,
NavigationGuard,
NavigationGuardNext,
NavigationGuardWithThis,
NavigationHookAfter,
} from './types'
export {
createRouter,
Router,
RouterOptions,
RouterScrollBehavior,
} from './router'
export {
NavigationFailureType,
NavigationFailure,
isNavigationFailure,
} from './errors'
export { onBeforeRouteLeave, onBeforeRouteUpdate } from './navigationGuards'
export {
RouterLink,
useLink,
RouterLinkProps,
UseLinkOptions,
} from './RouterLink'
export { RouterView, RouterViewProps } from './RouterView'
export * from './useApi'
export * from './globalExtensions'
```
我們主要關注:
* `history`模塊
* `matcher`模塊
* `router`模塊
* `RouterLink、RouterView`模塊
* `navigationGuards`模塊
## createRouter
我們一般使用時這樣使用的:


在入口文件中找到`createRouter`方法,它是通過`router`模塊導出的,`router`模塊源碼路徑為`src/router.ts`,在該文件中找到`createRouter`方法源碼,傳入`RouterOptions`類型的對象,然后返回一個`Router`實例,同時了提供了很多方法。
```
export function createRouter(options: RouterOptions): Router {
const router: Router = {
currentRoute,
addRoute,
removeRoute,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
isReady,
install(app: App) {
// ...
},
}
return router
}
```
## install方法
當我們在vue main.js中 app.use(router)其實就是調用了 router中install方法,接受一個vue app實例作為參數。
```
const router = {
install(app) {
const router = this
// 注冊路由組件
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// 全局配置定義 $router 和 $route
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
get: () = >unref(currentRoute),
})
// 在瀏覽器端初始化導航
if (isBrowser &&
!started &&
currentRoute.value === START_LOCATION_NORMALIZED) {
// see above
started = true
push(routerHistory.location).
catch(err = >{
warn('Unexpected error when starting the router:', err)
})
}
// 路徑變成響應式
const reactiveRoute = {}
for (let key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = computed(() = >currentRoute.value[key])
}
// 全局注入 router 和 reactiveRoute
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
let unmountApp = app.unmount
installedApps.add(app)
// 應用卸載的時候,需要做一些路由清理工作
app.unmount = function() {
installedApps.delete(app)
if (installedApps.size < 1) {
removeHistoryListener()
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp.call(this, arguments)
}
}
}
```
簡單說install做了幾件事:
1. 引入vue, 注冊RouterLink、RouterView為全局組件
2. 在瀏覽器端初始化導航
3. 路徑變成響應式
4. 通過provide全局注入 router 和 reactiveRoute
5. 攔截vue實例的unmount方法,在unmount方法調用之前,先執行VueRouter相關的卸載工作
## createRouter的參數(RouterOptions)

這里的類型文件寫的非常的清楚并且貼心的帶了example(源碼中直接復制過來的,有點又臭又長的的意思,但是實際很簡單):
```
export interface RouterOptions extends PathParserOptions {
/**
* History implementation used by the router. Most web applications should use
* `createWebHistory` but it requires the server to be properly configured.
* You can also use a _hash_ based history with `createWebHashHistory` that
* does not require any configuration on the server but isn't handled at all
* by search engines and does poorly on SEO.
*
* @example
* ```js
* createRouter({
* history: createWebHistory(),
* // other options...
* })
* ```
*/
history: RouterHistory
/**
* Initial list of routes that should be added to the router.
*/
routes: RouteRecordRaw[]
/**
* Function to control scrolling when navigating between pages. Can return a
* Promise to delay scrolling. Check {@link ScrollBehavior}.
*
* @example
* ```js
* function scrollBehavior(to, from, savedPosition) {
* // `to` and `from` are both route locations
* // `savedPosition` can be null if there isn't one
* }
* ```
*/
scrollBehavior ? :RouterScrollBehavior
/**
* Custom implementation to parse a query. See its counterpart,
* {@link RouterOptions.stringifyQuery}.
*
* @example
* Let's say you want to use the package {@link https://github.com/ljharb/qs | qs}
* to parse queries, you can provide both `parseQuery` and `stringifyQuery`:
* ```js
* import qs from 'qs'
*
* createRouter({
* // other options...
* parse: qs.parse,
* stringifyQuery: qs.stringify,
* })
* ```
*/
parseQuery ? :typeof originalParseQuery
/**
* Custom implementation to stringify a query object. Should not prepend a leading `?`.
* {@link RouterOptions.parseQuery | parseQuery} counterpart to handle query parsing.
*/
stringifyQuery ? :typeof originalStringifyQuery
/**
* Default class applied to active {@link RouterLink}. If none is provided,
* `router-link-active` will be applied.
*/
linkActiveClass ? :string
/**
* Default class applied to exact active {@link RouterLink}. If none is provided,
* `router-link-exact-active` will be applied.
*/
linkExactActiveClass ? :string
/**
* Default class applied to non active {@link RouterLink}. If none is provided,
* `router-link-inactive` will be applied.
*/
// linkInactiveClass?: string
}
```
上面的參數:
* history:可以用createWebHistory創建也可以用createWebhashHistory
* `routes`:應該添加到路由的初始路由列表
其他的可選參數:
* `scrollBehavior`:在頁面之間導航時控制滾動的函數。可以返回一個 `Promise` 來延遲滾動
* `parseQuery`:用于解析查詢的自定義實現。必須解碼查詢鍵和值。參見對應的 `stringifyQuery`
* `stringifyQuery`:對查詢對象進行字符串化的自定義實現。不應該在前面加上 ?。應該正確編碼查詢鍵和- 值。 `parseQuery` 對應于處理查詢解析。
* `linkActiveClass`:用于激活的 `RouterLink` 的默認類。如果什么都沒提供,則會使用 `router-link-active`
* `linkExactActiveClass`:用于精準激活的 `RouterLink` 的默認類。如果什么都沒提供,則會使用 `router-link-exact-active`
### 其中options參數中的history的type是RouterHistory
定義如下
```
interface RouterHistory {
// 只讀屬性,基本路徑,會添加到每個url的前面
readonly base: string
// 只讀屬性,當前路由
readonly location: HistoryLocation
// 只讀屬性,當前狀態
readonly state: HistoryState
// 路由跳轉方法
push(to: HistoryLocation, data?: HistoryState): void
// 路由跳轉方法
replace(to: HistoryLocation, data?: HistoryState): void
// 路由跳轉方法
go(delta: number, triggerListeners?: boolean): void
// 添加一個路由事件監聽器
listen(callback: NavigationCallback): () => void
// 生成在錨點標簽中使用的href的方法
createHref(location: HistoryLocation): string
// 清除listeners
destroy(): void
}
```
`options.history`參數為createWebHistory、createWebHashHistory、createMemoryHistory三種的其中一種。
`hash`和`history`路由模式,除了`base`的處理邏輯不同,其他屬性或者方法使用的是共同的邏輯。
**1、createWebHashHistory**

createWebHashHistory這個模式很簡單,這是檢測下有沒有#號 沒有就加上,然后調用createWebHistory(base),base為基本路徑,其中重點分析還是createWebHistory
**2、createWebHistory**



`changeLocation`方法。`changeLocation`方法十分重要,它是`push`方法和`replace`等路由跳轉方法的實現基礎。該方法包含三個參數:目標`location`、目標`state`對象、是否為替換當前`location`,這里發生錯誤都時候用location做了一個降級。
在createWebHistory的時候,執行了這樣一句:
```
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace)
```
其中useHistoryListeners的代碼實現:
這里的listen方法由useHistoryListeners函數返回,在useHistoryListeners函數內部查看listen方法相關源碼
~~~
function useHistoryListeners() {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
function listen(callback: NavigationCallback) {
listeners.push(callback)
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
return {
listen,
}
}
~~~
listen方法的作用是將傳入的回調函數添加到listeners數組中,并返回移除函數,同時將移除函數添加到teardowns數組中,方便進行批量移除。

這里在useHistoryListeners中執行了這句,就是添加監聽,同時執行posStateHandler,在posStateHandler中:

執行數組中的回調。
當用戶操作瀏覽器導航按鈕或者應用中調用了push/replace/go等方法時,都會觸發該事件,調用此函數。除了更新某些對象值之外,這個函數的關鍵在于遍歷listeners數組調用每一個注冊的回調函數。

beforeunload事件當瀏覽器窗口關閉或者刷新時被觸發,此時會調用beforeUnloadListener函數,該函數源碼如下,主要作用是將當前滾動信息保存到當前歷史記錄實體中。
history和hash模式的差別在于base的處理,換句話可以說是瀏覽器url表現方式的差異,路由的跳轉、事件的監聽,都是基于history api。
# routes
回到createRouter方法中,可以看到該方法中只有一個地方用到了options.routes,它作為createRouterMatcher參數,執行后返回一個RouterMatcher類型的對象

createRouterMatcher函數的基本邏輯簡化后的代碼如下
~~~
function createRouterMatcher(
routes: 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) {
// ...
}
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// ...
}
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// ...
}
function getRoutes() {
// ...
}
function insertMatcher(matcher: RouteRecordMatcher) {
// ...
}
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
// ...
}
// add initial routes
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
~~~
該函數接收兩個參數,第一個參數是路由配置數組,第二個參數是VueRouter初始化時傳進來的options。然后聲明兩個變量matchers和matcherMap,然后是聲明一系列方法,在return之前,遍歷routes,通過addRoute方法,將路由配置轉化為matcher。
- 前言
- 工作中的一些記錄
- 破解快手直播間的webSocket的連接
- 快手「反」反爬蟲的研究記錄
- HTML AND CSS
- 遇到的一些還行的css筆試題
- css常見面試題
- JavaScript 深度剖析
- ES6到ESNext新特性
- 關于http與緩存
- 關于頁面性能
- 關于瀏覽器的重排(reflow、layout)與重繪
- 手寫函數節流
- 手寫promise
- 手寫函數防抖
- 手寫圖片懶加載
- 手寫jsonp
- 手寫深拷貝
- 手寫new
- 數據結構和算法
- 前言
- 時間復雜度
- 棧
- 隊列
- 集合
- 字典
- 鏈表
- 樹
- 圖
- 堆
- 排序
- 搜索
- Webpack
- Webpack原理與實踐
- Vue
- Vuejs的Virtual Dom的源碼實現
- minVue
- Vuex實現原理
- 一道關于diff算法的面試題
- Vue2源碼筆記:源碼目錄設計
- vue-router源碼分析(v4.x)
- React及周邊
- 深入理解redux(一步步實現一個 redux)
- React常見面試題匯總
- Taro、小程序等
- TypeScript
- CI/CD
- docker踩坑筆記
- jenkins
- 最后