本文基于vue-router 4.1.6
版本源码进行分析
前言
在上一篇《Vue3相关源码-Vue Router源码解析(一)》文章中,我们已经分析了createWebHashHistory()
和createRouter()
的相关内容,本文将继续下一个知识点app.use(router)
展示分析
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: 'Home' }
const About = { template: 'About' }
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router)
app.mount('#app')
初始化
app.use(router)使用VueRouter
use(plugin, ...options) {
if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin);
plugin.install(app, ...options);
}
else if (isFunction(plugin)) {
installedPlugins.add(plugin);
plugin(app, ...options);
}
return app;
}
router.install(app)
从上面Vue3
的源码可以知道,最终会触发Vue Router
的install()
方法
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
};
const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
const router = {
//....
addRoute,
removeRoute,
push,
replace,
beforeEach: beforeGuards.add,
isReady,
install(app) {
//...省略,外部app.use()时调用
},
};
如下面代码块所示,主要执行了:
- 注册了
RouterLink
和RouterView
两个组件 - 注册
router
为Vue
全局对象,保证this.$router
能注入到每一个子组件中 push(routerHistory.location)
:初始化时触发push()
操作(局部作用域下的方法)provide
:router
、reactiveRoute
(本质是currentRoute
的reactive
结构模式)、currentRoute
install(app) {
const router = this;
app.component('RouterLink', RouterLink);
app.component('RouterView', RouterView);
app.config.globalProperties.$router = router;
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => vue.unref(currentRoute), // 自动解构Ref,拿出.value
});
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
started = true;
push(routerHistory.location).catch(err => {
warn('Unexpected error when starting the router:', err);
});
}
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如上面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如上面代码块所示
// 省略Vue.unmount的一些逻辑处理...
}
Vue Router
整体初始化的流程已经分析完毕,一些基础的API
,如router.push
等也在初始化过程中分析,因此下面将分析Vue Router
提供的自定义组件以及组合式API
的内容
组件
RouterView
我们使用代码改变路由时,也在改变的
Component
内容,实现组件渲染
Hello App!
Go to Home
Go to About
就是一个Vue Component
代码量还是挺多的,因此下面将切割为2个部分进行分析
const RouterViewImpl = vue.defineComponent({
setup(props, { attrs, slots }) {
//========= 第1部分 =============
const injectedRoute = vue.inject(routerViewLocationKey);
const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {
let initialDepth = vue.unref(injectedDepth);
const { matched } = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {
initialDepth++;
}
return initialDepth;
});
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
vue.provide(matchedRouteKey, matchedRouteRef);
vue.provide(routerViewLocationKey, routeToDisplay);
const viewRef = vue.ref();
vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
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 () => {
//========= 第2部分 =============
//...
};
},
});
第1部分:初始化变量以及初始化响应式变化监听
routerViewLocationKey拿到目前的路由injectedRoute
// 代码
const injectedRoute = vue.inject(routerViewLocationKey);
在app.use(router)
的源码中,我们可以知道,routerViewLocationKey
代表的是目前的currentRoute
,每次路由变化时,会触发finalizeNavigation()
,同时更新currentRoute.value
=toLocation
因此currentRoute
代表的就是目前最新的路由
install(app) {
//...
const router = this;
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如上面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如上面代码块所示
// 省略Vue.unmount的一些逻辑处理...
}
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
};
function createRouter(options) {
const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {
//...
currentRoute.value = toLocation;
//...
}
routeToDisplay
实时同步为目前最新要跳转的路由
使用computed
监听路由变化,优先获取props.route
,如果有发生路由跳转现象,则routeToDisplay
会动态变化
// 代码
const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
depth
实时同步为目前要跳转的路由对应的matched数组的index
当路由发生变化时,会触发routeToDisplay
发生变化,depth
使用computed
监听routeToDisplay
变化
const depth = vue.computed(() => {
let initialDepth = vue.unref(injectedDepth);
const { matched } = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {
initialDepth++;
}
return initialDepth;
});
那Vue Router
是如何利用depth
来不断加载目前路由路径上所有的Component
的呢?
如下面代码块所示,我们在push()
->resolve()
的流程中,会收集当前路由的所有parent
举个例子,路径path
="/child1/child2"
,那么我们拿到的matched
就是[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
function push(to) {
return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {
const targetLocation = (pendingLocation = resolve(to));
//...
}
function resolve(location, currentLocation) {
//....
const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {
// reversed order so parents are at the beginning
matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent;
}
return {matched, ...}
}
当我们跳转到path
="/child1/child2"
时,会首先加载path="/child1"
对应的Child1
组件,此时injectedDepth
=0
,在depth
的computed()
计算中,得出initialDepth
=0
,因为matchedRoute.components
是存在的,无法进行initialDepth++
因此此时
组件拿的数据是matched[0]
={path: '/child1', Component: parent}
[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
// Child1组件
目前路由是Child1
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {
let initialDepth = vue.unref(injectedDepth);
const { matched } = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {
initialDepth++;
}
return initialDepth;
});
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
此时Child1
组件仍然有
组件,因此我们再次初始化一次RouterView
,加载path="/child1/child2"
对应的Child2
组件
在上面的代码我们可以知道,viewDepthKey
会变为depth.value + 1
,因此此时
中injectedDepth
=1
,在depth
的computed()
计算中,得出initialDepth
=1
,因为matchedRoute.components
是存在的,无法进行initialDepth++
从这里我们可以轻易猜测出,如果路径上有一个片段,比如path
='/child1/child2/child3'
中child2
没有对应的components
,那么就会跳过这个child2
,直接渲染child1
和child3
对应的组件
因此此时
组件拿的数据是matched[1]
={path: '/child1/child2', Component: son}
// Child2组件
目前路由是Child2
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {
let initialDepth = vue.unref(injectedDepth);
const { matched } = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {
initialDepth++;
}
return initialDepth;
});
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
matchedRouteRef
实时同步为目前最新要跳转的路由对应的matcher
// 代码
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
更新组件示例,触发beforeRouteEnter
回调
const viewRef = vue.ref();
vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
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;
}
}
}
// trigger beforeRouteEnter next callbacks
if (instance &&
to &&
(!from || !isSameRouteRecord(to, from) || !oldInstance)) {
(to.enterCallbacks[name] || []).forEach(callback => callback(instance));
}
}, { flush: 'post' });
而viewRef.value
是在哪里赋值的呢?请看下面组件渲染的分析
第2部分:组件渲染
setup()
中监听routeToDisplay
变化触发组件重新渲染
当响应式数据发生变化时,会触发setup()
重新渲染,调用vue.h
进行新的Component
的渲染更新,此时的viewRef.value
通过vue.h
赋值
const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterView',
setup() {
const viewRef = vue.ref();
return () => {
const route = routeToDisplay.value;
const currentName = props.name; //默认值为"default"
const matchedRoute = matchedRouteRef.value;
const ViewComponent = matchedRoute && matchedRoute.components[currentName];
const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
}));
//...省略了routeProps和onVnodeUnmounted相关代码逻辑
return (
// pass the vnode to the slot as a prop.
// h and both accept vnodes
normalizeSlot(slots.default, { Component: component, route }) ||
component);
}
}
});
RouterLink
从下面的代码块可以知道,整个组件本质就是使用useLink()
封装的一系列方法,然后渲染一个"a"
标签,上面携带了点击事件、要跳转的href
、样式等等
因此这个组件的核心内容就是分析useLink()
到底封装了什么方法,这个部分我们接下来会进行详细的分析
const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true,
},
replace: Boolean,
//...
},
useLink,
setup(props, { slots }) {
const link = vue.reactive(useLink(props));
const { options } = vue.inject(routerKey);
const elClass = vue.computed(() => ({
[getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
[getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
}));
return () => {
const children = slots.default && slots.default(link);
return props.custom
? children
: vue.h('a', {
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
onClick: link.navigate,
class: elClass.value,
}, children);
};
},
});
组合式API
onBeforeRouteLeave
在上面app.use(router)
的分析中,我们可以知道,一开始就会初始化RouterView
组件,而在初始化时,我们会进行vue.provide(matchedRouteKey, matchedRouteRef)
,将目前匹配路由的matcher
放入到key:matchedRouteKey
中
install(app) {
const router = this;
app.component('RouterLink', RouterLink);
app.component('RouterView', RouterView);
//...
}
const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterView',
//...
setup(props, { attrs, slots }) {
const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
vue.provide(matchedRouteKey, matchedRouteRef);
return () => {
//...
}
}
})
在onBeforeRouteLeave()
中,我们拿到的activeRecord
就是目前路由对应的matcher
,然后将外部传入的leaveGuard
放入到我们的matcher["leaveGuard"]
中,在路由跳转的navigate()
方法中进行调用
function onBeforeRouteLeave(leaveGuard) {
if (!vue.getCurrentInstance()) {
warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function');
return;
}
const activeRecord = vue.inject(matchedRouteKey,
// to avoid warning
{}).value;
if (!activeRecord) {
warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of . Maybe you called it inside of App.vue?');
return;
}
registerGuard(activeRecord, 'leaveGuards', leaveGuard);
}
function registerGuard(record, name, guard) {
const removeFromList = () => {
record[name].delete(guard);
};
vue.onUnmounted(removeFromList);
vue.onDeactivated(removeFromList);
vue.onActivated(() => {
record[name].add(guard);
});
record[name].add(guard);
}
onBeforeRouteUpdate
跟上面分析的onBeforeRouteLeave
一模一样的流程,拿到目前路由对应的matcher
,然后将外部传入的updateGuards
放入到我们的matcher["updateGuards"]
中,在路由跳转的navigate()
方法中进行调用
function onBeforeRouteUpdate(updateGuard) {
if (!vue.getCurrentInstance()) {
warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function');
return;
}
const activeRecord = vue.inject(matchedRouteKey,
// to avoid warning
{}).value;
if (!activeRecord) {
warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside a component child of . Maybe you called it inside of App.vue?');
return;
}
registerGuard(activeRecord, 'updateGuards', updateGuard);
}
function registerGuard(record, name, guard) {
const removeFromList = () => {
record[name].delete(guard);
};
vue.onUnmounted(removeFromList);
vue.onDeactivated(removeFromList);
vue.onActivated(() => {
record[name].add(guard);
});
record[name].add(guard);
}
useRouter
在之前的分析app.use(router)
中,我们可以知道,我们将当前的router
使用provide
进行存储,即app.provide(routerKey, router);
install(app) {
//...
const router = this;
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如上面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如上面代码块所示
// 省略Vue.unmount的一些逻辑处理...
}
因此useRouter()
本质就是拿到目前Vue
使用Vue Router
示例
function useRouter() {
return vue.inject(routerKey);
}
useRoute
从上面useRouter()
的分析中,我们可以知道,routeLocationKey
对应的就是当前路由currentRoute
封装的响应式对象reactiveRoute
因此const route = useRoute()
代表的就是当前路由的响应式对象
function useRoute() {
return vue.inject(routeLocationKey);
}
useLink
Vue Router
将RouterLink
的内部行为作为一个组合式 API 函数公开。它提供了与 v-slotAPI 相同的访问属性:
import { RouterLink, useLink } from 'vue-router'
import { computed } from 'vue'
export default {
name: 'AppLink',
props: {
// 如果使用 TypeScript,请添加 @ts-ignore
...RouterLink.props,
inactiveClass: String,
},
setup(props) {
const { route, href, isActive, isExactActive, navigate } = useLink(props)
const isExternalLink = computed(
() => typeof props.to === 'string' && props.to.startsWith('http')
)
return { isExternalLink, href, navigate, isActive }
},
}
RouterLink
组件提供了足够的props
来满足大多数基本应用程序的需求,但它并未尝试涵盖所有可能的用例,在某些高级情况下,你可能会发现自己使用了v-slot
。在大多数中型到大型应用程序中,值得创建一个(如果不是多个)自定义RouterLink
组件,以在整个应用程序中重用它们。例如导航菜单中的链接,处理外部链接,添加inactive-class
等useLink()
是为了扩展RouterLink
而服务的
从下面的代码块可以知道,userLink()
主要提供了
route
: 获取props.to
进行router.resolve()
,拿到要跳转的新路由的route
对象href
: 监听route.value.href
isActive
: 当前路由与props.to
部分匹配isExactActive
: 当前路由与props.to
完全精准匹配navigate()
: 跳转方法,实际还是调用router.push(props.to)
/router.replace(props.to)
function useLink(props) {
const router = vue.inject(routerKey);
const currentRoute = vue.inject(routeLocationKey);
const route = vue.computed(() => router.resolve(vue.unref(props.to)));
const activeRecordIndex = vue.computed(() => {
//...
});
const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
includesParams(currentRoute.params, route.value.params));
const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
activeRecordIndex.value === currentRoute.matched.length - 1 &&
isSameRouteLocationParams(currentRoute.params, route.value.params));
function navigate(e = {}) {
if (guardEvent(e)) {
return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)
// avoid uncaught errors are they are logged anyway
).catch(noop);
}
return Promise.resolve();
}
return {
route,
href: vue.computed(() => route.value.href),
isActive,
isExactActive,
navigate,
};
}
其中activeRecordIndex
的逻辑比较复杂,我们摘出来单独分析下
const activeRecordIndex = vue.computed(() => {
const { matched } = route.value;
const { length } = matched;
const routeMatched = matched[length - 1];
const currentMatched = currentRoute.matched;
if (!routeMatched || !currentMatched.length)
return -1;
const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
if (index > -1)
return index;
const parentRecordPath = getOriginalPath(matched[length - 2]);
return (
length > 1 &&
getOriginalPath(routeMatched) === parentRecordPath &&
currentMatched[currentMatched.length - 1].path !== parentRecordPath
? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
: index);
});
直接看上面的代码可能有些懵,直接去源码中找对应位置提交的git
记录
从RouterLink.spec.ts
中可以发现增加以下这段代码
it('empty path child is active as if it was the parent when on adjacent child', async () => {
const { wrapper } = await factory(
locations.child.normalized,
{ to: locations.childEmpty.string },
locations.childEmpty.normalized
)
expect(wrapper.find('a')!.classes()).toContain('router-link-active')
expect(wrapper.find('a')!.classes()).not.toContain(
'router-link-exact-active'
)
})
从RouterLink.spec.ts
拿到locations.child
和locations.childEmpty
的值,如下所示
child: {
string: '/parent/child',
normalized: {
fullPath: '/parent/child',
href: '/parent/child',
matched: [records.parent, records.child]
}
}
childEmpty: {
string: '/parent',
normalized: {
fullPath: '/parent',
href: '/parent',
matched: [records.parent, records.childEmpty]
}
}
上面实际就是进行/parent/child
->/parent
的跳转,并且childEmpty
对应的/parent
还有两个matched
元素,说明它的子路由的path
=""
结合上面router-link-active
、router-link-exact-active
、/parent/child
->/parent
出现的关键字,以及Vue Router
源码提交记录的App.vue
和router.ts
的修改记录,我们可以构建出以下的测试文件,具体代码放在github地址
跳转到/child/a
跳转到父路由/child
跳转到没有路径的子路由/child
const routes = [
{path: '/', component: Home},
{
path: '/child',
component: TEST,
name: 'WithChildren',
children: [
{ path: '', name: 'default-child', component: TEST },
{ path: 'a', name: 'a-child', component: TEST },
]
}
]
当我们处于home
路由时
当我们点击跳转到/child/a
路由时,我们可以发现
- 完全匹配的路由增加两个
class
:router-link-active
和router-link-exact-active
- 路径上只有
/child
匹配的路由增加了class
:router-link-active
从上面的测试结果中,我们可以大概猜测出
如果
中的to
所代表的路由是可以在当前路由中找到的,则加上router-link-active
和router-link-exact-active
如果to
所代表的路由(或者它的嵌套parent
路由,只要它path
=嵌套parent
路由的path
)是可以在当前路由的嵌套parent
路由中找到的,加上router-link-active
现在我们可以尝试对activeRecordIndex
代码进行分析
const route = computed(() => router.resolve(unref(props.to)))
const activeRecordIndex = vue.computed(() => {
const { matched } = route.value;
const { length } = matched;
const routeMatched = matched[length - 1];
const currentMatched = currentRoute.matched;
if (!routeMatched || !currentMatched.length)
return -1;
const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
if (index > -1)
return index;
const parentRecordPath = getOriginalPath(matched[length - 2]);
return (
length > 1 &&
getOriginalPath(routeMatched) === parentRecordPath &&
currentMatched[currentMatched.length - 1].path !== parentRecordPath
? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
: index);
});
由于
初始化时就会注册,因此每一个
都会初始化上面的代码,进行computed
的监听,而此时route
代表传进来的路由,即
跳转到没有路径的子路由/child
初始化时的props.to
="{ name: 'default-child' }"
当目前的路由发生变化时,即currentRoute
发生变化时,也会触发computed(fn)
重新执行一次,此时会去匹配props.to
="/child"
所对应的matched[最后一位index]
能否在currentRoute
对应的matched
找到,如果找到了,直接返回index
如果找不到,则使用
目前对应的props.to
的matched
的倒数第二个matched[最后第二位index]
,看看这个倒数第二个matched[最后第二位index]
能不能在currentRoute
对应的matched
找到,如果找到了,直接返回index
使用props.to
的matched数组
的倒数第二个matched[最后第二位index]
的前提是对应的路由是它
path
=嵌套parent
路由的path
那activeRecordIndex
的用处是什么呢?
在上面userLink
的源码分析中,我们知道了isActive
和isExactActive
的赋值是通过computed
计算
const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
includesParams(currentRoute.params, route.value.params));
const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
activeRecordIndex.value === currentRoute.matched.length - 1 &&
isSameRouteLocationParams(currentRoute.params, route.value.params));
在嵌套路由中找到符合条件的
是isActive=true
,但是isExactActive=false
只有符合currentRoute.matched.length - 1
条件下匹配的
才是isActive=true
和isExactActive=true
而isActive
和isExactActive
也就是添加router-link-active
/router-link-exact-active
的条件
issues分析
Vue Router
中有大量注释,其中包含一些issues
的注释,本文将进行简单地分析Vue Router 4.1.6
源码中出现的issues
注释每一个
issues
都会结合github
上的讨论记录以及作者提交的源码修复记录进行分析,通过issues
的分析,可以明白Vue Router 4.1.6
源码中很多看起来不知道是什么东西的逻辑
issues/685注释分析
function changeLocation(to, state, replace) {
/**
* if a base tag is provided, and we are on a normal domain, we have to
* respect the provided `base` attribute because pushState() will use it and
* potentially erase anything before the `#` like at
* https://github.com/vuejs/router/issues/685 where a base of
* `/folder/#` but a base of `/` would erase the `/folder/` section. If
* there is no host, the ` ` tag makes no sense and if there isn't a
* base tag we can just use everything after the `#`.
*/
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) {
{
warn('Error with push/replace State', err);
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url);
}
}
issues/685问题描述
当部署项目到子目录后,访问[https://test.vladovic.sk/router-bug/](https://test.vladovic.sk/router-bug/)
- 预想结果路径变为:
https://test.vladovic.sk/router-bug/#/
- 实际路径变为:
https://test.vladovic.sk/#/
提出issues
的人还提供了正确部署链接:[https://test.vladovic.sk/router](https://test.vladovic.sk/router)
,能够正常跳转到https://test.vladovic.sk/router/#/
,跟错误链接代码的区别在于文件使用了
<%= htmlWebpackPlugin.options.title %>
HTML元素 指定用于一个文档中包含的所有相对 URL 的根 URL。一份中只能有一个 元素。
一个文档的基本 URL,可以通过使用 document.baseURI(en-US) 的 JS 脚本查询。如果文档不包含元素,baseURI 默认为 document.location.href。
issues/685问题发生的原因
在VueRouter 4.0.2
版本中,changeLocation()
直接使用base.indexOf("#")
进行后面字段的截取
function changeLocation(to, state, replace) {
// when the base has a `#`, only use that for the URL
const hashIndex = base.indexOf('#');
const url = hashIndex > -1
? 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 ((process.env.NODE_ENV !== 'production')) {
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);
}
}
将发生问题的项目git clone
在本地运行,打开http://localhost:8080/router-bug/
时,初始化时会触发changeLocation("/")
,从而触发history['pushState'](state, '', '#/')
逻辑
- 目前的
location.href
是http://localhost:8080/router-bug/
对应的
- 触发了
history['pushState'](state, '', '#/')
上面3种条件使得目前的链接更改为:http://localhost:8080/#/
issues/685修复分析
在github issues的一开始的讨论中,最先建议的是更改对应的
,因为history.pushState
会用到
属性
后面提交了一个修复记录,如下图所示:
那为什么要增加
标签的判断呢?
我们在一开始初始化的时候就知道,createWebHashHistory()
支持传入一个base
默认的字符串,如果不传入,则取location.pathname+location.search
,在上面http://localhost:8080/router-bug/
这个例子中,我们是没有传入一个默认的字符串,因此base
="/router-bug/#"
function createWebHashHistory(base) {
// Make sure this implementation is fine in terms of encoding, specially for IE11
// for `file://`, directly use the pathname and ignore the base
// href="https://example.com"的location.pathname也是"/"
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);
}
HTML 元素指定用于文档中所有相对 URL 的基本 URL。如果文档没有
元素,则 baseURI
默认为 location.href
很明显,目前对应的
跟目前的base
="/router-bug/#"
是冲突的,因为url
=base.slice(hashIndex)+to
是建立在history.pushState
对应的地址包含location.pathname
的前提下,而可能存在对应的
就是不包含location.pathname
// VueRouter 4.0.2,未修复前的代码
function changeLocation(to, state, replace) {
// when the base has a `#`, only use that for the URL
const hashIndex = base.indexOf('#');
const url = hashIndex > -1
? base.slice(hashIndex) + to
: createBaseLocation() + base + to;
}
因此当我们检测到存在
标签时,我们直接使用base
(经过格式化处理过的,包含location.pathname
)的字符串进行当前history.pushState()
地址的拼接,杜绝这种可能冲突的情况
下面代码中的base
不是的
标签!!是我们处理过的
base
字符串
// VueRouter修复后的代码
function changeLocation(to, state, replace) {
const hashIndex = base.indexOf('#');
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
}
issues/366注释分析
function push(to, data) {
// Add to current entry the information of where we are going
// as well as saving the current position
const currentState = assign({},
// use current history state to gracefully handle a wrong call to
// history.replaceState
// https://github.com/vuejs/router/issues/366
historyState.value, history.state, {
forward: to,
scroll: computeScrollPosition(),
});
if (!history.state) {
warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
`history.replaceState(history.state, '', url)\n\n` +
`You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`);
}
changeLocation(currentState.current, currentState, true);
const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);
changeLocation(to, state, false);
currentLocation.value = to;
}
issues/366问题描述
这个issues
包含了两个人的反馈
手动history.replaceState没有传递当前state,手动触发router.push
报错
开发者甲在router.push(...)
之前调用window.history.replaceState(...)
window.history.replaceState({}, '', ...)
router.push(...)
然后就报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL
currentState.current拿不到具体的地址
function push(to, data) {
const currentState = assign({}, history.state, {
forward: to,
scroll: computeScrollPosition(),
});
changeLocation(currentState.current, currentState, true); // this is the line that fails
}
跳转到授权页面,授权成功后回来进行router.push
报错
开发者乙从A页面跳转到B页面后,B页面授权成功后,重定向回来A页面,然后调用router.push()
报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL
currentState.current拿不到具体的地址
function push(to, data) {
const currentState = assign({}, history.state, {
forward: to,
scroll: computeScrollPosition(),
});
changeLocation(currentState.current, currentState, true); // this is the line that fails
}
issues/366问题发生的原因
Vue Router
作者在官方文档中强调:
Vue Router 将信息保存在history.state
上。如果你有任何手动调用 history.pushState()
的代码,你应该避免它,或者用的router.push()
和 history.replaceState()
进行重构:
// 将
history.pushState(myState, '', url)
// 替换成
await router.push(url)
history.replaceState({ ...history.state, ...myState }, '')
同样,如果你在调用 history.replaceState() 时没有保留当前状态,你需要传递当前 history.state:
// 将
history.replaceState({}, '', url)
// 替换成
history.replaceState(history.state, '', url)
原因:我们使用历史状态来保存导航信息,如滚动位置,以前的地址等。
以上内容摘录于Vue Router官方文档
开发者甲的问题将当前的history.state
添加进去后就解决了问题,即
// 将
history.replaceState({}, '', url)
// 替换成
history.replaceState(history.state, '', url)
而开发者乙的问题是在Vue Router的A页面
->B页面
->Vue Router的A页面
的过程中丢失了当前的history.state
开发者乙也在使用的授权开源库提了amplify-js issues
Vue Router
作者则建议当前history.state
应该保留,不应该清除
因此无论开发者甲还是开发者乙,本质都是没有保留好当前的history.state
导致的错误
issues/366修复分析
经过上面问题的描述以及原因分析后,我们知道要修复问题的关键就是保留好当前的history.state
因此Vue Router
作者直接使用变量historyState
来进行数据合并
当你实际的window.history.state
丢失时,我们还有一个自己维护的historyState
数据,它在正常路由情况下historyState.value
就等于window.history.state
无论因为什么原因丢失了当前浏览器自身的window.history.state
,最起码有个自己定义的historyState.value
,就能保证currentState.current
不会为空,执行到语句history.replaceState
或者history.pushState
时都能有正确的state
如下面代码块所示,historyState.value
初始化就是history.state
,并且在每次路由变化时都实时同步更新
function useHistoryStateNavigation(base) {
const historyState = { value: history.state };
function changeLocation(to, state, replace) {
//...
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state;
}
}
function useHistoryListeners(base, historyState, currentLocation, replace) {
const popStateHandler = ({ state, }) => {
const to = createCurrentLocation(base, location);
//...
if (state) {
currentLocation.value = to;
historyState.value = state;
}
}
window.addEventListener('popstate', popStateHandler);
}
issues/328分析
function resolve(rawLocation, currentLocation) {
//...
return assign({
fullPath,
// keep the hash encoded so fullPath is effectively path + encodedQuery +
// hash
hash,
query:
// if the user is using a custom query lib like qs, we might have
// nested objects, so we keep the query as is, meaning it can contain
// numbers at `$route.query`, but at the point, the user will have to
// use their own type anyway.
// https://github.com/vuejs/router/issues/328#issuecomment-649481567
stringifyQuery$1 === stringifyQuery
? normalizeQuery(rawLocation.query)
: (rawLocation.query || {}),
}, matchedRoute, {
redirectedFrom: undefined,
href,
});
}
issues/328问题描述
在Vue Router 4.0.0-alpha.13
版本中,并没有考虑query
嵌套的情况,比如下面这种情况
目前的地址是:{{ $route.fullPath}}
Home
Page 1
Page 2
点击Page1
,$route.fullPath
=/?users%5Bpage%5D=1
点击Page2
,$route.fullPath
还是/?users%5Bpage%5D=1
理想情况下,应该有变化,$route.fullPath
会变为/?users%5Bpage%5D=2
issues/328问题发生的原因
在Vue Router 4.0.0-alpha.13
版本中,在进行路由跳转时,会触发isSameRouteLocation()
的检测
function push(to) {
return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {
const targetLocation = (pendingLocation = resolve(to));
//...
if (!force && isSameRouteLocation(from, targetLocation)) {
failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });
}
return (failure ? Promise.resolve(failure) : navigate(toLocation, from));
}
在isSameRouteLocation()
中,会使用isSameLocationObject(a.query, b.query)
进行检测,如果此时的query
是两个嵌套的Object
数据,会返回true
,导致Vue Router
认为是同一个路由,无法跳转成功,自然也无法触发$route.fullPath
改变
function isSameRouteLocation(a, b) {
//...
return (aLastIndex > -1 &&
aLastIndex === bLastIndex &&
isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
isSameLocationObject(a.params, b.params) &&
isSameLocationObject(a.query, b.query) &&
a.hash === b.hash);
}
function isSameLocationObject(a, b) {
if (Object.keys(a).length !== Object.keys(b).length)
return false;
for (let key in a) {
if (!isSameLocationObjectValue(a[key], b[key]))
return false;
}
return true;
}
function isSameLocationObjectValue(a, b) {
return Array.isArray(a)
? isEquivalentArray(a, b)
: Array.isArray(b)
? isEquivalentArray(b, a)
: a === b;
}
function isEquivalentArray(a, b) {
return Array.isArray(b)
? a.length === b.length && a.every((value, i) => value === b[i])
: a.length === 1 && a[0] === b;
}
从上面的代码可以知道,如果我们的a.query
是一个嵌套Object
,最终会触发isSameLocationObjectValue()
的a===b
的比较,最终应该会返回false
才对,那为什么会返回true
呢?
那是因为在调用isSameRouteLocation()
之前会进行resolve(to)
操作,而在这个方法中,我们会进行normalizeQuery(rawLocation.query)
,无论rawLocation.query
是嵌套多少层的Object
,normalizeQuery()
中的'' + value
都会变成'[object Object]'
,因此导致了a===b
的比较实际就是'[object Object]'
==='[object Object]'
,返回true
function createRouter(options) {
function resolve(rawLocation, currentLocation) {
return assign({
fullPath,
hash,
query: normalizeQuery(rawLocation.query),
}, matchedRoute, {
redirectedFrom: undefined,
href: routerHistory.base + fullPath,
});
}
function push(to) {
return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {
const targetLocation = (pendingLocation = resolve(to));
//...
if (!force && isSameRouteLocation(from, targetLocation)) {
failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });
}
}
}
function normalizeQuery(query) {
const normalizedQuery = {};
for (let key in query) {
let value = query[key];
if (value !== undefined) {
normalizedQuery[key] = Array.isArray(value)
? value.map(v => (v == null ? null : '' + v))
: value == null
? value
: '' + value;
}
}
return normalizedQuery;
}
issues/328修复分析
初始化可传入stringifyQuery
,内部对query
不进行处理
由于query
可能是复杂的结构,因此修复该问题第一考虑的点就是放开给开发者自己解析
开发者可以在初始化createRouter()
传入自定义的parseQuery()
和stringifyQuery()
方法,开发者自行解析和转化目前的query
参数
- 如果开发者不传入自定义的
stringifyQuery()
方法,那么stringifyQuery
就会等于originalStringifyQuery
(一个Vue Router
内置的stringifyQuery
方法),这个时候query
就会使用normalizeQuery(rawLocation.query)
进行数据的整理,最终返回的还是一个Object
对象 - 如果开发者传入自定义的
stringifyQuery()
方法,那么就不会触发任何处理,还是使用rawLocation.query
,在上面示例中就是一个嵌套的Object
对象,避免使用normalizeQuery()
将'' + value
变成'[object Object]'
的情况
isSameRouteLocation
比较query
时,使用它们stringify
处理后的字符串进行比较
开发者可以自定义传入stringifyQuery()
进行复杂结构的处理,然后返回字符串进行比较
如果不传入stringifyQuery()
,则使用默认的方法进行stringify
,然后根据返回的字符串进行比较
默认方法的stringify
不会考虑复杂的数据结构,只会当做普通对象进行stringify
issues/1124分析
function insertMatcher(matcher) {
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++;
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
issues/1124问题描述
在Vue Router 4.0.11
中
使用动态添加路由
addRoute()
为当前的name="Root"
路由添加子路由后- 我们想要访问
Component:B
,使用了router.push("/")
,但是无法渲染出Component:B
,它渲染的是它的上一级路径Component: Root
- 而如果我们使用
router.push({name: 'Home'})
时,就能正常访问Component:B
- 我们想要访问
- 如果我们不使用动态添加路由,直接在初始化的时候,如下面注释
children
那样,直接添加Component:B
,当我们使用router.push("/")
,可以正常渲染出Component:B
const routes = [
{
path: '/',
name: 'Root',
component: Root,
// Work with non dynamic add and empty path
/*children: [
{
path: '',
component: B
}
]*/
}
];
// Doesn't work with empty path and dynamic adding
router.addRoute('Root', {
path: '',
name: 'Home',
component: B
});
issues/1124问题发生的原因
当静态添加路由时,由于是递归调用addRoute()
,即父addRoute()
->子addRoute
->子insertMatcher()
->父addRoute()
,因此子matcher
是排在父matcher
前面的,因为从《Vue3相关源码-Vue Router源码解析(一)》文章中计算路由权重的逻辑可以知道,路径相同分数则相同,comparePathParserScore()
的值为0
,因此先调用insertMatcher()
,位置就越靠前,因此子路由位置靠前
function addRoute(record, parent, originalRecord) {
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
//...
insertMatcher(matcher);
}
function insertMatcher(matcher) {
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))
i++;
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
但是动态添加路由时,同样是触发matcher.addRoute()
,只不过由于是动态添加,因此要添加的新路由的parent
之前已经插入到matchers
数组中去了,由上面的分析可以知道,路由权重相同,因此先调用insertMatcher()
,位置就越靠前,此时子路由位置靠后
function createRouter(options) {
function addRoute(parentOrRoute, route) {
let parent;
let record;
if (isRouteName(parentOrRoute)) {
parent = matcher.getRecordMatcher(parentOrRoute);
record = route;
}
else {
record = parentOrRoute;
}
return matcher.addRoute(record, parent);
}
}
function createRouterMatcher(routes, globalOptions) {
function addRoute(record, parent, originalRecord) {
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
//...
insertMatcher(matcher);
}
}
在上一篇文章《Vue3相关源码-Vue Router源码解析(一)》的分析中,我们知道,当我们使用router.push("/")
或者router.push({path: "/"})
时,会触发正则表达式的匹配,如下面代码所示,matchers.find()
会优先匹配位置靠前的路由matcher
,从而发生了动态添加子路由找不到子路由(位置比较靠后),初始化添加子路由能够渲染子路由(位置比较靠前)的情况
path = location.path;
matcher = matchers.find(m => m.re.test(path));
if (matcher) {
params = matcher.parse(path);
name = matcher.record.name;
}
issues/1124修复分析
既然是添加顺序导致的问题,那么只要让子路由动态添加时,遇到它的parent
不要i++
即可,如下面修复提交代码所示,当子路由动态添加时,检测目前对比的是不是它的parent
,如果是它的parent
,则阻止i++
,子路由位置就可以顺利地放在它的parent
前面
issues/916分析
很长的一段navigte()
失败之后的处理.....简化下
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {
if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
return error;
}
if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
pushWithRedirect(error.to, toLocation)
.then(failure => {
// manual change in hash history #916 ending up in the URL not
// changing, but it was changed by the manual url change, so we
// need to manually change it ourselves
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* 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) => {
failure =
failure ||
finalizeNavigation(
// after navigation, all matched components are resolved
toLocation, from, false);
// revert the navigation
if (failure) {
if (info.delta &&
// a new navigation has been triggered, so we do not want to revert, that will change the current history
// entry while a different route is displayed
!isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
routerHistory.go(-info.delta, false);
} else if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* 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, from, failure);
})
.catch(noop);
});
}
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {
// error是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED
return error;
// error是 NAVIGATION_GUARD_REDIRECT
pushWithRedirect(error.to, toLocation)
.then(failure => {
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
!info.delta &&
info.type === NavigationType.pop) {
routerHistory.go(-1, false);
}
})
})
.then((failure) => {
if (failure) {
if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
routerHistory.go(-1, false);
}
}
})
});
}
issues/916问题描述
目前有两个路由,/login
路由和/about
路由,它们配置了一个全局的导航守卫,当遇到/about
路由时,会重定向到/login
路由
router.beforeEach((to, from) => {
if (to.path.includes('/login')) {
return true;
} else {
return {
path: '/login'
}
}
})
目前问题是:
- 目前是
/login
路由,各方面正常,手动在浏览器地址更改/login
->/about
- 期望行为是:由于
router.beforeEach
配置了重定向跳转,浏览器地址会重新变为/login
,页面也还是保留在Login
组件 - 实际表现是:浏览器地址是
/about
,页面保留在Login
组件,造成浏览器地址跟实际映射组件不符合的bug
issues/916问题发生的原因
issues/916修复分析
如果导航守卫next()
返回的是路由数据,会触发popStateHandler()
->navigate()
->pushWithRedirect()
,然后返回NAVIGATION_DUPLICATED
,因此我们要做的就是回退一个路由,并且不触发组件更新,即routerHistory.go(-1, false)
因为NAVIGATION_DUPLICATED
就意味着要重定向的这个新路由(/login
)跟启动重定向路由(/about
)之前的路由(/login
)是重复的,那么这个启动重定向路由(/about
)就得回退,因为它(/about
)势必会不成功
除了在pushWithRedirect()
上修复错误之外,还在navigate().then()
上也进行同种状态的判断
这是为什么呢?除了next("/login")
之外,还有情况导致错误吗?
这是因为除了next("/login")
,还有一种可能就是next(false)
也会导致错误,即
router.beforeEach((to, from) => {
if (to.path.includes('/login')) {
return true;
} else {
return false;
// return {
// path: '/login'
// }
}
})
当重定向路由改为false
时,如下面代码块所示,navigate()
会返回NAVIGATION_ABORTED
的错误,从而触发navigate().catch(()=> error)
而Promise.catch()
中返回值,这个值也会包裹触发下一个then()
,也就是说NAVIGATION_ABORTED
会传递给navigate().catch().then()
中,因此还需要在then()
里面进行routerHistory.go(-1, false)
NAVIGATION_ABORTED
意味着这种routerHistory.listen
传递的to
路由因为next()
返回false
而取消,因此需要回退一个路由,因为这个to
路由已经改变浏览器的记录了!
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {
// error是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED
return error;
})
.then((failure) => {
if (failure) {
if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
routerHistory.go(-1, false);
}
}
})
});
}
总结
1. 外部定义的路由,是如何在Vue Router
内部建立联系的?
Vue Router
支持多种路径写法,静态路径、普通的动态路径以及动态路径和正则表达式的结合
初始化时会对routes
进行解析,根据多种路径的不同形态解析出对应的正则表达式、路由权重和路由其它数据,包括组件名称、组件等等
2. Vue Router
是如何实现push
、replace
、pop
操作的?
push
/replace
- 通过
resolve()
整理出跳转数据的对象,该对象包括找到一开始初始化routes
对应的matched
以及跳转的完整路径 - 然后通过
navigate()
进行一系列导航守卫的调用 - 然后通过
changeLocation()
,也就是window.history.pushState/replaceState()
实现的当前浏览器路径替换 - 更新目前的
currentRoute
,从而触发
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发
的setup
函数重新渲染currentRoute的matched
携带的Component
,实现路由更新功能
- 通过
pop
- 初始化会进行
setupListeners()
进行routerHistory.listen
事件的监听 - 监听
popstate
事件,后退事件触发时,触发初始化监听的routerHistory.listen
事件 - 通过
resolve()
整理出跳转数据的对象,该对象包括找到一开始初始化routes
对应的matched
以及跳转的完整路径 - 然后通过
navigate()
进行一系列导航守卫的调用 - 然后通过
changeLocation()
,也就是window.history.pushState/replaceState()
实现的当前浏览器路径替换 - 更新目前的
currentRoute
,从而触发
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发
的setup
函数重新渲染currentRoute的matched
携带的Component
,实现路由更新功能
- 初始化会进行
3. Vue Router
是如何命中多层嵌套路由,比如/parent/child/child1
需要加载多个组件,是如何实现的?
在每次路由跳转的过程中,会解析出当前路由对应的matcher
对象,并且将它所有的parent matcher
都加入到matched
数组中
在实现push
、replace
、pop
操作时,每一个路由都会在router-view
中计算出目前对应的嵌套深度,然后根据嵌套深度,拿到上面matched
对应的item
,实现路由组件的渲染
4. Vue Router
有什么导航守卫?触发的流程是怎样的?
通过Promise
链式顺序调用多个导航守卫,在每次路由跳转时,会触发push/replace()
->navigate()
->finalizeNavigation()
,导航守卫就是在navigate()
中进行链式调用,可以在其中一个导航守卫中中断流程,从而中断整个路由的跳转
参考官方文档的资料,我们可以推断出/child1
->/child2
导航守卫的调用顺序为
- 【组件守卫】在失活的组件里调用
beforeRouteLeave
守卫 - 【全局守卫】
beforeEach
- 【路由守卫】
beforeEnter
- 解析异步路由组件
- 【组件守卫】在被激活的组件里调用
beforeRouteEnter
(无法访问this,实例未创建) - 【全局守卫】
beforeResolve
- 导航被确认
- 【全局守卫】
afterEach
- 【vue生命周期】
beforeCreate
、created
、beforeMount
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入 【vue生命周期】
mounted
参考官方文档的资料,我们可以推断出路由
/user/:id
从/user/a
->/user/b
导航守卫的调用顺序为- 【全局守卫】
beforeEach
- 【组件守卫】在重用的组件里调用
beforeRouteUpdate
守卫(2.2+) - 【全局守卫】
beforeResolve
- 【全局守卫】
afterEach
- 【vue生命周期】
beforeUpdate
、updated
5. Vue Router
的导航守卫是如何做到链式调用的?
在navigate()
的源码中,我们截取beforeEach
和beforeRouteUpdate
的片段进行分析,主要涉及有几个点:
runGuardQueue(guards)
本质就是链式不停地调用promise.then()
,然后执行guards[x]()
,最终完成某一个阶段,比如beforeEach
阶段之后,再使用guards
收集下一个阶段的function
数组,然后再启用runGuardQueue(guards)
使用promise.then()
不断执行guards
里面的方法guards.push()
添加的是一个Promise
,传递的是在外部注册的方法guard
、to
、from
三个参数
function navigate(to, from) {
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);
})
//....
)
}
function runGuardQueue(guards) {
return guards.reduce(
(promise, guard) => promise.then(() => guard()),
Promise.resolve());
}
对于guards.push()
添加的Promise
,会判断外部function
(也就是guard
)有多少个参数,然后调用不同的条件逻辑,最终根据外部注册的方法,比如beforeEach()
返回的值,进行Promise()
的返回,从而形成链式调用
Function: length
就是有多少个parameters
参数,guard.length
主要区别在于传不传next
参数,可以看下面代码块的注释部分
如果你在外部的方法,比如beforeEach
携带了next
参数,你就必须调用它,不然会报错
//router.beforeEach((to, from)=> {return false;}
//router.beforeEach((to, from, next)=> {next(false);}
function guardToPromiseFn(guard, to, from, record, name) {
// keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
const enterCallbackArray = record &&
// name is defined if record is because of the function overload
(record.enterCallbacks[name] = record.enterCallbacks[name] || []);
return () => new Promise((resolve, reject) => {
const next = (valid) => {
resolve();
};
const guardReturn = guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from));
let guardCall = Promise.resolve(guardReturn);
if (guard.length < 3)
guardCall = guardCall.then(next);
if (guard.length > 2) {
//...处理有next()的情况
}
guardCall.catch(err => reject(err));
});
}
function canOnlyBeCalledOnce(next, to, from) {
let called = 0;
return function () {
next._called = true;
if (called === 1)
next.apply(null, arguments);
};
}
6. Vue Router
的beforeRouteEnter
和beforeRouteUpdate
的触发时机
如果复用一个路由,比如/user/:id
会导致不同的path
会使用同一个路由,那么就不会调用beforeRouteEnter
,因此我们需要在beforeRouteUpdate
获取数据
export default {
data() {
return {
post: null,
error: null,
}
},
beforeRouteEnter(to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
async beforeRouteUpdate(to, from) {
this.post = null
try {
this.post = await getPost(to.params.id)
} catch (error) {
this.error = error.toString()
}
},
7. Vue Router
中route
和router
的区别
router
是目前Vue
使用Vue Router
示例,具有多个方法和多个对象数据,包含currentRoute
route
是当前路由的响应式对象,内容本质就是currentRoute
8. hash
模式跟h5 history
模式在Vue Router
中有什么区别?
hash
模式:监听浏览器地址hash
值变化,使用pushState/replaceState
进行路由地址的改变,从而触发组件渲染改变,不需要服务器配合配置对应的地址history
模式:改变浏览器url
地址,从而触发浏览器向服务器发送请求,需要服务器配合配置对应的地址
9. Vue Router
的hash
模式重定向后还会保留浏览记录吗?比如重定向后再使用router.go(-1)
会返回重定向之前的页面吗?
在Vue Router
的hash
模式中,如果发生重定向,从push()
->pushWithRedirect()
的源码可以知道,会在navigate()
之前就进行重定向的跳转,因此不会触发finalizeNavigation()
的pushState()
方法往浏览器中留下记录,因此不会在Vue Router
的hash
模式中,不会保留浏览器history state
记录
function pushWithRedirect(to, redirectedFrom) {
const targetLocation = (pendingLocation = resolve(to));
const from = currentRoute.value;
const data = to.state;
const force = to.force;
// to could be a string where `replace` is a function
const replace = to.replace === true;
const shouldRedirect = handleRedirectRecord(targetLocation);
if (shouldRedirect) {
//...处理重定向的逻辑
return pushWithRedirect(...)
}
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation;
toLocation.redirectedFrom = redirectedFrom;
//...处理SameRouteLocation的情况
// ...去除failure的处理,默认都成功
return navigate(toLocation, from)
.then((failure) => {
failure = finalizeNavigation(toLocation, from, true, replace, data);
triggerAfterEach(toLocation, from, failure);
return failure;
});
}
10. Vue Router
的hash
模式什么地方最容易导致路由切换失败?
在之前的分析中,我们可以知道,我们可以使用go(-1, false)
进行pauseListeners()
的调用
function go(delta, triggerListeners = true) {
if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
function listen(callback) {
// 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 pauseListeners() {
pauseState = currentLocation.value;
}
从上面的代码中,我们可以总结出几个关键的问题:
pauseListeners()
是如何暂停监听方法执行的?pauseState
什么时候使用?- 什么时候触发
listen()
注册监听方法? listen()
注册的监听方法有什么用处?- 为什么要使用
pauseListeners()
暂停监听?
pauseListeners()
是如何暂停监听方法执行的?
pauseState
是如何做到暂停监听方法执行的?
当触发后退事件时,会检测pauseState
是否存在以及是否等于后退之前的路由,如果是的话,则直接return
,阻止后续的listeners
的循环调用,达到暂停listeners
的目的
const popStateHandler = ({ state, }) => {
const from = currentLocation.value;
//....
let delta = 0;
if (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 => {
//...
});
};
window.addEventListener('popstate', popStateHandler);
什么时候触发listen()
注册监听方法?
在上一篇文章的setupListeners()
注册pop
操作相关监听方法的分析中,我们可以知道,初始化app.use(router)
会触发router.install(app)
,然后进行一次push()
操作,此时就是初始化阶段!ready
,push()
->navigate()
->finalizeNavigation()
,然后触发listen()
注册监听方法
function finalizeNavigation(toLocation, from, isPush, replace, data) {
markAsReady();
}
function markAsReady(err) {
if (!ready) {
ready = !err;
setupListeners();
}
return err;
}
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...navigate()
});
}
listen()
监听方法有什么用处?
在上一篇文章的setupListeners()
注册pop
操作相关监听方法的分析中,我们可以知道,在原生popstate
后退事件触发时,会触发对应的listeners
监听方法执行,将当前的路由作为参数传递过去,因此listen()
监听方法本质的功能就是监听后退事件,执行一系列导航守卫,实现路由切换相关逻辑
pop()
事件的监听本质跟push()
执行的逻辑是一致的,都是切换路由映射的组件
function useHistoryListeners(base, historyState, currentLocation, replace) {
const popStateHandler = ({ state, }) => {
const to = createCurrentLocation(base, location);
if (state) {
currentLocation.value = to;
}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
window.addEventListener('popstate', popStateHandler);
return {
pauseListeners,
listen,
destroy,
};
}
listen()
监听方法触发后,具体做了什么?
当后退事件触发,listen()
监听方法触发,本质就是执行了navigate()
导航,进行了对应的组件切换功能
手动模拟了主动触发路由切换,但是不会进行pushState
/replaceState
改变当前的路由地址,只是改变currentRoute
,触发的组件重新渲染
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
navigate(toLocation, from).then(() => {
// false代表不会触发pushState/replaceState
finalizeNavigation(toLocation, from, false);
});
});
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {
if (isPush) {
if (replace || isFirstNavigation)
routerHistory.replace(toLocation.fullPath, assign({
scroll: isFirstNavigation && state && state.scroll,
}, data));
else
routerHistory.push(toLocation.fullPath, data);
}
currentRoute.value = toLocation;
}
什么时候调用pauseListeners()
?pauseListeners()
的作用是什么?
go()
方法中,如果我们使用router.go(-1, false)
,那么我们就会触发pauseListeners()
->pauseState = currentLocation.value
路由后退会触发popStateHandler()
,此时我们已经注册pauseState = currentLocation.value
,因此在popStateHandler()
中会阻止后续的listeners
的循环调用,达到暂停listeners
的目的
function go(delta, triggerListeners = true) {
if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
我们什么时候调用go(-xxxx, false)
?第二个参数跟popStateHandler()
有什么联系?
在Vue Router 4.1.6
的源码中,我们可以发现,所有涉及到go(-xxxx, false)
都集中在后退事件对应的监听方法中,如下面代码块所示,在后退事件触发,进行组件的切换过程中,Vue Router
可能会产生多种不同类型的路由切换失败,比如上面分析的issues/916
一样,当我们手动更改路由,新路由重定向的路由跟目前路由重复时,我们就需要主动后退一个路由,但是我们不希望重新渲染组件,只是单纯想要回退路由记录,那么我们就可以调用routerHistory.go(-1, false)
触发pauseListeners()
,从而暂停listeners
执行,从而阻止navigate()
函数的调用,阻止导航守卫的发生和scroll
滚动位置的恢复等一系列逻辑
function setupListeners() {
removeHistoryListener = routerHistory.listen((to, _from, info) => {
navigate(toLocation, from)
.catch((error) => {
if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
pushWithRedirect(error.to, toLocation)
.then(failure => {
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
!info.delta &&
info.type === NavigationType.pop) {
routerHistory.go(-1, false);
}
});
} else {
if (info.delta) {
routerHistory.go(-info.delta, false);
}
}
})
.then((failure) => {
// false代表不会触发pushState/replaceState
failure = failure || finalizeNavigation(toLocation, from, false);
if (failure) {
if (info.delta &&
// a new navigation has been triggered, so we do not want to revert, that will change the current history
// entry while a different route is displayed
!isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
routerHistory.go(-info.delta, false);
}
else if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* 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);
}
}
})
});
}