🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
![](https://img.kancloud.cn/c8/31/c83114b2e9f22e06483ff8cfbd89065b_1352x254.png)关于前端路由,网上有很多可以参考,但是感觉都是一些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 我们一般使用时这样使用的: ![](https://img.kancloud.cn/e8/23/e823510dd507d7036072428038313f2d_940x398.png) ![](https://img.kancloud.cn/04/ee/04ee4275cd1675d26c3a87f398c85a3b_992x210.png) 在入口文件中找到`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) ![](https://img.kancloud.cn/59/0a/590a8cb4df9185a886052da077953ba9_1380x674.png) 这里的类型文件写的非常的清楚并且贴心的带了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** ![](https://img.kancloud.cn/07/66/07661391837582f8fb87fd7b17a9bec6_1382x644.png) createWebHashHistory这个模式很简单,这是检测下有没有#号 没有就加上,然后调用createWebHistory(base),base为基本路径,其中重点分析还是createWebHistory **2、createWebHistory** ![](https://img.kancloud.cn/39/35/39352f26441a1e2fe71bffdcf48d8900_1298x332.png) ![](https://img.kancloud.cn/45/f3/45f3d34ffecf5054321e5aa3b735e07d_1280x1050.png)![](https://img.kancloud.cn/92/28/92289221ada413b1f1f9fab486b2c4dc_1128x564.png) ![](https://img.kancloud.cn/ca/c7/cac7bc2b4c4e8834922c59cb48db791a_1294x1028.png) `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数组中,方便进行批量移除。 ![](https://img.kancloud.cn/33/18/3318a5ff42eac1f5e3d77822f23bde64_1148x148.png) 这里在useHistoryListeners中执行了这句,就是添加监听,同时执行posStateHandler,在posStateHandler中: ![](https://img.kancloud.cn/01/8f/018fccfb7b278cdffec7914a61fb0648_958x990.png) 执行数组中的回调。 当用户操作浏览器导航按钮或者应用中调用了push/replace/go等方法时,都会触发该事件,调用此函数。除了更新某些对象值之外,这个函数的关键在于遍历listeners数组调用每一个注册的回调函数。 ![](https://img.kancloud.cn/f8/00/f8005cd33b23aaa24350c477b05a9abd_1226x352.png) beforeunload事件当浏览器窗口关闭或者刷新时被触发,此时会调用beforeUnloadListener函数,该函数源码如下,主要作用是将当前滚动信息保存到当前历史记录实体中。 history和hash模式的差别在于base的处理,换句话可以说是浏览器url表现方式的差异,路由的跳转、事件的监听,都是基于history api。 # routes 回到createRouter方法中,可以看到该方法中只有一个地方用到了options.routes,它作为createRouterMatcher参数,执行后返回一个RouterMatcher类型的对象 ![](https://img.kancloud.cn/c8/31/c83114b2e9f22e06483ff8cfbd89065b_1352x254.png) 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。