Vue3.js
如何关闭烦人的vscode的提示框
https://blog.csdn.net/liuyuemozhu/article/details/101056556
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 默认导出的成员
//定义模块私有成员n1
let n1 = 10
//定义模块私有成员n2(外界访问不到n2,因为他没有被共享出去)
let n2 = 20
function show() {}
export default {
//使用export default 默认导出语法,向外共享n1 和show 两个创建
n1,
show
}
默认导入的语法: import 接收名称 from ‘模块标识符’
import m1 from './01-默认导出'
console.log(m1)
每个模块中,只允许使用唯一的一次 export default,否则会报错!
5.2 按需导出
按需导出的语法: export 按需导出的成员
export let s1 = 'aaa'
export let s2 = 'ccc'
export function say() {}
按需导入的语法: import { s1 } from ‘模块标识符’
//导入模块成员
import { s1, s2, say } from './03- 按需导出 .js'
console.log(s1)
console.log(s2)
console.log(say)
这里插播一个:真的就是无语他妈给无语开门,无语到家了,代码是这个代码,然后在导出时,一直报错,错误提示是:code: ‘ERR_MODULE_NOT_FOUND’,然后我一直到网上找这个那个解决方法,然后没有用。然后,我没有理他,准备跳过的时候,他就好了。如果有大佬看见,希望可以解答一下我的疑惑。
按需导出与按需导入的注意事项
① 每个模块中可以使用多次按需导出
② 按需导入的成员名称必须和按需导出的名称保持一致
③ 按需导入时,可以使用 as 关键字进行重命名
④ 按需导入可以和默认导入一起使用
5.3 直接导入并执行模块中的代码
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模 块代码,示例代码如下:
//在当前模块中执行一个for循环操作
for (let i = 0; i < 3 ; i++) {
console.log(i)
}
----------分割线-------------
//直接导入并执行模块代码,不需要得到模块向外共享的成员
//注意要加后缀名
import './05-直接导入并执行模块中的代码.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 => { })
- 调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的
2. 基于回调函数按顺序读取文件内容
//读取文件1.txt
fs.readFiles('./files/1.txt', 'utf-8', (err1, r1) => {
if (err1) return console.log(err1.message) //读取文件失败
console.log(r1)
//读取文件1.txt
fs.readFiles('./files/2.txt', 'utf-8', (err2, r2) => {
if (err2) return console.log(err2.message) //读取文件失败
console.log(r2)
fs.readFiles('./files/3.txt', 'utf-8', (err3, r3) => {
if (err3) return console.log(err3.message) //读取文件失败
console.log(r3)
})
})
})
3. 基于 then-fs 读取文件内容
由于 node.js 官方提供的 fs 模块仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此,需 要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件的内容:
npm install then-fs
3.1 then-fs 的基本使用
调用 then-fs 提供的 readFile() 方法,可以异步地读取文件的内容,它的返回值是 Promise 的实例对象。因 此可以调用 .then() 方法为每个 Promise 异步操作指定成功和失败之后的回调函数。示例代码如下:
import thenFs from 'then-fs'
thenFs.readFile('./files/1.txt', 'utf-8').then((r1) => { console.log(r1) })
thenFs.readFile('./files/2.txt', 'utf-8').then((r2) => { console.log(r2) })
thenFs.readFile('./files/3.txt', 'utf-8').then((r3) => { console.log(r3) })
注意:上述的代码无法保证文件的读取顺序,需要做进一步的改进!
3.2 .then() 方法的特性
如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续进行处理。通 过 .then() 方法的链式调用,就解决了回调地狱的问题。
3.3 基于 Promise 按顺序读取文件的内容
Promise 支持链式调用,从而来解决回调地狱的问题。示例代码如下:
import thenFs from 'then-fs'
// 返回值是Promise的实例对象
thenFs.readFile('./files/1.txt', 'utf-8')
.then((r1) => { //2.通过.then为第一个Promise实例指定成功之后的回调函数
console.log(r1)
return thenFs.readFile('./files/2.txt', 'utf-8') //3.在第一个.then中返回一个新的Promise实例对象
})
.then((r2) => { //4.继续调用.then,为上一个.then 的返回值(新的 Promise实例)指定成功之后的回调函数
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf-8') //5.在第二个 .then中再返回一个新的 Promise实例对象
})
.then((r3) => { //6.继续调用.then,为上一个.then的返回值(新的Promise实例)指定成功之后的回调函数
console.log(r3)
})
3.4 通过 .catch 捕获错误
在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理:
.catch((err) => {
console.log(err.message)
})
如果不希望前面的错误导致后续的 .then 无法正常执行,则可以将 .catch 的调用提前,示例代码如下:
import thenFs from 'then-fs'
// 返回值是Promise的实例对象
thenFs.readFile('./files/11.txt', 'utf-8')
.catch((err) => {
console.log(err.message)
})
.then((r1) => { //2.通过.then为第一个Promise实例指定成功之后的回调函数
console.log(r1)
return thenFs.readFile('./files/2.txt', 'utf-8') //3.在第一个.then中返回一个新的Promise实例对象
})
.then((r2) => { //4.继续调用.then,为上一个.then 的返回值(新的 Promise实例)指定成功之后的回调函数
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf-8') //5.在第二个 .then中再返回一个新的 Promise实例对象
})
.then((r3) => { //6.继续调用.then,为上一个.then的返回值(新的Promise实例)指定成功之后的回调函数
console.log(r3)
})
.catch((err) => {
console.log(err.message)
})
3.5 Promise.all() 方法
Promise.all() 方法会发起并行的 Promise 异步操作,等所有的异步操作全部结束后才会执行下一步的 .then 操作(等待机制)。示例代码如下:
import thenFs from 'then-fs'
const promiseArr = [
thenFs.readFile('./files/1.txt', 'utf-8'),
thenFs.readFile('./files/2.txt', 'utf-8'),
thenFs.readFile('./files/3.txt', 'utf-8')
]
Promise.all(promiseArr).then(result => {
console.log(result)
})
3.6 Promise.race() 方法
Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作(赛跑机制)。示例代码如下:
import thenFs from 'then-fs'
const promiseArr = [
thenFs.readFile('./files/1.txt', 'utf-8'),
thenFs.readFile('./files/2.txt', 'utf-8'),
thenFs.readFile('./files/3.txt', 'utf-8')
]
Promise.race(promiseArr)
.then(result => {
console.log(result) //111
})
.catch(err => {
console.log(err.message)
})
4. 基于 Promise 封装读文件的方法
方法的封装要求:
① 方法的名称要定义为 getFile
② 方法接收一个形参 fpath,表示要读取的文件的路径
③ 方法的返回值为 Promise 实例对象
4.1 getFile 方法的基本定义
// 1.方法的名称为getFile
// 2.方法接收一个形参 fpath,表示要读取的文件的路径
function getFile(fpath){
//3.方法的返回值为Promise的实例对象
return new Promise()
}
注意:第 5 行代码中的 new Promise() 只是创建了一个形式上的异步操作。
4.2 创建具体的异步操作
如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的 异步操作定义到 function 函数内部。示例代码如下:
import fs from 'fs'
// 1.方法的名称为getFile
// 2.方法接收一个形参 fpath,表示要读取的文件的路径
function getFile(fpath){
//3.方法的返回值为Promise的实例对象
return new Promise(function () {
fs.readFile(fpath,'utf-8',(err,dataStr) => {})
})
}
4.3 获取 .then 的两个实参
通过 .then() 指定的成功和失败的回调函数,可以在 function 的形参中进行接收,示例代码如下:
import fs from 'fs'
// 1.方法的名称为getFile
// 2.方法接收一个形参 fpath,表示要读取的文件的路径
function getFile(fpath) {
//3.方法的返回值为Promise的实例对象
// resolve形参是:调用getFiles()方法时,通过 .then指定的成功的"回调函数
// reject形参是:调用 getFiles()方法时,通过.then指定的"失败的""回调函数
return new Promise(function(resolve,reject) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {})
})
}
//getFile方法的调用过程
getFile('./files/1.txt').then(成功的回调函数,失败的回调函数)
4.4 调用 resolve 和 reject 回调函数
Promise 异步操作的结果,可以调用 resolve 或 reject 回调函数进行处理。示例代码如下:
import fs from 'fs'
// 1.方法的名称为getFile
// 2.方法接收一个形参 fpath,表示要读取的文件的路径
function getFile(fpath) {
//3.方法的返回值为Promise的实例对象
// resolve形参是:调用getFiles()方法时,通过 .then指定的成功的"回调函数
// reject形参是:调用 getFiles()方法时,通过.then指定的"失败的""回调函数
return new Promise(function(resolve, reject) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err) return reject(err) //如果读取失败,则调用"失败的回调函数"
resolve(dataStr) //如果读取成功,则调用"成功的回调函数"
})
})
}
//getFile方法的调用过程
getFile('./files/1.txt').then((r1) => { console.log(r1) }, (err) => { console.log(err.message) })
getFile('./files/11.txt').then((r1) => { console.log(r1) }).catch(err => console.log(err.message))
async/await
1. 什么是 async/await
async/await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。在 async/await 出 现之前,开发者只能通过链式 .then() 的方式处理 Promise 异步操作。示例代码如下:
import thenFs from 'then-fs'
// 返回值是Promise的实例对象
thenFs.readFile('./files/11.txt', 'utf-8')
.then((r1) => { //2.通过.then为第一个Promise实例指定成功之后的回调函数
console.log(r1)
return thenFs.readFile('./files/2.txt', 'utf-8') //3.在第一个.then中返回一个新的Promise实例对象
})
.then((r2) => { //4.继续调用.then,为上一个.then 的返回值(新的 Promise实例)指定成功之后的回调函数
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf-8') //5.在第二个 .then中再返回一个新的 Promise实例对象
})
.then((r3) => { //6.继续调用.then,为上一个.then的返回值(新的Promise实例)指定成功之后的回调函数
console.log(r3)
})
.then 链式调用的优点:
解决了回调地狱的问题
.then 链式调用的缺点:
代码冗余、阅读性差、 不易理解
2. async/await 的基本使用
使用 async/await 简化 Promise 异步操作的示例代码如下:
import thenFs from 'then-fs'
async function getAllFile() {
const r1 = await thenFs.readFile('./files/1.txt', 'utf-8')
console.log(r1)
const r2 = await thenFs.readFile('./files/2.txt', 'utf-8')
console.log(r2)
const r3 = await thenFs.readFile('./files/3.txt', 'utf-8')
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', 'utf-8')
const r2 = await thenFs.readFile('./files/2.txt', 'utf-8')
const r3 = await thenFs.readFile('./files/3.txt', 'utf-8')
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 主线程执行异步任务的回调函数 EventLoop
3. 同步任务和异步任务的执行过程
① 同步任务由 JavaScript 主线程次序执行
② 异步任务委托给宿主环境执行
③ 已完成的异步任务对应的回调函数,会被 加入到任务队列中等待执行
④ JavaScript 主线程的执行栈被清空后,会 读取任务队列中的回调函数,次序执行
⑤ JavaScript 主线程不断重复上面的第 4 步 EventLoop
4. EventLoop 的基本概念
JavaScript 主线程从“任务队列”中读取异步 任务的回调函数,放到执行栈中依次执行。这 个过程是循环不断的,所以整个的这种运行机 制又称为 EventLoop(事件循环)。
5.结合 EventLoop 分析输出的顺序
import thenFs from "then-fs"
console.log('a')
thenFs.readFile('./files/1.txt', 'utf-8').then(dataStr => {
console.log('b')
})
setTimeout(() => {
console.log('c')
}, 0)
console.log('d')
// a
// d
// c 因为延时为0,所以比b读取文件更快,所以先打印c后打印b
// b
正确的输出结果: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 行代码
② 再执行微任务
执行第 9 行代码
③ 再执行下一个宏任务
执行第 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')
}).then(function() {
console.log('9')
})
})
正确的输出顺序是:156234789
API接口案例
前端工程化与 webpack
前端工程化
1. 小白眼中的前端开发 vs 实际的前端开发
小白眼中的前端开发:
- 会写 HTML + CSS + JavaScript 就会前端开发
- 需要美化页面样式,就拽一个 bootstrap 过来
- 需要操作 DOM 或发起 Ajax 请求,再拽一个 jQuery 过来
- 需要渲染模板结构,就用 art-template 等模板引擎
实际的前端开发:
- 模块化(js 的模块化、css 的模块化、其它资源的模块化)
- 组件化(复用现有的 UI 结构、样式、行为)
- 规范化(目录结构的划分、编码规范化、接口规范化、文档规范化、 Git 分支管理)
- 自动化(自动化构建、自动部署、自动化测试)
2. 什么是前端工程化
前端工程化指的是:在企业级的前端项目开发中,把前端开发所需的工具、技术、流程、经验等进行规范化、 标准化。最终落实到细节上,就是实现前端的“4 个现代化”: 模块化、组件化、规范化、自动化
3. 前端工程化的好处
前端工程化的好处主要体现在如下两方面:
① 前端工程化让前端开发能够“自成体系”,覆盖了前端项目从创建到部署的方方面面
② 最大程度地提高了前端的开发效率,降低了技术选型、前后端联调等带来的协调沟通成本 前端工程化
4.前端工程化的解决方案
早期的前端工程化解决方案:
- grunt( https://www.gruntjs.net/ )
- gulp( https://www.gulpjs.com.cn/ )
目前主流的前端工程化解决方案:
- webpack( https://www.webpackjs.com/ )
- parcel( https://zh.parceljs.org/ )
webpack 的基本使用
1. 什么是 webpack
概念:webpack 是前端项目工程化的具体解决方案。
主要功能:它提供了友好的前端模块化开发支持,以及代码压缩混淆、处理浏览器端 JavaScript 的兼容性、性 能优化等强大的功能。
好处:让程序员把工作的重心放到具体功能的实现上,提高了前端开发效率和项目的可维护性。
注意:目前企业级的前端项目开发中,绝大多数的项目都是基于 webpack 进行打包构建的。
2. 创建列表隔行变色项目
① 新建项目空白目录,并运行 npm init –y 命令,初始化包管理配置文件 package.json
② 新建 src 源代码目录
③ 新建 src -> index.html 首页和 src -> index.js 脚本文件
④ 初始化首页基本的结构
⑤ 运行 npm install jquery –S 命令,安装 jQuery
⑥ 通过 ES6 模块化的方式导入 jQuery,实现列表隔行变色效果 webpack 的基本使用
3. 在项目中安装 webpack
在终端运行如下的命令,安装 webpack 相关的两个包:
npm install webpack@5.5.1 webpack-cli@4.2.0 -D
4. 在项目中配置
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
}
③ 在终端中运行 npm run dev 命令,启动 webpack 进行项目的打包构建 webpack 的基本使用
4.1 mode 的可选值
mode 节点的可选值有两个,分别是:
① development
- 开发环境
- 不会对打包生成的文件进行代码压缩和性能优化
- 打包速度快,适合在开发阶段使用
② production
- 生产环境
- 会对打包生成的文件进行代码压缩和性能优化
- 打包速度很慢,仅适合在项目发布阶段使用
4.2 webpack.config.js 文件的作用
webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件, 从而基于给定的配置,对项目进行打包。
注意:由于 webpack 是基于 node.js 开发出来的打包工具,因此在它的配置文件中,支持使用 node.js 相关 的语法和模块进行 webpack 的个性化配置。
4.3 webpack 中的默认约定
在 webpack 中有如下的默认约定:
① 默认的打包入口文件为 src -> index.js
② 默认的输出文件路径为 dist -> main.js
注意:可以在 webpack.config.js 中修改打包的默认约定
4.4 自定义打包的入口与出口
在 webpack.config.js 配置文件中,通过 entry 节点指定打包的入口。通过 output 节点指定打包的出口。 示例代码如下:
const path = require('path')
module.exports = {
mode: 'development',
//打包入口文件的路径
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"
}
② 再次运行 npm run dev 命令,重新进行项目的打包
③ 在浏览器中访问 http://localhost:8080 地址,查看自动打包效果 注意:webpack-dev-server 会启动一个实时打包的 http 服务器 webpack 中的插件
2.3 打包生成的文件哪儿去了?
① 不配置 webpack-dev-server 的情况下,webpack 打包生成的文件,会存放到实际的物理磁盘上
- 严格遵守开发者在 webpack.config.js 中指定配置
- 根据 output 节点指定路径进行存放
② 配置了 webpack-dev-server 之后,打包生成的文件存放到了内存中
- 不再根据 output 节点指定的路径,存放到实际的物理磁盘上
- 提高了实时打包输出的性能,因为内存比物理磁盘速度快很多
2.4 生成到内存中的文件该如何访问?
webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中,而且是虚拟的、不可见的。
-
可以直接用 / 表示项目根目录,后面跟上要访问的文件名称,即可访问内存中的文件
-
例如 /bundle.js 就表示要访问 webpack-dev-server 生成到内存中的 bundle.js 文件
-
<script src="/bundle.js"></script>
3. 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
在webpack.config.js里面添加
// 1.导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建插件的实例对象
const HtmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html'
})
module.exports = {
mode: 'development',
// 3.挂载插件的实例对象
Plugin: [HtmlPlugin]
}
const path = require('path')
// 1.导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建插件的实例对象
const HtmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html',
})
module.exports = {
mode: 'development',
//打包入口文件的路径
entry: path.join(__dirname, './src/index.js'),
output: {
//输出文件的存放路径
path: path.join(__dirname, './dist'),
// 输出文件的名称
filename: 'bundle.js',
},
// 挂载插件的实例对象
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: '12.0.0.1',//实时打包所使用的主机地址
port: 80 //实时打包所使用的端口号
}
注意:凡是修改了 webpack.config.js 配置文件,或修改了 package.json 配置文件,必须重启实时打包的服 务器,否则最新的配置文件无法生效!
webpack.config.js
const path = require('path')
// 1.导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html',
})
module.exports = {
mode: 'development',
//打包入口文件的路径
entry: path.join(__dirname, './src/index.js'),
output: {
//输出文件的存放路径
path: path.join(__dirname, './dist'),
// 输出文件的名称
filename: 'bundle.js',
},
// 挂载插件的实例对象
plugins: [htmlPlugin],
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 文件
① 运行 npm i style-loader@2.0.0 css-loader@5.0.1 -D 命令,安装处理 css 文件的 loader
② 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
module: {//所有第三方文件配置模块的匹配规则
rules: [//文件后缀的匹配规则
{ test: /\.css/, use: ['style-loager', 'css-loader'] }//\代表转译的意思,把这个点转译成一个真正的引号,以css结尾的文件,都用ues数组调用的loader
]
}
其中,test 表示匹配的文件类型, use 表示对应要调用的 loader
注意:
-
use 数组中指定的 loader 顺序是固定的
-
多个 loader 的调用顺序是:从后往前调用 webpack 中的 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$/,ues:'url-loader?limit=22229'},
]
}
其中 ? 之后的是 loader 的参数项:
-
limit 用来指定图片的大小,单位是字节(byte)
-
只有 ≤ limit 大小的图片,才会被转为 base64 格式的图片
相关文件
index.less
html,body,ul{
margin: 0;
padding: 0;
li{
line-height: 35px;
padding: 10px;
font-size: 12px;
}
}
#box {
width: 380px;
height: 114px;
background-color: red;
background: url('../image/1BC1D6FF35C9D9FE28A5C4FEB17F55A2.png');
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- index.js存在兼容性问题 -->
<!-- <script src="./index.js"></script> -->
<!-- <script src="../dist/bundle.js"></script> -->
<!-- <script src="/bundle.js"></script> -->
</head>
<body>
<ul>
<li>这是第1个li</li>
<li>这是第2个li</li>
<li>这是第3个li</li>
<li>这是第4个li</li>
<li>这是第5个li</li>
<li>这是第6个li</li>
<li>这是第7个li</li>
<li>这是第8个li</li>
<li>这是第9个li</li>
</ul>
<div id='box'></div>
</body>
</html>
5.1 loader 的另一种配置方式
带参数项的 loader 还可以通过对象的方式进行配置:
module: {//所有第三方文件配置模块的匹配规则
rules: [//文件后缀的匹配规则
{test:/\/.jpg|png|gif$/,ues:
loader:'url-loader',//通过loader属性指定调用的loader
options:{//通过options属性指定参数项
limit:66300
}
},
]
}
const path = require('path')
// 1.导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html',
})
module.exports = {
mode: 'development',
//打包入口文件的路径
entry: path.join(__dirname, './src/index.js'),
output: {
//输出文件的存放路径
path: path.join(__dirname, './dist'),
// 输出文件的名称
filename: 'bundle.js',
},
// 挂载插件的实例对象
plugins: [htmlPlugin],
devServer: {
open: true,
host: '127.0.0.1',
port: 80
},
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{
test: /\/.jpg|png|gif$/,
use: {
loader: 'url-loader', //通过loader属性指定调用的loader
options: { //通过options属性指定参数项
limit: 66300
}
}
},
],
},
}
6. 打包处理 js 文件中的高级语法
webpack 只能打包处理一部分高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法,需要借 助于 babel-loader 进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码:
class Person{
//通过start关键字,为Person类定义了一个静态属性info
//webpack无法打包处理"静态属性"这个高级语法
static info = 'person info'
}
console.log(Person.info)
6.1 安装 babel-loader 相关的包
运行如下的命令安装对应的依赖包:
npm install babel-loader@8.2.1 @babel/core@7.12.3 @babel/plugin-proposal-class-properties@7.12.1 -D
包的名称及版本号列表如下(红色是包的名称、黑色是包的版本号):
-
babel-loader@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为排除项
// 表示badel-loader 只需要处理开发编写的js文件,不需要处理node-modules下的js文件
exclude: /node-modules/,
use: {
loader: 'babel-loader',
options: {
//参数项
// 声明一个badel插件,此插件用来转化class中的高级语法
plugins: ['@babel/plugin-proposal-class-properties'],
}
}
},
打包发布
1. 为什么要打包发布
项目开发完成之后,使用 webpack 对项目进行打包发布的主要原因有以下两点:
① 开发环境下,打包生成的文件存放于内存中,无法获取到最终打包生成的文件
② 开发环境下,打包生成的文件不会进行代码压缩和性能优化
为了让项目能够在生产环境中高性能的运行,因此需要对项目进行打包发布。
2. 配置 webpack 的打包发布
在 package.json 文件的 scripts 节点下,新增 build 命令如下:
"scripts": {
"dev": "webpack serve",
//项目发布时,运行build命令
"build":"webpack --mode production"
},
–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', //通过loader属性指定调用的loader
options: { //通过options属性指定参数项
limit: 66300,
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],
const path = require('path')
// 1.导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. 创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html',
})
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
new cleanPlugin = CleanWebpackPlugin()
module.exports = {
mode: 'development',
//打包入口文件的路径
entry: path.join(__dirname, './src/index.js'),
output: {
//输出文件的存放路径
path: path.join(__dirname, './dist'),
// 输出文件的名称
filename: 'js/bundle.js',
},
// 挂载插件的实例对象
plugins: [htmlPlugin, cleanPlugin],
devServer: {
open: true,
host: '127.0.0.1',
port: 80
},
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{
test: /\/.jpg|png|gif$/,
use: {
loader: 'url-loader', //通过loader属性指定调用的loader
options: { //通过options属性指定参数项
limit: 66300,
outputPath: 'image'
},
}
},
{
test: /\.js$/,
// exclude为排除项
// 表示badel-loader 只需要处理开发编写的js文件,不需要处理node-modules下的js文件
exclude: /node-modules/,
use: {
loader: 'babel-loader',
options: {
//参数项
// 声明一个badel插件,此插件用来转化class中的高级语法
plugins: ['@babel/plugin-proposal-class-properties'],
}
}
},
],
},
}
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',
//此选项生成的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。实际效果如图所示:
采用此选项后:你应该将你的服务器配置为,不允许普通用户访问 source map 文件!
5. Source Map 的最佳实践
① 开发环境下:
-
建议把 devtool 的值设置为 eval-source-map
-
好处:可以精准定位到具体的错误行
② 生产环境下:
-
建议关闭 Source Map 或将 devtool 的值设置为 nosources-source-map
-
好处:防止源码泄露,提高网站的安全性
vue 基础入门
vue 简介
1. 什么是 vue
官方给出的概念:Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的前端框架。
核心关键词:构建用户 界面、框架
1.1 解读核心关键词:构建用户界面
前端开发者最主要的工作,就是为网站的使用者(又称为:网站的用户)构建出美观、舒适、好用的网页。
编写结构:基于 HTML 超文本标记语言,搭建出网页的内容结构。
美化样式:基础 CSS 样式,美化网页的可视化效果。
处理交互:基于 Javascript 来操作网页中的 DOM 对象,处理用户和网页之间的交互行为。
1.2 构建用户界面的传统方式
在传统的 Web 前端开发中,是基于 jQuery + 模板引擎 的方式来构建用户界面的。
编写结构:基于模板引擎技术,把数据渲染到页面上。
优点:初步解放了前端开发者,从此不用手动拼接字符串来渲染网页结构了。
缺点:1.需要定义大量的模板结构;2.缺少语法高亮和智能提示;3.数据变化时需要重新调 用模板编译的函数,否则页面结构不会更新;
美化样式:基础 CSS 样式,美化网页的可视化效果。
处理交互:基于 jQuery 技术,处理用户和网页之间的交互行为。
优点:屏蔽了 DOM API 之间的兼容性,提高了 DOM 操作的效率和体验。 缺点:当业务复杂时、数据变化频繁时,前端程开发者需要把大量的时间和精力浪费在 DOM 的操作上,而不是核心业务的处理上。
1.3 使用 vue 构建用户界面
使用 vue 构建用户界面,解决了 jQuery + 模板引擎 的诸多痛点,极大的提高了前端开发的效率和体验。
编写结构:基于 vue 中提供的指令,可以方便快捷的渲染页面的结构(乐不思蜀)。
数据驱动视图(只要页面依赖的数据源变化,则页面自动重新渲染)
Ps:指令是 vue 为开发者提供的模板语法,用来辅助开发者渲染页面的结构。
美化样式:基础 CSS 样式,美化网页的可视化效果。
处理交互:基于 vue 中提供的事件绑定,可以轻松处理用户和页面之间的交互行为。
Ps:开发者把工作的重心放在核心业务的实现上
1.4 解读核心关键词:框架
官方给 vue 的定位是前端框架,因为它提供了构建用户界面的一整套解决方案(俗称 vue 全家桶):
- vue(核心库)
- vue-router(路由方案)
- vuex(状态管理方案)
- vue 组件库(快速搭建页面 UI 效果的方案)
以及辅助 vue 项目开发的一系列工具:
- vue-cli(npm 全局包:一键生成工程化的 vue 项目 - 基于 webpack、大而全)
- vite(npm 全局包:一键生成工程化的 vue 项目 - 小而巧)
- vue-devtools(浏览器插件:辅助调试的工具)
- vetur(vscode 插件:提供语法高亮和智能提示)
1.5 总结:什么是 vue
vue 是一套用于构建用户界面的前端框架。
前端框架:
-
构建用户界面的一整套解决方案
-
vue 全家桶:vue + vue-router + vuex + 组件库
-
提供了辅助项目开发的配套工具 vue-cli + vue-devtools
构建用户界面:
- 指令: 用于辅助开发者渲染页面的模板语法
- 事件绑定: 用于处理用户和网页之间的交互行为
- 数据驱动视图: 数据源变化,页面结构自动重新渲染
2. vue 的特性
vue 框架的特性,主要体现在如下两方面:
① 数据驱动视图
② 双向数据绑定
2.1 数据驱动视图
在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。示意图如下:
好处:当页面数据发生变化时,页面会自动重新渲染!
注意:数据驱动视图是单向的数据绑定。
2.2 双向数据绑定
在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源 中。示意图如下:
好处:开发者不再需要手动操作 DOM 元素,来获取表单元素最新的值!
2.3 MVVM
MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。它把每个 HTML 页面都拆分成了如下三个部分:
在 MVVM 概念中:
- View 表示当前页面所渲染的 DOM 结构。
- Model 表示当前页面渲染时所依赖的数据源。
- ViewModel 表示 vue 的实例,它是 MVVM 的核心。
2.4 MVVM 的工作原理
ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。
当数据源发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面的结构
当表单元素的值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中
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 支持等
废弃的旧功能如下:
过滤器、不再支持 o n , on, on,off 和 $once 实例方法等
详细的变更信息,请参考官方文档给出的迁移指南: https://v3.vuejs.org/guide/migration/introduction.html
vue 的基本使用
1. 基本使用步骤
① 导入 vue.js 的 script 脚本文件
② 在页面中声明一个将要被 vue 所控制的 DOM 区域
③ 创建 vm 实例对象(vue 实例对象)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 2.声明要被vue所控制的dom区域 -->
<div id='app'>{{username}}</div>
<!-- 1.导入vue的脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 3.创建vue实例对象 -->
<script>
const vm = new Vue({
// 3.1使用el属性指定vue要控制的区域
el: '#app',
// 3.2数据源
data: {
username: 'zs',
},
})
</script>
</body>
</html>
2. 基本代码与 MVVM 的对应关系
vue 的调试工具
1. 安装 vue-devtools 调试工具
vue 官方提供的 vue-devtools 调试工具,能够方便开发者对 vue 项目进行调试与开发。
Chrome 浏览器在线安装 vue-devtools
vue 2.x 调试工具:
https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd
vue 3.x 调试工具:
https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg
注意:vue2 和 vue3 的浏览器调试工具不能交叉使用!
2. 配置 Chrome 浏览器中的 vue-devtools
点击 Chrome 浏览器右上角的
按钮,选择更多工具 -> 扩展程序 -> Vue.js devtools 详细信息,并勾选如下 的两个选项:
注意:修改完配置项,须重启浏览器才能生效!
3. 使用 vue-devtools 调试 vue 页面
在浏览器中访问一个使用了 vue 的页面,打开浏览器的开发者工具,切换到 Vue 面板,即可使用 vue-devtools 调试当前的页面。
vue 的指令与过滤器
1. 指令的概念
指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。
vue 中的指令按照不同的用途可以分为如下 6 大类:
① 内容渲染指令
② 属性绑定指令
③ 事件绑定指令
④ 双向绑定指令
⑤ 条件渲染指令
⑥ 列表渲染指令
注意:指令是 vue 开发中最基础、最常用、最简单的知识点。
1.1 内容渲染指令
内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下 3 个:
- v-text
- {{ }}
- v-html
v-text
用法示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='app'>
<!-- 把username对应的值,渲染到第一个p标签中 -->
<p v-text="username"></p>
<!-- 把gender对应的值,渲染到第二个p标签中 -->
<!-- 注意:第二个p标签中,默认的文本‘性别’会被值覆盖掉 -->
<p v-tetx="gender">性别</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: 'app',
data: {
username: 'zs',
gender: '男'
}
})
</script>
</body>
</html>
注意:v-text 指令会覆盖元素内默认的值。
{{ }} 语法
vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种 {{ }} 语法的专业名称是插值表达式(英文名为:Mustache)。
<!-- 使用{{}} 插值表达式,将对应的值渲染到元素的内容节点中 -->
<!-- 同时保留元素自身的默认值 -->
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容。
v-html
v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素, 则需要用到 v-html 这个指令:
<--desc: '<i>abc<i>'-->
<P v-html="desc"></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:placcholder="inputvalue"/>
<br/>
<!--使用v-bind指令,为img的src动态绑定属性值-->
<img v-bind:src="imgSrc" alt=""/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='app'>
<input type="text" v-bind:placeholder="inputValue">
<hr>
<img v-bind:src="imgSrc">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
inputValue: '请输入内容',
//图片的src地址
imgSrc: './image/1.png',
},
})
</script>
</body>
</html>
属性绑定指令的简写形式
由于 v-bind 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的 : )。
<!--假设有如下的data数据:
data:{
inputValue:'请输入内容',
imgSrc:'https://cn.vuejs.org/images/logo.png'
}
-->
<!--使用v-bind指令,为input的placeholder动态绑定属性值-->
<input type="text" :placcholder="inputvalue"/>
<br/>
<!--使用v-bind指令,为img的src动态绑定属性值-->
<img :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
}
},
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button v-on:click="addCount">+1</button>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
//v-on 绑定的事件处理函数,需要声明在methods节点中
addCount() {
//事件处理函数的名字
//this表示当前new出来的vm实例对象
//通过this可以访问到data中的数据
this.count += 1
}
},
})
</script>
</body>
</html>
事件绑定的简写形式
由于 v-on 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的 @ )。
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button @click="count+=1">+1</button>
</div>
事件对象 event
在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件对象 event。同理,在 v-on 指令(简 写为 @ )所绑定的事件处理函数中,同样可以接收到事件对象 event,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button @click="addCount">+1</button>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
//v-on 绑定的事件处理函数,需要声明在methods节点中
addCount(e) {
const nowBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
this.count += 1
}
},
})
</script>
</body>
</html>
绑定事件并传参
在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参,示例代码如下:
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button @click="addCount(2)">+2</button>
</div>
methods: {
//在形参处用step接收传递过来的参数值
addCount(step) {
this.count += step
}
},
$event
e v e n t 是 v u e 提供的特殊变量,用来表示原生的事件参数对象 e v e n t 。 event 是 vue 提供的特殊变量,用来表示原生的事件参数对象 event。 event是vue提供的特殊变量,用来表示原生的事件参数对象event。event 可以解决事件参数对象 event 被覆盖的问题。示例用法如下:
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button @click="addCount(2,$event)">+2</button>
methods: {
//v-on 绑定的事件处理函数,需要声明在methods节点中
addCount(step, e) {
const nowBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
this.count += step
}
},
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!--语法格式为v-on:事件名称="事件处理函数的名称"-->
<button @click="addCount(2,$event)">+2</button>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
//v-on 绑定的事件处理函数,需要声明在methods节点中
addCount(step, e) {
const nowBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
this.count += step
}
},
})
</script>
</body>
</html>
事件修饰符
在事件处理函数中调用 preventDefault() 或 stopPropagation() 是非常常见的需求。因此,vue 提供了事件 修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:
事件修饰符 | 说明 |
---|---|
.prevent | 阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等) |
.stop | 阻止事件冒泡 |
.capture | 以捕获模式触发当前的事件处理函数 |
.once | 绑定的事件只触发1次 |
.self | 只有在 event.target 是当前元素自身时触发事件处理函数 |
语法格式如下:
<!--触发click点击事件,阻止a链接的默认行为-->
<a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.inner {
line-height: 100px;
background-color: aliceblue;
font-size: 13px;
text-align: center;
}
.outer {
background-color: bisque;
padding: 50px;
font-size: 13px;
}
.box {
background-color: coral;
padding: 50px;
}
</style>
</head>
<body>
<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">
<h4>① .prevent 事件修饰符的应用场景</h4>
<a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>
<hr />
<h4>② .stop 事件修饰符的应用场景</h4>
<div class="outer" @click="onOuterClick">
外层的 div
<div class="inner" @click.stop="onInnerClick">内部的 div</div>
</div>
<hr />
<h4>③ .capture 事件修饰符的应用场景</h4>
<div class="outer" @click.capture="onOuterClick">
外层的 div
<div class="inner" @click="onInnerClick">内部的 div</div>
</div>
<hr />
<h4>④ .once 事件修饰符的应用场景</h4>
<div class="inner" @click.once="onInnerClick">内部的 div</div>
<hr />
<h4>⑤ .self 事件修饰符的应用场景</h4>
<div class="box" @click="onBoxClick">
最外层的 box
<div class="outer" @click.self="onOuterClick">
中间的 div
<div class="inner" @click="onInnerClick">内部的 div</div>
</div>
</div>
<hr />
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
// 声明处理函数的节点
methods: {
// 超链接的点击事件处理函数
onLinkClick() {
alert('ok')
},
// 点击了外层的 div
onOuterClick() {
console.log('触发了 outer 的 click 事件处理函数')
},
// 点击了内部的 div
onInnerClick() {
console.log('触发了 inner 的 click 事件处理函数')
},
onBoxClick() {
console.log('触发了 box 的 click 事件处理函数')
}
},
})
</script>
</body>
</html>
按键修饰符
在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:
<!--只有在‘key”是“Enter’时调用“vm. submit()'' -->
<input @keyup.enter="submit">
<!--只有在'key'是'ESc'时调用'vm.clearInput()' -->
<input @keyup.esc="clearInput">
1.4 双向绑定指令
vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<p>用户名是:{{username}}</p>
<input type="text" v-model="username" />
<hr />
<p>选中的省份是:{{province}}</p>
<select v-model="province">
<option value="">请选择</option>
<option value="1">北京</option>
<option value="2">河北</option>
<option value="3">黑龙江</option>
</select>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 姓名
username: 'zs',
// 省份
province: '1',
},
})
</script>
</body>
</html>
注意:v-model 指令只能配合表单元素一起使用!
v-model 指令的修饰符
为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:
修饰符 | 作用 | 示例 |
---|---|---|
.number | 自动将用户的输入值转为数值类型 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='app'>
姓名:<input type="text" v-model.trim="username">
</hr>
年龄:<input type="text" v-model.number="age">
</hr>
地址:<input type="text" v-model.lazt="address">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
address: '北京市',
age: '12',
username: "zs"
},
})
</script>
</body>
</html>
1.5 条件渲染指令
条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:
- v-if
- v-show
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="flag =!flag">Toggle Flag</button>
<p v-if="flag">请求成功---被v-if控制</p>
<p v-show="flag">请求成功---被v-show控制</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// flag用来控制元素的显示与隐藏
// 值为 true 时显示元素
// 值为false时隐藏元素
flag: true,
}
})
</script>
</body>
</html>
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 指令一起使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p v-if="num>0.5">随机数 > 0.5</p>
<p v-else>随机数 ≤ 0 .5</p>
<hr>
<p>优秀</p>
<p>良好</p>
<p>一般</p>
<p>差</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 生成1以内的随机数
num: Math.random(),
// 类型
type: 'A'
},
})
</script>
</body>
</html>
v-else-if
v-else-if 指令,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else-if="type === 'C'">一般</p>
<p v-else>差</p>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p v-if="num>0.5">随机数 > 0.5</p>
<p v-else>随机数 ≤ 0 .5</p>
<hr>
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else-if="type === 'C'">一般</p>
<p v-else>差</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 生成1以内的随机数
num: Math.random(),
// 类型
type: 'A'
},
})
</script>
</body>
</html>
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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="user in list">姓名是:{{user.name}}</li>
</ul>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
list: [{
id: 1,
name: 'zs'
}, {
id: 2,
name: 'ls'
}]
}
})
</script>
</body>
</html>
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="(user,i) in list">索引是:{{i+1}},姓名是:{{user.name}}</li>
</ul>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
list: [{
id: 1,
name: 'zs'
}, {
id: 2,
name: 'ls'
}]
}
})
</script>
</body>
</html>
使用 key 维护列表的状态
当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种 默认的性能优化策略,会导致有状态的列表无法被正确更新。
为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲 染的性能。此时,需要为每项提供一个唯一的 key 属性:
<!-- 用户列表区域 -->
<ul>
<!-- 加key属性的好吃: -->
<!-- 1.正确维护列表的状去-->
<!-- 2.复用现有的DOM元素,提升渲染的性能 -->
<li v-for="(user, index) 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 vuc.js ',
info: 'title info'
},
filters: {
//在 filters节点下定义"过滤器"
capitalize(str) {
//把首字母转为大写的过滤器
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
})
过滤器
过滤器(Filters)是 vue 为开发者提供的功能,常用于文本的格式化。过滤器可以用在两个地方:插值表达式 和 v-bind 属性绑定。
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:
2.2 私有过滤器和全局过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p :title="info">{{message | capitalize}}</p>
</div>
<div id="app2">
<p>{{abc|capitalize}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello',
info: 'title info',
},
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
})
</script>
<script>
const vm2 = new Vue({
el: '#app2',
data: {
abc: 'abc',
},
})
</script>
</body>
</html>
如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:
//全局过滤器–独立于每个vm实例之外
// Vue.filter()方法接收两个参数
//第1个参数,是全局过滤器的""名字”
//第2个参数,是全局过滤器的""处理函数”
Vue.filter( ' capitalize", (str)=> {
return str.charAt(0).toUpperCase() + str.slice(1) + '~~'
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p :title="info">{{message | capitalize}}</p>
</div>
<div id="app2">
<p>{{abc|capitalize}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1) + '~~'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello',
info: 'title info',
},
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
})
</script>
<script>
const vm2 = new Vue({
el: '#app2',
data: {
abc: 'abc',
},
})
</script>
</body>
</html>
2.3 连续调用多个过滤器
过滤器可以串联地进行调用,例如:
<!--把message 的值。交给 filterA进行处理-->
<!--把 filtcrA 处理的结果,再交给filterB进行处理-->
<!--最终把 filterB处理的结果,作为最终的值渲染到页面上 -->
{{message | filterA | filterB }}
2.3 连续调用多个过滤器
示例代码如下:
<!--串联调用多个过滤器-->
<p>{{text | capitalize | maxLength}}</p>
// 全局过滤器–首字母大写
Vue.filter( ' capitalize", (str) => {
return str.charAt(O).touppercase() + str.slice(1) + '~-')
//全局过滤器–控制文本的最大长度
Vue.filter( 'maxLength". (str)=>{
if (str.length <= 10) return str
return str.slice(o,10)+ '...'
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p :title="info | capitalize">{{message | capitalize | maxLength}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
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(0, 10) + '...'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue javascript',
info: 'title info',
},
})
</script>
</body>
</html>
2.4 过滤器传参
过滤器的本质是 JavaScript 函数,因此可以接收参数,格式如下:
<!-- arg1和arg2是传递给filterA 的参数-->
<p>{{message | filterAarg1, arg2)}}</p>
//过滤器处理函数的形参列表中:
//第一个参数。永远都是管道符前面待处理的值
//从第二个参数开始,才是调用过滤器时传递过来的arg1 和arg2参数Vue.filter( "filterA',(msg.arg1,arg2)=> {
//过滤器的代码逻辑...
})
2.4 过滤器传参
示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p :title="info | capitalize">{{message | capitalize | maxLength(5)}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
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(0, len) + '...'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue javascript',
info: 'title info',
},
})
</script>
</body>
</html>
2.5 过滤器的兼容性
过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x 的版本中剔除了过滤器相关的功能。
在企业级项目开发中:
如果使用的是 2.x 版本的 vue,则依然可以使用过滤器相关的功能
如果项目已经升级到了 3.x 版本的 vue,官方建议使用计算属性或方法代替被剔除的过滤器功能
具体的迁移指南,请参考 vue 3.x 的官方文档给出的说明: https://v3.vuejs.org/guide/migration/filters.html#migration-strategy
品牌列表案例
1. 案例效果
2. 用到的知识点
bootstrap 4.x 相关的知识点:
卡片(Card)、表单相关(Forms)、按钮(Buttons)、表格(Tables)
vue 指令与过滤器相关的知识点: 插值表达式、属性绑定、事件绑定、双向数据绑定、修饰符、条件渲染、列表渲染、全局过滤器
3.整体实现步骤
① 创建基本的 vue 实例
② 基于 vue 渲染表格数据
③ 实现添加品牌的功能
④ 实现删除品牌的功能
⑤ 实现修改品牌状态的功能
3.1 创建基本的 vue 实例
步骤1:导入 vue 的 js 文件:
步骤2:在 标签中声明 el 区域:
步骤3:创建 vue 实例对象:
3.2 基于 vue 渲染表格数据
步骤1:使用 v-for 指令循环渲染表格的数据:
步骤2:将品牌的状态渲染为 Switch 开关效果:
Switch 开关效果的官方文档地址: https://v4.bootcss.com/docs/components/forms/#switches
步骤3:使用全局过滤器对时间进行格式化:
使用全局过滤器对时间进行格式化:
使用全局过滤器对时间进行格式化:
3.3 添加品牌
步骤1:阻止表单的默认提交行为:
步骤2:为 input 输入框进行 v-model 双向数据绑定: 注意:需要在 data 数据中声明 brandname 属性字段。
步骤3:为“添加品牌”的 button 按钮绑定 click 事件处理函数:
步骤4:在 data 中声明 nextId 属性(用来记录下一个可用的 id 值),并在 methods 中声明 addNewBrand 事件处理函数:
步骤5:监听 input 输入框的 keyup 事件,通过 .esc 按键修饰符快速清空文本框中的内容:
3.4 删除品牌
步骤1:为删除的 a 链接绑定 click 点击事件处理函数,并阻止其默认行为:
步骤2:在 methods 节点中声明 removeBrand 事件处理函数如下:
组件基础
单页面应用程序
1. 什么是单页面应用程序
单页面应用程序(英文名:Single Page Application)简称 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 install vite-app 项目名称
cd 项目名称
npm install
npm run dev
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.1 在 App.vue 中编写模板结构
清空 App.vue 的默认内容,并书写如下的模板结构:
<template>
<h1>这是app.vue根组件</h1>
<h3>abc</h3>
</template>
3.2 在 index.html 中预留 el 区域
打开 index.html 页面,确认预留了 el 区域:
<body>
<!-- id 为app的div元素,就是将来vue要控制的区域 -->
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
3.3 在 main.js 中进行渲染
按照 vue 3.x 的标准用法,把 App.vue 中的模板内容渲染到 index.html 页面的 el 区域中:
// 1.从 vue中按需导入 createApp函数
import { createApp } from 'vue'
// 2.导入待渲染的App 组件
import App from './App.vue'
//3.调用createApp()函数,返回值是"单页面应用程序的实例",
// 用常量 spa_app进行接收,
const app = createApp(App)
//4.调用 mount方法,把App 组件的模板结构,渲染到指定的el区域中
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 规定:每个组件对应的模板结构,需要定义到 < template> 节点中。
<template>
<!-- 当前组件的DOM结构,需要定义到template标签的内部-->
</template>
注意:< template> 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素。
2.1 在 template 中使用指令
在组件的 < template> 节点中,支持使用前面所学的指令语法,来辅助开发者渲染当前组件的 DOM 结构。代码示例如下:
<template>
<h1>这是 App 根组件</h1>
<!--使用i该插值表达式-->
<p>生成一个随机数字: {{(Math.random()*10).toFixed(2)}}</p>
<!--使用v-bind 属性绑定-->
<p :title="new Date().toLocaleTimeString()">vue.js</p>
<!--属性v-on事件绑定-->
<button eclick="showInfo">按钮</button>
</template>
2.2 在 template 中定义根节点
在 vue 2.x 的版本中,< template> 节点内的 DOM 结构仅支持单个根节点:
<template>
<div>
<h1>这是app.vue根组件</h1>
<h3>abc</h3>
</div>
</template>
但是,在 vue 3.x 的版本中,< template> 中支持定义多个根节点:
<template>
<h1>这是app.vue根组件</h1>
<h3>abc</h3>
</template>
3. 组件的 script 节点
vue 规定:组件内的 < script> 节点是可选的,开发者可以在 < script> 节点中封装组件的 JavaScript 业务逻辑。< script > 节点的基本结构如下:
<script>
// 今后,组件相关的data数据、methods方法等,
// 都需要定义到export default所导出的对象中。
export default {}
</script>
3.1 script 中的 name 节点
可以通过 name 节点为当前组件定义一个名称:
<script>
// 今后,组件相关的data数据、methods方法等,
// 都需要定义到export default所导出的对象中。
export default {
// name属性指向的是当前组件的名称《建议:每个单词的首字母大写>
name:'MyApp'
}
</script>
在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件
3.2 script 中的 data 节点
vue 组件渲染期间需要用到的数据,可以定义在 data 节点中:
<script>
// 今后,组件相关的data数据、methods方法等,
// 都需要定义到export default所导出的对象中。
export default {
// name属性指向的是当前组件的名称《建议:每个单词的首字母大写>
name:'MyApp',
data() {
return {
username:'zs',
}
}
}
</script>
组件中的 data 必须是函数
vue 规定:组件中的 data 必须是一个函数,不能直接指向一个数据对象。因此在组件中定义 data 数据节点 时,下面的方式是错误的:
data:{
//组件中,不能直接让data 指向一个数据对象(会报错)
count:0
}
3.3 script 中的 methods 节点
组件中的事件处理函数,必须定义到 methods 节点中,示例代码如下
<template>
<h1>这是app.vue根组件</h1>
<h3>abc ---{{username}}</h3>
<p>count值是--{{count}}</p>
<button @click="addCount">+1</button>
</template>
<script>
// 今后,组件相关的data数据、methods方法等,
// 都需要定义到export default所导出的对象中。
export default {
// name属性指向的是当前组件的名称《建议:每个单词的首字母大写>
name:'MyApp',
data() {
return {
count:0,
}
},
methods: {
addCount() {
this.count++
},
},
}
</script>
4. 组件的 style 节点
vue 规定:组件内的 < style> 节点是可选的,开发者可以在 < style> 节点中编写样式美化当前组件的 UI 结构。< script > 节点的基本结构如下:
<style lang="css">
h1{
color:red
}
</style>
其中 < style> 标签上的 lang=“css” 属性是可选的,它表示所使用的样式语言。默认只支持普通的 css 语法,可选值还有 less、scss 等。
4.1 让 style 中支持 less 语法
如果希望使用 less 语法编写组件的 style 样式,可以按照如下两个步骤进行配置:
① 运行 npm install less -D 命令安装依赖包,从而提供 less 语法的编译支持 ② 在 < style> 标签上添加 lang=“less” 属性,即可使用 less 语法编写组件的样式
<template>
<h1>这是<i>App.vue</i>根组件</h1>
<h3>abc ---{{username}}</h3>
<p>count值是--{{count}}</p>
<button @click="addCount">+1</button>
</template>
<style lang="less">
h1{
color:red;
i {
color: blue;
}
}
</style>
<script>
// 今后,组件相关的data数据、methods方法等,
// 都需要定义到export default所导出的对象中。
export default {
// name属性指向的是当前组件的名称《建议:每个单词的首字母大写>
name:'MyApp',
data() {
return {
count:0,
}
},
methods: {
addCount() {
this.count++
},
},
}
</script>
组件的基本使用
1. 组件的注册
组件之间可以进行相互的引用,例如:
vue 中组件的引用原则:先注册后使用。
1.1 注册组件的两种方式
vue 中注册组件的方式分为“全局注册”和“局部注册”两种,其中:
- 被全局注册的组件,可以在全局任何一个组件内使用
- 被局部注册的组件,只能在当前注册的范围内使用
1.2 全局注册组件
// 1.从 vue中按需导入 createApp函数
import { createApp } from 'vue'
// 2.导入待渲染的App 组件
import App from './App.vue'
//1.导入需要被全局注册的组件
import Swiper from './components/01-globalReg/Swiper.vue'
import Test from './components/01-globalReg/Test.vue'
//3.调用createApp()函数,返回值是"单页面应用程序的实例"
const app = createApp(App)
//2.调用app . component()方法全局注册组件
app.component('my-swiper', Swiper)
app.component('my-test', Test)
//4.调用 mount方法,把App 组件的模板结构,渲染到指定的el区域中
app.mount('#app')
1.3 使用全局注册组件
使用 app.component() 方法注册的全局组件,直接以标签的形式进行使用即可,例如:
<template>
<h1>这是<i>App.vue</i>根组件</h1>
<h3>abc ---{{username}}</h3>
<p>count值是--{{count}}</p>
<button @click="addCount">+1</button>
<my-swiper></my-swiper>
<my-test></my-test>
</template>
1.4 局部注册组件
<template>
<h1>这是<i>App.vue</i>根组件</h1>
<h3>abc ---{{username}}</h3>
<p>count值是--{{count}}</p>
<button @click="addCount">+1</button>
<my-swiper></my-swiper>
<my-search></my-search>
</template>
<script>
import Search from './components/02-privateReg/Search.vue'
export default{
components : {
'my-search': Search
}
}
</script>
1.5 全局注册和局部注册的区别
被全局注册的组件,可以在全局任何一个组件内使用
被局部注册的组件,只能在当前注册的范围内使用
应用场景:
如果某些组件在开发期间的使用频率很高,推荐进行全局注册;
如果某些组件只在特定的情况下会被用到,推荐进行局部注册。
1.6 组件注册时名称的大小写
在进行组件的注册时,定义组件注册名称的方式有两种:
① 使用 kebab-case 命名法(俗称短横线命名法,例如 my-swiper 和 my-search)
② 使用 PascalCase 命名法(俗称帕斯卡命名法或大驼峰命名法,例如 MySwiper 和 MySearch)
短横线命名法的特点:
- 必须严格按照短横线名称进行使用
帕斯卡命名法的特点:
- 既可以严格按照帕斯卡名称进行使用,又可以转化为短横线名称进行使用
注意:在实际开发中,推荐使用帕斯卡命名法为组件注册名称,因为它的适用性更强。
1.7 通过 name 属性注册组件
在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的 name 属性作为注册后组件的名称, 示例代码如下:
<template>
<h3>Swiper轮播图</h3>
</template>
<script>
export default {
name:'MySwiper'
}
</script>
import Swiper from './components/01-globalReg/Swiper.vue'
app.component(Swiper,name,Swiper)
//等同于app.component('my-swiper', Swiper)
2. 组件之间的样式冲突问题
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。导致组件 之间样式冲突的根本原因是:
① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
2.1 思考:如何解决组件样式冲突的问题
为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:
<template>
<div class="container" data-v-001>
<h3 data-v-001>Swiper轮播图</h3>
</div>
</template>
<script>
export default {
name:'MySwiper'
}
</script>
<style>
.container[data-v-001] {
border:1px solid red;
}
</style>
<template>
<div data-v-002>
<h1 data-v-002>这是App.vue根组件</h1>
<p data-v-002> app中的p标签</p>
<p data-v-002>app中的p标签</p>
<hr data-v-002>
<my-list data-v-002></my-list>
</div>
</template>
<script>
import MyList from './list.vue'
export default {
name:'MyApp',
components: {
MyList
}
}
</script>
<style lang="less">
p[data-v-002] {
color: aqua;
}
</style>
<template>
<div data-v-003>
<h3 data-v-003>这是list.vue组件</h3>
<p data-v-003>这是list.vue中的p标签</p>
<p data-v-003>这是list.vue中的p标签</p>
</div>
</template>
<script>
export default {
name:'MyList',
}
</script>
2.2 style 节点的 scoped 属性
为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题:
<template>
<div >
<h3>这是list.vue组件</h3>
<p>这是list.vue中的p标签</p>
<p>这是list.vue中的p标签</p>
</div>
</template>
<script>
export default {
name:'MyList',
}
</script>
<style lang="less" scoped></style>>
2.3 /deep/ 样式穿透
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样 式对子组件生效,可以使用 /deep/ 深度选择器。
<style lang="less" scoped>
p {
color: aqua;
}
/deep/.title {
color:aquamarine;
// 加上 /deep/时,生成的选择器格式为[data-v-052242de] .title
}
注意:/deep/是 vue2.x中实现样式穿透的方案。在vue3.x中推荐使用:deep()替代/deep/。
3. 组件的 props
为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:
组件的 DOM 结构、Style 样式要尽量复用
组件中要展示的数据,尽量由组件的使用者提供
为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。
3.1 什么是组件的 props
props 是组件的自定义属性,组件的使用者可以通过 props 把数据传递到子组件内部,供子组件内部进行使 用。代码示例如下:
<!--通过自定义props,把文章的标题和作者,传递到 my-article 组件中 -->
<my-article title="面朝大海,春暖花开 author="海子"></my-article>
props 的作用:父组件通过 props 向子组件传递要展示的数据。
props 的好处:提高了组件的复用性。
//App.vue
<template>
<div>
<h1>这是App.vue根组件</h1>
<hr>
<my-article title="面朝大海,春暖花开" author="海子"></my-article>
</div>
</template>
<script>
import MyArticle from './Article.vue'
export default {
name:'MyApp',
components: {
MyArticle
}
}
</script>
//----------Article.vue
<template>
<div>
<h1>标题:{{title}}</h1>
<h3>作者:{{author}}</h3>
</div>
</template>
<script>
export default {
name:'MyArticle',
// 外界可以传递指定的数据到当前的组件中
props:['title','author']
}
</script>
3.2 在组件中声明 props
在封装 vue 组件时,可以把动态的数据项声明为 props 自定义属性。自定义属性可以在当前组件的模板结构 中被直接使用。示例代码如下:
<!-- my-article 组件的定义如下:-->
<tcmplate>
<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>
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>
<template>
<div>
<h1>这是App.vue根组件</h1>
<hr>
<my-article :title="info.title" :author="'post by' +info.author"></my-article>
</div>
</template>
<script>
import MyArticle from './Article.vue'
export default {
name:'MyApp',
data() {
return {
info:{
title:'abc',
author:'123',
}
}
},
components: {
MyArticle
}
}
</script>
3.5 props 的大小写命名
组件中如果使用“camelCase (驼峰命名法)”声明了 props 属性的名称,则有两种方式为其绑定属性的值:
<template>
<p>发布时问:iipubTime}}</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() {
rcturn { isItalic: true }
.thin { //字体变细
font-weight: 200;
}
.italic { //倾斜字体
font-style: italic;
}
<template>
<div>
<h3 class="thin" :class="isItalic ? 'italic' : '' "> MyStyle组件</h3>
<button @click="isItalic = !isItalic">Toggle Itali</button>
</div>
</template>
<script>
export default {
name:'MyStyle',
data() {
return {
isItalic:true,
}
}
}
</script>
<style lang="less">
// 字体粗细
.thin {
font-weight:200 ;
}
.italic {
// 倾斜字体
font-style: italic;
}
</style>
4.2 以数组语法绑定 HTML 的 class
如果元素需要动态绑定多个 class 的类名,此时可以使用数组的语法格式:
<h3 class="thin" :class="[isItalic ? 'italic' : "",isDelete ? 'delete' : ""]">
MyDcep 组件
</h3>
<button eclick="isItalic-!isItalic">Toggle Italic</button><button eclickm"isDelete-!isDelete">Toggle Delete</button>
data(){
return{
isItalic:true,
isDelete:false,
}
}
<template>
<div>
<!-- <h3 class="thin" :class="isItalic ? 'italic' : '' "> MyStyle组件</h3> -->
<h3 class="thin" :class="[isItalic ? 'italic' : '',isDelete ? 'delete' : '']"> MyStyle组件</h3>
<button @click="isItalic = !isItalic">Toggle Itali</button>
<br>
<button @click="isDelete = !isDelete">Toggle delete</button>
</div>
</template>
<script>
export default {
name:'MyStyle',
data() {
return {
isItalic:false,
isDelete:false
}
}
}
</script>
<style lang="less">
// 字体粗细
.thin {
font-weight:200 ;
}
.italic {
// 倾斜字体
font-style: italic;
}
.delete {
text-decoration: line-through;
}
</style>
4.3 以对象语法绑定 HTML 的 class
使用数组语法动态绑定 class 会导致模板结构臃肿的问题。此时可以使用对象语法进行简化:
<h3 class="thin" :class="[isItalic ? 'italic' : '',isDelete ? 'delete' : '']"> MyStyle组件</h3>
<button @click="isItalic = !isItalic">Toggle Itali</button>
<br>
<button @click="isDelete = !isDelete">Toggle delete</button>
data() {
return {
classObj: {
//对象中,属性名是 class类名,值是布尔值
italic:false,
delete:false,
}
}
}
<template>
<div>
<!-- <h3 class="thin" :class="isItalic ? 'italic' : '' "> MyStyle组件</h3> -->
<!-- <h3 class="thin" :class="[isItalic ? 'italic' : '',isDelete ? 'delete' : '']"> MyStyle组件</h3>
<button @click="isItalic = !isItalic">Toggle Itali</button>
<br>
<button @click="isDelete = !isDelete">Toggle delete</button> -->
<h3 class="thin" :class="classObj"> MyStyle组件</h3>
<button @click="classObj.italic = !classObj.italic">Toggle Itali</button>
<br>
<button @click="classObj.delete = !classObj.delete">Toggle delete</button>
</div>
</template>
<script>
export default {
name:'MyStyle',
data() {
return {
isItalic:false,
isDelete:false,
classObj:{
//对象中,属性名是 class类名,值是布尔值
italic:false,
delete:false,
},
}
}
}
</script>
<style lang="less">
// 字体粗细
.thin {
font-weight:200 ;
}
.italic {
// 倾斜字体
font-style: italic;
}
.delete {
text-decoration: line-through;
}
</style>
4.4 以对象语法绑定内联的 style
:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼 峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:
<div :style="{color: active,fontSize: fsize + 'px",'background-color ': bgcolor)">
vue组件
</div>
<button @click="fsizc += 1">字号 + 1</button>
<button @click="fsize -= 1">字号 - 1</button>
data(){
return {
active: 'red',
fsize: 30,
bgcolor: "pink" ,
}
}
<template>
<div>
<!-- <h3 class="thin" :class="isItalic ? 'italic' : '' "> MyStyle组件</h3> -->
<!-- <h3 class="thin" :class="[isItalic ? 'italic' : '',isDelete ? 'delete' : '']"> MyStyle组件</h3>
<button @click="isItalic = !isItalic">Toggle Itali</button>
<br>
<button @click="isDelete = !isDelete">Toggle delete</button> -->
<h3 class="thin" :class="classObj"> MyStyle组件</h3>
<button @click="classObj.italic = !classObj.italic">Toggle Itali</button>
<br>
<button @click="classObj.delete = !classObj.delete">Toggle delete</button>
<hr>
<!-- font-szie要写成fontSzie -->
<div :style="{color:active, fontSize:fsize+'px','background-color':bgcolor}">vue根组件</div>
<button @click="fsize+=1">字号 + 1</button>
<button @click="fsize-=1">字号 - 1</button>
</div>
</template>
<script>
export default {
name:'MyStyle',
data() {
return {
isItalic:false,
isDelete:false,
classObj:{
//对象中,属性名是 class类名,值是布尔值
italic:false,
delete:false,
},
//高亮时的文本颜色
active: 'red',
fsize: 30,
bgcolor: "pink" ,
}
}
}
</script>
<style lang="less">
// 字体粗细
.thin {
font-weight:200 ;
}
.italic {
// 倾斜字体
font-style: italic;
}
.delete {
text-decoration: line-through;
}
</style>
封装组件的案例
1. 案例效果 封装要求:
① 允许用户自定义 title 标题
② 允许用户自定义 bgcolor 背景色
③ 允许用户自定义 color 文本颜色
④ MyHeader 组件需要在页面顶部进行 fixed 固定定位,且 z-index 等于 999
使用示例如下:
2. 用到的知识点
组件的封装与注册
props
样式绑定
3. 整体实现步骤
创建 MyHeader 组件
渲染 MyHeader 组件的基本结构
在 App 组件中注册并使用 MyHeader 组件
通过 props 为组件传递数据
props 验证
1. 什么是 props 验证
指的是:在封装组件时对外界传递过来的 props 数据进行合法性的校验,从而防止数据不合法的问题。
使用数组类型的 props 节点的缺点:无法为每个 prop 指定具体的数据类型。
2. 对象类型的 props 节点
使用对象类型的 props 节点,可以对每个 prop 进行数据类型的校验,示意图如下:
//app.vue
<template>
<div>
<h1>App组件</h1>
</div>
<hr>
<!-- <my-count count="abc" :state="3"></my-count> -->
<my-count count=12 :state="true"></my-count>
</template>
<script>
import MyCount from './count.vue'
export default {
name: 'MyApp',
components: {
MyCount
}
}
</script>
//count.vue
<template>
<div>
<p>数量:{{count}}</p>
<p>状态:{{state}}</p>
</div>
</template>
<script>
export default {
name:'MyCount',
// props:['count','state']
props: {
count:Number,
state:Boolean
}
}
</script>
<style lang="less" scoped></style>
3. props 验证
对象类型的 props 节点提供了多种数据验证方案,例如:
① 基础的类型检查
② 多个可能的类型
③ 必填项校验
④ 属性默认值
⑤ 自定义验证函数
3.1 基础的类型检查
可以直接为组件的 prop 属性指定基础的校验类型,从而防止组件的使用者为其绑定错误类型的数据:
export default {
props: { //支持的8种基础关型
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字符串类型
erquired:true //当前属性的值是必填项,如果使用者没指定 propB属性的值,则在终端进行警告提示
},
},
}
<template>
<div>
<p>数量:{{count}}</p>
<p>状态:{{state}}</p>
</div>
</template>
<script>
export default {
name:'MyCount',
// props:['count','state']
props: {
count:{
type:[String,Number],
required:true
},
state:Boolean,
info:[String,Number]
}
}
</script>
<style lang="less" scoped></style>
3.4 属性默认值
在封装组件时,可以为某个 prop 属性指定默认值。示例代码如下:
export default {
props: {
// 通过“配置对象"的形式,来定义propB属性的“验证规则”
propC:{
type:Number,
default:100 //如果使用者没有指定propC的值,则propC属性的默认值为100
},
},
}
3.5 自定义验证函数
在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制:
export default {
props: {
// 通过“配置对象"的形式,来定义propB属性的“验证规则”
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.nmuber='count'/>
<p>{{count}} 乘以2的值为:{{plus}}</p>
data() {
return {count:1}
},
computed: {
plus() {
//计算属性。监听 data中 count值的变化,自动计算出count * 2 之后的新值
return this.count * 2
},
}
注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有 return 返回值!
<template>
<div>
<input type="text" v-model.number="count">
<p>{{count}} 乘以2的值为:{{plus}}</p>
</div>
</template>
<script>
export default {
name: 'MyCount',
data() {
return {
count:1,
}
},
computed:{
plus() {
return this.count * 2
}
}
}
</script>
3. 计算属性的使用注意点
① 计算属性必须定义在 computed 节点中
② 计算属性必须是一个 function 函数
③ 计算属性必须有返回值
④ 计算属性必须当做普通属性使用
4. 计算属性 vs 方法
相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行运算。因此 计算属性的性能更好:
5. 计算属性案例
案例需求,使用计算属性动态计算:
① 已勾选的商品总个数
② 已勾选的商品总价
③ 结算按钮的禁用状态
自定义事件
1. 什么是自定义事件
在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件。
2. 自定义事件的 3 个使用步骤
在封装组件时:
① 声明自定义事件
② 触发自定义事件
在使用组件时:
③ 监听自定义事件
2.1 声明自定义事件
开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明,示例代码如下:
<template>
<h3>Counter组件</h3>
<button> +1 </button>
</template>
<script>
export default {
//my-counter组件的自定义事件,必须事先声明到 emits节点中
emits:['change']
}
</script>
2.2 触发自定义事件
在 emits 节点下声明的自定义事件,可以通过 this.$emit(‘自定义事件的名称’) 方法进行触发,示例代码如下:
<template>
<h3>Counter组件</h3>
<button @click="onBtnClick"> +1 </button>
</template>
<script>
export default {
//my-counter组件的自定义事件,必须事先声明到 emits节点中
emits:['change'],
methods: {
onBtnClick() {
this.$emit('change')
//当点击+1按钮时,调用this.$cmitO)方法,触发自定义的changc事件
}
}
}
</script>
2.3 监听自定义事件
在使用自定义的组件时,可以通过 v-on 的形式监听自定义事件。示例代码如下:
<my-counter @change='getCount'>
methods: {
getCount() {
console.log('监听到了count值的变化')
}
}
//App.vue
<template>
<div>
<h1>app根组件</h1>
</div>
<hr>
<my-counter @count-change="getCount"></my-counter>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name:'MyApp',
methods:{
getCount() {
console.log('触发了countChange自定义事件');
}
},
components:{
MyCounter,
}
}
</script>
<style>
</style>
//Counter.vue
<template>
<div>Count的值是:{{count}}</div>
<button @click="add"> +1</button>
</template>
<script>
export default {
name:'MyCounter',
//声明自定义事件
emits:['countChange'],
data() {
return {
count:0,
}
},
methods:{
add() {
this.count++
// this .$emito触发自定义事件
this.$emit('countChange')
}
}
}
</script>
<style>
</style>
3. 自定义事件传参
在调用 this.$emit() 方法触发自定义事件时,可以通过第 2 个参数为自定义事件传参,示例代码如下:
<template>
<h3>Counter组件</h3>
<button @click="onBtnClick"> +1 </button>
</template>
<script>
export default {
//my-counter组件的自定义事件,必须事先声明到 emits节点中
emits:['change'],
methods: {
onBtnClick() {
this.$emit('change',this.count)
//触发白定义事件时,通过第二个参数传参
},
},
}
</script>
<template>
<div>
<h1>app根组件</h1>
</div>
<hr>
<my-counter @count-change="getCount"></my-counter>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name:'MyApp',
methods:{
getCount(val) {
console.log('触发了countChange自定义事件',val);
}
},
components:{
MyCounter,
}
}
</script>
<style>
</style>
<template>
<div>Count的值是:{{count}}</div>
<button @click="add"> +1</button>
</template>
<script>
export default {
name:'MyCounter',
//声明自定义事件
emits:['countChange'],
data() {
return {
count:0,
}
},
methods:{
add() {
this.count++
// this .$emito触发自定义事件
this.$emit('countChange',this.count)
}
}
}
</script>
<style>
</style>
组件上的 v-model
1. 为什么需要在组件上使用 v-model
v-model 是双向数据绑定指令,当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。示意 图如下:
- 外界数据的变化会自动同步到 counter 组件中
- counter 组件中数据的变化,也会自动同步到外界
2. 在组件上使用 v-model 的步骤
① 父组件通过 v-bind: 属性绑定的形式,把数据传递给子组件
② 子组件中,通过 props 接收父组件传递过来的数据
<template>
<div>App根组件------{{count}}</div>
<button @click="count += 1">+1</button>
<hr>
<my-counter :number="count"></my-counter>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name:"MyApp",
data() {
return {
count:0,
}
},
components: {
MyCounter
}
}
</script>
<style>
</style>
<template>
<div>
<p>count值是:{{number}}</p>
</div>
</template>
<script>
export default {
name:'MyCounter',
props:['number']
}
</script>
<style>
</style>
① 在 v-bind: 指令之前添加 v-model 指令
② 在子组件中声明 emits 自定义事件,格式为 update:xxx
③ 调用 $emit() 触发自定义事件,更新父组件中的数据
<template>
<div>App根组件------{{count}}</div>
<button @click="count += 1">+1</button>
<hr>
<my-counter v-model:number="count"></my-counter>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name:"MyApp",
data() {
return {
count:0,
}
},
components: {
MyCounter
}
}
</script>
<style>
</style>
<template>
<div>
<p>count值是:{{number}}</p>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name:'MyCounter',
props:['number'],
emits:['update:number'],
methods: {
add() {
this.$emit('update:number',this.number + 1)
}
}
}
</script>
<style>
</style>
任务列表案例
1. 案例效果
2. 用到的知识点
① vite 创建项目
② 组件的封装与注册
③ props
④ 样式绑定
⑤ 计算属性
⑥ 自定义事件
⑦ 组件上的 v-model
3. 整体实现步骤
① 使用 vite 初始化项目
② 梳理项目结构
③ 封装 todo-list 组件
④ 封装 todo-input 组件
⑤ 封装 todo-button 组件
watch 侦听器
1. 什么是 watch
侦听器 watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发 起请求,判断用户名是否可用。
2. watch 侦听器的基本语法
开发者需要在 watch 节点下,定义自己的侦听器。实例代码如下:
export default {
data() {
return {username:''}
},
watch: {
//监听username的值的变化,
//形参列表中,第一个值是"变化后的新值",第二个值是"变化之前的旧值”
username(newVal,oldVal) {
console.log(newVal,oldVal)
},
},
}
<template>
<div>
<h3>watch侦听器的用法</h3>
<input type="text" class="form-control" v-model.trim="username">
</div>
</template>
<script>
export default {
name:'MyWatch',
data() {
return {
username: ''
}
},
watch: {
username(newVal,oldVal) {
console.log(newVal,oldVal)
}
}
}
</script>
<style>
</style>
3. 使用 watch 检测用户名是否可用
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
import axios from axios
export default {
name:'MyWatch',
data() {
return {
username: ''
}
},
watch: {
async username(newVal,oldVal) {
console.log(newVal,oldVal)
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
console.log(res)
}
}
}
4. immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使 用 immediate 选项。实例代码如下:
watch: {
//1.监听username值的变化
username:{
//2.handle属性是固定写法,但username变化时,调用handle
async handler(newVal,oldVal) {
console.log(newVal,oldVal)
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
console.log(res)
},
//3.表示组件加载完毕后立即调用一次当前的watch侦听器
immediate:true
},
}
<template>
<div>
<h3>watch侦听器的用法</h3>
<input type="text" class="form-control" v-model.trim="username">
</div>
</template>
<script>
import axios from 'axios'
export default {
name:'MyWatch',
data() {
return {
username: 'admin'
}
},
watch: {
// async username(newVal,oldVal) {
// console.log(newVal,oldVal)
// const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
// console.log(res)
// }
username: {
async handler(newVal,oldVal) {
console.log(newVal,oldVal)
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
console.log(res)
},
immediate:true
}
}
}
</script>
<style>
</style>
5. deep 选项
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项, 代码示例如下:
data() {
return {
username: 'admin',
info:{
username:'zs'
//info中包含username属性
}
}
},
watch: {
info: {
async handler(newVal) {
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal.username)
console.log(res)
},
deep:true //霄要使用deep选项,否则username值的变化无法被监听到
},
}
<template>
<div>
<h3>watch侦听器的用法</h3>
<input type="text" class="form-control" v-model.trim="info.username">
</div>
</template>
<script>
import axios from 'axios'
export default {
name:'MyWatch',
data() {
return {
username: 'admin',
info:{
username:'zs'
}
}
},
watch: {
// async username(newVal,oldVal) {
// console.log(newVal,oldVal)
// const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
// console.log(res)
// }
// username: {
// async handler(newVal,oldVal) {
// console.log(newVal,oldVal)
// const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal)
// console.log(res)
// },
// immediate:true
// }
info: {
async handler(newVal) {
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal.username)
console.log(res)
},
deep:true
},
}
}
</script>
<style>
</style>
6. 监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:
data() {
return {
username: 'admin',
info:{
username:'zs'
//info中包含username属性
}
}
},
watch: {
'info.username': {
async handler(newVal) {
const {data:res} = await axios('https://www.escook.cn/api/finduser/'+newVal.username)
console.log(res)
},
deep:true //霄要使用deep选项,否则username值的变化无法被监听到
},
}
7. 计算属性 vs 侦听器
计算属性和侦听器侧重的应用场景不同:
计算属性侧重于监听多个值的变化,最终计算并返回一个新值
侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
组件的生命周期
1. 组件运行的过程
组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段。
2. 如何监听组件的不同时刻
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。例如:
① 当组件在内存中被创建完毕之后,会自动调用 created 函数
② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数
③ 当组件被销毁完毕之后,会自动调用 unmounted 函数
<template>
<div>
<h1>App根组件</h1>
<hr>
<button @click="flag = !flag">Toggle</button>
<!-- 3.以标签形式使用组件 -->
<life-cycle v-if="flag"></life-cycle>
</div>
</template>
<script>
import LifeCycle from './life-cycle.vue'
export default {
name:'MyApp',
data() {
return{
flag:true
}
},
// 2.components注册组件
components:{
LifeCycle,
},
}
</script>
<style>
</style>
<template>
<div>
<h3>LifeCycle组件</h3>
</div>
</template>
<script>
export default {
name:'LifeCycle',
// 组件在内存中创建完毕
created() {
console.log('created:组件在内存中创建完毕');
},
// 组件第一次被渲染到页面
mounted() {
console.log('mounted:组件第一次被渲染到页面');
},
// 组件被销毁完毕
unmounted() {
console.log('unmounted:组件被销毁完毕');
}
}
</script>
<style lang="less" scoped>
</style>
3. 如何监听组件的更新
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
<template>
<div>
<h3>LifeCycle组件-------{{count}}</h3>
<button @click="count += 1">+1</button>
</div>
</template>
<script>
export default {
name:'LifeCycle',
data() {
return {
count:0,
}
},
// 组件在内存中创建完毕
created() {
console.log('created:组件在内存中创建完毕');
},
// 组件第一次被渲染到页面
mounted() {
console.log('mounted:组件第一次被渲染到页面');
},
// 组件被重新渲染完毕了
updated() {
console.log('updated:组件被重新渲染完毕了');
},
// 组件被销毁完毕
unmounted() {
console.log('unmounted:组件被销毁完毕');
}
}
</script>
<style lang="less" scoped>
</style>
4. 组件中主要的生命周期函数
注意:在实际开发中,
生命周期函数 | 执行时机 | 所属阶段 | 执行次数 | 应用场景 |
---|---|---|---|---|
created | 组件在内存中创建完毕后 | 创建阶段 | 唯一一次 | 发Ajax请求初始数据 |
mounted | 组件初次在页面渲染完毕后 | 创建阶段 | 唯一一次 | 操作DOM元素 |
updated | 组件在页面中被重新渲染完毕后 | 运行阶段 | 0次或多次 | - |
unmounted | 组件被销毁后(页面和内存) | 销毁阶段 | 唯一一次 | - |
created 是最常用的生命周期函数!
5. 组件中全部的生命周期函数
疑问:为什么不在 beforeCreate 中发 ajax 请求初始数据?
6. 完整的生命周期图示
可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程: https://www.vue3js.cn/docs/zh/guide/instance.html#生命周期图示
组件之间的数据共享
1. 组件之间的关系
在项目开发中,组件之间的关系分为如下 3 种:
① 父子关系
② 兄弟关系
③ 后代关系
2. 父子组件之间的数据共享
父子组件之间的数据共享又分为:
① 父 -> 子共享数据
② 子 -> 父共享数据
③ 父 <-> 子双向数据同步
2.1 父组件向子组件共享数据
父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。示例代码如下:
//父组件
<template>
<div>
<h1>App根组件---{{count}}</h1>
<button @click="count += 1">+1</button>
<hr>
<my-son :num="count"></my-son>
</div>
</template>
<script>
import MySon from './son.vue'
export default {
name:"MyApp",
data() {
return {
count: 0
}
},
components: {
MySon,
},
}
</script>
<style>
</style>
//子组件
<template>
<div>
<h3>Son子组件----{{num}}</h3>
</div>
</template>
<script>
export default {
name:'MySon',
props:['num']
}
</script>
<style lang="less" scoped>
</style>
2.2 子组件向父组件共享数据
子组件通过自定义事件的方式向父组件共享数据。示例代码如下:
<template>
<div>
<h1>App根组件---{{count}}</h1>
<button @click="count += 1">+1</button>
<hr>
<my-son :num="count" @numchange="getNum"></my-son>
</div>
</template>
<script>
import MySon from './son.vue'
export default {
name:"MyApp",
data() {
return {
count: 0
}
},
methods:{
getNum (num) {
this.count = num
}
},
components: {
MySon,
},
}
</script>
<style>
</style>
<template>
<div>
<h3>Son子组件----{{num}}</h3>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name:'MySon',
props:['num'],
emits:['numchange'],
methods: {
add() {
this.$emit('numchange',this.num+1)
}
}
}
</script>
<style lang="less" scoped>
</style>
2.3 父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:
<template>
<div>
<h1>App根组件---{{count}}</h1>
<button @click="count += 1">+1</button>
<hr>
<!-- <my-son :num="count" @numchange="getNum"></my-son> -->
<my-son v-model:num="count"></my-son>
</div>
</template>
<script>
import MySon from './son.vue'
export default {
name:"MyApp",
data() {
return {
count: 0
}
},
// methods:{
// getNum (num) {
// this.count = num
// }
// },
components: {
MySon,
},
}
</script>
<style>
</style>
<template>
<div>
<h3>Son子组件----{{num}}</h3>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name:'MySon',
props:['num'],
// emits:['numchange'],
emits:['update:num'],
methods: {
add() {
// this.$emit('numchange',this.num+1)
this.$emit('update:num',this.num+1)
}
}
}
</script>
<style lang="less" scoped>
</style>
3. 兄弟组件之间的数据共享
兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实 现兄弟组件之间的数据共享。示意图如下:
3.1 安装 mitt 依赖包
在项目中运行如下的命令,安装 mitt 依赖包:
npm install mitt@2.1.0 -S
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块如下:
//eventBus
//导入mitt包
import mitt from 'mitt'
//创建EventBus的实例对象
const bus = mitt()
//将EventBus的实例对象共享出去
export default bus
3.3 在数据接收方自定义事件
在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件。示例代码如下:
export default {
nname:'MyRight',
data() {
return {
num:0,
}
},
created() {
// 调用bus.on()方法注册一个自定义事件,通过事件处理函数的形参接收数据
bus.on('countChange',count =>{
this.num = count
})
}
}
3.4 在数据接发送方触发事件
在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件。示例代码如下:
import bus from './eventBus.js'
export default {
name:'MyLeft',
data() {
return {
count:0,
}
},
methods: {
add() {
this.count++
bus.emit('countChange',this.count)
}
}
}
<template>
<div>
<h1>App根组件----{{count}}</h1>
<!-- <button @click="count += 1">+1</button> -->
<hr>
<level-two></level-two>
</div>
</template>
<script>
import LevelTwo from './level-two.vue'
export default {
name:'MyApp',
data() {
return {
color:'red',
count: 1
}
},
provide() {
return {
// 返回要共享的数据对象
color:this.color,
count:this.count
}
},
components: {
LevelTwo,
}
}
</script>
<style lang="less" scoped>
</style>
<template>
<div>
<h3>Lever Two 二级组件</h3>
<hr>
<level-three></level-three>
</div>
</template>
<script>
import LevelThree from './level-three.vue'
export default {
name:'LevelTwo',
components: {
LevelThree,
}
}
</script>
<style lang="less" scoped>
</style>
<template>
<div>
<h5>Level Three 三级组件---{{color}}---{{count}}</h5>
</div>
</template>
<script>
export default {
name:'LevelThree',
inject:['color','count']
}
</script>
<style lang="less" scoped>
</style>
4. 后代关系组件之间的数据共享
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂, 可以使用 provide(发送数据用) 和 inject (谁接收谁用)实现后代关系组件之间的数据共享。
4.1 父节点通过 provide 共享数据
父节点的组件可以通过 provide 方法,对其子孙组件共享数据:
export default {
data() {
return {
color:'red'//1.定义"父组件"要向"子孙组件共享的数招
}
},
provide() {
//2.provide函数return的对象中,包含了"要向子孙组件共享的数据
return {
color: this.color,
}
}
}
4.2 子孙节点通过 inject 接收数据
子孙节点可以使用 inject 数组,接收父级节点向下共享的数据。示例代码如下:
<template>
<div>
<h3>子孙组件-----{{color}}</h3>
</div>
</template>
<script>
export default {
//子孙组件,使用inject接收父节点向下共享的color数据,并在页面上使用
inject:['color']
}
</script>
4.3 父节点对外共享响应式的数据
父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据。示例代码如下:
//1.从 vue中按需导入computed函数
import {computed} from 'vue'
export default {
data() {
return {
color:'red',
count: 1
}
},
provide() {
return {
color: computed(() => this.color),
}
},
}
4.4 子孙节点使用响应式的数据
如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用。示例代码如下:
<template>
<div>
<h5>Level Three 三级组件---{{color}}---{{count.value}}</h5>
</div>
</template>
<script>
export default {
name:'LevelThree',
inject:['color','count']
}
</script>
5. vuex
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高 效、清晰、且易于维护。
5.1 vuex基本使用
安装vuex依赖包
npm install vuex --save
导入vuex包
import Vuex from 'vuex'
Vue.use(Vuex)
创建store对象
const store = new Vuex.Store({
//state 中存放的就是全局共享的数据
state:{count:0}
})
将store对象挂载到vue 实例中
new Vue ({
el:'#app',
render:h => h(app),
router,
//将创建的共享数据对象,挂载到vue实例中
//所有的组件,就可以直接从store中获取全局的数据了
store
})
6. 总结
父子关系
① 父 -> 子 属性绑定
② 子 -> 父 事件绑定
③ 父 <-> 子 组件上的 v-model
兄弟关系
④ EventBus
后代关系
⑤ provide & inject
全局数据共享
⑥ vuex
vue 3.x 中全局配置 axios
1. 为什么要全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
① 每个组件中都需要导入 axios(代码臃肿)
② 每次发请求都需要填写完整的请求路径(不利于后期的维护)
2. 如何全局配置 axios
在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios,示例代码如下:
//main.js
// 1.从 vue中按需导入 createApp函数
import { createApp } from 'vue'
// 2.导入待渲染的App 组件
// import App from './App.vue'
// import App from './components/03-style/App.vue'
// import App from './components/04-props/App.vue'
// import App from './components/05-class&style/App.vue'
// import App from './components/10-watch/App.vue'
import App from './components/15-network/App.vue'
import axios from 'axios'
//1.导入需要被全局注册的组件
import Swiper from './components/01-globalReg/Swiper.vue'
import Test from './components/01-globalReg/Test.vue'
//3.调用createApp()函数,返回值是"单页面应用程序的实例"
const app = createApp(App)
//2.调用app . component()方法全局注册组件
app.component('my-swiper', Swiper)
// app.component(Swiper,name,Swiper)
app.component('my-test', Test)
axios.defaults.baseURL = 'https://www.escook.cn'
app.config.globalProperties.$http = axios
//4.调用 mount方法,把App 组件的模板结构,渲染到指定的el区域中
app.mount('#app')
//App.vue
<template>
<div>
<h1>App根组件</h1>
<hr>
<div class="box">
<get-info></get-info>
<post-info></post-info>
</div>
</div>
</template>
<script>
import GetInfo from './GetInfo.vue'
import PostInfo from './PostInfo.vue'
export default {
name:"MyApp",
// 注册组件
components:{
GetInfo,
PostInfo
}
}
</script>
<style lang="less" scoped></style>
//GetInfo.vue
<template>
<div>
<h3>Get Info 组件</h3>
<hr>
<button @click="getinfo">发起GET请求</button>
</div>
</template>
<script>
export default {
name:'GetInfo',
methods: {
async getinfo() {
const {data:res} = await this.$http.get('/api/get',{
params: {
name:'ls',
age:33
}
})
console.log(res)
}
}
}
</script>
<style lang="less" scoped>
</style>
//PostInfo.vue
<template>
<div>
<h3>Post Info 组件</h3>
<hr>
<button @click="postinfo">发起POST请求</button>
</div>
</template>
<script>
export default {
name:'PostInfo',
methods: {
async postinfo() {
const {data:res} = await this.$http.post('/api/post',{name:'zs',age:20})
console.log(res)
}
}
}
</script>
<style lang="less" scoped>
</style>
购物车案例
-
案例效果
-
实现步骤
① 初始化项目基本结构
② 封装 EsHeader 组件
③ 基于 axios 请求商品列表数据( GET 请求,地址为 https://www.escook.cn/api/cart )
④ 封装 EsFooter 组件
⑤ 封装 EsGoods 组件
⑥ 封装 EsCounter 组件
ref 引用
1. 什么是 ref 引用
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。
每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下, 组件的 $refs 指向一个空对象。
<template>
<div>
<h3>MyRef 组件</h3>
<button @click="getRef">获取$refs引用</button>
</div>
</template>
<script>
export default {
methods:{
getRef() {
//this代表当前组件的实例对象,this.$refs默认指向空对象
console.log(this)
}
}
}
</script>
2. 使用 ref 引用 DOM 元素
如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:
<template>
<div>
<h3 ref='myh3'>MyRef 组件</h3>
<button @click="getRef">获取$refs引用</button>
</div>
</template>
<script>
export default {
methods:{
getRef() {
//this代表当前组件的实例对象,this.$refs默认指向空对象
console.log(this.$refs.myh3)
//操作DOM元素,把文本颜色改为红色
this.$refs.myh3.style.color = 'red'
}
}
}
</script>
3. 使用 ref 引用组件实例
如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:
<!--使用ref属性,为对应的"组件"添加引用名称-->
<my-counter ref="counterRef"></my-counter>
<button @click="getRef">获取$refs 引用</button>
methods: {
getRef() {
//通过this.$refs.引用的名称可以引用组件的实例 console.log(this.$refs.counterRef)
//引用到组件的实例之后,就可以调用组件上的 methods方法 this.$refs.counterRef.add()
},
}
4. 控制文本框和按钮的按需切换
通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换。示例代码如下:
<template>
<input type='text' v-if='inputVisible'>
<button v-eles @click="showinput">展示input输入框</button>
</template>
export default {
data() {
return {
//控制文本框和按钮的按需切换
inputVisible:false,
}
},
methods: {
showinput() {
thisinputVisible = true
}
}
}
</script>
5. 让文本框自动获得焦点
当文本框展示出来之后,如果希望它立即获得焦点,则可以为其添加 ref 引用,并调用原生 DOM 对象的 .focus() 方法即可。示例代码如下:
<input type='text' class="form-control" v-if='inputVisible' ref="ipt" >
<!-- v-eles @click="showinput" -->
<button v-else @click="showinput">展示input输入框</button>
methods: {
showinput() {
this.inputVisible = true
//获取文本框的DOM引用。并调用.focus(使其自动获得焦点
this.$refs.ipt.focus()
}
}
6. this.$nextTick(cb) 方法
组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 异步地重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素。
<input type='text' class="form-control" v-if='inputVisible' ref="ipt" >
<!-- v-eles @click="showinput" -->
<button v-else @click="showinput">展示input输入框</button>
methods: {
showinput() {
this.inputVisible = true
//获取文本框的DOM引用。并调用.focus()使其自动获得焦点
this.$nextTick(() => {
this.$refs.ipt.focus()
})
}
}
动态组件
1. 什么是动态组件
动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的
< component> 组件,专门用来实现组件 的动态渲染。
① 是组件的占位符
② 通过 is 属性动态指定要渲染的组件名称
③ < component is=“要渲染的组件的名称”>< /component>
2. 如何实现动态组件渲染
示例代码如下:
data() {
return {
comName: "my-dynamic-1'
// 1.当前要渲染的组件的名称)
}
}
<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组件的第1个p标签</p>
<!--通过slot标签,为用户预留内容占位符(插槽) -->
<slot></slot>
<p>这是MyCom1组件的最后一个p标签</p>
</template>
<my-com-1>
<!--在使用MyCom1组件时,为插槽指定具体的内容-->
<P>---用户自定义的内容---</p>
</my-com-1>
2.1 没有预留插槽的内容会被丢弃
如果在封装组件时没有预留任何 插槽,则用户提供的任何自定义内容都会被丢弃。示例代码如下:
<template>
<p>这是MyCom1组件的第1个p标签</p>
<!--封装组件时吗,没有预留任何插槽 -->
<p>这是MyCom1组件的最后一个p标签</p>
</template>
<my-com-1>
<!--在使用MyCom1组件时,为插槽指定具体的内容-->
<P>---用户自定义的内容---</p>
</my-com-1>
2.2 后备内容
封装组件时,可以为预留的 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何 内容,则后备内容会生效。示例代码如下:
<template>
<p>这是MyCom1组件的第1个p标签</p>
<!--通过slot标签,为用户预留内容占位符(插槽) -->
<slot>这是后备内容</slot>
<p>这是MyCom1组件的最后一个p标签</p>
</template>
3. 具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个 插槽指定具体的 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
4. 作用域插槽
在封装组件的过程中,可以为预留的 插槽绑定 props 数据,这种带有 props 数据的 叫做“作用 域插槽”。示例代码如下:
<div>
<h3>这是TEST组件</h3>
<slot :info="infomation"></slot>
</div>
<!--使用自定义组件-->
<my-test>
<template v-slot:default="scope">
{{ scope }}
</template>
</my-test>
4.1 解构作用域插槽的 Prop
作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。示例代码如下:
<my-table>
<!-- v-slot:可以简写成#-->
<!-- 作用域插槽对外提供的数据对象,可以通过“解构赋值"简化接收的过程->
<template #default="{ user , info }">
<!--【使用】作用域插槽的数据-->
<td>i{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.state }</td>
</template>
</my-table>
4.2 声明作用域插槽
在封装 MyTable 组件的过程中,可以通过作用域插槽把表格每一行的数据传递给组件的使用者。示例代码 如下:
<!--表格主体区域-->
<tbody>
<!--循环渲染表格数据-->
<tr v-for="item in list" :key="item.id">
<!-- 下面的 slot是一个【作用域插槽】-->
<slot :user="item"></slot>
</tr>
</tbody>
4.3 使用作用域插槽
在使用 MyTable 组件时,自定义单元格的渲染方式,并接收作用域插槽对外提供的数据。示例代码如下:
自定义指令
1. 什么是自定义指令
vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。除此之外 vue 还允许开发者自定义指令。
vue 中的自定义指令分为两类,分别是:
- 私有自定义指令
- 全局自定义指令
2. 声明私有自定义指令的语法
在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。示例代码如下:
directives: {
//自定义一个私有指令
focus: {
//当被绑定的元素插入到DOM中时,自动触发mounted函数 mounted(el) {
el.focus() //让被绑定的元素自动获得焦点
}
}
}
3. 使用自定义指令
在使用自定义指令时,需要加上 v- 前缀。示例代码如下:
<!--声明自定义指令时,指令的名字是focus -->
<!--使用自定义指令是,需要加上v-指令前缀-->
<input v-focus />
4. 声明全局自定义指令的语法
全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明,示例代码如下:
const app = vue.createApp({})
//注册一个全局自定义指令`v-focus '
app.directive( 'focus ', {
//当被绑定的元素插入到DOM中时,自动触发 mounted函数
mounted(el) {
// Focus the element
el.focus()
}
})
5. updated 函数
mounted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted 函数不会被触发。 updated 函数会在每次 DOM 更新完成后被调用。示例代码如下:
app.directive( 'focus ', {
mounted(el) {
//第一次插入DOM时触发这个函数
el.focus()
},
updated(el) {//每次DOM更新时都会触发updated函数
el.focus(
}
})
注意:在 vue2 的项目中使用自定义指令时,【 mounted -> bind 】【 updated -> update 】
6. 函数简写
如果 mounted 和updated 函数中的逻辑完全相同,则可以简写成如下格式:
app.directive( 'focus', (el) =>{
//在mounted 和 updated时都会触发相同的业务处理
el.focus()
})
7. 指令的参数值
在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值,示例代码如下:
<!--在使用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>
//自定义v-color 指令
app.directive( 'color ' , (el, binding) =>{
// binding.value就是通过"等号"为指令绑定的值
el.style.color = binding.value
})
Table 案例
1. 案例效果
2. 用到的知识点
组件封装
具名插槽
作用域插槽
自定义指令
3. 实现步骤
① 搭建项目的基本结构
② 请求商品列表的数据
③ 封装 MyTable 组件
④实现删除功能
⑤ 实现添加标签的功能
路由
前端路由的概念与原理
1. 什么是路由
路由(英文:router)就是对应关系。路由分为两大类:
① 后端路由
② 前端路由
2. 回顾:后端路由
后端路由指的是:请求方式、请求地址与 function 处理函数之间的对应关系。在 node.js 课程中,express 路由的基本用法如下:
const express = require( 'express ")
const router = express.Router()
router.get( " /userlist ', function(req,res) {产路由的处理函数*/ })
router.post( " ladduser " , function(req,res) { /*路由的处理函数*/ })
module.exports = router
3. SPA 与前端路由
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。 此时,不同组件之间的切换需要通过前端路由来实现。
结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!
4. 什么是前端路由
通俗易懂的概念:Hash 地址与组件之间的对应关系。
5. 前端路由的工作方式
① 用户点击了页面上的路由链接
② 导致了 URL 地址栏中的 Hash 值发生了变化
③ 前端路由监听了到 Hash 地址的变化
④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
6. 实现简易的前端路由
步骤1:导入并注册 MyHome、MyMovie、MyAbout 三个组件。示例代码如下:
import MyHome from './MyHome.vue'
import MyMovie from './MyMovie.vue'
import MyAbout from './MyAbout.vue'
export default {
components: {
MyHome,
MyMovie,
MyAbout,
},
}
步骤2:通过 标签的 is 属性,动态切换要显示的组件。示例代码如下:
<template>
<h1>App组件</h1>
<component :is="comName"></component>
</template>
export default {
data() {
return {
comName: 'my-home ',
//要展示的组件的名称}
},
}
}
步骤3:在组件的结构中声明如下 3 个 链接,通过点击不同的 链接,切换浏览器地址栏中的 Hash 值:
<a href="#/home">Home</a>
<a href="# /movie">Moviec</a>
<a href="#/about">About</a>
步骤4:在 created 生命周期函数中监听浏览器地址栏中 Hash 地址的变化,动态切换要展示的组件的名称:
created(){
windaw.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 进行使用
vue-router 3.x 的官方文档地址:https://router.vuejs.org/zh/
vue-router 4.x 的官方文档地址:https://next.router.vuejs.org/
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 中按需导入两个方法
//创建router.js
//1.从 vue-router中按需导入两个方法
//createRouter方法用于创建路由的实例对象
//createwebHashHistory用于指定路由的工作模式(hash模式)
import { createRouter, createWebHashHistory } from 'vue-router'
导入需要使用路由控制的组件
// 2.导入组件,这些组件将要以路由的方式,来控制它们的切换
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
在 main.js 中导入并挂载路由模块
import { createApp } from "vue '
import App from './App. vue"import './index.css'
// 1.导入路由模块
import router from './router"
const app = creatcApp(App)
// 2.挂载路由模块
//app.use()方法用来挂载"第三方的插件模块”
app.use(router)
app.mount( ' #app ')
vue-router 的高级用法
1. 路由重定向
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。 通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
const router = createRouter({
// 3.1通过history属性指定路由的工作模式
history: createWebHashHistory(),
// 3.2 通过routes数组,指定路由规则
routes: [
// path 是 hash 地址,component是要展示的组件
{ path: '/', redirect: "/home" },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
],
})
2. 路由高亮
可以通过如下的两种方式,将激活的路由链接进行高亮显示:
① 使用默认的高亮 class 类
② 自定义路由高亮的 class 类
2.1 默认的高亮 class 类
被激活的路由链接,默认会应用一个叫做 router-link-active 的类名。开发者可以使用此类名选择器,为激活 的路由链接设置高亮的样式:
/*在index.css全局样式表中,重新router-link-active 的样*/
.routcr-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: ' labout ', component: About },
],
})
3. 嵌套路由
通过路由实现组件的嵌套展示,叫做嵌套路由。
① 声明子路由链接和子路由占位符
② 在父路由规则中,通过 children 属性嵌套声明子路由规则
3.1 声明子路由链接和子路由占位符
在 About.vue 组件中,声明 tab1 和 tab2 的子路由链接以及子路由占位符。示例代码如下:
<template>
<div>
<h3>MyAbout组件</h3>
<hr>
<!-- 说明子路由链接 -->
<router-link to="'/about/tab1"></router-link>
<router-link to="'/about/tab2"></router-link>
<!-- 在关于页面中,声明两个子路由链接 -->
<router-view></router-view>
</div>
</template>
3.2 通过 children 属性声明子路由规则
在 router.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则。示例代码如下:
router:{
path:'/about',
component:about,
children:[
{
path:'tab1',component:Tab1
},
{
path:'tab2',component: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>
<h3>MyMovie组件 --- {{$route.params.id}}</h3>
</template>
<script>
export default {
name:'MyMovie',
}
</script>
4.3 使用 props 接收路由参数
为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参。示例代码如下:
//1.在定义路由规则中,声明props:ture选项
// 即可在Movic组件中,以props 的形式接收到路由规则匹配到的参数项
{path:'/momvie/:id',component:Movie,props:ture}
<template>
<div>
<h3>MyMovie组件 --- {{id}}</h3>
</div>
</template>
<script>
export default {
props:['id'],//使用props接收路由规则中匹配到的参数项
}
</script>
5. 编程式导航
通过调用 API 实现导航的方式,叫做编程式导航。与之对应的,通过点击链接实现导航的方式,叫做声明式导 航。例如:
- 普通网页中点击 链接、vue 项目中点击 都属于声明式导航
- 普通网页中调用 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 type="button" class="btn btn-primary" @click="goToMovie(3)">导航到Movie页面</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
methods: {
goToMovie(id) {
this.$router.push('/movie/${id}')
},
},
}
</script>
<style lang="less" scoped></style>
5.2 $router.go
调用 this.$router.go() 方法,可以在浏览历史中进行前进和后退。示例代码如下:
<template>
<div>
<h3>MyHome 组件</h3>
<button type="button" class="btn btn-primary" @click="goBack)">后退</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
props:["id"],
methods: {
goBack() {
this.$router.go(-1)
},
},
}
</script>
<style lang="less" scoped></style>
6. 命名路由
通过 name 属性为路由规则定义名称的方式,叫做命名路由。示例代码如下:
注意:命名路由的 name 值不能重复,必须保证唯一性!
6.1 使用命名路由实现声明式导航
为 < router-link> 标签动态绑定 to 属性的值,并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数。示例代码 如下:
6.2 使用命名路由实现编程式导航
调用 push 函数期间指定一个配置对象,name 是要跳转到的路由规则、params 是携带的路由参数
7. 导航守卫
导航守卫可以控制路由的访问权限。示意图如下:
7.1 如何声明全局导航守卫
全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航 守卫:
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 控制后台主页的访问权限
后台管理案例
1. 案例效果
2. 案例用到的知识点
- 命名路由
- 路由重定向
- 导航守卫
- 嵌套路由
- 动态路由匹配
- 编程式导航
vue-cli、组件库、axios、proxy
vue-cli
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:创建项目并自动安装依赖包:
4. 基于 vue ui 创建 vue 项目
vue ui 的本质:通过可视化的面板采集到用户的配置信息后,在后台基于命令行的方式自动初始化项目:
项目创建完成后,自动进入项目仪表盘:
5 . 基于命令行创建 vue 项目
步骤1:在终端下运行 vue create demo2 命令,基于交互式的命令行创建 vue 的项目:
步骤2:选择要安装的功能
步骤3:使用上下箭头选择 vue 的版本,并使用回车键确认选择
步骤4:使用上下箭头选择要使用的 css 预处理器,并使用回车键确认选择:
步骤5:使用上下箭头选择如何存储插件的配置信息,并使用回车键确认选择:
步骤6:是否将刚才的配置保存为预设:
步骤7:选择如何安装项目中的依赖包:
步骤8:开始创建项目并自动安装依赖包
步骤9:项目创建完成:
6. 梳理 vue2 项目的基本结构
7. 分析 main.js 中的主要代码
8. 在 vue2 的项目中使用路由
在 vue2 的项目中,只能安装并使用 3.x 版本的 vue-router。
版本 3 和版本 4 的路由最主要的区别:创建路由模块的方式不同!
8.1 回顾:4.x 版本的路由如何创建路由模块
8.2 学习:3.x 版本的路由如何创建路由模块
步骤1:在 vue2 的项目中安装 3.x 版本的路由:
步骤2:在 src -> components 目录下,创建需要使用路由切换的组件:
步骤3:在 src 目录下创建 router -> index.js 路由模块: vue-cli
步骤4:在 main.js 中导入路由模块,并通过 router 属性进行挂载: vue-cli
步骤5:在 App.vue 根组件中,使用 声明路由的占位符
组件库
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(http://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.2 在 vue2 的项目中安装 element-ui
运行如下的终端命令:
引入 element-ui
开发者可以一次性完整引入所有的 element-ui 组件,或是根据需求,只按需引入用到的 element-ui 组件:
完整引入:操作简单,但是会额外引入一些用不到的组件,导致项目体积过大
按需引入:操作相对复杂一些,但是只会引入用到的组件,能起到优化项目体积的目的
4.3 完整引入
在 main.js 中写入以下内容:
4.4 按需引入
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。 步骤1,安装 babel-plugin-component:
步骤2,修改根目录下的 babel.config.js 配置文件,新增 plugins 节点如下:
步骤3,如果你只希望引入部分组件,比如 Button,那么需要在 main.js 中写入以下内容:
4.5 把组件的导入和注册封装为独立的模块
在 src 目录下新建 element-ui/index.js 模块,并声明如下的代码:
axios 拦截器
1. 回顾:在 vue3 的项目中全局配置 axios
2. 在 vue2 的项目中全局配置 axios
需要在 main.js 入口文件中,通过 Vue 构造函数的 prototype 原型对象全局配置 axios:
3. 什么是拦截器
拦截器(英文:Interceptors)会在每次发起 ajax 请求和得到响应的时候自动被触发。
应用场景: ① Token 身份认证 ② Loading 效果 ③ etc…
4. 配置请求拦截器
通过 axios.interceptors.request.use(成功的回调, 失败的回调) 可以配置请求拦截器。示例代码如下:
注意:失败的回调函数可以被省略!
4.1 请求拦截器 – Token 认证
4.2 请求拦截器 – 展示 Loading 效果
借助于 element ui 提供的 Loading 效果组件(https://element.eleme.cn/#/zh-CN/component/loading) 可以方便的实现 Loading 效果的展示
5. 配置响应拦截器
通过 axios.interceptors.response.use(成功的回调, 失败的回调) 可以配置响应拦截器。示例代码如下:
5.1 响应拦截器 – 关闭 Loading 效果
调用 Loading 实例提供的 close() 方法即可关闭 Loading 效果,示例代码如下
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 项目的根路径:
步骤2,在项目根目录下创建 vue.config.js 的配置文件,并声明如下的配置: 注意:
① devServer.proxy 提供的代理功能,仅在开发调试阶段生效
② 项目上线发布时,依旧需要 API 接口服务器开启 CORS 跨域资源共享
用户列表案例
1. 案例效果
2. 用到的知识点
vue-cli 创建 vue2 项目
element ui 组件库
axios 拦截器
proxy 跨域接口代理
vuer-router 路由
3. 整体实现步骤
① 初始化项目
② 渲染用户表格的数据
③ 基于全局过滤器处理时间格式
④ 实现添加用户的操作
⑤ 实现删除用户的操作
⑥ 通过路由跳转到详情页