ES6模块化与异步编程高级用法
ES6 模块化
1. 回顾:node.js 中如何实现模块化
node.js 遵循了 CommonJS
的模块化规范。其中:
- 导入其他模块使用
require()
方法 - 模块对外共享成员使用
module.exports
对象
模块化的好处:
大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用
,利人利己。
2. 前端模块化规范的分类
在 ES6 模块化规范
诞生之前,JavaScript 社区已经尝试并提出了 AMD、CMD、CommonJS
等模块化规范。
但是,这些有社区提出的模块化标准,还是存在一定的差异性
与局限性
,并不是
浏览器与服务器通用的模块化标准
,例如:
- AMD 和 CMD 适用于
浏览器端
的 JavaScript 模块化 - CommonJS 适用于
服务器端
的 JavaScript 模块化
太多的模块化规范给开发者增加了学习的难度
与开发的成本
。因此,大一统的 ES6 模块化规范诞生了
!
3. 什么是 ES6 模块化规范
ES6 模块化规范
是浏览器端
与服务器端
通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本,开发者不需再额外学习AMD、CMD或CommonJS 等模块化规范。
ES6 模块化规范中定义:
- 每个 js 文件都是一个独立的模块
- 导入其他模块成员使用
import
关键字 - 向外共享模块成员使用
export
关键字
4. 在 node.js中体验 ES6 模块化
node.js中默认仅支持 CommonJS 模块化规范
,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照如下两个步骤进行配置:
- 确保安装了
v14.15.1
或更高版本的 node.js - 在package.json 的根节点中添加
“type": "module"
节点
5. ES6 模块化的基本语法
ES6 的模块化主要包含如下3中用法:
默认导出
与默认导入
按需导出
与按需导入
直接导入
并执行
模块中的代码
5.1 默认导出
默认导出的语法:export default
默认导出的成员
let n1 = 10; // 定义模块私有成员 n1
let n2 = 20; // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去
function show() {} // 定义模块私有方法 show
export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
n1,
show
}
5.2 默认导入
默认导入的语法 import 接收名称 from ‘模块标识符’
// 从01_m1.js 模块中导入 export default 向外共享的成员
// 并使用 m1 成员进行接收
import m1 from './01_m1.js'
// 打印输出的结果为:
// {n1: 10, show: [Function :show]}
console.log(m1)
1. 默认导出的注意事项
每个模块中,只允许使用唯一的一次 export default,否则会报错!
let n1 = 10; // 定义模块私有成员 n1
let n2 = 20; // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去
function show() {} // 定义模块私有方法 show
export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
n1,
show
}
// SyntaxError: Identifier '.default' has already been declared
export default {
n2
}
2. 默认导入的注意事项
默认导入时的接收名称
可以任意名称,只要是合法的成员名称即可
:
// m1 是合法的名称
import m1 from './01_m1.js'
// 123m 不是合法的名称,因为成员名称不能以数字开头
import 123m from './01_m1.js'
5.3 按需导出
按需导出的语法:export
按需导出的成员
// 当前模块为 03_m2.js
// 向外按需导出变量 s1
export let s1 = 'aaa'
// 向外按需导出变量 s2
export let s2 = 'ccc'
// 向外按需导出方法 say
export function say() {}
5.4 按需导入
按需导入的语法:import
{ s1 } from
‘模块标识符’
// 导入模块成员
import { s1, s2, say } from './03_m2.js'
console.log(s1) // 打印输出 aaa
console.log(s2) // 打印输出 ccc
console.log(say) // 打印输出 [Function: say]
1. 按需导出与按需导入的注意事项
- 每个模块中可以使用
多次
按需导出 - 按需
导入的成员名称
必须和按需导出的名称
保持一致 - 按需导入时,可以使用
as 关键字
进行重命名 - 按需导入可以和默认导入一起使用
5.5 直接导入并执行模块中的代码
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模块代码,示例代码如下:
// 当前文件模块为 05_m3.js
// 在当前模块中执行一个 for 循环操作
for (let i = 0; i < 3; i++) {
console.log(i)
}
// -------------------分割线----------------------
// 直接导入并执行模块代码,不需要得到模块向外共享的成员
import './05_m3.js'
Promise
1. 回调地狱
多层回调函数的相互嵌套
,就形成了回调地狱
。示例代码如下:
setTimeout(() { // 第一层回调函数
console.log('延时 1 秒后输出')
setTimeout(() { // 第二次回调函数
console.log('再延时 2 秒后输出')
setTimeout(() {
console.log('再延时 3 秒后输出')
}, 3000)
}, 2000)
}, 1000)
回调地狱的缺点:
- 代码耦合性太强,牵一发而动全身,
难以维护
- 大量冗余的代码相互嵌套,代码的
可读性变差
1.1 如何解决回调地狱的问题
为了解决回调地狱的问题,ES6
(ECMAScript 2015)中新增了 Promise
的概念。
1.2 Promise 的基本概念
Promise 是一个构造函数
- 我们可以创建 Promise 的实例 const p = new Promise()
- new 出来的 Promise 实例对象,代表一个异步操作
Promise.prototype 上包含一个 .then() 方法
- 每一个 new Promise() 构造函数得到的实例对象
- 都可以通过原型链的方式访问到.then() 方法,例如p.then()
.then() 方法用来预先指定成功和失败的回调函数
- p.then(成功的回调函数,失败的回调函数)
- p.then(result => { }, error => { })
2. 基于回调函数按顺序读取文件内容
// 读取文件 1.txt
fs.readFile('./files/1.txt', 'utf8', (err1, r1) => {
if (err1) return console.log(err1.message) // 读取文件1 失败
console.log(r1) // 读取文件1 成功
// 读取文件 2.txt
fs.readFile('./filse/2.txt', 'utf8', (err2, r2) => {
if (err2) return console.log(err2.message) // 读取文件2 失败
console.log(r2) // 读取文件2 成功
fs.readFile('./filse/3.txt', 'utf8', (err3, r3) => {
if (err3) return console.log(err3.message) // 读取文件3 失败
console.log(r3) // 读取文件3 成功
})
})
})
3. 基于then-fs 读取文件内容
由于 node.js 官方提供的 fs 模块 仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此需要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件内容:
npm install then-fs
3.1 then-fs 的基本使用
调用 then-fs 提供的 readFile()
方法,可以异步地读取文件的内容,它的返回值是 Promise 的实例对象
。因此可以调用.then()方法
为每个 Promise 异步操作指定成功
和失败
之后的回调函数。示例代码如下:
// 基于 Promise 的方式读取文件
import thenFs from 'then-fs'
// 注意:.then() 中的失败回调是可选的,可以被省略
thenFs.readFile('./files/1.txt', 'utf8').then(r1 => { console.log(r1)}, err1 => { console.log(err1.message) })
thenFs.readFile('./files/2.txt', 'utf8').then(r2 => { console.log(r2)}, err2 => { console.log(err2.message) })
thenFs.readFile('./files/3.txt', 'utf8').then(r3 => { console.log(r3)}, err3 => { console.log(err3.message) })
注意:上述的代码无法保证文件的读取顺序
,需要做进一步的改进!
3.2 .then()方法的特性
如果上一个.then()方法中返回了一个新的 Promise 实例对象
,则可以通过下一个.then() 继续进行处理。通过.then()方法的链式调用
,就可以解决了回调函数的问题。
3.3 基于 Promise
按顺序读取文件的内容
Promise 支持链式调用
,从而来解决回调地狱的问题。示例代码如下:
thenFs.readFile('./files/1.txt', 'utf8') // 1. 返回值是 Promise 的实例对象
.then(r1 => { // 2. 通过 .then 为第一个 Promise 实例指定成功之后的回调函数
console.log(r1);
return thenFs.readFile('./files/2.txt', 'utf8') // 3. 在第一个 .then 中返回一个新的 Promise 实例对象
})
.then(r2 => { // 4. 继续调用 .then 为上一个 .then 的返回值 (新的 Promise 实例) 指定成功之后的回调函数
console.log(r2);
return thenFs.readFile('./files/3.txt', 'utf8') // 5. 在第二个 .then 中再返回一个新的 Promise 实例对象
})
.then(r3 => { // 6. 继续调用 .then 为上一个 .then 的返回值 (新的 Promise 实例) 指定成功之后的回调函数
console.log(r3);
})
3.4 通过.catch 捕获错误
在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理:
thenFs.readFile('./files/1.txt', 'utf8') // 文件不存在导致读取失败,后面的 3 个 .then 都不执行
.then(r1 => {
console.log(r1);
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then(r2 => {
console.log(r2);
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then(r3 => {
console.log(r3);
})
.catch(err => { // 捕获第一行发生的错误,并输出错误的信息
console.log(err.message);
})
3.5 Promise.all()
方法
Promise.all() 方法会发起并行的 Promise 异步操作,等所有的异步操作全部结束后才会执行下一步的 .then 操作
(等待机制)。示例代码如下:
// 1. 定义一个数组,存放3个读文件的异步操作
const promiseArr = [
thenFs.readFile('./files/11.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/3.txt', 'utf8'),
]
// 2. 将 Promise 的数组,作为 Promise.all() 的参数
Promise.all(promiseArr)
.then(([r1, r2, r3]) => { // 2.1 所有文件读取成功(等待机制)
console.log(r1, r2, r3);
})
.catch(err => { // 2.2 捕获 Promise 异步操作中的错误
console.log(err.message);
})
3.6 Promise.race()
方法
Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作
(赛跑机制)。示例代码如下:
// 1. 定义一个数组,存放3个读文件的异步操作
const promiseArr = [
thenFs.readFile('./files/11.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/3.txt', 'utf8'),
]
// 2. 将 Promise 的数组,作为 Promise.all() 的参数
Promise.race(promiseArr)
.then(([r1, r2, r3]) => { // 2.1 所有文件读取成功(等待机制)
console.log(r1, r2, r3);
})
.catch(err => { // 2.2 捕获 Promise 异步操作中的错误
console.log(err.message);
})
4. 基于 Promise 封装读文件的方法
方法的封装要求:
- 方法的名称要定义为
getFile
- 方法接收一个形参
fpath
,表示要读取的文件的路径 - 方法的
返回值
为 Promise 实例对象
4.1 getFile 方法的基本定义
// 1. 方法的名称为 getFile
// 2. 方法接收一个形参 fpath,表示要读取的文件路径
function getFile(Fpath) {
// 3. 方法的返回值为 Promise 的实例对象
return new Promise()
}
4.2 创建具体的异步操作
如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的异步操作定义到 function 函数内部。示例代码如下:
// 1. 方法的名称为 getFile
// 2. 方法接收一个形参 fpath,表示要读取的文件路径
function getFile(fpath) {
// 3. 方法的返回值为 Promise 的实例对象
return new Promise(function() {
// 4. 下面这行代码,表示这是一个具体的、读文件的异步操作
fs.readFile(fpath, 'utf8', (err, dataStr) => {})
})
}
4.3 获取 .then 的两个实参
通过 .then() 指定的成功
和失败
的回调函数,可以在 function 的形参中
进行接收,示例代码如下:
function getFile(fpath) {
// resolve 形参是:调用 getFile() 方法时,通过 .then 指定的“成功的”回调函数
// reject 形参是:调用 getFile() 方法时,通过 .then 指定的“失败的”回调函数
return new Promise(function(resolve, reject) {
fs.readFile(fpath, 'utf8', (err, dataStr) => {})
})
// getFile 方法的调用过程
getFile('./files/1.txt').then(成功的回调函数, 失败的回调函数)
}
4.4 调用 resolve 和 reject 回调函数
Promise 异步操作的结果
,可以调用 resolve
或
回调函数进行处理。实例代码如下:
function getFile(fpath) {
// resolve 形参是:调用 getFile() 方法时,通过 .then 指定的“成功的”回调函数
// reject 形参是:调用 getFile() 方法时,通过 .then 指定的“失败的”回调函数
return new Promise(function(resolve, reject) {
fs.readFile(fpath, 'utf8', (err, dataStr) => {
if(err) return reject(err) // 如果读取失败,则调用“失败的回调函数”
resolve(dataStr) // 如果读取成功,则调用“成功的回调函数”
})
})
// getFile 方法的调用过程
getFile('./files/1.txt').then(成功的回调函数, 失败的回调函数)
}
async/await
1. 什么是 async/await
async/await
是 ES8
(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作
。在 async/await 出现之前,开发者只能通过 链式 .then() 的方式
处理 Promise 异步操作。
.then 链式调用的优点
:解决了回调地狱的问题
.then 链式 调用的缺点
:代码冗余、阅读性差、不易理解
2. async/await 的基本使用
使用 async/await 简化 Promise 异步操作的示例代码如下:
import thenFs from 'then-fs'
// 按照顺序读取文件 1,2,3 的内容
async funtion getAllFile() {
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
console.log(r1)
const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
console.log(r2)
const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
console.log(r3)
}
getAllFile()
3. async/await 的使用注意事项
- 如果在 function 中使用了 await,则 function
必须
被 async 修饰 - 在 async 方法中,
第一个 await 之前的代码会同步执行
,await 之后的代码会异步执行
console.log('A')
async function getAllFile() {
console.log('B')
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
console.log(r1, r2, r3)
console.log('D')
}
getAllFile()
console.log('C')
// 最终输出的顺序
A
B
C
111 222 333
D
EventLoop
1. JavaScript 是单线程的语言
JavaScript 是一门单线程执行
的编程语言。也就是说,同一时间只能做一件事情。
单线程执行任务队列的问题:
如果前一个任务非常耗时
,则后续的任务就不得不一直等待,从而导致程序假死
的问题。
2. 同步任务和异步任务
为了防止某个耗时任务
导致程序假死
的问题,JavaScript 把待执行的任务分为了两类:
-
同步任务
(synchronous)- 又叫做
非耗时任务
,指的是在主线程上排队执行的那些任务 - 只有前一个任务执行完毕,才能执行后一个任务
- 又叫做
-
异步任务(asynchronous)
-
又叫做
耗时任务
,异步任务由 JavaScript委托给
宿主环境进行执行 -
当异步任务执行完成后,会
通知 JavaScript 主线程
执行异步任务的回调函数
-
3. 同步任务和异步任务执行过程
- 同步任务由 JavaScript 主线程次序执行
- 异步任务
委托给
宿主环境执行 - 已完成的异步任务
对应的回调函数
,会被加入到任务队列中等待执行 - JavaScript 主线程的
执行栈
被清空后,会读取任务队列中的回调函数,次序执行 JavaScript 主线程不断重复上面的第4步
4. EventLoop 的基本概念
JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行
。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop
(事件循环)
5. 结合 EventLoop 分析输出的顺序
import thenFs from 'then-fs'
console.log('A')
thenFs.readFile('./files/1.txt', 'utf8').then(dataStr => {
console.log('B')
})
setTimeout(() => {
console.log('C')
}, 0)
console.log('D')
正确的输出结果:ADCB。其中:
- A 和 D 属于
同步任务
。会根据代码的先后顺序依次被执行
- C 和 B 属于
异步任务
。他们的回调函数会被加入到任务队列中,等待主线程空闲时再执行
宏任务和微任务
1. 什么是宏任务和微任务
JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
宏任务
(macrotask)- 异步 Ajax 请求
- setTimeout、setInterval
- 文件操作
- 其他宏任务
微任务
(microtask)- Promise.then、.catch 和 .finally
- process.nextTick
- 其他微任务
2. 宏任务和微任务的执行顺序
每一个宏任务执行完之后,都会检查是否存在待执行的微任务
如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
3.去银行办业务的场景
- 小云和小腾去银行办业务。首先,需要
取号之后进行排队
(宏任务队列) - 假设当前银行网店只有一个柜员,小云在办理存款业务时,
小腾只能等待
(单线程
,宏任务按次序执行
) - 小云办完存款业务后,柜员询问他
是否还想办理其他业务
? (当前宏任务执行完,检查是否有微任务
) - 小云告诉柜员:想要买理财产品,再办个信用卡、最后再兑换点马年纪念币? (执行微任务,后续
宏任务被推迟
) - 小云离开柜台后,柜员开始为小腾办理业务 (
所有微任务执行完毕
,开始执行下一个宏任务
)
4. 分析以下代码输出的顺序
setTimeout(function() {
console.log('1')
})
new Promise(function (resolve) {
console.log('2')
resolve()
}).then(function() {
console.log('3')
})
console.log('4')
正确输出顺序是:2431
分析:
- 先执行所有的
同步任务
- 执行第6行、第12行代码
- 再执行
微任务
- 执行第8行代码
- 再执行
下一个宏任务
- 执行第2行代码
5. 经典面试题
请分析以下代码输出的顺序
console.log('1');
setTimeout(function () {
console.log('2');
new Promise(function (resolve) {
console.log('3');
resolve();
}).then(function () {
console.log('4');
})
});
new Promise(function (resolve) {
console.log('5');
resolve()
}).then(function () {
console.log('6');
})
setTimeout(() => {
console.log('7');
new Promise(function (resolve) {
console.log('8');
resolve()
}).then(function () {
console.log('9');
})
});
正确的输出顺序是:1 5 6 2 3 4 7 8 9
总结
- 能够知道如何
使用 ES6 的模块化语法
- 默认导出与默认导入、按需导出与按需导入
- 能够知道
如何使用 Promise 解决回调地狱问题
- promise.
then
()、promise.catch
()
- promise.
- 能够使用
async/await
简化 Promise 的调用- 方法中用到了 await ,则方法需要被 async 修饰
- 能够说出什么是 EventLoop
EventLoop 示意图
- 能够说出宏任务和微任务的执行顺序
- 在执行下一个宏任务之前,
先检查是否有待执行的微任务
- 在执行下一个宏任务之前,
前端工程化与webpack
前端工程化
1. 小白眼中的前端开发 vs 实际的前端开发
小白眼中的前端开发:
- 会写HTML + CSS + JavaScript 就会前端开发
- 需要美化页面样式,就拽一个 bootstrap 过来
- 需要操作 DOM 或发起 Ajax 请求,再拽一个 jQuery 过来
- 需要渲染模板结构,就用 art-template 等模板引擎
实际的前端开发:
模块化
(js的模块化、css的模块化、其他资源的模块化)组件化
(复用现有的 UI 结构,样式,行为)规范化
(目录结构的划分、编码规范化、接口规划好、文档规范化、Git 分支管理)自动化
(自动化构建、自动部署、自动化测试)
2. 什么是前端工程化
前端工程化指的是:在企业级的前端项目开发
中,把前端开发所需的工具
、技术
、流程
、经验
等进行规范化、标准化。最终落实到细节上,就是实现前端的“4个现代化
”:
模块化、组件化、规范化、自动化
3. 前端工程化的好处
前端工程化的好处主要体现在如下两方面:
- 前端工程化
让前端开发能够“自成体系”
,覆盖了前端项目从创建到部署的方方面面 - 最大程度地提高了前端的开发效率,降低了技术选型、前后端联调等带来的协调沟通成本
webpack 的基本使用
1. 什么是 webpack
概念:webpack 是前端项目工程化的具体解决方案
。
主要功能:它提供了友好的前端模块化开发
支持,以及代码压缩混淆
、处理浏览器端 JavaScript 的兼容性
、性能优化
等强大的功能。
好处:让程序员把工作的重心
放到具体功能的实现上,提高了前端开发效率
和项目的可维护性
。
注意:目前企业级的前端项目开发中,绝大多数的项目都是基于 webpack 进行打包构建的。
2. 创建列表隔行变色项目
- 新建项目空白目录,并
运行 npm init -y
命令,初始化包管理配置文件package.json
- 新建
src
源代码目录 - 新建 src ->
index.html
首页 和 scr ->index.js
脚本文件 - 初始化首页基本结构
- 运行
npm install jquery -S
命令,安装jQuery - 通过 ES6 模块化的方式导入 jQuery,实现列表隔行变色效果
3. 在项目中配置 webpack
- 在项目根目录中,创建
webpack.config.js
的 webpack 配置文件,并初始化如下的基本配置:
module.exports = {
mode: 'development' // mode 用来指定构建模式。可选值有 development 和 production
}
- 在 package.json 的 scripts 节点下,新增
dev 脚本
如下:
"scripts": {
"dev": "webpack" // script 节点下的脚本,可以通过 npm run 执行。例如 npm run dev
}
3.1 mode 的可选值
mode 节点
的可选值有两个,分别是:
- development
开发环境
不会
对打包生成的文件进行代码压缩
和性能优化
- 打包
速度快
,适合在开发阶段
使用
- production
生产环境
会
对打包生成的文件进行代码压缩
和性能优化
- 打包
速度很慢
,仅适合在项目发布阶段
使用
3.2 webpack.config.js 文件的使用
webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件
,从而基于给定的配置,对项目进行打包。
注意:由于 webpack 是基于 node.js 开发出来的
打包工具,因此在它的配置文件中,支持使用node.js相关的语法和模块进行 webpack 的个性化配置。
3.3 webpack 中的默认约定
在 webpack 中有如下的默认约定
:
- 默认的打包入口文件为
src
->index.js
- 默认的输出文件路径为
dist
->main.js
注意:可以在 webpack.config.js
中修改打包的默认约定
3.4 自定义打包的入口与出口
在 webpack.config.js 配置文件中,通过 entry 节点
指定打包的入口
。通过 output 节点
指定打包的出口
。示例代码如下:
const path = require('path') // 导入 node.js 中专门操作路径的模块
module.exports = {
entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
output: {
path: path.join(__dirname, './dist'), // 输出文件的存放路径
filename: 'bundle.js' // 输出文件的名称
}
}
webpack 中的插件
1. webpack 插件的使用
通过安装和配置第三方的插件,可以拓展 webpack 的能力
,从而让 webpack 用起来更方便
。最常用的 webpack 插件有如下两个:
webpack-dev-server
- 类似于 node.js 阶段用到的 nodemon 工具
- 每当修改了源代码,webpack 会自动进行项目的打包和构建
html-webpack-plugin
- webpack 中的HTML插件(类似于一个模块引擎插件)
- 可以通过此插件自定制 index.html 页面的内容
2. webpack-dev-server
webpack-dev-server
可以让 webpack 监听项目源代码的变化
,从而进行自动打包构建
。
2.1 安装 webpack-dev-server
运行如下的命令,即可在项目中安装此插件:
npm install webpack-dev-server@3.11.0 -D
2.2 配置 webpack-dev-server
- 修改
package.json
->scripts
中的dev
命令如下
scripts": {
"dev": "webpack serve" // script 节点下的脚本,可以通过 npm run 执行
},
- 再次执行
npm run dev
命令,重新进行项目的打包 - 在浏览器中访问
http://localhost:8080
地址,查看自动打包效果
注意:webpack-dev-server 会启动一个实时打包的 http 服务器
2.3 打包生成的文件去哪儿了?
- 不配置 webpack-dev-server 的情况下,webpack 打包生成的文件,会存放到
实际的物理磁盘上
- 严格遵守开发者在 webpack.config.js 中指定配置
- 根据
output 节点
指定路径进行存放
- 配置了 webpack-dev-server 之后,打包生成的文件
存放到了内存中
- 不再根据 output 节点指定的路径,存放到实际的物理磁盘上
提高了
实时打包输出的性能
,因为内存比物理磁盘速度快很多
2.4 生成到内存中的文件该如何访问?
webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中
,而且是虚拟的、不可见的
。
3. html-webpack-plugin
html-webpack-plugin 是 webpack 中的HTML 插件
,可以通过此插件自定制
index.html 页面的内容
。
需求
:通过 html-webpack-plugin 插件,将 src 目录下的 index.html 首页,复制到项目根目录中一份
3.1 安装 html-webpack-plugin
运行如下的命令,即可在项目中安装此插件:
npm install html-webpack-plugin@4.5.0 -D
3.2 配置 html-webpack-plugin
// 1. 导入 HTML 插件,得到一个构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建 HTML 插件的实例对象
const htmlPlugin = new HtmlPlugin({
template: './src/index.html', // 指定原文件的存放路径
filename: './index.html', // 指定生成的文件的存放路径
})
module.exports = {
mode: 'development',
plugins: [htmlPlugin], // 3. 通过 plugins 节点,使 htmlPlugin 插件生效
}
3.3 解惑 html-webpack-plugin
- 通过 HTML 插件复制到项目根目录中的 index.html 页面,
也别放到了内存中
- HTML 插件在生成的 index.html
页面的底部
,自动注入
了打包的 bundle.js 文件
4. devServer 节点
在 webpack.config.js 配置文件中,可以通过 devServer
节点对 webpack-dev-server 插件进行更多的配置,示例代码如下:
devServer: {
open: true, // 初次打包完成后,自动打开浏览器
host: '127.0.0.1', // 实时打包所使用的主机地址
port: 80, // 实时打包所使用的端口号
}
webpack 中的 loader
1. loader 概述
在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块
,webpack默认处理不了,需要调用 loader 加载器才可以正常打包
,否则会报错!
loader 加载器的作用:协助 webpack 打包处理特定文件模块
。比如:
- css-loader 可以打包处理 .css 相关的文件
- less-loader 可以打包处理 .less 相关的文件
- babel-loader 可以打包处理 webpack 无法处理的高级 js 语法
2. loader 的调用过程
3. 打包处理 css 文件
- 运行 命令,安装处理 css 文件的 loader
- 在 webpack.config.js 的
module
->rules
数组中,添加 loader 规则如下:
module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.css$/, use: ['style-loader', 'css-loader']}
]
}
其中,test
表示匹配的文件类型
,use
表示对应要调用的 loader
注意:
- use 数组总指定的 loader
顺序是固定的
- 多个 loader 的调用顺序是:
从后往前调用
4. 打包处理 less 文件
- 运行 npm i less-loader@7.1.0 less@3.12.2 -D 命令
- 在 webpack.config.js 的
module
->rules
数组中,添加 loader 规则如下:
module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
]
}
5. 打包处理样式表中与 url 路径相关的文件
- 运行 npm i
url-loader@4.1.1 file-loader@6.2.0
-D 命令 - 在 webpack.config.js 的
module
->rules
数组中,添加 loader 规则如下:
module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.jpg|png|gif$/, use: ['url-loader?limit=22229']}
]
}
其中 ?
之后的是 loader 的参数项
:
- limit 用来指定
图片的大小
,单位是字节(byte) - 只有
<=
limit 大小的图片,才会被转为 base64 格式的图片
5.1 loader 的另一个配置方式
带参数的 loader
还可以通过对象的方式
进行配置:
module: { // 用来处理所有的第三方模块
rules: [ // 第三方模块的匹配规则
{
test: /\.jpg|png|git/, // 匹配图片文件
use: {
loader: 'url-loader', // 通过 loader 属性指定要调用的 loader
options: { // 通过 options 属性指定参数项
limit: 22229
}
}
}
]
}
6. 打包处理 js 文件中的高级语法
webpack 只能打包处理一部分
高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法,需要借助于 babel-loader
进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码:
class Person {
// 通过 static 关键字,为 Person 类定义了一个静态属性 info
// webpack 无法打包处理“静态属性”这个高级语法
static info = 'person info'
}
console.log(Person.info)
6.1 安装 babel-loader 相关的包
运行如下的命令安装对应的依赖包:
npm i babel-loader@8.2.1 @babel/core@7.12.3 @babel/plugin-proppsal-class-properties@7.12.1 -D
包的名称及版本号列表如下(红色是包的名称,黑色是包的版本号):
babel-loade
r@8.2.1@babel/core
@7.12.3@babel/plugin-proposal-class-properties
@7.12.1
6.2 配置 babel-loader
在 webpack.config.js 的 module
-> rules
数组中,添加 loader 规则如下:
{
test: /\.js$/,
// exclude 为排除项,
// 表示 babel-loader 只需处理开发者编写的 js 文件,不需处理 node_modules 下的 js 文件
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { // 参数项
// 声明一个 babel 插件,此插件用来转化 class 中的高级语法
plugins: ['@babel/plugin-proppsal-class-properties'],
}
}
}
打包发布
1. 为什么要打包发布
项目开发完成之后
,使用 webpack 对项目进行打包发布
的主要原因有以下两点:
- 开发环境下,打包生成的文件存
放于内存中
,无法获取到最终打包生成的文件 - 开发环境下,打包生成的文件
不会进行代码压缩和性能优化
2. 配置 webpack 的打包发布
在 package.json
文件的 scripts
节点下: 新增 build 命令如下:
"scripts": {
"dev": "webpack serve", // 开发环境中, 运行 dev 命令
"build": "webpack --mode production" // 项目发布时,运行 build 命令
}
–model
是一个参数项,用来指定 webpack 的运行模式
。production 代表生产环境,会对打包生成的文件进行代码压缩
和性能优化
。
注意:通过 –model 指定的参数项,会覆盖
webpack.config.js 中的 model 选项。
3. 把 JavaScript 文件统一生成到 js 目录中
在 webpack.config.js 配置文件的 output 节点中,进行如下的配置:
output: {
path: path.join(__dirname, 'dist');
// 明确告诉 webpack 把生成的 bundle.js 文件存放到 dist 目录下的 js 子目录中
filename: 'js/bundle.js'
}
4. 把图片文件统一生成到 image 目录中
修改 webpack.config.js 中 的url-loader 匹配项,新增 outputPath 选项即可指定图片文件的输出路径:
{
test: /\.jpg|png|gif$/,
use: {
loader: 'url-loader',
options: {
limit: 22229
// 明确指定把打包生成的图片文件,存储到 dist 目录下的image 文件夹中
outputPath: 'image'
}
}
}
5. 自动清理 dist 目录下的旧文件
为了在每次打包发布时自动清理掉 dist 目录中的旧文件
,可以安装并配置 clean-webpack-plugin
插件:
// 1. 安装清理 dist 目录的 webpack 插件
npm i clean-webpack-plugin@3.0.0 -D
// 2. 按需导入插件、得到插件的构造函数之后,创建插件的实例对象
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const cleanPlugin = new CleanWebpackPlugin()
// 3. 把创建的 cleanPlugin 插件实例对象,挂载到 plugins 节点中
plugins: [htmlPlugin, cleanPlugin] // 挂载插件
6. 企业级项目的打包发布
企业级的项目在进行打包发布时,远比刚才的方式要复杂的多,主要的发布流程如下:
- 生成打包报告,根据报告分析具体的优化方案
- Tree-Shaking
- 为第三方库启用 CDN 加载
- 配置组件的按需加载
- 开启路由懒加载
- 自定制首页内容
Source Map
1.生产环境遇到的问题
前端项目在投入生产环境之前,都需要对 JavaScript 源代码进行压缩混淆
,从而先小文件的体积,提高文件的加载效率。此时就不可避免的产生了另一个问题:
对压缩混之后的代码除错(debug)
是一件极其困难的事情
- 变量被替换成
没有任何语义
的名称 - 空行和注释被剔除
2. 什么是 Source Map
Source Map 就是一个信息文件,里面储存这位置信息
。也就是说,Source Map 文件中存储着代码压缩混淆前后
的对应关系
。
有了它,出错的时候,除错工具将直接显示原始代码,而不是转化后的代码
,能够极大的方便后期的调试。
3. webpack 开发环境下的 Source Map
在开发环境下
,webpack 默认启用了
Source Map 功能。当程序运行出错时,可以直接在控制台提示错误行的位置
,并定位到具体的源代码
3.1 默认 Source Map 的问题
开发环境下默认生成的 Source Map,记录的是生成后的代码的位置
。会导致运行时报错的行数
与源代码的行数
不一致的问题
3.2 解决默认 Source Map 的问题
开发环境下,推荐在 webpack.config.js
中添加如下的配置,即可保证运行时报错的行数
与源代码的行数
保持一致:
module.exports = {
mode: 'development',
// eval-source-map 仅限在“开发模式”下使用,不建议在“生产模式”下使用
// 此选项生成的 Source Map 能够保证“运行是报错的行数”与源代码的行数“保持一致
devtool: 'eval-source-map',
// 省略其他配置项...
}
4. webpack 生产环境下的 Source Map
在生成环境下
,如果省略了 devtool 选项
,则最终生成的文件中不包含 Source Map
。这能够防止原始代码
通过 Source Map 的形式暴露
给别有所图之人
4.1 只定位行数不暴露源码
在生产环境下,如果只想定位报错的具体行数
,且不想暴露源码
。此时可以将 devtool 的值设置为 nosources-source-map
。
4.2 定位行数且暴露源码
在生产环境下,如果想在定位报错行数的同时
,展示具体报错的源码
。此时可以将 devtool
的值设置为 source-map
。
5. Source Map 的最佳实践
开发环境下:
- 建议把 devtool 的值设置为
eval-source-map
- 好处:可以精准定位到具体的错误行
生产环境下:
- 建议
关闭 Source Map
或将 devtool 的值设置为 - 好处:防止源码泄露,提高网站的安全性
总结
-
能够掌握 webpack 的基本使用
- 安装、
webpack.config.js
、修改打包入口
- 安装、
-
了解常用的 plugin 的基本使用
- webpack-dev-server、html-webpack-plugin
-
了解常用的 loader 的基本使用
- loader 的作用、
loader 的调用过程
- loader 的作用、
-
能够说出 Source Map 的作用
- 精准定位到
错误行
并显示对应的源码
- 方便开发者调试源码中的错误
- 精准定位到
vue 基础入门
vue 简介
1. 什么是 vue
官方给出的概念:Vue是一套用于构建用户界面
的前端框架
1.1 解读核心关键词:构建用户界面
前端开发者最主要的工作,就是为网站的使用者
(又称为:网站的用户)构建出美观、舒适、好用的网页。
1.2 构建用户界面的传统方式
在传统的 web 前端开发中,是基于 jQuery + 模板引擎
的方式来构建用户界面的
1.3 使用 vue 构建用户界面
使用 vue 构建用户界面,解决了 jQuery + 模块引擎
的诸多痛点,极大的提高了前端开发的效率和体验。
1.4 解读核心关键词:框架
官方给 vue 的定位是前端框架
,因为它提供了构建用户界面的一整套解决方案
(俗称 vue 全家桶):
- vue (核心库)
- vue-router(路由方案)
- vuex(状态管理方案)
- vue 组件库(快速搭建页面 UI 效果的方案)
以及辅助 vue 项目开发
的一系列工具:
- vue-cil (npm 全局包:一键生成工程化的 vue项目 - 基于webpack、大而全)
- vite(npm全局包:一键生成工程化的 vue 项目 - 小而巧)
- vue-devtools(浏览器插件:辅助调试的工具)
- vetur(vscode 插件:提供语法高亮和智能提示)
1.5 总结:什么是 vue
2. vue 的特性
vue 框架的特性,主要体现在如下两个方面:
数据驱动视图
双向数据绑定
2.1 数据驱动视图
在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。示意图如下:
好处:当页面数据发生变化时,页面会自动重新渲染!
注意:数据驱动视图是单向的数据绑定
2.2 双向数据绑定
在填写表单
时,双向数据绑定可以辅助开发在不操作 DOM 的前提
下,自动
把用户填写的内容同步到
数据源中
2.3 MVVM
MVVM
是 vue 实现数据驱动视图
和双向数据绑定
的核心原理。它把每个 HTML 页面 都拆分成了如下三个部分:
在 MVVM 概念中:
View 表示当前页面所渲染的 DOM 结构。
Model 表示当前页面渲染时所依赖的数据源。
ViewModel 表示 Vue 的实例,它是 MVVM 的核心
2.4 MVVM 的工作原理
ViewModel 作为 MVVM 的核心
,是它把当前页面的数据源
(Model)和页面的结构
(View)连接在了一起。
3. vue 的版本
当前,vue 共有 3 个大版本,其中:
2.x 版本的 vue 是目前企业级项目开发中的主流版本
3.x 版本的 vue 于 2020-09-19 发布,生态还不完善,尚未在企业级项目开发中普及和推广
1.x 版本的 vue 几乎被淘汰,不再建议学习与使用
总结:
3.x 版本的 vue 是未来企业级项目开发的趋势;
2.x 版本的 vue 在未来(1~2年内)会被逐渐淘汰;
3.1 vue3.x 和 vue2.x 版本的对比
vue2.x 中绝大多数的 API 与特性
,在 vue3.x 中同样支持
。同时,vue3.x 中还新增了 3.x 所特有的功能
、并废弃了某些 2.x 中的旧功能
:
新增的功能例如:
组合式API
、多根节点组件、更好的 typescript 支持等
废弃的旧功能如下:
过滤器
、不在支持$on,$off 和 $once 实例方法等
vue 的基本使用
1. 基本使用步骤
- 导入 vue.js 的script 脚本文件
- 在页面中声明一个将要被 vue 所控制的 DOM 区域
- 创建 vm 实例对象 (vue 实例对象)
2. 基本代码与 MVVM 的对应关系
vue 的调试工具
1. 安装 vue-devtools 调试工具
vue 官方提供的 vue-devtools 调试工具,能够方便开发者对 vue 项目进行调试与开发。
在线安装 vue-devtools
vue 的指令与过滤器
1. 指令的概念
指令(Directives)
是 vue 为开发者提供的模块语法
,用于辅助开发者渲染页面的基本结构
。
vue 中指令按照不同的用途
可以分为如下 6 大类:
内容渲染
指令属性绑定
指令事件绑定
指令双向绑定
指令条件渲染
指令列表渲染
指令
1.1 内容渲染指令
内容渲染指令
用来辅助开发者渲染 DOM 元素的文本内容
。常用的内容渲染指令有如下 3 个:
- v-text
- {{ }}
- v-html
v-text
用法示例:
<!-- 把 username 对应的值,渲染到第一个 p 标签中 -->
<p v-text="username"></p>
<!-- 把 gender 对应的值,渲染到第二个 p 标签中 -->
<!-- 注意:第二个 p 标签中,默认的文本“性别”会被 gender 的值覆盖掉 -->
<p v-text="gender">性别</p>
注意:v-text 指令会覆盖元素内默认的值
{{ }}语法
vue 提供的 {{ }}
语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这个{{ }}语法的专业名称是插值表达式
(英文名为:Mustache
)
<!-- 使用{{ }} 插值表达式,将对应的值渲染到元素的内容节点中 -->
<!-- 同时保留元素自身的默认值 -->
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
v-html
v-text指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html 这个指令:
<!-- 假设 data 中定义了名为 discription 的数据,数据的值包含 HTML 标签字符串: -->
<!-- '<h5 style="color: red;">我在黑马程序员学习 vue.js 课程。</h5>' -->
<p v-html="discription"></p>
1.2 属性绑定指令
如果需要为元素的属性
动态绑定属性值
,则需要用到 v-bind
属性绑定指令。用法示例如下:
<!-- 假设有如下的 data 数据:
data: {
inputValue: '请输入内容',
imgSrc: 'https://cn.vuejs.org/images/logo.png'
}
-->
<!-- 使用 v-bind 指令,为 input 的 placeholder 动态绑定属性值 -->
<input type="text" v-bind:placeholder="inputValue">
<br>
<!-- 使用 v-bind 指令,为 img 的src 动态绑定属性值 -->
<img v-bind:src="imgSrc" alt="">
使用JavaScript 表达式
在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值
之外,还支持 JavaScript 表达式的运算
,例如:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id"></div>
1.3 事件绑定指令
vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。语法格式如下:
<h3>count 的值:{{count}}</h3>
<!-- 语法格式为 v-on:事件名称=“事件处理函数的名称” -->
<button v-on:click="addCount">+1</button>
注意:原生 DOM 对象 有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,
分别为:v-on:click、v-on:input、v-on:keyup
通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明:
const vm = new Vue({
el: '#app',
data: { count: 0 },
methods: { // v-on 绑定的事件处理函数,需要声明再 methods 节点中
addCount() { // 事件处理函数的名字
// this 表示当前 new 出来的 vm 实例对象
// 通过 this 可以访问到 data 中的数据
this.count += 1;
}
}
})
事件绑定的简写形式
<div id="app">
<h3>count 的值为: {{count}}</h3>
<!-- 完整写法 -->
<button v-on:cilck="addCount">+1</button>
<!-- 简写形式,把 v-on:简写为 @ 符号 -->
<!-- 如果事件处理函数中的代码足够简单,只有一行代码,则可以建写道行内 -->
<button @click="count += 1">+1</button>
</div>
事件对象 event
在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件对象 event。同理,在 v-on 指令
(简写为 @
)所绑定的事件处理函数中,同样可以接收到事件对象 event
,示例代码如下:
<h3>count 的值为 {{count}}</h3>
<button @:click="addCount">+1</button>
----------------------- 分割线 -----------------------
methods: {
addCount(e) { // 接收事件参数对象 event,简写为 e
const nowBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
this.count += 1
}
}
绑定事件并传参
在使用 v-on 指令绑定事件时,可以使用 ( )
进行传参,示例代码如下:
<h3>count 的值为:{{count}}</h3>
<button @click="addNewCount(2)">+2</button>
// ----------------- 分割线 ------------------
methods: {
// 在形参处用 step 接收传递过来的参数值
addNewCount(step) {
this.count += step
}
}
$event
$event
是 vue 提供的特殊变量
,用来表示原生的事件参数对象 event
。$event 可以解决事件参数对象 event 被覆盖的问题。示例用法如下:
<h3>count 的值为:{{count}}</h3>
<button @click="addNewCount(2, $event)">+2</button>
// ----------------- 分割线 ------------------
methods: {
// 在形参处用 e 接收传递过来的原生事件参数对象 $event
addNewCount(step, e) {
const newBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = newBgColor === 'red' ? '' : 'red'
this.count += step
}
}
事件修饰符
在事件处理函数中调用 preventDefault()
或 stopPropagation()
是非常常见的需要。因此,vue 提供了事件修饰符
的概念,用来辅助程序员更方便的对事件的触发进行控制
。常用的 5 个事件修饰符如下:
事件修饰符 | 说明 |
---|---|
.prevent | 阻止默认行为 (例如:阻止 a 链接的跳转、阻止表单的提交等) |
.stop | 阻止事件冒泡 |
.capture | 以捕获模式触发当前的事件处理函数 |
.once | 绑定的事件只触发一次 |
.self | 只有在 event.target 是当前元素自身时触发事件处理函数 |
语法格式如下:
<!-- 触发 click 点击事件时,阻止 a 链接的默认跳转行为 -->
<a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>
按键修饰符
在监听键盘事件
时,我们经常需要判断详细的按键
。此时,可以为键盘相关的事件
添加按键修饰符
,例如:
<!-- 只有在 ‘key’ 是 'Enter' 时调用 `vm.submit()` -->
<input type="text" @keyup.enter="submit">
<!-- 只有在 `key` 是 `Esc` 时调用 `vm.clearInput()` -->
<input type="text" @keyup.esc="clearInput">
1.4 双向绑定指令
vue 提供了 v-model 双向数据绑定
指令,用来辅助开发者在 不操作 DOM
的前提下,快速获取表单的数据
。
<p>用户名是: {{username}}</p>
<input type="text" v-model="username">
<p>选中的省份是:{{province}}</p>
<select name="" id="" v-model="province">
<option value="">请选择</option>
<option value="1">北京</option>
<option value="2">河北</option>
<option value="3">黑龙江</option>
</select>
v-model 指令的修饰符
为了方便对用户输入的内容进行处理
,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.5 条件渲染指令
条件渲染指令
用来辅助开发者按需控制 DOM 的显示与隐藏
。条件渲染指令有如下两个,分别是:
- v-if
- v-show
v-if 和 v-show 的区别
实现原理不同:
- v-if 指令会
动态的创建或移除 DOM 元素
,从而控制元素在页面上的显示与隐藏; - v-show 指令会动态为元素
添加或移除 style="display: none;" 样式
,从而控制元素的显示与隐藏;
性能消耗不同:
v-if
有更高的切换开销
,而 v-show
有更高的初始渲染开销
。
- 如果需要
非常频繁地切换
,则使用 v-show 较好 - 如果在
运行是条件很少改变
,则使用 v-if 较好
v-else
v-if 可以单独使用,或配合 v-else 指令一起使用:
<div v-if="Math.random() > 0.5">
随机数大于 0.5
</div>
<div>
随机数小于或等于 0.5
</div v-else>
v-else-if
v-else-if 指令,顾名思义,充当 v-if 的 “else-if 块“,可以连续使用:
<div v-if="type === 'A'">优秀</div>
<div v-else-if="type === 'B'">良好</div>
<div v-else-if="type === 'C'">一般</div>
<div v-else>差</div>
1.6 列表渲染指令
vue 提供了 v-for
指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构
。
v-for 指令需要使用 item in items
的特殊语法,其中:
- items 的
待循环的数组
- item 是
当前的循环项
data: {
list: [ // 列表数据
{id: 1, name: 'zs'},
{id: 2, name: 'ls'}
]
}
// -------------- 分割线 ----------------
<ul>
<li v-for="item in list">姓名是: {{item.name}}</li>
</ul>
v-for 中是索引
v-for 指令还支持一个可选的第二个参数,即当前的索引。语法格式为 (item, index) in items,示例代码如下:
data: {
list: [
{id: 1, name: 'zs'},
{id: 2, name: 'ls'}
]
}
// -------- 分割线 -------
<ul>
<li v-for="(item, index) in list">索引是:{{index}}, 姓名是:{{item.name}}</li>
</ul>
注意:v-for 指令中的 item 项
和 index 索引
都是形参,可以根据需要进行重命名
。例如(user, i
) in userlist
使用 key 维护列表的状态
当列表的数据变化
时,默认情况下,vue 会尽可能的复用
已存在的 DOM 元素,从而提升渲染的性能
。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新
。
为了给 vue 一个提示,以便它 跟踪每个节点的身份,从而在保证有状态的列表被正确更新
的前提下,提升渲染的性能
。此时,需要为每项提供一个唯一的 key 属性
:
<!-- 用户列表区域 -->
<ul>
<!-- 加 key 属性的好处: -->
<!-- 1. 正确维护列表的状态 -->
<!-- 2. 复用现有的 DOM 元素,提升渲染的性能 -->
<li v-for="user in userlist" :key="user.id">
<input type="checkbox">
姓名:{{user.name}}
</li>
</ul>
key 的注意事项
- key 的值只能是
字符串
或者数字
类型 - key 的值
必须具有唯一性
(即:key 的值不能重复) - 建议把
数据项 id 属性的值
作为 key 的值(因为 id 属性的值具有唯一性) - 使用
index 的值
当作 key 的值没有任何意义
(因为 index 的值不具有唯一性) - 建议使用 v-for 指令是
一定要指定 key 的值
(既提升性能、有防止列表状态紊乱)
2. 过滤器
过滤器
(Filters
)常用于文本的格式化
。例如:
hello -> Hello
过滤器应该被添加在 JavaScript 表达式的尾部
,由“管道符
”进行调用,示例代码如下:
<!-- 在双花括号中通过"管道符"调用 capitalize 过滤器,对 message 的值进行格式化 -->
<p> {{ message | capitalize }} </p>
<!-- 在 v-bind 中通过“管道符”调用 formatId 过滤器,对 rawId 的值进行格式化 -->
<div v-bind:id="rawId | formatId"></div>
过滤器可以用在两个地方:插值表达式
和 v-bind 属性绑定
。
2.1 定义过滤器
在创建 vue 实例期间,可以在 filters 节点
中定义过滤器,示例代码如下:
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js',
info: 'title info'
},
filters: { // 在 filters 节点下定义“过滤器”
capitalize (str) { // 把首字母转为大写的过滤器
return str.charAt(0).toUpperCase + str.slice(1)
}
}
})
2.2 私有过滤器
和全局过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器
”,因为它只能在当前 vm 实例所控制的 el 区域内使用
。
如果希望在多个 vue 实例之间共享过滤器
,则可以按照如下的格式定义全局过滤器
:
// 全局过滤器 - 独立于每个 vm 实例之外
// Vue.filter() 方法接收两个参数:
// 第一个参数:是全局过滤器的“名字”
// 第二个参数,是全局过滤器的“处理函数”
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase + str.slice(1) + '~~'
})
2.3 连续调用多个过滤器
示例代码如下:
// 串联调用多个过滤器
<p> {{text | capitalize | maxLength }} </p>
// 全局过滤器 - 首字母大写
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
})
// 全局过滤器 - 控制文本的最大长度
Vue.filter('maxLength', (str) => {
if (str.length <= 10) return str
return str.slice(1, 10) + '...'
})
2.4 过滤器传参
过滤器的本质
是 JavaScript 函数
,因此可以接收参数,格式如下:
// arg1 和 arg2 是传递给 filterA 的参数
<p> {{message | filterA(arg1, arg2)}} </p>
// 过滤器处理函数的形参列表中:
// 第一个参数:永远都是“管道符”前面待处理的值
// 从第二参数开始,才是调用过滤器是传递过来的 arg1 和 arg2 参数
Vue.filter('filterA', (msg, arg1, arg2) => {
// 过滤器的代码逻辑...
})
示例代码如下:
// 调用 maxLength 过滤器时传参
<p> {{text | capitalize | maxLength(5) }} </p>
// 全局过滤器 - 首字母大写
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
})
// 全局过滤器 - 控制文本的最大长度
Vue.filter('maxLength', (str, len = 10) => {
if (str.length <= len) return str
return str.slice(1, len) + '...'
})
2.5 过滤器的兼容性
过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x
的版本中 剔除了过滤器
相关的功能。
在企业级项目的开发中:
- 如果使用的是 2.x 版本的 vue 则依然可以使用过滤器相关的功能
- 如果项目已经升级到了 3.x 版本的 vue,官方建议使用
计算机属性
或方法
代替被剔除的过滤器功能
总结
- 能够真的 vue 的
基本使用步骤
- 导入 vue.js 文件
- new Vue() 构造函数,得到 vm 实例对象
- 声明 el 和 data 数据节点
- MVVM 的对应关系
- 掌握 vue 中常见的
指令
的基本用法- 插值表达式、v-bind、v-on、v-if 和 v-else
- v-for 和 :key、v-model
- 掌握 vue 中
过滤器
的基本用法- 全局过滤器 Vue.filter(‘过滤器名称’, funciton)
- 私有过滤器 filters 节点
组件基础(上)
单页面应用程序
1. 什么是单页面应用程序
单页面应该程序
(英文名 S
ingle P
age A
pplication)简称 SPA,顾名思义,指的是一个 Web 网站上 只有唯一的一个 HTML 页面
,所有的功能与交互都在这唯一的一个页面完成。
2. 单页面应用程序的特点
单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源
(HTML、JavaScript 和 CSS)。
一旦页面加载完成了,SPA不会
因为用户的操作而进行页面的重新加载或跳转
。而是利用 JavaScript 动态的变换 HTML 内容,从而实现页面与用户的交互
3. 单页面应用程序的优点
SPA 单页面应用程序最显著的3个优点如下:
良好的交互体验
- 单页应用的内容的改变不需要重新加载整个页面
- 获取数据也是通过Ajax 异步获取
- 没有页面之间的跳转,不会出现“白屏现象”
良好的前后端工作分离模式
- 后端专注于提供 API 接口,更易实现 API 接口的复用
- 前端专注于页面的渲染,更利于前端工程化的发展
减轻服务器的压力
- 服务器只提供数据,不负责页面的合成与逻辑的处理,吞吐能力会提高几倍
4. 单页面应用程序的缺点
任务一种技术都有自己的局限性
,对于 SPA 单页面应用程序来说,主要的缺点有如下两个:
首屏加载慢
- 路由懒加载
- 代码压缩
- CDN 加速
- 网络传输压缩
不利于 SEO
- SSR 服务器端渲染
5. 如何快速创建 vue 的 SPA 项目
vue 官方提供了两种
快速创建工程化的 SPA 项目的方式:
- 基于
vite
创建 SPA 项目 - 基于
vue-cli
创建 SPA 项目
vite | vue-cli | |
---|---|---|
支持的 vue 版本 | 仅支持 vue3.x | 支持 3.x 和 2.x |
是否基于 webpack | 否 | 是 |
运行速度 | 快 | 较慢 |
功能完整度 | 小而巧(逐渐完善) | 大而全 |
是否建议在企业级开发中使用 | 目前不建议 | 建议在企业级开发中使用 |
vite 的基本使用
1. 创建 vite 的项目
按照顺序执行如下的命令,即可基于 vite 创建 vue 3.x 的工程化项目:
npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev f
2. 梳理项目的结构
使用 vite 创建的项目结构如下:
其中:
- node_modules 目录用来存放第三方依赖包
- public 是公共的静态资源目录
src
是项目的源代码目录(程序员写的所有代码都要放在此目录下)- .gitignore 是 Git 的忽略文件
index.html
是 SPA 单页面应用程序中唯一的 HTML 页面- package.json 是项目的包管理配置文件
在 src 这个项目源代码目录之下,包含了如下的文件和文件夹:
其中:
assets
目录用来存放项目中所有的静态资源文件
(CSS,fonts等)components
目录用来存放项目中的所有的自定义组件
App.vue
是项目的根组件
index.css
是项目的全局样式表
文件main.js
是整个项目的打包入口文件
3. vite 项目的运行流程
在工程化的项目中,vue 要做的事情很单纯:通过 main.js
把 App.vue
渲染到 index.html
的指定区域中。
其中:
App.vue
用来编写待渲染的模板结构
index.html
中需要预留一个el 区域
main.js
把 App.vue 渲染到了 index.html 所预留的区域中
3.3 在 main.js 中进行渲染
按照 vue 3.x
的标准用法
,把 APP.vue 中的模板内容
渲染到 index.html 页面的 el 区域
中:
// 1. 从 vue 中按需导入 createApp 函数
//creatApp 函数的作用:创建 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')
组件开发思想
1. 什么是组件化开发
组件化开发
指的是:根据封装
的思想,把页面上可重用的部分封装为组件
,从而方便项目的开发和维护。
例如:http://www.ibootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。用户可以通过拖拽组件的方式,快速生成一个页面的布局结构。
2. 组件化开发的好处
前端组件化开发的好处主要体现在以下两方面:
- 提高了前端代码的
复用性
和灵活性
- 提升了
开发效率
和后期的可维护性
3. vue 中的组件开发
vue 是一个完全支持组件化开发
的框架。vue 中规定组件的后缀名
是 .vue
。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。
vue 组件的构成
1. vue 组件组成结构
每个 .vue 组件都由 3 部分构成,分别是:
template
-> 组件的模板结构
script
-> 组件的JavaScript 行为
style
-> 组件的样式
其中,每个组件中必须包含 template 模板结构
,而 script 行为
和 style 样式
是可选的
组成部分
2. 组件的 template 节点
vue 规定:每个组件对应的模板结构
,需要定义到 <tepmlate> 节点
中。
<template>
<!-- 当前组件的 DOM 结构,需要定义到 template 标签的内部 -->
</template>
注意:<template> 是 vue 提供的容器标签
,只起到包裹性质的作用
,它不会被渲染为真正的 DOM 元素。
2.1 在 template 中使用指令
在组件的<template> 节点中,支持使用前面所学的指令语法
,来辅助开发者渲染当前组件的 DOM 结构。
代码示例如下:
<template>
<h1>这是 App.vue 根组件</h1>
<!-- 使用 {{ }} 插值表达式 -->
<p>生成一个随机数字:{{ (Math.random() * 10).toFixed(2) }}</p>
<!-- 使用 v-bind 属性绑定 -->
<p :title="new Date().toLocaleTimeString()">我在黑马程序员学习</p>
<!-- 属性 v-on 事件绑定 -->
<button @click="showInfo">按钮</button>
</template>
2.2 在 template 中定义根节点
在 vue 2.x
的版本中,<template> 节点内的 DOM 结构仅支持单个根节点
<template>
<!-- vue 2.x 中,template 节点内的所有元素,最外层“必须有”唯一的根节点进行包裹,否则报错 -->
<h1>这是 App.vue 根组件</h1>
<h2>这是副标题</h2>
</template>
3. 组件的 script 节点
vue 规定,组件内的 <script> 节点是可选的
,开发者可以在 <script> 节点中封装组件的 JavaScript 业务逻辑
。
<script> 节点的基本结构如下:
<script>
// 今后,组件相关的 data 数据、methods 方法等,
// 都需要定义到 export default 所导出的对象中。
export default {}
</script>
3.1 script 中的 name节点
可以通过 name 节点为当前组件定义一个名称:
<script>
export default {
// name 属性指向的是当前组件的名称(建议: 每个单词的首字母大写)
name: 'MyApp',
}
</script>
在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件
3.2 script 中的 data 节点
vue 组件渲染期间
需要用到的数据
,可以定义在 data 节点中:
<script>
// 今后,组件相关的 data 数据、methods 方法等,
// 都需要定义到 export default 所导出的对象中。
export default {
// 组件的名称
name: 'MyApp',
// 组件的数据(data 方法中 return 出去的对象,就是当前组件渲染期间需要用到的数据对象)
data() {
return {
username: '娃哈哈'
}
}
}
</script>
3.3 script 中的 methods
节点
组件中的事件处理函数
,必须定义到 methods 节点
中,示例代码如下:
<script>
// 今后,组件相关的 data 数据、methods 方法等,
// 都需要定义到 export default 所导出的对象中。
export default {
// 组件的名称
name: 'MyApp',
// 组件的数据(data 方法中 return 出去的对象,就是当前组件渲染期间需要用到的数据对象)
data() {
return {
count: 0
}
},
methods: { // 处理函数
addCount() {
this.count++
}
}
}
</script>
4. 组件的 style 节点
vue 规定:组件内的 <style> 节点是可选的
,开发者可以在 <style> 节点中编写样式美化当前组件的 UI 结构
。
<style lang="css">
h1 {
font-weight: normal;
}
</style>
其中 <style> 标签上的 lang="css"
属性是可选的
,它表示所使用的样式语言
。默认只支持普通的 css 语法
,可选值还有 less、scss等。
4.1 让 style 中支持 less 语法
如果希望使用 less 语法编写组件的 style 样式,可以按照如下两个步骤进行配置:
- 运行
npm install less -D
命令安装依赖包,从而提供 less 语法的编译支持 - 在 <style> 标签上添加
lang="less"
属性,即可使用 less 语法编写组件的样式
<style lang="less">
h1 {
font-weight: normal;
i {
color: red;
font-style: normal;
}
}
</style>
组件的基本使用
组件之间可以进行相互的引用
,例如:
vue 中组件的引用
原则:先注册后使用
。
1.1 注册组件的两种方式
vue 中注册组件的方式分为“全局注册
”和”局部注册
“两种,其中:
- 被
全局
注册的组件,可以在全局任何一个组件内使用
- 被
局部
注册的组件,只能在当前注册的范围内使用
1.2 全局注册组件
// 1. 按需导入 createApp 函数
import { createApp } from 'vue'
// 2. 导入待渲染的 App.vue 组件
import App from './App.vue'
// (1) 导入 Swiper 和 Test 两个组件
import Swiper from './components/MySwiper.vue'
import Test from './components/MyTest.vue'
// 3. 调用 createApp 函数 创建 SPA 应用的实例
const app = createApp(App)
// (2) 调用 app 实例的component() 方法,在全局注册 my-swiper 和 my-test 两个组件
app.component('my-swiper', Swiper)
app.component('my-test', Test)
// 4. 调用 mount() 把 app 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')
1.3 使用全局注册组件
使用 app.component() 方法注册的全局组件,直接以标签的形式进行使用
即可,例如:
// 在 main.js 中,注册了 my-swiper 和 my-test 两个全局组件
spa_app.component('my-swiper', Swiper)
spa_app.component('my-test', Test)
<!-- 在其他组件中,直接以标签的形式,使用刚才注册的全局组件即可 -->
<template>
<h1>
这是 App 根组件
</h1>
<hr/>
<my-swiper></my-swiper>
<my-test></my-test>
</template>
1.4 局部注册组件
<template>
<h1>
这是 App 根组件
</h1>
<hr/>
<my-swiper></my-swiper>
<my-test></my-test>
</template>
<script>
import Search from './component/MySearch.vue'
export default {
components: { // 通过 components 节点,为当前的组件组件私有子组件
'my-search': Search,
}
}
</script>
1.5 全局注册和局部注册的区别
- 被
全局
注册的组件,可以在全局任何一个组件内使用
- 被
局部
注册的组件,只能在当前注册的范围内使用
应用场景:
如果某些组件在开发期间的使用频率很高
,推荐进行全局
注册;
如果某些组件只在特定的情况下会被用到
,推荐进行局部
注册。
1.6 组件注册时名称的大小写
在进行组件的注册时,定义组件注册名称的方式
有两种:
- 使用
kebab-case
命名法(俗称短横线命名法
,例如 my-swiper 和 my-search) - 使用
PascalCase
命名法(俗称帕斯卡命名法
或大驼峰命名法
,例如 MySwiper 和 MySearch)
短横线命名法的特点:
- 必须严格按照短横线名称进行使用
帕斯卡命名法的特点:
- 既可以严格按照帕斯卡名称进行使用,又可以
转化为短横线名称
进行使用
注意:在实际开发中,推荐使用帕斯卡命名法
为组件注册名称,因为它的适用性更强
1.7 通过 name 属性注册组件
在注册组件期间,除了可以直接提供组件的注册名称
之外,还可以把组件的 name 属性
作为注册后组件的名称
,示例代码如下:
<template>
<h3>
轮播图组件
</h3>
</template>
<script>
exports default {
name: 'MySwiper' // name 属性为当前组件的名字
}
</script>
import Swiper from './components/MySwiper.vue'
app.component(Swiper.name, Swiper) // 相当于 app.component('MySwiper', Swiper)
2. 组件之间的样式冲突问题
默认情况下,写在 .vue 组件中红的样式会全局生效
,因此很容易造成多个组件之间的样式冲突问题
。导致组件之间样式冲突的根本原因是:
- 单页面应用程序中,所有组件的 DOM 结构,都是基于
唯一的 index.html 页面
进行呈现的 - 每个组件中的样式,都会
影响整个 index.html 页面
中的 DOM 元素
2.1 思考
:如何解决组件样式冲突的问题
为每个组件分配唯一的自定义属性
,在编写组件样式时,通过属性选择器
来控制样式的作用域
,示例代码如下:
<template>
<div class="container">
<h3>轮播图组件</h3>
</div>
</template>
<style>
/* 通过中括号“属性选择器”,来防止组件之间的样式冲突问题,
因为每个组件分配的自定义属性是“唯一的” */
.container[data-v-001] {
border: 1px solid red;
}
</style>
2.2 style 节点的 scoped 属性
为了提高开发效率和开发体验,vue 为 style 节点
提供了 scoped
属性,从而防止组件之间的样式冲突问题:
<template>
<div class="container">
<h3>轮播图组件</h3>
</div>
</template>
<style scoped>
/* style 节点的 scoped 属性 用来自动为每个组件分配唯一的“自定义属性”,
并自定为当前组件的 DOM 标签 和 style 样式应用这个自定义属性,防止组件的样式冲突问题 */
.container {
border: 1px solid red;
}
</style>
2.3 /deep/
样式穿透
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的
。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器
<style lang="less" scoped>
.title {
color: bule; /* 不加 :deep() 时,生成的选择器格式为 .title[data-v-052242de] */
}
:deep(.title) {
color: bule; /* 加上 :deep() 时,生成的选择器格式为 [data-v-052242de] .title */
}
</style>
3. 组件的 props
为了提高组件的复用性
,在封装 vue 组件时需要遵守如下的原则:
- 组件的
DOM 结构
、style 样式
要尽量复用 - 组件中
要展示的数据
,尽量由组件的使用者提供
为了方便使用者
为组件提供要展示的数据,vue 组件提供了 props 的概念。
3.1 什么是组件的 props
props 是
组件的自定义属性
,组件的使用者
可以通过 props 把数据传递到子组件内部
,供子组件内部进行使用。示例代码如下:
<!-- 通过自定义 props,把文章的标题和作者,传递到 my-article 组件中 -->
<my-article title="面朝大海,春暖花开" author="海子"></my-article>
props 的作用
:父组件通过 props 向子组件传递要展示的数据。
props的好处
:提高了组件的复用性
3.2 在组件中生命 props
在封装 vue 组件时,可以把动态的数据项
声明为 props
自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。示例代码如下:
<template>
<h3>
标题: {{title}}
</h3>
<h5>
作者:{{author}}
</h5>
</template>
<script>
export default {
props: ['title', 'author'], // 父组件传递给 my-article 组件的数据,必须在 props 节点中声明
}
</script>
3.3 无法使用未声明的 props
如果父组件给子组件传递了未声明的 props 属性
,则这些属性会被忽略,无法被子组件使用,示例代码如下:
<my-article title="致橡树" author="舒婷"></my-article>
<template>
<h3>
标题: {{title}}
</h3>
<h5>
作者:{{author}}
</h5>
</template>
<script>
export default {
props: ['title'], // author 属性没有声明,因此组件中无法访问到 author 的值
}
</script>
3.4 动态绑定 props 的值
可以使用 v-bind 属性绑定
的形式,为组件动态绑定 props 的值,示例代码如下:
<!-- 通过 v-bind 属性绑定,为 title 动态赋予一个变量的值 -->
<!-- 通过 v-bind 属性绑定,为 author 动态赋予一个表达式的值 -->
<my-article :title="info.title" :author="'post by' + info.author"></my-article>
3.5 props 的大小写命名
组件中如果使用 “camelCase
(驼峰命名法
)”声明了 props 属性的名称,则有两种方式为其绑定属性的值:
<template>
<p>
发布时间: {{pubTime}}
</p>
</template>
<script>
export default {
props: ['pubTime'], // 采用“驼峰命名法”为当前的组件声明了 pubTime 属性
}
</script>
<!-- 既可以直接使用 “驼峰命名法” 的形式为组件绑定属性的值 -->
<my-article pubTime="1989"></my-article>
<!-- 也可以使用其等价的 “短横线分隔命名” 的形式为组件绑定属性的值 -->
<my-article pub-time="1989"></my-article>
4. Class 与 Style 绑定
在实际开发中经常会遇到动态操作元素样式
的需求。因此,vue 允许开发者通过 v-bind
属性绑定指令,为元素动态绑定 class 属性的值
和行内的 style 样式
。
4.1 动态绑定 HTML 的class
可以通过三元表达式,动态的为元素绑定 class 的类名
。示例代码如下:
<h3 class="thin" :class="isItalic ? 'italic' : ''">MyDeep 组件</h3>
<button @click="isItalic=!isItalic">Toggle Italic</button>
data() {
return { isItalic: true }
}
.thin { // 字体变细
font-weight: 200;
}
.italic { // 倾斜字体
font-style: italic;
}
4.2 以数组语法绑定 HTML 的class
如果元素需要动态绑定多个
class 的类名,此时可以使用数组的语法格式
:
<h3 class="thin" :class="[isItalic ? 'italic' : '', isDelete ? 'delete' : '']">MyDeep 组件</h3>
<button @click="isItalic=!isItalic">Toggle Italic</button>
<button @click="isDelete=!isDelete">Toggle Delete</button>
data() {
return {
isItalic: true,
isDelete: false,
}
}
4.3 以对象语法
绑定 HTML 的 class
使用数组语法
动态绑定 class 会导致模板结构臃肿
的问题。此时可以使用对象语法
进行简化
:
<h3 class="thin" :class="classObj">MyDeep 组件</h3>
<button @click="classObj.italic = !classObj.italic">Toggle Italic</button>
<button @click="classObj.delete = !classObj.delete">Toggle Delete</button>
delete
data() {
return {
classOjb: { // 对象中,属性名是 class 类名,值是布尔值
isItalic: true,
isDelete: false,
}
}
}
4.4 以对象语法
绑定内联的 style
:style
的对象语法
十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象
。CSS property 名可以用驼峰式(camelCase)或短横线分隔(kebab-case,记得用引号括起来)来命名
<div :style="{color: active, fontSize: fsize + 'px', 'background-color': bgcolor}">
</div>
<button @click="fsize += 1">字号 + 1</button>
<button @click="fsize -= 1">字号 - 1</button>
data() {
return {
active: 'red',
fsize: 30,
bgcolor: 'pink',
}
}
总结
- 能够说出什么是单页面应用程序及组件化开发
SPA
、只有一个页面、组件是对UI 结构的复用
- 能够说出 .vue 单文件组件的组成部分
- template、script、style(scoped、lang)
- 能够知道如何注册 vue 组件
- 全局注册(
app.component
)、局部注册(components
)
- 全局注册(
- 能够知道如何声明组件的 props 属性
props 数组
- 能够知道如何在组件中进行样式绑定
- 动态绑定
class
、动态绑定style
- 动态绑定
组件基础(下)
props 验证
1. 什么是 props 验证
指的是:在封装组件时对外界传递过来的 props 数据
进行合法性的校验,从而防止数据不合法的问题。
使用数组类型
的 props 节点的缺点:无法为每个 props 指定具体的数据类型
2. 对象类型
的 props 节点
使用对象类型
的 props 节点,可以对每个 prop 进行数据类型的校验
,示意图如下:
3. props 验证
对象类型的 props 节点
提供了多种数据验证方案,例如:
- 基础的类型检查
- 多个可能的类型
- 必填项校验
- 属性默认值
- 自定义验证函数
3.1 基础的类型检查
可以直接为组件 prop 属性指定基础的校验类型
,从而防止组件的使用者
为其绑定错误类型的数据
:
export default {
props: {
propA: String, // 字符串类型
propB: Number, // 数字类型
propC: Boolean, // 布尔值类型
propD: Array, // 数组类型
propE: Object, // 对象类型
propF: Date, // 日期类型
propG: Function, // 函数类型
propH: Symbol // 符号类型
}
}
3.2 多个可能的类型
如果某个 prop 属性值的类型不唯一
,此时可以通过数组的形式,为其指定多个可能的类型,示例代码如下:
export default {
props: {
// propA 属性的值可以是“字符串”或“数字”
propA: [String, Number],
}
}
3.3 必填项校验
如果组件的某个 prop 属性是必填项
,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项:
export default {
props: {
// 通过“配置对象”的形式,来定义 propB 属性的“验证规则”
propB: {
type: String, // 当前属性的是必须是 String 字符串类型
required: true // 当前属性的值是必须项,如果使用者没指定 propB 属性的值,则在终端进行警告提示
}
}
}
3.4 属性默认值
在封装组件时,可以为某个 prop 属性指定默认值
。示例代码如下:
export default {
props: {
// 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
propC: {
type: Number,
default: 100 // 如果使用者没有指定 propC 的值,则 propC 属性的默认值为 100
}
}
}
3.5 自定义验证函数
在封装组件时,可以为 prop 属性指定自定义的验证函数
,从而对 prop 属性的值进行更加精确的控制
:
export default {
// 通过“配置对象”的形式,来定义 propD 属性的“验证规则”
propD: {
// 通过 validator 函数,对 propD 属性的值进行检验,“属性的值”可以通过形参 value 进行接收
validator(value) {
// propD 属性的值,必须匹配下列字符串中的一个
// validator 函数的返回值为 true 表示验证通过,false 表示验证失败
return ['success', 'warning', 'danger'].indexOf(value) !== -1;
}
}
},
}
计算属性
1. 什么是计算属性
计算属性本质上
就是一个 function 函数
,它可以实时监听
data 中数据的变化,并 return 一个计算后的新值
供组件渲染 DOM 是使用。
2. 如何声明计算属性
计算属性需要以 function 函数
的形式声明到组件的 computed 选项
中,示例代码如下:
<input type="text" v-model.number="count">
<p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
export default {
data() {
return { count: 1 }
},
computed: {
plus() { // 计算属性:监听 data 中 count 值的变化,自动计算出 count * 2 之后的新值
return this.count * 2
}
}
}
3. 计算属性的使用注意点
- 计算属性
必须定义在 computed 节点中
- 计算属性
必须是一个 function 函数
- 计算属性
必须有返回值
- 计算属性
必须当作普通属性使用
4. 计算属性 vs 方法
相对于方法来说,计算属性会缓存计算的结果
,只有计算属性的依赖项发生变化
时,才会重新进行运算
,因此计算属性的性能更好:
computed: {
plus() { // 计算属性的计算结果会被缓存,性能好
console.log('计算属性被执行了')
return this.count * 2
}
}
methods: {
plus() { // 方法的计算结果无法被缓存,性能低
console.log('方法被执行了')
return this.count * 2
}
}
自定义事件
1. 什么是自定义事件
在封装组件时,为了让组件的使用者
可以监听到组件内状态的变化
,此时需要用到组件的自定义事件
2. 自定义事件的 3 个使用步骤
在封装组件时:
声明
自定义事件触发
自定义事件
在使用组件时:
监听
自定义事件
2.1 声明
自定义事件
开发者为自定义组件封装的自定义事件
,必须事先在 emits
节点中声明,实例代码如下:
<template>
<h3>Counter组件</h3>
<button>+1</button>
</template>
export default {
// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
emits: ['change'],
}
2.2 触发自定义事件
在 emits
节点下声明的自定义事件,可以通过 this.$emit
(‘自定义事件的名称’) 方法进行触发,示例代码如下:
<template>
<h3>Counter组件</h3>
<button @click="onBthClick">+1</button>
</template>
export default {
// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
emits: ['change'],
methods: {
onBtnClick() {
this.$emit('change') // 当点击 + 1 按钮时,调用 this.$emit() 方法,触发自定义的 change 事件
}
}
}
2.3 监听
自定义事件
在使用自定义的组件时,可以通过 v-on
的形式监听自定义事件
。示例代码如下:
<!-- 使用 v-on 指令绑定事件监听 -->
<my-counter @change="getCount"></my-counter>
methods: {
getCount() {
console.log('监听到了 count 值的变化!')
}
}
3. 自定义事件传参
在调用 this.$emit()
方法触发自定义事件时,可以通过第二个参数
为自定义事件传参,示例代码如下:
<template>
<h3>Counter组件</h3>
<button @click="onBthClick">+1</button>
</template>
export default {
// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
emits: ['change'],
methods: {
onBtnClick() {
this.$emit('change', this.count) // 触发自定义事件时,通过第二个参数传参
}
}
}
组件上的 v-model
1. 为什么需要在组件上使用 v-mode
v-model 是双向数据绑定指令,当需要维护组件内外数据同步
时,可以在组件上使用 v-model 指令。示意图如下:
外界数据的变化
会自动同步
到 counter 组件中- counter 组件中数据的变化,也会
自动同步到外界
2. 在组件上使用 v-model 的步骤
- 父组件通过
v-bind:
属性绑定的形式,把数据传递给子组件 - 子组件中,通过
props
接收父组件传递过来的数据
总结
- 能够知道如何对 props 进行验证
- 数组格式,
对象格式
type
、default
、required
、validator
- 数组格式,
- 能够知道如何使用计算属性
computed 节点
、必须return一个结果、缓存计算结果
- 能够知道如何为组件绑定自定义事件
- v-on 绑定自定义事件、
emits
、$emit()
- v-on 绑定自定义事件、
- 能够知道如何在组件上使用 v-model
- 应用场景:
实现组件内外的数据同步
v-model
: props名称、emits、$emit(‘update
:props名称’)
- 应用场景:
组件高级(上)
watch 侦听器
1. 什么是 watch 侦听器
watch 侦听器
允许开发者监视数据的变化,从而针对数据的变化做特定的操作
。例如,监视用户名的变化并发起请求,判断用户名是否可用。
2. watch 侦听器的基本语法
开发者需要在 watch 节点
下,定义自己的侦听器。示例代码如下:
export default {
data() {
return { username: ''}
},
watch: {
// 监听 username 的值的变化,
// 形参列表中,第一个值是“变化后的新值”,第一个值是“变化之前的旧值”
username(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
3. 使用 watch 检测用户名是否可用
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用
:
import axios from 'axios'
export default {
data() {
return { username: ''}
},
watch: {
async username(newVal, oldVal) {
const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${newVal}`)
console.log(res)
}
}
}
4. immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用
,则需要使用 immediate
选项。示例代码如下:
watch: {
username: {
async handler (newVal, oldVal) {
const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal }`);
console.log(res);
},
// 立即触发 watch 侦听器
immediate: true,
}
}
5. deep 选项
当 watch 侦听的是一个对象
,如果对象中的属性值发生了变化
,则无法被监听到
。此时需要使用 deep 选项
,代码示例如下:
data () {
return {
info: {
username: 'zs', // info 中包含 username 属性
}
}
},
watch: {
info: { // 直接监听 info 对象的变化
async handler (newVal) {
const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal.username }`);
console.log(res);
},
deep: true, // 需要使用 deep 选项,否则 username 值的变化无法被监听到
}
}
6. 监听对象单个属性的变化
如果只想监听对象中单个属性的变化
,则可以按照如下的方式定义 watch 侦听器:
data () {
return {
info: {
username: 'zs', // info 中包含 username 属性
}
}
},
watch: {
'info.username': { // 只想监听 info.username 属性值的变化
async handler (newVal) {
const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal.username }`);
console.log(res);
},
deep: true, // 需要使用 deep 选项,否则 username 值的变化无法被监听到
}
}
7. 计算属性 vs 侦听器
计算属性和侦听器侧重的应用场景不同
:
计算属性侧重于监听多个值
的变化,最终计算并返回一个新值
侦听器侧重于监听单个数据
的变化,最终执行特定的业务处理,不需要有任务返回值
组件的生命周期
1. 组件运行的过程
组件的生命周期
指的是:组件从创建
-> 运行
(渲染) -> 销毁
的整个过程,强调的是一个时间段
2. 如何监听
组件的不同时刻
vue 框架
为组件内置了不同时刻的生命周期函数
,生命周期函数会伴随着
这组件的运行而自动调用
。例如:
- 当组件
在内存中被创建完毕
之后,会自动调用created
函数 - 当组件被成功的渲染到页面上之后,会自动调用
mounted
函数 - 当组件
被销毁完毕
之后,会自动调用unmounted
函数
3. 如何监听
组件的更新
当组件的 data 数据更新
之后,vue 会自动重新渲染组件
的 DOM 结构,从而保证 View 视图
展示的数据和 Model 数据源
保持一致。
当组件被重新渲染完毕
之后,会自动调用 updated
生命周期函数。
4. 组件中主要的
生命周期函数
生命周期函数 | 执行时机 | 所属阶段 | 执行次数 | 应用场景 |
---|---|---|---|---|
created | 组件在内存中创建完毕后 | 创建阶段 | 唯一1次 | 发 Ajax 请求初始数据 |
mounted | 组件初次在页面中渲染完毕后 | 创建阶段 | 唯一1次 | 操作 DOM 元素 |
updated | 组件咋页面被重新渲染完毕后 | 运行阶段 | 0 或 多次 | - |
unmounted | 组件被销毁后(页面和内存) | 销毁阶段 | 唯一1次 | - |
注意:在实际开发中,created
是最常用的
生命周期函数!
5. 组件中全部的
生命周期函数
生命周期函数 | 执行时机 | 所属阶段 | 执行次数 | 应用场景 |
---|---|---|---|---|
beforeCreate | 在内存中开始创建组件之前 | 创建阶段 | 唯一1次 | - |
created | 组件在内存中创建完毕后 | 创建阶段 | 唯一1次 | 发 Ajax 请求初始数据 |
beforeMount | 在把组件初次渲染到页面之前 | 创建阶段 | 唯一1次 | - |
mounted | 组件初次在页面中渲染完毕后 | 创建阶段 | 唯一1次 | 操作 DOM 元素 |
beforeUpdate | 在组件被重新渲染之前 | 运行阶段 | 0 或 多次 | - |
updated | 组件咋页面被重新渲染完毕后 | 运行阶段 | 0 或 多次 | - |
beforeUnmount | 在组件被销毁之前 | 销毁阶段 | 唯一1次 | - |
unmounted | 组件被销毁后(页面和内存) | 销毁阶段 | 唯一1次 | - |
疑问:为什么不在 beforeCreate
中发 ajax 请求初始数据?
组件之间的数据共享
1. 组件之前的关系
在项目开发中,组件之间的关系分为如下 3 种:
- 父子关系
- 兄弟关系
- 后代关系
2. 父子组件之间的数据共享
2.1 父组件向子组件共享数据
父组件通过 v-bind 属性绑
定向子组件共享数据。同时,子组件需要使用 props
接收数据。示例代码如下:
// 父组件
<my-test-1 :msg="message" :user="userinfo"></my-test-1>
data() {
return {
message: 'hello vue',
userinfo {
name: 'zs',
age: 20,
}
}
}
// 子组件
<template>
<h3>
测试父子传值
</h3>
<p>
{{msg}}
</p>
<p>
{{user}}
</p>
</template>
<script>
export default {
props: ['msg', 'user'],
}
</script>
2.2 子组件向父组件共享数据
子组件通过自定义事件
的方式向父组件共享数据。示例代码如下:
// 子组件
<script>
export default {
emits: ['n1chage'], // 1. 声明自定义事件
data() {
return {n1:0},
},
methods: {
addN1() {
this.n1++
// 2. 数据变化时,触发自定义的事件
this.$emit('n1change', this.n1)
}
}
}
</script>
<template>
// 父组件
// 1. 监听子组件的自定义事件 n1change
<my-test-1 @n1change="getn1"></my-test-1>
</template>
<script>
export default {
data() { return { n1FromSon: 0 } },
methods: {
getn1(n1) { // 通过形参,接收子组件传递过来的数据
this.n1FromSon = n1
}
}
}
</script>
2.3 父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用 v-model 指令
维护组件内数据的双向同步:
3. 兄弟组件之间的数据共享
兄弟组件之间
实现数据共享的方案是 EventBus
。可以借助于第三方的包 mitt
来创建 eventBus 对象
,从而实现兄弟组件之间的数据共享。示意图如下:
3.1 安装 mitt 依赖包
在项目中运行如下的命令,安装 mitt 依赖包:
npm install mitt
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块
如下:
// eventBus.js
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus
3.3 在数据接收方
自定义事件
在数据接收方,调用 bus.on
(‘事件名称’, 事件处理函数) 方法注册一个自定义事件
。示例代码如下:
// 导入 eventBus.js 模块,得到共享的 bug 对象
import bus from './eventBus.js'
export default {
name: 'MyRight',
data() {
return {
num: 0,
}
},
created() {
// 调用 bus.on() 方法注册一个自定义事件,通过事件处理函数的形参接收数据
bus.on('countChange', (count) => {
this.num = count
})
}
}
3.4 在数据发送方
触发事件
在数据发送方,调用 bus.emit
(‘事件名称’, 要发送的数据) 方法触发自定义事件
。示例代码如下:
// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'
export default {
name: 'MyLeft',
data() {
return {
count: 0,
}
},
methods: {
addCount() {
this.count++
bus.emit('countChange', this.count) // 调用 bus.emit() 方法触发自定义事件,并发送数据
}
}
}
4. 后代关系组件之间的数据共享
后代关系组件之间共享数据,指的是父节点的组件
向其子孙组件
共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide
和 inject
实现后代关系组件之间的数据共享。
4.1 父节点通过 provide
共享数据
父节点的组件可以通过 provide 方法
,对其子孙组件
共享数据:
<script>
export default {
data() {
return {
color: 'red' // 1. 定义“父组件”要向“子孙组件”共享的数据
}
},
provide() { // 2. provide 函数 return 的对象中,包含了“要向子孙组件共享的数据”
return {
color:this.color,
}
}
}
</script>
4.2 子孙节点通过 inject
接收数据
子孙节点可以使用 inject
数组,接收父级节点向下共享的数据
。示例代码如下:
<template>
<h5>
子孙组件 --- {{color}}
</h5>
</template>
<script>
export default {
// 子孙组件:使用 inject 接收父节点向下共享的 color 数据,并在页面上使用
inject: ['color'],
}
</script>
4.3 父节点对外共享响应式的数据
父节点使用 provide 向下共享数据时,可以结合 computed 函数
向下共享响应式的数据
。示例代码如下:
import { computed } from 'vue' // 1. 从 vue 中按需导入 computed 函数
<script>
export default {
data() {
return { color: 'red' }
},
provide() {
return {
// 2. 使用 computed 函数,可以把要共享的数据“包装为”响应式的数据
color: computed(() => this.color),
}
}
}
</script>
4.4 子孙节点使用响应式的数据
如果父级节点共享的是响应式的数据
,则子孙节点必须以 .value
的形式进行使用。示例代码如下:
<template>
<!-- 响应式的数据,必须以 .value 的形式进行使用 -->
<h5>
子孙组件 --- {{color.value}}
</h5>
</template>
<script>
export default {
// 接收父节点向下共享的 color 数据,并在页面上使用
inject: ['color'],
}
</script>
5. vuex
vuex 是终极的
组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效
、清晰
、且易于维护
6. 总结
- 父子关系
- 父 -> 子
属性绑定
- 子 -> 父
事件绑定
- 父 <-> 子
组件上的 v-model
- 父 -> 子
- 兄弟关系
4.EventBus
- 后代关系
5.provide
&inject
- 全局数据共享
6.vuex
vue 3.x 中全局配置 axios
1. 为什么要全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
- 每个组件中都需要
导入 axios
(代码臃肿) - 每次发请求度需要填写
完整的请求路径
(不利用后期的维护)
2. 如何全局配置 axios
在 main.js
入口文件中,通过app.config.globalProperties
全局挂载 axios,示例代码如下:
利用全局挂载的 axios 发起 post 请求
<script>
export default {
methods: {
async postInfo() {
const { data: res } = await this.$http.post('/api/post', {name: 'zs', age: 20})4
console.log(res)
}
}
}
</script>
利用全局挂载的 axios 发起 get 请求
<script>
export default {
methods: {
async getInfo() {
const {data:res} = await this.$http.get('/api/get', {
params: {
name: 'ls',
age: 23,
}
})
}
}
}
</script>
vuex
1. Vuex 概述
1.1 组件之间共享数据的方式
父向子传值:v-bind 属性绑定
子向父传值:v-on 事件绑定
兄弟组件之间共享数据:EventBus
- $on 接收数据的那个组件
- $emit 发送数据的那个组件
1.2 Vuex 是什么
Vuex
是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。
1.3 使用 Vuex 统一管理状态的好处
- 能够在 vuex 中集中管理共享的数据,易于开发和后期维护
- 能够高效的实现组件之间的数据共享,提高开发效率
- 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步
1.4 什么样的数据适合存储到 Vuex 中
一般情况下,只有组件之间共享
的数据,才有必要存储到 vuex
中;对于组件中的私有数据
,依旧存储在组件自身的 data 中
即可
2. Vuex 的基本使用
1. 安装 vuex 依赖包
npm install vuex --save
2. 导入 vuex 包
import Vuex from 'vuex'
Vue.use(Vuex)
3. 创建 store 对象
const store = new Vuex.Store({
// state 中存放的就是全局共享的数据
state: { count: 0 }
})
4. 将store 对象挂载到 vue 实例中
new Vue({
el: '#app',
render: h => h(app),
router,
将创建的共享数据对象,挂载 到 vue 实例中
所有的组件,就可以直接从 store 中获取全局的数据了
store
})
格式化文件 .prettierrc
{
"semi" :false, // 不使用 ";" 符号
"singleQuote": true // 将 "" 改为 ''
}
3. Vuex 的核心概念
3.1 核心概念概述
Vuex 中的主要核心概念如下:
State
- Mutation
- Action
- Getter
3.2 State
State 提供唯一的公共数据源,所有共享的数据都要统一到 Store 的 State 中进行存储。
// 创建 store 数据源,提供唯一的公共数据
const store = new Vuex.Store({
state: {
count: 0,
}
})
组件访问 State 中数据的第一种方式
:
this.$store.state.全局数据名称
组件访问 State 中数据的第二种方式:
// 1. 从 vuex 中按需导入 mapState 函数
import { mapState } from 'vuex'
通过刚才导入的 mapState 函数,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性:
// 2. 将全局数据,映射为当前组件的计算属性
computed: {
...mapState(['count'])
}
3.3. Mutation
Mutation 用于变更 Store 中的数据。
- 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据。
- 通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
}
}
})
// 触发 mutation
methods: {
handlel() {
// 触发 mutations 的第一个方式
this.$store.commit('add')
}
}
可以在触发 mutations 是传递参数:
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
addN(state, step) {
// 变更状态
state.count += step
}
}
})
// 触发 Mutation
methods: {
handlel2() {
// 在调用 commit 函数,
// 触发 Mutations 时携带参数
this.$store.commit('addN', 3 )
}
}
this.$store.commit() 是触发 mutations 的第一种方式,触发 mutations 的第二种方式:
// 1. 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'
通过刚才导入的 mapMutations 函数,将需要 的 Mutations 函数,映射为当前组件的 methods 方法:
// 2. 将指定的 mutations 函数,映射为当前组件的 methods 函数
methods: {
...mapMutations(['add', 'addN']),
handlel() {
this.add();
}
}
3.4 Action
Action 用于处理异步任务。
如果通过异步操作变更数据,必须通过 Action,而不能使用 Mutation,但是在 Action 中还是要通过触发 Mutation 的方式间接变更数据
// 定义 Action
const store = new Vuex.Store({
// 省略其他代码
mutations: {
state.count++
},
actions: {
addAsync(context) {
setTimeout(() => {
context.commit('add')
}, 1000)
}
}
})
// 触发 Action
methods: {
handle() {
// 触发 actions 的第一种方式
this.$store.dispatch('addAsync')
}
}
触发 actions 异步任务是携带参数:
// 定义 Action
const store = new Vuex.Store({
// 省略其他代码
mutations: {
state.count++
},
actions: {
addAsync(context, stpe) {
setTimeout(() => {
context.commit('addN', step)
}, 1000)
}
}
})
// 触发 Action
methods: {
handle() {
// 在调用 dispatch 函数,
// 触发 actions 是携带参数
this.$store.dispatch('addAsync', 5)
}
}
this.$store.dispatch() 是触发 actions 的第一种方式,触发 actions 的第二种方式
:
// 1. 从 vuex 中按需导入 mapActions 函数
import { mapActions } from 'vuex'
通过刚才导入的 mapActions 函数,将需要的 actions 函数,映射为当前组件的 methods 方法:
// 2. 将指定的 actions 函数,映射为当前组件的 methods 函数
methods: {
...mapActions(['addAsync', 'addNAsync'])
}
3.5 Getter
Getter 用于对 Store 中的数据进行加工处理形成新的数据。
- Getter 可以对Store 中已有的数据加工处理之后形成新的数据,类似于 Vue 的计算属性。
- Store 中数据发生变化,Getter 的数据也会跟着变化
// 定义 Getter
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
showNum: state => {
return '当前最新的数量是【' + state.count + '】'
}
}
})
使用 getters 的第一种方式
:
this.$store.getters.名称
使用 getters 的第二种方式
:
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['showNum'])
}
总结
- 能够掌握 watch 侦听器的基本使用
- 定义最基本的 watch 侦听器
immediate
、deep
、监听对象中单个属性的变化
- 能够知道 vue 中常用的生命周期函数
- 创建阶段、运行阶段、销毁阶段
created
、mounted
- 能够知道如何实现组件之间的数据共享
父子组件
、兄弟组件
、后代组件
- 能够知道如何在 vue3 的项目中全局配置 axios
main.js
入口文件中进行配置- app.
config.globalProperties
.$http = axios
组件高级(下)
ref 引用
1. 什么是 ref 引用
ref 用来辅助开发者在不依赖于 jQuery 的情况下
,获取 DOM 元素或组件的引用
每个 vue 的组件实例上,都包含一个 \$refs 对象
,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 \$refs 指向一个空对象
。
<template>
<h3>
MyRef 组件
</h3>
<button @click="getRef">
获取 $refs 引用
</button>
</template>
<script>
export default {
methods: {
getRef() {
console.log(this) // this 代表当前组件的实例对象, this.$refs 默认指向空对象
}
}
}
</script>
2. 使用 ref 引用 DOM 元素
如果想要使用 ref 引用页面上的 DOM 元素
,则可以按照如下的方式进行操作:
<template>
<!-- 使用 ref 属性,为对应的 DOM 添加引用名称 -->
<h3 ref="myh3">
MyRef 组件
</h3>
<button @click="getRef">
获取 $refs 引用
</button>
</template>
<script>
export default {
methods: {
getRef() {
// 通过 this.$refs.引用的名称 可以获取到 DOM 元素的引用
console.log(this.$refs.myh3)
// 操作 DOM 元素,把文本颜色改为红色
this.$refs.myh3.style.color = 'red'
}
}
}
</script>
3. 使用 ref 引用组件实例
如果想要使用 ref 引用页面上的组件实例
,则可以按照如下的方式进行操作:
<template>
<!-- -->
<my-counter ref="counterRef"></my-counter>
<button>
获取 $refs 引用
</button>
</template>
<script>
export default {
methods: {
getRef() {
// 通过 this.$refs.引用的名称 可以引用组件的实例
console.log(this.$refs.counterRef)
// 引用到组件的实例之后,就可以调用组件上的 methods 方法
this.$refs.counterRef.add()
}
}
}
</script>
4. 控制文本框和按钮的按需切换
通过布尔值 inputVisible
来控制组件中的文本框与按钮的按需切换。示例代码如下:
<template>
<input type="text" v-if="inputVisible">
<button v-else @click="showInput">
展示input输入框
</button>
</template>
<script>
export default {
data() {
return {
// 控制文本框和按钮的按需切换
inputVisible: false,
}
},
methods: {
showInput() { // 切换布尔值
this.inputVisible = true
}
}
}
</script>
5. 让文本框自动获得焦点
当文本框展示出来之后,如果希望它立即获得焦点,则可以为其添加 ref 引用,并调用原生 DOM 对象的 .focus()
方法即可。示例代码如下:
<template>
<input type="text" v-if="inputVisible" ref="ipt">
<button v-else @click="showInput">
展示input输入框
</button>
</template>
<script>
export default {
data() {
return {
// 控制文本框和按钮的按需切换
inputVisible: false,
}
},
methods: {
showInput() { // 切换布尔值
this.inputVisible = true
// 获取文本框的 DOM 引用,并调用 .focus() 使其自动获得焦点
this.$refs.ipt.focus()
}
}
}
</script>
6. this.$nextTick(cb)
方法
组件的 $nextTick(cb)
方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行
。通俗的理解是:等组件的 DOM 异步重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素
<template>
<input type="text" v-if="inputVisible" ref="ipt">
<button v-else @click="showInput">
展示input输入框
</button>
</template>
<script>
export default {
data() {
return {
// 控制文本框和按钮的按需切换
inputVisible: false,
}
},
methods: {
showInput() { // 切换布尔值
this.inputVisible = true
// 把对 input 文本框的操作,推迟到下次 DOM 更新之后,否则页面上根本不存在文本框元素
this.$nextTick(() => {
this.$refs.ipt.focus()
})
}
}
}
</script>
动态组件
1. 什么是动态组件
动态组件指的是动态切换组件的显示与隐藏
。vue 提供了一个内置的 <component>
组件,专门用来实现组件的动态渲染。
- <component> 是组件的
占位符
- 通过
is 属性
动态指定要渲染的组件名称
- <component
is
=“要渲染的组件的名称”></component>
2. 如何实现动态组件渲染
<script>
export default {
data() {
return {
// 1. 当前要渲染的组件的名称
comName: 'my-dynamic-1'
}
},
</script>
<template>
<!-- 3. 点击按钮,动态切换组件的名称 -->
<button @click="comName='my-dynamic-1'">
组件1
</button>
<button @click="comName='my-dynamic-2'">
组件2
</button>
<!-- 2. 通过 is 属性,动态指定要渲染的组件的名称 -->
<component :is="comName"></component>
</template>
3. 使用 keep-alive 保持状态
默认情况下,切换动态组件时无法保持组件的状态
。此时可以使用 vue 内置的<keep-alive>
组件保持动态组件的状态,示例代码如下:
<keep-alive>
<component :is="comName"></component>
</keep-alive>
插槽
1. 什么是插槽
插槽
(Slot
)是 vue 为组件的封装者
提供的能力。允许开发者在封装组件时,把不确定的
、希望由用户指定的部分
定义为插槽。
可以把插槽认识是组件封装期间,为用户预留的内容的占位符
2. 体验插槽的基础用法
在封装组件时,可以通过<slot>
元素定义插槽
,从而为用户预留内容占位符
。示例代码如下:
<template>
<p>
这是 MyCom1 组件的第一个 p 标签
</p>
<!-- 通过 slot 标签,为用户预留内容占位符(插槽) -->
<slot></slot>
<p>
这是 MyCom1 组件最后一个 p 标签
</p>
</template>
<my-com-1>
<!-- 在使用 MyCom1 组件时,为插槽指定具体的内容 -->
<p>
~~~用户自定义的内容~~~
</p>
</my-com-1>
2.1 没有预留插槽
的内容会被丢弃
如果在封装组件时没有预留任何 <slor> 插槽
,则用户提供的任何自定义内容
都会被丢弃
。示例代码如下:
<template>
<p>
这是 MyCom1 组件的第一个 p 标签
</p>
<!-- 封装组件时没有预留任何 <slor> 插槽 -->
<p>
这是 MyCom1 组件最后一个 p 标签
</p>
</template>
<my-com-1>
<!-- 自定义的内容会被丢弃 -->
<p>
~~~用户自定义的内容~~~
</p>
</my-com-1>
2.2 后备内容
封装组件时,可以为预留的 <slot> 插槽提供后备内容
(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例代码如下:
<template>
<p>
这是 MyCom1 组件的第一个 p 标签
</p>
<slot>这是后备内容</slot>
<p>
这是 MyCom1 组件最后一个 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
”。
3.1 为具名插槽提供内容
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot 的参数的形式提供其名称。示例代码如下:
<my-com-2>
<template v-slot:header>
<h1>滕王阁序</h1>
</template>
<template v-slot:default>
<p>豫章故郡,洪都新府。</p>
<p>星分翼轸,地接衡庐。</p>
<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
</template>
<template v-slot:footer>
<p>落款:王勃</p>
</template>
</ my-com-2>
3.2 具名插槽的简写形式
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容(v-slot:
)替换为字符 #
。例如 v-slot:
header 可以被重写成 #
header
<my-com-2>
<template #header>
<h1>滕王阁序</h1>
</template>
<template #default>
<p>豫章故郡,洪都新府。</p>
<p>星分翼轸,地接衡庐。</p>
<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
</template>
<template #footer>
<p>落款:王勃</p>
</template>
</ my-com-2>
4. 作用域插槽
在封装组件的过程中,可以为预留的 <slot> 插槽绑定 props 数据,这种 带有 props 数据的 <slot>
叫做“作用域插槽
”。示例代码如下:
<div>
<h3>
这是 TEST 组件
</h3>
<slot :info="infomation"></slot>
</div>
<!-- 使用自定义组件 -->
<my-test>
<template #default="scope">
{{ scope }}
</template>
</my-test>
自定义指令
1. 声明私有自定义指令的语法
在每个 vue 组件中,可以在 directives
节点下声明私有自定义指令
。示例代码如下:
directives: {
// 自定义一个私有指令
focus: {
// 当被绑定的元素插入到 DOM 元素中时, 自动触发 mounted 函数
mounted(el) {
el.focus() // 让被绑定的元素自定获得焦点
}
}
}
2. 声明全局自定义指令的语法
需要通过“单页面应用程序的实例对象”进行声明,示例代码如下:
const app = Vue.createApp({})
// 注册一个全局自定义指令 'v-focus'
app.directive('focus', {
mounted(el) {
// Focus the element
el.focus()
}
})
3. updated 函数
mounted 函数
只在元素第一次插入 DOM 时被调用
,当 DOM 更新时 mounted 函数不会被触发。updated 函数
会在每次 DOM 元素更新完成后
被调用。示例代码如下:
app.directive('focus', {
mounted(el) { // 第一次插入 DOM 时触发这个函数
// Focus the element
el.focus()
},
updated(el) { // 每次 DOM 更新时都会触发 updated 函数
el.focus()
}
})
注意:在 vue2
的项目中使用自定义指令时,【 mounted -> bind
】 【 updated -> update
】
4. 函数简写
如果 mounted 和 updated 函数中的逻辑完全相同,则可以简写成如下格式:
app.directive('focus', (el) => {
// 在 mounted 和 updated 时都会触发相同的业务处理
el.focus()
})
5. 指令的参数值
在绑定指令时,可以通过“等号
”的形式为指令绑定具体的参数值
,示例代码如下:
<template>
<!-- 在使用 v-color 指令时,可以通过“等号”绑定指令的值 -->
<input type="text" v-model.number="count" v-focus v-color="red">
<p v-color="cyan">
{{count}}
</p>
<button @click="count++">
+1
</button>
</template>
// 自定义 v-color 指令
app.directive('color', (el, binding) => {
// binding.value 就是通过“等号”为指令绑定的值
el.style.color = binding.value
})
总结
- 能够知道如何使用
ref
引用 DOM 和组件实例- 通过
ref 属性
指定引用的名称、使用this.$refs
访问到引用实例
- 通过
- 能够知道
$nextTick
的调用时机- 组件的 DOM 更新之后,才执行 $nextTick 中的回调
- 能够说出
keep-alive
元素的作用- 保持动态组件的状态
- 能够掌握
插槽
的基本用法- <slot> 标签、具名插槽、
作用域插槽
、v-slot:
简写为#
- <slot> 标签、具名插槽、
- 能够知道如何
自定义指令
- 私有自定义指令、
全局自定义指令
- 私有自定义指令、
路由
前端路由的概念与原理
1. 什么是路由
路由(英文名:router)就是对应关系
。路由分为两大类:
- 后端路由
- 前端路由
2. 回顾:后端路由
后端路由指的是:请求方式
、请求地址
与 function 处理函数
之间的对应关系
。在 Node.js 课程中,express 路由的基本用法如下:
const express = require('express')
const router = express.Router()
router.get('/userlist', function(req, res) { 路由的处理函数 })
router.post('/adduser', function(req, res) { 路由的处理函数 })
module.exports = router
3. SPA 与前端路由
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有的组件的展示与切换
都在这唯一的一个页面内完成。
此时,不同组件之间的切换
需要通过
来实现。
结论:在 SPA 项目中,不同功能之间的切换
,要依赖于前端路由
来完成!
4. 什么是前端路由
通俗易懂的概念:Hash 地址
与组件
之间的对应关系
5. 前端路由的工作方式
- 用户
点击了
页面上的路由链接
- 导致了
URL 地址栏
中的Hash 值
发生了变化 前端路由监听到了 Hash 地址的变化
- 前端路由把当前
Hash 地址对应的组件
渲染到浏览器中
结论:前端路由,指的是 Hash 地址
与组件之间
的对应关系
!
6. 实现简易的前端路由
步骤1:导入并注册
MyHome、MyMovie、MyAbout 三个组件。示例代码如下:
import MyHome from './components/MyHome.vue'
import MyMovie from './components/MyMovie.vue'
import MyAbout from './components/MyAbout.vue'
export default {
components: {
MyHome,
MyMovie,
MyAbout,
}
}
步骤2:通过<component> 标签的 is 属性
,动态切换要显示的组件。示例代码如下:
<template>
<h1>
App 组件
</h1>
<component :is="comName"></component>
</template>
<script>
export default {
data() {
return {
comName: 'my-home', // 要展示的组件的名称
}
}
}
</script>
步骤3: 在组件的结构中声明如下 3 个<a> 链接,通过点击不同的 a 标签,切换浏览器地址栏中的 Hash 值:
<a href="#/home">Home</a>
<a href="#/movie">Movie</a>
<a href="#/about">About</a>
步骤4:在 created 生命周期函数中监听浏览器地址栏中 Hash 地址的变化,动态切换要展示的组件的名称:
created() {
window.onhashchange = () => {
switch (location.hash) {
case '#/home': // 点击了“首页”的链接
this.comName = 'my-home'
break
case '#/movie': // 点击了“电影”的链接
this.comName = 'my-movie'
break
case '#/about': // 点击了“关于”的链接
this.comName = 'my-about'
break
}
}
}
vue-router 的基本用法
1. 什么是 vue-router
vue-router
是 vue.js 官方给出的路由解决方案
。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。
2. vue-router 的版本
vue-router 目前有 3.x
的版本 和4.x
的版本。其中:
- vue-router 3.x 只能结合
vue2
进行使用 - vue-router 4.x 只能结合
vue3
进行使用
3. vue-router 4.x 的基本使用步骤
- 在项目中安装 vue-router
- 定义路由组件
- 声明
路由链接
和占位符
- 创建
路由模块
导入并挂载
路由模块
3.1 在项目中安装 vue-router
在 vue3 的项目中,只能安装并使用 vue-router 4.x。安装的命令如下:
npm install vue-router@next -S
3.2 定义路由组件
在项目中定义 MyHome.vue
、MyMovie.vue
、MyAbout.vue
三个组件,将来要使用 vue-router 来控制它们的展示与切换
3.3 声明路由链接
和占位符
可以使用 <router-link>
标签来声明路由链接
,并使用 <router-view>
标签来声明路由占位符
。示例代码如下:
<template>
<h1>
App 组件
</h1>
<!-- 声明路由链接 -->
<router-link to="/home">首页</router-link>
<router-link to="/movie">电影</router-link>
<router-link to="/about">关于</router-link>
<!-- 声明路由占位符 -->
<router-view></router-view>
</template>
3.4 创建路由模块
在项目中创建 router.js
路由模块,在其中按照如下 4 个步骤
创建并得到路由的实例对象:
- 从 vue-router 中按需导入两个方法
- 导入需要使用路由控制的组件
- 创建路由实例对象
- 向外共享路由实例对象
- 在 main.js 中导入并挂载路由模块
从 vue-router 中按需导入两个方法
// 1. 从 vue-router 中按需导入两个方法
// createRouter 方法用于创建路由的实例对象
// createWebHashHistory 用于指定路由的工作模式(hash 模式)
import { createRouter, createWebHashHistory } from 'vue-router'
创建路由实例对象
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'
// 3. 创建路由实例对象
const router = createRouter({
// 3.1. 通过 history 属性指定路由的工作模式
history: createWebHashHistory(),
// 3.2 通过 routes 数组,指定路由规则
routes: [
// path 是 hash 地址,component 是要展示的组件
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
]
})
// 4. 向外界共享路由实例对象
export default = router
import { createApp } from 'vue'
import App from './App.vue'
// 5. 在 main.js 入口文件中导入并挂载路由模块
import router from './router.js'
const app = createApp(App)
app.use(router)
app.mount('#app')
vue-router 的高级用法
1. 路由重定向
路由重定向
指的是:用户在访问地址 A
的时候,强制用户跳转
到地址 C,从而展示特定的组件页面。
ton过哦路由规则的 redirect
属性,指定一个新的路由地址,可以很方便的设置路由的重定向:
const router = createRouter({
history: createWebHashHistory(),
routes: [
// 其中,path 表示需要被重定向的“原地址”,redirect 表示将要被重定向的“新地址”
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
]
})
// 向外界共享路由实例对象
export default = router
2. 路由高亮
可以通过如下的两种方式,将激活的路由链接
进行高亮显示:
- 使用
默认的
高亮 class 类 自定义
路由高亮的 class 类
2.1 默认的高亮 class 类
被激活的路由链接,默认会应用一个叫做 router-link-active
的类名。开发者可以使用此类名选择器
,为激活的路由链接
设置高亮的样式:
/* 在 index.css 全局样式表中, 重新 router-link-active 的样式 */
.router-link-active {
background-color: red;
color: white;
font-weight: bold;
}
2.2 自定义路由高亮的 class 类
在创建路由的实例对象时,开发者可以基于 linkActiveClass
属性,自定义路由链接被激活时,所应用的类名:
const router = createRouter({
history: createWebHashHistory(),
// 指定被激活的路由链接,会应用 router-active 这个类名,
// 默认的 router-link-active 类名会被覆盖掉
linkActiveClass: 'router-active',
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
]
})
3. 嵌套路由
通过路由实现组件的嵌套展示
,叫做嵌套路由
- 声明
子路由链接
和子路由占位符
- 在父路由规则中,通过
children
属性嵌套声明
子路由规则
3.1 声明子路由链接
和子路由占位符
在 About.vue 组件中, 声明 tab1 和 tab2 的子路由链接
以及子路由占位符
。示例代码如下:
<template>
<h3>这是 MyAbout 组件</h3>
<!-- 在关于页面中,声明两个子路由链接 -->
<router-link to="/about/tab1">tab1</router-link>
<router-link to="/about/tab2">tab2</router-link>
<!-- 在关于页面中,声明 tab1 和 tab2 的路由占位符 -->
<router-view></router-view>
</template>
3.2 通过 children
属性声明子路由规则
在 router.js 路由模块中,导入需要的组件,并使用 children 属性
声明子路由规则。示例代码如下:
import Tab1 from './MyTab1.vue'
import Tab2 from './MyTab2.vue'
const router = createRouter({
routes: [
{ // about 页面的路由规则(父级路由规则)
path: '/about',
component: About,
children: [
{ path: 'tab1', component: Tab1 }, // 访问我 /about/tab1 时,展示 Tab1 组件
{ path: 'tab2', component: Tab2 }, // 访问我 /about/tab2 时,展示 Tab2 组件
]
},
]
})
4. 动态路由匹配
思考:有如下 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 个路由规则,是否可行???
{ path: '/movie/1', component: Movie }
{ path: '/movie/2', component: Movie }
{ path: '/movie/3', component: Movie }
缺点:路由规则的复用性差。
4.1 动态路由的概念
动态路由指的是:把 Hash 地址中的可变的部分
定义为参数项
,从而提高路由规则的复用性
。在 vue-router 中使用使用英文的冒号
(:
)来定义路由的参数项,示例代码如下:
// 路由中的动态参数以 :进行声明,冒号后面的是动态参数的名称
{ path: '/movie/:id', component: Movie }
// 将以下 3 个路由规则,合并成了一个,提高了路由规则的复用性
{ path: '/movie/1', component: Movie }
{ path: '/movie/2', component: Movie }
{ path: '/movie/3', component: Movie }
4.2 $route.params
参数对象
通过动态路由匹配的方式渲染出来的 组件中,可以使用 $route.params
对象访问到动态匹配的参数值
。
<template>
<!-- $route.params 是路由的“参数对象” -->
<h3>
MyMovie 组件 --- {{$route.params.id}}
</h3>
</template>
4.3 使用 props 接收路由参数
为了简化路由参数的获取形式
,vue-router 允许在路由规则
中开始 props 传参
。示例代码如下:
// 1. 在定义路由规则时,声明 props: true 选项,
// 即可在 Movie 组件中,以 props 的形式接收到路由规则匹配到的参数项
{ path: '/movie/:id', component: Movie, props: true }
<template>
<!-- 3. 直接使用 props 中接收的路由参数 -->
<h3>
MyMovie 组件 --- {{id}}
</h3>
</template>
<script>
export default {
props: ['id'] // 2. 使用 props 接收路由规则中匹配到的参数项
}
</script>
5. 编程式导航
通过调用 API
实现导航的方式,叫做编程式导航
。与之对应的,通过点击链接
实现导航的方式,叫做声明式导航
。例如:
- 普通网页中点击
<a> 链接
、vue 项目中点击<router-link>
都属于声明式导航- 普通网页中调用
location.href
跳转到新页面的方式,属于编程式导航
- 普通网页中调用
5.1 vue-router 中的编程式导航 API
vue-router 提供了许多编程式导航的 API,其中最常用的两个 API 分别是:
- this.
$router.push
(‘hash 地址’)- 跳转到指定 Hash 地址,从而展示对应的组件
- this.
$router.go
(数值 n)- 实现导航历史的前进、后退
5.2 $router.push
调用 this.$router.push
() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。示例代码如下:
<template>
<div>
<h3>这是 MyHome 组件</h3>
<button @click="gotoMovie(3)">go to Movie</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
methods: {
gotoMovie(id) { // id 参数是电影的 id 值
this.$router.push(`/movie/${id}`)
}
},
}
</script>
5.3 $router.go
调用 this.$router.go
() 方法,可以在浏览历史中进行前进和后退。示例代码如下:
<template>
<div>
<h3>这是 MyMovie 组件 --- {{ id }}</h3>
<button @click="goBack">后退</button>
</div>
</template>
<script>
export default {
name: 'MyMovie',
props: ['id'],
methods: {
goBack() {
this.$router.go(-1) // 后退到之前的组件页面
}
}
}
</script>
6. 命名路由
通过 name 属性
为路由规则定义名称
的方式,叫做命名路由
。示例代码如下:
{
path: '/movie/:id',
// 使用 name 属性为当前的路由规则定义一个“名称”
name:'mov',
component: Movie,
props: true,
}
注意:命名路由的 name 值不能重复,必须保证唯一性!
6.1 使用命名路由实现声明式导航
为 <router-link> 标签动态绑定 to 属性的值,并通过 name 属性
指定要跳转到的路由规则。期间还可以用 params 属性
指定跳转期间要携带的路由参数。示例代码如下:
<template>
<div>
<h3>这是 MyHome 组件</h3>
<router-link :to="{name: 'mov', params: {id:3}}">go to Movie</router-link>
</div>
</template>
<script>
export default {
name: 'MyHome',
}
</script>
6.2 使用命名路由实现编程式导航
调用 push 函数
期间指定一个配置对象
,name
是要跳转到的路由规则、params
是携带的路由参数:
<template>
<div>
<h3>这是 MyHome 组件</h3>
<button @click="gotoMovie(3)">go to Movie</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
methods: {
gotoMovie(id) { // id 参数是电影的 id 值
this.$router.push({
name: 'mov',
params: {id: 3}
})
}
},
}
</script>
7. 导航守卫
导航守卫
可以控制路由的访问权限
。示意图如下:
7.1 如何声明全局的导航守卫
全局导航守卫
会拦截每个路由规则
,从而对每个路由进行访问权限
的控制。可以按照如下的方式定义全局导航守卫:
// 创建路由实例对象
const router = createRouter({ ... })
// 调用路由实例对象的 beforeEach 函数,声明“全局前置守卫”
// fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
// 因此 fn 叫做 “守卫方法”
router.beforeEach(fn)
7.2 守卫方法的 3 个形参
全局导航守卫
的守卫方法中接收 3 个形参,格式为:
// 创建路由实例对象
const router = createRouter({ ... })
// 全局前置守卫
router.beforeEach((to, from, next) => {
// to 目标路由对象
// from 当前导航正要离开的路由对象
// next 是一个函数,表示放行
})
注意:
- 在守卫方法中如果
不声明 next 形参
,则默认允许用户访问每一个路由!
- 在守卫方法中如果
声明了 next 形参
,则必须调用 next() 函数
,否则不允许用户访问任何一个路由!
7.3 next 函数的 3 种调用方式
参考示意图,分析 next 函数的 3 中调用方式最终导致的结果:
直接放行:next()
强制其停留在当前页面
:next(false
)
强制其跳转到登录页面
:next(‘/login
’)
7.4 结合 token 控制后台主页的访问权限
// 全局前置守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token') // 1. 读取 token
if (to.path === '/main' && !token) {
// next(false) 3.1 不允许跳转
next('/login') // 3.2 强制跳转到“登录页面”
} else {
next() // 3.3 直接放行,允许访问“后台主页”
}
})
总结
- 能够知道如何在 vue 中配置路由
- createRouter、app.
use
(router)
- createRouter、app.
- 能够知道如何使用嵌套路由
- 通过
children
属性进行路由嵌套、子路由的 hash 地址不要以/开头
- 通过
- 能够知道如何实现动态路由匹配
- 使用
冒号
声明参数项、this.$route
.params、props: true
- 使用
- 能够知道如何使用编程式导航
- this.$router.push、this.$router.go(-1)
- 能够知道如何使用全局导航守卫
- 路由实例.
beforeEach
((to, from,next
) => { })
- 路由实例.
VUE 基础 - 综合案例
vue-cil
1. 什么是 vue-cli
vue-cli
(俗称:vue 脚手架
)是 vue 官方提供的、快速生成 vue 工程化项目
的工具。
特点
:
- 开箱即用
- 基于 webpack
- 功能丰富且易于扩展
- 支持创建 vue2 和 vue3 的项目
vue-cli 的中文官网首页:https://cli.vuejs.org/zh/
2. 安装 vue-cli
vue-cli 是基于 Node.js 开发出来的工具,因此需要使用 npm
将它安装为全局可用的工具
:
// 全局安装 vue-cli
npm install -g @vue/cli
// 查看 vue-cli 的版本,检验 vue-cli 是否安装成功
vue --version
2.1 解决 windows PowerShell 不识别 vue 命令
的问题
默认情况下,在 PowerShell 中执行 vue –version
命令会提示错误信息
解决方案如下:
- 以
管理员身份
运行 PowerShell - 执行
set-ExecutionPolicy RemoteSigned
命令 - 输入字符
Y
,回车
即可
3. 创建项目
vue-cli 提供了创建项目的两种方式
# 基于【命令行】的方式创建 vue 项目
vue create 项目名称
# OR
# 基于 【可视化面板】 创建 vue 项目
vue ui
4. 基于 vue ui 创建 vue项目
步骤1:在终端下运行 vue ui
命令,自动在浏览器中打开创建项目的可视化面板
:
步骤2:在详情
页面填写项目名称
:
步骤3:在预设
页面选择手动配置项目
:
步骤4:在功能
页面勾选需要安装的功能
(Choose Vue Version、Babel、CSS 预处理器、== ==):
步骤5:在配置
页面勾选 vue 的版本
和需要的预处理器
:
步骤6:将刚才所有的配置保存为预设
(模板),方便下一次创建项目时直接复用之前的配置
:
步骤7:创建项目并自定安装依赖包:
vue ui 的本质
:通过可视化的面板采集
到用户的配置信息后,在后台基于命令行的方式
自动初始化项目:
项目创建完成后:自动进入项目仪表盘
:
5. 基于命令行
创建 vue 项目
步骤1:在终端下运行 vue create
dome2 命令,基于交互式的命令行
创建 vue 的项目:
步骤2: 选择要安装的功能:
步骤3:使用上下箭头
选择 vue 的版本,并使用回车键
确认选择:
步骤4:使用上下箭头
选择要使用的 css 预处理器,并使用回车键
确认选择:
步骤5:使用上下箭头
选择如何存储插件的配置信息
,并使用回车键
确认选择:
步骤6:是否将刚才的配置保存为预设:
步骤7:选择如何安装项目中的依赖包:
步骤8:开始创建项目并自定安装依赖包:
步骤9:项目创建完成:
6. 在 Vue2 的项目中使用路由
在 vue2 的项目中,只能安装并使用 3.x 版本
的 vue-router。
版本3 和版本4的路由的最主要
的区别:创建路由模块的方式不同!
6.1 回顾:4.x 版本的路由如何创建路由模块
import { createRouter, createWebHashHistory } from 'vue-router' // 1. 按需导入需要的方法
import Home from './components/MyHome.vue' // 2. 导入需要使用路由进行切换的组件
import Movie from './components/MyMovie.vue'
import About from './components/MyAbout.vue'
// 3. 创建路由实例对象
const router = createRouter({ // 3. 创建路由对象
history: createWebHashHistory(), // 3.1 指定通过 hash 管理路由的切换
routes: [ // 3.2 创建路由规则
// path 是 hash 地址,component 是要展示的组件
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
]
})
export default router // 4. 向外共享路由对象
6.2 学习:3.x 版本的路由如何创建路由模块
步骤1:在 vue2 的项目中安装 3.x 版本的路由:
npm i vue-router@3.4.9 -S
步骤2:在 src -> components 目录下,创建需要使用路由切换的组件
步骤3:在 src 目录下创建 router -> index.js 路由模块:
import Vue from 'vue' // 1. 导入 Vue2 的构造函数
import VueRouter from 'vue-router' // 2. 导入 3.x 路由的构造函数
import Home from '@/components/MyHome.vue' // 2. 导入需要使用路由进行切换的组件
import Movie from '@/components/MyMovie.vue'
import About from '@/components/MyAbout.vue'
Vue.use(VueRouter) // 4. 调用 Vue.use() 函数,把路由配置为 Vue 的插件
const router = new VueRouter({ // 5. 创建路由对象
routes: [ // 5.1 创建路由规则
// path 是 hash 地址,component 是要展示的组件
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
]
})
export default router // 6. 向外共享路由对象
步骤4:在 main.js
中导入路由模块,并通过 router 属性
进行挂载:
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,
router,
}).$mount('#app')
组件库
1. 什么是 vue 组件库
在实际开发中,前端开发者可以把自己封装的 .vue 组件
整理、打包、并发布为 npm 的包
,从而供其他人下载和使用。这种可以直接下载并在项目中使用的现场组件,就叫做 vue 组件库。
2. vue 组件库
和 bootstrap
的区别
二者之间存在本质的区别:
- bootstrap 只提供了
纯粹的原材料
(css 样式、HTML 结构以及 js 特效),需要由开发者做进一步的组装
和改造
- vue 组件库是
遵循 vue 语法、高度定制
的现场组件,开箱即用
3. 最常用的 vue 组件库
PC 端
- Element UI (https://element.eleme.cn/#/zh-CN)
- View UI (http://v1.iviewui.com/)
移动端
- Mint UI (htpp://mint-ui.github.io/#!/zh-cn)
- Vant (https://vant-contrib.gitee.io/vant/#/zh-CN/)
4. Element UI
Element UI 是饿了么前端团队
开源的一套 PC 端 vue 组件库
。支持在 vue2 和 vue3 的项目中使用:
Vue2
的项目使用旧版
的 Element UI (https://element.eleme.cn/#/zh-CN)vue3
的项目使用新版
的 Element Plus(https://element-plus.gitee.io/#/zh-CN)
4.1 在 vue2 的项目中安装 element-ui
npm i element-ui -S
4.2 引入
element-ui
开发者可以一次性完整引入所有的
element-ui 组件,或是根据需求,只按需引入用到的
element-ui 组件:
- 完整引入:操作简单,但是会
额外引入
一些用不到的组件,导致项目体积过大
- 按需引入:操作相对复杂一些,但是
只会引入用到的组件
,能起到优化项目体积的目的
4.3 完整
引入
在 main.js
中写入以下内容:
import Vue from 'vue'
import App from './App.vue'
// 1. 导入 element-ui 组件
import ElementUI from 'element-ui'
// 2. 导入 element-ui 组件的样式
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
// 3. 把 ElementUI 注册为 vue 的插件 【注册之后,即可在每个组件中直接使用每一个 element ui 的组件】
Vue.use(ElementUI)
new Vue({
render: h => h(App),
// 2. 挂载路由模块,
// router: router,
router,
}).$mount('#app')
4.4 按需
引入
借助 babel-plugin-component
,我们可以只引入需要的组件,以达到减小项目体积
的目的。
步骤1,安装 babel-plugin-component:
npm install babel-plugin-component -D
步骤2,修改根目录下的 babel.config.js
配置文件,新增 plugins
节点如下:
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk',
}
]
]
}
步骤3,如果你只希望引入部分组件
,比如 Button,那么需要在 main.js
中写入以下内容:
import Vue from 'vue'
import App from './App.vue'
// 1. 按需导入 element-ui 的组件
import {Button} from 'element-ui'
// 2. 注册需要的组件
Vue.component(Button.name, Button)
// 或写为 Vue.use(Button)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
// 2. 挂载路由模块,
// router: router,
router,
}).$mount('#app')
4.5 把组件的导入和注册封装为独立的模块
在 src 目录下新建 element-ui/index.js
模块,并声明如下的代码:
import Vue from 'vue' // -> 模块路径 /src/element-ui/index.js
// 按需导入element-ui 的组件
import { Button, Input } from 'element-ui'
// 注册需要的组件
Vue.use(Button)
Vue.use(Input)
// -> 在 main.js 中导入
import './element-ui'
axios 拦截器
1. 回顾:在 vue3 的项目中全局配置 axios
import { createApp } from 'vue'
import App from './App.vue'
// 1. 导入 axios
import axios from 'axios'
const app = createApp(App)
// 2. 配置请求根路径
axios.defaults.baseURL = 'https://www.escook.cn'
// 3. 全局配置 axios
app.config.globalProperties.$http = axios
app.mount('#app')
2. 在 vue2 的项目中全局配置 axios
需要在 main.js
入口文件中,通过 Vue 构造函数
的 prototype 原型对象
全局配置 axios:
import Vue from 'vue'
import App from './App.vue'
// 1. 导入 axios
import axios from 'axios'
// 2. 配置请求根路径
axios.dafaults.baseURL = 'https://www.escook.cn'
// 3. 通过 Vue 构造函数的原型对象,全局配置 axios
Vue.prottype.$htpp = axios
new Vue({
render: h => h(App),
}).$mount('#app')
3. 什么是拦截器
拦截器
(英文名:Interceptors)会在每次发起 Ajax 请求
和得到响应
的时候自动被触发
应用场景:
- Token 身份认证
- Loading 效果
- etc…
4. 配置请求拦截器
通过 axios.interceptors.request
.use(成功的回调
,失败的回调) 可以配置请求拦截器。示例代码如下:
axios.interceptors.request.use(config => {
// Do something before request is sent
return config;
}, error => {
// Do something with request error
return Promise.reject(error)
})
注意:失败的回调函数可以被省略!
4.1 请求拦截器 - Token 认证
import axios from 'axios'
axios.dafaults.baseURL = 'https://www.escook.cn'
// 配置请求的拦截器
axios.interceptors.request.use(config => {
// 为当前请求配置 Token 认证字段
config.headers.Authorization = 'Bearer xxx'
return config
})
Vue.prottype.$htpp = axios
4.2 请求拦截器 - 展示 Loading 效果
借助于 element ui 提供的 Loading 效果
组件(https://element.eleme.cn/#/zh-CN/component/loading)可以方便的实例 Loading 效果的展示:
// 1. 按需导入 Loading 效果组件
import { Loading } from 'element-ui'
// 2. 声明变量,用来存储 Loading 组件的实例对象
let ladingInstance = null;
// 配置请求的拦截器
xios.interceptors.request.use(config => {
// 3. 调用 Loading 组件的 service() 方法,创建 Loading 组件的实例,并全屏展示 loading 效果
loadingInstance = Loading.service({ fullscreen: true })
return config
})
5. 配置响应拦截器
通过 axios.interceptors.response
.use(成功的回调
,失败的回调) 可以配置请求拦截器。示例代码如下:
axios.interceptors.response.use(response => {
// Do something with response data
return cresponse;
}, error => {
// Do something with response error
return Promise.reject(error)
})
注意:失败的回调函数可以被省略!
5.1 响应拦截器 - 关闭 Loading 效果
调用 Loading 实例
提供的 close()
方法即可关闭 Loading 效果
,示例代码如下:
// 响应拦截器
axios.interceptors.response.use(response => {
// 调用 Loading 实例的 close 方法即可关闭 loading 效果
loadingInstance.close()
return response
})
proxy 跨域代理
1. 回顾:接口的跨域问题
vue 项目运行的地址:http://localhost:8080/
API 接口运行的地址:https://www.escook.cn/api/users
由于当前的 API 接口没有开启 CORS
跨域资源共享,因此默认情况下,上面的接口无法请求成功
!
2. 通过代理
解决接口的跨域问题
通过 vue-cli 创建的项目在遇到接口跨域问题时,可以通过代理
的方式来解决:
- 把 axios 的
请求根路径
设置为vue 项目的运行地址
(接口请求不再跨域) - vue 项目发现请求的接口不存在,把请求
转交给 proxy 代理
- 代理把请求根路径
替换为
devServer.proxy 属性的值,发起真正的数据请求
- 代理把请求到的数据,
转发给 axios
3. 在项目中配置 proxy 代理
步骤1,在 main.js
入口文件中,把 axios 的请求根路径
改造为当前 web 项目的根路径
:
// axios.defaults.baseURL = 'httts://www.escook.cn'
// 配置请求根路径
axios.defaults.baseURL = 'httts://localhost:8080'
步骤2,在项目根目录
下创建 vue.config.js
的配置文件,并声明如下的配置:
module.exports = {
devServer: {
// 当前项目在开发调试阶段,
// 会将任何未知请求(没有匹配到静态文件的请求)代理到 https://www.escook.cn
proxy: 'https://www.escook.cn',
}
}
总结
- 能够知道如何使用 vue-cli 创建项目
- vue ui、
vue create 项目名称
- vue ui、
- 能够知道如何在项目中安装和配置 element-ui
- 完整引入、
按需引入
、参考官方文档进行配置
- 完整引入、
- 能够知道 element-ui 中
常见的组件
的用法- Table 表格、Form 表单、Dialog 对话框、Message 消息、MessageBox 弹框
- 能够知道如何使用 axios 拦截器
- axios.interceptors.
request
.use()、axios.interceptors.response
.use()
- axios.interceptors.
- 能够知道如何配置 proxy 代理
- 修改请求根路径、
vue.config.js
、devServer.proxy
- 修改请求根路径、