vue 基础

文章探讨了ES6模块化规范及其在node.js中的应用,对比了多种前端模块化方案,强调了ES6模块化的统一性。随后介绍了Vue框架中的过滤器、SPA应用构建与vite、vue-cli工具的使用,深入讲解了Vue组件开发中的样式管理、props传递、事件绑定及v-model的应用。最后,分析了JavaScript的异步编程模型,包括EventLoop机制、宏任务与微任务的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 的模块化语法,可以按照如下两个步骤进行配置:

  1. 确保安装了v14.15.1 或更高版本的 node.js
  2. 在package.json 的根节点中添加 “type": "module" 节点

5. ES6 模块化的基本语法

ES6 的模块化主要包含如下3中用法:

  1. 默认导出默认导入
  2. 按需导出按需导入
  3. 直接导入执行模块中的代码
5.1 默认导出

默认导出的语法:export default 默认导出的成员

	let n1 = 10; // 定义模块私有成员 n1
	let n2 = 20; // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去
	function show() {} // 定义模块私有方法 show

	export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
        n1,
        show
    }
5.2 默认导入

默认导入的语法 import 接收名称 from ‘模块标识符’

	// 从01_m1.js 模块中导入 export default 向外共享的成员
	// 并使用 m1 成员进行接收
	import m1 from './01_m1.js'

	// 打印输出的结果为:
	// {n1: 10, show: [Function :show]}
	console.log(m1)
1. 默认导出的注意事项

每个模块中,只允许使用唯一的一次 export default,否则会报错!

	let n1 = 10; // 定义模块私有成员 n1
	let n2 = 20; // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去
	function show() {} // 定义模块私有方法 show

	export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
        n1,
        show
    }	

	// SyntaxError: Identifier '.default' has already been declared
	export default {
        n2
    }
2. 默认导入的注意事项

默认导入时的接收名称可以任意名称,只要是合法的成员名称即可

	// m1 是合法的名称
	import m1 from './01_m1.js'

	// 123m 不是合法的名称,因为成员名称不能以数字开头
	import 123m from './01_m1.js'
5.3 按需导出

按需导出的语法:export 按需导出的成员

	// 当前模块为 03_m2.js
	
	// 向外按需导出变量 s1
	export let s1 = 'aaa'
    // 向外按需导出变量 s2
    export let s2 = 'ccc'
    // 向外按需导出方法 say
    export function say() {}
5.4 按需导入

按需导入的语法:import { s1 } from ‘模块标识符’

	// 导入模块成员
	import { s1, s2, say } from './03_m2.js'

	console.log(s1) // 打印输出 aaa
	console.log(s2) // 打印输出 ccc
	console.log(say) // 打印输出 [Function: say]
1. 按需导出与按需导入的注意事项
  1. 每个模块中可以使用多次按需导出
  2. 按需导入的成员名称必须和按需导出的名称保持一致
  3. 按需导入时,可以使用 as 关键字进行重命名
  4. 按需导入可以和默认导入一起使用
5.5 直接导入并执行模块中的代码

如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模块代码,示例代码如下:

	// 	当前文件模块为 05_m3.js
	
	// 在当前模块中执行一个 for 循环操作
	for (let i = 0; i < 3; i++) {
        console.log(i)
    }

	// -------------------分割线----------------------
	
	// 	直接导入并执行模块代码,不需要得到模块向外共享的成员
	import './05_m3.js'

Promise

1. 回调地狱

多层回调函数的相互嵌套,就形成了回调地狱。示例代码如下:

	setTimeout(() { // 第一层回调函数
        console.log('延时 1 秒后输出')
		

		setTimeout(() { // 第二次回调函数
        	console.log('再延时 2 秒后输出')    


			setTimeout(() {
            	console.log('再延时 3 秒后输出')        
            }, 3000)
        }, 2000)
    }, 1000)

回调地狱的缺点:

  • 代码耦合性太强,牵一发而动全身,难以维护
  • 大量冗余的代码相互嵌套,代码的可读性变差
1.1 如何解决回调地狱的问题

为了解决回调地狱的问题,ES6(ECMAScript 2015)中新增了 Promise 的概念。

1.2 Promise 的基本概念
  1. Promise 是一个构造函数
    • 我们可以创建 Promise 的实例 const p = new Promise()
    • new 出来的 Promise 实例对象,代表一个异步操作
  2. Promise.prototype 上包含一个 .then() 方法
    • 每一个 new Promise() 构造函数得到的实例对象
    • 都可以通过原型链的方式访问到.then() 方法,例如p.then()
  3. .then() 方法用来预先指定成功和失败的回调函数
    • p.then(成功的回调函数,失败的回调函数)
    • p.then(result => { }, error => { })

2. 基于回调函数按顺序读取文件内容

	// 读取文件 1.txt
	fs.readFile('./files/1.txt', 'utf8', (err1, r1) => {
        if (err1) return console.log(err1.message) // 读取文件1 失败
        console.log(r1) // 读取文件1 成功
        // 读取文件 2.txt
        fs.readFile('./filse/2.txt', 'utf8', (err2, r2) => {
            if (err2) return console.log(err2.message) // 读取文件2 失败
            console.log(r2) // 读取文件2 成功
            fs.readFile('./filse/3.txt', 'utf8', (err3, r3) => {
                if (err3) return console.log(err3.message) // 读取文件3 失败
                console.log(r3) // 读取文件3 成功
            })
        })
    })

3. 基于then-fs 读取文件内容

由于 node.js 官方提供的 fs 模块 仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此需要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件内容:

	npm install then-fs
3.1 then-fs 的基本使用

调用 then-fs 提供的 readFile()方法,可以异步地读取文件的内容,它的返回值是 Promise 的实例对象。因此可以调用.then()方法为每个 Promise 异步操作指定成功失败之后的回调函数。示例代码如下:

	// 基于 Promise 的方式读取文件
	import thenFs from 'then-fs'
	// 注意:.then() 中的失败回调是可选的,可以被省略
	thenFs.readFile('./files/1.txt', 'utf8').then(r1 => { console.log(r1)}, err1 => { console.log(err1.message) })
	thenFs.readFile('./files/2.txt', 'utf8').then(r2 => { console.log(r2)}, err2 => { console.log(err2.message) })
	thenFs.readFile('./files/3.txt', 'utf8').then(r3 => { console.log(r3)}, err3 => { console.log(err3.message) })

注意:上述的代码无法保证文件的读取顺序,需要做进一步的改进!

3.2 .then()方法的特性

如果上一个.then()方法中返回了一个新的 Promise 实例对象,则可以通过下一个.then() 继续进行处理。通过.then()方法的链式调用,就可以解决了回调函数的问题。

3.3 基于 Promise 按顺序读取文件的内容

Promise 支持链式调用,从而来解决回调地狱的问题。示例代码如下:

	thenFs.readFile('./files/1.txt', 'utf8')   // 1. 返回值是 Promise 的实例对象
    .then(r1 => { // 2. 通过 .then  为第一个 Promise 实例指定成功之后的回调函数
        console.log(r1);
        return thenFs.readFile('./files/2.txt', 'utf8') // 3. 在第一个 .then 中返回一个新的 Promise 实例对象
    })
    .then(r2 => { // 4. 继续调用 .then 为上一个 .then 的返回值 (新的 Promise 实例) 指定成功之后的回调函数
        console.log(r2);
        return thenFs.readFile('./files/3.txt', 'utf8') // 5. 在第二个 .then 中再返回一个新的 Promise 实例对象
    })
    .then(r3 => { // 6. 继续调用 .then 为上一个 .then 的返回值 (新的 Promise 实例) 指定成功之后的回调函数
        console.log(r3);
    })
3.4 通过.catch 捕获错误

在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理:

	thenFs.readFile('./files/1.txt', 'utf8') // 文件不存在导致读取失败,后面的 3 个 .then 都不执行
    .then(r1 => {
        console.log(r1);
        return thenFs.readFile('./files/2.txt', 'utf8')
    })
    .then(r2 => {
        console.log(r2);
        return thenFs.readFile('./files/3.txt', 'utf8')
    })
    .then(r3 => {
        console.log(r3);
    })
    .catch(err => { // 捕获第一行发生的错误,并输出错误的信息
        console.log(err.message);
    })
3.5 Promise.all()方法

Promise.all() 方法会发起并行的 Promise 异步操作,等所有的异步操作全部结束后才会执行下一步的 .then 操作(等待机制)。示例代码如下:

	// 1. 定义一个数组,存放3个读文件的异步操作
    const promiseArr = [
        thenFs.readFile('./files/11.txt', 'utf8'),
        thenFs.readFile('./files/2.txt', 'utf8'),
        thenFs.readFile('./files/3.txt', 'utf8'),
    ]
    // 2. 将 Promise 的数组,作为 Promise.all() 的参数
    Promise.all(promiseArr)
    .then(([r1, r2, r3]) => {   // 2.1 所有文件读取成功(等待机制)
        console.log(r1, r2, r3);
    })
    .catch(err => { // 2.2 捕获 Promise 异步操作中的错误
        console.log(err.message);
    })
3.6 Promise.race()方法

Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作(赛跑机制)。示例代码如下:

	// 1. 定义一个数组,存放3个读文件的异步操作
    const promiseArr = [
        thenFs.readFile('./files/11.txt', 'utf8'),
        thenFs.readFile('./files/2.txt', 'utf8'),
        thenFs.readFile('./files/3.txt', 'utf8'),
    ]
    // 2. 将 Promise 的数组,作为 Promise.all() 的参数
    Promise.race(promiseArr)
    .then(([r1, r2, r3]) => {   // 2.1 所有文件读取成功(等待机制)
        console.log(r1, r2, r3);
    })
    .catch(err => { // 2.2 捕获 Promise 异步操作中的错误
        console.log(err.message);
    })

4. 基于 Promise 封装读文件的方法

方法的封装要求:

  1. 方法的名称要定义为 getFile
  2. 方法接收一个形参 fpath,表示要读取的文件的路径
  3. 方法的返回值为 Promise 实例对象
4.1 getFile 方法的基本定义
	// 1. 方法的名称为 getFile
	// 2. 方法接收一个形参 fpath,表示要读取的文件路径	
	function getFile(Fpath) {
        // 3. 方法的返回值为 Promise 的实例对象
        return new Promise()
    }
4.2 创建具体的异步操作

如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的异步操作定义到 function 函数内部。示例代码如下:

	// 1. 方法的名称为 getFile
	// 2. 方法接收一个形参 fpath,表示要读取的文件路径
	function getFile(fpath) {
        // 3. 方法的返回值为 Promise 的实例对象
        return new Promise(function() {
            // 4. 下面这行代码,表示这是一个具体的、读文件的异步操作
            fs.readFile(fpath, 'utf8', (err, dataStr) => {})
        })
    }
4.3 获取 .then 的两个实参

通过 .then() 指定的成功失败的回调函数,可以在 function 的形参中进行接收,示例代码如下:

	function getFile(fpath) {
        // resolve 形参是:调用 getFile() 方法时,通过 .then 指定的“成功的”回调函数
        // reject 形参是:调用 getFile() 方法时,通过 .then 指定的“失败的”回调函数
        return new Promise(function(resolve, reject) {
            fs.readFile(fpath, 'utf8', (err, dataStr) => {})
        }) 

        // getFile 方法的调用过程
        getFile('./files/1.txt').then(成功的回调函数, 失败的回调函数)
    }
4.4 调用 resolve 和 reject 回调函数

Promise 异步操作的结果,可以调用 resolve 回调函数进行处理。实例代码如下:

	function getFile(fpath) {
        // resolve 形参是:调用 getFile() 方法时,通过 .then 指定的“成功的”回调函数
        // reject 形参是:调用 getFile() 方法时,通过 .then 指定的“失败的”回调函数
        return new Promise(function(resolve, reject) {
            fs.readFile(fpath, 'utf8', (err, dataStr) => {
                if(err) return reject(err) // 如果读取失败,则调用“失败的回调函数”
                resolve(dataStr)           // 如果读取成功,则调用“成功的回调函数”
            })
        }) 

        // getFile 方法的调用过程
        getFile('./files/1.txt').then(成功的回调函数, 失败的回调函数)
    }

async/await

1. 什么是 async/await

async/awaitES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。在 async/await 出现之前,开发者只能通过 链式 .then() 的方式处理 Promise 异步操作。

.then 链式调用的优点:解决了回调地狱的问题

.then 链式 调用的缺点:代码冗余、阅读性差、不易理解

2. async/await 的基本使用

使用 async/await 简化 Promise 异步操作的示例代码如下:

	import thenFs from 'then-fs'
	
	// 按照顺序读取文件 1,2,3 的内容
	async funtion getAllFile() {
        const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
        console.log(r1)
        const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
        console.log(r2)
        const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
        console.log(r3)
    }
	getAllFile()
3. async/await 的使用注意事项
  1. 如果在 function 中使用了 await,则 function 必须被 async 修饰
  2. 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行
	console.log('A')
	async function getAllFile() {
        console.log('B')
        const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
        const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
        const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
        console.log(r1, r2, r3)
        console.log('D')
    }

	getAllFile()
	console.log('C')
	// 最终输出的顺序
	A
    B
    C
    111 222 333
	D

EventLoop

1. JavaScript 是单线程的语言

JavaScript 是一门单线程执行的编程语言。也就是说,同一时间只能做一件事情。

任务1
任务2
任务3
任务N...

单线程执行任务队列的问题:

如果前一个任务非常耗时,则后续的任务就不得不一直等待,从而导致程序假死的问题。

2. 同步任务和异步任务

为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:

  1. 同步任务(synchronous)

    • 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
    • 只有前一个任务执行完毕,才能执行后一个任务
  2. 异步任务(asynchronous)

    • 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行

    • 当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数

3. 同步任务和异步任务执行过程

在这里插入图片描述

  1. 同步任务由 JavaScript 主线程次序执行
  2. 异步任务委托给宿主环境执行
  3. 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行
  4. JavaScript 主线程的执行栈被清空后,会读取任务队列中的回调函数,次序执行
  5. JavaScript 主线程不断重复上面的第4步

4. EventLoop 的基本概念

JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop(事件循环)

5. 结合 EventLoop 分析输出的顺序

	import thenFs from 'then-fs'

	console.log('A')
	thenFs.readFile('./files/1.txt', 'utf8').then(dataStr => {
        console.log('B')
    })
	setTimeout(() => {
        console.log('C')
    }, 0)
	console.log('D')

正确的输出结果:ADCB。其中:

  • A 和 D 属于同步任务。会根据代码的先后顺序依次被执行
  • C 和 B 属于异步任务。他们的回调函数会被加入到任务队列中,等待主线程空闲时再执行

宏任务和微任务

1. 什么是宏任务和微任务

JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:

  1. 宏任务(macrotask)
    • 异步 Ajax 请求
    • setTimeout、setInterval
    • 文件操作
    • 其他宏任务
  2. 微任务(microtask)
    • Promise.then、.catch 和 .finally
    • process.nextTick
    • 其他微任务
JS 任务
同步任务
(非耗时任务)
异步任务
(耗时任务)
宏任务
异步Ajax、setTimeout、setInterval、文件操作等
微任务
Promise.then、Promise.catch、Promise.finally等

2. 宏任务和微任务的执行顺序

宏任务
执行结束
有微任务?
执行所有微任务
执行下一个
宏任务

每一个宏任务执行完之后,都会检查是否存在待执行的微任务

如果有,则执行完所有微任务之后,再继续执行下一个宏任务。

3.去银行办业务的场景

  1. 小云和小腾去银行办业务。首先,需要取号之后进行排队 (宏任务队列)
  2. 假设当前银行网店只有一个柜员,小云在办理存款业务时,小腾只能等待单线程,宏任务按次序执行
  3. 小云办完存款业务后,柜员询问他是否还想办理其他业务? (当前宏任务执行完,检查是否有微任务
  4. 小云告诉柜员:想要买理财产品,再办个信用卡、最后再兑换点马年纪念币? (执行微任务,后续宏任务被推迟
  5. 小云离开柜台后,柜员开始为小腾办理业务 (所有微任务执行完毕,开始执行下一个宏任务

4. 分析以下代码输出的顺序

	setTimeout(function() {
        console.log('1')
    })

	new Promise(function (resolve) {
        console.log('2')
        resolve()
    }).then(function() {
        console.log('3')
    })

	console.log('4')

正确输出顺序是:2431

分析:

  1. 先执行所有的同步任务
    • 执行第6行、第12行代码
  2. 再执行微任务
    • 执行第8行代码
  3. 再执行下一个宏任务
    • 执行第2行代码

5. 经典面试题

请分析以下代码输出的顺序

	console.log('1');
setTimeout(function () {
    console.log('2');
    new Promise(function (resolve) {
        console.log('3');
        resolve();
    }).then(function () {
        console.log('4');
    })
});
new Promise(function (resolve) {
    console.log('5');
    resolve()
}).then(function () {
    console.log('6');
})
setTimeout(() => {
    console.log('7');
    new Promise(function (resolve) {
        console.log('8');
        resolve()
    }).then(function () {
        console.log('9');
    })
});

正确的输出顺序是:1 5 6 2 3 4 7 8 9

总结

  1. 能够知道如何使用 ES6 的模块化语法
    • 默认导出与默认导入、按需导出与按需导入
  2. 能够知道如何使用 Promise 解决回调地狱问题
    • promise.then()、promise.catch()
  3. 能够使用 async/await 简化 Promise 的调用
    • 方法中用到了 await ,则方法需要被 async 修饰
  4. 能够说出什么是 EventLoop
    • EventLoop 示意图
  5. 能够说出宏任务和微任务的执行顺序
    • 在执行下一个宏任务之前,先检查是否有待执行的微任务

前端工程化与webpack

前端工程化

1. 小白眼中的前端开发 vs 实际的前端开发

小白眼中的前端开发:

  • 会写HTML + CSS + JavaScript 就会前端开发
  • 需要美化页面样式,就拽一个 bootstrap 过来
  • 需要操作 DOM 或发起 Ajax 请求,再拽一个 jQuery 过来
  • 需要渲染模板结构,就用 art-template 等模板引擎

实际的前端开发:

  • 模块化(js的模块化、css的模块化、其他资源的模块化)
  • 组件化(复用现有的 UI 结构,样式,行为)
  • 规范化(目录结构的划分、编码规范化、接口规划好、文档规范化、Git 分支管理)
  • 自动化(自动化构建、自动部署、自动化测试)

2. 什么是前端工程化

前端工程化指的是:在企业级的前端项目开发中,把前端开发所需的工具技术流程经验等进行规范化、标准化。最终落实到细节上,就是实现前端的“4个现代化”:

模块化、组件化、规范化、自动化

3. 前端工程化的好处

前端工程化的好处主要体现在如下两方面:

  1. 前端工程化让前端开发能够“自成体系”,覆盖了前端项目从创建到部署的方方面面
  2. 最大程度地提高了前端的开发效率,降低了技术选型、前后端联调等带来的协调沟通成本

webpack 的基本使用

1. 什么是 webpack

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

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

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

注意:目前企业级的前端项目开发中,绝大多数的项目都是基于 webpack 进行打包构建的。

2. 创建列表隔行变色项目

  1. 新建项目空白目录,并运行 npm init -y 命令,初始化包管理配置文件 package.json
  2. 新建 src 源代码目录
  3. 新建 src -> index.html 首页 和 scr -> index.js 脚本文件
  4. 初始化首页基本结构
  5. 运行 npm install jquery -S 命令,安装jQuery
  6. 通过 ES6 模块化的方式导入 jQuery,实现列表隔行变色效果

3. 在项目中配置 webpack

  1. 在项目根目录中,创建 webpack.config.js 的 webpack 配置文件,并初始化如下的基本配置:
	module.exports = {
        mode: 'development'	// mode 用来指定构建模式。可选值有 development 和 production
    }
  1. 在 package.json 的 scripts 节点下,新增 dev 脚本如下:
	"scripts": {
        "dev": "webpack"	// script 节点下的脚本,可以通过 npm run 执行。例如 npm run dev
    }
3.1 mode 的可选值

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

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

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

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

3.3 webpack 中的默认约定

在 webpack 中有如下的默认约定

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

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

3.4 自定义打包的入口与出口

在 webpack.config.js 配置文件中,通过 entry 节点指定打包的入口。通过 output 节点指定打包的出口。示例代码如下:

	const path = require('path')	// 导入 node.js 中专门操作路径的模块
    module.exports = {
        entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
        output: {
            path: path.join(__dirname, './dist'), // 输出文件的存放路径
            filename: 'bundle.js' // 输出文件的名称
        }
    }

webpack 中的插件

1. webpack 插件的使用

通过安装和配置第三方的插件,可以拓展 webpack 的能力,从而让 webpack 用起来更方便。最常用的 webpack 插件有如下两个:

  1. webpack-dev-server
    • 类似于 node.js 阶段用到的 nodemon 工具
    • 每当修改了源代码,webpack 会自动进行项目的打包和构建
  2. 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
  1. 修改package.json -> scripts 中的 dev 命令如下
	scripts": {
    "dev": "webpack serve" // script 节点下的脚本,可以通过 npm run 执行
  },
  1. 再次执行 npm run dev 命令,重新进行项目的打包
  2. 在浏览器中访问 http://localhost:8080 地址,查看自动打包效果

注意:webpack-dev-server 会启动一个实时打包的 http 服务器

2.3 打包生成的文件去哪儿了?
  1. 不配置 webpack-dev-server 的情况下,webpack 打包生成的文件,会存放到实际的物理磁盘上
    • 严格遵守开发者在 webpack.config.js 中指定配置
    • 根据 output 节点指定路径进行存放
  2. 配置了 webpack-dev-server 之后,打包生成的文件存放到了内存中
    • 不再根据 output 节点指定的路径,存放到实际的物理磁盘上
    • 提高了实时打包输出的性能,因为内存比物理磁盘速度快很多
2.4 生成到内存中的文件该如何访问?

webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中,而且是虚拟的、不可见的

3. html-webpack-plugin

html-webpack-plugin 是 webpack 中的HTML 插件,可以通过此插件自定制 index.html 页面的内容

需求:通过 html-webpack-plugin 插件,将 src 目录下的 index.html 首页,复制到项目根目录中一份

3.1 安装 html-webpack-plugin

运行如下的命令,即可在项目中安装此插件:

	npm install html-webpack-plugin@4.5.0 -D
3.2 配置 html-webpack-plugin
	// 1. 导入 HTML 插件,得到一个构造函数
	const HtmlPlugin = require('html-webpack-plugin')
    
    // 2. 创建 HTML 插件的实例对象
    const htmlPlugin = new HtmlPlugin({
        template: './src/index.html', // 指定原文件的存放路径
        filename: './index.html', // 指定生成的文件的存放路径
    })
    
    module.exports = {
        mode: 'development',
        plugins: [htmlPlugin], // 3. 通过 plugins 节点,使 htmlPlugin 插件生效
    }
3.3 解惑 html-webpack-plugin
  1. 通过 HTML 插件复制到项目根目录中的 index.html 页面,也别放到了内存中
  2. HTML 插件在生成的 index.html 页面的底部自动注入了打包的 bundle.js 文件

4. devServer 节点

在 webpack.config.js 配置文件中,可以通过 devServer 节点对 webpack-dev-server 插件进行更多的配置,示例代码如下:

	devServer: {
		open: true, // 初次打包完成后,自动打开浏览器
        host: '127.0.0.1', // 实时打包所使用的主机地址
        port: 80, // 实时打包所使用的端口号
    }

webpack 中的 loader

1. loader 概述

在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块,webpack默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错!

loader 加载器的作用:协助 webpack 打包处理特定文件模块。比如:

  • css-loader 可以打包处理 .css 相关的文件
  • less-loader 可以打包处理 .less 相关的文件
  • babel-loader 可以打包处理 webpack 无法处理的高级 js 语法

2. loader 的调用过程

将要被 webpack 打包
处理的文件模块
是否为 js 模块
是否包含
高级 js 语法
是否
配置了 babel
调用 loader 处理
是否配置了
对应 loader
调用 loader 处理
报错
webpack 进行处理
报错

3. 打包处理 css 文件

  1. 运行 命令,安装处理 css 文件的 loader
  2. 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
	module: { // 所有第三方文件模块的匹配规则
        rules: [ // 文件后缀名的匹配规则
            {test: /\.css$/, use: ['style-loader', 'css-loader']}
        ]
    }

其中,test 表示匹配的文件类型use 表示对应要调用的 loader

注意:

  • use 数组总指定的 loader 顺序是固定的
  • 多个 loader 的调用顺序是:从后往前调用

4. 打包处理 less 文件

  1. 运行 npm i less-loader@7.1.0 less@3.12.2 -D 命令
  2. 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
	module: { // 所有第三方文件模块的匹配规则
        rules: [ // 文件后缀名的匹配规则
            {test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
        ]
    }

5. 打包处理样式表中与 url 路径相关的文件

  1. 运行 npm i url-loader@4.1.1 file-loader@6.2.0 -D 命令
  2. 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
	module: { // 所有第三方文件模块的匹配规则
        rules: [ // 文件后缀名的匹配规则
            {test: /\.jpg|png|gif$/, use: ['url-loader?limit=22229']}
        ]
    }

其中 ? 之后的是 loader 的参数项

  • limit 用来指定图片的大小,单位是字节(byte)
  • 只有 <= limit 大小的图片,才会被转为 base64 格式的图片
5.1 loader 的另一个配置方式

带参数的 loader 还可以通过对象的方式进行配置:

	module: { // 用来处理所有的第三方模块
        rules: [ // 第三方模块的匹配规则
            {
                test: /\.jpg|png|git/, // 匹配图片文件
                use: {
                    loader: 'url-loader', // 通过 loader 属性指定要调用的 loader
                    options: { // 通过 options 属性指定参数项
                        limit: 22229
                    }
                }
            }
        ]
    }

6. 打包处理 js 文件中的高级语法

webpack 只能打包处理一部分高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法,需要借助于 babel-loader 进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码:

	class Person {
        // 通过 static 关键字,为 Person 类定义了一个静态属性 info
        // webpack 无法打包处理“静态属性”这个高级语法
        static info = 'person info'
    }

	console.log(Person.info)
6.1 安装 babel-loader 相关的包

运行如下的命令安装对应的依赖包:

	npm i babel-loader@8.2.1 @babel/core@7.12.3 @babel/plugin-proppsal-class-properties@7.12.1 -D

包的名称及版本号列表如下(红色是包的名称,黑色是包的版本号):

  • babel-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 为排除项,
        // 表示 babel-loader 只需处理开发者编写的 js 文件,不需处理 node_modules 下的 js 文件
        exclude: /node_modules/,
        use: {
            loader: 'babel-loader',
            options: { // 参数项
                // 声明一个 babel 插件,此插件用来转化 class 中的高级语法
                plugins: ['@babel/plugin-proppsal-class-properties'],
            }
        }
    }

打包发布

1. 为什么要打包发布

项目开发完成之后,使用 webpack 对项目进行打包发布的主要原因有以下两点:

  1. 开发环境下,打包生成的文件存放于内存中,无法获取到最终打包生成的文件
  2. 开发环境下,打包生成的文件不会进行代码压缩和性能优化

2. 配置 webpack 的打包发布

package.json 文件的 scripts 节点下: 新增 build 命令如下:

	"scripts": {
        "dev": "webpack serve", // 开发环境中, 运行 dev 命令
        "build": "webpack --mode production" // 项目发布时,运行 build 命令
    }

–model 是一个参数项,用来指定 webpack 的运行模式。production 代表生产环境,会对打包生成的文件进行代码压缩性能优化

注意:通过 –model 指定的参数项,会覆盖 webpack.config.js 中的 model 选项。

3. 把 JavaScript 文件统一生成到 js 目录中

在 webpack.config.js 配置文件的 output 节点中,进行如下的配置:

	output: {
        path: path.join(__dirname, 'dist');
        // 明确告诉 webpack 把生成的 bundle.js 文件存放到 dist 目录下的 js 子目录中
        filename: 'js/bundle.js'
    }

4. 把图片文件统一生成到 image 目录中

修改 webpack.config.js 中 的url-loader 匹配项,新增 outputPath 选项即可指定图片文件的输出路径:

	{
        test: /\.jpg|png|gif$/,
        use: {
            loader: 'url-loader',
            options: {
                limit: 22229
                // 明确指定把打包生成的图片文件,存储到 dist 目录下的image 文件夹中
                outputPath: 'image'
            }
        }
    }

5. 自动清理 dist 目录下的旧文件

为了在每次打包发布时自动清理掉 dist 目录中的旧文件,可以安装并配置 clean-webpack-plugin 插件:

	// 1. 安装清理 dist 目录的 webpack 插件
	npm i clean-webpack-plugin@3.0.0 -D
	
	// 2. 按需导入插件、得到插件的构造函数之后,创建插件的实例对象
	const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const cleanPlugin = new CleanWebpackPlugin()
    
    // 3. 把创建的 cleanPlugin 插件实例对象,挂载到 plugins 节点中
    plugins: [htmlPlugin, cleanPlugin]  // 挂载插件

6. 企业级项目的打包发布

企业级的项目在进行打包发布时,远比刚才的方式要复杂的多,主要的发布流程如下:

  • 生成打包报告,根据报告分析具体的优化方案
  • Tree-Shaking
  • 为第三方库启用 CDN 加载
  • 配置组件的按需加载
  • 开启路由懒加载
  • 自定制首页内容

Source Map

1.生产环境遇到的问题

前端项目在投入生产环境之前,都需要对 JavaScript 源代码进行压缩混淆,从而先小文件的体积,提高文件的加载效率。此时就不可避免的产生了另一个问题:

对压缩混之后的代码除错(debug)是一件极其困难的事情

  • 变量被替换成没有任何语义的名称
  • 空行和注释被剔除

2. 什么是 Source Map

Source Map 就是一个信息文件,里面储存这位置信息。也就是说,Source Map 文件中存储着代码压缩混淆前后对应关系

有了它,出错的时候,除错工具将直接显示原始代码,而不是转化后的代码,能够极大的方便后期的调试。

3. webpack 开发环境下的 Source Map

开发环境下,webpack 默认启用了 Source Map 功能。当程序运行出错时,可以直接在控制台提示错误行的位置,并定位到具体的源代码

3.1 默认 Source Map 的问题

开发环境下默认生成的 Source Map,记录的是生成后的代码的位置。会导致运行时报错的行数源代码的行数不一致的问题

3.2 解决默认 Source Map 的问题

开发环境下,推荐在 webpack.config.js 中添加如下的配置,即可保证运行时报错的行数源代码的行数保持一致:

	module.exports = {
        mode: 'development',
        // eval-source-map 仅限在“开发模式”下使用,不建议在“生产模式”下使用
        // 此选项生成的 Source Map 能够保证“运行是报错的行数”与源代码的行数“保持一致
        devtool: 'eval-source-map',
        // 省略其他配置项...
    }

4. webpack 生产环境下的 Source Map

生成环境下,如果省略了 devtool 选项,则最终生成的文件中不包含 Source Map。这能够防止原始代码通过 Source Map 的形式暴露给别有所图之人

4.1 只定位行数不暴露源码

在生产环境下,如果只想定位报错的具体行数,且不想暴露源码。此时可以将 devtool 的值设置为 nosources-source-map

4.2 定位行数且暴露源码

在生产环境下,如果想在定位报错行数的同时展示具体报错的源码。此时可以将 devtool 的值设置为 source-map

5. Source Map 的最佳实践

开发环境下:

  • 建议把 devtool 的值设置为 eval-source-map
  • 好处:可以精准定位到具体的错误行

生产环境下:

  • 建议关闭 Source Map 或将 devtool 的值设置为
  • 好处:防止源码泄露,提高网站的安全性

总结

  1. 能够掌握 webpack 的基本使用

    • 安装、webpack.config.js、修改打包入口
  2. 了解常用的 plugin 的基本使用

    • webpack-dev-server、html-webpack-plugin
  3. 了解常用的 loader 的基本使用

    • loader 的作用、loader 的调用过程
  4. 能够说出 Source Map 的作用

    • 精准定位到错误行显示对应的源码
    • 方便开发者调试源码中的错误

vue 基础入门

vue 简介

1. 什么是 vue

官方给出的概念:Vue是一套用于构建用户界面的前端框架

1.1 解读核心关键词:构建用户界面

前端开发者最主要的工作,就是为网站的使用者(又称为:网站的用户)构建出美观、舒适、好用的网页。

在这里插入图片描述

1.2 构建用户界面的传统方式

在传统的 web 前端开发中,是基于 jQuery + 模板引擎的方式来构建用户界面的

在这里插入图片描述

1.3 使用 vue 构建用户界面

使用 vue 构建用户界面,解决了 jQuery + 模块引擎的诸多痛点,极大的提高了前端开发的效率和体验。

在这里插入图片描述

1.4 解读核心关键词:框架

官方给 vue 的定位是前端框架,因为它提供了构建用户界面的一整套解决方案(俗称 vue 全家桶):

  • vue (核心库)
  • vue-router(路由方案)
  • vuex(状态管理方案)
  • vue 组件库(快速搭建页面 UI 效果的方案)

以及辅助 vue 项目开发的一系列工具:

  • vue-cil (npm 全局包:一键生成工程化的 vue项目 - 基于webpack、大而全)
  • vite(npm全局包:一键生成工程化的 vue 项目 - 小而巧)
  • vue-devtools(浏览器插件:辅助调试的工具)
  • vetur(vscode 插件:提供语法高亮和智能提示)
1.5 总结:什么是 vue

在这里插入图片描述

2. vue 的特性

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

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

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

变化
自动渲染
页面所
依赖的数据
vue
监听数据的变化
页面结构

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

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

2.2 双向数据绑定

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

2.3 MVVM

MVVM 是 vue 实现数据驱动视图双向数据绑定的核心原理。它把每个 HTML 页面 都拆分成了如下三个部分:

在这里插入图片描述

在 MVVM 概念中:

View 表示当前页面所渲染的 DOM 结构。

Model 表示当前页面渲染时所依赖的数据源。

ViewModel 表示 Vue 的实例,它是 MVVM 的核心

2.4 MVVM 的工作原理

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

监听 DOM 变化
自动同步
监听数据源变化
自动更新
View
ViewModel
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 支持等

废弃的旧功能如下:

过滤器、不在支持$on,$off 和 $once 实例方法等

vue 的基本使用

1. 基本使用步骤

  1. 导入 vue.js 的script 脚本文件
  2. 在页面中声明一个将要被 vue 所控制的 DOM 区域
  3. 创建 vm 实例对象 (vue 实例对象)

2. 基本代码与 MVVM 的对应关系

在这里插入图片描述

vue 的调试工具

1. 安装 vue-devtools 调试工具

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

在线安装 vue-devtools

vue 的指令与过滤器

1. 指令的概念

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

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

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

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

  • v-text
  • {{ }}
  • v-html
v-text

用法示例:

	<!-- 把 username 对应的值,渲染到第一个 p 标签中 -->
	<p v-text="username"></p>
	
	<!-- 把 gender 对应的值,渲染到第二个 p 标签中 --> 
	<!-- 注意:第二个 p 标签中,默认的文本“性别”会被 gender 的值覆盖掉 -->
	<p v-text="gender">性别</p>

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

{{ }}语法

vue 提供的 {{ }}语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这个{{ }}语法的专业名称是插值表达式(英文名为:Mustache

	<!-- 使用{{ }} 插值表达式,将对应的值渲染到元素的内容节点中 -->
	<!-- 同时保留元素自身的默认值 -->
	<p>姓名:{{username}}</p>
	<p>性别:{{gender}}</p>
v-html

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

	<!-- 假设 data 中定义了名为 discription 的数据,数据的值包含 HTML 标签字符串: -->
	<!-- '<h5 style="color: red;">我在黑马程序员学习 vue.js 课程。</h5>' -->

	<p v-html="discription"></p>
1.2 属性绑定指令

如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令。用法示例如下:

	<!-- 假设有如下的 data 数据:
        data: {
            inputValue: '请输入内容',
            imgSrc: 'https://cn.vuejs.org/images/logo.png'
        }
    -->

    <!-- 使用 v-bind 指令,为 input 的 placeholder 动态绑定属性值 -->
    <input type="text" v-bind:placeholder="inputValue">
    <br>
    <!-- 使用 v-bind 指令,为 img 的src 动态绑定属性值 -->
    <img v-bind:src="imgSrc" alt="">
使用JavaScript 表达式

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

	{{ number + 1 }}
	
	{{ ok ? 'YES' : 'NO' }}

	{{ message.split('').reverse().join('') }}

	<div v-bind:id="'list-' + id"></div>
1.3 事件绑定指令

vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。语法格式如下:

	<h3>count 的值:{{count}}</h3>
	<!-- 语法格式为 v-on:事件名称=“事件处理函数的名称” -->
	<button v-on:click="addCount">+1</button>

注意:原生 DOM 对象 有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,

分别为:v-on:click、v-on:input、v-on:keyup

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

	const vm = new Vue({
        el: '#app',
        data: { count: 0 },
        methods: {		// v-on 绑定的事件处理函数,需要声明再 methods 节点中
            addCount() { // 事件处理函数的名字
                // this 表示当前 new 出来的 vm 实例对象
                // 通过 this 可以访问到 data 中的数据
                this.count += 1;
            }
        }
    })
事件绑定的简写形式
	<div id="app">
        <h3>count 的值为: {{count}}</h3>
        
        <!-- 完整写法 -->
        <button v-on:cilck="addCount">+1</button>
        
        <!-- 简写形式,把 v-on:简写为 @ 符号 -->
        <!-- 如果事件处理函数中的代码足够简单,只有一行代码,则可以建写道行内 -->
        <button @click="count += 1">+1</button>
	</div>
事件对象 event

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

	<h3>count 的值为 {{count}}</h3>
    <button @:click="addCount">+1</button>
    ----------------------- 分割线 -----------------------
    methods: {
        addCount(e) {	// 接收事件参数对象 event,简写为 e
            const nowBgColor = e.target.style.backgroundColor
            e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
            this.count += 1
        }
    }
绑定事件并传参

在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参,示例代码如下:

	<h3>count 的值为:{{count}}</h3>
    <button @click="addNewCount(2)">+2</button>
    // ----------------- 分割线 ------------------
    methods: {
        // 在形参处用 step 接收传递过来的参数值
        addNewCount(step) {
            this.count += step
        }
    }
$event

$event 是 vue 提供的特殊变量,用来表示原生的事件参数对象 event。$event 可以解决事件参数对象 event 被覆盖的问题。示例用法如下:

	<h3>count 的值为:{{count}}</h3>
    <button @click="addNewCount(2, $event)">+2</button>
    // ----------------- 分割线 ------------------
    methods: {
        // 在形参处用 e 接收传递过来的原生事件参数对象 $event
        addNewCount(step, e) {
            const newBgColor = e.target.style.backgroundColor
            e.target.style.backgroundColor = newBgColor === 'red' ? '' : 'red'
            this.count += step
        }
    }	
事件修饰符

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

事件修饰符说明
.prevent阻止默认行为(例如:阻止 a 链接的跳转、阻止表单的提交等)
.stop阻止事件冒泡
.capture以捕获模式触发当前的事件处理函数
.once绑定的事件只触发一次
.self只有在 event.target 是当前元素自身时触发事件处理函数

语法格式如下:

    <!-- 触发 click 点击事件时,阻止 a 链接的默认跳转行为 -->
    <a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>

按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:

    <!-- 只有在 ‘key’ 是 'Enter' 时调用 `vm.submit()` -->
    <input type="text" @keyup.enter="submit">

    <!-- 只有在 `key` 是 `Esc` 时调用 `vm.clearInput()`  -->
    <input type="text" @keyup.esc="clearInput">

1.4 双向绑定指令

vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在 不操作 DOM 的前提下,快速获取表单的数据

    <p>用户名是: {{username}}</p>
    <input type="text" v-model="username">

    <p>选中的省份是:{{province}}</p>
    <select name="" id="" v-model="province">
        <option value="">请选择</option>
        <option value="1">北京</option>
        <option value="2">河北</option>
        <option value="3">黑龙江</option>
    </select>

v-model 指令的修饰符

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

修饰符作用示例
.number自动将用户的输入值转为数值类型<input v-model.number=“age”/>
.trim自动过滤用户输入的首尾空白字符<input v-model.trim=“msg”/>
.lazy在 ”change“ 是而非 ”input“ 时更新<input v-model.lazy=“msg”/>
1.5 条件渲染指令

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

  • v-if
  • v-show
v-if 和 v-show 的区别

实现原理不同:

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

性能消耗不同:

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销

  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行是条件很少改变,则使用 v-if 较好
v-else

v-if 可以单独使用,或配合 v-else 指令一起使用:

    <div v-if="Math.random() > 0.5">
        随机数大于 0.5
    </div>
    <div>
        随机数小于或等于 0.5
    </div v-else>

v-else-if

v-else-if 指令,顾名思义,充当 v-if 的 “else-if 块“,可以连续使用:

    <div v-if="type === 'A'">优秀</div>
    <div v-else-if="type === 'B'">良好</div>
    <div v-else-if="type === 'C'">一般</div>
    <div v-else></div>

1.6 列表渲染指令

vue 提供了 v-for 指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构

v-for 指令需要使用 item in items 的特殊语法,其中:

  • items 的待循环的数组
  • item 是当前的循环项
	data: {
        list: [ // 列表数据
            {id: 1, name: 'zs'},
            {id: 2, name: 'ls'}
        ]
    }
	// -------------- 分割线 ----------------
	<ul>
    	<li v-for="item in list">姓名是: {{item.name}}</li>    
    </ul>
v-for 中是索引

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

	data: {
		list: [
            {id: 1, name: 'zs'},
            {id: 2, name: 'ls'}
        ]
    }
	// -------- 分割线 -------
	<ul>
    	<li v-for="(item, index) in list">索引是:{{index}}, 姓名是:{{item.name}}</li>    
    </ul>

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

使用 key 维护列表的状态

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

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

    <!-- 用户列表区域 -->
    <ul>
        <!-- 加 key 属性的好处: -->
        <!-- 1. 正确维护列表的状态 -->
        <!-- 2. 复用现有的 DOM 元素,提升渲染的性能 -->
        <li v-for="user in userlist" :key="user.id">
            <input type="checkbox">
            姓名:{{user.name}}
        </li>
    </ul>

key 的注意事项
  1. key 的值只能是字符串或者数字类型
  2. key 的值必须具有唯一性(即:key 的值不能重复)
  3. 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
  4. 使用index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性)
  5. 建议使用 v-for 指令是一定要指定 key 的值(既提升性能、有防止列表状态紊乱)

2. 过滤器

过滤器Filters)常用于文本的格式化。例如:

hello -> Hello

过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:

    <!-- 在双花括号中通过"管道符"调用 capitalize 过滤器,对 message 的值进行格式化 -->
    <p> {{ message | capitalize }} </p>

    <!-- 在 v-bind 中通过“管道符”调用 formatId 过滤器,对 rawId 的值进行格式化 -->
    <div v-bind:id="rawId | formatId"></div>

过滤器可以用在两个地方:插值表达式v-bind 属性绑定

2.1 定义过滤器

在创建 vue 实例期间,可以在 filters 节点中定义过滤器,示例代码如下:

    const vm = new Vue({
        el: '#app',
        data: {
            message: 'hello vue.js',
            info: 'title info'
        },
        filters: {              // 在 filters 节点下定义“过滤器”
            capitalize (str) {  // 把首字母转为大写的过滤器
                return str.charAt(0).toUpperCase + str.slice(1)
            }
        }
    })

2.2 私有过滤器全局过滤器

在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用

如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器

    // 全局过滤器 - 独立于每个 vm 实例之外
    // Vue.filter() 方法接收两个参数:
    // 第一个参数:是全局过滤器的“名字”
    // 第二个参数,是全局过滤器的“处理函数”

    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase + str.slice(1) + '~~'
    })

2.3 连续调用多个过滤器

示例代码如下:

    // 串联调用多个过滤器
    <p> {{text | capitalize | maxLength }} </p>

    // 全局过滤器 - 首字母大写
    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
    })

    // 全局过滤器 - 控制文本的最大长度
    Vue.filter('maxLength', (str) => {
        if (str.length <= 10) return str
        return str.slice(1, 10) + '...'
    })

2.4 过滤器传参

过滤器的本质JavaScript 函数,因此可以接收参数,格式如下:

    // arg1 和 arg2 是传递给 filterA 的参数
    <p> {{message | filterA(arg1, arg2)}} </p>

    // 过滤器处理函数的形参列表中:
    // 第一个参数:永远都是“管道符”前面待处理的值
    // 从第二参数开始,才是调用过滤器是传递过来的 arg1 和 arg2 参数
    Vue.filter('filterA', (msg, arg1, arg2) => {
        // 过滤器的代码逻辑...
    })

示例代码如下:

	// 调用 maxLength 过滤器时传参
    <p> {{text | capitalize | maxLength(5) }} </p>

    // 全局过滤器 - 首字母大写
    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
    })

    // 全局过滤器 - 控制文本的最大长度
    Vue.filter('maxLength', (str, len = 10) => {
        if (str.length <= len) return str
        return str.slice(1, len) + '...'
    })
2.5 过滤器的兼容性

过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x 的版本中 剔除了过滤器相关的功能。

在企业级项目的开发中:

  • 如果使用的是 2.x 版本的 vue 则依然可以使用过滤器相关的功能
  • 如果项目已经升级到了 3.x 版本的 vue,官方建议使用计算机属性方法代替被剔除的过滤器功能

总结

  1. 能够真的 vue 的基本使用步骤
    • 导入 vue.js 文件
    • new Vue() 构造函数,得到 vm 实例对象
    • 声明 el 和 data 数据节点
    • MVVM 的对应关系
  2. 掌握 vue 中常见的指令的基本用法
    • 插值表达式、v-bind、v-on、v-if 和 v-else
    • v-for 和 :key、v-model
  3. 掌握 vue 中过滤器的基本用法
    • 全局过滤器 Vue.filter(‘过滤器名称’, funciton)
    • 私有过滤器 filters 节点

组件基础(上)

单页面应用程序

1. 什么是单页面应用程序

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

2. 单页面应用程序的特点

单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源(HTML、JavaScript 和 CSS)。

一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态的变换 HTML 内容,从而实现页面与用户的交互

3. 单页面应用程序的优点

SPA 单页面应用程序最显著的3个优点如下:

  1. 良好的交互体验
    • 单页应用的内容的改变不需要重新加载整个页面
    • 获取数据也是通过Ajax 异步获取
    • 没有页面之间的跳转,不会出现“白屏现象”
  2. 良好的前后端工作分离模式
    • 后端专注于提供 API 接口,更易实现 API 接口的复用
    • 前端专注于页面的渲染,更利于前端工程化的发展
  3. 减轻服务器的压力
    • 服务器只提供数据,不负责页面的合成与逻辑的处理,吞吐能力会提高几倍
4. 单页面应用程序的缺点

任务一种技术都有自己的局限性,对于 SPA 单页面应用程序来说,主要的缺点有如下两个:

  1. 首屏加载慢
    • 路由懒加载
    • 代码压缩
    • CDN 加速
    • 网络传输压缩
  2. 不利于 SEO
    • SSR 服务器端渲染

5. 如何快速创建 vue 的 SPA 项目

vue 官方提供了两种快速创建工程化的 SPA 项目的方式:

  1. 基于 vite 创建 SPA 项目
  2. 基于 vue-cli 创建 SPA 项目
vitevue-cli
支持的 vue 版本仅支持 vue3.x支持 3.x 和 2.x
是否基于 webpack
运行速度较慢
功能完整度小而巧(逐渐完善)大而全
是否建议在企业级开发中使用目前不建议建议在企业级开发中使用

vite 的基本使用

1. 创建 vite 的项目

按照顺序执行如下的命令,即可基于 vite 创建 vue 3.x 的工程化项目:

	npm init vite-app 项目名称
    
    cd 项目名称
    npm install
    npm run dev f

2. 梳理项目的结构

使用 vite 创建的项目结构如下:

其中:

  • node_modules 目录用来存放第三方依赖包
  • public 是公共的静态资源目录
  • src 是项目的源代码目录(程序员写的所有代码都要放在此目录下)
  • .gitignore 是 Git 的忽略文件
  • index.html 是 SPA 单页面应用程序中唯一的 HTML 页面
  • package.json 是项目的包管理配置文件

在 src 这个项目源代码目录之下,包含了如下的文件和文件夹:

其中:

  • assets 目录用来存放项目中所有的静态资源文件(CSS,fonts等)
  • components 目录用来存放项目中的所有的自定义组件
  • App.vue 是项目的根组件
  • index.css 是项目的全局样式表文件
  • main.js 是整个项目的打包入口文件

3. vite 项目的运行流程

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

其中:

  1. App.vue 用来编写待渲染的模板结构
  2. index.html 中需要预留一个 el 区域
  3. main.js 把 App.vue 渲染到了 index.html 所预留的区域中
3.3 在 main.js 中进行渲染

按照 vue 3.x标准用法,把 APP.vue 中的模板内容渲染到 index.html 页面的 el 区域中:

	// 1. 从 vue 中按需导入 createApp 函数
	//creatApp 函数的作用:创建 vue 的“单页面应用程序实例”
	import { createApp } from 'vue'
	// 2. 导入待渲染的 App 组件
	import App from './App.vue'

	// 3. 调用 createApp() 函数,返回值是“单页面应用程序的实例”,用常量 spa_app 进行接收,
	// 	  同时把 App 组件作为参数传递给 createApp 函数,表示要把 App 渲染到 index.html 页面上
	const spa_app = createApp(App)
	// 4. 调用 spa_app 实例的 mount 方法,用来指定 vue 实际要控制的区域
	spa_app.mount('#app')

组件开发思想

1. 什么是组件化开发

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

例如:http://www.ibootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。用户可以通过拖拽组件的方式,快速生成一个页面的布局结构。

2. 组件化开发的好处

前端组件化开发的好处主要体现在以下两方面:

  • 提高了前端代码的复用性灵活性
  • 提升了开发效率和后期的可维护性

3. vue 中的组件开发

vue 是一个完全支持组件化开发的框架。vue 中规定组件的后缀名.vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。

vue 组件的构成

1. vue 组件组成结构

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

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

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

2. 组件的 template 节点

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

	<template>
  		<!-- 当前组件的 DOM 结构,需要定义到 template 标签的内部 --> 
	</template>

注意:<template> 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素。

2.1 在 template 中使用指令

在组件的<template> 节点中,支持使用前面所学的指令语法,来辅助开发者渲染当前组件的 DOM 结构。

代码示例如下:

  <template>
    <h1>这是 App.vue 根组件</h1>

    <!-- 使用 {{  }} 插值表达式 -->
    <p>生成一个随机数字:{{ (Math.random() * 10).toFixed(2) }}</p>
    <!-- 使用 v-bind 属性绑定 -->
    <p :title="new Date().toLocaleTimeString()">我在黑马程序员学习</p>
    <!-- 属性 v-on 事件绑定 -->
    <button @click="showInfo">按钮</button>
  </template>

2.2 在 template 中定义根节点

vue 2.x 的版本中,<template> 节点内的 DOM 结构仅支持单个根节点

	<template>
  		<!-- vue 2.x 中,template 节点内的所有元素,最外层“必须有”唯一的根节点进行包裹,否则报错 -->
  		<h1>这是 App.vue 根组件</h1>
  		<h2>这是副标题</h2>
	</template>

3. 组件的 script 节点

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

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

  <script>
    // 今后,组件相关的 data 数据、methods 方法等,
    // 都需要定义到 export default 所导出的对象中。
    export default {}
  </script>

3.1 script 中的 name节点

可以通过 name 节点为当前组件定义一个名称:

	<script>
		export default {
            // name 属性指向的是当前组件的名称(建议: 每个单词的首字母大写)
            name: 'MyApp',
        }	
	</script>

在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件

3.2 script 中的 data 节点

vue 组件渲染期间需要用到的数据,可以定义在 data 节点中:

  <script>
    // 今后,组件相关的 data 数据、methods 方法等,
    // 都需要定义到 export default 所导出的对象中。
    export default {
      // 组件的名称
      name: 'MyApp',
      // 组件的数据(data 方法中 return 出去的对象,就是当前组件渲染期间需要用到的数据对象)
      data() {
        return {
          username: '娃哈哈'
        }
      }
    }
  </script>
3.3 script 中的 methods 节点

组件中的事件处理函数,必须定义到 methods 节点中,示例代码如下:

  <script>
    // 今后,组件相关的 data 数据、methods 方法等,
    // 都需要定义到 export default 所导出的对象中。
    export default {
      // 组件的名称
      name: 'MyApp',
      // 组件的数据(data 方法中 return 出去的对象,就是当前组件渲染期间需要用到的数据对象)
      data() {
        return {
          count: 0
        }
      },
      methods: { // 处理函数
        addCount() {
          this.count++
        }
      }
    }
  </script>

4. 组件的 style 节点

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

<style lang="css">
  h1 {
    font-weight: normal;
  }
</style>

其中 <style> 标签上的 lang="css" 属性是可选的,它表示所使用的样式语言。默认只支持普通的 css 语法,可选值还有 less、scss等。

4.1 让 style 中支持 less 语法

如果希望使用 less 语法编写组件的 style 样式,可以按照如下两个步骤进行配置:

  1. 运行 npm install less -D 命令安装依赖包,从而提供 less 语法的编译支持
  2. 在 <style> 标签上添加 lang="less" 属性,即可使用 less 语法编写组件的样式
<style lang="less">
  h1 {
    font-weight: normal;
    i {
      color: red;
      font-style: normal;
    }
  }
</style>

组件的基本使用

组件之间可以进行相互的引用,例如:

在这里插入图片描述

vue 中组件的引用原则:先注册后使用

1.1 注册组件的两种方式

vue 中注册组件的方式分为“全局注册”和”局部注册“两种,其中:

  • 全局注册的组件,可以在全局任何一个组件内使用
  • 局部注册的组件,只能在当前注册的范围内使用

在这里插入图片描述

1.2 全局注册组件
// 1. 按需导入 createApp 函数
import { createApp } from 'vue'
// 2. 导入待渲染的 App.vue 组件
import App from './App.vue'
// (1) 导入 Swiper 和 Test 两个组件
import Swiper from './components/MySwiper.vue'
import Test from './components/MyTest.vue'

// 3. 调用 createApp 函数 创建 SPA 应用的实例
const app = createApp(App)

// (2) 调用 app 实例的component() 方法,在全局注册 my-swiper 和 my-test 两个组件
app.component('my-swiper', Swiper)
app.component('my-test', Test)

// 4. 调用 mount() 把 app 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')

1.3 使用全局注册组件

使用 app.component() 方法注册的全局组件,直接以标签的形式进行使用即可,例如:

	// 在 main.js 中,注册了 my-swiper 和 my-test 两个全局组件
	spa_app.component('my-swiper', Swiper)
	spa_app.component('my-test', Test)

	<!-- 在其他组件中,直接以标签的形式,使用刚才注册的全局组件即可 -->
	<template>
		<h1>
            这是 App 根组件
        </h1>
		<hr/>
		<my-swiper></my-swiper>
		<my-test></my-test>
	</template>
1.4 局部注册组件
	<template>
		<h1>
            这是 App 根组件
        </h1>
		<hr/>
		<my-swiper></my-swiper>
		<my-test></my-test>
	</template>

	<script>
		import Search from './component/MySearch.vue'
        export default {
            components: { // 通过 components 节点,为当前的组件组件私有子组件
                'my-search': Search,
            }
        }
	</script>

1.5 全局注册和局部注册的区别
  • 全局注册的组件,可以在全局任何一个组件内使用
  • 局部注册的组件,只能在当前注册的范围内使用

应用场景:

如果某些组件在开发期间的使用频率很高,推荐进行全局注册;

如果某些组件只在特定的情况下会被用到,推荐进行局部注册。

1.6 组件注册时名称的大小写

在进行组件的注册时,定义组件注册名称的方式有两种:

  1. 使用 kebab-case 命名法(俗称短横线命名法,例如 my-swiper 和 my-search)
  2. 使用 PascalCase 命名法(俗称帕斯卡命名法大驼峰命名法,例如 MySwiper 和 MySearch)

短横线命名法的特点:

  • 必须严格按照短横线名称进行使用

帕斯卡命名法的特点:

  • 既可以严格按照帕斯卡名称进行使用,又可以转化为短横线名称进行使用

注意:在实际开发中,推荐使用帕斯卡命名法为组件注册名称,因为它的适用性更强

1.7 通过 name 属性注册组件

在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的 name 属性作为注册后组件的名称,示例代码如下:

	<template>
		<h3>
            轮播图组件
        </h3>
	</template>

	<script>
		exports default {
        	name: 'MySwiper'  // name 属性为当前组件的名字
        }
	</script>

        import Swiper from './components/MySwiper.vue'
	app.component(Swiper.name, Swiper) // 相当于 app.component('MySwiper', Swiper)

2. 组件之间的样式冲突问题

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

  1. 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
  2. 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
2.1 思考:如何解决组件样式冲突的问题

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

	<template>
		<div class="container">
            <h3>轮播图组件</h3>
        </div>
	</template>

	<style>
		/* 通过中括号“属性选择器”,来防止组件之间的样式冲突问题,
           因为每个组件分配的自定义属性是“唯一的” */
        .container[data-v-001] {
            border: 1px solid red;
        }
	</style>
2.2 style 节点的 scoped 属性

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

	<template>
		<div class="container">
            <h3>轮播图组件</h3>
        </div>
	</template>

	<style scoped>
		/* style 节点的 scoped 属性 用来自动为每个组件分配唯一的“自定义属性”,
           并自定为当前组件的 DOM 标签 和 style 样式应用这个自定义属性,防止组件的样式冲突问题 */
        .container {
            border: 1px solid red;
        }
	</style>
2.3 /deep/ 样式穿透

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

	<style lang="less" scoped>
        .title {
            color: bule; /* 不加 :deep() 时,生成的选择器格式为 .title[data-v-052242de] */
        }
        
        :deep(.title) {
            color: bule; /* 加上 :deep() 时,生成的选择器格式为 [data-v-052242de] .title */
        }
	</style>

3. 组件的 props

为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:

  • 组件的 DOM 结构style 样式要尽量复用
  • 组件中要展示的数据,尽量由组件的使用者提供

为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。

3.1 什么是组件的 props

props 是组件的自定义属性,组件的使用者可以通过 props 把数据传递到子组件内部,供子组件内部进行使用。示例代码如下:

	<!-- 通过自定义 props,把文章的标题和作者,传递到 my-article 组件中 -->
	<my-article title="面朝大海,春暖花开" author="海子"></my-article>

props 的作用:父组件通过 props 向子组件传递要展示的数据。

props的好处:提高了组件的复用性

3.2 在组件中生命 props

在封装 vue 组件时,可以把动态的数据项声明为 props 自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。示例代码如下:

	<template>
		<h3>
            标题: {{title}}
        </h3>
		<h5>
            作者:{{author}}
        </h5>
	</template>

	<script>
		export default {
            props: ['title', 'author'], // 父组件传递给 my-article 组件的数据,必须在 props 节点中声明
        }
	</script>
3.3 无法使用未声明的 props

如果父组件给子组件传递了未声明的 props 属性,则这些属性会被忽略,无法被子组件使用,示例代码如下:

	<my-article title="致橡树" author="舒婷"></my-article>

	<template>
		<h3>
            标题: {{title}}
        </h3>
		<h5>
            作者:{{author}}
        </h5>
	</template>

	<script>
		export default {
            props: ['title'], // author 属性没有声明,因此组件中无法访问到 author 的值
        }
	</script>
3.4 动态绑定 props 的值

可以使用 v-bind 属性绑定的形式,为组件动态绑定 props 的值,示例代码如下:

	<!-- 通过 v-bind 属性绑定,为 title 动态赋予一个变量的值 -->
	<!-- 通过 v-bind 属性绑定,为 author 动态赋予一个表达式的值 -->
	<my-article :title="info.title" :author="'post by' + info.author"></my-article>
3.5 props 的大小写命名

组件中如果使用 “camelCase驼峰命名法)”声明了 props 属性的名称,则有两种方式为其绑定属性的值:

	<template>
		<p>
            发布时间: {{pubTime}}
        </p>
	</template>

	<script>
		export default {
            props: ['pubTime'], // 采用“驼峰命名法”为当前的组件声明了 pubTime 属性
        }
	</script>

	<!-- 既可以直接使用 “驼峰命名法” 的形式为组件绑定属性的值 -->
	<my-article pubTime="1989"></my-article>
	<!-- 也可以使用其等价的 “短横线分隔命名” 的形式为组件绑定属性的值 -->
	<my-article pub-time="1989"></my-article>

4. Class 与 Style 绑定

在实际开发中经常会遇到动态操作元素样式的需求。因此,vue 允许开发者通过 v-bind 属性绑定指令,为元素动态绑定 class 属性的值行内的 style 样式

4.1 动态绑定 HTML 的class

可以通过三元表达式,动态的为元素绑定 class 的类名。示例代码如下:

	<h3 class="thin" :class="isItalic ? 'italic' : ''">MyDeep 组件</h3>
    <button @click="isItalic=!isItalic">Toggle Italic</button>	
	
	data() {
        return { isItalic: true }
    }

	.thin { // 字体变细
        font-weight: 200;
    }

    .italic { // 倾斜字体
        font-style: italic;
    }
4.2 以数组语法绑定 HTML 的class

如果元素需要动态绑定多个 class 的类名,此时可以使用数组的语法格式

    <h3 class="thin" :class="[isItalic ? 'italic' : '', isDelete ? 'delete' : '']">MyDeep 组件</h3>
    <button @click="isItalic=!isItalic">Toggle Italic</button>	
    <button @click="isDelete=!isDelete">Toggle Delete</button>	

    data() {
        return {
            isItalic: true,
            isDelete: false,
        }
    }

4.3 以对象语法绑定 HTML 的 class

使用数组语法动态绑定 class 会导致模板结构臃肿的问题。此时可以使用对象语法进行简化

	<h3 class="thin" :class="classObj">MyDeep 组件</h3>
    <button @click="classObj.italic = !classObj.italic">Toggle Italic</button>	
    <button @click="classObj.delete = !classObj.delete">Toggle Delete</button>	
delete
    data() {
        return {
            classOjb: { // 对象中,属性名是 class 类名,值是布尔值
				isItalic: true,
            	isDelete: false,
			}
        }
    }
4.4 以对象语法绑定内联的 style

:style对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式(camelCase)或短横线分隔(kebab-case,记得用引号括起来)来命名

    <div :style="{color: active, fontSize: fsize + 'px', 'background-color': bgcolor}">

    </div>
    <button @click="fsize += 1">字号 + 1</button>
    <button @click="fsize -= 1">字号 - 1</button>

    data() {
        return {
            active: 'red',
            fsize: 30,
            bgcolor: 'pink',
        }
    }

总结

  1. 能够说出什么是单页面应用程序及组件化开发
    • SPA、只有一个页面、组件是对 UI 结构的复用
  2. 能够说出 .vue 单文件组件的组成部分
    • template、script、style(scoped、lang)
  3. 能够知道如何注册 vue 组件
    • 全局注册(app.component)、局部注册(components
  4. 能够知道如何声明组件的 props 属性
    • props 数组
  5. 能够知道如何在组件中进行样式绑定
    • 动态绑定 class、动态绑定 style

组件基础(下)

props 验证

1. 什么是 props 验证

指的是:在封装组件时对外界传递过来的 props 数据进行合法性的校验,从而防止数据不合法的问题。

在这里插入图片描述

使用数组类型的 props 节点的缺点:无法为每个 props 指定具体的数据类型

2. 对象类型的 props 节点

使用对象类型的 props 节点,可以对每个 prop 进行数据类型的校验,示意图如下:

在这里插入图片描述

3. props 验证

对象类型的 props 节点提供了多种数据验证方案,例如:

  1. 基础的类型检查
  2. 多个可能的类型
  3. 必填项校验
  4. 属性默认值
  5. 自定义验证函数
3.1 基础的类型检查

可以直接为组件 prop 属性指定基础的校验类型,从而防止组件的使用者为其绑定错误类型的数据

	export default {
		props: {
			propA: String,   // 字符串类型
            propB: Number,   // 数字类型
            propC: Boolean,  // 布尔值类型
            propD: Array,    // 数组类型
            propE: Object,   // 对象类型
            propF: Date,     // 日期类型
            propG: Function, // 函数类型
            propH: Symbol    // 符号类型
		}
    }
3.2 多个可能的类型

如果某个 prop 属性值的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型,示例代码如下:

	export default {
		props: {
			// propA 属性的值可以是“字符串”或“数字”
			propA: [String, Number],
		}
	}
3.3 必填项校验

如果组件的某个 prop 属性是必填项,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项:

	export default {
        props: {
            // 通过“配置对象”的形式,来定义 propB 属性的“验证规则”
            propB: {
                type: String,  // 当前属性的是必须是 String 字符串类型
                required: true // 当前属性的值是必须项,如果使用者没指定 propB 属性的值,则在终端进行警告提示
            }
        }
    }		
3.4 属性默认值

在封装组件时,可以为某个 prop 属性指定默认值。示例代码如下:

	export default {
        props: {
            // 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
            propC: {
                type: Number,
                default: 100  // 如果使用者没有指定 propC 的值,则 propC 属性的默认值为 100
            }
        }
    }
3.5 自定义验证函数

在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制

	export default {
            // 通过“配置对象”的形式,来定义 propD 属性的“验证规则”
            propD: {
                // 通过 validator 函数,对 propD 属性的值进行检验,“属性的值”可以通过形参 value 进行接收
                validator(value) {
                    // propD 属性的值,必须匹配下列字符串中的一个
                    // validator 函数的返回值为 true 表示验证通过,false 表示验证失败
                    return ['success', 'warning', 'danger'].indexOf(value) !== -1;
                }
            }
        },
    }

计算属性

1. 什么是计算属性

计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值供组件渲染 DOM 是使用。

2. 如何声明计算属性

计算属性需要以 function 函数的形式声明到组件的 computed 选项中,示例代码如下:

	<input type="text" v-model.number="count">
    <p>{{ count }} 乘以 2 的值为:{{ plus }}</p>

	export default {
        data() {
            return { count: 1 }
        },
        computed: {
            plus() { // 计算属性:监听 data 中 count 值的变化,自动计算出 count * 2 之后的新值
                return this.count * 2
            }
        }
    }

3. 计算属性的使用注意点

  1. 计算属性必须定义在 computed 节点中
  2. 计算属性必须是一个 function 函数
  3. 计算属性必须有返回值
  4. 计算属性必须当作普通属性使用

4. 计算属性 vs 方法

相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行运算,因此计算属性的性能更好:

	computed: {
        plus() { // 计算属性的计算结果会被缓存,性能好
            console.log('计算属性被执行了')
            return this.count * 2
        }
    }
	methods: {
        plus() { // 方法的计算结果无法被缓存,性能低
            console.log('方法被执行了')
            return this.count * 2
        }
    }

自定义事件

1. 什么是自定义事件

在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件

在这里插入图片描述

2. 自定义事件的 3 个使用步骤

在封装组件时:

  1. 声明自定义事件
  2. 触发自定义事件

在使用组件时:

  1. 监听自定义事件
2.1 声明自定义事件

开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明,实例代码如下:

    <template>
        <h3>Counter组件</h3>
        <button>+1</button>
    </template>

	export default {
		// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
        emits: ['change'],
    }
2.2 触发自定义事件

emits 节点下声明的自定义事件,可以通过 this.$emit(‘自定义事件的名称’) 方法进行触发,示例代码如下:

	<template>
        <h3>Counter组件</h3>
        <button @click="onBthClick">+1</button>
    </template>

	export default {
		// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
        emits: ['change'],
        methods: {
            onBtnClick() {
                this.$emit('change') // 当点击 + 1 按钮时,调用 this.$emit() 方法,触发自定义的 change 事件
            }
        }
    }
2.3 监听自定义事件

在使用自定义的组件时,可以通过 v-on 的形式监听自定义事件。示例代码如下:

	<!-- 使用 v-on 指令绑定事件监听 -->	
	<my-counter @change="getCount"></my-counter>

	methods: {
            getCount() {
                console.log('监听到了 count 值的变化!') 
            }
        }

3. 自定义事件传参

在调用 this.$emit()方法触发自定义事件时,可以通过第二个参数为自定义事件传参,示例代码如下:

	<template>
        <h3>Counter组件</h3>
        <button @click="onBthClick">+1</button>
    </template>

	export default {
		// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
        emits: ['change'],
        methods: {
            onBtnClick() {
                this.$emit('change', this.count) // 触发自定义事件时,通过第二个参数传参
            }
        }
    }

组件上的 v-model

1. 为什么需要在组件上使用 v-mode

v-model 是双向数据绑定指令,当需要维护组件内外数据同步时,可以在组件上使用 v-model 指令。示意图如下:

在这里插入图片描述

  • 外界数据的变化自动同步到 counter 组件中
  • counter 组件中数据的变化,也会自动同步到外界

2. 在组件上使用 v-model 的步骤

在这里插入图片描述

  1. 父组件通过v-bind:属性绑定的形式,把数据传递给子组件
  2. 子组件中,通过 props 接收父组件传递过来的数据

总结

  1. 能够知道如何对 props 进行验证
    • 数组格式,对象格式
    • typedefaultrequired、validator
  2. 能够知道如何使用计算属性
    • computed 节点、必须return一个结果、缓存计算结果
  3. 能够知道如何为组件绑定自定义事件
    • v-on 绑定自定义事件、emits$emit()
  4. 能够知道如何在组件上使用 v-model
    • 应用场景:实现组件内外的数据同步
    • v-model: props名称、emits、$emit(‘update:props名称’)

组件高级(上)

watch 侦听器

1. 什么是 watch 侦听器

watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发起请求,判断用户名是否可用。

2. watch 侦听器的基本语法

开发者需要在 watch 节点下,定义自己的侦听器。示例代码如下:

	export default {
		data() {
            return { username: ''}
        },
        watch: {
            // 监听 username 的值的变化,
            // 形参列表中,第一个值是“变化后的新值”,第一个值是“变化之前的旧值”
            username(newVal, oldVal) {
                console.log(newVal, oldVal)
            }
        }
    }

3. 使用 watch 检测用户名是否可用

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

	import axios from 'axios'
	export default {
		data() {
            return { username: ''}
        },
        watch: {
			async username(newVal, oldVal) {
                const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${newVal}`)
                console.log(res)
            }
        }
    }

4. immediate 选项

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

	watch: {
        username: {
            async handler (newVal, oldVal) {
                const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal }`);
                console.log(res);
            },
            // 立即触发 watch 侦听器
            immediate: true,
        }
    }

5. deep 选项

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

	data () {
        return {
            info: {
                username: 'zs',  // info 中包含 username 属性
            }
        }
    },
    watch: {
        info: { // 直接监听 info 对象的变化
            async handler (newVal) {
                const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal.username }`);
                console.log(res);
            },
            deep: true, // 需要使用 deep 选项,否则 username 值的变化无法被监听到
        }
    }

6. 监听对象单个属性的变化

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

	data () {
        return {
            info: {
                username: 'zs',  // info 中包含 username 属性
            }
        }
    },
    watch: {
        'info.username': { // 只想监听 info.username 属性值的变化
            async handler (newVal) {
                const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${ newVal.username }`);
                console.log(res);
            },
            deep: true, // 需要使用 deep 选项,否则 username 值的变化无法被监听到
        }
    }

7. 计算属性 vs 侦听器

计算属性和侦听器侧重的应用场景不同

计算属性侧重于监听多个值的变化,最终计算并返回一个新值

侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任务返回值

组件的生命周期

1. 组件运行的过程

开始
import
导入组件
components
注册组件
以标签形式
使用组件
在内存中创建
组件的实例对象
把创建的组件实例
渲染到页面上
组件切换时
销毁需要被
隐藏的组件
结束

组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段

2. 如何监听组件的不同时刻

vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着这组件的运行而自动调用。例如:

  1. 当组件在内存中被创建完毕之后,会自动调用 created 函数
  2. 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数
  3. 当组件被销毁完毕之后,会自动调用 unmounted 函数

3. 如何监听组件的更新

当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源保持一致。

当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。

4. 组件中主要的生命周期函数

生命周期函数执行时机所属阶段执行次数应用场景
created组件在内存中创建完毕后创建阶段唯一1次发 Ajax 请求初始数据
mounted组件初次在页面中渲染完毕后创建阶段唯一1次操作 DOM 元素
updated组件咋页面被重新渲染完毕后运行阶段0 或 多次-
unmounted组件被销毁后(页面和内存)销毁阶段唯一1次-

注意:在实际开发中,created最常用的生命周期函数!

5. 组件中全部的生命周期函数

生命周期函数执行时机所属阶段执行次数应用场景
beforeCreate在内存中开始创建组件之前创建阶段唯一1次-
created组件在内存中创建完毕后创建阶段唯一1次发 Ajax 请求初始数据
beforeMount在把组件初次渲染到页面之前创建阶段唯一1次-
mounted组件初次在页面中渲染完毕后创建阶段唯一1次操作 DOM 元素
beforeUpdate在组件被重新渲染之前运行阶段0 或 多次-
updated组件咋页面被重新渲染完毕后运行阶段0 或 多次-
beforeUnmount在组件被销毁之前销毁阶段唯一1次-
unmounted组件被销毁后(页面和内存)销毁阶段唯一1次-

疑问:为什么不在 beforeCreate 中发 ajax 请求初始数据?

组件之间的数据共享

1. 组件之前的关系

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

  1. 父子关系
  2. 兄弟关系
  3. 后代关系

2. 父子组件之间的数据共享

2.1 父组件向子组件共享数据

父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。示例代码如下:

	// 父组件
	<my-test-1 :msg="message" :user="userinfo"></my-test-1>
	
	data() {
        return {
            message: 'hello vue',
            userinfo {
            	name: 'zs',
            	age: 20,
        	}
        }
    }
	// 子组件	
	<template>
		<h3>
            测试父子传值
        </h3>
		<p>
            {{msg}}
        </p>
		<p>
            {{user}}
        </p>
	</template>

	<script>
		export default {
            props: ['msg', 'user'],
        }
	</script>
2.2 子组件向父组件共享数据

子组件通过自定义事件的方式向父组件共享数据。示例代码如下:

	// 子组件
	<script>
		export default {
			emits: ['n1chage'], // 1. 声明自定义事件
			data() {
                return {n1:0},
            },
            methods: {
                addN1() {
                    this.n1++
                    // 2. 数据变化时,触发自定义的事件
                    this.$emit('n1change', this.n1)
                }
            }
		}
	</script>
	<template>
		// 父组件
		// 1. 监听子组件的自定义事件 n1change
		<my-test-1 @n1change="getn1"></my-test-1>
	</template>

	<script>
		export default {
            data() { return { n1FromSon: 0 } },
            methods: {
                getn1(n1) { // 通过形参,接收子组件传递过来的数据
                    this.n1FromSon = n1
                }
            }
        }
	</script>
2.3 父子组件之间数据的双向同步

父组件在使用子组件期间,可以使用 v-model 指令维护组件内数据的双向同步:

在这里插入图片描述

3. 兄弟组件之间的数据共享

兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。示意图如下:

在这里插入图片描述

3.1 安装 mitt 依赖包

在项目中运行如下的命令,安装 mitt 依赖包:

	npm install mitt
3.2 创建公共的 EventBus 模块

在项目中创建公共的 eventBus 模块如下:

	// eventBus.js
	// 导入 mitt 包
	import mitt from 'mitt'
	// 创建 EventBus 的实例对象
	const bus = mitt()

	// 将 EventBus 的实例对象共享出去
	export default bus
3.3 在数据接收方自定义事件

在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件。示例代码如下:

// 导入 eventBus.js 模块,得到共享的 bug 对象
import bus from './eventBus.js'
    export default {
        name: 'MyRight',
        data() {
            return {
                num: 0,
            }
        },
        created() {
            // 调用 bus.on() 方法注册一个自定义事件,通过事件处理函数的形参接收数据
            bus.on('countChange', (count) => {
                this.num = count
            })
        }
    }
3.4 在数据发送方触发事件

在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件。示例代码如下:

// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'
    export default {
        name: 'MyLeft',
        data() {
            return {
                count: 0,
            }
        },
        methods: {
            addCount() {
                this.count++
                bus.emit('countChange', this.count) // 调用 bus.emit() 方法触发自定义事件,并发送数据
            }
        }
    }

4. 后代关系组件之间的数据共享

后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide inject 实现后代关系组件之间的数据共享。

4.1 父节点通过 provide 共享数据

父节点的组件可以通过 provide 方法,对其子孙组件共享数据:

	<script>
		export default {
            data() {
                return {
                    color: 'red'  // 1. 定义“父组件”要向“子孙组件”共享的数据
                }
            },
            provide() { // 2. provide 函数 return 的对象中,包含了“要向子孙组件共享的数据”
                return {
                    color:this.color,
                }
            }
        }
	</script>
4.2 子孙节点通过 inject 接收数据

子孙节点可以使用 inject 数组,接收父级节点向下共享的数据。示例代码如下:

	<template>
		<h5>
            子孙组件 --- {{color}}
        </h5>
	</template>
	<script>
		export default {
            // 子孙组件:使用 inject 接收父节点向下共享的 color 数据,并在页面上使用
            inject: ['color'],
        }
	</script>
4.3 父节点对外共享响应式的数据

父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据。示例代码如下:

	import { computed } from 'vue'  // 1. 从 vue 中按需导入 computed 函数
	<script>
		export default {
            data() {
                return { color: 'red' }
            },
            provide() {
                return {
                    // 2. 使用 computed 函数,可以把要共享的数据“包装为”响应式的数据
                    color: computed(() => this.color),
                }
            }
        }
	</script>
4.4 子孙节点使用响应式的数据

如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用。示例代码如下:

	<template>
		<!-- 响应式的数据,必须以 .value 的形式进行使用 -->
		<h5>
            子孙组件 --- {{color.value}}
        </h5>
	</template>

	<script>
		export default {
            // 接收父节点向下共享的 color 数据,并在页面上使用
            inject: ['color'],
        }
	</script>

5. vuex

vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效清晰、且易于维护

6. 总结
  • 父子关系
    1. 父 -> 子 属性绑定
    2. 子 -> 父 事件绑定
    3. 父 <-> 子 组件上的 v-model
  • 兄弟关系
    4. EventBus
  • 后代关系
    5. provide & inject
  • 全局数据共享
    6. vuex

vue 3.x 中全局配置 axios

1. 为什么要全局配置 axios

在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:

  1. 每个组件中都需要导入 axios (代码臃肿)
  2. 每次发请求度需要填写完整的请求路径(不利用后期的维护)

在这里插入图片描述

2. 如何全局配置 axios

main.js 入口文件中,通过app.config.globalProperties 全局挂载 axios,示例代码如下:

在这里插入图片描述

利用全局挂载的 axios 发起 post 请求
	<script>
		export default {
            methods: {
                async postInfo() {
                    const { data: res } = await this.$http.post('/api/post', {name: 'zs', age: 20})4
                    console.log(res)
                }
            }
        }
	</script>
利用全局挂载的 axios 发起 get 请求
	<script>
		export default {
            methods: {
                async getInfo() {
                    const {data:res} = await this.$http.get('/api/get', {
                        params: {
                            name: 'ls', 
                            age: 23,
                        }
                    })
                }
            }
        }
	</script>

vuex

1. Vuex 概述

1.1 组件之间共享数据的方式

父向子传值:v-bind 属性绑定

子向父传值:v-on 事件绑定

兄弟组件之间共享数据:EventBus

  • $on 接收数据的那个组件
  • $emit 发送数据的那个组件
1.2 Vuex 是什么

Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。

在这里插入图片描述

1.3 使用 Vuex 统一管理状态的好处
  1. 能够在 vuex 中集中管理共享的数据,易于开发和后期维护
  2. 能够高效的实现组件之间的数据共享,提高开发效率
  3. 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步
1.4 什么样的数据适合存储到 Vuex 中

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

2. Vuex 的基本使用

1. 安装 vuex 依赖包

	npm install vuex --save

2. 导入 vuex 包

	import Vuex from 'vuex'
	Vue.use(Vuex)

3. 创建 store 对象

	const store = new Vuex.Store({
        // state 中存放的就是全局共享的数据
        state: { count: 0 }
    })

4. 将store 对象挂载到 vue 实例中

	new Vue({
        el: '#app',
        render: h => h(app),
        router,
        将创建的共享数据对象,挂载 到 vue 实例中
        所有的组件,就可以直接从 store 中获取全局的数据了
        store
    })
格式化文件 .prettierrc
	{
        "semi" :false,  // 不使用 ";" 符号
        "singleQuote": true // 将 "" 改为 ''
    }

3. Vuex 的核心概念

3.1 核心概念概述

Vuex 中的主要核心概念如下:

  • State
  • Mutation
  • Action
  • Getter
3.2 State

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

	// 创建 store 数据源,提供唯一的公共数据
	const store = new Vuex.Store({
        state: {
            count: 0,
        }
    })

组件访问 State 中数据的第一种方式

	this.$store.state.全局数据名称

组件访问 State 中数据的第二种方式:

	// 1. 从 vuex 中按需导入 mapState 函数
	import { mapState } from 'vuex'

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

	// 2. 将全局数据,映射为当前组件的计算属性
	computed: {
        ...mapState(['count'])	
    }
3.3. Mutation

Mutation 用于变更 Store 中的数据。

  1. 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据。
  2. 通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化
	// 定义 Mutation
	const store = new Vuex.Store({
        state: {
            count: 0
        },
        mutations: {
            add(state) {
                // 变更状态
                state.count++
            }
        }
    })
	// 触发 mutation
	methods: {
        handlel() {
            // 触发 mutations 的第一个方式
            this.$store.commit('add')
        }
    }

可以在触发 mutations 是传递参数:

	// 定义 Mutation
	const store = new Vuex.Store({
        state: {
            count: 0
        },
        mutations: {
            addN(state, step) {
                // 变更状态
                state.count += step
            }
        }
    })
	// 触发 Mutation
	methods: {
        handlel2() {
            // 在调用 commit 函数,
            // 触发 Mutations 时携带参数
            this.$store.commit('addN', 3		)
        }
    }

this.$store.commit() 是触发 mutations 的第一种方式,触发 mutations 的第二种方式:

	// 1. 从 vuex 中按需导入 mapMutations 函数
	import { mapMutations } from 'vuex'

通过刚才导入的 mapMutations 函数,将需要 的 Mutations 函数,映射为当前组件的 methods 方法:

	// 2. 将指定的 mutations 函数,映射为当前组件的 methods 函数
	methods: {
		...mapMutations(['add', 'addN']),
        handlel() {
            this.add();
        }
    }
3.4 Action

Action 用于处理异步任务。

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

	// 定义 Action
	const store = new Vuex.Store({
        // 省略其他代码 
        mutations: {
            state.count++
        },
        actions: {
            addAsync(context) {
                setTimeout(() => {
                    context.commit('add')
                }, 1000)
            }
        }
    })
	// 触发 Action
	methods: {
        handle() {
            // 触发 actions 的第一种方式
            this.$store.dispatch('addAsync')
        }
    }

触发 actions 异步任务是携带参数:

	// 定义 Action
	const store = new Vuex.Store({
        // 省略其他代码 
        mutations: {
            state.count++
        },
        actions: {
            addAsync(context, stpe) {
                setTimeout(() => {
                    context.commit('addN', step)
                }, 1000)
            }
        }
    })
	// 触发 Action
	methods: {
        handle() {
            // 在调用 dispatch 函数,
            // 触发 actions 是携带参数
            this.$store.dispatch('addAsync', 5)
        }
    }

this.$store.dispatch() 是触发 actions 的第一种方式,触发 actions 的第二种方式

	// 1. 从 vuex 中按需导入 mapActions 函数
	import { mapActions } from 'vuex'

通过刚才导入的 mapActions 函数,将需要的 actions 函数,映射为当前组件的 methods 方法:

	// 2. 将指定的 actions 函数,映射为当前组件的 methods 函数
	methods: {
		...mapActions(['addAsync', 'addNAsync'])
    }
3.5 Getter

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

  1. Getter 可以对Store 中已有的数据加工处理之后形成新的数据,类似于 Vue 的计算属性。
  2. Store 中数据发生变化,Getter 的数据也会跟着变化
	// 定义 Getter
	const store = new Vuex.Store({
        state: {
            count: 0
        },
        getters: {
            showNum: state => {
                return '当前最新的数量是【' + state.count + '】'
            }
        }
    })

使用 getters 的第一种方式

	this.$store.getters.名称

使用 getters 的第二种方式

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

总结

  1. 能够掌握 watch 侦听器的基本使用
    • 定义最基本的 watch 侦听器
    • immediatedeep、监听对象中单个属性的变化
  2. 能够知道 vue 中常用的生命周期函数
    • 创建阶段、运行阶段、销毁阶段
    • created、mounted
  3. 能够知道如何实现组件之间的数据共享
    • 父子组件兄弟组件、后代组件
  4. 能够知道如何在 vue3 的项目中全局配置 axios
    • main.js 入口文件中进行配置
    • app.config.globalProperties.$http = axios

组件高级(下)

ref 引用

1. 什么是 ref 引用

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

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

	<template>
		<h3>
            MyRef 组件
        </h3>
		<button @click="getRef">
            获取 $refs 引用
        </button>
	</template>

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

2. 使用 ref 引用 DOM 元素

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

	<template>
		<!-- 使用 ref 属性,为对应的 DOM 添加引用名称 -->
		<h3 ref="myh3">
            MyRef 组件
        </h3>
		<button @click="getRef">
            获取 $refs 引用
        </button>
	</template>

	<script>
		export default {
            methods: {
                getRef() {
                    // 通过 this.$refs.引用的名称 可以获取到 DOM 元素的引用
                    console.log(this.$refs.myh3)  
                    // 操作 DOM 元素,把文本颜色改为红色
                    this.$refs.myh3.style.color = 'red'
                }
            }
        }
	</script>

3. 使用 ref 引用组件实例

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

	<template>
		<!--  -->
		<my-counter ref="counterRef"></my-counter>
		<button>
            获取 $refs 引用
        </button>
	</template>

	<script>
		export default {
            methods: {
                getRef() {
                    // 通过 this.$refs.引用的名称 可以引用组件的实例
                    console.log(this.$refs.counterRef)  
                    // 引用到组件的实例之后,就可以调用组件上的 methods 方法
                    this.$refs.counterRef.add()
                }
            }
        }
	</script>
4. 控制文本框和按钮的按需切换

通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换。示例代码如下:

	<template>
		<input type="text" v-if="inputVisible">
		<button v-else @click="showInput">
            展示input输入框
        </button>
	</template>

	<script>
		export default {
            data() {
                return {
                    // 控制文本框和按钮的按需切换
                    inputVisible: false,
                }
            },
            methods: {
                showInput() { // 切换布尔值
                    this.inputVisible = true
                }
            }
        }
	</script>

5. 让文本框自动获得焦点

当文本框展示出来之后,如果希望它立即获得焦点,则可以为其添加 ref 引用,并调用原生 DOM 对象的 .focus() 方法即可。示例代码如下:

	<template>
		<input type="text" v-if="inputVisible" ref="ipt">
		<button v-else @click="showInput">
            展示input输入框
        </button>
	</template>

	<script>
		export default {
            data() {
                return {
                    // 控制文本框和按钮的按需切换
                    inputVisible: false,
                }
            },
            methods: {
                showInput() { // 切换布尔值
                    this.inputVisible = true
                    // 获取文本框的 DOM 引用,并调用 .focus() 使其自动获得焦点
                    this.$refs.ipt.focus()
                }	
            }
        }
	</script>

6. this.$nextTick(cb)方法

组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 异步重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素

	<template>
		<input type="text" v-if="inputVisible" ref="ipt">
		<button v-else @click="showInput">
            展示input输入框
        </button>
	</template>

	<script>
		export default {
            data() {
                return {
                    // 控制文本框和按钮的按需切换
                    inputVisible: false,
                }
            },
            methods: {
                showInput() { // 切换布尔值
                    this.inputVisible = true
                    // 把对 input 文本框的操作,推迟到下次 DOM 更新之后,否则页面上根本不存在文本框元素
                    this.$nextTick(() => {
                        this.$refs.ipt.focus()
                    })
                }	
            }
        }
	</script>

动态组件

1. 什么是动态组件

动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 <component> 组件,专门用来实现组件的动态渲染。

  1. <component> 是组件的占位符
  2. 通过 is 属性动态指定要渲染的组件名称
  3. <component is=“要渲染的组件的名称”></component>

2. 如何实现动态组件渲染

	<script>
		export default {
            data() {
                return {
                    // 1. 当前要渲染的组件的名称
                    comName: 'my-dynamic-1'
                }
            },
          
	</script>

	<template>
		<!-- 3. 点击按钮,动态切换组件的名称 --> 
		<button @click="comName='my-dynamic-1'">
            组件1
        </button>
		<button @click="comName='my-dynamic-2'">
            组件2
        </button>
		<!-- 2. 通过 is 属性,动态指定要渲染的组件的名称 -->
		<component :is="comName"></component>
	</template>

3. 使用 keep-alive 保持状态

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

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

插槽

1. 什么是插槽

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

在这里插入图片描述

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

2. 体验插槽的基础用法

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

	<template>
		<p>
            这是 MyCom1 组件的第一个 p 标签
        </p>
		<!-- 通过 slot 标签,为用户预留内容占位符(插槽) -->
		<slot></slot>
		<p>
            这是 MyCom1 组件最后一个 p 标签
        </p>
	</template>
	<my-com-1>
		<!-- 在使用 MyCom1 组件时,为插槽指定具体的内容 -->
        <p>
            ~~~用户自定义的内容~~~
        </p>
	</my-com-1>
2.1 没有预留插槽的内容会被丢弃

如果在封装组件时没有预留任何 <slor> 插槽,则用户提供的任何自定义内容会被丢弃。示例代码如下:

	<template>
		<p>
            这是 MyCom1 组件的第一个 p 标签
        </p>
		<!-- 封装组件时没有预留任何 <slor> 插槽 -->
		<p>
            这是 MyCom1 组件最后一个 p 标签
        </p>
	</template>
	<my-com-1>
		<!-- 自定义的内容会被丢弃 -->
        <p>
            ~~~用户自定义的内容~~~
        </p>
	</my-com-1>
2.2 后备内容

封装组件时,可以为预留的 <slot> 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例代码如下:

	<template>
		<p>
            这是 MyCom1 组件的第一个 p 标签
        </p>
		<slot>这是后备内容</slot>
		<p>
            这是 MyCom1 组件最后一个 p 标签
        </p>
	</template>

3. 具名插槽

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

	<div class="container">
        <header>
        	<!-- 我们希望把页头放这里 -->
            <slot name="header"></slot>
        </header>
        <main>
        	<!-- 我们希望把主要内容放这里 -->
            <slot></slot>
        </main>
        <footer>
        	<!-- 我们希望把页脚放这里 -->
            <slot name="footer"></slot>
        </footer>
	</div>

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

3.1 为具名插槽提供内容

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称。示例代码如下:

	<my-com-2>
		<template v-slot:header>
			<h1>滕王阁序</h1>
		</template>
        
		<template v-slot:default>
			<p>豫章故郡,洪都新府。</p>
			<p>星分翼轸,地接衡庐。</p>
			<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
        </template>
		<template v-slot:footer>
			<p>落款:王勃</p>
		</template>
	</ my-com-2>

3.2 具名插槽的简写形式

跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容(v-slot:)替换为字符 # 。例如 v-slot:header 可以被重写成 #header

	<my-com-2>
		<template #header>
			<h1>滕王阁序</h1>
		</template>
        
		<template #default>
			<p>豫章故郡,洪都新府。</p>
			<p>星分翼轸,地接衡庐。</p>
			<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
        </template>
		<template #footer>
			<p>落款:王勃</p>
		</template>
	</ my-com-2>

4. 作用域插槽

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

	<div>
        <h3>
            这是 TEST 组件
        </h3>
        <slot :info="infomation"></slot>
	</div>

	<!-- 使用自定义组件 -->
	<my-test>
		<template #default="scope">
        	{{ scope }}
        </template>
	</my-test>

自定义指令

1. 声明私有自定义指令的语法

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。示例代码如下:

	directives: {
		// 自定义一个私有指令
		focus: {
			// 当被绑定的元素插入到 DOM 元素中时, 自动触发 mounted 函数
            mounted(el) {
                el.focus() // 让被绑定的元素自定获得焦点
            }
		}
	}

2. 声明全局自定义指令的语法

需要通过“单页面应用程序的实例对象”进行声明,示例代码如下:

	const app = Vue.createApp({})
    
    // 注册一个全局自定义指令 'v-focus'
    app.directive('focus', {
        mounted(el) {
            // Focus the element
            el.focus()
        }
    })

3. updated 函数

mounted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted 函数不会被触发。updated 函数会在每次 DOM 元素更新完成后被调用。示例代码如下:

	app.directive('focus', {
        mounted(el) { // 第一次插入 DOM 时触发这个函数
            // Focus the element
            el.focus()
        },
        updated(el) { // 每次 DOM 更新时都会触发 updated 函数
            el.focus()
        }
    })

注意:在 vue2 的项目中使用自定义指令时,【 mounted -> bind 】 【 updated -> update

4. 函数简写

如果 mounted 和 updated 函数中的逻辑完全相同,则可以简写成如下格式:

	app.directive('focus', (el) => {
        // 在 mounted 和 updated 时都会触发相同的业务处理
        el.focus()
    })

5. 指令的参数值

在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值,示例代码如下:

	<template>
		<!-- 在使用 v-color 指令时,可以通过“等号”绑定指令的值 -->
		<input type="text" v-model.number="count" v-focus v-color="red">
		<p v-color="cyan">
            {{count}}
        </p>
		<button @click="count++">
            +1
        </button>
	</template>

	// 自定义 v-color 指令
	app.directive('color', (el, binding) => {
        // binding.value 就是通过“等号”为指令绑定的值
        el.style.color = binding.value
    })

总结

  1. 能够知道如何使用 ref 引用 DOM 和组件实例
    • 通过 ref 属性指定引用的名称、使用 this.$refs 访问到引用实例
  2. 能够知道 $nextTick 的调用时机
    • 组件的 DOM 更新之后,才执行 $nextTick 中的回调
  3. 能够说出 keep-alive 元素的作用
    • 保持动态组件的状态
  4. 能够掌握插槽的基本用法
    • <slot> 标签、具名插槽、作用域插槽v-slot: 简写为 #
  5. 能够知道如何自定义指令
    • 私有自定义指令、全局自定义指令

路由

前端路由的概念与原理

1. 什么是路由

路由(英文名:router)就是对应关系。路由分为两大类:

  1. 后端路由
  2. 前端路由

2. 回顾:后端路由

后端路由指的是:请求方式、请求地址function 处理函数之间的对应关系。在 Node.js 课程中,express 路由的基本用法如下:

	const express = require('express')
    const router = express.Router()
    
    router.get('/userlist', function(req, res) {  路由的处理函数  })
    router.post('/adduser', function(req, res) { 路由的处理函数 })
	
	module.exports = router

3. SPA 与前端路由

SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有的组件的展示与切换都在这唯一的一个页面内完成。

此时,不同组件之间的切换需要通过 来实现。

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

4. 什么是前端路由

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

5. 前端路由的工作方式

  1. 用户点击了页面上的路由链接
  2. 导致了 URL 地址栏中的 Hash 值发生了变化
  3. 前端路由监听到了 Hash 地址的变化
  4. 前端路由把当前 Hash 地址对应的组件渲染到浏览器中

在这里插入图片描述

结论:前端路由,指的是 Hash 地址组件之间对应关系

6. 实现简易的前端路由

步骤1:导入并注册 MyHome、MyMovie、MyAbout 三个组件。示例代码如下:

	import MyHome from './components/MyHome.vue'
	import MyMovie from './components/MyMovie.vue'
	import MyAbout from './components/MyAbout.vue'

	export default {
        components: {
            MyHome,
            MyMovie,
            MyAbout,
        }
    }

步骤2:通过<component> 标签的 is 属性,动态切换要显示的组件。示例代码如下:

	<template>
		<h1>
            App 组件
        </h1>
		<component :is="comName"></component>
	</template>

	<script>
		export default {
            data() {
                return {
                    comName: 'my-home', // 要展示的组件的名称
                }
            }
        }
	</script>

步骤3: 在组件的结构中声明如下 3 个<a> 链接,通过点击不同的 a 标签,切换浏览器地址栏中的 Hash 值:

	<a href="#/home">Home</a>&nbsp;
	<a href="#/movie">Movie</a>&nbsp;
	<a href="#/about">About</a>

步骤4:在 created 生命周期函数中监听浏览器地址栏中 Hash 地址的变化,动态切换要展示的组件的名称:

	created() {
        window.onhashchange = () => {
            switch (location.hash) {
                case '#/home': // 点击了“首页”的链接
                    this.comName = 'my-home'
                    break
                case '#/movie': // 点击了“电影”的链接
                    this.comName = 'my-movie'
                    break
                case '#/about': // 点击了“关于”的链接
                    this.comName = 'my-about'
                    break
            }
        }
    }	

vue-router 的基本用法

1. 什么是 vue-router

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

2. vue-router 的版本

vue-router 目前有 3.x 的版本 和4.x的版本。其中:

  • vue-router 3.x 只能结合 vue2 进行使用
  • vue-router 4.x 只能结合 vue3 进行使用

3. vue-router 4.x 的基本使用步骤

  1. 在项目中安装 vue-router
  2. 定义路由组件
  3. 声明路由链接占位符
  4. 创建路由模块
  5. 导入并挂载路由模块
3.1 在项目中安装 vue-router

在 vue3 的项目中,只能安装并使用 vue-router 4.x。安装的命令如下:

	npm install vue-router@next -S
3.2 定义路由组件

在项目中定义 MyHome.vueMyMovie.vueMyAbout.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 个步骤创建并得到路由的实例对象:

  1. 从 vue-router 中按需导入两个方法
  2. 导入需要使用路由控制的组件
  3. 创建路由实例对象
  4. 向外共享路由实例对象
  5. 在 main.js 中导入并挂载路由模块
从 vue-router 中按需导入两个方法
	// 1. 从 vue-router 中按需导入两个方法
	// createRouter 方法用于创建路由的实例对象
	// createWebHashHistory 用于指定路由的工作模式(hash 模式)
	import { createRouter, createWebHashHistory } from 'vue-router'
创建路由实例对象
	import Home from './MyHome.vue'
    import Movie from './MyMovie.vue'
    import About from './MyAbout.vue'


	// 3. 创建路由实例对象
	const router = createRouter({
    	// 3.1. 通过 history 属性指定路由的工作模式
    	history: createWebHashHistory(),
    	// 3.2 通过 routes 数组,指定路由规则
    	routes: [
        	// path 是 hash 地址,component 是要展示的组件
            { path: '/home', component: Home },
            { path: '/movie', component: Movie },
            { path: '/about', component: About },
        ]
	})
	
	// 4. 向外界共享路由实例对象
	export default = router
	import { createApp } from 'vue'
	import App from './App.vue'
	// 5.  在 main.js 入口文件中导入并挂载路由模块
	import router from './router.js'
	const app = createApp(App)
	app.use(router)
	app.mount('#app')

vue-router 的高级用法

1. 路由重定向

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

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

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        // 其中,path 表示需要被重定向的“原地址”,redirect 表示将要被重定向的“新地址”
        { path: '/', redirect: '/home' }, 
        { path: '/home', component: Home },
        { path: '/movie', component: Movie },
        { path: '/about', component: About },
    ]
})

// 向外界共享路由实例对象
export default = router

2. 路由高亮

可以通过如下的两种方式,将激活的路由链接进行高亮显示:

  1. 使用默认的高亮 class 类
  2. 自定义路由高亮的 class 类
2.1 默认的高亮 class 类

被激活的路由链接,默认会应用一个叫做 router-link-active 的类名。开发者可以使用此类名选择器,为激活的路由链接设置高亮的样式:

/* 在 index.css 全局样式表中, 重新 router-link-active 的样式 */
.router-link-active {
    background-color: red;
    color: white;
    font-weight: bold;
}
2.2 自定义路由高亮的 class 类

在创建路由的实例对象时,开发者可以基于 linkActiveClass 属性,自定义路由链接被激活时,所应用的类名:

const router = createRouter({
    history: createWebHashHistory(),
    // 指定被激活的路由链接,会应用 router-active 这个类名,
    // 默认的 router-link-active 类名会被覆盖掉
    linkActiveClass: 'router-active',
    routes: [
        { path: '/', redirect: '/home' }, 
        { path: '/home', component: Home },
        { path: '/movie', component: Movie },
        { path: '/about', component: About },
    ]
})

3. 嵌套路由

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

  1. 声明子路由链接子路由占位符
  2. 在父路由规则中,通过 children 属性嵌套声明子路由规则
3.1 声明子路由链接子路由占位符

在 About.vue 组件中, 声明 tab1 和 tab2 的子路由链接以及子路由占位符。示例代码如下:

<template>
    <h3>这是 MyAbout 组件</h3>
    <!-- 在关于页面中,声明两个子路由链接 -->
    <router-link to="/about/tab1">tab1</router-link>&nbsp;
    <router-link to="/about/tab2">tab2</router-link>

    <!-- 在关于页面中,声明 tab1 和 tab2 的路由占位符 -->
    <router-view></router-view>
</template>
3.2 通过 children 属性声明子路由规则

在 router.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则。示例代码如下:

import Tab1 from './MyTab1.vue'
import Tab2 from './MyTab2.vue'

const router = createRouter({
    routes: [ 
        { // about 页面的路由规则(父级路由规则)
            path: '/about',
            component: About,
            children: [
                { path: 'tab1', component: Tab1 }, // 访问我 /about/tab1 时,展示 Tab1 组件
                { path: 'tab2', component: Tab2 }, // 访问我 /about/tab2 时,展示 Tab2 组件
            ]
        }, 
    ]
})

4. 动态路由匹配

思考:有如下 3 个路由链接:

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

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

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

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

4.1 动态路由的概念

动态路由指的是:把 Hash 地址中的可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用使用英文的冒号:)来定义路由的参数项,示例代码如下:

	// 路由中的动态参数以 :进行声明,冒号后面的是动态参数的名称
	{ path: '/movie/:id', component: Movie }

	// 将以下 3 个路由规则,合并成了一个,提高了路由规则的复用性
	{ path: '/movie/1', component: Movie }
	{ path: '/movie/2', component: Movie }
	{ path: '/movie/3', component: Movie }
4.2 $route.params 参数对象

通过动态路由匹配的方式渲染出来的 组件中,可以使用 $route.params 对象访问到动态匹配的参数值

	<template>
		<!-- $route.params 是路由的“参数对象” -->
		<h3>
            MyMovie 组件 --- {{$route.params.id}}
        </h3>
	</template>

4.3 使用 props 接收路由参数

为了简化路由参数的获取形式,vue-router 允许在路由规则开始 props 传参。示例代码如下:

	// 1. 在定义路由规则时,声明 props: true 选项,
	// 即可在 Movie 组件中,以 props 的形式接收到路由规则匹配到的参数项
	{ path: '/movie/:id', component: Movie, props: true }

	<template>
		<!-- 3. 直接使用 props 中接收的路由参数 -->
		<h3>
            MyMovie 组件 --- {{id}}
        </h3>
	</template>

	<script>
		export default {
            props: ['id'] // 2. 使用 props 接收路由规则中匹配到的参数项
        }
	</script>

5. 编程式导航

通过调用 API 实现导航的方式,叫做编程式导航。与之对应的,通过点击链接实现导航的方式,叫做声明式导航。例如:

  • 普通网页中点击 <a> 链接、vue 项目中点击 <router-link> 都属于声明式导航
    • 普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航
5.1 vue-router 中的编程式导航 API

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

  1. this.$router.push(‘hash 地址’)
    • 跳转到指定 Hash 地址,从而展示对应的组件
  2. this.$router.go(数值 n)
    • 实现导航历史的前进、后退
5.2 $router.push

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

<template>
    <div>
        <h3>这是 MyHome 组件</h3>
        <button @click="gotoMovie(3)">go to Movie</button>
    </div>
</template>

<script>
    export default {
        name: 'MyHome',
        methods: {
            gotoMovie(id) { // id 参数是电影的 id 值
                this.$router.push(`/movie/${id}`)
            }
        },
    }
</script>
5.3 $router.go

调用 this.$router.go() 方法,可以在浏览历史中进行前进和后退。示例代码如下:

<template>
    <div>
        <h3>这是 MyMovie 组件 --- {{ id }}</h3>
        <button @click="goBack">后退</button>
    </div>
</template>

<script>
    export default {
        name: 'MyMovie',
        props: ['id'],
        methods: {
            goBack() {
                this.$router.go(-1)  // 后退到之前的组件页面
            }
        }
    }
</script>

6. 命名路由

通过 name 属性为路由规则定义名称的方式,叫做命名路由。示例代码如下:

{
    path: '/movie/:id',
    // 使用 name 属性为当前的路由规则定义一个“名称”
    name:'mov',
    component: Movie,
    props: true,
}

注意:命名路由的 name 值不能重复,必须保证唯一性!

6.1 使用命名路由实现声明式导航

为 <router-link> 标签动态绑定 to 属性的值,并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数。示例代码如下:

<template>
    <div>
        <h3>这是 MyHome 组件</h3>
        <router-link :to="{name: 'mov', params: {id:3}}">go to Movie</router-link>
    </div>
</template>

<script>
    export default {
        name: 'MyHome',
    }
</script>
6.2 使用命名路由实现编程式导航

调用 push 函数期间指定一个配置对象name 是要跳转到的路由规则、params 是携带的路由参数:

<template>
    <div>
        <h3>这是 MyHome 组件</h3>
        <button @click="gotoMovie(3)">go to Movie</button>
    </div>
</template>

<script>
    export default {
        name: 'MyHome',
        methods: {
            gotoMovie(id) { // id 参数是电影的 id 值
                this.$router.push({
                    name: 'mov',
                    params: {id: 3}
                })
            }
        },
    }
</script>

7. 导航守卫

导航守卫可以控制路由的访问权限。示意图如下:

在这里插入图片描述

7.1 如何声明全局的导航守卫

全局导航守卫拦截每个路由规则,从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航守卫:

	// 创建路由实例对象
	const router = createRouter({ ... })
    // 调用路由实例对象的 beforeEach 函数,声明“全局前置守卫”
    // fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
    // 因此 fn 叫做 “守卫方法”
    router.beforeEach(fn)
7.2 守卫方法的 3 个形参

全局导航守卫的守卫方法中接收 3 个形参,格式为:

	// 创建路由实例对象
	const router = createRouter({ ... })
                                 
    // 全局前置守卫
    router.beforeEach((to, from, next) => {
        // to 目标路由对象
        // from 当前导航正要离开的路由对象
        // next 是一个函数,表示放行
    })

注意:

  1. 在守卫方法中如果不声明 next 形参,则默认允许用户访问每一个路由!
  2. 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由!
7.3 next 函数的 3 种调用方式

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

在这里插入图片描述

直接放行:next()

强制其停留在当前页面:next(false)

强制其跳转到登录页面:next(‘/login’)

7.4 结合 token 控制后台主页的访问权限
	// 全局前置守卫
	router.beforeEach((to, from, next) => {
        const token = localStorage.getItem('token') // 1. 读取 token
        if (to.path === '/main' && !token) {
            // next(false) 3.1 不允许跳转
            next('/login') // 3.2 强制跳转到“登录页面”
        } else {
			next()   	   // 3.3 直接放行,允许访问“后台主页”
        }
    })

总结

  1. 能够知道如何在 vue 中配置路由
    • createRouter、app.use(router)
  2. 能够知道如何使用嵌套路由
    • 通过 children 属性进行路由嵌套、子路由的 hash 地址不要以/开头
  3. 能够知道如何实现动态路由匹配
    • 使用冒号声明参数项、this.$route.params、props: true
  4. 能够知道如何使用编程式导航
    • this.$router.push、this.$router.go(-1)
  5. 能够知道如何使用全局导航守卫
    • 路由实例.beforeEach((to, from, next) => { })

VUE 基础 - 综合案例

vue-cil

1. 什么是 vue-cli

vue-cli (俗称:vue 脚手架)是 vue 官方提供的、快速生成 vue 工程化项目的工具。

特点

  1. 开箱即用
  2. 基于 webpack
  3. 功能丰富且易于扩展
  4. 支持创建 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 命令会提示错误信息

解决方案如下:

  1. 管理员身份运行 PowerShell
  2. 执行 set-ExecutionPolicy RemoteSigned 命令
  3. 输入字符 Y回车即可

3. 创建项目

vue-cli 提供了创建项目的两种方式

	# 基于【命令行】的方式创建 vue 项目
	vue create 项目名称
	
	# OR
	
	# 基于 【可视化面板】 创建 vue 项目
	vue ui

4. 基于 vue ui 创建 vue项目

步骤1:在终端下运行 vue ui 命令,自动在浏览器中打开创建项目的可视化面板

在这里插入图片描述

步骤2:在详情页面填写项目名称

在这里插入图片描述

步骤3:在预设页面选择手动配置项目

在这里插入图片描述

步骤4:在功能页面勾选需要安装的功能Choose Vue VersionBabelCSS 预处理器、== ==):

在这里插入图片描述

步骤5:在配置页面勾选 vue 的版本需要的预处理器

在这里插入图片描述

步骤6:将刚才所有的配置保存为预设(模板),方便下一次创建项目时直接复用之前的配置

在这里插入图片描述

步骤7:创建项目并自定安装依赖包:

在这里插入图片描述

vue ui 的本质:通过可视化的面板采集到用户的配置信息后,在后台基于命令行的方式自动初始化项目:

项目创建完成后:自动进入项目仪表盘

在这里插入图片描述

5. 基于命令行创建 vue 项目

步骤1:在终端下运行 vue create dome2 命令,基于交互式的命令行创建 vue 的项目:

在这里插入图片描述

步骤2: 选择要安装的功能:

在这里插入图片描述

步骤3:使用上下箭头选择 vue 的版本,并使用回车键确认选择:

在这里插入图片描述

步骤4:使用上下箭头选择要使用的 css 预处理器,并使用回车键确认选择:

在这里插入图片描述

步骤5:使用上下箭头选择如何存储插件的配置信息,并使用回车键确认选择:

在这里插入图片描述

步骤6:是否将刚才的配置保存为预设:

在这里插入图片描述

步骤7:选择如何安装项目中的依赖包:

在这里插入图片描述

步骤8:开始创建项目并自定安装依赖包:

在这里插入图片描述

步骤9:项目创建完成:

在这里插入图片描述

6. 在 Vue2 的项目中使用路由

在 vue2 的项目中,只能安装并使用 3.x 版本的 vue-router。

版本3 和版本4的路由的最主要的区别:创建路由模块的方式不同!

6.1 回顾:4.x 版本的路由如何创建路由模块
	import { createRouter, createWebHashHistory } from 'vue-router' // 1. 按需导入需要的方法


	import Home from './components/MyHome.vue'  // 2. 导入需要使用路由进行切换的组件
    import Movie from './components/MyMovie.vue'
    import About from './components/MyAbout.vue'


	// 3. 创建路由实例对象
	const router = createRouter({  				// 3. 创建路由对象
    	history: createWebHashHistory(),		// 3.1 指定通过 hash 管理路由的切换
    	routes: [								// 3.2 创建路由规则
        	// path 是 hash 地址,component 是要展示的组件
            { path: '/home', component: Home },
            { path: '/movie', component: Movie },
            { path: '/about', component: About },
        ]
	})
    
    export default router 						// 4. 向外共享路由对象
6.2 学习:3.x 版本的路由如何创建路由模块

步骤1:在 vue2 的项目中安装 3.x 版本的路由:

	npm i vue-router@3.4.9 -S

步骤2:在 src -> components 目录下,创建需要使用路由切换的组件

步骤3:在 src 目录下创建 router -> index.js 路由模块:

	import Vue from 'vue' 						// 1. 导入 Vue2 的构造函数
	import VueRouter from 'vue-router'			// 2. 导入 3.x 路由的构造函数


	import Home from '@/components/MyHome.vue'  // 2. 导入需要使用路由进行切换的组件
    import Movie from '@/components/MyMovie.vue'
    import About from '@/components/MyAbout.vue'


	
	Vue.use(VueRouter)											// 4. 调用 Vue.use() 函数,把路由配置为 Vue 的插件

	const router = new VueRouter({  			// 5. 创建路由对象		
    	routes: [								// 5.1 创建路由规则
        	// path 是 hash 地址,component 是要展示的组件
            { path: '/', redirect: '/home' },
            { path: '/home', component: Home },
            { path: '/movie', component: Movie },
        ]
	})
    
    export default router 						// 6. 向外共享路由对象

步骤4:在 main.js 中导入路由模块,并通过 router 属性进行挂载:

	import Vue from 'vue'
	import App from './App.vue'

	// 1. 导入路由模块
	import router from './router'

	Vue.config.productionTip = false

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

组件库

1. 什么是 vue 组件库

在实际开发中,前端开发者可以把自己封装的 .vue 组件整理、打包、并发布为 npm 的包,从而供其他人下载和使用。这种可以直接下载并在项目中使用的现场组件,就叫做 vue 组件库。

2. vue 组件库bootstrap 的区别

二者之间存在本质的区别:

  • bootstrap 只提供了纯粹的原材料(css 样式、HTML 结构以及 js 特效),需要由开发者做进一步的组装改造
  • vue 组件库是遵循 vue 语法、高度定制的现场组件,开箱即用

3. 最常用的 vue 组件库

  1. PC 端
    • Element UI (https://element.eleme.cn/#/zh-CN)
    • View UI (http://v1.iviewui.com/)
  2. 移动端
    • Mint UI (htpp://mint-ui.github.io/#!/zh-cn)
    • Vant (https://vant-contrib.gitee.io/vant/#/zh-CN/)

4. Element UI

Element UI 是饿了么前端团队开源的一套 PC 端 vue 组件库。支持在 vue2 和 vue3 的项目中使用:

  • Vue2 的项目使用旧版的 Element UI (https://element.eleme.cn/#/zh-CN)
  • vue3 的项目使用新版的 Element Plus(https://element-plus.gitee.io/#/zh-CN)
4.1 在 vue2 的项目中安装 element-ui
	npm i element-ui -S
4.2 引入 element-ui

开发者可以一次性完整引入所有的 element-ui 组件,或是根据需求,只按需引入用到的 element-ui 组件:

  • 完整引入:操作简单,但是会额外引入一些用不到的组件,导致项目体积过大
  • 按需引入:操作相对复杂一些,但是只会引入用到的组件,能起到优化项目体积的目的
4.3 完整引入

main.js 中写入以下内容:

	import Vue from 'vue'
	import App from './App.vue'

	// 1. 导入 element-ui 组件
	import ElementUI from 'element-ui'

	// 2. 导入 element-ui 组件的样式
	import 'element-ui/lib/theme-chalk/index.css'
	

	Vue.config.productionTip = false

	// 3. 把 ElementUI 注册为 vue 的插件 【注册之后,即可在每个组件中直接使用每一个 element ui 的组件】
	Vue.use(ElementUI)

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

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

步骤1,安装 babel-plugin-component:

	npm install babel-plugin-component -D

步骤2,修改根目录下的 babel.config.js 配置文件,新增 plugins 节点如下:

	module.exports = {
        presets: ['@vue/cli-plugin-babel/preset'],
        plugins: [
            [
                'component',
                {
                    libraryName: 'element-ui',
                    styleLibraryName: 'theme-chalk',
                }
            ]
        ]
    }

步骤3,如果你只希望引入部分组件,比如 Button,那么需要在 main.js 中写入以下内容:

	import Vue from 'vue'
	import App from './App.vue'

	// 1. 按需导入 element-ui 的组件
	import {Button} from 'element-ui'

	// 2. 注册需要的组件
	Vue.component(Button.name, Button)
	// 或写为 Vue.use(Button)
	

	Vue.config.productionTip = false

	new Vue({
        render: h => h(App),
        // 2. 挂载路由模块,
        // router: router,
        router,
    }).$mount('#app')
4.5 把组件的导入和注册封装为独立的模块

在 src 目录下新建 element-ui/index.js 模块,并声明如下的代码:

	import Vue from 'vue'  // -> 模块路径 /src/element-ui/index.js
	// 按需导入element-ui 的组件
	import { Button, Input } from 'element-ui'

	// 注册需要的组件
	Vue.use(Button)
	Vue.use(Input)


	// -> 在 main.js 中导入
	import './element-ui'

axios 拦截器

1. 回顾:在 vue3 的项目中全局配置 axios

	import { createApp } from 'vue'
	import App from './App.vue'

	// 1. 导入 axios 
	import axios from 'axios'

	const app = createApp(App)
    
    // 2. 配置请求根路径
    axios.defaults.baseURL = 'https://www.escook.cn'
	// 3. 全局配置 axios 
	app.config.globalProperties.$http = axios
	app.mount('#app')

2. 在 vue2 的项目中全局配置 axios

需要在 main.js 入口文件中,通过 Vue 构造函数prototype 原型对象全局配置 axios:

	import Vue from 'vue'
	import App from './App.vue'

	// 1. 导入 axios
	import axios from 'axios'

	// 2. 配置请求根路径
	axios.dafaults.baseURL = 'https://www.escook.cn'
	// 3. 通过 Vue 构造函数的原型对象,全局配置 axios
	Vue.prottype.$htpp = axios

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

3. 什么是拦截器

拦截器(英文名:Interceptors)会在每次发起 Ajax 请求得到响应的时候自动被触发

在这里插入图片描述

应用场景:

  1. Token 身份认证
  2. Loading 效果
  3. etc…

4. 配置请求拦截器

通过 axios.interceptors.request.use(成功的回调,失败的回调) 可以配置请求拦截器。示例代码如下:

	axios.interceptors.request.use(config => {
        // Do something before request is sent
        return config;
    }, error => {
        // Do something with request error
        return Promise.reject(error)
    })

注意:失败的回调函数可以被省略!

4.1 请求拦截器 - Token 认证
	import axios from 'axios'

	axios.dafaults.baseURL = 'https://www.escook.cn'
	// 配置请求的拦截器
	axios.interceptors.request.use(config => {
        // 为当前请求配置 Token 认证字段
        config.headers.Authorization = 'Bearer xxx'
        return config
    })

	Vue.prottype.$htpp = axios
4.2 请求拦截器 - 展示 Loading 效果

借助于 element ui 提供的 Loading 效果组件(https://element.eleme.cn/#/zh-CN/component/loading)可以方便的实例 Loading 效果的展示:

	// 1. 按需导入 Loading 效果组件
	import { Loading } from 'element-ui'

	// 2. 声明变量,用来存储 Loading 组件的实例对象
	let ladingInstance = null;

	// 配置请求的拦截器
	xios.interceptors.request.use(config => {
        // 3. 调用 Loading 组件的 service() 方法,创建 Loading 组件的实例,并全屏展示 loading 效果
      	loadingInstance = Loading.service({ fullscreen: true })  
        return config
    })

5. 配置响应拦截器

通过 axios.interceptors.response.use(成功的回调,失败的回调) 可以配置请求拦截器。示例代码如下:

	axios.interceptors.response.use(response => {
        // Do something with response data
        return cresponse;
    }, error => {
        // Do something with response error
        return Promise.reject(error)
    })

注意:失败的回调函数可以被省略!

5.1 响应拦截器 - 关闭 Loading 效果

调用 Loading 实例提供的 close() 方法即可关闭 Loading 效果,示例代码如下:

	// 响应拦截器
	axios.interceptors.response.use(response => {
        // 调用 Loading 实例的 close 方法即可关闭 loading 效果
        loadingInstance.close()
        return response
    })

proxy 跨域代理

1. 回顾:接口的跨域问题

vue 项目运行的地址:http://localhost:8080/

API 接口运行的地址:https://www.escook.cn/api/users

由于当前的 API 接口没有开启 CORS 跨域资源共享,因此默认情况下,上面的接口无法请求成功

在这里插入图片描述

2. 通过代理解决接口的跨域问题

通过 vue-cli 创建的项目在遇到接口跨域问题时,可以通过代理的方式来解决:

在这里插入图片描述

  1. 把 axios 的请求根路径设置为 vue 项目的运行地址(接口请求不再跨域)
  2. vue 项目发现请求的接口不存在,把请求转交给 proxy 代理
  3. 代理把请求根路径替换为 devServer.proxy 属性的值,发起真正的数据请求
  4. 代理把请求到的数据,转发给 axios

3. 在项目中配置 proxy 代理

步骤1,在 main.js 入口文件中,把 axios 的请求根路径改造为当前 web 项目的根路径

	// axios.defaults.baseURL = 'httts://www.escook.cn'

	// 配置请求根路径
	axios.defaults.baseURL = 'httts://localhost:8080'

步骤2,在项目根目录下创建 vue.config.js 的配置文件,并声明如下的配置:

	module.exports = {
        devServer: {
            // 当前项目在开发调试阶段,
            // 会将任何未知请求(没有匹配到静态文件的请求)代理到 https://www.escook.cn
            proxy: 'https://www.escook.cn',
        }
    }

总结

  1. 能够知道如何使用 vue-cli 创建项目
    • vue ui、vue create 项目名称
  2. 能够知道如何在项目中安装和配置 element-ui
    • 完整引入、按需引入、参考官方文档进行配置
  3. 能够知道 element-ui 中常见的组件的用法
    • Table 表格、Form 表单、Dialog 对话框、Message 消息、MessageBox 弹框
  4. 能够知道如何使用 axios 拦截器
    • axios.interceptors.request.use()、axios.interceptors.response.use()
  5. 能够知道如何配置 proxy 代理
    • 修改请求根路径、vue.config.jsdevServer.proxy

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值