vue-router 使用
npm install vue-router@next
src/router/index.js
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
src/main.js
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
src/App.vue
<!-- src/App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view></router-view>
</div>
</template>
使用
<template>
<div>
<button @click="goToHome">Go to Home</button>
<p>当前页面: {{ route.name }}</p>
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); // 用于编程式导航
const route = useRoute(); // 用于获取当前路由信息
function goToHome() {
router.push({ name: 'About' }); // 跳转到 Home 路由
router.push('/about'); // 跳转到 Home 路由
}
console.log('当前路由的参数:', route.params); // 可以获取路径参数
console.log('当前路由的查询:', route.query); // 可以获取查询参数
// 路由守卫
router.beforeEach((to, from, next) => {
console.log('Navigating to:', to.name);
next();
});
</script>
迷你版源码实现
说明
- hash
- 通过#锚点实现
- hashChange监听变化
- history
- 通过history.pushState,replaceState
- history.popState监听变化
实现
vueRouterMini.js
import {ref,inject} from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
const ROUTER_KEY = '__router__'
function createRouter(options){
return new Router(options)
}
function useRouter(){
return inject(ROUTER_KEY)
}
function createWebHashHistory(){//构造方法 返回 {bindEvents:xx, url:xxx}
function bindEvents(fn){
window.addEventListener('hashchange',fn)
}
return {
bindEvents,
url:window.location.hash.slice(1) || '/'
}
// 等价于
// this.current = ref("/")
// window.addEventListener('hashchange', () => {
// this.current.value = window.location.hash.slice(1)|| '/'
// })
}
class Router{
constructor(options){
this.history = options.history
this.current = ref(this.history.url)
this.history.bindEvents(()=>{
this.current.value = window.location.hash.slice(1)
})
this.routes = options.routes
}
install(app){
app.provide(ROUTER_KEY,this)
app.component("router-link",RouterLink)
app.component("router-view",RouterView)
}
}
export {createRouter,createWebHashHistory,useRouter}
RouterLink.vue
<template>
<a :href="'#'+props.to">
<slot />
</a>
</template>
<script setup>
import {defineProps} from 'vue'
let props = defineProps({
to:{type:String,required:true}
})
</script>
RouterView.vue
<template>
<component :is="comp"></component>
</template>
<script setup>
import {computed } from 'vue'
import { useRouter } from './routerMini.js'
let router = useRouter()
const comp = computed(()=>{
const route = router.routes.find(
(route) => route.path === router.current.value
)
return route?route.component : null
})
</script>
使用
router/index.js
import {
createRouter,
createWebHashHistory,
} from './routerMini'
import Home from '../pages/home.vue'
import About from '../pages/about.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
源码学习
环境搭建
源码版本 4.4.5
git clone https://github.com/vuejs/router.git
pnpm i #安装依赖
pnpm play # 运行
运行的效果
加入调试
在 ./router 中加入 debugger
主要流程
- createRouter方法传入 RouterHistory对象
- 内部定义了一个 router 对象并返回,里面包含 addRoute,removeRoute,push 等
- 并且定义了install 用于注册 RouterLink 和 RouterView
- 使用 app.provide(routerKey, router)挂载到全局
- 使用 currentRoute 记录当前激活的路由,默认为START_LOCATION_NORMALIZED
- 并通过 Object.defineProperty 实现app.config.globalProperties.$route等价于currentRoute
- createRouterMatcher负责把传入的routes,内部循环输出
routes.forEach(route => addRoute(route)),返回 {getRoutes} - 循环的时候 通过createRouteRecordMatcher方法 加入parent、children等信息。
- 返回的router对象也包含getRoutes的信息,并且赋值给 matchedRoute
createWebHistory
这里调用了 createWebHistory创建
在 src/history/html5.ts
- 使用useHistoryStateNavigation方法创建了 push和replace方法
- 使用useHistoryListeners 实现路由监听逻辑
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base)
const historyNavigation = useHistoryStateNavigation(base)
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
)
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}
function useHistoryListeners(
base: string,
historyState: ValueContainer<StateEntry>,
currentLocation: ValueContainer<HistoryLocation>,
replace: RouterHistory['replace']
) {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
// TODO: should it be a stack? a Dict. Check if the popstate listener
// can trigger twice
let pauseState: HistoryLocation | null = null
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = state
// ignore the popstate and reset the pauseState
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
function pauseListeners() {
pauseState = currentLocation.value
}
function listen(callback: NavigationCallback) {
// set up the listener and prepare teardown callbacks
listeners.push(callback)
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
function destroy() {
for (const teardown of teardowns) teardown()
teardowns = []
window.removeEventListener('popstate', popStateHandler)
window.removeEventListener('beforeunload', beforeUnloadListener)
}
// set up the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler)
// TODO: could we use 'pagehide' or 'visibilitychange' instead?
// https://developer.chrome.com/blog/page-lifecycle-api/
window.addEventListener('beforeunload', beforeUnloadListener, {
passive: true,
})
return {
pauseListeners,
listen,
destroy,
}
}
//返回 push 和 replace方法
function useHistoryStateNavigation(base: string) {
const { history, location } = window
// private variables
const currentLocation: ValueContainer<HistoryLocation> = {
value: createCurrentLocation(base, location),
}
const historyState: ValueContainer<StateEntry> = { value: history.state }
// build current history entry as this is a fresh navigation
if (!historyState.value) {
changeLocation(
currentLocation.value,
{
back: null,
current: currentLocation.value,
forward: null,
position: history.length - 1,
replaced: true,
scroll: null,
},
true
)
}
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
const hashIndex = base.indexOf('#')
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url)
}
}
function replace(to: HistoryLocation, data?: HistoryState) {
const state: StateEntry = assign(
{},
history.state,
buildState(
historyState.value.back,
to,
historyState.value.forward,
true
),
data,
{ position: historyState.value.position }
)
changeLocation(to, state, true)
currentLocation.value = to
}
function push(to: HistoryLocation, data?: HistoryState) {
const currentState = assign(
{},
historyState.value,
history.state as Partial<StateEntry> | null,
{
forward: to,
scroll: computeScrollPosition(),
}
)
changeLocation(currentState.current, currentState, true)
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false)
currentLocation.value = to
}
return {
location: currentLocation,
state: historyState,
push,
replace,
}
}
RouterHistory
export interface RouterHistory {
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
createHref(location: HistoryLocation): string
destroy(): void
}
顺便看一下 hash
src/history/hash.ts
hash模式最终还是调用了createWebHistory 的方法
import { RouterHistory } from './common'
import { createWebHistory } from './html5'
import { warn } from '../warning'
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`
if (!base.includes('#')) base += '#'
return createWebHistory(base)
}
START_LOCATION_NORMALIZED
定义了一个默认路由的空值
export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
path: '/',
// TODO: could we use a symbol in the future?
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
}
createRouter
createRouter 初始化
使用
export const router = createRouter({
history: routerHistory,
strict: true,
routes: [
{ path: '/home', redirect: '/' },
....
]
})
packages/router/src/router.ts
大致的代码
export function createRouter(options: RouterOptions): Router {
....
let started: boolean | undefined
const installedApps = new Set<App>()
// 路由对象
const router: Router = {
currentRoute,
addRoute,
push,
replace,
install(app: App) {
const router = this
// 注册全局组件 router-link和router-view
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
app.config.globalProperties.$router = router
// 提供全局配置
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
},
}
return router
}
传入参数
RouterOptions
export interface RouterOptions extends PathParserOptions {
history: RouterHistory
routes: Readonly<RouteRecordRaw[]>
scrollBehavior?: RouterScrollBehavior
parseQuery?: typeof originalParseQuery
stringifyQuery?: typeof originalStringifyQuery
linkActiveClass?: string
linkExactActiveClass?: string
}
RouteRecordRaw
由多个类型 组合
export type RouteRecordRaw =
| RouteRecordSingleView
| RouteRecordSingleViewWithChildren
| RouteRecordMultipleViews
| RouteRecordMultipleViewsWithChildren
| RouteRecordRedirect
export interface RouteRecordSingleView extends _RouteRecordBase {
component: RawRouteComponent
components?: never
children?: never
redirect?: never
props?: _RouteRecordProps
}
export interface RouteRecordSingleViewWithChildren extends _RouteRecordBase {
component?: RawRouteComponent | null | undefined
components?: never
children: RouteRecordRaw[]
props?: _RouteRecordProps
}
export interface RouteRecordMultipleViews extends _RouteRecordBase {
components: Record<string, RawRouteComponent>
component?: never
children?: never
redirect?: never
props?: Record<string, _RouteRecordProps> | boolean
}
export interface RouteRecordMultipleViewsWithChildren extends _RouteRecordBase {
components?: Record<string, RawRouteComponent> | null | undefined
component?: never
children: RouteRecordRaw[]
props?: Record<string, _RouteRecordProps> | boolean
}
export interface RouteRecordRedirect extends _RouteRecordBase {
redirect: RouteRecordRedirectOption
component?: never
components?: never
props?: never
}
返回对象
Router
创建后返回的router对象,这是定义的属性和方法
export interface Router {
readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
readonly options: RouterOptions
listening: boolean
addRoute(
parentName: NonNullable<RouteRecordNameGeneric>,
route: RouteRecordRaw
): () => void
addRoute(route: RouteRecordRaw): () => void
removeRoute(name: NonNullable<RouteRecordNameGeneric>): void
hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean
getRoutes(): RouteRecord[]
clearRoutes(): void
resolve<Name extends keyof RouteMap = keyof RouteMap>(
to: RouteLocationAsRelativeTyped<RouteMap, Name>,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocationResolved<Name>
resolve(
to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocationResolved
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: _ErrorListener): () => void
isReady(): Promise<void>
install(app: App): void
}
返回的对象
const router: Router = {
currentRoute,
listening: true,
addRoute,
removeRoute,
clearRoutes: matcher.clearRoutes,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorListeners.add,
isReady,
install(app: App) {
const router = this
app.component('RouterLink', RouterLink) // 注册组件
app.component('RouterView', RouterView)// 注册组件
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', { // 这里支持app.$route 访问当前路由
enumerable: true,
get: () => unref(currentRoute),
})
if (
isBrowser &&
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
// see above
started = true
push(routerHistory.location).catch(err => {
})
}
const reactiveRoute = {} as RouteLocationNormalizedLoaded
for (const key in START_LOCATION_NORMALIZED) {
Object.defineProperty(reactiveRoute, key, {
get: () => currentRoute.value[key as keyof RouteLocationNormalized],
enumerable: true,
})
}
app.provide(routerKey, router)
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
// the router is not attached to an app anymore
if (installedApps.size < 1) {
// invalidate the current navigation
pendingLocation = START_LOCATION_NORMALIZED
removeHistoryListener && removeHistoryListener()
removeHistoryListener = null
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp()
}
},
}
createRouterMatcher 加工路由对象
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// normalized ordered array of matchers
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<
NonNullable<RouteRecordNameGeneric>,
RouteRecordMatcher
>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
function getRecordMatcher(name: NonNullable<RouteRecordNameGeneric>) {
return matcherMap.get(name)
}
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// used later on to remove by name
const isRootAdd = !originalRecord
const mainNormalizedRecord = normalizeRouteRecord(record)
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
const normalizedRecords: RouteRecordNormalized[] = [mainNormalizedRecord]
if ('alias' in record) {
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
for (const alias of aliases) {
normalizedRecords.push(
normalizeRouteRecord(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
})
)
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
for (const normalizedRecord of normalizedRecords) {
const { path } = normalizedRecord
if (parent && path[0] !== '/') {
const parentPath = parent.record.path
const connectingSlash =
parentPath[parentPath.length - 1] === '/' ? '' : '/'
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path)
}
// create the object beforehand, so it can be passed to children
matcher = createRouteRecordMatcher(normalizedRecord, parent, options) // 添加扩展的属性
if (originalRecord) {
originalRecord.alias.push(matcher)
} else {
// otherwise, the first record is the original and others are aliases
originalMatcher = originalMatcher || matcher
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
if (isRootAdd && record.name && !isAliasRecord(matcher)) {
removeRoute(record.name)
}
}
if (isMatchable(matcher)) {
insertMatcher(matcher)
}
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
originalRecord = originalRecord || matcher
}
return originalMatcher
? () => {
removeRoute(originalMatcher!)
}
: noop
}
function removeRoute(
matcherRef: NonNullable<RouteRecordNameGeneric> | RouteRecordMatcher
) {
if (isRouteName(matcherRef)) {
const matcher = matcherMap.get(matcherRef)
if (matcher) {
matcherMap.delete(matcherRef)
matchers.splice(matchers.indexOf(matcher), 1)
matcher.children.forEach(removeRoute)
matcher.alias.forEach(removeRoute)
}
} else {
const index = matchers.indexOf(matcherRef)
if (index > -1) {
matchers.splice(index, 1)
if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
matcherRef.children.forEach(removeRoute)
matcherRef.alias.forEach(removeRoute)
}
}
}
function getRoutes() {
return matchers
}
function insertMatcher(matcher: RouteRecordMatcher) {
const index = findInsertionIndex(matcher, matchers)
matchers.splice(index, 0, matcher)
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
let matcher: RouteRecordMatcher | undefined
let params: PathParams = {}
let path: MatcherLocation['path']
let name: MatcherLocation['name']
if ('name' in location && location.name) {
matcher = matcherMap.get(location.name)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
})
name = matcher.record.name
params = assign(
// paramsFromLocation is a new object
paramsFromLocation(
currentLocation.params,
matcher.keys
.filter(k => !k.optional)
.concat(
matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
)
.map(k => k.name)
),
location.params &&
paramsFromLocation(
location.params,
matcher.keys.map(k => k.name)
)
)
// throws if cannot be stringified
path = matcher.stringify(params)
} else if (location.path != null) {
path = location.path
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
params = matcher.parse(path)!
name = matcher.record.name
}
} else {
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path))
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
currentLocation,
})
name = matcher.record.name
params = assign({}, currentLocation.params, location.params)
path = matcher.stringify(params)
}
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
// add initial routes
routes.forEach(route => addRoute(route))
function clearRoutes() {
matchers.length = 0
matcherMap.clear()
}
return {
addRoute,
resolve,
removeRoute,
clearRoutes,
getRoutes,
getRecordMatcher,
}
}
pushWithRedirect路由守卫
路由守卫
- 通过pushWithRedirect 控制跳转前的入口
- routerHistory.push 或者 replace 更新路由 currentRoute.value
- 每次跳转都会使用finalizeNavigation, 更新 toLocation到 currentRoute.value 当前路由
- handleScroll滚动到指定的位置
// 路由守卫
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
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
// to could be a string where `replace` is a function
const replace = (to as RouteLocationOptions).replace === true
const shouldRedirect = handleRedirectRecord(targetLocation)
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
// keep original redirectedFrom if it exists
redirectedFrom || targetLocation
)
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation as RouteLocationNormalized
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{ to: toLocation, from }
)
// trigger scroll to allow scrolling to the same anchor
handleScroll(
from,
from,
true,
false
)
}
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) =>
isNavigationFailure(error)
? // navigation redirects still mark the router as ready
isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
? error
: markAsReady(error) // also returns the error
: // reject any unknown error
triggerError(error, toLocation, from)
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
if (failure) {
if (
isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
return pushWithRedirect(
// keep options
assign(
{
// preserve an existing replacement but allow the redirect to override it
replace,
},
locationAsObject(failure.to),
{
state:
typeof failure.to === 'object'
? assign({}, data, failure.to.state)
: data,
force,
}
),
// preserve the original redirectedFrom if any
redirectedFrom || toLocation
)
}
} else {
// if we fail we don't finalize the navigation
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
return failure
})
}
// routerHistory.push 或者 replace 更新路由 currentRoute.value
// 每次跳转都会 更新 toLocation到 currentRoute.value 当前路由
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
replace?: boolean,
data?: HistoryState
): NavigationFailure | void {
// a more recent navigation took place
const error = checkCanceledNavigation(toLocation, from)
if (error) return error
// only consider as push if it's not the first navigation
const isFirstNavigation = from === START_LOCATION_NORMALIZED
const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state
if (isPush) {
if (replace || isFirstNavigation)
routerHistory.replace(
toLocation.fullPath,
assign(
{
scroll: isFirstNavigation && state && state.scroll,
},
data
)
)
else routerHistory.push(toLocation.fullPath, data)
}
// accept current navigation
currentRoute.value = toLocation
handleScroll(toLocation, from, isPush, isFirstNavigation)
markAsReady()
}
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()
}
return err
}
createRouter完整的代码
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
// leave the scrollRestoration if no scrollBehavior is provided
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
const normalizeParams = applyToParams.bind(
null,
paramValue => '' + paramValue
)
const encodeParams = applyToParams.bind(null, encodeParam)
const decodeParams: (params: RouteParams | undefined) => RouteParams =
// @ts-expect-error: intentionally avoid the type check
applyToParams.bind(null, decode)
function addRoute(
parentOrRoute: NonNullable<RouteRecordNameGeneric> | 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)
}
function removeRoute(name: NonNullable<RouteRecordNameGeneric>) {
const recordMatcher = matcher.getRecordMatcher(name)
if (recordMatcher) {
matcher.removeRoute(recordMatcher)
}
}
function getRoutes() {
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
function hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean {
return !!matcher.getRecordMatcher(name)
}
function resolve(
rawLocation: RouteLocationRaw,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocationResolved {
currentLocation = assign({}, currentLocation || currentRoute.value)
if (typeof rawLocation === 'string') {
const locationNormalized = parseURL(
parseQuery,
rawLocation,
currentLocation.path
)
const matchedRoute = matcher.resolve(
{ path: locationNormalized.path },
currentLocation
)
const href = routerHistory.createHref(locationNormalized.fullPath)
// locationNormalized is always a new object
return assign(locationNormalized, matchedRoute, {
params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
})
}
let matcherLocation: MatcherLocationRaw
// path could be relative in object as well
if (rawLocation.path != null) {
matcherLocation = assign({}, rawLocation, {
path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
})
} else {
// remove any nullish param
const targetParams = assign({}, rawLocation.params)
for (const key in targetParams) {
if (targetParams[key] == null) {
delete targetParams[key]
}
}
// pass encoded values to the matcher, so it can produce encoded path and fullPath
matcherLocation = assign({}, rawLocation, {
params: encodeParams(targetParams),
})
// current location params are decoded, we need to encode them in case the
// matcher merges the params
currentLocation.params = encodeParams(currentLocation.params)
}
const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
const hash = rawLocation.hash || ''
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
const fullPath = stringifyURL(
stringifyQuery,
assign({}, rawLocation, {
hash: encodeHash(hash),
path: matchedRoute.path,
})
)
const href = routerHistory.createHref(fullPath)
return assign(
{
fullPath,
hash,
query:
stringifyQuery === originalStringifyQuery
? normalizeQuery(rawLocation.query)
: ((rawLocation.query || {}) as LocationQuery),
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
)
}
function locationAsObject(
to: RouteLocationRaw | RouteLocationNormalized
): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
return typeof to === 'string'
? parseURL(parseQuery, to, currentRoute.value.path)
: assign({}, to)
}
function checkCanceledNavigation(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): NavigationFailure | void {
if (pendingLocation !== to) {
return createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_CANCELLED,
{
from,
to,
}
)
}
}
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
function replace(to: RouteLocationRaw) {
return push(assign(locationAsObject(to), { replace: true }))
}
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
const lastMatched = to.matched[to.matched.length - 1]
if (lastMatched && lastMatched.redirect) {
const { redirect } = lastMatched
let newTargetLocation =
typeof redirect === 'function' ? redirect(to) : redirect
if (typeof newTargetLocation === 'string') {
newTargetLocation =
newTargetLocation.includes('?') || newTargetLocation.includes('#')
? (newTargetLocation = locationAsObject(newTargetLocation))
: // force empty params
{ path: newTargetLocation }
// @ts-expect-error: force empty params when a string is passed to let
// the router parse them again
newTargetLocation.params = {}
}
return assign(
{
query: to.query,
hash: to.hash,
// avoid transferring params if the redirect has a path
params: newTargetLocation.path != null ? {} : to.params,
},
newTargetLocation
)
}
}
// 路由守卫
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
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
// to could be a string where `replace` is a function
const replace = (to as RouteLocationOptions).replace === true
const shouldRedirect = handleRedirectRecord(targetLocation)
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
// keep original redirectedFrom if it exists
redirectedFrom || targetLocation
)
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation as RouteLocationNormalized
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{ to: toLocation, from }
)
// trigger scroll to allow scrolling to the same anchor
handleScroll(
from,
from,
true,
false
)
}
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) =>
isNavigationFailure(error)
? // navigation redirects still mark the router as ready
isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
? error
: markAsReady(error) // also returns the error
: // reject any unknown error
triggerError(error, toLocation, from)
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
if (failure) {
if (
isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
return pushWithRedirect(
// keep options
assign(
{
// preserve an existing replacement but allow the redirect to override it
replace,
},
locationAsObject(failure.to),
{
state:
typeof failure.to === 'object'
? assign({}, data, failure.to.state)
: data,
force,
}
),
// preserve the original redirectedFrom if any
redirectedFrom || toLocation
)
}
} else {
// if we fail we don't finalize the navigation
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
return failure
})
}
/**
* Helper to reject and skip all navigation guards if a new navigation happened
* @param to
* @param from
*/
function checkCanceledNavigationAndReject(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): Promise<void> {
const error = checkCanceledNavigation(to, from)
return error ? Promise.reject(error) : Promise.resolve()
}
function runWithContext<T>(fn: () => T): T {
const app: App | undefined = installedApps.values().next().value
// support Vue < 3.3
return app && typeof app.runWithContext === 'function'
? app.runWithContext(fn)
: fn()
}
// TODO: refactor the whole before guards by internally using router.beforeEach
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
// all components here have been resolved once because we are leaving
guards = extractComponentsGuards(
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
)
// leavingRecords is already reversed
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
null,
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeRouteLeave guards
return (
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components 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)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of enteringRecords) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter) {
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)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from,
runWithContext
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
)
}
function triggerAfterEach(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
failure?: NavigationFailure | void
): void {
afterGuards
.list()
.forEach(guard => runWithContext(() => guard(to, from, failure)))
}
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
replace?: boolean,
data?: HistoryState
): NavigationFailure | void {
// a more recent navigation took place
const error = checkCanceledNavigation(toLocation, from)
if (error) return error
// only consider as push if it's not the first navigation
const isFirstNavigation = from === START_LOCATION_NORMALIZED
const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state
if (isPush) {
if (replace || isFirstNavigation)
routerHistory.replace(
toLocation.fullPath,
assign(
{
scroll: isFirstNavigation && state && state.scroll,
},
data
)
)
else routerHistory.push(toLocation.fullPath, data)
}
// accept current navigation
currentRoute.value = toLocation
handleScroll(toLocation, from, isPush, isFirstNavigation)
markAsReady()
}
let removeHistoryListener: undefined | null | (() => void)
// attach listener to history to trigger navigations
function setupListeners() {
// avoid setting up listeners twice due to an invalid first navigation
if (removeHistoryListener) return
removeHistoryListener = routerHistory.listen((to, _from, info) => {
if (!router.listening) return
const toLocation = resolve(to) as RouteLocationNormalized
const shouldRedirect = handleRedirectRecord(toLocation)
if (shouldRedirect) {
pushWithRedirect(
assign(shouldRedirect, { replace: true }),
toLocation
).catch(noop)
return
}
pendingLocation = toLocation
const from = currentRoute.value
// TODO: should be moved to web history?
if (isBrowser) {
saveScrollPosition(
getScrollKey(from.fullPath, info.delta),
computeScrollPosition()
)
}
navigate(toLocation, from)
.catch((error: NavigationFailure | NavigationRedirectError) => {
if (
isNavigationFailure(
error,
ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
)
) {
return error
}
if (
isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
pushWithRedirect(
(error as NavigationRedirectError).to,
toLocation
// avoid an uncaught rejection, let push call triggerError
)
.then(failure => {
if (
isNavigationFailure(
failure,
ErrorTypes.NAVIGATION_ABORTED |
ErrorTypes.NAVIGATION_DUPLICATED
) &&
!info.delta &&
info.type === NavigationType.pop
) {
routerHistory.go(-1, false)
}
})
.catch(noop)
// avoid the then branch
return Promise.reject()
}
// do not restore history on unknown direction
if (info.delta) {
routerHistory.go(-info.delta, false)
}
// unrecognized error, transfer to the global handler
return triggerError(error, toLocation, from)
})
.then((failure: NavigationFailure | void) => {
failure =
failure ||
finalizeNavigation(
// after navigation, all matched components are resolved
toLocation as RouteLocationNormalizedLoaded,
from,
false
)
// revert the navigation
if (failure) {
if (
info.delta &&
!isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED)
) {
routerHistory.go(-info.delta, false)
} else if (
info.type === NavigationType.pop &&
isNavigationFailure(
failure,
ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
)
) {
// manual change in hash history #916
// it's like a push but lacks the information of the direction
routerHistory.go(-1, false)
}
}
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
})
// avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
.catch(noop)
})
}
// Initialization and Errors
let readyHandlers = useCallbacks<OnReadyCallback>()
let errorListeners = useCallbacks<_ErrorListener>()
let ready: boolean
function triggerError(
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<unknown> {
markAsReady(error)
const list = errorListeners.list()
if (list.length) {
list.forEach(handler => handler(error, to, from))
}
// reject the error no matter there were error listeners or not
return Promise.reject(error)
}
function isReady(): Promise<void> {
if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
return Promise.resolve()
return new Promise((resolve, reject) => {
readyHandlers.add([resolve, reject])
})
}
function markAsReady<E = any>(err: E): E
function markAsReady<E = any>(): void
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()
}
return err
}
// Scroll behavior
function handleScroll(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
isFirstNavigation: boolean
): // the return is not meant to be used
Promise<unknown> {
const { scrollBehavior } = options
if (!isBrowser || !scrollBehavior) return Promise.resolve()
const scrollPosition: _ScrollPositionNormalized | null =
(!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
((isFirstNavigation || !isPush) &&
(history.state as HistoryState) &&
history.state.scroll) ||
null
return nextTick()
.then(() => scrollBehavior(to, from, scrollPosition))
.then(position => position && scrollToPosition(position))
.catch(err => triggerError(err, to, from))
}
const go = (delta: number) => routerHistory.go(delta)
let started: boolean | undefined
const installedApps = new Set<App>()
const router: Router = {
currentRoute,
listening: true,
addRoute,
removeRoute,
clearRoutes: matcher.clearRoutes,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorListeners.add,
isReady,
install(app: App) {
const router = this
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', { // 在访问 app.config.globalProperties.$route 等价于访问当前的 currentRoute
enumerable: true,
get: () => unref(currentRoute),
})
if (
isBrowser &&
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
// see above
started = true
push(routerHistory.location).catch(err => {})
}
const reactiveRoute = {} as RouteLocationNormalizedLoaded
for (const key in START_LOCATION_NORMALIZED) {
Object.defineProperty(reactiveRoute, key, {
get: () => currentRoute.value[key as keyof RouteLocationNormalized],
enumerable: true,
})
}
app.provide(routerKey, router)
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
// the router is not attached to an app anymore
if (installedApps.size < 1) {
// invalidate the current navigation
pendingLocation = START_LOCATION_NORMALIZED
removeHistoryListener && removeHistoryListener()
removeHistoryListener = null
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp()
}
},
}
// TODO: type this as NavigationGuardReturn or similar instead of any
function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
return guards.reduce(
(promise, guard) => promise.then(() => runWithContext(guard)),
Promise.resolve()
)
}
return router
}
RouterView
packages/router/src/RouterView.ts
- inject(routerViewLocationKey)!拿到全局注入的 currentRoute 当前路由
- 通过while 循环找到当前有效的 depth值
- 通过routeToDisplay.value.matched[depth.value] 拿到当前有效的matchedRouteRef
- 通过 matchedRoute.components![currentName]拿到 ViewComponent
- matchedRoute!.props[props.name] 拿到 routeProps
- h函数再通过h函数 传入ViewComponent和 routeProps 返回一个component
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
name: 'RouterView',
inheritAttrs: false,
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},
compatConfig: { MODE: 3 },
setup(props, { attrs, slots }) {
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
const injectedDepth = inject(viewDepthKey, 0)
const depth = computed<number>(() => {
let initialDepth = unref(injectedDepth)
const { matched } = routeToDisplay.value
let matchedRoute: RouteLocationMatched | undefined
while (
(matchedRoute = matched[initialDepth]) &&
!matchedRoute.components
) {
initialDepth++
}
return initialDepth
})
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth.value]
)
provide(
viewDepthKey,
computed(() => depth.value + 1)
)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
const viewRef = ref<ComponentPublicInstance>()
watch( // 实时监听
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
([instance, to, name], [oldInstance, from, oldName]) => {
// copy reused instances
if (to) {
to.instances[name] = instance
if (from && from !== to && instance && instance === oldInstance) {
if (!to.leaveGuards.size) {
to.leaveGuards = from.leaveGuards
}
if (!to.updateGuards.size) {
to.updateGuards = from.updateGuards
}
}
}
if (
instance &&
to &&
(!from || !isSameRouteRecord(to, from) || !oldInstance)
) {
;(to.enterCallbacks[name] || []).forEach(callback =>
callback(instance)
)
}
},
{ flush: 'post' }
)
return () => {
const route = routeToDisplay.value
const currentName = props.name
const matchedRoute = matchedRouteRef.value
const ViewComponent =
matchedRoute && matchedRoute.components![currentName]
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
const routePropsOption = matchedRoute.props[currentName]
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
if (vnode.component!.isUnmounted) {
matchedRoute.instances[currentName] = null
}
}
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
return (
normalizeSlot(slots.default, { Component: component, route }) ||
component
)
}
},
})
RouterLink
packages/router/src/RouterLink.ts
内部渲染一个a标签,点击时候通过navigate方法 执行 'replace' 或者 'push' 跳转路由
export interface RouterLinkOptions {
to: RouteLocationRaw
replace?: boolean
}
export interface RouterLinkProps extends RouterLinkOptions {
custom?: boolean
activeClass?: string
exactActiveClass?: string
ariaCurrentValue?:
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
| 'true'
| 'false'
}
// a标签跳转
function navigate(
e: MouseEvent = {} as MouseEvent
): Promise<void | NavigationFailure> {
if (guardEvent(e)) {
return router[unref(props.replace) ? 'replace' : 'push'](
unref(props.to)
// avoid uncaught errors are they are logged anyway
).catch(noop)
}
return Promise.resolve()
}
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
name: 'RouterLink',
compatConfig: { MODE: 3 },
props: {
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
required: true,
},
replace: Boolean,
activeClass: String,
exactActiveClass: String,
custom: Boolean,
ariaCurrentValue: {
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
default: 'page',
},
},
useLink,
setup(props, { slots }) {
const link = reactive(useLink(props))
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,
}))
// 内部渲染一个a标签
return () => {
const children = slots.default && preferSingleVNode(slots.default(link))
return props.custom
? children
: h(
'a',
{
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
onClick: link.navigate,
class: elClass.value,
},
children
)
}
},
})