vue 基础

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. 同步任务和异步任务执行过程

vue 基础_第1张图片

  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 [email protected] [email protected] -D 命令
  2. 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
	module: { // 所有第三方文件模块的匹配规则
        rules: [ // 文件后缀名的匹配规则
            {test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
        ]
    }

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

  1. 运行 npm i [email protected] [email protected] -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-loade[email protected]
  • @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 解读核心关键词:构建用户界面

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

vue 基础_第2张图片

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

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

vue 基础_第3张图片

1.3 使用 vue 构建用户界面

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

vue 基础_第4张图片

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

vue 基础_第5张图片

2. vue 的特性

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

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

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

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

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

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

2.2 双向数据绑定

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

2.3 MVVM

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

vue 基础_第6张图片

在 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 基础_第7张图片

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

用法示例:

	
	<p v-text="username">p>
	
	 
	
	<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 这个指令:

	
	

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

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

	

    
    <input type="text" v-bind:placeholder="inputValue">
    <br>
    
    <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>
	
	<button v-on:click="addCount">+1button>

注意:原生 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">+1button>
        
        
        
        <button @click="count += 1">+1button>
	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 是当前元素自身时触发事件处理函数

语法格式如下:

    
    <a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页a>

按键修饰符

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

    
    <input type="text" @keyup.enter="submit">

    
    <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 自动将用户的输入值转为数值类型 number=“age”/>
.trim 自动过滤用户输入的首尾空白字符 trim=“msg”/>
.lazy 在 ”change“ 是而非 ”input“ 时更新 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>
        
        
        
        <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 表达式的尾部,由“管道符”进行调用,示例代码如下:

    
    <p> {{ message | capitalize }} p>

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

vite 的基本使用

1. 创建 vite 的项目

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

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

2. 梳理项目的结构

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

其中:

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

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

其中:

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

3. vite 项目的运行流程

在工程化的项目中,vue 要做的事情很单纯:通过 main.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 规定:每个组件对应的模板结构,需要定义到 节点中。

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

注意: