Vue 学习笔记
一、Vue 基础
1、Vue 简介
(1)概述
使用 vue 构建用户界面,解决了 jQuery + 模板引擎的诸多痛点,极大的提高了前端开发的效率和体验。
官方给 vue 的定位是前端框架,因为它提供了构建用户界面的一整套解决方案(俗称 vue 全家桶):
- vue(核心库)
- vue-router(路由方案)
- vuex(状态管理方案)
- vue 组件库(快速搭建页面 UI 效果的方案)
以及辅助 vue 项目开发的一系列工具:
- vue-cli( npm 全局包:一键生成工程化的 vue 项目-基于 webpack、大而全)
- vite( npm 全局包:一键生成工程化的 vue 项目-小而巧)
- vue-devtools(浏览器插件:辅助调试的工具)
- vetur( vscode 插件:提供语法高亮和智能提示)
总结:
(2)Vue 特性
数据驱动:在使用了vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。
双向数据绑定:在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源中。示意图如下:
意义:开发者不再需要手动操作DOM元素,来获取表单元素最新的值!
MVVM:MVVM(Model-View-ViewModel) 是 vue 实现数据驱动视图和双向数据绑定的核心原理。它把每个 HTML 页面都拆分成了如下三个部分:
在MVVM概念中:
- View 表示当前页面所渲染的 DOM 结构。
- Model 表示当前页面渲染时所依赖的数据源。
- ViewModel 表示 vue 的实例,它是 MVVM 的核心。
(3)Vue 版本
3.x 版本的 vue 是未来企业级项目开发的趋势,2.x 版本的 vue 在未来(1~2年内)会被逐渐淘汰。
对比:vue2.x 中绝大多数的 API 与特性,在 vue3.x 中同样支持。同时,vue3.x 中还新增了 3.x 所特有的功能、并废弃了某些 2.x 中的旧功能。
2、基本使用(Vue2 为例)
- 导入 vue.js 的 script 脚本文件
- 在页面中声明一个将要被 vue 所控制的 DOM 区域
- 创建 vm 实例对象(vue实例对象)
<!-- 声明 vue 控制的 dom 区域-->
<div id="app">{{ username }}</div>
<!-- 导入 Vue 脚本 -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.2/vue.js"></script>
<!-- 创建 Vue 实例 -->
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'MelodyEcho',
},
})
</script>
代码与 MVVM 的对应关系:
3、指令
指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。
有以下六类:
- 内容渲染指令
- 属性绑定指令
- 事件绑定指令
- 双向绑定指令
- 条件渲染指令
- 列表渲染指令
(1)内容渲染指令
常用的有 v-text
、{{}}
、v-html
。
<!-- 把username对应的值,渲染到第一个p标签中 -->
<p v-text="username"></p>
<!-- 把gender对应的值,渲染到第二个p标签中 -->
<!-- 注意:第二个p标签中,默认的文本“性别”会被 gender的值覆盖掉 -->
<p v-text="gender">性别</p>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'MelodyEcho',
gender: 'female'
}
})
</script>
<!-- 不会覆盖默认的内容 -->
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
v-text
指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html
这个指令:
<!-- description = '<h2>二级标题</h2>' -->
<p v-html="description"></p>
(2)属性绑定指令
如果需要为元素的属性动态绑定属性值,则需要用到 v-bind
属性绑定指令。用法示例如下:
<input type="text" v-bind:placeholder="inputValue" />
<img v-bind:src="imgSrc" alt="" />
<script>
...
data : {
inputValue: '输入框提示',
imgSrc: './img/bg.jpg',
}
</script>
简写形式:v-bind:xxx
-> :xxx
(3)渲染时使用 js 表达式
如:
<div id="app">
<span>{{ string?'True':'False' }}</span>
<span>{{ msg.split('').reverse().join('') }}</span>
<span :id="'list-' + id"></span>
</div>
(4)事件对象绑定指令
vue 提供了 v-on
事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。
<button v-on:click="addCount">+1</button>
<!-- 由于函数简单,也可以直接使用 js 表达式 -->
<!-- <button @click="count += 1">+1</button> -->
<script>
...
data : { count = 0 },
methods: {
// 事件监听在 method 节点
addCount() {
// this 表示当前的 vw 对象
// 通过 this 可以访问到 data 中的数据
this.count += 1
}
}
</script>
注意:原生DOM对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,分别为:v-on:click、v-oninput、v-on:keyup
简写形式:v-on:click
-> @click
事件对象:(同样可以接收到事件对象)
methods: {
addCount(e) {
const color = e.target.style.backgroundColor
console.log(color)
}
}
绑定传参:
<button @click="addNewCount(2)">+2</button>
<script>
...
methods: {
addNewCount(step) { this.count += step }
}
</script>
如果同时需要参数和事件对象:(需要使用 $event 特殊变量)
<button @click="addNewCount(2, $event)">+2</button>
<script>
...
methods: {
addNewCount(step, e) {
const color = e.target.style.backgroundColor
console.log(color)
this.count += step
}
}
</script>
(5)事件、按键修饰符
事件修饰符:在事件处理函数中调用 preventDefault()
或 stopPropagation()
是非常常见的需求。因此, vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:
时间修饰符 | 说明 |
---|---|
.prevent | 阻止默认行为(例如:阻止 a 链接的跳转、阻止表单的提交等) |
.stop | 阻止事件冒泡 |
.capture | 以捕获模式触发当前的事件处理函数 |
.once | 绑定的事件只触发 1 次 |
.self | 只有在 event.target 是当前元素自身时,才触发事件处理函数 |
<a href="https://glowmem.com" @click.prevent="onLinkClick">去往律回彼境</a>
按键修饰符:在监听键盘事件时,我们经常需要判断详细的按键。可以为键盘相关的事件添加按键修饰符,例如:
<input @keyup.enter="submit">
<input @keyup.esc="clearInput">
(6)双向绑定指令
vue 提供了 v-model
双向数据绑定,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。只能配合表单元素使用!
<p>用户名是:{{username}}</p>
<input type="text" v-model="username"/>
<p>选中的省份是:{{province}}</p>
<select v-model="province">
<!-- 这里绑定的是 option 的 value -->
<option value="">请选择</option>
<option value="1">北京</option>
<option value="2">河北</option>
<option value="3">黑龙江</option>
</select>
有三种修饰符,协助快速格式化数据类型:
修饰符 | 作用 | 示例 |
---|---|---|
.number | 自动将用户的输入值转为数值类型 | <input v-model.number=“age” /> |
.trim | 自动过滤用户输入的首尾空白字符 | <input v-model.trim=“msg” /> |
.lazy | 在“change"时而非“input”时更新 | <input v-model.lazy=“msg” /> |
(7)条件渲染指令
条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:v-if
和 v-show
。
<span v-if="flag">一个想被隐藏的元素</span>
<span v-show="flag">一个想被隐藏的元素</span>
区别:v-if
会操作 DOM 节点来控制显示,v-show
则通过样式来控制显示。但是 v-show
有很高的初始渲染开销。因此如果需要频繁切换,则使用 v-show
,如果运行时条件很少改变,则应该使用 v-if
。
配套的 v-else
指令:(类似于 php)
<div v-if="Math.random() > 0.5">随机数 > 0.5</div>
<div v-else>随机数 <= 0.5</div>
当然还有 v-else-if
指令 ,用法类似。
(8)列表渲染指令
vue 提供了v-for
指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构。v-for
指令需要使用 item in items 的特殊语法,其中:items 是待循环的数组,item 是当前的循环项
<ul>
<li v-for="person in list">姓名是: {{ person.name }}</li>
</ul>
<script>
...
data: {
list: [
{id: 1, name: "MelodyEcho"},
{id: 2, name: "MelodyScend"},
]
}
</script>
也可以携带索引:
<ul>
<li v-for="(item, i) in list">索引:{{ i }}, 姓名: {{ item.name }}</li>
</ul>
(9)使用 key 属性维护列表状态
问题:当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。
因此:为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每个元素提供一个唯一的 key 属性:
<ul>
<li v-for="(user, i) in list" :key="user.id">
<input type="checkbox" /> {{ user.name }}
</li>
</ul>
<script>
...
data: {
list: [
{id: 1, name: "MelodyEcho"},
{id: 2, name: "MelodyScend"},
]
}
</script>
注意:
- key 只能为字符串或数字类型
- key 必须唯一
- 建议将原数据的 id 特征值作为 key
- 不要使用索引,因为索引会随状态而更新!
- 建议使用
v-for
时,都指定 key 的值
4、过滤器(Vue3 弃用)
注:过滤器已经在 Vue3 弃用,建议使用计算属性替代。
(1)定义
过滤器(Filters)常用于文本的格式化。例如:
hello -> Hello
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:
<!-- 通过“管道符”调用 capitalize 过滤器,对 message 的值进行格式化 -->
<p>{{ message | capitalize }}</p>
<!-- 在 v-bind 中通过“管道符”调用 formatId 过滤器,对 rawId 的值进行格式化 -->
<div v-bind:id="rawId | formatId"></div>
<script>
// 在 filters 节点定义过滤器
...
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
</script>
注:过滤器可以用在两个地方:插值表达式和 v-bind
属性绑定
(2)私有过滤器和全局过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:
// 全局过滤器-独立于每个 vm 实例之外
// Vue.filter() 方法接收两个参数:
// 第 1 个参数,是全局过滤器的“名字”
// 第 2 个参数,是全局过滤器的“处理函数”
Vue.filter('capitalize', (str)=>{
return str.charAt(0).toupperCase() + str.slice(1)
})
当全局和私有过滤器同名,私有过滤器优先。
(3)连续调用多个过滤器
<!-- 调用方向为从左到右 -->
<p>{{ text | capitalize | maxLength }}</p>
(4)过滤器传参
<!-- 第一个参数永远是管道符前待处理的值,第二个参数开始,才是调用传递的参数 -->
<span>{{ message | fillterA(arg1, arg2) }}</span>
<script>
...
filters: {
filterA(msg, arg1, arg2) {}
}
</script>
二、组件基础(上)
1、单页面应用程序
单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成。
单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web页面初始化时加载相应的资源(HTML、JavaScript 和 CSS)。一旦页面加载完成了,SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换 HTML 的内容,从而实现页面与用户的交互。
(1)优缺点
优点:
-
良好的交互体验:
-
单页应用的内容的改变不需要重新加载整个页面
-
获取数据也是通过 Ajax 异步获取
-
没有页面之间的跳转,不会出现“白屏现象”
-
-
良好的前后端工作分离模式
-
减轻服务器压力
缺点:
- 首屏慢(但可以解决)
- 不利于 SEO(可以使用 SSR 服务器端渲染)
(2)快速创建 Vue 的 SPA 项目
vue 官方提供了两种快速创建工程化的 SPA 项目的方式:
- 基于 vite 创建 SPA 项目(仅支持 vue3.x,小而巧)
- 基于 vue-cli 创建 SPA 项目(支持 vue3.x 和 vue2.x,大而全)
2、Vite 的基本使用
按照顺序执行如下的命令,即可基于 vite 创建 vue3.x 的工程化项目:
npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev
(1)项目基本结构
- public 是公共的静态资源目录
- src 是项目的源代码目录(程序员写的所有代码都要放在此目录下)
- assets 目录用来存放项目中所有的静态资源文件(css、fonts等)
- components 目录用来存放项目中所有的自定义组件
- App.vue 是项目的根组件
- index.css 是项目的全局样式表文件
- main.js 是整个项目的打包入口文件
- index.html 是 SPA 单页面应用程序中唯一的 HTML 页面
(2)vite 项目运行流程
在工程化的项目中,vue 要做的事情很单纯:通过 main.js 把 App.vue 渲染到 index.html 的指定区域中。
其中:
- App.vue 用来编写待渲染的模板结构
- index.html 中需要预留一个 el 区域
- main.js 把 App.vue 渲染到了 index.html 所预留的区域中
main.js 中:
// 1.从 vue 中按需导入 createApp 函数,
// createApp 函数的作用:创建 vue 的“单页面应用程序实例”
import { createApp } from 'vue'
// 2.导入待渲染的 App 组件
import App from './App.vue'
// 3.调用 createApp 函数,返回值是“单页面应用程序的实例”,用常量 spa_app 进行接收,
// 同时把 App 组件作为参数传给 createApp 函数,表示要把 App 渲染到 index.html 页面上
const spa_app = createApp(App)
// 4.调用spa_app 实例的 mount 方法,用来指定 vue 实际要控制的区域
spa_app.mount('#app')
3、组件化开发
组件化开发指的是:根据封装的思想,把页面上可重用的部分封装为组件,从而方便项目的开发和维护。
vue 是一个完全支持组件化开发的框架。vue 中规定组件的后缀名是 .vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。
(1)vue 组件构成
每个 .vue 组件都由 3 部分构成,分别是:
- template -> 组件的模板结构
- script -> 组件的 JavaScript 行为
- style -> 组件的样式
注:script 和 style 省略。
(2)组件 template 节点
vue 规定:每个组件对应的模板结构,需要定义到 <template> 节点中。它是 vue 提供的容器,只起到包裹的作用,不会被真正渲染为 DOM 元素。
在 template 中,支持使用指令来渲染 DOM 结构:
<template>
<button @click="showInfo">按钮</button>
<span :title="new Date().toLocalTimeString()">只是一个 span 元素</span>
</template>
注:vue2.x 中 template 只支持单个根节点,但 vue3.x 支持多个根节点。
(3)组件 script 节点
vue规定:组件内的 <script> 节点是可选的,开发者可以在 <script> 节点中封装组件的 JavaScript 业务逻辑。结构如下:
<script>
// 今后,组件相关的 data 数据、methods 方法等,
// 都需要定义到 export default 所导出的对象中。
export default{
// name 属性指向的是当前组件的名称(建议每个首字母大写),可以在调试工具看到这个名称
name: 'MyApp',
// 渲染期间用到的数据,注意组件中一定要使用函数:
data() {
return {
count: 0,
username: 'MelodyEcho',
}
},
// 绑定方法
methods: {
addCount() {
this.count++
}
}
}
</script>
(4)组件 style 节点
vue 规定:组件内的 <style> 节点是可选的,开发者可以在 <style> 节点中编写样式美化当前组件的 UI 结构。
其中 <style> 标签上的 lang=“css” 属性是可选的,它表示所使用的样式语言。默认只支持普通的 css 语法,可选值还有 less、scss 等。
<style lang="css">
h1 {
font-weight: normal;
}
</style>
使用 less 语法前需要先配置:
- 运行
npm install less -D
命令安装依赖包,从而提供 less 语法的编译支持 - 在 <style> 标签上添加 lang=“less” 属性,即可使用 less 语法编写组件的样式
4、组件使用
组件之间可以进行相互的引用。引用原则:先注册,再引用。
(1)注册组件
全局注册和局部注册:
- 被全局注册的组件,可以在全局任何一个组件内被使用
- 局部注册的组件,只能在当前注册的组件范围内使用
全局注册:(在 main.js 中,要在 mount 前注册)
// 导入需要被全局注册的组件
import Swiper from './components/Swiper.vue'
// 使用 app.component() 方法
app.component('my-swiper', Swiper)
<!-- 此时就可以在其他组件中这样导入使用:-->
<my-swiper></my-swiper>
局部注册:
<template>
<h1>组件注册测试 </h1>
<TextBox></TextBox>
</template>
<script>
import TextBox from './components/textBox.vue'
export default {
// 通过该节点完成私有组件的注册
components: {
// 键值对完全一致也可直接省略键,只写值
TextBox,
}
}
</script>
通过 name 属性注册组件:
app.component(Swiper.name, Swiper)
(2)组件命名法
- 使用 kebab-case 命名法(俗称短横线命名法,例如 my-swiper 和 my-search )
- 使用 PascalCase 命名法(俗称帕斯卡命名法或大驼峰命名法,例如 MySwiper 和 MySearch )
注:短横线命名法必须严格按照短横线使用,但帕斯卡命名法可以兼容短横线写法使用。因此推荐使用帕斯卡命名法,这样适应性更强。
(3)组件样式冲突问题
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。这个问题的根本原因是:
- 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
- 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
解决方案:(通过为每个组件所有元素分配自定义属性)
<template>
<div class="container" data-v-001>
<span data-v-001></span>
</div>
</template>
<style>
.container[data-v-001] {
font-size: 20px;
}
</style>
但是这样太麻烦,因此 vue 提供了对应的自动化的方案:(为 style 添加 scoped 属性即可)
<style scoped>
/* 会自动分配唯一的“自定义属性” */
.container {
font-size: 18px;
}
</style>
(4):deep() 样式穿透
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 :deep() 深度选择器。
<style scoped>
/* 不加 /deep/ 时,生成格式为: .title[data-v-001] */
.title {
color: blue;
}
/* 加了之后,生成格式为: [data-v-001] .title */
:deep(title) {
color: blue;
}
</style>
5、组件的 props
为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:
- 组件的 DOM 结构、Style 样式要尽量复用
- 组件中要展示的数据,尽量由组件的使用者提供
因此为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。
(1)什么是组件的 props
props 是组件的自定义属性,组件的使用者可以通过 props 把数据传递到子组件内部,供子组件内部进行使用。如:
<!-- 通过两个自定义属性传递了数据 -->
<my-article title="面朝大海,春暖花开" author="海子"></my-article>
注:props 对于子组件是只读的。
(2)在组件中声明 props
在封装 vue 组件时,可以把动态的数据项声明为 props 自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。如:
<template>
<span>标题:{{ title }}</span>
<span>作者:{{ author }}</span>
</template>
<script>
export default {
// 父组件传递给该组件的数据,必须在 props 节点声明
props: ['title', 'author']
}
</script>
(3)动态绑定 props 的值
可以使用 v-bind
的形式动态指定值:
<my-article :title="info.title" :author="'post by ' + info.author"></my-article>
(4)props 命名法
组件中如果使用 “camelCase(驼峰命名法)” 声明了 props 属性的名称,则有两种方式为其绑定属性的值:
<!-- 子组件中 -->
<script>
export default {
props: ['pubTime'], // 驼峰命名法
}
</script>
<!-- 父组件中这两种写法都可以 -->
<my-article pubTime="1989"></my-article>
<!-- 或 -->
<my-article pub-time="1989"></my-article>
特别注意:只是父组件绑定可以用两种方式,子组件中使用数据时不能更改名字!
6、动态样式操作
(1)动态绑定 HTML 的 class
<h3 class="thin" :class="isItalic? 'italic' : ''">字体粗细切换组件</h3>
<button @click="isItalic=!isItalic">切换字体样式</button>
...
data() {
return { isItalic: true }
}
...
<style scoped>
.thin {
font-weight: 200;
}
.italic {
font-style: italic;
}
</style>
如果需要同时动态绑定多个 class 类名,可以使用数组的语法格式:
<h3 class="thin" :class="[isItalic? 'italic': '', isBold ? 'bold' : '']">
字体粗细切换组件
</h3>
但这样会过于臃肿,可以使用对象语法绑定:
<!-- 特别注意,此时 classObj 的键就是类名了,将会根据布尔值决定是否添加该类名 -->
<h3 class="thin" :class="classObj">字体粗细切换组件</h3>
<button @click="classObj.italic=!classObj.italic">切换字体样式</button>
...
data() {
return {
classObj: {
italic: true,
}
}
}
(2)对象语法绑定内联 style
语法十分简洁,但注意样式名驼峰命名或使用字符串。
<div :style="{color: active, fontSize: fsize+'px', 'background-color': bgColor}">测试文本</div>
...
data() {
return {
active: 'red',
fsize: 30,
bgColor: 'pink',
}
}
三、组件基础(下)
1、props 验证
props 验证:在封装组件时对外界传递过来的 props 数据进行合法性的校验,从而防止数据不合法的问题。
之前我们使用的是数组类型的 props 节点,但存在缺点:无法为每个prop指定具体的数据类型。因此现在我们需要使用对象类型的 props 节点,它可以实现对数据类型的校验。
<p>状态:{{ state }}</p>
<p>数量:{{ count }}</p>
...
props: {
state: Boolean,
count: Number,
}
(1)基础类型校验
<script>
props: {
// 支持的类型如下:
propA: String,
propB: Number,
PropC: Boolean,
PropD: Array,
propE: Object,
propF: Date,
propG: Function,
propH: Symbol,
}
</script>
(2)多个可能类型
如果 prop 属性值不唯一:
<script>
props: {
propA: [Number, String],
}
</script>
(3)必填项校验
如果某个 prop 属性是必填项:
<script>
props: {
propB: {
type: String,
required: true,
}
}
</script>
(4)属性默认值
在封装组件时,可以为某个 prop 属性指定默认值:
<script>
props: {
propB: {
type: Number,
default: 100
}
}
</script>
(5)自定义验证函数
在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制:
<script>
props: {
propB: {
// 利用 validator 函数
validator(value) {
// true 验证成功,false 验证失败
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
</script>
2、计算属性
计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值,供组件渲染 DOM 时使用。
计算属性需要以 function 函数的形式(且只能是 function 函数形式)声明到组件的 computed 选项中,示例:
<input type="text" v-model.number="count" />
<p> {{ count }} 乘以 2 的值为:{{ plus }}</p>
...
computed: {
plus() {
return this.count * 2
}
}
注:计算属性侧重于得到一个计算的结果,因此计算属性中必须有 return 返回值!
相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行运算。因此计算属性的性能更好。
3、自定义事件
在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件。
(1)使用
声明自定义事件:开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明,如:
...
<script>
export default {
// 组件声明的自定义事件,必须先定义在 emits 节点中
emits: ['change'],
}
</script>
触发自定义事件:在 emits 节点下声明的自定义事件,可以通过 this.$emit(自定义事件名称)
方法进行触发,示例代码如下:
...
<script>
export default {
// 组件声明的自定义事件,必须先定义在 emits 节点中
emits: ['change'],
methods: {
onBtnClick() {
// 当点击了按钮,调用该方法,触发事件
this.$emit('change')
}
}
}
</script>
监听自定义事件:在使用自定义的组件时,可以通过 v-on
的形式监听自定义事件:
<my-counter @change="getCount"></my-counter>
...
methods: {
getCount() {
console.log('监听到了 count 值的变化')
}
}
(2)传参
在调用 this.$emit(自定义事件名称)
方法触发自定义事件时,可以通过第 2 个参数为自定义事件传参,如:
// 子组件 script:
this.$emit('change', this.count)
<!-- 父组件 -->
<my-counter @change="getCount"></my-counter>
...
methods: {
getCount(num) {
console.log('监听到了 count 值的变化', num)
}
}
4、组件上的 v-model
v-model 是双向数据绑定指令,除表单元素数据绑定外,当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。如使用 v-model 实现父子的双向数据绑定:
(1)具体实现
- 父组件向子组件传递数据:
-
父组件通过
v-bind
属性绑定的形式,把数据传递给子组件 -
子组件中通过 props 接收父组件传递过来的数据
2. 子组件向父组件传递数据:
- 在
v-bind
指令之前添加v-model
指令 - 在子组件中声明
emit
自定义事件,格式为update:xxx
- 调用
$emit()
触发自定义事件,更新父组件中的数据
第二部分代码:
<template>
<p>子组件计数:{{ number }}</p>
<button @click="add">+1</button>
</template>
<script>
export default {
...
props: {
number: Number,
},
emits: ['update:number'],
methods: {
add() {
// 这里不能给 this.number 加 1,事件传递给父组件后,由父组件接受并设置加 1 后的值,然后再将值传递给子组件(因为 props 是只读的)
this.$emit('update:number', this.number + 1)
}
},
}
</script>
四、组件高级(上)
1、watch 监听器
(1)基础
watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发起请求,判断用户名是否可用。
基本语法,在 watch 节点下:
<template>
<input type="text" class="form-control" v-model.trim="username">
</template>
<script>
export default {
data() {
return {
username: '',
}
},
watch: {
async username(newVal, oldVal) {
// 解构 axios 响应对象
const {data:res} = await axios.get('https://localhost/api/user');
console.log(res);
}
}
}
</script>
(2)immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项。实例代码如下:
<script>
watch: {
username: {
// 注意 handler 属性是默认写法
async handler(newVal) {
const {data:res} = await axios.get(`https://localhost/api/user/${newVal}`);
console.log(res);
},
// 组件加载完毕后立即调用当前 watch 监听器
immediate: true,
},
}
</script>
(3)deep 选项
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项,代码示例如下:
<script>
watch: {
info: {
// 注意 handler 属性是默认写法
async handler(newVal) {
const {data:res} = await axios.get(`https://localhost/api/user/${newVal.username}`);
console.log(res);
},
// 使用 deep 选项
deep: true,
},
}
</script>
(4)监听对象单个属性
代码示例:
<script>
watch: {
// 只监听 info.username
'info.username': {
async handler(newVal) {
const {data:res} = await axios.get(`https://localhost/api/user/${newVal}`);
console.log(res);
},
},
}
</script>
(5)计算属性和监听器
计算属性和侦听器侧重的应用场景不同:
- 计算属性侧重于监听多个值的变化,最终计算并返回一个新值
- 侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
2、组件生命周期
(1)组件运行过程
组件的生命周期指的是:组件从创建->运行(渲染)->销毁的整个过程,强调的是一个时间段。
(2)监听组件的不同时刻
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。
- 当组件在内存中被创建完毕之后,会自动调用 created 函数
- 适用于发 ajax 初次请求
- 当组件第一次被成功地渲染到页面上之后,会自动调用 mounted 函数
- 适用于最早操作 dom 元素(此时刚好渲染完成)
- 当组件被销毁完毕之后,会自动调用 unmounted 函数
<script>
export default {
...
created() {...},
mounted() {...},
unmounted() {...},
}
</script>
(3)监听组件的更新
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
<script>
export default {
...
updated() {...},
}
</script>
(4)全部生命周期函数
beforeCreate、created、
beforeMount、mounted、
beforeUpdate、updated、
beforeUnmount、Unmounted
注:不在 beforeCreate 中发起 ajax 请求是因为此时组件还没创建好,请求到的数据是无法挂载到组件上的。
3、组件间的数据共享
(1)组件关系
- 父子关系
- 兄弟关系
- 后代关系
(2)父子数据共享
父->子:参见笔记前面部分
子->父:参见笔记前面部分
子父数据同步:参见笔记前面部分
(3)兄弟间的数据共享
兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。示意图如下:
安装:
npm i mitt@2.1.0
创建 eventBus.js:
import mitt from 'mitt'
const but = mitt()
export default bus
在数据接收方:
<script>
import bus from '../eventBus.js'
export default {
...
created() {
bus.on('countChange', count => {
this.count = count;
})
}
}
</script>
在数据发送方:
<script>
import bus from '../eventBus.js'
export default {
...
methods: {
add() {
this.count++;
bus.emit('countChange', this.count);
}
}
}
</script>
(4)后代关系组件的数据共享
a. 非响应数据共享
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。
父节点通过 provide 共享数据 color:
<script>
export default {
...
provide() {
return {
color: this.color,
}
}
</script>
子孙节点使用 Inject 数组接收数据:
<template>
<span>{{ color }}</span>
</template>
<script>
export default {
...
inject: ['color'],
}
</script>
b. 响应数据共享
思路:结合 computed 函数向下共享:
<script>
import { computed } from 'vue'
export default {
...,
provide() {
return {
color: computed(() => this.color);
}
}
}
</script>
<template>
<span>{{ color.value }}</span>
</template>
(5)全局数据共享 vuex
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。
vuex 内容较多,本文暂不提及。
4、vue3.x 全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
- 每个组件中都需要导入 axios(代码臃肿)
- 每次发请求都需要填写完整的请求路径(不利于后期的维护)
main.js 中:
// 配置根路径
axios.defaults.baseURL = 'https://glowmem.com/api';
// 挂载为 app 全局自定义属性
app.config.globalProperties.$axios = axios;
任意组件:
const {data:usersRes} = await this.$axios.get('/users');
const {data:newsRes} = await this.$axios.post('/news', {type: 'tech'});
五、组件高级(下)
1、ref 引用和 $nextTick 函数
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。
每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。
使用 DOM 或 组件的 ref 引用:
<template>
<span ref="spanRef"></span>
<my-comp ref="compRef"></my-comp>
</template>
<script>
...
getRef() {
console.log(this.$refs.spanRef);
console.log(this.$refs.compRef);
// 引用到组件的实例后,可以直接调用其上的 methods 方法
this.$refs.compRef.action();
}
</script>
注意:由于组件是异步执行 DOM 更新的,所以操作 ref 引用时要注意等到 DOM 元素渲染后再获取。
可以使用 $nextTick() 函数解决这个问题。组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 异步地重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素。
<script>
...
showInput() {
this.inputVisible = true;
this.$nextTick(() => {
// 更新后再获取 DOM 元素
this.$refs.ipt.focus();
})
}
</script>
2、动态组件
动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 <component> 组件,专门用来实现组件的动态渲染。
- <component> 是组件的占位符
- 通过 is 属性动态指定要渲染的组件名称
- <companent is=“要渲染的组件的名称”></component>
<template>
<component :is="compName"></component>
</template>
<script>
...
data() {
return {
compName: 'HomeComp',
}
}
</script>
注:默认情况下,切换动态组件时无法保持组件的状态。此时可以使用vue内置的 <keep-alive> 组件保持动态组件的状态。示例代码如下:
<template>
<keep-alive>
<component :is="compName"></component>
</keep-alive>
</template>
3、插槽
插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。如图:
可以把插槽认为是组件封装期间,为用户预留的内容的占位符。
(1)插槽基本用法
在封装组件时,可以通过 <slot> 元素定义插槽,从而为用户预留内容占位符。示例代码如下:
组件:
<template>
<p>这是前一个 p 标签</p>
<slot></slot>
<p>这是后一个 p 标签</p>
</template>
使用组件时:
<template>
<my-comp>
<p>这是自定义的内容</p>
</my-comp>
</template>
注:如果封装时没有预留任何的 slot 插槽,则任何提供的自定义内容都会被丢弃
(2)插槽默认内容
封装组件时,可以为预留的 <slot> 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例代码如下:
<template>
<p>这是前一个 p 标签</p>
<slot>这是备用默认内容</slot>
<p>这是后一个 p 标签</p>
</template>
(3)具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot> 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”。示例代码如下:
组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
注:没有指定 name 名称的插槽,会有隐含的名称叫做“default"。
使用:
<my-comp>
<template v-slot:header>
<h1>诗歌标题</h1>
</template>
<template v-slot:default>
<h1>诗歌内容</h1>
</template>
<template v-slot:footer>
<h1>诗歌作者</h1>
</template>
</my-comp>
实际上,就是说,只有默认插槽在使用时可以省略 <template> 标签。但具名插槽也有其简写形式:
<my-comp>
<template #header>
<h1>诗歌标题</h1>
</template>
...
</my-comp>
(4)作用域插槽
在封装组件的过程中,可以为预留的 <slot> 插槽绑定数据,这种带有数据的 <slot> 叫做“作用域插槽”。示例代码如下:
组件:
<div>
<h3>这是 TEST 组件</h3>
<slot :info="information" :msg="message"></slot>
</div>
使用:(接受插槽给的数据)
<my-test>
<template v-slot:default="scope">
info: {{ scope.info }}
msg: {{ scope.msg }}
</template>
</my-test>
或解构使用:
<my-test>
<template v-slot:default="{ msg, info }">
info: {{ msg }}
msg: {{ info }}
</template>
</my-test>
4、自定义指令
(1)私有自定义指令
vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。除此之外 vue 还允许开发者自定义指令。
vue 中的自定义指令分为两类,分别是:
- 私有自定义指令
- 全局自定义指令
在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。示例代码如下:
<template>
<input type="text" class="form-control" v-focus>
</template>
<script>
...
directive: {
// 自定义一个私有指令
focus: {
// el 指向绑定的元素。当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
mounted(el) {
el.focus(); // 让元素自动获得焦点
}
}
}
</script>
(2)全局自定义指令
main.js 中:
const app = Vue.createApp()
app.directive('focus', {
mounted(el) {
el.focus();
}
})
(3)updaeted 函数
mounted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted 函数不会被触发。updated 函数会在每次 DOM 更新完成后被调用。示例代码如下:
app.directive('focus', {
mounted(el) {
el.focus();
},
updated(el) {
el.focus();
}
})
注:如果 mounted 和 update 中的逻辑完全相同,可以简写为:
app.directive('focus', el => {
el.focus();
})
(4)指令的参数值
在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值,示例代码如下:
<template>
<input type="text" v-color="'red'">
<p v-color="'cyan'">一段文本</p>
</template>
main.js 中:
app.directive('color', (el, binding) => {
el.style.color = binding.value;
})
六、路由
1、前端路由概念与原理
路由(英文:router)就是对应关系。路由分为两大类:
- 后端路由
- 前端路由
后端路由:指的是请求方式、请求地址与 function 处理函数之间的对应关系。
前端路由:通俗易懂的概念:Hash 地址与组件之间的对应关系。如:
https://xxx.com/spa#/index
https://xxx.com/spa#/info
(1)SPA 与前端路由
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。
此时,不同组件之间的切换需要通过前端路由来实现。
结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!
(2)前端路由的工作方式
- 用户点击了页面上的路由链接
- 导致了 URL 地址栏中的 Hash 值发生了变化
- 前端路由监听了到 Hash 地址的变化
- 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
结论:前端路由,指的是 Hash 地址与组件之间的对应关系!
注:我们可以手动实现简单的前端路由,但实际开发中我们一般使用一些集成的路由解决方案。
2、vue-router4.x 基本用法
vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。vue-router 3.x 只能结合 vue2 进行使用 vue-router 4.x 只能结合 vue3 进行使用。
(1)安装
npm i vue-router@4.0.3
(2)声明路由连接和占位符
可以使用 <router-link> 标签来声明路由链接,并使用 <router-view> 标签来声明路由占位符。示例代码如下:
<template>
<h1>App 根组件</h1>
<!-- 路由连接 -->
<router-link to="/home">首页</router-link>
<router-link to="/detail">详情页</router-link>
<!-- 路由组件占位符 -->
<router-view></router-view>
</template>
(3)创建 router.js 并挂载
router.js:
// createRouter 方法用于创建路由的实例对象
// createwebHashHistory 用于指定路由的工作模式(hash模式)
import { createRouter,createWebHashHistory } from 'vue-router'
import Home from './component/Home.vue'
import Movie from './component/Movie.vue'
import About from './component/About.vue'
// 创建路由实例对象
const router = createRouter({
// history 属性指定路由的工作模式
history: createWebHistory(),
// routes 数组,指定路由规则
routes: [
// path 是 hash 地址,component 是要展示的组件
{ path:'/home', component:Home },
{ path:'/movie', component:Movie },
{ path:'/about', component:About },
],
})
export default router;
main.js 中:
import router from './router.js'
app.use(router);
3、路由重定向
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C,从而展示特定的组件页面。通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
const router = createRouter({
history: createWebHistory(),
routes: [
{ path:'/', redirect:'/home' },
{ path:'/home', component:Home },
{ path:'/movie', component:Movie },
{ path:'/about', component:About },
],
})
4、路由高亮
方法:
- 写
.router-link-active
类的样式 - 通过 linkActiveClass 属性自定义 路由高亮的 class 类
const router = createRouter({
...
// 默认的类样式会被覆盖
linkActiveClass: 'router-active',
})
5、嵌套路由
通过路由实现组件的嵌套展示,叫做嵌套路由。
方法:
- 声明子路由链接和子路由占位符
- 在父路由规则中,通过 children 属性嵌套声明子路由规则
在组件中声明子路由链接和占位符:
<template>
<router-link to="/about/tab1">tab1</router-link>
<router-link to="/about/tab2">tab2</router-link>
<router-view></router-view>
</template>
在 router.js 中导入子组件,然后使用 children 属性声明子路由规则。
import Tab1 from './component/tabs/Tab1.vue'
import Tab2 from './component/tabs/Tab2.vue'
const router = create Router({
...
routes: [
{// about 页面的路由规则
path: '/about',
component: About,
children: [// 通过 children 属性子级嵌套路由规则,注意这里不要加'/'
{ path: 'tab1', component: Tab1 },
{ path: 'tab2', component: Tab2 },
]
},
{ path:'/home', component:Home },
...
]
})
此时的路由重定向:
...
{// about 页面的路由规则
path: '/about',
component: About,
redirect: '/about/tab1',
children: [
{ path: 'tab1', component: Tab1 },
{ path: 'tab2', component: Tab2 },
]
},
6、动态路由
动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用英文的冒号 : 来定义路由的参数项。示例代码如下:
{ path: '/movie/:id', component: Movie },
通过 $route.params 获取动态路由的参数:
<template>
<h3>参数:{{ $route.params.id }}</h3>
</template>
同时,为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参:
{ path: '/movie/:id', component: Movie, props: true },
<template>
<h3>参数:{{ id }}</h3>
</template>
<script>
export default {
...
props: ['id'],
}
</script>
关于参数传递,更详细的说明,见于:https://blog.csdn.net/hyk521/article/details/105479333/
7、编程式导航
通过调用 API 实现导航的方式,叫做编程式导航。与之对应的,通过点击链接实现导航的方式,叫做声明式导航。例如:
- 普通网页中点击 <a> 链接、vue 项目中点击 <router-link>都属于声明式导航
- 普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航
vue-router 提供的编程式导航的 api 有:
- this.$router.push(‘/movie/3’)
- this.$router.go(-1)
8、命名路由
通过 name 属性为路由规则定义名称的方式,叫做命名路由。示例代码如下:
{
path: '/movie/:id',
name: 'mov',
component: Movie,
props: true,
}
注:命名路由的 name 不能重复。
作用:
实现声明式导航:为 <router-link> 标签动态绑定 to 属性的值,并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数。示例代码如下:
<router-link :to="{ name: 'mov', params: { id: 2 } }"></router-link>
实现编程式导航:调用 push 函数期间指定一个配置对象,name 是要跳转到的路由规则、params 是携带的路由参数:
this.$router.push({ name: 'mov', params: { id: 2 } })
9、导航守卫
导航守卫可以控制路由的访问权限。示意图如下:
全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航守卫:
const router = createRouter({...,});
// 调用路由实例对象的 beforeEach 函数,声明”全局前置守卫"
// fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
// 因此fn叫做”守卫方法"
router.beforeEach(fn);
(1)守卫方法的 3 个形参
全局导航守卫的守卫方法中接收 3 个形参,格式为:
router.beforeEach((to, from, next) => {
// to 目标路由对象,from 当前导航正要离开的路由对象,next 为表示放行的函数
});
注:
- 如果不声明 next 形参,则默认允许用户访问每一个路由!
- 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由!
next 函数的三种调用方式:
next() // 直接放行
next(false) // 强制停留在当前页面
next('/login') // 强制跳转到指定页面
(2)结合 token 控制后台主页的访问权限
router.beforeEach((to, from, next) => {
// 这里只是演示,实际使用时要做 token 值的校验
const token = localStorage.getItem('token');
if (to.path === '/main' && !token) {
next('/login');
}
else {
next();
}
})
Q.E.D.