安装、配置 Router
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。
了解路由之前,我们需要先理解一个概念:单页应用。
单页应用
SPA(single page application):单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面。
路由
这里的路由指的是SPA(单页应用)的路径管理器。
vue的单页面应用将路径和组件映射起来,路由用于设定访问路径,由路径之间的切换,实现组件的切换。
路由模块的本质就是建立起 url 和页面之间的映射关系。
安装、配置 vue-router
安装路由
对于 vue 工程,我们通常用命令行的方式进行安装:
npm install vue-router
// 或者
yarn add vue-router
配置路由
在 main.js 中进行如下路由配置:
// 0. 导入 Vue 和 VueRouter,要调用 Vue.use(VueRouter)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
import Foo from "./views/Foo.vue";
import Bar from "./views/Bar.vue";
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#app')
我们知道路由是路径和组件间的映射,所以在上面的代码中,路径 “/foo” 对应的是 Foo 组件,路径 “/bar” 对应的是 Bar 组件。
路由的使用
配置好路由之后,我们来看一下如何在 vue 文件中使用路由实现路径切换引起组件切换。
首先,在 App.vue 中我们使用 <router-link>
组件来导航:
<template>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<!-- App.vue 中的 <router-view></router-view> 是路由的最高级出口 -->
<router-view></router-view>
</div>
</template>
页面显示效果:
-
导航标签
<router-link>
<router-link>
标签有一个to
属性,这个属性指定路径(链接),根据我们配置的路由,路径/foo
对应的组件是 Foo.vue 组件。 -
“插槽”标签
<router-view>
<router-view>
标签相当于一个插槽,它所在的位置将渲染路由匹配到的组件。
比如,我们点击 “Go to Foo” 这个导航的时候,<router-view></router-view>
处将展示 Foo.vue 组件。
<router-link>
组件支持用户在具有路由功能的应用中 (点击) 导航。默认渲染成带有正确链接的<a>
标签。
注意:Foo.vue 和 Bar.vue 文件在
src/views
文件夹里哦。
注意,在代码演示中,我们把 router 的配置从 main.js 中抽出来,放到了 src/router/index.js 中,当 router 的配置内容过多时,我们强烈建议大家这么做。
代码运行结束后,尝试点击“Go to Foo”、“Go to Bar”,查看页面变化。
添加路由
我们知道路由是路径和组件间的映射,而路径所对应的组件将在 <router-view>
标签处渲染。
现在我们给工程添加一些路由,即在 routes 中添加一些路径和组件的映射:
// 1. 将要用到的组件从其他文件 import 进来
import Foo from './views/Foo.vue';
import Bar from './views/Bar.vue';
import User from './views/User.vue';
// 2. 定义路由,每个路由应该映射一个组件
// 添加路径即在 routes 数组中增加新的成员
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
// 新增一项
{ path: '/user', component: User }
];
// 3. 创建 Router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes
});
导入组件的写法
导入组件不但可以像上面那样用 import 的方法导入,还可以像下面这样直接在 routes
中写:
const routes = [
{ path: '/foo', component: () => import('./views/Foo.vue') },
{ path: '/bar', component: () => import('./views/Bar.vue') },
{ path: '/user', component: () => import('./views/User.vue') }
];
命名路由
有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。我们可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称:
// 0. 导入 Album、List、Add、Empty 三个组件
// 1. 定义路由
const routes = [
{ path: '/foo',
name: 'fooName',
component: () => import('./views/Foo.vue')
}
];
通过命名跳转:
<!-- to 的值是一个对象而不是字符串,所以要在 to 前加 : -->
<router-link :to="{name: 'fooName'}">Go to Foo</router-link>
路由布局管理
在实际开发中,工程比较复杂,应用界面通常由多层嵌套的组件组合而成,相应的,路由也会按某种结构对应嵌套的各层组件。
例如用户相册里有头部、功能区,功能区有相册列表和新建相册两个功能,切换路径时,“banner” 没变,只有功能区的组件发生了切换:
路径、组件的嵌套结构:
/album/list /album/add
+------------------+ +-----------------+
| Album | | Album |
| +--------------+ | | +-------------+ |
| | List | | +------------> | | Add | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
1. 在组件中配合路由使用 <router-view>
App.vue 组件:
<template>
<div id="app">
<h1>Hello App!</h1>
<p>
<router-link to="/album/list">Go to Album List</router-link>
|
<router-link to="/album/add">Go to Album Add</router-link>
</p>
<!-- 路由匹配到的组件将渲染在这里 -->
<!-- 在本例中为 Album.vue 组件 -->
<router-view></router-view>
</div>
</template>
Album.vue 组件:
<template>
<div id="album">
<div class="banner">banner</div>
<!-- 路由匹配到的组件将渲染在这里 -->
<!-- 在本例中为 List.vue 组件或 Add.vue 组件 -->
<router-view></router-view>
</div>
</template>
图示如下:
2. 定义嵌套路由
路由的嵌套关系和组件嵌套关系一致:
// 0. 导入 Album、List、Add 三个组件
const routes = [
{
path: '/album',
component: Album,
// children 属性可以用来配置下一级路由(子路由)
children: [
{ path: 'list', component: List },
{ path: 'add', component: Add }
]
}
];
App.vue 中的 <router-view>
对应 routes
里的第一层路由,即:
{ path: '/album', component: Album }
Album.vue 组件中的 <router-view>
对应 routes
里的第二层路由,即:
{ path: 'list', component: List },
{ path: 'add', component: Add }
根路径 /
特别注意,在上例中如果希望用路径 “/album/list” 对应在 Album.vue 中渲染出相册列表,那么子路由中 path 的写法有两种:
path: 'list'
path: '/album/list'
但是 path: '/list'
这样的写法是不对的!因为以 /
开头的嵌套路径会被当作根路径,那么 List.vue 这个组件会直接渲染到 App.vue 的 <router-view>
处。
当路径从 “/album/list” 切换到 “/album/add” 时,<router-view>
处渲染的组件也从 List.vue 切换成了 Add.vue。
空的子路由
基于上面的路由配置,如果在路径为 “/album” 时仍想在功能区渲染些什么,那么可以给 “/album” 添加一个空路由:
// 0. 导入 Album、List、Add、Empty 三个组件
const routes = [
{
path: '/album',
component: Album,
// children 属性可以用来配置下一级路由(子路由)
children: [
// 空的子路由
{ path: '', component: Empty },
{ path: 'list', component: List },
{ path: 'add', component: Add }
]
}
];
动态路由
之前我们遇到的都是一个路径对应一个组件的情况,但有时我们会遇到多个路径对应一个组件的情况。
比如个人中心对应一个 User 组件,学员登录个人中心时,由于大家的 id 各不相同,所以个人中心的路径可能会不同,像 /user/123
、/user/456
这样,但其实页面会使用同一个 User 组件来渲染。
这里的
123
和456
指的是用户 id,也就是在路径中添加的参数。
像这种多个路径都映射到一个组件这种情况就属于动态路由。
动态路由
动态路由即符合某种模式的多个路径映射到同一个组件,请看动态路由的写法:
import User from "./views/User.vue";
const routes = [
// id 就是路径参数
{ path: '/user/:id', component: User }
]
id 为路径参数,一个“路径参数”前需要使用冒号 :
标记。当 url 匹配到路由中的一个路径时,参数值会被设置到 this.$route.params
里,可以在组件内读取到。
比如 /user/456
匹配的就是 /user/:id
,那么这个用户的 id 就是 456,this.$route.params.id
的值就是 456。
现在我们在 User 的模板,输出当前用户的 id:
<template>
<div>user id: {{ $route.params.id }}</div>
</template>
捕获 404 页面
当用户输入的 url 不属于我们注册的任何一个路由时,我们常需要将页面用 404 NotFound 组件渲染,这里我们可以用通配符(*)来匹配任意路径:
import NotFound from "./views/NotFound.vue";
const routes = [
{
// 会匹配所有路径
path: '*',
component: NotFound
}
]
当使用通配符路由时,请确保含有通配符的路由应该放在最后。因为路由的匹配通常是根据注册的顺序匹配的,如果 path: '*'
路由放在最前面,那么所有的页面都会因为先匹配到通配符路由而由 NotFound 组件渲染。
如何读取匹配到的路径值
当使用一个通配符时,$route.params
内会自动添加一个名为 pathMatch
的参数。它包含了 URL 通过通配符被匹配的部分,比如用上面的路由 { path: '*' }
匹配 URL http://localhost:8081/non-existing/file
:
this.$route.params.pathMatch // '/non-existing/file'
页面跳转
编程式导航
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 Router 的实例方法,通过编写代码来实现:
声明式 | 编程式 |
---|---|
router.push(…) |
注意,因为在 Vue 实例内部,我们可以通过 r o u t e r 访问 R o u t e r 的实例。所以 R o u t e r 的实例方法 p u s h 可以用 ‘ t h i s . router 访问 Router 的实例。所以 Router 的实例方法 push 可以用 `this. router访问Router的实例。所以Router的实例方法push可以用‘this.router.push(…)` 调用。
用 router.push
进行页面跳转及参数传递
一、router.push 的参数为字符串路径
router.push 方法的参数可以是一个字符串路径:
router.push('user')
router.push('/user')
下面详细说明上面两种写法的不同,主要是跳转后 url 的变化不同:
原 url | localhost:8080 | localhost:8080/home |
---|---|---|
router.push(‘user’) 跳转后的 url | localhost:8080/user | localhost:8080/home/user |
router.push(‘/user’) 跳转后的 url | localhost:8080/user | localhost:8080/user |
因为 /
意味着匹配根路由,所以 '/user'
这样的写法不管原路径 localhost:8080/??
中的 ??
是什么,跳转后 url 都会变为 localhost:8080/user
。
二、router.push 的参数为描述地址的对象
router.push 方法的参数可以是一个描述地址的对象:
// 对象
// 这种写法和字符串类型的参数一样
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
只提供 path
值的参数和字符串类型的参数是一样的。这里就不再说了。我们主要看一下后两种写法。
{ name: 'user', params: { userId: '123' }}
-
对应的命名路由为:
{ path:'user/:userId', name:'user' }
; -
跳转后 url:
localhost:8080/user/123
; -
取参数:
$route.params.userId
;
{ path: 'register', query: { plan: 'private' }}
- 对应的路由为:
{ path:'register' }
; - 跳转后 url:
localhost:8080/register?plan=private
; - 取参数:
$route.query.plan
;
小结一下参数传递的对应规则:
- name 对应 params,路径形式:user/123;
- path 对应 query,路径形式:user?id=123;
如果使用 path 进行页面跳转的时候,写 params 进行传参会被忽略:
// params 会被忽
router.push({ path: 'user', params: { userId: '123' }})
可以换成下面的写法:
router.push({ path: 'user/123'})
同样的规则也适用于 router-link
组件的 to
属性。
页面跳转后如何获取参数
之前我们说可以通过 $route 访问当前路由,现在我们看看 $route 到底能给我们提供些什么信息。
现在我们访问本地启动的工程的一个地址 http://localhost:8080/user/123?name=userName#abc
,对应的 $route 如下:
// $route
{
// 路由名称
name: "user",
meta: {},
// 路由path
path: "/user/123",
// 网页位置指定标识符
hash: "#abc",
// window.location.search
query: {name: "userName"},
// 路径参数 user/:userId
params: {userId: "123"},
fullPath: "/user/123?name=userName#abc"
}
页面跳转后获取参数可以很方便的通过 $route.query
、$route.params
、$route.hash
获取
重定向和别名
上节课作业中我们开发了优课达官网的首页、全部课程页面和课程详情页,他们的路由关系是:
首页 | 全部课程页 | 课程详情页 |
---|---|---|
/layout/home | /layout/courseall | /layout/coursedetail/:courseId |
可以看到这三个路径中都有 layout
,但实际我们遇到的情况是,官网首页一般是 https://www.xxx.com/
。这时候我们就可以使用路由功能中的别名和重定向把 /layout/home
变成 /
。
首先我们使用别名。
别名
别名,顾名思义就是两个名字指向同一个路由:
const routes: [
// 定义 alias 属性
{ path: '/a', alias: '/b', component: A }
];
当访问 /a
时,渲染的是 A;当访问 /b
时,就像访问 /a
一样,渲染的也是 A。
我们的目的是把 /layout/home
变成 /
,所以要把 layout 去掉:
const routes: [
{
path: '/layout',
// 别名定为 /
alias: '/',
component: ()=> import('@/pages/nest/Layout.vue'),
children: [
{ path: 'home', component: Home },
{ path: 'courseall', component: CourseAll },
{ path: 'coursedetail/:courseId', component: CourseDetail }
]
}
];
这时候 /layout/home
变成了 /home
:
接下来我们要用重定向把 /home
变成 /
。
路由重定向
先了解重定向的意思。
若路由 /a
重定向到 /b
,即访问 /a
时, url 会自动跳到 /b
,然后匹配路由 /b
:
const routes: [
// 定义 redirect 属性,将 /a 重定向到 /b
{ path: '/a', redirect: '/b' }
]
现在我们希望 /
重定向到 /home
,这样访问 /
时会自动跳到 /home
:
const routes: [
{
path: '/layout',
alias: '/',
// 重定向到 /home
redirect: '/home',
component: ()=> import('@/pages/nest/Layout.vue'),
children: [
{ path: 'home', component: Home },
{ path: 'courseall', component: CourseAll },
{ path: 'coursedetail/:courseId', component: CourseDetail }
]
}
];
现在的路由是这样的:
首页 | 全部课程页 | 课程详情页 |
---|---|---|
/layout/home | /layout/courseall | /layout/coursedetail/:courseId |
/ | /courseall | /coursedetail/:courseId |
监听路由
有时我们需要对路由的变化进行监听,比如优课达官网展示所有课程的页面:
当页面头部的标签切换时,路径也会发生变化,我们需要监听路由变化来改变标签的样式,或者在页面中加载对应的内容。
那么怎么监听路由呢?
监听路由 $route
的变化
监听路由变化我们需要用到两个知识:vue 中的 watch 和 $route
。
watch 即之前讲过的数据变化监听,$route
即当前路由。
路由监听写法如下:
watch: {
$route(to,from){
console.log(to, from);
}
}
// 或者
watch: {
$route: {
handler: function(to,from) {
console.log(to,from);
},
// 深度观察监听
deep: true
}
},
to 是变化后的路由,from 是变化前的路由。一般我们用第一种写法就可以了。
监听路由的使用
现在我们来实现上面 gif 的例子,根据路径参数定位点击的标签:
1. 基础样式
tab 的样式分两种,被点击的和未被点击的:
被点击的 tab 会多个类名 “active”。
定义 tab 点击事件
<script>
export default {
methods: {
// 点击 tab 时会执行 changeTab 方法
changeTab(type) {
// 使用 Router 实例方法改变路径参数
this.$router.push({ query: { type: type } });
}
}
};
</script>
3. 监听路由变化,更新 tab 样式
<script>
export default {
watch: {
$route(to, from) {
// 路由变化了就执行更新样式的方法
this.updateTab();
console.log(to, from);
}
},
methods: {
changeTab(type) {
this.$router.push({ query: { type: type } });
},
// 更新样式的方法
updateTab() {
this.tabList.map(menu => {
menu.active = menu.type === this.$route.query.type;
});
}
}
};
</script>
4. 处理特殊情况
如果页面路径一开始没有路径参数 type,那我们默认选择第一个 tab,如果页面路径一开始就有路径参数,那么需要马上更新 tab 样式:
<script>
export default {
methods: {
// ...
updateTab() {
if (!this.$route.query.type) {
return;
}
this.tabList.map(tab => {
tab.active = tab.type === this.$route.query.type;
})
}
}
};
</script>
网络请求 async 与 await
本章我们会学习如何在 vue 中请求远程数据。
在 JavaScript 中我们曾学过用 fetch
请求数据,比如一个显示优课达公司信息的 API
:
https://www.done.kim/api/m/f4-11-1-1
把这个 URL 贴到浏览器,可以看到查询返回结果为:
文字内容如下:
{
"company": "优课达",
"slogan": "学的比别人好一点"
}
用 fetch
请求数据的代码如下:
fetch(
'https://www.done.kim/api/m/f4-11-1-1'
)
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
});
由于 fetch
返回的是一个 Promise
对象,所以我们在请求数据的时候用了 then
采用平铺式回调的方式,允许我们在数据返回之后再对数据进行 response.json()
处理。
这里就有一个问题,因为 Promise
对象的一个特点是无等待,所以想对返回的数据进行操作,就必须在 then
里处理。假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。那么 then
链就会很长
现在我们来学习异步编程终级解决方案 —— async
和 await
。
“异步” async
async
是“异步”的简写,用于申明一个异步 function
,而这个 async
函数返回的是一个 Promise
对象。
async function asyncFn() {
return {
"company": "优课达",
"slogan": "学的比别人好一点"
};
}
const result = asyncFn();
console.log(result);
既然 async
返回的是一个 Promise
对象,那和普通的 fetch
方法有什么区别?着关键点就在于 await
关键字。
“等待异步” await
await
用于等待一个异步方法执行的完成,它会阻塞后面的代码,等着 Promise 对象 resolve *,然后得到 resolve 的值,作为 await 表达式的运算结果:
async function getAsyncFn() {
const result = await asyncFn();
console.log(result);
}
getAsyncFn();
要注意,await
只能出现在 async
函数中,如果用在普通函数里,就会报错,所以上面我们又定义了一个 async
函数 getAsyncFn
。
如果大家想要了解更多关于
Promise
对象的知识,可以点击查看这个文档
多个请求并发执行 Promise.all
如果要完成多个请求并发执行,可以使用 Promise.all
:
async function asyncFn1() {
return "优课达";
}
async function asyncFn2() {
return "学的比别人好一点";
}
async function getAsyncFn() {
const result = await Promise.all([asyncFn1(), asyncFn2()]);
console.log(result);
}
getAsyncFn();
在 vue 中运用 async 和 await 请求数据
之前我们在开发“全部课程页”和“课程详情页”时,课程数据是直接写在 data
里的,现在我们将运用 async 和 await 请求数据,完成后页面中的课程信息均为动态获取的:
已知我们 mock 的数据返回格式如下,其中的 data 是我们实际需要的数据:
{
data: {...},
isSuccess: true
}
在全部课程页 CourseAll.vue 组件里获取所有课程:
<script>
export default {
data: function() {
return {
courseList: []
};
},
async mounted() {
// 在生命周期 mounted 中调用获取课程信息的方法
await this.queryAllCourse();
},
methods: {
// 在 methods 对象中定义一个 async 异步函数
async queryAllCourse() {
// 在 fetch 中传入接口地址
const res = await fetch('https://www.done.kim/api/m/queryallcourse');
// 将文本体解析为 JSON 格式的promise对象
const myJson = await res.json();
// 获取返回数据中的 data 赋值给 courseList
this.courseList = myJson.data;
}
}
}
</script>
给 api 传参数
在课程详情页 CourseDetail.vue 组件,我们需要根据课程 id 获取课程信息,这时需要给获取课程信息的 api 传课程 id:
<script>
export default {
data: function() {
return {
course: []
};
},
async mounted() {
await this.getCourse();
},
methods: {
async getCourse() {
// 从路径中获取课程 id
const courseId = this.$route.params.courseId
// 在接口地址后传入参数 id
const res = await fetch('https://www.done.kim/api/m/getcourse?id=' + courseId);
const myJson = await res.json();
this.course = myJson.data;
}
}
}
</script>