vue-router4是vue官方针对Vue3版本提供的路由系统,具体使用参考官方文档,本篇文章主要是从背后的源码来看vue-router的具体实现。
先通过下面的代码,看下我们日常开发中使用vue-router的方式。
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router).mount('#app');
// router.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import ('../Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
我们看到,vue-router的主要通过createRouter方法创建路由对象实例并通过应用实例app.use()方法注册路由系统。在之前的一篇文章# Vue3源码之初始化渲染流程解读一中,我们有学习到app.use()方法是一个vue3的插件机制,通过该方法我们可以向应用添加第三方或者开发者自己的插件。
1. createRouter方法
在上面的应用代码示例中我们看到createRouter方法接受一个对象作为参数:
- history: 路由模式
- routes: 路由配置
// options类型定义
export interface RouterOptions extends PathParserOptions {
// 路由模式
history: RouterHistory
// 应该添加到路由的初始路由列表
routes: RouteRecordRaw[]
// 在页面之间导航时控制滚动的函数
scrollBehavior?: RouterScrollBehavior
// 用于解析查询的自定义实现
parseQuery?: typeof originalParseQuery
// 对查询对象进行字符串化的自定义实现
stringifyQuery?: typeof originalStringifyQuery
// 于激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-active
linkActiveClass?: string
// 于精准激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-exact-active
linkExactActiveClass?: string
}
// 返回Router实例类型定义
export interface Router {
// 向路由配置添加路由方法
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
addRoute(route: RouteRecordRaw): () => void
// 从路由配置列表删除某一个路由
removeRoute(name: RouteRecordName): void
// 检测判断方法是否存在路由
hasRoute(name: RouteRecordName): boolean
// 返回路由配置
getRoutes(): RouteRecord[]
// 要解析的原始路由地址,返回路由地址的标准化版本
resolve(
to: RouteLocationRaw,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string }
// 跳转
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
// 导航守卫
beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
// 导航守卫
beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
// 导航守卫
afterEach(guard: NavigationHookAfter): () => void
// 错误捕获
onError(handler: _ErrorHandler): () => void
// app.use方法调用
install(app: App): void
}
export function createRouter(options: RouterOptions): Router {
// 用户路由配置options树扁平化,便于操作
const matcher = createRouterMatcher(options.routes, options)
// 获取用户自定义的解析查询字符串或者使用系统默认
let parseQuery = options.parseQuery || originalParseQuery
// 获取用户自定义对对象字符串化的自定义实现或者默认
let stringifyQuery = options.stringifyQuery || originalStringifyQuery
// 获取路由模式
let routerHistory = options.history
// 当前活动的route,响应式包裹,变化更新视图
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
// ....
// 省略路由实例方法的实现
const router: Router = {
// ...
install(app:App) {
// this 指向app
const router = this
// 注册全局组件
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// vue 添加app应用路由实例,this.$router访问
app.config.globalProperties.$router = router
// app.$route可遍历,不可修改操作
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
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)
})
}
// 对外暴露路由对象
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
// 应用卸载处理
// 获取卸载方法
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()
}
}
}
return router
}
2. createWebHistory
createWebHistory方法创建基于HTML5新增的History apipushState和replaceState的实现,关于History Api的简单使用可以参考之前的一篇文章# History Api一文详解
export function createWebHistory(base?: string): RouterHistory {
// 主要提供hash的复用
base = normalizeBase(base)
// 提供路由当前信息:路径、状态、切换方法push replace
const historyNavigation = useHistoryStateNavigation(base)
// 地址栏前进、后退更新维护state
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 代理模式 routerHistory.location代表当前路径
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// routerHistory.state代表当前状态
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}
3. createWebHashHistory
注意:在vue2中,我们使用vue-router是区分hash和history两种模式的,主要是在ie9中不支持新增History Api pushState和replaceState方法,为了兼容ie9,之前的vue-router提供了两种路由模式实现:
- 基于History pushState和replaceState的浏览地址栈维护,地址记录发生变化监听window.onpopstate事件实现
- 基于location.hash的实现,hash值改变监听window.onhashChange事件
而在vue3中,由于放弃了对ie9的支持,所以真正意义上,我们使用的都是基于history的路由模式,看源码实现
export function createWebHashHistory(base?: string): RouterHistory {
// 基础路径处理
base = location.host ? base || location.pathname + location.search : ''
// allow the user to provide a `#` in the middle: `/base/#/app`
// base会再追加一个#符号
if (!base.includes('#')) base += '#'
// 看这里-----
return createWebHistory(base)
}
一定要知道,vue3中vue-router是没有真正的hash模式的。采用history模式实现,只是在地址栏添加了#号。
4. RouterLink组件
router-link组件是vue-router注册的一个应用全局组件,提供以标签的形式跳转路由,使用示例
<router-link to="/home">Home</router-link>
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
name: 'RouterLink',
props: {
// 路由目标
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
required: true,
},
// 添加or替换栈记录
replace: Boolean,
// 自定义激活class样式
activeClass: String,
// inactiveClass: String,
exactActiveClass: String,
// 自定义标签
custom: Boolean,
ariaCurrentValue: {
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
default: 'page',
},
},
useLink,
setup(props, { slots }) {
const link = reactive(useLink(props))
// 获取createRouter提供路由实例
const { options } = inject(routerKey)!
// 样式计算
const elClass = computed(() => ({
[getLinkClass(
props.activeClass,
options.linkActiveClass,
'router-link-active'
)]: link.isActive,
[getLinkClass(
props.exactActiveClass,
options.linkExactActiveClass,
'router-link-exact-active'
)]: link.isExactActive,
}))
// 返回render
return () => {
//
const children = slots.default && slots.default(link)
return props.custom
? children
: h(
'a',
{
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
// this would override user added attrs but Vue will still add
// the listener so we end up triggering both
onClick: link.navigate,
class: elClass.value,
},
children
)
}
},
})
router-link的实现比较简单,默认会被渲染为a标签,通多劫持a标签的点击事件来维护路由栈记录并更新视图
5. router-view组件
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
name: 'RouterView',
inheritAttrs: false,
props: {
// 命名路由视图
name: {
type: String as PropType<string>,
default: 'default',
}
},
setup(props, { attrs, slots }) {
// 根据location key获取createRouter内部暴露的路由信息
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed(() => props.route || injectedRoute.value)
// 路由层级,起始为0
const depth = inject(viewDepthKey, 0)
// 获取匹配路由
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth]
)
// 再暴露路由层级,子router-view +1
provide(viewDepthKey, depth + 1)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
const viewRef = ref<ComponentPublicInstance>()
return () => {
// 获取渲染路由信息
const route = routeToDisplay.value
const matchedRoute = matchedRouteRef.value
const ViewComponent = matchedRoute && matchedRoute.components[props.name]
const currentName = props.name
// 没有匹配路由,渲染router-view标签内容
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
// 渲染匹配路由组件
const component = h(ViewComponent)
return (
component
)
}
},
})
router-view组件主要通过判断当前组件嵌套层次,通过层次获取route.matched匹配需要渲染的组件,最后调用h渲染函数渲染匹配路由组件。