Vue.js 深入学习 vue-router

vue-router是Vue官方的核心插件之一,用来实现单页面应用(SPA)。

实现单页面应用的核心就是前端路由。SPA其实就是在前后端分离的基础上,加一层前端路由。
前端路由实现的方式有两种,url的hash,js通过hashChange事件监听url的改变。
另一种是H5的history模式。

安装

1
2
3
4
5
6
7
npm install vue-router
//main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

Usage

将组件(components)映射到路由(routes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//html
<div id="app">
<router-link to="/list" class="header-title">ChapOps</router-link>
<router-link to="/add" class="header-title">Add</router-link>
</div>
<router-view></router-view>
//main.js 路由配置
import Vue from 'vue'
import VueRouter from 'vue-router'
const RouterConfig = {
// 使用 H5 的 History 模式
mode: 'history',
routes: Routers
};
const router = new VueRouter(RouterConfig);
const app = new Vue({
el: "#app",
router
})
//router.js 定义路由
const routers = [
{
path: '/list',
meta: {
title: 'My Bots'
},
component: (resolve) => require(['./views/list.vue'], resolve)
},
{
path: '/add',
meta: {
title: 'Add Bot'
},
component: (resolve) => require(['./views/add.vue'], resolve)
},
{
path: '*',
redirect: '/list'
}
];
export default routers;

通过注入路由器,我们可以在任何组件内通过 this.$router 访问路由器,使用 this.$router 的就不需要在封装路由的组件中都导入路由。
最后一个path为*的路由,是为了当访问路径不存在时,访问list页面。

上面的写法,webpack会把每个路由打包,请求到该页面时才会加载,也就是懒加载(按需加载),如果需要一次性加载:

1
2
3
4
{
path: '/add',
component: require('./views/add.vue')
}

路由懒加载在webpack打包后可能会抛出404的错误,改写成resolve => require.ensure([], () => resolve(require('../views/BotList.vue')), 'chatops-view')
之后,问题解决。把所有组件都打包在同个异步 chunk 中,只需要 给 chunk 命名,提供 require.ensure 第三个参数作为 chunk 的名称
,Webpack 将相同 chunk 下的所有异步模块打包到一个异步块里面。

路径参数

在路由路径中使用参数:

1
2
3
4
5
6
const routers = [
{
path: '/bot/:name',
component: bot
}
];

路径参数使用冒号:标记。参数值会被设置到this.$route.params。用上面的代码距离,当url/bot/testBot匹配到路由时,
可以通过this.$route.params.name访问。

监测路由变化

当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。由于组件被复用,组件的生命周期钩子不会再被调用。
所以这是想要对参数变化做出响应可以使用侦听属性:

1
2
3
4
5
6
7
8
const bot = {
template: '...',
watch: {
'$route' (to, from) {
// do someTing...
}
}
}

优先级

同一个路径可以匹配多个路由,优先级按照路由的定义顺序:谁先定义的,谁的优先级就最高。

路由嵌套

上面的例子里<router-view> 是用来渲染路由匹配到的组件,同样地,一个被渲染组件同样可以包含自己的嵌套 <router-view>,例如:

1
2
3
4
5
6
7
const bot = {
template: '<div>
<h2>Bot {{ $route.params.name }}</h2>
<router-view></router-view>
</div>',
}

要在嵌套的<router-view>中渲染组件,还需要配置children参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routers = [
{
path: '/bot/:name',
component: bot,
children: [
{
// /bot/:name/env 匹配成功,
// botEnv 会被渲染在 bot 的 <router-view> 中
path: 'env',
component: botEnv
},
{ path: '', component: bot },
]
}
];

上面的配置,有一个path为空字符串的自理由,如果没有配置这个子路由,当访问 /bot/testBot 时,不会渲染任何东西,因为没有匹配到合适的子路由。

/ 开头的嵌套路径会被当作根路径。

路由跳转

两种方式,一种是使用<router-link>

1
2
3
4
<div id="app">
<router-link to="/list" class="header-title">ChapOps</router-link>
<router-link to="/add" class="header-title">Add</router-link>
</div>

上面的to是一个prop,指定要跳转的path。
router-link还有一些其他的prop:

  • tag,默认值: “a”,指定router-link渲染成某种标签,例如
  • 。同样它还是会监听点击,触发导航。
  • replace,下面会介绍。
  • active-class,默认值: “router-link-active”,当路由匹配成功时,自动给当前元素添加一个router-link-activeclass。可以通过设置active-class修改class。

使用<router-link>,在H5的history模式下会拦截点击,避免浏览器重新加载。

如果要在JavaScript中跳转,就要使用router.push

1
2
3
4
5
6
export default {
name: "app",
mounted() {
this.$router.push({ path: "list"});
}
}

router.push router-link用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 字符串
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 会被忽略,这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user

同样的规则也适用于router-link组件的to属性。

replace

router.replace和router.push的区别就是,router.replace 不会向 history 添加新记录,而是替换掉当前的 history 记录。
<router-link :to="..." replace>router.replace(...)的效果是一样的

go

router.go(n)方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

路由命名

1
2
3
4
5
6
7
8
9
10
const routers = [
{
path: '/bot/:name',
name: bot,
component: bot
}
];
//调用
this.$router.push({ name: 'bot', params: { name: "testBot" }})

视图命名

如果需要同时(同级)展示多个视图,就需要给视图命名:

1
2
3
<router-view></router-view>
<router-view name="one"></router-view>
<router-view name="two"></router-view>

如果 router-view 没有设置名字,默认为 default。
多个视图就需要多个组件:

1
2
3
4
5
6
7
8
9
10
11
const routers = [
{
path: '/bot/:name',
name: bot,
components: {
default: bot,
one: One,
two: Two
}
}
];

在嵌套路由中也可以使用视图命名。

重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const routers = [
{
path: '/bot/:name',
redirect: "/list"
}
];
//重定向的目标也可以是一个命名的路由
const routers = [
{
path: '/bot/:name',
redirect: {name: "list"}
}
];
//还可以是一个函数,返回重定向的路径
const routers = [
{
path: '/bot/:name',
redirect: (to) => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
return ...
}
}
];

别名

/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

1
2
3
const routers = [
{ path: '/a', component: A, alias: '/b' }
];

路由组件prop

1
2
3
4
5
6
7
8
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})

上面的代码,使用$route,组件和路由高度解耦,可以通过props解耦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 被设置为 true,$route.params 将会被设置为组件属性。props 还可以是一个对象,它会被按原样设置为组件属性。也可以是一个函数

H5 History 模式

1
2
3
4
5
6
const RouterConfig = {
mode: 'history',
routes: Routers
};
const router = new VueRouter(RouterConfig);

当你使用 history 模式时,URL 就像正常的 url,例如 http://website.com/bot/name,也好看!
不过这种模式需要后台配置支持。服务端将所有路由都指向同一个html,如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,否则刷新时页面会出现404。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http')
const fs = require('fs')
const httpPort = 80
http.createServer((req, res) => {
fs.readFile('index.htm', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.htm" 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)
})

对于 Node.js/Express,推荐使用 connect-history-api-fallback 中间件。

钩子

导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

beforeEach afterEach

beforeEach 和afterEach ,它们会在路由即将改变前和改变后触发,钩子函数有3个参数:

  • to,即将要进入的目标的路由对象
  • from,当前导航即将要离开的路由对象
  • next,调用该方法后,才能进入下一个钩子。

其他钩子函数查看官方文档

路由元信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routers = [
{
path: '/list',
meta: {
title: 'My Bots'
},
component: (resolve) => require(['./views/list.vue'], resolve)
},
{
path: '/add',
meta: {
title: 'Add Bot'
},
component: (resolve) => require(['./views/add.vue'], resolve)
}
];

在钩子函数中可以通过to.meta.some来访问。