本文基于vue-router 4.1.6
版本源码进行剖析
前言
在上一篇《Vue3相干源码-Vue Router源码解析(一)》文章中,咱们曾经剖析了createWebHashHistory()
和createRouter()
的相干内容,本文将持续下一个知识点app.use(router)
展现剖析
// 1. 定义路由组件.// 也能够从其余文件导入const Home = { template: '<div>Home</div>' }const About = { template: '<div>About</div>' }// 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
咱们应用代码扭转路由时,也在扭转<router-view></router-view>
的Component
内容,实现组件渲染
<script src="https://unpkg.com/vue@3"></script><script src="https://unpkg.com/vue-router@4"></script><div id="app"> <h1>Hello App!</h1> <p> <!--应用 router-link 组件进行导航 --> <!--通过传递 `to` 来指定链接 --> <!--`<router-link>` 将出现一个带有正确 `href` 属性的 `<a>` 标签--> <router-link to="/">Go to Home</router-link> <router-link to="/about">Go to About</router-link> </p> <!-- 路由进口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view></div>
<router-view></router-view>
就是一个Vue Component
<router-view>
代码量还是挺多的,因而上面将切割为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
// <router-view></router-view>代码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
会动态变化
// <router-view></router-view>代码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++
因而此时<router-view>
组件拿的数据是matched[0]
={path: '/child1', Component: parent}
[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
// Child1组件<div>目前路由是Child1</div><router-view></router-view>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
组件依然有<router-view></router-view>
组件,因而咱们再次初始化一次RouterView
,加载path="/child1/child2"
对应的Child2
组件
在下面的代码咱们能够晓得,viewDepthKey
会变为depth.value + 1
,因而此时<router-view>
中injectedDepth
=1
,在depth
的computed()
计算中,得出initialDepth
=1
,因为matchedRoute.components
是存在的,无奈进行initialDepth++
从这里咱们能够轻易猜测出,如果门路上有一个片段,比方path
='/child1/child2/child3'
中child2
没有对应的components
,那么就会跳过这个child2
,间接渲染child1
和child3
对应的组件
因而此时<router-view>
组件拿的数据是matched[1]
={path: '/child1/child2', Component: son}
// Child2组件<div>目前路由是Child2</div>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
// <router-view></router-view>代码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 <component :is="..."> 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 <router-view>. 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 <router-view>. 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地址
<div id="app-wrapper"> <div class="routerLinkWrapper"> <router-link to="/child/a">跳转到/child/a</router-link> </div> <div class="routerLinkWrapper"> <router-link :to="{ name: 'WithChildren' }">跳转到父路由/child</router-link> </div> <div class="routerLinkWrapper"> <router-link :to="{ name: 'default-child' }">跳转到没有门路的子路由/child</router-link> </div> <!-- route outlet --> <!-- component matched by the route will render here --> <router-view></router-view></div>
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
从下面的测试后果中,咱们能够大略猜测出
如果<route-link to=xxx>
中的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);});
因为<router-link>
初始化时就会注册,因而每一个<router-link>
都会初始化下面的代码,进行computed
的监听,而此时route
代表传进来的路由,即
<router-link :to="{ name: 'default-child' }">跳转到没有门路的子路由/child</router-link>
初始化时的props.to
="{ name: 'default-child' }"
当目前的路由发生变化时,即currentRoute
发生变化时,也会触发computed(fn)
从新执行一次,此时会去匹配props.to
="/child"
所对应的matched[最初一位index]
是否在currentRoute
对应的matched
找到,如果找到了,间接返回index
如果找不到,则应用<router-link>
目前对应的props.to
的matched
的倒数第二个matched[最初第二位index]
,看看这个倒数第二个matched[最初第二位index]
能不能在currentRoute
对应的matched
找到,如果找到了,间接返回index
应用props.to
的matched数组
的倒数第二个matched[最初第二位index]
的前提是<router-link to="/child">
对应的路由是它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));
在嵌套路由中找到符合条件的<router-link>
是isActive=true
,然而isExactActive=false
只有合乎currentRoute.matched.length - 1
条件下匹配的<router-link>
才是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 `<base>` 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/#/
,跟谬误链接代码的区别在于<html>
文件应用了<base href="/">
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> <base href="/"> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body></html>
HTML <base> 元素 指定用于一个文档中蕴含的所有绝对 URL 的根 URL。一份中只能有一个 <base> 元素。
一个文档的根本 URL,能够通过应用 document.baseURI(en-US) 的 JS 脚本查问。如果文档不蕴含 <base> 元素,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/
<html></html>
对应的<base href="/">
- 触发了
history['pushState'](state, '', '#/')
下面3种条件使得目前的链接更改为:http://localhost:8080/#/
issues/685修复剖析
在github issues的一开始的探讨中,最先倡议的是更改<html></html>
对应的<base href="/router-bug/">
,因为history.pushState
会用到<base>
属性
前面提交了一个修复记录,如下图所示:
那为什么要减少<base>
标签的判断呢?
咱们在一开始初始化的时候就晓得,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);}
<base>
HTML 元素指定用于文档中所有绝对 URL 的根本 URL。如果文档没有 <base>
元素,则 baseURI
默认为 location.href
很显著,目前<html></html>
对应的<base href="/">
跟目前的base
="/router-bug/#"
是抵触的,因为url
=base.slice(hashIndex)+to
是建设在history.pushState
对应的地址蕴含location.pathname
的前提下,而可能存在<html></html>
对应的<base>
就是不蕴含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;}
因而当咱们检测到<html></html>
存在<base>
标签时,咱们间接应用base
(通过格式化解决过的,蕴含location.pathname
)的字符串进行以后history.pushState()
地址的拼接,杜绝这种可能抵触的状况
上面代码中的base
不是<html></html>
的<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
嵌套的状况,比方上面这种状况
<div id="app"> 目前的地址是:{{ $route.fullPath}} <ul> <li><router-link to="/">Home</router-link></li> <li><router-link :to="{ query: { users: { page: 1 } } }">Page 1</router-link></li> <li><router-link :to="{ query: { users: { page: 2 } } }">Page 2</router-link></li> </ul> <router-view></router-view></div>
点击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 addingrouter.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
,从而触发<route-view>
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发<route-view>
的setup
函数从新渲染currentRoute的matched
携带的Component
,实现路由更新性能
- 通过
pop
- 初始化会进行
setupListeners()
进行routerHistory.listen
事件的监听 - 监听
popstate
事件,后退事件触发时,触发初始化监听的routerHistory.listen
事件 - 通过
resolve()
整顿出跳转数据的对象,该对象包含找到一开始初始化routes
对应的matched
以及跳转的残缺门路 - 而后通过
navigate()
进行一系列导航守卫的调用 - 而后通过
changeLocation()
,也就是window.history.pushState/replaceState()
实现的以后浏览器门路替换 - 更新目前的
currentRoute
,从而触发<route-view>
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发<route-view>
的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
,触发<router-view>
的组件从新渲染
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); } } }) });}
参考文章
- 7张图,从零实现一个简易版Vue-Router,太通俗易懂了!
- VueRouter4路由权重
Vue系列其它文章
- Vue2源码-响应式原理浅析
- Vue2源码-整体流程浅析
- Vue2源码-双端比拟diff算法 patchVNode流程浅析
- Vue3源码-响应式零碎-依赖收集和派发更新流程浅析
- Vue3源码-响应式零碎-Object、Array数据响应式总结
- Vue3源码-响应式零碎-Set、Map数据响应式总结
- Vue3源码-响应式零碎-ref、shallow、readonly相干浅析
- Vue3源码-整体流程浅析
- Vue3源码-diff算法-patchKeyChildren流程浅析