Vue-Router
路由定义得越早,优先级就越高。
动态路由匹配
- 更自由的设计路由的表现
- 我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
- $route.query
- $route.hash
- $route.params
模式 | 匹配路径 | $route.params |
---|---|---|
/user/:username | /user/evan | username: 'evan' |
/user/username/post/post_id | /user/evan/post/123 | username: 'evan', post_id: '123' |
JavaScript
const User = {
template: '<div>User</div>'
//可以读取/user/1001 和 /user/1002 中的 1001 1002
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头 /user/1001 和 /user/1002 都将映射到相同的路由
//一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:
{ path: '/user/:id', component: User }
]
})
响应路由参数的变化
- 路由参数变化时,执行动作。可利用导航守卫
- 从 /user/1001 导航到 /user/1002 ,原来的组件实例会被复用。生命周期钩子不会再被调用
JavaScript
const User = {
template: '...',
//复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象
watch: {
$route(to, from) {
// 对路由变化作出响应... 如网络请求,本地数据读取等
}
},
//复用组件时,导航守卫,便于路由变化时,做出相应的动作
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 'this'
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 'this'
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 'this'
}
}
捕获所有路由或404路由
- 如果想匹配任意路径,我们可以使用通配符 * ,含有通配符的路由应该放在最后。
JavaScript
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'
嵌套路由
- 子界面路由效果,多层深入
- 渲染组件同样可以包含自己的嵌套 router-view
JavaScript
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
const routes = [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 router-view 中
{
path: '',
component: UserHome
},
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 router-view 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 router-view 中
path: 'posts',
component: UserPosts
}
]
}
]
const router = new VueRouter({
routes
})
编程式的导航
- 用代码,跳转路由
router.push(location, onComplete?, onAbort?)
- Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this. $router.push
- 这个方法会向 history 栈添加一个新的记录。所以可以使用后退返回上一个页面。
- 点击 < router-link :to="..."> 等同于调用 router.push(...)
JavaScript
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
/*
如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path
*/
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
/*
如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate[导航守卫] 来响应这个变化 (比如抓取用户信息)
*/
router.replace(location, onComplete?, onAbort?)
- Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this. $router.replace
- 这个方法不会向 history 栈添加一个新的记录,而是直接替换掉当前的history记录。所以可以无法使用后退返回上一个页面。
- 点击 < router-link :to="..." replace> 等同于调用 router.replace(...)
JavaScript
用法和 router.push 同
router.go(n)
- 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
JavaScript
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
命名路由
- 设置别名,便于代码编写
- 在 routes 配置中给某个路由设置名称
JavaScript
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
//通过名称去对应的path 等于前往 /user/123
router.push({ name: 'user', params: { userId: 123 } })
命名视图
- 同级别展示多个带router的界面。
- 展示多个同级视图。例如有 sidebar (侧导航) 和 main (主内容) ,你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。
- 如果 router-view 没有设置名字,那么默认为 default。
html
<router-view class="top"></router-view>
<router-view class="left" name="a"></router-view>
<router-view class="right" name="b"></router-view>
JavaScript
//同个路由,多个视图就需要多个组件。注意是components不是component
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
嵌套命名视图
- Nav 只是一个常规组件。
- UserSettings 是一个视图组件。
- UserEmailsSubscriptions、UserProfile、UserProfilePreview 是嵌套的视图组件。
html
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
JavaScript
//同个路由,多个视图就需要多个组件。注意是components不是component
const router = new VueRouter({
routes: [
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
]
})
重定向和别名
- 验证登录,跳转到home,login等
重定向
JavaScript
//访问A时自动重定向到B
const router = new VueRouter({
routes: [
{ path: '/about', redirect: '/info' }
]
})
//通用写法,命名路由
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
//动态判断,如根据login-session,重定向到login或home
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})
别名
JavaScript
///a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
路由组件传参
- 更优雅的传输路由参数
- 使用 props 将组件和路由解耦
- 保持props 函数为无状态的,因为它只会在路由发生变化时起作用
- 需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应
JavaScript
//取代与 $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>'
}
//布尔模式。如果 props 被设置为 true,route.params 将会被设置为组件属性
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 }
}
]
})
//对象模式
const router = new VueRouter({
routes: [
{
path: '/promotion/from-newsletter',
component: Promotion,
//如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
props: { newsletterPopup: false }
}
]
})
//函数模式
//你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
routes: [
{
path: '/search',
component: SearchUser,
//URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。
props: route => ({ query: route.query.q })
}
]
})
History 模式
- 让路由看起来更好看 查看文档
- 项目设置
JS
const router = new VueRouter({
mode: 'history',
routes: [...]
})
- 服务器设置。在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。404的页面由之前的服务器设置转为vue中去进行设置。
- nginx
location / {
try_files $uri $uri/ /index.html;
}
- nodejs
JS
const http = require('http')
const fs = require('fs')
const httpPort = 80
http.createServer((req, res) => {
fs.readFile('index.html', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.html" file.')
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})
res.end(content)
})
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
导航守卫
- 截获路由变化,全局变化用于鉴权,组件内守卫二次确认未保存内容。
全局前置守卫
JS
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated){
next({ name: 'Login' })
}
else {
next()
}
})
全局解析守卫
- router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
全局后置钩子
- 你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
JS
router.afterEach((to, from) => {
// ...
})
路由独享的守卫
JS
//特殊的路由二次鉴权等
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫
JS
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
// 想访问组件实例的方式
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
// 通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消
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 的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由元信息
- 结合全局导航守卫,用于鉴权。
JS
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
router.beforeEach((to, from, next) => {
//matched包含了当前所使用的router信息,对其进行判断是否为true决定是否需要进行登录校验等。
if (to.matched.some(record => record.meta.requiresAuth)) {
// 检测是否登录,没有登录,返回登录页面,并把目标页面的链接传递,用于登录后回跳。
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
过渡动效
- 全页面的动画效果
html
<transition>
<router-view></router-view>
</transition>
JS
//每一个页面使用不同的动效
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}
//基于路由的动效
const User = {
template: `
<!-- 使用动态的 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'
}
}
}
数据获取
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
导航完成后获取数据
- 先跳转界面,再请求数据。
- 常在created 钩子中获取数据,展现效果更优,用户点击后可能会看到未渲染的界面。
JS
const Info = {
template : `<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//loading显示
// 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
}
})
}
}
}
}
导航完成前获取数据
- 在 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法
- 实际的展现效果不好,用户会感觉点击按钮后有卡顿。
JS
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 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
JS
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
//顶部
return { x: 0, y: 0 }
//记录的位置
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
//滚动到锚点
if (to.hash) {
return {
selector: to.hash
}
}
},
//异步滚动
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
//平滑滚动
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth',
}
}
}
})
路由懒加载
- 所有代码打包,JavaScript 包会变得非常大,影响加载速度。不同路由对应的组件分割成不同的代码块,按需加载,增加访问速度。
JS
//结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载
// 先将异步组件定义为一个返回promise的工厂函数
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
})
//在router中来定义代码分块点 (split point)
const Foo = () => import('./Foo.vue') // 返回 Promise
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo }]
})
//把组件按组分块
//把某个路由下的所有组件都打包在同个异步块 (chunk) 中,只需要使用 命名 chunk
//Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
导航故障
- 用户留在同一页面上的情况。
- 用户已经位于他们正在尝试导航到的页面
- 一个导航守卫通过调用 next(false) 中断了这次导航
- 一个导航守卫抛出了一个错误,或者调用了 next(new Error())
JS
// 导航故障是一个 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')
}
})
// NavigationFailureType
// 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 // '/'
// }
// })