9.路由
对于大多数单页面应用,都推荐使用官方支持的 vue-router 库。
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌,Vue Router主要包含功能:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
9.1 路由示例
基于 Vue.js 已经可以通过组合组件来组成一个应用程序,而当把 Vue Router 添加进来,从结构上要做的便是:
- 将组件 (components) 映射到路由 (routes)
- 告诉 Vue Router 在哪里渲染组件
简单的路由示例:
html
<script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> <div id="app"> <h1>Hello App!</h1> <p> <!-- 使用 router-link 组件来导航:<router-link> 默认会被渲染成一个 `<a>` 标签 --> <!-- 通过传入 `to` 属性指定链接. --> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> <!-- 路由出口:路由匹配到的组件将渲染在这里 --> <router-view></router-view> </div>
js
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter) // 1. 定义组件 const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } // 2. 定义路由 // 每个路由应该映射一个组件。其中"component" 可以是通过 Vue.extend() 创建的组件构造器, // 或者,只是一个组件配置对象(关于嵌套路由的内容会在之后讨论)。 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] // 3. 创建 VueRouter 的实例,其中传入 `routes` 配置 const router = new VueRouter({ routes // (缩写) 相当于 routes: routes }) // 4. 创建和挂载根实例 // 需要通过 router 参数注入路由,从而让整个应用都有路由功能 const app = new Vue({ router }).$mount('#app') // 现在,应用已经启动了!
Home.vue
// Home.vue export default { computed: { username() { return this.$route.params.username } }, methods: { goBack() { window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/') } } }
通过注入路由器,我们可以在任何组件内通过 this.$router
访问路由器,也可以通过 this.$route
访问当前路由。
当 <router-link>
对应的路由匹配成功,将自动设置其 class 属性值 .router-link-active
注意,
this.$router
和router
使用起来完全一样。Vue文档中使用
this.$router
的原因是避免在每个独立需要封装路由的组件中都导入路由。??????
$route对象
除了 $route.params
外,$route
对象还提供了其它有用的信息,例如,$route.query
(如果 URL 中有查询参数)、$route.hash
等等。你可以查看 API 文档 的详细说明。
注意:在 Vue 实例内部,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
。
9.2 动态路由
有些路由可能对应于同一种模式,需要全部映射到同个组件。例如,假设有一个 User
组件,对于所有 ID 各不相同的用户都要使用这个组件来渲染,那就可以在 vue-router
的路由路径中使用“动态路径参数”来达到这个效果:
const User = {
template: '<div>User</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数以冒号开头
{ path: '/user/:id', component: User }
]
})
// 此时像 `/user/foo` 和 `/user/bar` 都将映射到相同的路由。
路径参数:
一个“路径参数”使用冒号
:
标记,当匹配到一个路由时,参数值会被设置到this.$route.params
,它可以在每个组件内使用。可以在一个路由中设置多段“路径参数”,对应的值都会设置到
$route.params
中,如:模式 匹配路径 $route.params /user/:username /user/evan { username: 'evan' }
/user/:username/post/:post_id /user/evan/post/123 { username: 'evan', post_id: '123' }
在动态路径中当路由参数切换时,原来的组件实例会被复用,例如从
/user/foo
导航到/user/bar
。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效,但与此同时这也意味着组件的生命周期钩子不会被再次调用。复用组件时,想对路由参数的变化作出响应的话,可以 watch
$route
对象:const User = { template: '...', watch: { $route(to, from) { // 对路由变化作出响应... } } }
或者使用
beforeRouteUpdate
导航守卫:const User = { template: '...', beforeRouteUpdate(to, from, next) { // react to route changes... // don't forget to call next() } }
路径参数也可以使用通配符
*
,此时,$route.params
内会添加一个名为pathMatch
参数,其包含了 URL 通过通配符被匹配的部分:// 给出一个路由 { path: '/user-*' } this.$router.push('/user-admin') this.$route.params.pathMatch // 'admin' // 给出一个路由 { path: '*' } this.$router.push('/non-existing') this.$route.params.pathMatch // '/non-existing'
vue-router
使用 path-to-regexp 作为路径匹配引擎,因还支持很多高级的匹配模式同一个路径可以匹配多个路由,此时匹配的优先级按照路由的定义顺序:定义越早,优先级越高。
9.3 嵌套路由
实际场景中的应用界面,通常由多层嵌套的组件组合而成,与此同时相应的URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:
/user/foo/profile /user/foo/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
借助 vue-router
的嵌套路由配置,可以很简单地表达这种关系。
仍以之前创建的app为例:
<div id="app">
<router-view></router-view>
</div>
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
这里的 <router-view>
是最顶层的出口,渲染最高层路由匹配到的组件,而在这个被渲染的组件内部,同样可以包含嵌套的 <router-view>
,即:
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
此时若要在这个嵌套的<router-view>
出口中渲染组件,需要在VueRouter
的参数中使用children
配置:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 组件会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 组件会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
})
嵌套路径写法的注意事项:如果嵌套路径以/
开头,它会被认为是从根路径开始。这也意味着,你可以充分地使用嵌套的组件,但并不需要在组件的路径上也设定嵌套的关系。
值得一提的是,只基于上面的配置的话,当访问
/user/zhangsan
时,User
组件内的出口是不会渲染任何东西的,这是因为没有匹配到合适的子路由。这种情况下,可以选择提供一个空的子路由:const router = new VueRouter({ routes: [ { path: '/user/:id', component: User, children: [ // 当 /user/:id 匹配成功, // UserHome 会被渲染在 User 的 <router-view> 中 { path: '', component: UserHome } // ...其他子路由 ] } ] })
9.4 路由的编程式写法
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现路径。
9.4.1 router.push(location, onComplete?, onAbort?)
当点击 <router-link>
时, Vue会在内部调用 router.push
方法。实际上,点击 <router-link :to="...">
等同于调用 router.push(...)
:
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
router.push
方法的参数可以是一个字符串路径,亦或一个描述地址的对象:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
当给 router.push
方法既提供了path
参数,又提供了params
参数,则params
会被忽略。此时若仍想使用给你的路径提供一个参数,可以在path
上携带,或使用具名路由:
const userId = '123'
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
// 1、可以选择在路径上携带参数
router.push({ path: '/user/${userId}' }) // -> /user/123
// 2、使用具名路由
router.push({ name: 'user', params: { userId }}) // -> /user/123
实际上这样的规则也适用于
router-link
组件的to
属性。
在 router.push
中的第二、三个参数为 onComplete
和 onAbort
回调,它们会在导航成功完成或终止时执行相应的调用。这里所谓的导航成功、终止的含义为:
- 成功: 在所有的异步钩子被解析之后
- 终止: 导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由
在 3.1.0+,可以省略第二个和第三个参数,此时如果支持 Promise,
router.push
将返回一个 Promise。
值得提醒的是,如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1
-> /users/2
),你需要使用 beforeRouteUpdate
来响应这个变化 (比如抓取用户信息)。
9.4.2 router.replace(location, onComplete?, onAbort?)
router.replace
和router.push
几乎一样,唯一的不同就是它不会向 history 添加新记录,而是替换掉当前的 history 记录。
声明式 | 编程式 |
---|---|
<router-link :to="..." replace> | router.replace(...) |
9.4.3 router.go(n)
router.go(n)
意为在 history 记录中向前或者后退多少步,类似 window.history.go(n)
。
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
9.4.4 操作History
至此,你应该注意到 router.push
、 router.replace
和 router.go
跟 window.history.pushState
、 window.history.replaceState
和 window.history.go
很像,实际上它们确实是效仿 window.history
API 的。
因此,如果你已经熟悉了 Browser History APIs,那在 Vue Router 中操作 history 就是相当简单的。
此外,Vue Router 的导航方法 (push
、 replace
、 go
) 在各类路由模式 (history
、 hash
和 abstract
) 下表现一致。
9.5 具名路由
似乎只有阅读语义上更加方便的作用。
有时候,通过一个名称来标识一个路由(在语义上)显得更方便一些,因此Vue可以在创建 Router 实例时在 routes
配置中给某个路由设置名称:
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
这跟代码调用 router.push()
是一回事——它们都会把路由导航到 /user/123
路径:
router.push({ name: 'user', params: { userId: 123 } })
9.6 具名视图
如果 router-view
没有设置名字,其默认名为 default
,实际上可以通过其name
属性来给该视图设置一个名字:
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
具名视图在想要同时 (同级) 展示多个视图,而不是将它们嵌套时特别有用,例如在一个布局中有 sidebar
和 main
两个视图,具名视图使得你可以在界面中拥有多个独立命名的视图,而不是只有一个单独的出口。
由于一个视图需要使用一个组件来渲染,因此对于同一个路由,多个视图就需要多个组件,因而在配置时要确保正确使用 components
:
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
9.7 重定向&别名
9.7.1 重定向
通过 routes
也可以配置重定向,重定向的目的地可以是一个新的路由、具名路由、动态返回重定向目标的方法。
下面例子从
/a
重定向到/b
:const router = new VueRouter({ routes: [ { path: '/a', redirect: '/b' } ] })
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({ routes: [ { path: '/a', redirect: { name: 'foo' }} ] })
甚至是一个动态返回重定向目标的方法:
const router = new VueRouter({ routes: [ { path: '/a', redirect: to => { // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象 }} ] })
9.7.2 别名
重定向的意思是,当用户访问 /a
时,URL 将会被替换成 /b
,然后匹配路由为 /b
。而对于别名,/a
的别名是 /b
意味着,当用户访问 /b
时,URL 会保持为 /b
,但是路由匹配为 /a
,就像用户直接访问 /a
一样。
别名的路由配置为:
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
别名让你可以自由地将 UI 结构映射到任意的 URL,而不受限于配置的嵌套路由结构。
9.8 路由组件传参
在组件中使用 $route
会使之与其对应的路由形成高度耦合,从而导致组件只能在某些特定的 URL 上使用,限制了其灵活性。因此,通常使用 props
将组件和路由解耦:
对于一个使用 $route
的情况:
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
可通过props
解耦如下:
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})
props
可为以下几种类型/模式:
布尔模式:如果
props
被设置为true
,route.params
将会被设置为组件属性。对象模式:如果
props
是一个对象,它会被按原样设置为组件属性,这在props
是静态的时候特别有用。const router = new VueRouter({ routes: [ { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } } ] })
函数模式:可以创建一个函数返回
props
,从而将参数转换成另一种类型、将静态值与基于路由的值结合等等。const router = new VueRouter({ routes: [ { path: '/search', component: SearchUser, props: route => ({ query: route.query.q }) } ] })
这里 url
/search?q=vue
会将{query: 'vue'}
作为属性传递给SearchUser
组件。使用函数时应尽可能保持
props
函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义props
,应使用包装组件,这样 Vue 才可以对状态变化做出反应。
9.9 H5 History模式
vue-router
默认使用 hash 模式,如果想要用 history 模式,可以在创建VueRouter
实例时通过mode
参数进行设定:
const router = new VueRouter({
mode: 'history',
routes: [...]
})
vue-router
的 history 模式充分利用了 history.pushState
API 来完成 URL 跳转。
不过要玩好 history 模式的话,还需要后台配置支持。
9.10 导航守卫
所谓导航,表示路由正在发生改变。
vue-router
提供的导航守卫主要用来通过跳转或取消的方式来守卫导航。
每个守卫方法接收三个参数:
to: Route
: 即将要进入的目标路由对象;from: Route
: 当前导航正要离开的路由;next: Function
: 必须调用next
方法来解析这个(Function
?)钩子,执行效果依赖next
方法的调用参数:next()
: 进行管道中的下一个钩子。如果全部钩子都执行完了,导航的状变为 confirmed。next(false)
: 中断当前导航。如果浏览器的 URL 改变了,那么 URL 地址会重置到from
路由对应的地址。next('/')
或next({ path: '/' })
: 跳转到一个不同的地址——当前的导航被中断,然后进行一个新的导航。你可以向next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在router-link
的to
prop或router.push
中的选项。next(error)
: 如果参数是一个Error
实例,导航会被终止且该错误会被传递给router.onError()
注册过的回调。
在使用过程中应该确保 next
函数在任何给定的导航守卫中都被严格调用一次,否则钩子永远都不会被解析或报错。但在所有的逻辑路径都不重叠的情况下,next
也可以出现多次。
参数或查询的改变并不会触发进入/离开的导航守卫,这些变化可以通过观察 $route
对象来应对,也可以使用 beforeRouteUpdate
的组件内守卫。
导航守卫植入路由导航过程中的方式可以是:全局的、单个路由独享的、组件级的。
9.10.1 全局导航守卫
全局前置守卫
可以使用 router.beforeEach
注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, 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()
})
全局解析守卫
router.beforeResolve
和 router.beforeEach
类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫router.beforeResolve
就被调用。
全局后置钩子
也可以注册全局后置钩子,不过和守卫不同的是,这些钩子不会接受 next
函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
9.10.2 路由独享的守卫
可以在路由配置上直接定义 beforeEnter
守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这个守卫与全局前置守卫的方法参数是一样的。
9.10.3 组件内的守卫
可以在路由组件内直接定义以下导航守卫:
beforeRouteEnter
beforeRouteUpdate
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
已经可用了,所以Vue设定它们不支持传递回调。
beforeRouteLeave
守卫通常用来禁止用户在还未保存修改前突然离开,该导航可以通过 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)
}
}
9.10.4 导航解析流程
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数——创建好的组件实例会作为回调函数的参数传入。
9.11 路由元信息
通常称 routes
配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此当一个路由匹配成功后,它可能匹配多个路由记录,一个路由匹配到的所有路由记录会暴露为 $route
对象的 $route.matched
数组。
例如,根据下面的路由配置,
/foo/bar
这个 URL 将会匹配父路由记录以及子路由记录。
定义路由的时候可以配置 meta
字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
通过遍历 $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()
}
})
9.12 路由过渡动效
<router-view>
是基本的动态组件,可以用 <transition>
组件给它添加一些过渡效果:
<transition>
<router-view></router-view>
</transition>
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 -->
<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'
}
}
9.13 数据获取
有时候,进入某个路由后需要从服务器获取数据,这可以通过两种方式来实现:
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据,在数据获取期间显示“加载中”之类的提示。
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
从技术角度讲,两种方式都没什么问题,主要取决于希望的用户体验是哪种。
9.13.1 导航完成之后获取
这种方式会马上导航和渲染组件,然后在组件的 created
钩子中获取数据,因而通常在数据获取期间展示一个 loading 状态,同时还可以在不同视图间展示不同的 loading 状态。
例如,假设有一个 Post
组件需要基于 $route.params.id
获取文章数据:
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="post" class="content">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
export default {
data () {
return {
loading: false,
post: null,
error: null
}
},
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.post = null
this.loading = true
// replace getPost with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
9.13.2 导航完成之前获取
这种方式在导航转入新的路由前获取数据,通常在接下来的组件的 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
}
}
}
}
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
9.14 滚动行为
在前端路由中当切换到新路由时,可以选择让页面滚到顶部/保持原先的滚动位置, vue-router
能更好地让你可以自定义路由切换时页面的滚动设置。
不过这个功能只在支持
history.pushState
的浏览器中可用。
具体而言,Vue Router 通过通过VueRouter
实例的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 }}`
- 如果返回一个 falsy 的值或一个空对象,那么不会发生滚动。
示例:
对于所有路由导航,简单地让页面滚动到顶部:
scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } }
模拟“滚动到锚点”的行为:
scrollBehavior (to, from, savedPosition) { if (to.hash) { return { selector: to.hash } } }
异步滚动
可以返回一个 Promise 来得出预期的位置描述:
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
可以将上面的scrollBehavior
挂载到从页面级别的过渡组件的事件上,令其滚动行为和页面过渡一起执行。不过考虑到用例的多样性和复杂性,Vue Router仅提供这个原始的接口,以支持不同用户场景的具体实现。
平滑滚动
只需将 behavior
选项添加到 scrollBehavior
内部返回的对象中,就可以为支持它的浏览器启用原生平滑滚动:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth',
}
}
}
9.15 路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
这可以通过结合 Vue 的异步组件和 Webpack 的代码分割功能来轻松实现路由组件的懒加载:
将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
综合来看,以下便定义了一个能够被 Webpack 自动代码分割的异步组件:
const Foo = () => import('./Foo.vue')
与此同时在路由配置中是任何都不需要改变的,只要像往常一样使用 Foo
即可:
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo }]
})
9.16 导航故障
导航故障是一个 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
可以帮助开发者来区分不同类型的导航故障,它有4种不同的类型:
redirected
:在导航守卫中调用了next(newLocation)
重定向到了其他地方。aborted
:在导航守卫中调用了next(false)
中断了本次导航。cancelled
:在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了router.push
。duplicated
:导航被阻止,因为我们已经在目标位置了。
所有的导航故障都会有 to
和 from
属性,分别表示这次失败的导航的目标位置和当前位置:
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
failure.to.path // '/admin'
failure.from.path // '/'
}
})