vue-router 进阶
导航守卫
vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫
全局前置守卫
你可以使用router.beforeEach注册一个全局前置守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中
每个守卫方法接收三个参数
-
to: Route: 即将要进入的目标 路由对象 -
from: Route: 当前导航正要离开的路由 -
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖next方法的调用参数 -
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的) -
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from路由对应的地址。 -
next('/')或者next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace: true、name: 'home'之类的选项以及任何用在router-link的toprop 或router.push中的选项 -
next(error): (2.4.0+) 如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调
确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})全局解析守卫
2.5.0 新增
在 2.5.0+ 你可以用router.beforeResolve注册一个全局守卫。这和router.beforeEach类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身
router.afterEach((to, from) => {
// ...
})路由独享的守卫
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})这些守卫与全局前置守卫的方法参数是一样的
组件内的守卫
最后,你可以在路由组件内直接定义以下路由导航守卫
- beforeRouteEnter
- beforeRouteUpdate (2.2 新增)
- beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建
不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫
对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}完整的导航解析流程
- 导航被触发
- 在失活的组件里调用
beforeRouteLeave守卫 - 调用全局的
beforeEach守卫 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+) - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve守卫 (2.5+) - 导航被确认
- 调用全局的
afterEach钩子 - 触发 DOM 更新
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入
<!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">
<title>Document</title>
<script src="./js/vue-2.7.13.js"></script>
<script src="./js/vue-router-3.6.5.js"></script>
</head>
<body>
<div id="app">
<h1>Navigation Guards</h1>
<ul>
<li>
<router-link to="/">/</router-link>
</li>
<li>
<router-link to="/foo">/foo</router-link>
</li>
<li>
<router-link to="/bar">/bar</router-link>
</li>
<li>
<router-link to="/baz">/baz</router-link>
</li>
<li>
<router-link to="/qux">/qux</router-link>
</li>
<li>
<router-link to="/qux-async">/qux-async</router-link>
</li>
<li>
<router-link to="/quux/1">/quux/1</router-link>
</li>
<li>
<router-link to="/quux/2">/quux/2</router-link>
</li>
<li>
<router-link to="/parent/child/2">/parent/child/2</router-link>
</li>
</ul>
<router-view class="view"></router-view>
</div>
<script>
const Home = {template: '<div>home</div>'}
const Foo = {template: '<div>foo</div>'}
const Bar = {template: '<div>bar</div>'}
function guardRoute(to, from, next) {
if (window.confirm(`Navigate to ${to.path}?`)) {
next()
} else if (window.confirm(`Redirect to /baz?`)) {
next('/baz')
} else {
next(false)
}
}
// Baz implements an in-component beforeRouteLeave hook
const Baz = {
data() {
return {saved: false}
},
template: `
<div>
<p>baz ({{ saved ? 'saved' : 'not saved' }})</p>
<button @click="saved = true">save</button>
</div>
`,
beforeRouteLeave(to, from, next) {
if (this.saved || window.confirm('Not saved, are you sure you want to navigate away?')) {
next()
} else {
next(false)
}
}
}
// Qux implements an in-component beforeRouteEnter hook
const Qux = {
data() {
return {
msg: null
}
},
template: `<div>{{ msg }}</div>`,
beforeRouteEnter(to, from, next) {
setTimeout(() => {
next(vm => {
vm.msg = 'Qux'
})
}, 1000)
}
}
const Quux = {
data() {
return {
prevId: 0
}
},
template: `<div>id:{{ $route.params.id }} prevId:{{ prevId }}</div>`,
beforeRouteUpdate(to, from, next) {
this.prevId = from.params.id
next()
}
}
const NestedParent = {
template: `
<div id="nested-parent">
Nested Parent
<hr>
<router-link to="/parent/child/1">/parent/child/1</router-link>
<router-link to="/parent/child/2">/parent/child/2</router-link>
<hr>
<p id="bre-order">
<span v-for="log in logs">{{ log }} </span>
</p>
<transition name="fade" mode="out-in">
<router-view :key="$route.path"/>
</transition>
</div>`,
data: () => ({logs: []}),
beforeRouteEnter(to, from, next) {
next(vm => {
vm.logs.push('parent')
})
}
}
const GuardMixin = {
beforeRouteEnter(to, from, next) {
next(vm => {
vm.$parent.logs.push('mixin')
})
}
}
const NestedChild = {
props: ['n'],
template: `<div>Child {{ n }}</div>`,
mixins: [GuardMixin],
beforeRouteEnter(to, from, next) {
next(vm => {
vm.$parent.logs.push('child ' + vm.n)
})
}
}
const router = new VueRouter({
mode: 'hash',
base: "guard",
routes: [
{path: '/', component: Home},
// inline guard
{path: '/foo', component: Foo, beforeEnter: guardRoute},
// using meta properties on the route config and check them in a global before hook
{path: '/bar', component: Bar, meta: {needGuard: true}},
// Baz implements an in-component beforeRouteLeave hook
{path: '/baz', component: Baz},
// Qux implements an in-component beforeRouteEnter hook
{path: '/qux', component: Qux},
// in-component beforeRouteEnter hook for async components
{
path: '/qux-async',
component: resolve => {
setTimeout(() => {
resolve(Qux)
}, 0)
}
},
// in-component beforeRouteUpdate hook
{path: '/quux/:id', component: Quux},
{
path: '/parent',
component: NestedParent,
children: [
{path: 'child/1', component: NestedChild, props: {n: 1}},
{path: 'child/2', component: NestedChild, props: {n: 2}}
]
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.some(m => m.meta.needGuard)) {
guardRoute(to, from, next)
} else {
next()
}
})
const app = new Vue({
router,
}).$mount('#app')
</script>
</body>
</html>路由元信息
定义路由的时候可以配置meta字段
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
meta: { requiresAuth: true }
}
]
}
]
})首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录
例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。
一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})过度动效
<router-view>是基本的动态组件,所以我们可以用<transition>组件给它添加一些过渡效果,Transition 的所有功能 在这里同样适用
<transition>
<router-view></router-view>
</transition>单个路由的过渡
上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 <transition> 并设置不同的 name
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}基于路由的动态过渡
还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:
<transition :name="transitionName">
<router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}<!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">
<title>Document</title>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity .75s ease;
}
.fade-enter, .fade-leave-active {
opacity: 0;
}
.child-view {
position: absolute;
transition: all .75s cubic-bezier(.55, 0, .1, 1);
}
.slide-left-enter, .slide-right-leave-active {
opacity: 0;
-webkit-transform: translate(30px, 0);
transform: translate(30px, 0);
}
.slide-left-leave-active, .slide-right-enter {
opacity: 0;
-webkit-transform: translate(-30px, 0);
transform: translate(-30px, 0);
}
</style>
<script src="./js/vue-2.7.13.js"></script>
<script src="./js/vue-router-3.6.5.js"></script>
</head>
<body>
<div id="app">
<h1>Transitions</h1>
<ul>
<li>
<router-link to="/">/</router-link>
</li>
<li>
<router-link to="/parent">/parent</router-link>
</li>
<li>
<router-link to="/parent/foo">/parent/foo</router-link>
</li>
<li>
<router-link to="/parent/bar">/parent/bar</router-link>
</li>
</ul>
<transition name="fade" mode="out-in">
<router-view class="view"></router-view>
</transition>
</div>
<script>
const Home = {
template: `
<div class="home">
<h2>Home</h2>
<p>hello</p>
</div>
`
}
const Parent = {
data() {
return {
transitionName: 'slide-left'
}
},
beforeRouteUpdate(to, from, next) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
next()
},
template: `
<div class="parent">
<h2>Parent</h2>
<transition :name="transitionName">
<router-view class="child-view"></router-view>
</transition>
</div>
`
}
const Default = {template: '<div class="default">default</div>'}
const Foo = {template: '<div class="foo">foo</div>'}
const Bar = {template: '<div class="bar">bar</div>'}
const router = new VueRouter({
mode: 'hash',
base: "transtion",
routes: [
{path: '/', component: Home},
{
path: '/parent', component: Parent,
children: [
{path: '', component: Default},
{path: 'foo', component: Foo},
{path: 'bar', component: Bar}
]
}
]
})
const app = new Vue({
router,
}).$mount('#app')
</script>
</body>
</html>数据获取
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航
从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种
导航完成后获取数据
当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态
假设我们有一个 Post 组件,需要基于 id 获取文章数据:
<!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">
<title>Document</title>
<script src="./js/vue-2.7.13.js"></script>
<script src="./js/vue-router-3.6.5.js"></script>
<style>
.loading {
position: absolute;
top: 10px;
right: 10px;
}
.error {
color: red;
}
.content {
transition: all .35s ease;
position: absolute;
}
.slide-enter {
opacity: 0;
transform: translate(30px, 0);
}
.slide-leave-active {
opacity: 0;
transform: translate(-30px, 0);
}
</style>
</head>
<body>
<div id="app">
<div class="loading" v-if="loading">Loading...</div>
<div v-if="error" class="error">
{{ error }}
</div>
<transition name="slide">
<div v-if="post" class="content" :key="post.id">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</transition>
</div>
<script>
const app = new Vue({
data() {
return {
loading: false,
post: null,
error: null
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
getPost(id, cb) {
const posts = {
'1': {
id: 1,
title: 'sunt aut facere',
body: 'quia et suscipit suscipit recusandae consequuntur expedita et cum'
},
'2': {
id: 2,
title: 'qui est esse',
body: 'est rerum tempore vitae sequi sint nihil reprehenderit dolor beatae ea dolores'
}
}
setTimeout(() => {
if (posts[id]) {
cb(null, posts[id])
} else {
cb(new Error('Post not found.'))
}
}, 100)
},
fetchData() {
this.error = this.post = null
this.loading = true
this.getPost("2", (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}).$mount('#app')
</script>
</body>
</html>在导航完成前获取数据
通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}
}在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒
滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router能做到,而且更好,它让你可以自定义路由切换时页面如何滚动
注意: 这个功能只在支持 history.pushState 的浏览器中可用
当创建一个 Router 实例,你可以提供一个scrollBehavior方法:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
这个方法返回滚动位置的对象信息,长这样:
{ x: number, y: number }{ selector: string, offset? : { x: number, y: number }}(offset 只在 2.6.0+ 支持)
如果返回一个 falsy (译者注:falsy 不是 false,参考这里)的值,或者是一个空对象,那么不会发生滚动
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}对于所有路由导航,简单地让页面滚动到顶部
返回savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}如果你要模拟“滚动到锚点”的行为
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}我们还可以利用路由元信息更细颗粒度地控制滚动
异步滚动
2.8.0 新增
你也可以返回一个 Promise 来得出预期的位置描述
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}将其挂载到从页面级别的过渡组件的事件上,令其滚动行为和页面过渡一起良好运行是可能的。但是考虑到用例的多样性和复杂性,我们仅提供这个原始的接口,以支持不同用户场景的具体实现
平滑滚动
只需将 behavior 选项添加到 scrollBehavior 内部返回的对象中,就可以为支持它的浏览器启用原生平滑滚动:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth',
}
}
}<!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">
<title>Document</title>
<script src="./js/vue-2.7.13.js"></script>
<script src="./js/vue-router-3.6.5.js"></script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
.view {
border: 1px solid red;
height: 4500px;
position: relative;
}
</style>
</head>
<body>
<div id="app">
<h1>Scroll Behavior</h1>
<ul>
<li>
<router-link to="/">/</router-link>
</li>
<li>
<router-link to="/foo">/foo</router-link>
</li>
<li>
<router-link to="/bar">/bar</router-link>
</li>
<li>
<router-link to="/bar#anchor">/bar#anchor</router-link>
</li>
<li>
<router-link to="/bar#anchor2">/bar#anchor2</router-link>
</li>
<li>
<router-link to="/bar#1number">/bar#1number</router-link>
</li>
</ul>
<transition name="fade" mode="out-in" @after-leave="afterLeave">
<router-view class="view"></router-view>
</transition>
</div>
<script>
const Home = { template: '<div class="home">home</div>' }
const Foo = { template: '<div class="foo">foo</div>' }
const Bar = {
template: `
<div class="bar">
bar
<div style="height:1500px"></div>
<p id="anchor" style="height:500px">Anchor</p>
<p id="anchor2" style="height:500px">Anchor2</p>
<p id="1number">with number</p>
</div>
`
}
// scrollBehavior:
// - only available in html5 history mode
// - defaults to no scroll behavior
// - return false to prevent scroll
const scrollBehavior = function (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
const position = {}
if (to.hash) {
position.selector = to.hash
if (to.hash === '#anchor2') {
position.offset = { y: 0 }
}
// bypass #1number check
if (/^#\d/.test(to.hash) || document.querySelector(to.hash)) {
return position
}
return false
}
return new Promise(resolve => {
if (to.matched.some(m => m.meta.scrollToTop)) {
position.x = 0
position.y = 0
}
this.app.$root.$once('triggerScroll', () => {
// if the resolved position is falsy or an empty object,
// will retain current scroll position.
resolve(position)
})
})
}
}
const router = new VueRouter({
mode: 'hash',
base: "Scroll",
scrollBehavior,
routes: [
{ path: '/', component: Home, meta: { scrollToTop: true }},
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar, meta: { scrollToTop: true }}
]
})
const app = new Vue({
router,
methods: {
afterLeave() {
this.$root.$emit('triggerScroll')
}
}
}).$mount('#app')
</script>
</body>
</html>路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
})第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise如果您使用的是 Babel,你将需要添加 **syntax-dynamic-import** 插件,才能使 Babel 可以正确地解析语法
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Foo = () => import('./Foo.vue')在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo }]
})把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中
导航故障
3.4.0中新增 导航故障,或者叫导航失败,表示一次失败的导航,原文叫 navigation failures,本文统一采用导航故障
当使用 router-link 组件时,Vue Router 会自动调用 router.push 来触发一次导航。 虽然大多数链接的预期行为是将用户导航到一个新页面,但也有少数情况下用户将留在同一页面上:
当使用 router-link 组件时,这些失败都不会打印出错误。然而,如果你使用 router.push 或者 router.replace 的话,可能会在控制台看到一条 *"Uncaught (in promise) Error"*这样的错误,后面跟着一条更具体的消息。让我们来了解一下如何区分导航故障
在 v3.2.0 中,可以通过使用 router.push 的两个可选的回调函数:onComplete 和 onAbort 来暴露导航故障。从版本 3.1.0 开始,router.push 和 router.replace 在没有提供 onComplete/onAbort 回调的情况下会返回一个 Promise。这个 Promise 的 resolve 和 reject 将分别用来代替 onComplete 和 onAbort 的调用
检测导航故障
导航故障是一个Error实例,附带了一些额外的属性。要检查一个错误是否来自于路由器,可以使用 isNavigationFailure 函数
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
// 向用户显示一个小通知
showToast('Login in order to access the admin panel')
}
})如果你忽略第二个参数:isNavigationFailure(failure),那么就只会检查这个错误是不是一个导航故障。
NavigationFailureType
NavigationFailureType 可以帮助开发者来区分不同类型的导航故障。有四种不同的类型:
redirected:在导航守卫中调用了next(newLocation)重定向到了其他地方aborted:在导航守卫中调用了next(false)中断了本次导航cancelled:在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了router.pushduplicated:导航被阻止,因为我们已经在目标位置了
导航故障的属性
所有的导航故障都会有to和from属性,分别用来表达这次失败的导航的目标位置和当前位置
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
failure.to.path // '/admin'
failure.from.path // '/'
}
})在所有情况下,to 和 from 都是规范化的路由位置
<!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">
<title>Document</title>
<script src="./js/vue-2.7.13.js"></script>
<script src="./js/vue-router-3.6.5.js"></script>
</head>
<body>
<div id="app">
<router-link to="/">/</router-link>
<router-link to="/foo">/foo</router-link>
<router-view></router-view>
</div>
<script>
const component = {
template: `
<div>
{{ $route.fullPath }}
</div>
`
}
const router = new VueRouter({
routes: [
{path: '/', component}, {path: '/foo', component}
]
})
window.router = router
router.beforeEach((to, from, next) => {
console.log('from', from.fullPath)
console.log('going to', to.fullPath)
if (to.query.wait) {
setTimeout(() => next(), 100)
} else if (to.query.redirect) {
next(to.query.redirect)
} else if (to.query.abort) {
next(false)
} else {
next()
}
})
// 4 NAVIGATION ERROR CASES :
// navigation duplicated
// router.push('/foo')
// router.push('/foo')
// navigation cancelled
// router.push('/foo?wait=y')
// router.push('/')
// navigation redirected
// router.push('/foo?redirect=/')
// navigation aborted
// router.push('/foo?abort=y')
const app = new Vue({
router,
}).$mount('#app')
</script>
</body>
</html>