Vue2学习笔记

学习来源:黑马程序员

1 预备知识

1.1 前端工程化

前端工程化指的是:在企业级的前端项目开发中,把前端开发所需的工具、技术、流程、经验等进行规范化、标准化。

企业中的 Vue 项目和 React 项目,都是基于工程化的方式进行开发的。

好处:前端开发自成体系,有一套标准的开发方案和流程。

前端工程化解决方案

早期的前端工程化解决方案:

目前主流的前端工程化解决方案:

1.2 webpack

1.2.1 概念

概念:webpack 是前端项目工程化的具体解决方案。

主要功能:它提供了友好的前端模块化开发支持,以及代码压缩混淆、处理浏览器端 JavaScript 的兼容性、性能优化等强大的功能。

好处:让程序员把工作的重心放到具体功能的实现上,提高了前端开发效率和项目的可维护性。

注意:目前 Vue,React 等前端项目,基本上都是基于 webpack 进行工程化开发的

1.2.2 基本使用

① 需求和初始化步骤

需求:页面隔行变色

步骤

  • 新建项目空白目录,并运行 npm init –y 命令,初始化包管理配置文件 package.json
  • 新建 src 源代码目录
  • 新建 src -> index.html 首页和 src -> index.js 脚本文件
  • 初始化首页基本的结构
  • 运行 npm install jquery –S 命令,安装 jQuery
  • 通过 ES6 模块化的方式导入 jQuery,实现列表隔行变色效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!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>
<!-- 引入js文件 -->
<script src="./index.js"></script>
</head>
<body>
<ul>
<li>这是第 1 个 li</li>
<li>这是第 2 个 li</li>
<li>这是第 3 个 li</li>
<li>这是第 4 个 li</li>
<li>这是第 5 个 li</li>
<li>这是第 6 个 li</li>
<li>这是第 7 个 li</li>
<li>这是第 8 个 li</li>
<li>这是第 9 个 li</li>
</ul>
</body>
</html>
1
2
3
4
5
6
7
8
9
// 使用es6语法导入jq
import $ from "jquery"

// 定义jq的入口函数
$(function () {
// 实现奇偶行的变色效果
$('li:odd').css('background-color', 'red')
$('li:even').css('background-color', 'pink')
})

打开浏览器发现没有效果。

② 安装和配置webpack

安装命令:

1
npm install webpack@5.42.1 webpack-cli@4.7.2 -D

-D表示开发环境,被记录在devDependencies节点下

配置

  • 在项目根目录中,创建名为 webpack.config.js 的 webpack 配置文件,并初始化如下的基本配置:
1
2
3
4
5
// 使用nodejs中的导出语法,导出一个webpack的配置对象
module.exports = {
// 可选值有development和production
mode: 'development'
}
  • package.json 的 scripts 节点下,新增 dev 脚本如下:
1
2
3
4
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev" : "webpack" // scripts节点下的脚本,可以通过 npm run xxx 来执行,例如npm run test 或者 npm run dev
}
  • 在终端中运行 npm run dev 命令,启动 webpack 进行项目的打包构建,运行后命令后在项目文件夹下生成dist,即为打包后的项目

image-20220516170946315

  • 将index.html的引入修改为:
1
<script src="../dist/main.js"></script>

页面效果

image-20220516171335725

③ 其他细节

mode 的可选值

mode 节点的可选值有两个,分别是:

  • development
    • 开发环境
    • 不会对打包生成的文件进行代码压缩和性能优化
    • 打包速度快,适合在开发阶段使用
  • production
    • 生产环境
    • 会对打包生成的文件进行代码压缩和性能优化
    • 打包速度很慢,仅适合在项目发布阶段使用

webpack.config.js 文件的作用

webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件,从而基于给定的配置,对项目进行打包。

注意:由于 webpack 是基于 node.js 开发出来的打包工具,因此在它的配置文件中,支持使用 node.js 相关的语法和模块进行 webpack 的个性化配置。

webpack 中的默认约定

在 webpack 4.x 和 5.x 的版本中,有如下的默认约定:

  • 默认的打包入口文件为 src -> index.js
  • 默认的输出文件路径为 dist -> main.js

注意:可以在 webpack.config.js 中修改打包的默认约定

在 webpack.config.js 配置文件中,通过 entry 节点指定打包的入口。通过 output 节点指定打包的出口。

2 vue基础

2.1 vue简介

官方给出的概念:Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的前端框架。

2.2.1 特性

vue 框架的特性,主要体现在如下两方面:

  1. 数据驱动视图
  2. 双向数据绑定

数据驱动视图

在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。示意图如下:

image-20220516172359350

好处:当页面数据发生变化时,页面会自动重新渲染!

注意:数据驱动视图是单向的数据绑定。

双向数据绑定

在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源中。示意图如下:

image-20220516172449631

image-20220516172455268

好处:开发者不再需要手动操作 DOM 元素来获取表单元素最新的值!

2.2.2 MVVM

MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。MVVM 指的是 Model、View 和 ViewModel,它把每个 HTML 页面都拆分成了这三个部分,如图所示:

image-20220516172609959

在 MVVM 概念中:

  • Model 表示当前页面渲染时所依赖的数据源
  • View 表示当前页面所渲染的 DOM 结构。
  • ViewModel 表示 vue 的实例,它是 MVVM 的核心。

工作原理

ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。

image-20220516172802797

当数据源发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面的结构。

当表单元素的值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中。

2.2.3 vue版本

当前,vue 共有 3 个大版本,其中:

  • 2.x 版本的 vue 是目前企业级项目开发中的主流版本
  • 3.x 版本的 vue 于 2020-09-19 发布,生态还不完善,尚未在企业级项目开发中普及和推广
  • 1.x 版本的 vue 几乎被淘汰,不再建议学习与使用

总结:

3.x 版本的 vue 是未来企业级项目开发的趋势;

2.x 版本的 vue 在未来(1 ~ 2年内)会被逐渐淘汰;

2.2 vue初体验

  • 导入 vue.js 的 script 脚本文件
  • 在页面中声明一个将要被 vue 所控制的 DOM 区域
  • 创建 vm 实例对象(vue 实例对象)
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!-- 希望vue能够控制下面的div,帮助我们把数据填充到div内部 -->
<div id="app">{{ username }}</div>

<!-- 导入vue库文件 -->
<script src="./lib//vue-2.6.12.js"></script>
<!-- 创建vue的实例对象 -->
<script>
// 创建vue的实例对象
const vm = new Vue({
// el属性是固定的写法,表示当前vm实例要控制页面上的哪个区域,接受的值是一个选择器
el: '#app',
// data对象就是要渲染到页面上的数据
data: {
username: 'Mark'
}
});
</script>
</body>
</html>

基本代码与 MVVM 的对应关系

image-20220516193926228

2.3 vue调试工具

vue 官方提供的 vue-devtools 调试工具,能够方便开发者对 vue 项目进行调试与开发。

Chrome 浏览器在线安装 vue-devtools :下载地址

image-20220516195633490

2.4 vue指令

2.4.1 指令概念

指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构

vue 中的指令按照不同的用途可以分为如下 6 大类:

  1. 内容渲染指令
  2. 属性绑定指令
  3. 事件绑定指令
  4. 双向绑定指令
  5. 条件渲染指令
  6. 列表渲染指令

指令是 vue 开发中最基础、最常用、最简单的知识点。

2.4.2 内容渲染指令

内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下 3 个:

  1. v-text
  2. 插值表达式
  3. v-html
① v-text

用法示例:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 把username对应的值,渲染到第一个p标签中 -->
<p v-text="username"></p>
<!-- 注意“性别”会被gender的值给覆盖掉 -->
<p v-text="gender">性别</p>
</div>

<script src="./lib//vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: "Mark",
gender: "男"
}
});
</script>
</body>
</html>

image-20220516201422365

注意:v-text 指令会覆盖元素内默认的值。

② 插值表达式

vue 提供的插值表达式语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种语法的专业名称是插值表达式(英文名为:Mustache)。注意只能放在元素的内容节点中,不能放在元素的属性节点中。

注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>姓名:{{ username }}</p>
<p>性别:{{ gender }}</p>
</div>

<script src="./lib//vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: "Mark",
gender: "男"
}
});
</script>
</body>
</html>
③ v-html

v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html 这个指令:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<div v-text="info"></div>
<div>{{ info }}</div>
<div v-html="info"></div>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: "Mark",
gender: "男",
info: '<h4 style="color: red; font-weight: bold;">Hello World!</h4>'
}
});
</script>
</body>
</html>

渲染结果:

image-20220516201822928

2.4.3 属性绑定指令

如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令,属于单向绑定,即数据源改变,则DOM元素值改变。用法示例如下:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-bind:placeholder="tips">
<hr>
<img v-bind:src="photo" style="width: 150px;">
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
tips: "请输入用户名",
photo: "https://cn.vuejs.org/images/logo.svg"
}
});
</script>
</body>
</html>

渲染结果:

image-20220516202809319

简写形式

由于 v-bind 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的:)。

1
2
3
4
5
<div id="app">
<input type="text" :placeholder="tips">
<hr>
<img :src="photo" style="width: 150px;">
</div>

使用 Javascript 表达式

在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值之外,还支持 Javascript 表达式的运算,例如:

1
2
3
4
5
{{number + 1}}

{{ok ? "YES" : "NO"}}

<div :id="'list-' + id"></div>

2.4.4 事件绑定指令

① 基本使用

vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听

原生 DOM 对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,分别为:v-on:clickv-on:inputv-on:keyup

通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明。

由于 v-on 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的 @ )。

代码示例

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>count的值是: {{ count }}</p>
<button v-on:click="add">+1</button>
<!-- v-on简写形式 -->
<button @click="sub">-1</button>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
count: 0
},
// 作用:定义事件的处理函数
methods: {
// es5写法
// add: function() {
// this.count += 1;
// }

// es6语法形式
add() {
// this表示当前new出来的vm实例对象
// 通过this可以访问到data中的数据
this.count += 1;
},
sub() {
this.count -=1;
}
}
});
</script>
</body>
</html>
② 事件参数对象

在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参

在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件参数对象 event。同理,在 v-on 指令(简写为 @ )所绑定的事件处理函数中,同样可以接收到事件参数对象 event。

$event 是 vue 提供的内置特殊变量,用来表示原生的事件参数对象 event。

$event 可以解决事件参数对象 event 被覆盖的问题。不常用。

代码示例

需求:count是偶数,按钮颜色为红色,为奇数则取消背景颜色

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>count的值是: {{ count }}</p>
<button @click="add(1, $event)">+1</button>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
count: 0
},
methods: {
add(n, e) { // 接收事件对象,简写为e
this.count += n;
// 判断count的值是否为偶数
if(this.count % 2 === 0) {
e.target.style.backgroundColor = "red";
} else {
e.target.style.backgroundColor = "";
}
}
}
});
</script>
</body>
</html>
③ 事件修饰符

在事件处理函数中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。因此,vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:

事件修饰符 说明
.prevent 阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等)
.stop 阻止事件冒泡
.capture 以捕获模式触发当前的事件处理函数
.once 绑定的事件只触发1次
.self 只有在 event.target 是当前元素自身时触发事件处理函数
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 事件修饰符:阻止超链接跳转 -->
<a href="http://www.baidu.com" @click.prevent="show">跳转到百度首页</a>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {},
methods: {
show() {
console.log("点击了超链接");
}
}
});
</script>
</body>
</html>

2.4.5 双向绑定指令

vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据,即双向绑定,只要DOM元素值或者数据源数据其中一个改变,相应地另一个就会改变,这与v-bind单向绑定不同。可以和表单元素一起使用:inputtextareaselect

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>用户的名字是:{{ username }}</p>
<!-- 双向绑定 -->
<input type="text" v-model="username">
<hr>
<!-- 单向绑定 -->
<input type="text" :value="username">
<hr>
<select v-model="city">
<option value="">请选择城市</option>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
</select>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: "Mark",
city: ""
}
});
</script>
</body>
</html>

image-20220517150330609

为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:

修饰符 作用 示例
.number 自动将用户的输入值转为数值类型 <input v-model.number="age" />
.trim 自动过滤用户输入的首尾空白字符 <input v-model.trim="msg" />
.lazy 在“change”时而非“input”时更新 <input v-model.lazy="msg" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model.number="n1"> + <input type="text" v-model.number="n2"> = <span>{{ n1 + n2}}</span>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
n1: 1,
n2: 2
}
});
</script>
</body>
</html>

image-20220517151229339

2.4.6 条件渲染指令

条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:

  1. v-if
  2. v-show

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p v-if="flag">这是被v-if控制的元素</p>
<p v-show="flag">这是被v-show控制的元素</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
// 用于控制是否展示元素
flag: true
}
});
</script>
</body>
</html>

将flag修改为false后,发现两者都不见了,但是:

image-20220517152117722

二者的区别:

  • 实现原理不同:

    • v-if 指令会动态地创建或移除 DOM 元素,从而控制元素在页面上的显示与隐藏;
    • v-show 指令会动态为元素添加或移除 style="display: none;" 样式,从而控制元素的显示与隐藏;
  • 性能消耗不同:

    • v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此:
      • 如果需要非常频繁地切换,则使用 v-show 较好
      • 如果在运行时条件很少改变,则使用 v-if 较好

v-if配套的指令

v-if 可以单独使用,或配合 v-else 指令一起使用。注意:v-else 指令必须配合 v-if 指令一起使用,否则它将不会被识别!v-else-if 指令,顾名思义,充当 v-if 的“else-if 块”,可以连续使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else>合格</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
type: "A"
}
});
</script>
</body>
</html>

2.4.7 列表渲染指令

① 基本使用

vue 提供了 v-for 列表渲染指令,用来辅助开发者基于一个数组来循环渲染一个列表结构。v-for 指令需要使用 item in items 形式的特殊语法。

v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items

注意:v-for 指令中的 item 项和 index 索引都是形参,可以根据需要进行重命名。例如 (user, i) in userlist

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="./lib/bootstrap.css">
</head>
<body>
<div id="app">
<table class="table table-bordered table-hover table-striped">
<th>索引</th>
<th>ID</th>
<th>姓名</th>
<tbody>
<tr v-for="(item, index) in list">
<td>{{ index }}</td>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
list: [
{id: 1, name: "Mark"},
{id: 2, name: "Hongyi"},
{id: 3, name: "Kisugi"}
]
}
});
</script>
</body>
</html>

image-20220517160024695

② key值

当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。

为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每项提供一个唯一的 key 属性。

key 的注意事项

  • key 的值只能是字符串或数字类型
  • key 的值必须具有唯一性(即:key 的值不能重复)
  • 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
  • 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性)
  • 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<table class="table table-bordered table-hover table-striped">
<th>索引</th>
<th>ID</th>
<th>姓名</th>
<tbody>
<!-- 官方建议,使用了v-for要绑定key属性 -->
<!-- 而且尽量将id作为key的值 -->
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index }}</td>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</div>

2.4.8 实践案例

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<!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>品牌列表案例</title>
<link rel="stylesheet" href="./lib/bootstrap.css">
<link rel="stylesheet" href="./css/brandlist.css">
</head>

<body>

<div id="app">
<!-- 卡片区域 -->
<div class="card">
<div class="card-header">
添加品牌
</div>
<div class="card-body">
<!-- 添加品牌的表单区域 -->
<form @submit.prevent="add">
<div class="form-row align-items-center">
<div class="col-auto">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text">品牌名称</div>
</div>
<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brand">
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-2">添加</button>
</div>
</div>
</form>
</div>
</div>

<!-- 表格区域 -->
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">品牌名称</th>
<th scope="col">状态</th>
<th scope="col">创建时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index }}</td>
<td>{{ item.name }}</td>
<td>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" :id="'cb' + item.id" v-model="item.status">
<label class="custom-control-label" :for="'cb' + item.id" v-if="item.status">已启用</label>
<label class="custom-control-label" :for="'cb' + item.id" v-else>已禁用</label>
</div>
</td>
<td>{{ item.time }}</td>
<td>
<a href="javascript:;" @click="remove(item.id)">删除</a>
</td>
</tr>
</tbody>
</table>
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
const vm = new Vue({
el: "#app",
data: {
// 品牌数据
list: [
{id: 1, name: "宝马", status: true, time: new Date()},
{id: 2, name: "奔驰", status: false, time: new Date()},
{id: 3, name: "大众", status: true, time: new Date()},
{id: 4, name: "路虎", status: true, time: new Date()}
],
// 用户输入的品牌名称
brand: "",
// 下一个可用的id
nextId: 5
},
methods: {
// 删除商品
remove(id) {
this.list = this.list.filter(item => item.id !== id);
},
// 阻止表单的默认提交行为后,触发add方法
add() {
// 如果brand为空,则return
if(this.brand === "") return alert("品牌名称不能为空");
const obj = {
id: this.nextId,
name: this.brand,
status: true,
time: new Date()
};
this.list.push(obj);
this.brand = "";
this.nextId += 1;
}
}
});
</script>
</body>

</html>

image-20220517165400163

2.5 vue侦听器

2.5.1 基本使用

watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。

代码示例1

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: ""
},
// 所有的侦听器都应该被定义在watch节点中
watch: {
// 侦听器本质上是一个函数
// 要监视哪个数据的变化,就将其作为方法名
// newVal是新值,oldVal是旧值,是固定的形参顺序
username(newVal, oldVal) {
console.log(newVal, oldVal);
}
}
});
</script>
</body>
</html>

代码示例2

监听 username 值的变化,并使用 ajax 发起 Ajax 请求,检测当前输入的用户名是否可用。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
username: ""
},
watch: {
username(newVal) {
if(newVal === "") {
return
}
// 向后端发送ajax请求,判断newVal是否被调用
$.get("https://www.escook.cn/api/finduser/" + newVal, function(res) {
console.log(res);
});
}
}
});
</script>
</body>
</html>

image-20220519200742187

2.5.2 immediate选项

  • 方法格式的侦听器:
    • 缺点1:无法在刚进入页面时,自动触发侦听器
    • 缺点2:如果侦听的是一个对象,则对象的属性发生变化时不会触发侦听器
  • 对象格式的侦听器:
    • 好处1:可以通过immediate选项让侦听器自动触发一次,默认不开启
    • 好处2:可以通过deep选项深度侦听对象的每个属性的变化,默认不开启

默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项。

代码示例

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
// 有一个初始值,在刚进入页面时希望让侦听器被触发
username: "admin"
},
watch: {
// 对象格式的侦听器
username: {
// 侦听器的处理函数,固定名称
handler(newVal, oldVal) {
console.log(newVal, oldVal);
},
// 一进入页面就出发一次侦听器
immediate: true
}
}
});
</script>
</body>
</html>

image-20220519201506702

2.5.3 deep选项

如果 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项。

代码示例1

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="infor.username">
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
// 用户信息的对象
infor: {
username: "Mark"
}
},
watch: {
infor: {
handler(newVal) {
console.log(newVal);
},
// 开启深度侦听
// 只要对象中任何一个属性变化了都会触发侦听器
deep: true
}
}
});
</script>
</body>
</html>

image-20220519202343011

代码示例2

如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
const vm = new Vue({
el: "#app",
data: {
// 用户信息的对象
infor: {
username: "Mark"
}
},
watch: {
infor.username: {
handler(newVal) {
console.log(newVal);
}
}
}
});
</script>

2.6 vue计算属性

计算属性指的是通过一系列运算之后,最终得到一个属性值

这个动态计算出来的属性值可以被模板结构或 methods 方法使用。

  • 计算属性的特点
    • 虽然计算属性在声明的时候被定义为方法,但是计算属性的本质是一个属性
    • 计算属性会缓存计算的结果,只有计算属性依赖的数据变化时,才会重新进行运算

代码示例

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./lib/vue-2.6.12.js"></script>
<style>
.box {
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
</style>
</head>

<body>
<div id="app">
<div>
<span>R: </span>
<input type="text" v-model.number="r">
</div>
<div>
<span>G: </span>
<input type="text" v-model.number="g">
</div>
<div>
<span>B: </span>
<input type="text" v-model.number="b">
</div>
<hr>

<!-- 专门用户呈现颜色的 div 盒子 -->
<div class="box" :style="{ backgroundColor: rgb }">
{{ rgb }}
</div>
<button @click="show">按钮</button>
</div>

<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
// 红色
r: 0,
// 绿色
g: 0,
// 蓝色
b: 0
},
methods: {
// 点击按钮,在终端显示最新的颜色
show() {
// 这里的rgb看成vm上的一个属性
console.log(this.rgb)
}
},
// 所有的计算属性都要定义在computed节点里
computed: {
// 计算属性要定义成函数形式,但在使用时作为vm上的一个属性
rgb() {
// 返回 rgb(x,x,x)的字符串
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
});
</script>
</body>
</html>

优点

  • 实现了代码的复用
  • 只要依赖的数据源发生改变时,计算属性会自动重新求值

2.7 vue-cli

2.7.1 概念

单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成。

vue-cli 是 Vue.js 开发的标准工具。它简化了程序员基于 webpack 创建工程化的 Vue 项目的过程。

引用自 vue-cli 官网上的一句话:程序员可以专注在撰写应用上,而不必花好几天去纠结 webpack 配置的问题。

2.7.2 安装和使用

vue-cli 是 npm 上的一个全局包,使用 npm install 命令,即可方便的把它安装到自己的电脑上:

1
npm install -g @vue/cli

安装完成后,输入以下命令查看vue版本号:

1
vue -V

基于 vue-cli 快速生成工程化的 Vue 项目:

1
vue create 项目的名称

image-20220520132142089

2.7.3 目录结构

image-20220520133715004

src的目录构成:

  • assets:存放项目的静态资源,例如css样式表,图片资源
  • components:存放封装的可复用的组件
  • main.js:项目的入口文件,整个项目的执行需要先执行main.js
  • App.vue:项目的根组件

2.7.4 运行流程

在工程化的项目中,vue 要做的事情很单纯:通过 main.js 把 App.vue 渲染到 index.html 的指定区域中。

其中:

  • App.vue 用来编写待渲染的模板结构
1
2
3
<template>
<h1>Hello World!</h1>
</template>
  • index.html 中需要预留一个 el 区域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 待渲染的位置 -->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

  • main.js 把 App.vue 渲染到了 index.html 所预留的区域中(直接替换掉)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 导入vue的包,得到vue构造函数
import Vue from 'vue'
// 导入App.vue组件,将来要把组件中的模板结构渲染到html中
import App from './App.vue'

Vue.config.productionTip = false

// 创建vue的实例对象
new Vue({
el: "#app",
// 把render函数指定的组件渲染(替换)到html页面中
render: h => h(App),
})

// 另一种渲染的指定方式,作用和el属性一致
new Vue({
render: h => h(App),
}).$mount('#app')

渲染结果:

image-20220520135259662

发现:原来index.html中预留的待渲染位置<div id="app"></div>已被替换为了App.vue中的整个模板结构<h1>Hello World!</h1>,二者都是main.js中render函数的功劳。

2.8 vue组件

2.8.1 组件化开发

组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护。

vue 是一个支持组件化开发的前端框架。

vue 中规定:组件的后缀名.vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。

2.8.2 组件组成部分

每个 .vue 组件都由 3 部分构成,分别是:

  • template -> 组件的模板结构
  • script -> 组件的 JavaScript 行为
  • style -> 组件的样式

其中,每个组件中必须包含 template 模板结构,而 script 行为和 style 样式是可选的组成部分。

代码示例

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
<template>
<div class="test-box">
<h1>Hello World! --- {{ username }}</h1>
<button @click="changeName">修改用户名</button>
</div>
</template>

<script>
// 默认导出 es6语法,这是固定写法
export default {
// 数据源
// 注意:组件内的data不能像之前那样指向对象,而是一个函数
data() {
// 这个return出去的{ }中,可以定义数据
return {
username: "Mark"
}
},
// 方法
methods: {
changeName() {
// 在组件中,this是当前组件的实例,原型是VueComponent
this.username = "Hongyi"
}
}
}
</script>

<style lang="less">
.test-box {
background-color: pink;
h1 {
color: red;
}
}
</style>

image-20220520145110543

① template

vue 规定:每个组件对应的模板结构,需要定义到 <template> 节点中。

注意:

  • template 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素
  • template 中只能包含唯一的根节点
1
2
3
<template>
// ...
</template>
② script

vue 规定:开发者可以在 <script> 节点中封装组件的 JavaScript 业务逻辑。

<script > 节点的基本结构如下:

1
2
3
4
5
6
7
<script>
// 今后,当前组件相关的data数据源,methods方法,侦听器watch,计算属性computed等
// 都需要定义在export default所导出的对象中
export default {
// ...
}
</script>

注意:vue 规定:.vue 组件中的 data 必须是一个函数,不能直接指向一个数据对象。

③ style

vue 规定:组件内的 <style> 节点是可选的,开发者可以在 <style> 节点中编写样式美化当前组件的 UI 结构。

<style> 节点的基本结构如下:

1
2
3
4
5
<style>
h1 {
color: red;
}
</style>

<style> 标签上添加 lang="less" 属性,即可使用 less 语法编写组件的样式。

2.8.3 组件父子关系

组件在被封装好之后,彼此之间是相互独立的,不存在父子关系 :

image-20220520145302459

在使用组件的时候,根据彼此的嵌套关系,形成了父子关系、兄弟关系:

image-20220520145313313

2.8.4 使用组件的三个步骤

在App.vue组件内:

  • 步骤1:使用 import 语法导入需要的组件
1
import Left from '@/components/Left.vue'

这里的@/就是./src/的别名

  • 步骤2:使用 components 节点注册组件
1
2
3
4
5
export default {
components: {
Left
}
}
  • 步骤3:以标签形式使用刚才注册的组件
1
2
3
<div class="box">
<Left></Left>
</div>

代码示例

  • 根组件App.vue
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
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<div class="box">
<!-- 渲染 Left 组件 -->
<!-- 3. 以标签形式,使用注册好的组件 -->
<Left></Left>
<Right></Right>
</div>
</div>
</template>

<script>
// 1.导入需要使用的 .vue 组件
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'

export default {
// 2.注册组件
components: {
Left,
Right,
Right
}
}
</script>

<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
  • Left.vue组件(Right.vue相同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="left-container">
<h3>Left 组件</h3>
</div>
</template>

<script>
export default {}
</script>

<style lang="less" scoped>
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}
</style>

image-20220523191342738

2.8.5 私有子组件和全局组件

tips:推荐使用插件vue 3 snippets

通过 components 注册的是私有子组件。

例如:在组件 A 的 components 节点下,注册了组件 F。则组件 F 只能用在组件 A 中;不能被用在组件 C 中。

如果某个组件被其他多个组件频繁地用到,则可以将这个组件注册为全局组件。在 vue 项目的 main.js 入口文件中,通过 Vue.component() 方法,可以注册全局组件

代码示例

  • Count.vue组件。该组件被Left和Right组件都要使用。此时将其注册为全局组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<h5>Count组件</h5>
</div>
</template>

<script>
export default {

}
</script>

<style lang="less">

</style>
  • main.js入口文件中注册为全局组件
1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'
// 导入需要被全局注册的组件
import Count from '@/components/Count.vue'

// 参数一:自定义全局组件的组件名称
// 参数二:需要被注册为全局组件的组件
Vue.component('MyCount', Count)
Vue.config.productionTip = false

new Vue({
render: h => h(App),
}).$mount('#app')
  • Left组件直接使用即可
1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr>
<MyCount></MyCount>
</div>
</template>

<script>
export default {}
</script>

image-20220523192815106

2.8.6 组件的props

① 基本使用

props 是组件的自定义属性,在封装通用组件的时候,合理地使用 props 可以极大的提高组件的复用性!

代码示例

需求:左右两个组件要各自自定义count的初始值。

  • count组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h5>Count组件</h5>
<p>count的值是: {{ init }}</p>
<button @click="count += 1">+1</button>
</div>
</template>

<script>
export default {
// 组件的自定义属性
props: ['init'],
data() {
return {
count: 0
}
}
}
</script>
  • 左组件
1
2
3
4
5
6
7
8
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr>
<!-- 初始化props -->
<MyCount :init="9"></MyCount>
</div>
</template>
② props是只读的

vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改 props 的值,否则会直接报错。

要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的。

  • count组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h5>Count组件</h5>
<p>count的值是: {{ count }}</p>
<button @click="count += 1">+1</button>
</div>
</template>

<script>
export default {
// 组件的自定义属性
props: ['init'],
data() {
return {
// 使用组件里的自定义属性
count: this.init
}
}
}
</script>
③ default默认值

在声明自定义属性时,可以通过 default 来定义属性的默认值。

  • count组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default {
// 对象形式的props,这样更加灵活和扩展性强
props: {
init: {
// 用default属性定义属性的默认值
default: 0
}
},
data() {
return {
count: this.init
}
}
}
</script>
  • 未给出init初始值的left组件
1
2
3
4
5
6
7
8
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr>
<!-- 用户没有确定init的初始值 -->
<MyCount></MyCount>
</div>
</template>
④ type值类型

在声明自定义属性时,可以通过 type 来定义属性的值类型。

1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
props: {
init: {
default: 0,
// 如果传递来的数据不符合该类型,则会在终端报错
type: Number
}
}
}
</script>
⑤ required 必填项

在声明自定义属性时,可以通过 required 选项,将属性设置为必填项,强制用户必须传递属性的值。

1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
props: {
init: {
default: 0,
type: Number,
required: true
}
}
}
</script>

2.8.7 组件间的样式冲突

① scoped属性

默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。

导致组件之间样式冲突的根本原因是:

  • 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
  • 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素

解决方法1

为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。

代码:略

解决方法2

为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题。

实质:为组件内的各元素分配了唯一的属性,即解决方法1

1
2
3
4
5
6
7
8
9
10
11
12
<style lang="less" scoped>
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}

h3 {
color: red;
}
</style>

image-20220523201340383

② deep样式穿透

如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style lang="less" scoped>
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}

h3 {
color: red;
}

/* 只修改left组件下的count子组件的标题h5 */
/deep/ h5 {
color: pink;
}
</style>

image-20220523201548117

3 生命周期和数据共享

3.1 生命周期概念

生命周期(Life Cycle)是指一个组件从创建 -> 运行 -> 销毁的整个阶段,强调的是一个时间段。

生命周期函数:是由 vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。

注意:生命周期强调的是时间段,生命周期函数强调的是时间点。

组件生命周期函数的分类

image-20220524183813901

生命周期图示

image-20220524184138735

代码准备

创建Test组件,并在根组件App中使用

3.2 生命周期函数

3.2.1 beforeCreate和created

beforeCreate

image-20220524195114781

这个函数使用的很少。

  • Test组件
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
<template>
<div class="test-container">
<h3>Test.vue组件</h3>
</div>
</template>

<script>
export default {
props: ['info'],
data() {
return {
message: 'hello world'
}
},
methods: {
show() {
console.log('调用了Test组件的show方法');
}
},
// 创建阶段的第一个生命周期函数
beforeCreate() {
console.log(this.info);
console.log(this.message);
this.show();
},
}
</script>

<style lang="less" scoped>
.test-container {
background-color: pink;
height: 200px;
}
</style>
  • 根组件
1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="app-container">
<h1>App根组件</h1>
<Test info="你好"></Test>
<hr />
<div class="box">
<Left></Left>
<Right></Right>
</div>
</div>
</template>

运行后报错:

image-20220524195228531

created

image-20220524195252195

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
<script>
export default {
props: ['info'],
data() {
return {
message: 'hello world'
}
},
methods: {
show() {
console.log('调用了Test组件的show方法');
}
},
// 创建阶段的第一个生命周期函数
beforeCreate() {
// 执行以下语句会报错
// console.log(this.info);
// console.log(this.message);
// this.show();
},
// 创建阶段的第二个生命周期函数
created() {
console.log(this.info);
console.log(this.message);
this.show();
},
}
</script>

打印结果:

1
2
3
你好
hello world
调用了Test组件的show方法

该函数可用于在页面尚未渲染时,发送ajax请求提前获取数据,之后就可以将数据渲染在页面中。注意,该函数内部尚不能操作DOM,因为页面尚未被渲染。例如:

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
<template>
<div class="test-container">
<h3>Test.vue组件 --- 一共有 {{ books.length }} 本图书</h3>
</div>
</template>

<script>
export default {
props: ['info'],
data() {
return {
message: 'hello world',
// 定义一个接收图书的数组
books: []
}
},
methods: {
show() {
console.log('调用了Test组件的show方法');
},
// 使用ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
const res = JSON.parse(xhr.responseText);
console.log(res);
this.books = res.data;
});
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks');
xhr.send();
}
},
created() {
// 经常使用该函数调用methods中的方法,向服务器请求数据
// 并将请求到的数据转存到数据源data中,供template模板渲染使用
this.initBookList();
},
}
</script>

image-20220524200529841

3.2.2 beforeMount和mounted

在此之前,已在内存中编译生成了HTML结构。

beforeMount

image-20220524200954706

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="test-container">
<h3 id="mark">Test.vue组件</h3>
</div>
</template>

<script>
export default {
beforeMount() {
const dom = document.querySelector('mark');
console.log(dom);
}
}
</script>

打印结果:

1
null

说明该生命周期函数不能操作DOM。该函数并不常用。

mounted

image-20220524201536925

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="test-container">
<h3 id="mark">Test.vue组件</h3>
</div>
</template>

<script>
export default {
mounted() {
const dom = document.querySelector('mark');
console.log(dom);
}
}
</script>

打印结果:

1
<h3 data-v-dc87507c="" id="#mark">Test.vue组件 --- 一共有 8 本图书</h3>

3.2.3 beforeUpdate和updated

该生命周期函数位于组件运行阶段执行。

beforeUpdate

image-20220525033414419

此时数据变化了,但是页面还未重新渲染。

updated

image-20220525033642260

此时页面已经重新渲染。

3.2.4 beforeDestroy和destroyed

略。

3.3 数据共享

3.3.1 概念

在项目开发中,组件之间的最常见的关系分为如下两种:

  • 父子关系
  • 兄弟关系

image-20220525034025549

父子组件之间的数据共享又分为:

  • 父 -> 子共享数据
  • 子 -> 父共享数据

3.3.2 父向子共享数据

父组件向子组件共享数据时,子组件需要使用自定义属性props来接收父组件的数据。

代码示例

  • 父组件
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
<template>
<div class="app-container">
<h1>App根组件</h1>
<hr>
<!-- 注意这里使用了属性绑定 -->
<Left :msg="message" :user="userinfo"></Left>
</div>
</template>

<script>
import Left from '@/components/Left.vue'

export default {
components: {
Left
},
data() {
return {
message: 'hello world',
userinfo: {
name: 'Mark',
age: 24
}
}
}
}
</script>
  • 子组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="left-container">
<h3>Left 组件</h3>
<p>msg的值是: {{ msg }}</p>
<p>user的值是: {{ user }}</p>
</div>
</template>

<script>
export default {
// 用于接收父组件传过来的数据
props: ['msg', 'user']
}
</script>

image-20220525035627462

3.3.3 子向父共享数据

子组件向父组件共享数据时,父组件需要使用自定义事件

代码示例

  • 子组件(右组件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="right-container">
<h3>Right 组件 -- {{count}}</h3>
<button @click="add">+1</button>
</div>
</template>

<script>
export default {
data() {
return {
// 子组件自己的数据,把该值传给父组件
count: 0
}
},
methods: {
add() {
this.count += 1;
// 修改数据时,通过$emit()触发自定义事件,将结果数据传递过去
this.$emit('numchange', this.count);
}
},
}
</script>
  • 父组件
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
<template>
<div class="app-container">
<h1>App根组件 --- {{ countFromSon }}</h1>
<hr>
<div class="box">
<Left :msg="message" :user="userinfo"></Left>
<!-- 绑定自定义事件 -->
<Right @numchange="getNewCount"></Right>
</div>
</div>
</template>

<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'

export default {
components: {
Left,
Right
},
methods: {
// 自定义事件处理函数
getNewCount(val) {
this.countFromSon = val;
}
},
}
</script>

image-20220525041432273

3.3.4 兄弟组件间共享数据

vue2.x 中,兄弟组件之间数据共享的方案是 EventBus

使用步骤

  1. 创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象
  2. 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件
  3. 在数据接收方,调用 bus.$on('事件名称', 事件处理函数) 方法注册一个自定义事件

image-20220525041509313

代码示例

  • evnetBus.js
1
2
import Vue from 'vue'
export default new Vue()
  • 左组件(数据发送方)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="left-container">
<h3>Left 组件</h3>
<button @click="send">将str发给Right</button>
</div>
</template>

<script>
import bus from './eventBus.js'
export default {
data() {
return {
// 向右组件发送的数据
str: 'Hello Vue.js!'
}
},
methods: {
send() {
// 通过eventBus发送数据
bus.$emit('share', this.str);
}
},
}
</script>
  • 右组件
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
<template>
<div class="right-container">
<h3>Right 组件 -- {{count}}</h3>
<p>接收到左组件的str: {{ msgFromLeft }}</p>
</div>
</template>

<script>
import bus from './eventBus.js'
export default {
data() {
return {
// 定义要接受左组件的数据
msgFromLeft: ''
}
},
methods: {
add() {
this.count += 1;
// 修改数据时,通过$emit()触发自定义事件,将结果数据传递过去
this.$emit('numchange', this.count);
}
},
created() {
// 为bus绑定自定义事件
bus.$on('share', str => {
this.msgFromLeft = str;
});
},
}
</script>

image-20220525042824542

3.4 ref引用

3.4.1 概念

ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。

每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="app-container">
<h1>App根组件 --- {{ countFromSon }}</h1>
<button @click="getRef">打印this</button>
</div>
</template>

<script>
export default {
methods: {
getRef() {
// this指向当前组件的实例对象,this.$ref默认指向空对象
console.log(this);
}
},
}
</script>

image-20220525043652626

3.4.2 使用ref引用DOM元素

如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="app-container">
<!-- 使用ref属性,为对应的DOM元素添加引用名称 -->
<!-- 注意ref属性不要重复,否则后者会覆盖前者 -->
<h1 ref="mark">App根组件 --- {{ countFromSon }}</h1>
<button @click="getRef">打印this</button>
</div>
</template>

<script>
export default {
methods: {
getRef() {
// this指向当前组件的实例对象,this.$ref默认指向空对象
console.log(this);
// 通过this.$refs引用的名称,可以获取到DOM元素
console.log(this.$refs.mark);
// 操作DOM元素
this.$refs.mark.style.color = 'red';
}
}
}
</script>

image-20220525044304724

3.4.3 使用 ref 引用组件实例

如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 使用ref属性,为组件添加引用名称 -->
<Left ref="counterRef"></Left>
</template>

<script>
export default {
methods: {
gerRef() {
// 引用到组件的实例后,就可以调用该组件上的methods中的方法
// 例如假设Left组件的methods中有add方法
this.$ref.counter.add();
}
}
}
</script>

3.5 购物车案例

后端传过来的数据格式:

image-20220527150324473

代码略,详见代码仓库

4 动态组件&插槽&自定义指令

4.1 动态组件

4.1.1 基本使用

动态组件指的是动态切换组件的显示与隐藏。

vue 提供了一个内置的 <component> 组件,专门用来实现动态组件的渲染。

代码示例1——基本使用

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
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 1.component是vue内置的 -->
<!-- 2.is属性的值,是要渲染的组件的名字 -->
<component :is="comName"></component>
</div>
</div>
</template>

<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'

export default {
data() {
return {
// comName表示要展示的组件的名字
comName: "Left"
}
},
components: {
Left,
Right
}
}
</script>

代码示例2——动态展示组件

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
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<button @click="comName = 'Left'">展示Left</button>
<button @click="comName = 'Right'">展示Right</button>
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 1.component是vue内置的 -->
<!-- 2.is属性的值,是要渲染的组件的名字 -->
<component :is="comName"></component>
</div>
</div>
</template>

<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'

export default {
data() {
return {
// comName表示要展示的组件的名字
comName: "Left"
}
},
components: {
Left,
Right
}
}
</script>

4.1.2 keep-alive

① 基本使用

默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 <keep-alive> 组件保持动态组件的状态。

keep-alive可以把包裹的内部的组件进行缓存,而不是将组件进行销毁。

1
2
3
<keep-alive>
<component :is="comName"></component>
</keep-alive>

image-20220529160103336

② keep-alive对应的生命周期函数

当组件被缓存时,会自动触发组件的 deactivated 生命周期函数。

当组件被激活时,会自动触发组件的 activated 生命周期函数。

当组件被第一次创建的时候,会执行created函数,也会执行activated函数。当组件被激活的时候,不再会执行前者,而只执行后者,因为该组件不是被创建出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="left-container">
<h3>Left 组件 --- {{ count }}</h3>
<button @click="count += 1">+1</button>
</div>
</template>

<script>
export default {
data() {
return {
count: 0
}
},
activated() {
console.log("组件被激活了");
},
deactivated() {
console.log("组件被缓存了");
},
}
</script>
③ include属性

include 属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔。

1
2
3
<keep-alive include="Left,Right">
<component :is="comName"></component>
</keep-alive>

image-20220529160830491

4.2 插槽

4.2.1 基本使用

插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

image-20220529160928461

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。

代码示例

在封装组件时,可以通过 <slot> 元素定义插槽,从而为用户预留内容占位符。

  • 根组件
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<div class="box">
<Left>
<!-- 默认情况下,提供的内容都会被填充到default的插槽中 -->
<p>这是在Left组件的内容区域声明的p标签</p>
</Left>
</div>
</div>
</template>
  • 左组件
1
2
3
4
5
6
7
8
9
10
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr>
<!-- 声明一个插槽区域 -->
<!-- vue官方规定,每一个插槽都要有一个名称name -->
<!-- 默认为default -->
<slot name="default"></slot>
</div>
</template>

image-20220529162531291

  • 如果在封装组件时没有预留任何 <slot> 插槽,则用户提供的任何自定义内容都会被丢弃。
  • 封装组件时,可以为预留的 <slot> 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。
1
2
3
4
5
<slot name="default">
<h6>
这是default插槽的默认内容
</h6>
</slot>

4.2.2 具名插槽

如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot> 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”。

注意:没有指定 name 名称的插槽,会有隐含的名称叫做 “default”。

  • 在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称。
    • 此时的template的作用仅仅起一个包裹的作用,不会渲染成真实的元素
    • v-slot不能用在元素上,只能用在template
  • 跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header可以被重写为 #header

代码示例1——v-slot指令

1
<slot name="default"></slot>
1
2
3
4
5
6
<Left>
<!-- 将内容放在名称为default的插槽中 -->
<template v-slot:default>
<p>这是在Left组件的内容区域声明的p标签</p>
</template>
</Left>
  • 简写形式
1
2
3
4
5
6
<Left>
<!-- 将内容放在名称为default的插槽中 -->
<template #default>
<p>这是在Left组件的内容区域声明的p标签</p>
</template>
</Left>

代码示例2——具名插槽使用

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
<template>
<div class="article-container">
<!-- 文章标题 -->
<div class="header-box">
<slot name="title"></slot>
</div>
<!-- 文章内容 -->
<div class="content-box">
<slot name="content"></slot>
</div>
<!-- 文章作者 -->
<div class="footer-box">
<slot name="author"></slot>
</div>
</div>
</template>

<script>
export default {
// 组件的名称
name: "Article"
}
</script>

<style lang="less" scoped>
.article-container{
> div {
min-height: 150px;
}
.header-box{
background-color: pink;
}

.content-box{
background-color: lightblue;
}

.footer-box{
background-color: lightsalmon;
}
}
</style>
  • 根组件
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
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr>
<Article>
<template #title>
<h3>一首诗</h3>
</template>

<template #content>
<div>
<p>大海全是水</p>
<p>漫展全是腿</p>
</div>
</template>

<template #author>
<div>
作者:佚名
</div>
</template>
</Article>
<hr>
</div>
</template>

<script>
import Article from '@/components/Article.vue'

export default {
data() {
return {

}
},
components: {
Article
}
}
</script>

image-20220529165839796

4.2.3 作用域插槽

在封装组件的过程中,可以为预留的 <slot> 插槽绑定 props 数据,这种带有 props 数据的 <slot> 叫做“作用域插槽”。

代码示例1——基本使用

1
2
3
4
5
<div class="content-box">
<!-- 绑定自定义属性 -->
<!-- 此时的插槽又称为作用域插槽 -->
<slot name="content" msg="hello vue.js"></slot>
</div>
1
2
3
4
5
6
7
8
<!-- 接收作用域插槽的数据 -->
<template #content="scope">
<div>
<p>大海全是水</p>
<p>漫展全是腿</p>
<p>{{ scope.msg }}</p>
</div>
</template>

相当于子组件向父组件传递数据。

代码示例2——解构插槽

作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。

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
<template>
<div class="article-container">
<!-- 文章标题 -->
<div class="header-box">
<slot name="title"></slot>
</div>
<!-- 文章内容 -->
<div class="content-box">
<!-- 绑定自定义属性 -->
<!-- 此时的插槽又称为作用域插槽 -->
<slot name="content" msg="hello vue.js" :user="userinfo"></slot>
</div>
<!-- 文章作者 -->
<div class="footer-box">
<slot name="author"></slot>
</div>
</div>
</template>

<script>
export default {
// 组件的名称
name: "Article",
data() {
return {
// 用户的信息对象
userinfo: {
name: "Mark",
age: 24
}
}
},
}
</script>
1
2
3
4
5
6
7
8
9
10
<!-- 接收作用域插槽的数据:解构赋值的形式 -->
<template #content="{msg,user}">
<div>
<p>大海全是水</p>
<p>漫展全是腿</p>
<p>{{ msg }}</p>
<p>{{ user.name }}</p>
<p>{{ user.age }}</p>
</div>
</template>

4.3 自定义指令

4.3.1 概念

vue 官方提供了 v-text、v-for、v-model、v-if 等常用的指令。除此之外 vue 还允许开发者自定义指令。

vue 中的自定义指令分为两类,分别是:

  • 私有自定义指令
  • 全局自定义指令

4.3.2 私有自定义指令

① 基本使用

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。 该指令只能在声明的组件中使用,其他组件不能使用。

在使用自定义指令时,需要加上 v- 前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="app-container">
<h1 v-color>App 根组件</h1>
</template>

<script>

export default {
// 私有自定义指令的节点
directives: {
// 定义名为color的指令
color: {
// 当指令第一次被绑定到元素上的时候,会立即触发bind函数
// 形参中的el表示当前指令所绑定到的那个DOM对象
bind(el) {
el.style.color = "blue";
}
}
}
}
</script>
② 动态绑定参数值

在 template 结构中使用自定义指令时,可以通过等号(=)的方式,为当前指令动态绑定参数值。

在声明自定义指令时,可以通过形参中的第二个参数(一般命名为binding),来接收指令的参数值。

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
<template>
<div class="app-container">
<h1 v-color="color">App 根组件</h1>
<p v-color="'red'">测试</p>
<hr>
</div>
</template>

<script>
export default {
data() {
return {
color: "green"
}
},
// 私有自定义指令的节点
directives: {
color: {
bind(el, binding) {
console.log(binding)
el.style.color = binding.value;
}
}
}
}
</script>

打印的内容(binding对象):

image-20220530204603384

③ update函数

bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发

update 函数会在每次 DOM 更新时被调用。

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
<template>
<div class="app-container">
<h1 v-color="color">App 根组件</h1>
<p v-color="'red'">测试</p>
<button @click="color = 'pink'">改变color的颜色</button>
<hr>
</div>
</template>

<script>
export default {
data() {
return {
color: "green"
}
},
// 私有自定义指令的节点
directives: {
color: {
bind(el, binding) {
el.style.color = binding.value;
},
// 每次DOM更新都会被触发
update(el,binding ) {
el.style.color = binding.value;
}
}
}
}
</script>

简写形式

如果 bind 和update 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式。

1
2
3
4
5
6
7
8
9
10
<script>
export default {
// 私有自定义指令的节点
directives: {
color(el, binding) {
el.style.color = binding.value;
}
}
}
</script>

4.3.3 全局自定义指令

全局共享的自定义指令需要通过Vue.directive()main.js中进行声明。

1
2
3
Vue.directive('color', function(el, binding) {
el.style.color = binding.value;
})

使用时,也需要加上v-前缀。

5 路由

5.1 概念

路由(英文:router)就是对应关系。

SPA(单页面应用程序) 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。

结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!

前端路由

通俗易懂的概念:Hash 地址与组件之间的对应关系。

  • 工作方式:
    • 用户点击了页面上的路由链接
    • 导致了 URL 地址栏中的 Hash 值发生了变化
    • 前端路由监听了到 Hash 地址的变化
    • 前端路由把当前 Hash 地址对应的组件渲染都浏览器中

image-20220530210538124

实现简易的前端路由

5.2 vue-router的基本使用

5.2.1 概念

vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。

5.2.2 安装和配置

  • 安装 vue-router 包
1
npm i vue-router@3.5.2 -S
  • 创建路由模块:在 src 源代码目录下,新建 router/index.js 路由模块,并初始化如下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 路由模块

// 1.导入Vue和VueRouter的包
import Vue from "vue"
import VueRouter from "vue-router"

// 2.调用vue.use()函数,把VueRouter安装为Vue的插件
Vue.use(VueRouter)

// 3.创建路由的实例对象
const router = new VueRouter()

// 4.向外共享路由的实例对象
export default router
  • 导入并挂载路由模块:在 src/main.js 入口文件中,导入并挂载路由模块。
1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App.vue'
// 1.导入路由模块
import router from "@/router"

Vue.config.productionTip = false

new Vue({
render: h => h(App),
// 2.挂载路由模块
router: router
}).$mount('#app')

5.2.3 使用

  • 声明路由链接和占位符:在 src/App.vue 组件中,使用 vue-router 提供的<router-view> 声明路由占位符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="app-container">
<h1>App根组件</h1>
<a href="#/home">首页</a>
<a href="#/movie">电影</a>
<a href="#/about">关于</a>
<hr>
<!-- 定义路由链接,作用:占位符 -->
<router-view></router-view>
</div>
</template>

<script>

export default {
name: "App"
}
</script>
  • 声明路由的匹配规则:在 src/router/index.js 路由模块中,通过 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
// 路由模块

// 1.导入Vue和VueRouter的包
import Vue from "vue"
import VueRouter from "vue-router"

// 导入需要的组件
import Home from "@/components/Home.vue"
import Movie from "@/components/Movie.vue"
import About from "@/components/About.vue"

// 2.调用vue.use()函数,把VueRouter安装为Vue的插件
Vue.use(VueRouter)

// 3.创建路由的实例对象
const router = new VueRouter({
// routes是数组,用于定义hash地址与组件之间的对应关系
routes: [
// # 可以省略
{ path: "/home", component: Home }, // 路由规则
{ path: "/movie", component: Movie },
{ path: "/about", component: About }
]
})

// 4.向外共享路由的实例对象
export default router

用路由链接<router-link>代替a标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="app-container">
<h1>App根组件</h1>
<!-- <a href="#/home">首页</a>
<a href="#/movie">电影</a>
<a href="#/about">关于</a> -->

<!-- 可以省略 # -->
<router-link to="home">首页</router-link>
<router-link to="movie">电影</router-link>
<router-link to="about">关于</router-link>
<hr>
<!-- 定义路由链接,作用:占位符 -->
<router-view></router-view>
</div>
</template>

<script>

export default {
name: "App"
}
</script>

image-20220531173523836

5.3 vue-router的常见使用

5.3.1 路由重定向

路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。

通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向。

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [
// 重定向
{ path: "/" , redirect: "/home"},
{ path: "/home", component: Home },
{ path: "/movie", component: Movie },
{ path: "/about", component: About }
]
});

5.3.2 嵌套路由

① 概念

通过路由实现组件的嵌套展示,叫做嵌套路由。

  • 点击父级路由链接显示模板内容

点击父级路由链接显示模板内容

  • 模板内容中又有子级路由链接,点击子级路由链接显示子级模板内容

image-20220531174031915

② 子路由链接和占位符

About.vue 组件中,声明 tab1tab2 的子路由链接以及子路由占位符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="about-container">
<h3>About 组件</h3>
<!-- 子级路由链接 -->
<router-link to="/about/tab1">tab1</router-link>
<router-link to="/about/tab2">tab2</router-link>
<hr />
<!-- 子级路由占位符 -->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'About'
}
</script>
③ 子路由规则

src/router/index.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则。

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
// 1.导入Vue和VueRouter的包
import Vue from "vue"
import VueRouter from "vue-router"

// 导入需要的组件
import Home from "@/components/Home.vue"
import Movie from "@/components/Movie.vue"
import About from "@/components/About.vue"
import Tab1 from "@/components/tabs/Tab1.vue"
import Tab2 from "@/components/tabs/Tab2.vue"

// 2.调用vue.use()函数,把VueRouter安装为Vue的插件
Vue.use(VueRouter)

// 3.创建路由的实例对象
const router = new VueRouter({
// routes是数组,用于定义hash地址与组件之间的对应关系
routes: [
// # 可以省略
{ path: "/" , redirect: "/home"},
{ path: "/home", component: Home },
{ path: "/movie", component: Movie },
{
path: "/about", // 父级路由规则
component: About,
// 通过children属性嵌套声明子级路由规则
children: [
{ path: "tab1", component: Tab1 },
{ path: "tab2", component: Tab2 }
]
}
]
})

// 4.向外共享路由的实例对象
export default router

image-20220531175512017

默认子路由

如果children数组中,某个路由规则的path值为空字符串,则这条路由规则叫做默认子路由。

5.3.3 动态匹配路由

① 概念

问题的提出

有如下 3 个路由链接:

1
2
3
<router-link to="/movie/1">电影1</router-link>
<router-link to="/movie/2">电影2</router-link>
<router-link to="/movie/3">电影3</router-link>

定义如下 3 个路由规则,是否可行?

1
2
3
{path: "/movie/1", component: Movie}
{path: "/movie/2", component: Movie}
{path: "/movie/3", component: Movie}

缺点:路由规则的复用性差。

动态路由概念

动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。

在 vue-router 中使用英文的冒号(:)来定义路由的参数项。

1
{path: "/movie/:id", component: Movie}
② $route.params 参数对象

在动态路由渲染出来的组件中,可以使用 this.$route.params 对象访问到动态匹配的参数值。不常用。

代码示例

  • 根组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="app-container">
<h1>App根组件</h1>
<router-link to="/home">首页</router-link>
<router-link to="/movie/1">洛基</router-link>
<router-link to="/movie/2">雷神</router-link>
<router-link to="/movie/3">复联</router-link>
<router-link to="/about">关于</router-link>
<hr>
<!-- 定义路由链接,作用:占位符 -->
<router-view></router-view>
</div>
</template>

<script>

export default {
name: "App"
}
</script>
  • 路由模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建路由的实例对象
const router = new VueRouter({
routes: [
{ path: "/" , redirect: "/home"},
{ path: "/home", component: Home },
// 在Movie组件中,希望根据id的值来展示对应电影的详情信息
{ path: "/movie/:id", component: Movie },
{
path: "/about",
component: About,
children: [
{ path: "tab1", component: Tab1 },
{ path: "tab2", component: Tab2 }
]
}
]
})
  • Movie组件
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
<template>
<div class="movie-container">
<!-- this.$route是路由的“参数对象” -->
<!-- this.$router是路由的“导航对象” -->
<h3>Movie 组件 --- {{ $route.params.id }}</h3>
<button @click="showThis">打印 this</button>
</div>
</template>

<script>
export default {
name: 'Movie',
methods: {
showThis() {
console.log(this)
}
}
}
</script>

<style lang="less" scoped>
.movie-container {
min-height: 200px;
background-color: lightsalmon;
padding: 15px;
}
</style>
③ 使用 props 接收路由参数

为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参。常用。

  • 路由模块
1
2
3
4
5
6
7
8
const router = new VueRouter({
routes: [
{ path: "/" , redirect: "/home"},
{ path: "/home", component: Home },
// 开启props传参选项
{ path: "/movie/:id", component: Movie, props: true }
]
})
  • Movie组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="movie-container">
<!-- this.$route是路由的“参数对象” -->
<!-- this.$router是路由的“导航对象” -->
<h3>Movie 组件 --- {{ $route.params.id }} --- {{ id }}</h3>
<button @click="showThis">打印 this</button>
</div>
</template>

<script>
export default {
name: 'Movie',
// 接收props数据
props: ["id"],
methods: {
showThis() {
console.log(this)
}
}
}
</script>

注意:例如/movie/1?name=zs&age=24

  • 在hash地址中,/后面的参数项叫做“路径参数”
    • 在路由参数对象中,需要使用this.$route.params来访问路径参数
  • 在hash地址中,?后面的参数项叫做“查询参数”
    • 在路由参数对象中,需要使用this.$route.query来访问查询参数

5.3.4 导航

① 概述

在浏览器中,点击链接实现导航的方式,叫做声明式导航。例如:普通网页中点击 <a>链接、vue 项目中点击 <router-link> 都属于声明式导航。

在浏览器中,调用 API 方法实现导航的方式,叫做编程式导航。例如:普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航。

vue-router 提供了许多编程式导航的 API,其中最常用的导航 API 分别是:

  • this.$router.push('hash 地址')

    • 跳转到指定 hash 地址,并增加一条历史记录
  • this.$router.replace('hash 地址')

    • 跳转到指定的 hash 地址,并替换掉当前的历史记录
  • this.$router.go(数值 n)

    • 实现导航历史前进、后退
② $router.push

调用 this.$router.push() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。

  • Home组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="home-container">
<h3>Home 组件</h3>
<hr/>
<button @click="gotoLk">通过 push 跳转到“洛基”页面</button>
</div>
</template>

<script>
export default {
name: 'Home',
methods: {
gotoLk() {
// 通过编程式导航 API,导航跳转到指定的页面
this.$router.push('/movie/1')
}
}
}
</script>
③ $router.replace

调用 this.$router.replace() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。

  • Home组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="home-container">
<h3>Home 组件</h3>
<hr />
<button @click="gotoLk">通过 push 跳转到“洛基”页面</button>
<button @click="gotoLk2">通过 replace 跳转到“洛基”页面</button>
</div>
</template>

<script>
export default {
name: 'Home',
methods: {
gotoLk() {
// 通过编程式导航 API,导航跳转到指定的页面
this.$router.push('/movie/1')
},
gotoLk2() {
this.$router.replace('/movie/1')
}
}
}
</script>
④ $router.go

调用 this.$router.go() 方法,可以在浏览历史中前进和后退。

  • Movie组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="movie-container">
<h3>Movie 组件 --- {{ $route.params.id }} --- {{ id }}</h3>
<button @click="goback">后退</button>
</div>
</template>

<script>
export default {
name: 'Movie',
// 接收props数据
props: ["id"],
methods: {
goback() {
// 负数表示后退几次
// 正数表示前进几次
this.$router.go(-1)
}
}
}
</script>

简化用法

在实际开发中,一般只会前进和后退一层页面。因此 vue-router 提供了如下两个便捷方法:

  • $router.back()

    • 在历史记录中,后退到上一个页面
  • $router.forward()

    • 在历史记录中,前进到下一个页面
1
2
<button @click="this.$router.back()">后退</button>
<button @click="this.$router.forward()">前进</button>

5.3.5 导航守卫

① 概念

导航守卫可以控制路由的访问权限。

image-20220602165810941

② 全局前置守卫

每次发生路由的导航跳转时,都会触发全局前置守卫。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 路由模块
// 1.导入Vue和VueRouter的包
import Vue from "vue"
import VueRouter from "vue-router"

// 导入需要的组件

// 2.调用vue.use()函数,把VueRouter安装为Vue的插件
Vue.use(VueRouter)

// 3.创建路由的实例对象
const router = new VueRouter({
// ...
})

// 声明全局前置守卫
// 每次发生路由导航跳转时,都会触发fn这个回调函数
router.beforeEach(() => {

})

全局前置守卫的回调函数中接收 3 个形参,格式为:

1
2
3
4
5
6
7
8
// 声明全局前置守卫
// 每次发生路由导航跳转时,都会触发这个回调函数
router.beforeEach((to, from, next) => {
// to是将要访问的路由的信息对象
// from 是将要离开的路由的信息对象
// next是一个函数,调用next()表示放行,允许这次路由导航
next()
})
③ next 函数的 3 种调用方式

参考示意图,分析 next 函数的 3 种调用方式最终导致的结果:

image-20220602171058990

  • 当前用户拥有后台主页的访问权限,直接放行:next()
  • 当前用户没有后台主页的访问权限,强制其跳转到登录页面:next('/login')
  • 当前用户没有后台主页的访问权限,不允许跳转到后台主页:next(false)
④ 控制后台主页的访问权限

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
router.beforeEach((to, from, next) => {
// 1.要拿到用户将要访问的hash地址
// 2.判断hash地址是否为/main
if(to.path === "/main") {
// 2.1如果等于/main,则需要登录才能访问成功
// 3.读取localStorage中的token值,如果有token则放行,否则强制跳转到login页面
const token = localStorage.getItem("token")
if(token) {
next()
} else {
next("/login")
}
} else {
// 2.2如果不等于/main,则直接放行
next()
}
})

5.4 实践案例

略,代码见代码仓库


以下学习来源:尚硅谷黑马程序员

6 vuex

6.1 概述

Vuex是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。

多组件共享数据——全局事件总线实现

image-20220606191548539

多组件共享数据——vuex实现

image-20220606191832639

  • 应用vuex的时机

    • 多个组件依赖于同一状态

    • 来自不同组件的行为需要变更同一状态


原理图:

image-20220606193910742

优点

  1. 能够在Vuex中集中管理共享的数居,易于开发和后期维护
  2. 能够高效地实现组件之间的数据共享,提高开发效率
  3. 存储在vuex中的数据都是响应式的,能够实时保持数据与页面的同步

一般情况下,只有组件之间共享的数据,才有必要存储到vuex中;对于组件中的私有数据,依旧存储在组件自身的data中即可。

6.2 基本使用

6.2.1 环境搭建

  • 安装(注意vue2对应的vuex版本为3,vue3对应的版本为4):
1
npm i vuex@3
  • 导入vuex包

    • 新建在src下新建store.js,引入vuex

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      import Vue from 'vue'
      import Vuex from 'vuex'

      Vue.use(Vuex)

      export default new Vuex.Store({
      state: {

      },
      mutations: {

      },
      actions: {

      },
      modules: {

      }
      })
    • 在全局入口文件main.js中引入store.js并挂载

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import Vue from 'vue'
      import App from './App.vue'
      import store from './store'

      Vue.config.productionTip = false

      new Vue({
      store,
      render: h => h(App),
      }).$mount('#app')

6.2.2 应用案例——准备

  • 新建两个组件,加法组件和减法组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h3>当前最新的count值为: </h3>
<button>+1</button>
</div>
</template>

<script>
export default {
data() {
return {

}
},
}
</script>
  • 根组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div id="app">
<my-addition></my-addition>
<p>------------------------------------------------</p>
<my-subtraction></my-subtraction>
</div>
</template>

<script>
import Addition from '@/components/Addition.vue'
import Subtraction from '@/components/Subtraction.vue'

export default {
name: 'App',
components: {
'my-addition': Addition,
'my-subtraction': Subtraction
}
}
</script>

运行结果

image-20220617102227758

6.3核心概念

Vuex中的主要核心概念有:

  • State
  • Mutation
  • Action
  • Getter

6.3.1 State

State提供唯一的公共数据源,所有共享的数据都要统一放到 Store的 State I中进行存储。

1
2
3
4
5
6
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
})

访问共享数据的方式

  • 方式1:通过 this.$store.state.全局数据名称 访问,由于在模板字符串中,是不需要写this的,所以直接写this后边的。
1
2
3
4
5
6
<template>
<div>
<h3>当前最新的count值为: {{ $store.state.count }}</h3>
<button>+1</button>
</div>
</template>
  • 方式2:在需要使用共享数据的组件内:

    • 按需导入mapState函数

    • 通过刚オ导入的 mapState 函数,将当前组件需要的全局数据,映射为当前组件的computed计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<h3>当前最新的count值为: {{ count }}</h3>
<button>-1</button>
</div>
</template>

<script>
import { mapState } from 'vuex'

export default {
data() {
return {

}
},
computed: {
...mapState(['count'])
},
}
</script>

6.3.2 Mutation

Mutations用于变更 Store 中的数据。只有 mutations里的函数,才有权利修改 state 的数据,此外mutations里不能包含异步操作

通过Mutation更改状态

  • 方式1:this.$store.commit()触发 mutations
1
2
3
4
5
6
7
8
9
10
11
12
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
data() {
return {

}
},
methods: {
add() {
// commit触发某个mutation
this.$store.commit('add')
}
},
}
</script>
  • 传递参数:可以在触发mutations时传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
},
addN(state, step) {
// 每次加step
state.count += step
}
}
})
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
<template>
<div>
<h3>当前最新的count值为: {{ $store.state.count }}</h3>
<button @click="add">+1</button>
<button @click="addN">+n</button>
</div>
</template>

<script>
export default {
data() {
return {

}
},
methods: {
add() {
this.$store.commit('add')
},
addN() {
// 每次加3
this.$store.commit('addN', 3)
}
},
}
</script>
  • 方式2:在需要变更状态的组件内:
    • 从vuex中按需导入 mapMutations函数
    • 通过刚才按需导入的 mapMutations函数,映射为当前组件的methods函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
},
addN(state, step) {
state.count += step
},
sub(state) {
state.count--
},
subN(state, step) {
state.count -= step
}
}
})
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
<template>
<div>
<h3>当前最新的count值为: {{ count }}</h3>
<button @click="handler1">-1</button>
<button @click="handler2">-n</button>
</div>
</template>

<script>
// 按需引入
import { mapState, mapMutations } from 'vuex'

export default {
data() {
return {

}
},
computed: {
...mapState(['count'])
},
methods: {
...mapMutations(['sub', 'subN']),
handler1() {
this.sub()
},
handler2() {
// 自减3
this.subN(3)
}
},
}
</script>

6.3.3 Action

Actions用于处理异步任务

如果通过异步操作变更数据,必须通过 Action,而不能使用Mutation,但是在 Action中还是要通过触发Mutation的方式间接变更数据。

注意: 在Actions 中不能直接修改 state中的数据,要通过 mutations修改。

不使用Action的情况

  • 需求:延时1s再加1
1
2
3
4
5
add(state) {
setTimeout(() => {
state.count++
}, 1000)
}
  • 执行情况:

image-20220617111858469

  • 分析:页面能够正常显示,但是在调试工具内发现count的值仍然为0

  • 原因:mutation内不能包含异步操作

使用Action处理异步操作

  • 方式1:使用this.$store.dispatch 触发 Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
mutations: {
add(state) {
// setTimeout(() => {
// state.count++
// }, 1000)
state.count++
}
},
actions: {
addAsync(context) {
setTimeout(() => {
// 在action中不能直接修改state
// 而是通过context.commit触发某个mutation才行
context.commit('add')
}, 1000)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<h3>当前最新的count值为: {{ $store.state.count }}</h3>
<button @click="handler1">+1 async</button>
</div>
</template>

<script>
export default {
methods: {
// 异步自增1
handler1() {
// dispatch用来触发某个action
this.$store.dispatch('addAsync')
}
},
}
</script>

执行结果:

image-20220617112829312

  • 传递参数
1
2
3
4
5
6
7
8
9
actions: {
addNAsync(context) {
setTimeout(() => {
// 在action中不能直接修改state
// 而是通过context.commit触发某个mutation才行
context.commit('addN', step)
}, 1000)
}
}
1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
methods: {
// 异步自增1
handler1() {
// dispatch用来触发某个action
this.$store.dispatch('addNAsync', 5)
}
},
}
</script>
  • 方式2:
    • 从Vuex中按需导入 mapActions 函数
    • 将指定的 actions 函数,映射为当前组件 methods 的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
mutations: {
sub(state) {
state.count--
}
},
actions: {
subAsync(context) {
setTimeout(() => {
context.commit('sub')
}, 1000)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h3>当前最新的count值为: {{ count }}</h3>
<button @click="handler3">-1</button>
</div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
methods: {
...mapActions(['subAsync'])
handler3() {
this.sub()
}
},
}
</script>

同样该方法可以传递参数

6.3.4 Getter

Getter 用于对 Store中的数据进行加工处理形成新的数据。

Getter 不会修改 Store 中的原数据,它只起到一个包装器的作用,将Store中的数据加工后输出出来。

  1. Getter可以对 Store中已有的数据加工处理之后形成新的数据,类似Vue的计算属性。
  2. Store中数据发生变化, Getter 的数据也会跟着变化。
1
2
3
4
5
6
7
8
9
10
11
export default new Vuex.Store({
state: {
// 共享数据
count: 0
},
getters: {
showNum(state) {
return '当前最新的数量为: ' + state.count
}
}
})

使用

  • 方式1:通过 this.$store.getters.名称 访问
1
<h3>{{ $store.getters.showNum }}</h3>
  • 方式2:mapGetters 映射为计算属性
1
2
3
4
5
import { mapGetters } from 'vuex'

computed:{
...mapGetters(['showNum'])
}