Introduction
Vue Router is the official router for Vue.js. Deep integration with Vue.js core makes it easier to build single-page applications (SPA) with Vue.js.
For front-end projects that develop and maintain management background classes, the page structure and composition may be very complicated, so it is particularly important to understand and use Vue Router correctly.
use
create
1. After installing the Vue Router dependency, App.vue
import it in router-view
, it is the container for rendering
<div id="app">
<router-view></router-view>
</div>
2. Create a routerouter/index.js
const routes = [
{ path: '/', component: Home},
{ path: '/login', name: 'login', component: Login},
]
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
export default router
3. main.js
Use routing in
import router from "./router";
const app = createApp(App)
app.use(router)
app.mount('#app')
Then you can access it in any component this.$router
, this.$route
and access the current route as:
// Home.vue
export default {
computed: {
username() {
// 我们很快就会看到 `params` 是什么
return this.$route.params.username
},
},
methods: {
goToDashboard() {
if (isAuthenticated) {
this.$router.push('/dashboard')
} else {
this.$router.push('/login')
}
},
},
}
nested routes
Some application UIs consist of multiple layers of nested components. In this case, the fragments of the URL usually correspond to specific nested component structures, for example:
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
Under the top layer of the upper app node router-view
, the components contained are nested by themselves router-view
, such as the above user
template:
const User = {
template: `
<div class="user">
<h2>User {
{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`,
}
To render components into this nest router-view
, we need to configure in the route children
:
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]
Let's look at how the page is loaded and displayed on the page from the source code point of view
principle
As can be seen from the above basic usage method, it mainly includes three steps:
- Create and use this route
createRouter
in the appuse
router-view
Using tags in templates- navigation
push
, jump page
As can be seen from the array structure declared by routers, the declared route path
will be registered as a routing table pointing to the component
declared component, and when push
the method is called, the corresponding component will be found from the routing table and loaded. Let's take a look at how the source code implements this process. The Vue Router source code analysis version is 4.1.5
create install
First look at createRouter
the method implementation:
/**
* Creates a Router instance that can be used by a Vue app.
*
* @param options - {@link RouterOptions}
*/
export function createRouter(options: RouterOptions): Router {
const matcher = createRouterMatcher(options.routes, options)
// ...
function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
) {
// ...
}
function getRoutes() {
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
function replace(to: RouteLocationRaw) {
return push(assign(locationAsObject(to), { replace: true }))
}
// ...
const router: Router = {
currentRoute,
listening: true,
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,
// 在app全局安装router
install(app: App) {
const router = this
// 全局注册组件RouterLink、RouterView
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// 全局声明router实例,this.$router访问
app.config.globalProperties.$router = router
// 全局注册this.$route 访问当前路由currentRoute
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
// this initial navigation is only necessary on client, on server it doesn't
// make sense because it will create an extra unnecessary navigation and could
// lead to problems
if (
isBrowser &&
// used for the initial navigation client side to avoid pushing
// multiple times when the router is used in multiple apps
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
// see above
// 浏览器情况下,push一个初始页面,不指定url默认首页‘/’
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))
// 全局注入当前路由currentRoute
app.provide(routerViewLocationKey, currentRoute)
// ...
},
}
return router
}
createRouter
this.$router
The method returns the current routing instance, and internally initializes some common routing methods, which are the same as printing the structure in the component . install
Where is the method called? Called during installation app.use(router)
, look at use
the method, runtime-core.cjs.prod.js
below :
use(plugin, ...options) {
if (installedPlugins.has(plugin)) ;
else if (plugin && shared.isFunction(plugin.install)) {
installedPlugins.add(plugin);
// 如果是插件,调用插件的install方法,并把当前app传入
plugin.install(app, ...options);
}
else if (shared.isFunction(plugin)) {
installedPlugins.add(plugin);
plugin(app, ...options);
}
else ;
return app;
},
So far, the global router creation and installation has been completed, and it can be used in the code router-view
, this.$router
and some methods of the example, so how to display the loaded on the page component
? Need to look at router-view
the internal implementation of the rendering component
rendering
install
The method registers RouterView
the component and is implemented in RouterView.ts
:
/**
* Component to display the current route the user is at.
*/
export const RouterView = RouterViewImpl as unknown as {
// ...
}
复制代码
RouterViewImpl
accomplish:
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
name: 'RouterView',
// ...
setup(props, { attrs, slots }) {
__DEV__ && warnDeprecatedUsage()
// 拿到之前注册的currentRoute
const injectedRoute = inject(routerViewLocationKey)!
// 当前要显示的route,监听route值变化时会刷新
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
// 获取当前router-view深度层级,在嵌套路由时使用
const injectedDepth = inject(viewDepthKey, 0)
// 在当前router-view深度下去匹配要显示的路由matched
// matched 是个数组,在resolve方法被赋值,如果有匹配到则在当前router-view渲染
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 at the same time the component instance, the route record we are
// rendering, and the name
// 监听匹配路由变化时,刷新
watch(
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
([instance, to, name], [oldInstance, from, oldName]) => {
// ...
},
{ flush: 'post' }
)
return () => {
const route = routeToDisplay.value
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
const matchedRoute = matchedRouteRef.value
const ViewComponent =
matchedRoute && matchedRoute.components![currentName]
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
// ...
// 关键:h函数,渲染路由中获得的组件
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
return (
// pass the vnode to the slot as a prop.
// h and <component :is="..."> both accept vnodes
normalizeSlot(slots.default, { Component: component, route }) ||
component
)
}
},
})
The core of implementing nested routing is to use depth depth
control. The initial router-view
depth is 0, and the internal nesting depth is incremented by 1. For example, for the following nesting relationships:
const routes = [
{
path: '/',
component: Home,
children: [
{
path: 'product',
component: ProductManage
},
]
},
{ path: '/login', name: 'login', component: Login }
]
They resolve
are parsed as follows in routeToDisplay.value
order:
matched
It is an array. When it push
is resolve
, the current path path
is split and parsed into routes
objects that can be matched in the corresponding array. Then, for the initial value, router-view
the value with a depth of 0 is taken, and for the one with a depth of 1, the corresponding route router-view
is taken and rendered separately.mactched[1]
'/product'
jump
Before analyzing the jump process, first look at the parsing logic of the route registration. The method createRouter
is called in the method createRouterMatcher
. This method creates a route matcher, internally encapsulates the specific implementation of route registration and jump, and creates the router
right matcher
package externally. A layer provides the API and shields the implementation details. Look at the implementation:
/**
* Creates a Router Matcher.
*
* @internal
* @param routes - array of initial routes
* @param globalOptions - global route options
*/
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// normalized ordered array of matchers
// 匹配器的两个容器,匹配器Array和命名路由Map
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// ...
// 如果记录中声明'alias'别名,把别名当作path,插入一条新的记录
if ('alias' in record) {
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
for (const alias of aliases) {
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
// this allows us to hold a copy of the `components` option
// so that async components cache is hold on the original record
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// we might be the child of an alias
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
// the aliases are always of the same kind as the original since they
// are defined on the same record
}) as typeof mainNormalizedRecord
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
for (const normalizedRecord of normalizedRecords) {
// ...
// create the object beforehand, so it can be passed to children
// 遍历记录,生成一个matcher
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
// ...
// 添加到容器
insertMatcher(matcher)
}
return originalMatcher
? () => {
// since other matchers are aliases, they should be removed by the original matcher
removeRoute(originalMatcher!)
}
: noop
}
function removeRoute(matcherRef: RouteRecordName | 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) {
let i = 0
while (
i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
// 将matcher添加到数组末尾
matchers.splice(i, 0, matcher)
// only add the original record to the name map
// 命名路由添加到路由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) {
// 命名路由解析出path
matcher = matcherMap.get(location.name)
// ...
// throws if cannot be stringified
path = matcher.stringify(params)
} else if ('path' in location) {
// no need to resolve the path with the matcher as it was provided
// this also allows the user to control the encoding
path = location.path
//...
matcher = matchers.find(m => m.re.test(path))
// matcher should have a value after the loop
if (matcher) {
// we know the matcher works because we tested the regexp
params = matcher.parse(path)!
name = matcher.record.name
}
// push相对路径
} else {
// match by name or path of current route
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
// since we are navigating to the same location, we don't need to pick the
// params like when `name` is provided
params = assign({}, currentLocation.params, location.params)
path = matcher.stringify(params)
}
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
// reversed order so parents are at the beginning
// 和当前path匹配的记录,插入到数组头部,让父级先匹配
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
// 添加初始路由
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
To sum up, the method executes the method createRouterMatcher
for each method, calls it , and inserts the generated one into the container. When it is called later, through the method, the record is matched to the array where the record is saved , and the follow-up will be based on the fetch from the array. The element that should be rendered. Method execution flow:routres
addRoute
insertMatcher
matchers
resolve
Matcher.record
MatcherLocation
matched
router-view
depth
push
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
// ...
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
// 解析出目标location
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
// ...
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) =>
// ...
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
if (failure) {
// ...
} 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
})
}
Call to do the final jump without failure finalizeNavigation
, see the implementation:
/**
* - Cleans up any navigation guards
* - Changes the url if necessary
* - Calls the scrollBehavior
*/
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 = !isBrowser ? {} : history.state
// change URL only if the user did a push/replace and if it's not the initial navigation because
// it's just reflecting the url
// 如果是push保存历史到routerHistory
if (isPush) {
// on the initial navigation, we want to reuse the scroll position from
// history state if it exists
if (replace || isFirstNavigation)
routerHistory.replace(
toLocation.fullPath,
assign(
{
scroll: isFirstNavigation && state && state.scroll,
},
data
)
)
else routerHistory.push(toLocation.fullPath, data)
}
// accept current navigation
// 给当前路由赋值,会触发监听的router-view刷新
currentRoute.value = toLocation
handleScroll(toLocation, from, isPush, isFirstNavigation)
markAsReady()
}
currentRoute.value = toLocation
After execution, it will trigger the router-view
median routeToDisplay
change, recalculate matchedRouteRef
to obtain a new value ViewComponent
, and complete the page refresh. There are two more points above, router
which will be resolve
called , fill the array just mentioned , and the method will execute the guard on the navigation. I will not read these two steps. Interested students can check the " Hospitalization Certificate Picture " by themselves. So far the main The process has been analyzed.matcher
resolve
matched
navigate