node.js 遵循了 CommonJS
的模块化规范。其中:
require()
方法module.exports
对象模块化的好处:
大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用
,利人利己。
在 ES6 模块化规范
诞生之前,JavaScript 社区已经尝试并提出了 AMD、CMD、CommonJS
等模块化规范。
但是,这些有社区提出的模块化标准,还是存在一定的差异性
与局限性
,并不是
浏览器与服务器通用的模块化标准
,例如:
浏览器端
的 JavaScript 模块化服务器端
的 JavaScript 模块化太多的模块化规范给开发者增加了学习的难度
与开发的成本
。因此,大一统的 ES6 模块化规范诞生了
!
ES6 模块化规范
是浏览器端
与服务器端
通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本,开发者不需再额外学习AMD、CMD或CommonJS 等模块化规范。
ES6 模块化规范中定义:
import
关键字export
关键字node.js中默认仅支持 CommonJS 模块化规范
,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照如下两个步骤进行配置:
v14.15.1
或更高版本的 node.js“type": "module"
节点ES6 的模块化主要包含如下3中用法:
默认导出
与默认导入
按需导出
与按需导入
直接导入
并执行
模块中的代码默认导出的语法:export default
默认导出的成员
let n1 = 10; // 定义模块私有成员 n1
let n2 = 20; // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去
function show() {} // 定义模块私有方法 show
export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
n1,
show
}
默认导入的语法 import 接收名称 from ‘模块标识符’
// 从01_m1.js 模块中导入 export default 向外共享的成员
// 并使用 m1 成员进行接收
import m1 from './01_m1.js'
// 打印输出的结果为:
// {n1: 10, show: [Function :show]}
console.log(m1)
每个模块中,只允许使用唯一的一次 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
}
默认导入时的接收名称
可以任意名称,只要是合法的成员名称即可
:
// m1 是合法的名称
import m1 from './01_m1.js'
// 123m 不是合法的名称,因为成员名称不能以数字开头
import 123m from './01_m1.js'
按需导出的语法:export
按需导出的成员
// 当前模块为 03_m2.js
// 向外按需导出变量 s1
export let s1 = 'aaa'
// 向外按需导出变量 s2
export let s2 = 'ccc'
// 向外按需导出方法 say
export function say() {}
按需导入的语法:import
{ s1 } from
‘模块标识符’
// 导入模块成员
import { s1, s2, say } from './03_m2.js'
console.log(s1) // 打印输出 aaa
console.log(s2) // 打印输出 ccc
console.log(say) // 打印输出 [Function: say]
多次
按需导出导入的成员名称
必须和按需导出的名称
保持一致as 关键字
进行重命名如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模块代码,示例代码如下:
// 当前文件模块为 05_m3.js
// 在当前模块中执行一个 for 循环操作
for (let i = 0; i < 3; i++) {
console.log(i)
}
// -------------------分割线----------------------
// 直接导入并执行模块代码,不需要得到模块向外共享的成员
import './05_m3.js'
多层回调函数的相互嵌套
,就形成了回调地狱
。示例代码如下:
setTimeout(() { // 第一层回调函数
console.log('延时 1 秒后输出')
setTimeout(() { // 第二次回调函数
console.log('再延时 2 秒后输出')
setTimeout(() {
console.log('再延时 3 秒后输出')
}, 3000)
}, 2000)
}, 1000)
回调地狱的缺点:
难以维护
可读性变差
为了解决回调地狱的问题,ES6
(ECMAScript 2015)中新增了 Promise
的概念。
Promise 是一个构造函数
Promise.prototype 上包含一个 .then() 方法
.then() 方法用来预先指定成功和失败的回调函数
// 读取文件 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 成功
})
})
})
由于 node.js 官方提供的 fs 模块 仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此需要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件内容:
npm install 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) })
注意:上述的代码无法保证文件的读取顺序
,需要做进一步的改进!
如果上一个.then()方法中返回了一个新的 Promise 实例对象
,则可以通过下一个.then() 继续进行处理。通过.then()方法的链式调用
,就可以解决了回调函数的问题。
基于 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);
})
在 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);
})
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);
})
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);
})
方法的封装要求:
getFile
fpath
,表示要读取的文件的路径返回值
为 Promise 实例对象 // 1. 方法的名称为 getFile
// 2. 方法接收一个形参 fpath,表示要读取的文件路径
function getFile(Fpath) {
// 3. 方法的返回值为 Promise 的实例对象
return new Promise()
}
如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的异步操作定义到 function 函数内部。示例代码如下:
// 1. 方法的名称为 getFile
// 2. 方法接收一个形参 fpath,表示要读取的文件路径
function getFile(fpath) {
// 3. 方法的返回值为 Promise 的实例对象
return new Promise(function() {
// 4. 下面这行代码,表示这是一个具体的、读文件的异步操作
fs.readFile(fpath, 'utf8', (err, dataStr) => {})
})
}
通过 .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(成功的回调函数, 失败的回调函数)
}
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
是 ES8
(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作
。在 async/await 出现之前,开发者只能通过 链式 .then() 的方式
处理 Promise 异步操作。
.then 链式调用的优点
:解决了回调地狱的问题
.then 链式 调用的缺点
:代码冗余、阅读性差、不易理解
使用 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()
使用注意事项
必须
被 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
JavaScript 是一门单线程执行
的编程语言。也就是说,同一时间只能做一件事情。
单线程执行任务队列的问题:
如果前一个任务非常耗时
,则后续的任务就不得不一直等待,从而导致程序假死
的问题。
为了防止某个耗时任务
导致程序假死
的问题,JavaScript 把待执行的任务分为了两类:
同步任务
(synchronous)
非耗时任务
,指的是在主线程上排队执行的那些任务异步任务(asynchronous)
又叫做耗时任务
,异步任务由 JavaScript 委托给
宿主环境进行执行
当异步任务执行完成后,会通知 JavaScript 主线程
执行异步任务的回调函数
委托给
宿主环境执行对应的回调函数
,会被加入到任务队列中等待执行执行栈
被清空后,会读取任务队列中的回调函数,次序执行JavaScript 主线程不断重复上面的第4步
JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行
。这个过程是循环不断的,所以整个的这种运行机制又称为 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。其中:
同步任务
。会根据代码的先后顺序依次被执行
异步任务
。他们的回调函数会被加入到任务队列中,等待主线程空闲时再执行JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
宏任务
(macrotask)
微任务
(microtask)
每一个宏任务执行完之后,都会检查是否存在待执行的微任务
如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
取号之后进行排队
(宏任务队列)小腾只能等待
(单线程
,宏任务按次序执行
)是否还想办理其他业务
? (当前宏任务执行完,检查是否有微任务
)宏任务被推迟
)所有微任务执行完毕
,开始执行下一个宏任务
) setTimeout(function() {
console.log('1')
})
new Promise(function (resolve) {
console.log('2')
resolve()
}).then(function() {
console.log('3')
})
console.log('4')
正确输出顺序是:2431
分析:
同步任务
微任务
下一个宏任务
请分析以下代码输出的顺序
console.log('1');
setTimeout(function () {
console.log('2');
new Promise(function (resolve) {
console.log('3');
resolve();
}).then(function () {
console.log('4');
})
});
new Promise(function (resolve) {
console.log('5');
resolve()
}).then(function () {
console.log('6');
})
setTimeout(() => {
console.log('7');
new Promise(function (resolve) {
console.log('8');
resolve()
}).then(function () {
console.log('9');
})
});
正确的输出顺序是:1 5 6 2 3 4 7 8 9
使用 ES6 的模块化语法
如何使用 Promise 解决回调地狱问题
then
()、promise.catch
()async/await
简化 Promise 的调用
EventLoop 示意图
先检查是否有待执行的微任务
小白眼中的前端开发:
实际的前端开发:
模块化
(js的模块化、css的模块化、其他资源的模块化)组件化
(复用现有的 UI 结构,样式,行为)规范化
(目录结构的划分、编码规范化、接口规划好、文档规范化、Git 分支管理)自动化
(自动化构建、自动部署、自动化测试)前端工程化指的是:在企业级的前端项目开发
中,把前端开发所需的工具
、技术
、流程
、经验
等进行规范化、标准化。最终落实到细节上,就是实现前端的“4个现代化
”:
模块化、组件化、规范化、自动化
前端工程化的好处主要体现在如下两方面:
让前端开发能够“自成体系”
,覆盖了前端项目从创建到部署的方方面面概念:webpack 是前端项目工程化的具体解决方案
。
主要功能:它提供了友好的前端模块化开发
支持,以及代码压缩混淆
、处理浏览器端 JavaScript 的兼容性
、性能优化
等强大的功能。
好处:让程序员把工作的重心
放到具体功能的实现上,提高了前端开发效率
和项目的可维护性
。
注意:目前企业级的前端项目开发中,绝大多数的项目都是基于 webpack 进行打包构建的。
运行 npm init -y
命令,初始化包管理配置文件 package.json
src
源代码目录 index.html
首页 和 scr -> index.js
脚本文件npm install jquery -S
命令,安装jQuerywebpack.config.js
的 webpack 配置文件,并初始化如下的基本配置: module.exports = {
mode: 'development' // mode 用来指定构建模式。可选值有 development 和 production
}
dev 脚本
如下: "scripts": {
"dev": "webpack" // script 节点下的脚本,可以通过 npm run 执行。例如 npm run dev
}
mode 节点
的可选值有两个,分别是:
开发环境
不会
对打包生成的文件进行代码压缩
和性能优化
速度快
,适合在开发阶段
使用生产环境
会
对打包生成的文件进行代码压缩
和性能优化
速度很慢
,仅适合在项目发布阶段
使用webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件
,从而基于给定的配置,对项目进行打包。
注意:由于 webpack 是基于 node.js 开发出来的
打包工具,因此在它的配置文件中,支持使用node.js相关的语法和模块进行 webpack 的个性化配置。
在 webpack 中有如下的默认约定
:
src
-> index.js
dist
-> main.js
注意:可以在 webpack.config.js
中修改打包的默认约定
在 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 的能力
,从而让 webpack 用起来更方便
。最常用的 webpack 插件有如下两个:
webpack-dev-server
html-webpack-plugin
webpack-dev-server
可以让 webpack 监听项目源代码的变化
,从而进行自动打包构建
。
运行如下的命令,即可在项目中安装此插件:
npm install webpack-dev-server@3.11.0 -D
package.json
-> scripts
中的 dev
命令如下 scripts": {
"dev": "webpack serve" // script 节点下的脚本,可以通过 npm run 执行
},
npm run dev
命令,重新进行项目的打包http://localhost:8080
地址,查看自动打包效果注意:webpack-dev-server 会启动一个实时打包的 http 服务器
实际的物理磁盘上
output 节点
指定路径进行存放存放到了内存中
提高了
实时打包输出的性能
,因为内存比物理磁盘速度快很多webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中
,而且是虚拟的、不可见的
。
html-webpack-plugin 是 webpack 中的HTML 插件
,可以通过此插件自定制
index.html 页面的内容
。
需求
:通过 html-webpack-plugin 插件,将 src 目录下的 index.html 首页,复制到项目根目录中一份
运行如下的命令,即可在项目中安装此插件:
npm install html-webpack-plugin@4.5.0 -D
// 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 插件生效
}
也别放到了内存中
页面的底部
,自动注入
了打包的 bundle.js 文件在 webpack.config.js 配置文件中,可以通过 devServer
节点对 webpack-dev-server 插件进行更多的配置,示例代码如下:
devServer: {
open: true, // 初次打包完成后,自动打开浏览器
host: '127.0.0.1', // 实时打包所使用的主机地址
port: 80, // 实时打包所使用的端口号
}
在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块
,webpack默认处理不了,需要调用 loader 加载器才可以正常打包
,否则会报错!
loader 加载器的作用:协助 webpack 打包处理特定文件模块
。比如:
module
-> rules
数组中,添加 loader 规则如下: module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.css$/, use: ['style-loader', 'css-loader']}
]
}
其中,test
表示匹配的文件类型
,use
表示对应要调用的 loader
注意:
顺序是固定的
从后往前调用
module
-> rules
数组中,添加 loader 规则如下: module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
]
}
[email protected] [email protected]
-D 命令module
-> rules
数组中,添加 loader 规则如下: module: { // 所有第三方文件模块的匹配规则
rules: [ // 文件后缀名的匹配规则
{test: /\.jpg|png|gif$/, use: ['url-loader?limit=22229']}
]
}
其中 ?
之后的是 loader 的参数项
:
图片的大小
,单位是字节(byte)<=
limit 大小的图片,才会被转为 base64 格式的图片带参数的 loader
还可以通过对象的方式
进行配置:
module: { // 用来处理所有的第三方模块
rules: [ // 第三方模块的匹配规则
{
test: /\.jpg|png|git/, // 匹配图片文件
use: {
loader: 'url-loader', // 通过 loader 属性指定要调用的 loader
options: { // 通过 options 属性指定参数项
limit: 22229
}
}
}
]
}
webpack 只能打包处理一部分
高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法,需要借助于 babel-loader
进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码:
class Person {
// 通过 static 关键字,为 Person 类定义了一个静态属性 info
// webpack 无法打包处理“静态属性”这个高级语法
static info = 'person info'
}
console.log(Person.info)
运行如下的命令安装对应的依赖包:
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在 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'],
}
}
}
项目开发完成之后
,使用 webpack 对项目进行打包发布
的主要原因有以下两点:
放于内存中
,无法获取到最终打包生成的文件不会进行代码压缩和性能优化
在 package.json
文件的 scripts
节点下: 新增 build 命令如下:
"scripts": {
"dev": "webpack serve", // 开发环境中, 运行 dev 命令
"build": "webpack --mode production" // 项目发布时,运行 build 命令
}
–model
是一个参数项,用来指定 webpack 的运行模式
。production 代表生产环境,会对打包生成的文件进行代码压缩
和性能优化
。
注意:通过 –model 指定的参数项,会覆盖
webpack.config.js 中的 model 选项。
在 webpack.config.js 配置文件的 output 节点中,进行如下的配置:
output: {
path: path.join(__dirname, 'dist');
// 明确告诉 webpack 把生成的 bundle.js 文件存放到 dist 目录下的 js 子目录中
filename: 'js/bundle.js'
}
修改 webpack.config.js 中 的url-loader 匹配项,新增 outputPath 选项即可指定图片文件的输出路径:
{
test: /\.jpg|png|gif$/,
use: {
loader: 'url-loader',
options: {
limit: 22229
// 明确指定把打包生成的图片文件,存储到 dist 目录下的image 文件夹中
outputPath: 'image'
}
}
}
为了在每次打包发布时自动清理掉 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] // 挂载插件
企业级的项目在进行打包发布时,远比刚才的方式要复杂的多,主要的发布流程如下:
前端项目在投入生产环境之前,都需要对 JavaScript 源代码进行压缩混淆
,从而先小文件的体积,提高文件的加载效率。此时就不可避免的产生了另一个问题:
对压缩混之后的代码除错(debug)
是一件极其困难的事情
没有任何语义
的名称Source Map 就是一个信息文件,里面储存这位置信息
。也就是说,Source Map 文件中存储着代码压缩混淆前后
的对应关系
。
有了它,出错的时候,除错工具将直接显示原始代码,而不是转化后的代码
,能够极大的方便后期的调试。
在开发环境下
,webpack 默认启用了
Source Map 功能。当程序运行出错时,可以直接在控制台提示错误行的位置
,并定位到具体的源代码
开发环境下默认生成的 Source Map,记录的是生成后的代码的位置
。会导致运行时报错的行数
与源代码的行数
不一致的问题
开发环境下,推荐在 webpack.config.js
中添加如下的配置,即可保证运行时报错的行数
与源代码的行数
保持一致:
module.exports = {
mode: 'development',
// eval-source-map 仅限在“开发模式”下使用,不建议在“生产模式”下使用
// 此选项生成的 Source Map 能够保证“运行是报错的行数”与源代码的行数“保持一致
devtool: 'eval-source-map',
// 省略其他配置项...
}
在生成环境下
,如果省略了 devtool 选项
,则最终生成的文件中不包含 Source Map
。这能够防止原始代码
通过 Source Map 的形式暴露
给别有所图之人
在生产环境下,如果只想定位报错的具体行数
,且不想暴露源码
。此时可以将 devtool 的值设置为 nosources-source-map
。
在生产环境下,如果想在定位报错行数的同时
,展示具体报错的源码
。此时可以将 devtool
的值设置为 source-map
。
开发环境下:
eval-source-map
生产环境下:
关闭 Source Map
或将 devtool 的值设置为
能够掌握 webpack 的基本使用
webpack.config.js
、修改打包入口了解常用的 plugin 的基本使用
了解常用的 loader 的基本使用
loader 的调用过程
能够说出 Source Map 的作用
错误行
并显示对应的源码
官方给出的概念:Vue是一套用于构建用户界面
的前端框架
前端开发者最主要的工作,就是为网站的使用者
(又称为:网站的用户)构建出美观、舒适、好用的网页。
在传统的 web 前端开发中,是基于 jQuery + 模板引擎
的方式来构建用户界面的
使用 vue 构建用户界面,解决了 jQuery + 模块引擎
的诸多痛点,极大的提高了前端开发的效率和体验。
官方给 vue 的定位是前端框架
,因为它提供了构建用户界面的一整套解决方案
(俗称 vue 全家桶):
以及辅助 vue 项目开发
的一系列工具:
vue 框架的特性,主要体现在如下两个方面:
数据驱动视图
双向数据绑定
在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。示意图如下:
好处:当页面数据发生变化时,页面会自动重新渲染!
注意:数据驱动视图是单向的数据绑定
在填写表单
时,双向数据绑定可以辅助开发在不操作 DOM 的前提
下,自动
把用户填写的内容同步到
数据源中
MVVM
是 vue 实现数据驱动视图
和双向数据绑定
的核心原理。它把每个 HTML 页面 都拆分成了如下三个部分:
在 MVVM 概念中:
View 表示当前页面所渲染的 DOM 结构。
Model 表示当前页面渲染时所依赖的数据源。
ViewModel 表示 Vue 的实例,它是 MVVM 的核心
ViewModel 作为 MVVM 的核心
,是它把当前页面的数据源
(Model)和页面的结构
(View)连接在了一起。
当前,vue 共有 3 个大版本,其中:
2.x 版本的 vue 是目前企业级项目开发中的主流版本
3.x 版本的 vue 于 2020-09-19 发布,生态还不完善,尚未在企业级项目开发中普及和推广
1.x 版本的 vue 几乎被淘汰,不再建议学习与使用
总结:
3.x 版本的 vue 是未来企业级项目开发的趋势;
2.x 版本的 vue 在未来(1~2年内)会被逐渐淘汰;
vue2.x 中绝大多数的 API 与特性
,在 vue3.x 中同样支持
。同时,vue3.x 中还新增了 3.x 所特有的功能
、并废弃了某些 2.x 中的旧功能
:
新增的功能例如:
组合式API
、多根节点组件、更好的 typescript 支持等
废弃的旧功能如下:
过滤器
、不在支持$on,$off 和 $once 实例方法等
vue 官方提供的 vue-devtools 调试工具,能够方便开发者对 vue 项目进行调试与开发。
在线安装 vue-devtools
指令(Directives)
是 vue 为开发者提供的模块语法
,用于辅助开发者渲染页面的基本结构
。
vue 中指令按照不同的用途
可以分为如下 6 大类:
内容渲染
指令属性绑定
指令事件绑定
指令双向绑定
指令条件渲染
指令列表渲染
指令内容渲染指令
用来辅助开发者渲染 DOM 元素的文本内容
。常用的内容渲染指令有如下 3 个:
用法示例:
<p v-text="username">p>
<p v-text="gender">性别p>
注意:v-text 指令会覆盖元素内默认的值
vue 提供的 {{ }}
语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这个{{ }}语法的专业名称是插值表达式
(英文名为:Mustache
)
<p>姓名:{{username}}p>
<p>性别:{{gender}}p>
v-text指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html 这个指令:
<p v-html="discription">p>
如果需要为元素的属性
动态绑定属性值
,则需要用到 v-bind
属性绑定指令。用法示例如下:
<input type="text" v-bind:placeholder="inputValue">
<br>
<img v-bind:src="imgSrc" alt="">
在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值
之外,还支持 JavaScript 表达式的运算
,例如:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id">div>
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>
在原生的 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
是 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">
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>
为了方便对用户输入的内容进行处理
,vue 为 v-model 指令提供了 3 个修饰符,分别是:
修饰符 | 作用 | 示例 |
---|---|---|
.number | 自动将用户的输入值转为数值类型 | number=“age”/> |
.trim | 自动过滤用户输入的首尾空白字符 | trim=“msg”/> |
.lazy | 在 ”change“ 是而非 ”input“ 时更新 | lazy=“msg”/> |
条件渲染指令
用来辅助开发者按需控制 DOM 的显示与隐藏
。条件渲染指令有如下两个,分别是:
实现原理不同:
动态的创建或移除 DOM 元素
,从而控制元素在页面上的显示与隐藏;添加或移除 style="display: none;" 样式
,从而控制元素的显示与隐藏;性能消耗不同:
v-if
有更高的切换开销
,而 v-show
有更高的初始渲染开销
。
非常频繁地切换
,则使用 v-show 较好运行是条件很少改变
,则使用 v-if 较好v-if 可以单独使用,或配合 v-else 指令一起使用:
<div v-if="Math.random() > 0.5">
随机数大于 0.5
div>
<div>
随机数小于或等于 0.5
div v-else>
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>
vue 提供了 v-for
指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构
。
v-for 指令需要使用 item in items
的特殊语法,其中:
待循环的数组
当前的循环项
data: {
list: [ // 列表数据
{id: 1, name: 'zs'},
{id: 2, name: 'ls'}
]
}
// -------------- 分割线 ----------------
<ul>
<li v-for="item in list">姓名是: {{item.name}}</li>
</ul>
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
当列表的数据变化
时,默认情况下,vue 会尽可能的复用
已存在的 DOM 元素,从而提升渲染的性能
。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新
。
为了给 vue 一个提示,以便它 跟踪每个节点的身份,从而在保证有状态的列表被正确更新
的前提下,提升渲染的性能
。此时,需要为每项提供一个唯一的 key 属性
:
<ul>
<li v-for="user in userlist" :key="user.id">
<input type="checkbox">
姓名:{{user.name}}
li>
ul>
字符串
或者数字
类型必须具有唯一性
(即:key 的值不能重复)数据项 id 属性的值
作为 key 的值(因为 id 属性的值具有唯一性)index 的值
当作 key 的值没有任何意义
(因为 index 的值不具有唯一性)一定要指定 key 的值
(既提升性能、有防止列表状态紊乱)过滤器
(Filters
)常用于文本的格式化
。例如:
hello -> Hello
过滤器应该被添加在 JavaScript 表达式的尾部
,由“管道符
”进行调用,示例代码如下:
<p> {{ message | capitalize }} p>
<div v-bind:id="rawId | formatId">div>
过滤器可以用在两个地方:插值表达式
和 v-bind 属性绑定
。
在创建 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)
}
}
})
私有过滤器
和全局过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器
”,因为它只能在当前 vm 实例所控制的 el 区域内使用
。
如果希望在多个 vue 实例之间共享过滤器
,则可以按照如下的格式定义全局过滤器
:
// 全局过滤器 - 独立于每个 vm 实例之外
// Vue.filter() 方法接收两个参数:
// 第一个参数:是全局过滤器的“名字”
// 第二个参数,是全局过滤器的“处理函数”
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase + str.slice(1) + '~~'
})
示例代码如下:
// 串联调用多个过滤器
<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) + '...'
})
过滤器的本质
是 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) + '...'
})
过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x
的版本中 剔除了过滤器
相关的功能。
在企业级项目的开发中:
计算机属性
或方法
代替被剔除的过滤器功能基本使用步骤
指令
的基本用法
过滤器
的基本用法
单页面应该程序
(英文名 S
ingle P
age A
pplication)简称 SPA,顾名思义,指的是一个 Web 网站上 只有唯一的一个 HTML 页面
,所有的功能与交互都在这唯一的一个页面完成。
特点
单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源
(HTML、JavaScript 和 CSS)。
一旦页面加载完成了,SPA不会
因为用户的操作而进行页面的重新加载或跳转
。而是利用 JavaScript 动态的变换 HTML 内容,从而实现页面与用户的交互
优点
SPA 单页面应用程序最显著的3个优点如下:
良好的交互体验
良好的前后端工作分离模式
减轻服务器的压力
缺点
任务一种技术都有自己的局限性
,对于 SPA 单页面应用程序来说,主要的缺点有如下两个:
首屏加载慢
不利于 SEO
vue 官方提供了两种
快速创建工程化的 SPA 项目的方式:
vite
创建 SPA 项目vue-cli
创建 SPA 项目vite | vue-cli | |
---|---|---|
支持的 vue 版本 | 仅支持 vue3.x |
支持 3.x 和 2.x |
是否基于 webpack | 否 | 是 |
运行速度 | 快 |
较慢 |
功能完整度 | 小而巧(逐渐完善) |
大而全 |
是否建议在企业级开发中使用 | 目前不建议 | 建议在企业级开发中使用 |
按照顺序执行如下的命令,即可基于 vite 创建 vue 3.x 的工程化项目:
npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev f
使用 vite 创建的项目结构如下:
其中:
src
是项目的源代码目录(程序员写的所有代码都要放在此目录下)index.html
是 SPA 单页面应用程序中唯一的 HTML 页面在 src 这个项目源代码目录之下,包含了如下的文件和文件夹:
其中:
assets
目录用来存放项目中所有的静态资源文件
(CSS,fonts等)components
目录用来存放项目中的所有的自定义组件
App.vue
是项目的根组件
index.css
是项目的全局样式表
文件main.js
是整个项目的打包入口文件
在工程化的项目中,vue 要做的事情很单纯:通过 main.js
把 App.vue
渲染到 index.html
的指定区域中。
其中:
App.vue
用来编写待渲染的模板结构
index.html
中需要预留一个 el 区域
main.js
把 App.vue 渲染到了 index.html 所预留的区域中按照 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')
组件化开发
指的是:根据封装
的思想,把页面上可重用的部分封装为组件
,从而方便项目的开发和维护。
例如:http://www.ibootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。用户可以通过拖拽组件的方式,快速生成一个页面的布局结构。
前端组件化开发的好处主要体现在以下两方面:
复用性
和灵活性
开发效率
和后期的可维护性
vue 是一个完全支持组件化开发
的框架。vue 中规定组件的后缀名
是 .vue
。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。
每个 .vue 组件都由 3 部分构成,分别是:
template
-> 组件的模板结构
script
-> 组件的 JavaScript 行为
style
-> 组件的样式
其中,每个组件中必须包含 template 模板结构
,而 script 行为
和 style 样式
是可选的
组成部分
vue 规定:每个组件对应的模板结构
,需要定义到
中。
<template>
<!-- 当前组件的 DOM 结构,需要定义到 template 标签的内部 -->
</template>
注意: 是 vue 提供的容器标签
,只起到包裹性质的作用
,它不会被渲染为真正的 DOM 元素。
在组件的 节点中,支持使用前面所学的指令语法
,来辅助开发者渲染当前组件的 DOM 结构。
代码示例如下:
这是 App.vue 根组件
生成一个随机数字:{{ (Math.random() * 10).toFixed(2) }}
我在黑马程序员学习
在 vue 2.x
的版本中, 节点内的 DOM 结构仅支持单个根节点
这是 App.vue 根组件
这是副标题
vue 规定,组件内的
可以通过 name 节点为当前组件定义一个名称:
在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件
vue 组件渲染期间
需要用到的数据
,可以定义在 data 节点中:
methods
节点组件中的事件处理函数
,必须定义到 methods 节点
中,示例代码如下:
vue 规定:组件内的
其中
组件之间可以进行相互的引用
,例如:
vue 中组件的引用
原则:先注册后使用
。
vue 中注册组件的方式分为“全局注册
”和”局部注册
“两种,其中:
全局
注册的组件,可以在全局任何一个组件内使用
局部
注册的组件,只能在当前注册的范围内使用
// 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')
使用 app.component() 方法注册的全局组件,直接以标签的形式进行使用
即可,例如:
// 在 main.js 中,注册了 my-swiper 和 my-test 两个全局组件
spa_app.component('my-swiper', Swiper)
spa_app.component('my-test', Test)
这是 App 根组件
这是 App 根组件
全局
注册的组件,可以在全局任何一个组件内使用
局部
注册的组件,只能在当前注册的范围内使用
应用场景:
如果某些组件在开发期间的使用频率很高
,推荐进行全局
注册;
如果某些组件只在特定的情况下会被用到
,推荐进行局部
注册。
在进行组件的注册时,定义组件注册名称的方式
有两种:
kebab-case
命名法(俗称短横线命名法
,例如 my-swiper 和 my-search)PascalCase
命名法(俗称帕斯卡命名法
或大驼峰命名法
,例如 MySwiper 和 MySearch)短横线命名法的特点:
帕斯卡命名法的特点:
转化为短横线名称
进行使用注意:在实际开发中,推荐使用帕斯卡命名法
为组件注册名称,因为它的适用性更强
在注册组件期间,除了可以直接提供组件的注册名称
之外,还可以把组件的 name 属性
作为注册后组件的名称
,示例代码如下:
轮播图组件
import Swiper from './components/MySwiper.vue'
app.component(Swiper.name, Swiper) // 相当于 app.component('MySwiper', Swiper)
默认情况下,写在 .vue 组件中红的样式会全局生效
,因此很容易造成多个组件之间的样式冲突问题
。导致组件之间样式冲突的根本原因是:
唯一的 index.html 页面
进行呈现的影响整个 index.html 页面
中的 DOM 元素思考
:如何解决组件样式冲突的问题为每个组件分配唯一的自定义属性
,在编写组件样式时,通过属性选择器
来控制样式的作用域
,示例代码如下:
轮播图组件
为了提高开发效率和开发体验,vue 为 style 节点
提供了 scoped
属性,从而防止组件之间的样式冲突问题:
轮播图组件
/deep/
样式穿透如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的
。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器
为了提高组件的复用性
,在封装 vue 组件时需要遵守如下的原则:
DOM 结构
、style 样式
要尽量复用要展示的数据
,尽量由组件的使用者提供为了方便使用者
为组件提供要展示的数据,vue 组件提供了 props 的概念。
props 是
组件的自定义属性
,组件的使用者
可以通过 props 把数据传递到子组件内部
,供子组件内部进行使用。示例代码如下:
props 的作用
:父组件通过 props 向子组件传递要展示的数据。
props的好处
:提高了组件的复用性
在封装 vue 组件时,可以把动态的数据项
声明为 props
自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。示例代码如下:
标题: {{title}}
作者:{{author}}
如果父组件给子组件传递了未声明的 props 属性
,则这些属性会被忽略,无法被子组件使用,示例代码如下:
标题: {{title}}
作者:{{author}}
可以使用 v-bind 属性绑定
的形式,为组件动态绑定 props 的值,示例代码如下:
组件中如果使用 “camelCase
(驼峰命名法
)”声明了 props 属性的名称,则有两种方式为其绑定属性的值:
发布时间: {{pubTime}}
在实际开发中经常会遇到动态操作元素样式
的需求。因此,vue 允许开发者通过 v-bind
属性绑定指令,为元素动态绑定 class 属性的值
和行内的 style 样式
。
可以通过三元表达式,动态的为元素绑定 class 的类名
。示例代码如下:
MyDeep 组件
data() {
return { isItalic: true }
}
.thin { // 字体变细
font-weight: 200;
}
.italic { // 倾斜字体
font-style: italic;
}
如果元素需要动态绑定多个
class 的类名,此时可以使用数组的语法格式
:
MyDeep 组件
data() {
return {
isItalic: true,
isDelete: false,
}
}
对象语法
绑定 HTML 的 class使用数组语法
动态绑定 class 会导致模板结构臃肿
的问题。此时可以使用对象语法
进行简化
:
MyDeep 组件
delete
data() {
return {
classOjb: { // 对象中,属性名是 class 类名,值是布尔值
isItalic: true,
isDelete: false,
}
}
}
对象语法
绑定内联的 style:style
的对象语法
十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象
。CSS property 名可以用驼峰式(camelCase)或短横线分隔(kebab-case,记得用引号括起来)来命名
data() {
return {
active: 'red',
fsize: 30,
bgcolor: 'pink',
}
}
SPA
、只有一个页面、组件是对 UI 结构的复用
app.component
)、局部注册(components
)props 数组
class
、动态绑定 style
指的是:在封装组件时对外界传递过来的 props 数据
进行合法性的校验,从而防止数据不合法的问题。
使用数组类型
的 props 节点的缺点:无法为每个 props 指定具体的数据类型
对象类型
的 props 节点使用对象类型
的 props 节点,可以对每个 prop 进行数据类型的校验
,示意图如下:
对象类型的 props 节点
提供了多种数据验证方案,例如:
可以直接为组件 prop 属性指定基础的校验类型
,从而防止组件的使用者
为其绑定错误类型的数据
:
export default {
props: {
propA: String, // 字符串类型
propB: Number, // 数字类型
propC: Boolean, // 布尔值类型
propD: Array, // 数组类型
propE: Object, // 对象类型
propF: Date, // 日期类型
propG: Function, // 函数类型
propH: Symbol // 符号类型
}
}
如果某个 prop 属性值的类型不唯一
,此时可以通过数组的形式,为其指定多个可能的类型,示例代码如下:
export default {
props: {
// propA 属性的值可以是“字符串”或“数字”
propA: [String, Number],
}
}
如果组件的某个 prop 属性是必填项
,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项:
export default {
props: {
// 通过“配置对象”的形式,来定义 propB 属性的“验证规则”
propB: {
type: String, // 当前属性的是必须是 String 字符串类型
required: true // 当前属性的值是必须项,如果使用者没指定 propB 属性的值,则在终端进行警告提示
}
}
}
在封装组件时,可以为某个 prop 属性指定默认值
。示例代码如下:
export default {
props: {
// 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
propC: {
type: Number,
default: 100 // 如果使用者没有指定 propC 的值,则 propC 属性的默认值为 100
}
}
}
在封装组件时,可以为 prop 属性指定自定义的验证函数
,从而对 prop 属性的值进行更加精确的控制
:
export default {
// 通过“配置对象”的形式,来定义 propD 属性的“验证规则”
propD: {
// 通过 validator 函数,对 propD 属性的值进行检验,“属性的值”可以通过形参 value 进行接收
validator(value) {
// propD 属性的值,必须匹配下列字符串中的一个
// validator 函数的返回值为 true 表示验证通过,false 表示验证失败
return ['success', 'warning', 'danger'].indexOf(value) !== -1;
}
}
},
}
计算属性本质上
就是一个 function 函数
,它可以实时监听
data 中数据的变化,并 return 一个计算后的新值
供组件渲染 DOM 是使用。
计算属性需要以 function 函数
的形式声明到组件的 computed 选项
中,示例代码如下:
{{ count }} 乘以 2 的值为:{{ plus }}
export default {
data() {
return { count: 1 }
},
computed: {
plus() { // 计算属性:监听 data 中 count 值的变化,自动计算出 count * 2 之后的新值
return this.count * 2
}
}
}
使用注意点
必须定义在 computed 节点中
必须是一个 function 函数
必须有返回值
必须当作普通属性使用
相对于方法来说,计算属性会缓存计算的结果
,只有计算属性的依赖项发生变化
时,才会重新进行运算
,因此计算属性的性能更好:
computed: {
plus() { // 计算属性的计算结果会被缓存,性能好
console.log('计算属性被执行了')
return this.count * 2
}
}
methods: {
plus() { // 方法的计算结果无法被缓存,性能低
console.log('方法被执行了')
return this.count * 2
}
}
在封装组件时,为了让组件的使用者
可以监听到组件内状态的变化
,此时需要用到组件的自定义事件
在封装组件时:
声明
自定义事件触发
自定义事件在使用组件时:
监听
自定义事件声明
自定义事件开发者为自定义组件封装的自定义事件
,必须事先在 emits
节点中声明,实例代码如下:
<template>
<h3>Counter组件</h3>
<button>+1</button>
</template>
export default {
// my-counter 组件的自定义事件,必须事先声明到 emits 节点中
emits: ['change'],
}
在 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 事件
}
}
}
监听
自定义事件在使用自定义的组件时,可以通过 v-on
的形式监听自定义事件
。示例代码如下:
<!-- 使用 v-on 指令绑定事件监听 -->
<my-counter @change="getCount"></my-counter>
methods: {
getCount() {
console.log('监听到了 count 值的变化!')
}
}
传参
在调用 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 是双向数据绑定指令,当需要维护组件内外数据同步
时,可以在组件上使用 v-model 指令。示意图如下:
外界数据的变化
会自动同步
到 counter 组件中自动同步到外界
v-bind:
属性绑定的形式,把数据传递给子组件props
接收父组件传递过来的数据对象格式
type
、default
、required
、validatorcomputed 节点
、必须return一个结果、缓存计算结果
emits
、$emit()
实现组件内外的数据同步
v-model
: props名称、emits、$emit(‘update
:props名称’)watch 侦听器
允许开发者监视数据的变化,从而针对数据的变化做特定的操作
。例如,监视用户名的变化并发起请求,判断用户名是否可用。
开发者需要在 watch 节点
下,定义自己的侦听器。示例代码如下:
export default {
data() {
return { username: ''}
},
watch: {
// 监听 username 的值的变化,
// 形参列表中,第一个值是“变化后的新值”,第一个值是“变化之前的旧值”
username(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
监听 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)
}
}
}
默认情况下,组件在初次加载完毕后不会调用 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,
}
}
当 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 值的变化无法被监听到
}
}
如果只想监听对象中单个属性的变化
,则可以按照如下的方式定义 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 值的变化无法被监听到
}
}
计算属性和侦听器侧重的应用场景不同
:
计算属性侧重于监听多个值
的变化,最终计算并返回一个新值
侦听器侧重于监听单个数据
的变化,最终执行特定的业务处理,不需要有任务返回值
运行的过程
组件的生命周期
指的是:组件从创建
-> 运行
(渲染) -> 销毁
的整个过程,强调的是一个时间段
监听
组件的不同时刻
vue 框架
为组件内置了不同时刻的生命周期函数
,生命周期函数会伴随着
这组件的运行而自动调用
。例如:
在内存中被创建完毕
之后,会自动调用 created
函数mounted
函数被销毁完毕
之后,会自动调用 unmounted
函数监听
组件的更新
当组件的 data 数据更新
之后,vue 会自动重新渲染组件
的 DOM 结构,从而保证 View 视图
展示的数据和 Model 数据源
保持一致。
当组件被重新渲染完毕
之后,会自动调用 updated
生命周期函数。
主要的
生命周期函数生命周期函数 | 执行时机 | 所属阶段 | 执行次数 | 应用场景 |
---|---|---|---|---|
created |
组件在内存中创建完毕后 | 创建阶段 |
唯一1次 | 发 Ajax 请求初始数据 |
mounted |
组件初次在页面中渲染完毕后 | 创建阶段 |
唯一1次 | 操作 DOM 元素 |
updated | 组件咋页面被重新渲染完毕后 | 运行阶段 | 0 或 多次 | - |
unmounted | 组件被销毁后(页面和内存) | 销毁阶段 | 唯一1次 | - |
注意:在实际开发中,created
是最常用的
生命周期函数!
全部的
生命周期函数生命周期函数 | 执行时机 | 所属阶段 | 执行次数 | 应用场景 |
---|---|---|---|---|
beforeCreate | 在内存中开始创建组件之前 | 创建阶段 |
唯一1次 | - |
created |
组件在内存中创建完毕后 | 创建阶段 |
唯一1次 | 发 Ajax 请求初始数据 |
beforeMount | 在把组件初次渲染到页面之前 | 创建阶段 |
唯一1次 | - |
mounted |
组件初次在页面中渲染完毕后 | 创建阶段 |
唯一1次 | 操作 DOM 元素 |
beforeUpdate | 在组件被重新渲染之前 | 运行阶段 | 0 或 多次 | - |
updated | 组件咋页面被重新渲染完毕后 | 运行阶段 | 0 或 多次 | - |
beforeUnmount | 在组件被销毁之前 | 销毁阶段 | 唯一1次 | - |
unmounted | 组件被销毁后(页面和内存) | 销毁阶段 | 唯一1次 | - |
疑问:为什么不在 beforeCreate
中发 ajax 请求初始数据?
在项目开发中,组件之间的关系分为如下 3 种:
父组件通过 v-bind 属性绑
定向子组件共享数据。同时,子组件需要使用 props
接收数据。示例代码如下:
// 父组件
data() {
return {
message: 'hello vue',
userinfo {
name: 'zs',
age: 20,
}
}
}
// 子组件
测试父子传值
{{msg}}
{{user}}
子组件通过自定义事件
的方式向父组件共享数据。示例代码如下:
// 子组件
// 父组件
// 1. 监听子组件的自定义事件 n1change
父组件在使用子组件期间,可以使用 v-model 指令
维护组件内数据的双向同步:
兄弟组件之间
实现数据共享的方案是 EventBus
。可以借助于第三方的包 mitt
来创建 eventBus 对象
,从而实现兄弟组件之间的数据共享。示意图如下:
在项目中运行如下的命令,安装 mitt 依赖包:
npm install mitt
在项目中创建公共的 eventBus 模块
如下:
// eventBus.js
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus
数据接收方
自定义事件在数据接收方,调用 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
})
}
}
数据发送方
触发事件在数据发送方,调用 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() 方法触发自定义事件,并发送数据
}
}
}
后代关系组件之间共享数据,指的是父节点的组件
向其子孙组件
共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide
和 inject
实现后代关系组件之间的数据共享。
provide
共享数据父节点的组件可以通过 provide 方法
,对其子孙组件
共享数据:
inject
接收数据子孙节点可以使用 inject
数组,接收父级节点向下共享的数据
。示例代码如下:
子孙组件 --- {{color}}
响应式的数据
父节点使用 provide 向下共享数据时,可以结合 computed 函数
向下共享响应式的数据
。示例代码如下:
import { computed } from 'vue' // 1. 从 vue 中按需导入 computed 函数
如果父级节点共享的是响应式的数据
,则子孙节点必须以 .value
的形式进行使用。示例代码如下:
子孙组件 --- {{color.value}}
vuex 是终极的
组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效
、清晰
、且易于维护
属性绑定
事件绑定
组件上的 v-model
EventBus
provide
& inject
vuex
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
导入 axios
(代码臃肿)完整的请求路径
(不利用后期的维护)在 main.js
入口文件中,通过app.config.globalProperties
全局挂载 axios,示例代码如下:
父向子传值:v-bind 属性绑定
子向父传值:v-on 事件绑定
兄弟组件之间共享数据:EventBus
Vuex
是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。
一般情况下,只有组件之间共享
的数据,才有必要存储到 vuex
中;对于组件中的私有数据
,依旧存储在组件自身的 data 中
即可
npm install vuex --save
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
// state 中存放的就是全局共享的数据
state: { count: 0 }
})
new Vue({
el: '#app',
render: h => h(app),
router,
将创建的共享数据对象,挂载 到 vue 实例中
所有的组件,就可以直接从 store 中获取全局的数据了
store
})
{
"semi" :false, // 不使用 ";" 符号
"singleQuote": true // 将 "" 改为 ''
}
Vuex 中的主要核心概念如下:
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'])
}
Mutation 用于变更 Store 中的数据。
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
}
}
})
// 触发 mutation
methods: {
handlel() {
// 触发 mutations 的第一个方式
this.$store.commit('add')
}
}
可以在触发 mutations 是传递参数:
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
addN(state, step) {
// 变更状态
state.count += step
}
}
})
// 触发 Mutation
methods: {
handlel2() {
// 在调用 commit 函数,
// 触发 Mutations 时携带参数
this.$store.commit('addN', 3 )
}
}
this.$store.commit() 是触发 mutations 的第一种方式,触发 mutations 的第二种方式:
// 1. 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'
通过刚才导入的 mapMutations 函数,将需要 的 Mutations 函数,映射为当前组件的 methods 方法:
// 2. 将指定的 mutations 函数,映射为当前组件的 methods 函数
methods: {
...mapMutations(['add', 'addN']),
handlel() {
this.add();
}
}
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'])
}
Getter 用于对 Store 中的数据进行加工处理形成新的数据。
// 定义 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'])
}
immediate
、deep
、监听对象中单个属性的变化created
、mounted父子组件
、兄弟组件
、后代组件main.js
入口文件中进行配置config.globalProperties
.$http = axiosref 用来辅助开发者在不依赖于 jQuery 的情况下
,获取 DOM 元素或组件的引用
每个 vue 的组件实例上,都包含一个 \$refs 对象
,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 \$refs 指向一个空对象
。
MyRef 组件
如果想要使用 ref 引用页面上的 DOM 元素
,则可以按照如下的方式进行操作:
MyRef 组件
如果想要使用 ref 引用页面上的组件实例
,则可以按照如下的方式进行操作:
通过布尔值 inputVisible
来控制组件中的文本框与按钮的按需切换。示例代码如下:
当文本框展示出来之后,如果希望它立即获得焦点,则可以为其添加 ref 引用,并调用原生 DOM 对象的 .focus()
方法即可。示例代码如下:
$nextTick(cb)
方法组件的 $nextTick(cb)
方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行
。通俗的理解是:等组件的 DOM 异步重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素
动态组件指的是动态切换组件的显示与隐藏
。vue 提供了一个内置的
组件,专门用来实现组件的动态渲染。
占位符
is 属性
动态指定要渲染的组件名称
默认情况下,切换动态组件时无法保持组件的状态
。此时可以使用 vue 内置的
组件保持动态组件的状态,示例代码如下:
插槽
(Slot
)是 vue 为组件的封装者
提供的能力。允许开发者在封装组件时,把不确定的
、希望由用户指定的部分
定义为插槽。
可以把插槽认识是组件封装期间,为用户预留的内容的占位符
在封装组件时,可以通过
元素定义插槽
,从而为用户预留内容占位符
。示例代码如下:
这是 MyCom1 组件的第一个 p 标签
这是 MyCom1 组件最后一个 p 标签
~~~用户自定义的内容~~~
没有预留插槽
的内容会被丢弃如果在封装组件时没有预留任何
,则用户提供的任何自定义内容
都会被丢弃
。示例代码如下:
这是 MyCom1 组件的第一个 p 标签
这是 MyCom1 组件最后一个 p 标签
~~~用户自定义的内容~~~
封装组件时,可以为预留的 后备内容
(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例代码如下:
这是 MyCom1 组件的第一个 p 标签
这是后备内容
这是 MyCom1 组件最后一个 p 标签
如果在封装组件时需要预留多个插槽节点
,则需要为每个 具体的 name 名称
。这种带有具体名称的插槽
叫做“具名插槽”。示例代码如下:
注意:没有指定 name 名称的插槽,会有隐含的名称叫做“default
”。
在向具名插槽提供内容的时候,我们可以在一个 元素上使用
v-slot
指令,并以 v-slot 的参数的形式提供其名称。示例代码如下:
滕王阁序
豫章故郡,洪都新府。
星分翼轸,地接衡庐。
襟三江而带五湖,控蛮荆而引瓯越。
落款:王勃
my-com-2>
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容(v-slot:
)替换为字符 #
。例如 v-slot:
header 可以被重写成 #
header
滕王阁序
豫章故郡,洪都新府。
星分翼轸,地接衡庐。
襟三江而带五湖,控蛮荆而引瓯越。
落款:王勃
my-com-2>
在封装组件的过程中,可以为预留的 带有 props 数据的
叫做“作用域插槽
”。示例代码如下:
这是 TEST 组件
{{ scope }}
在每个 vue 组件中,可以在 directives
节点下声明私有自定义指令
。示例代码如下:
directives: {
// 自定义一个私有指令
focus: {
// 当被绑定的元素插入到 DOM 元素中时, 自动触发 mounted 函数
mounted(el) {
el.focus() // 让被绑定的元素自定获得焦点
}
}
}
需要通过“单页面应用程序的实例对象”进行声明,示例代码如下:
const app = Vue.createApp({})
// 注册一个全局自定义指令 'v-focus'
app.directive('focus', {
mounted(el) {
// Focus the element
el.focus()
}
})
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
】
如果 mounted 和 updated 函数中的逻辑完全相同,则可以简写成如下格式:
app.directive('focus', (el) => {
// 在 mounted 和 updated 时都会触发相同的业务处理
el.focus()
})
在绑定指令时,可以通过“等号
”的形式为指令绑定具体的参数值
,示例代码如下:
{{count}}
// 自定义 v-color 指令
app.directive('color', (el, binding) => {
// binding.value 就是通过“等号”为指令绑定的值
el.style.color = binding.value
})
ref
引用 DOM 和组件实例
ref 属性
指定引用的名称、使用 this.$refs
访问到引用实例$nextTick
的调用时机
keep-alive
元素的作用
插槽
的基本用法
作用域插槽
、v-slot:
简写为 #
自定义指令
全局自定义指令
路由(英文名:router)就是对应关系
。路由分为两大类:
后端路由指的是:请求方式
、请求地址
与 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
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有的组件的展示与切换
都在这唯一的一个页面内完成。
此时,不同组件之间的切换
需要通过
来实现。
结论:在 SPA 项目中,不同功能之间的切换
,要依赖于前端路由
来完成!
通俗易懂的概念:Hash 地址
与组件
之间的对应关系
点击了
页面上的路由链接
URL 地址栏
中的 Hash 值
发生了变化前端路由监听到了 Hash 地址的变化
Hash 地址对应的组件
渲染到浏览器中结论:前端路由,指的是 Hash 地址
与组件之间
的对应关系
!
步骤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:通过is 属性
,动态切换要显示的组件。示例代码如下:
App 组件
步骤3: 在组件的结构中声明如下 3 个 链接,通过点击不同的 a 标签,切换浏览器地址栏中的 Hash 值:
<a href="#/home">Homea>
<a href="#/movie">Moviea>
<a href="#/about">Abouta>
步骤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
是 vue.js 官方给出的路由解决方案
。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。
vue-router 目前有 3.x
的版本 和4.x
的版本。其中:
vue2
进行使用vue3
进行使用路由链接
和占位符
路由模块
导入并挂载
路由模块在 vue3 的项目中,只能安装并使用 vue-router 4.x。安装的命令如下:
npm install vue-router@next -S
在项目中定义 MyHome.vue
、MyMovie.vue
、MyAbout.vue
三个组件,将来要使用 vue-router 来控制它们的展示与切换
路由链接
和占位符
可以使用
标签来声明路由链接
,并使用
标签来声明路由占位符
。示例代码如下:
App 组件
首页
电影
关于
在项目中创建 router.js
路由模块,在其中按照如下 4 个步骤
创建并得到路由的实例对象:
// 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')
路由重定向
指的是:用户在访问地址 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
可以通过如下的两种方式,将激活的路由链接
进行高亮显示:
默认的
高亮 class 类自定义
路由高亮的 class 类被激活的路由链接,默认会应用一个叫做 router-link-active
的类名。开发者可以使用此类名选择器
,为激活的路由链接
设置高亮的样式:
/* 在 index.css 全局样式表中, 重新 router-link-active 的样式 */
.router-link-active {
background-color: red;
color: white;
font-weight: bold;
}
在创建路由的实例对象时,开发者可以基于 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 },
]
})
通过路由实现组件的嵌套展示
,叫做嵌套路由
子路由链接
和子路由占位符
children
属性嵌套声明
子路由规则子路由链接
和子路由占位符
在 About.vue 组件中, 声明 tab1 和 tab2 的子路由链接
以及子路由占位符
。示例代码如下:
这是 MyAbout 组件
tab1
tab2
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 组件
]
},
]
})
思考:有如下 3 个路由链接:
<router-link to="movie/1">电影1router-link>
<router-link to="movie/2">电影2router-link>
<router-link to="movie/3">电影3router-link>
定义如下 3 个路由规则,是否可行???
{ path: '/movie/1', component: Movie }
{ path: '/movie/2', component: Movie }
{ path: '/movie/3', component: Movie }
缺点:路由规则的复用性差。
动态路由指的是:把 Hash 地址中的可变的部分
定义为参数项
,从而提高路由规则的复用性
。在 vue-router 中使用使用英文的冒号
(:
)来定义路由的参数项,示例代码如下:
// 路由中的动态参数以 :进行声明,冒号后面的是动态参数的名称
{ path: '/movie/:id', component: Movie }
// 将以下 3 个路由规则,合并成了一个,提高了路由规则的复用性
{ path: '/movie/1', component: Movie }
{ path: '/movie/2', component: Movie }
{ path: '/movie/3', component: Movie }
$route.params
参数对象通过动态路由匹配的方式渲染出来的 组件中,可以使用 $route.params
对象访问到动态匹配的参数值
。
MyMovie 组件 --- {{$route.params.id}}
为了简化路由参数的获取形式
,vue-router 允许在路由规则
中开始 props 传参
。示例代码如下:
// 1. 在定义路由规则时,声明 props: true 选项,
// 即可在 Movie 组件中,以 props 的形式接收到路由规则匹配到的参数项
{ path: '/movie/:id', component: Movie, props: true }
MyMovie 组件 --- {{id}}
通过调用 API
实现导航的方式,叫做编程式导航
。与之对应的,通过点击链接
实现导航的方式,叫做声明式导航
。例如:
链接
、vue 项目中点击
都属于声明式导航
location.href
跳转到新页面的方式,属于编程式导航vue-router 提供了许多编程式导航的 API,其中最常用的两个 API 分别是:
$router.push
(‘hash 地址’)
$router.go
(数值 n)
调用 this.$router.push
() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。示例代码如下:
这是 MyHome 组件
调用 this.$router.go
() 方法,可以在浏览历史中进行前进和后退。示例代码如下:
这是 MyMovie 组件 --- {{ id }}
通过 name 属性
为路由规则定义名称
的方式,叫做命名路由
。示例代码如下:
{
path: '/movie/:id',
// 使用 name 属性为当前的路由规则定义一个“名称”
name:'mov',
component: Movie,
props: true,
}
注意:命名路由的 name 值不能重复,必须保证唯一性!
为 name 属性
指定要跳转到的路由规则。期间还可以用 params 属性
指定跳转期间要携带的路由参数。示例代码如下:
这是 MyHome 组件
go to Movie
调用 push 函数
期间指定一个配置对象
,name
是要跳转到的路由规则、params
是携带的路由参数:
这是 MyHome 组件
导航守卫
可以控制路由的访问权限
。示意图如下:
全局导航守卫
会拦截每个路由规则
,从而对每个路由进行访问权限
的控制。可以按照如下的方式定义全局导航守卫:
// 创建路由实例对象
const router = createRouter({ ... })
// 调用路由实例对象的 beforeEach 函数,声明“全局前置守卫”
// fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
// 因此 fn 叫做 “守卫方法”
router.beforeEach(fn)
全局导航守卫
的守卫方法中接收 3 个形参,格式为:
// 创建路由实例对象
const router = createRouter({ ... })
// 全局前置守卫
router.beforeEach((to, from, next) => {
// to 目标路由对象
// from 当前导航正要离开的路由对象
// next 是一个函数,表示放行
})
注意:
不声明 next 形参
,则默认允许用户访问每一个路由!
声明了 next 形参
,则必须调用 next() 函数
,否则不允许用户访问任何一个路由!参考示意图,分析 next 函数的 3 中调用方式最终导致的结果:
直接放行:next()
强制其停留在当前页面
:next(false
)
强制其跳转到登录页面
:next(‘/login
’)
// 全局前置守卫
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 直接放行,允许访问“后台主页”
}
})
use
(router)children
属性进行路由嵌套、子路由的 hash 地址不要以/开头
冒号
声明参数项、this.$route
.params、props: true
beforeEach
((to, from, next
) => { })vue-cli
(俗称:vue 脚手架
)是 vue 官方提供的、快速生成 vue 工程化项目
的工具。
特点
:
vue-cli 的中文官网首页:https://cli.vuejs.org/zh/
vue-cli 是基于 Node.js 开发出来的工具,因此需要使用 npm
将它安装为全局可用的工具
:
// 全局安装 vue-cli
npm install -g @vue/cli
// 查看 vue-cli 的版本,检验 vue-cli 是否安装成功
vue --version
不识别 vue 命令
的问题默认情况下,在 PowerShell 中执行 vue –version
命令会提示错误信息
解决方案如下:
管理员身份
运行 PowerShellset-ExecutionPolicy RemoteSigned
命令 Y
,回车
即可vue-cli 提供了创建项目的两种方式
# 基于【命令行】的方式创建 vue 项目
vue create 项目名称
# OR
# 基于 【可视化面板】 创建 vue 项目
vue ui
步骤1:在终端下运行 vue ui
命令,自动在浏览器中打开创建项目的可视化面板
:
步骤2:在详情
页面填写项目名称
:
步骤3:在预设
页面选择手动配置项目
:
步骤4:在功能
页面勾选需要安装的功能
(Choose Vue Version、Babel、CSS 预处理器、== ==):
步骤5:在配置
页面勾选 vue 的版本
和需要的预处理器
:
步骤6:将刚才所有的配置保存为预设
(模板),方便下一次创建项目时直接复用之前的配置
:
步骤7:创建项目并自定安装依赖包:
vue ui 的本质
:通过可视化的面板采集
到用户的配置信息后,在后台基于命令行的方式
自动初始化项目:
项目创建完成后:自动进入项目仪表盘
:
命令行
创建 vue 项目步骤1:在终端下运行 vue create
dome2 命令,基于交互式的命令行
创建 vue 的项目:
步骤2: 选择要安装的功能:
步骤3:使用上下箭头
选择 vue 的版本,并使用回车键
确认选择:
步骤4:使用上下箭头
选择要使用的 css 预处理器,并使用回车键
确认选择:
步骤5:使用上下箭头
选择如何存储插件的配置信息
,并使用回车键
确认选择:
步骤6:是否将刚才的配置保存为预设:
步骤7:选择如何安装项目中的依赖包:
步骤8:开始创建项目并自定安装依赖包:
步骤9:项目创建完成:
在 vue2 的项目中,只能安装并使用 3.x 版本
的 vue-router。
版本3 和版本4的路由的最主要
的区别:创建路由模块的方式不同!
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. 向外共享路由对象
步骤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')
在实际开发中,前端开发者可以把自己封装的 .vue 组件
整理、打包、并发布为 npm 的包
,从而供其他人下载和使用。这种可以直接下载并在项目中使用的现场组件,就叫做 vue 组件库。
vue 组件库
和 bootstrap
的区别二者之间存在本质的区别:
纯粹的原材料
(css 样式、HTML 结构以及 js 特效),需要由开发者做进一步的组装
和改造
遵循 vue 语法、高度定制
的现场组件,开箱即用PC 端
移动端
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) npm i element-ui -S
引入
element-ui开发者可以一次性完整引入所有的
element-ui 组件,或是根据需求,只按需引入用到的
element-ui 组件:
额外引入
一些用不到的组件,导致项目体积过大
只会引入用到的组件
,能起到优化项目体积的目的
完整
引入在 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')
按需
引入借助 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')
封装为独立的模块
在 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'
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')
需要在 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')
拦截器
(英文名:Interceptors)会在每次发起 Ajax 请求
和得到响应
的时候自动被触发
应用场景:
通过 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)
})
注意:失败的回调函数可以被省略!
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
展示 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
})
通过 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)
})
注意:失败的回调函数可以被省略!
关闭 Loading 效果
调用 Loading 实例
提供的 close()
方法即可关闭 Loading 效果
,示例代码如下:
// 响应拦截器
axios.interceptors.response.use(response => {
// 调用 Loading 实例的 close 方法即可关闭 loading 效果
loadingInstance.close()
return response
})
vue 项目运行的地址:http://localhost:8080/
API 接口运行的地址:https://www.escook.cn/api/users
由于当前的 API 接口没有开启 CORS
跨域资源共享,因此默认情况下,上面的接口无法请求成功
!
代理
解决接口的跨域问题通过 vue-cli 创建的项目在遇到接口跨域问题时,可以通过代理
的方式来解决:
请求根路径
设置为 vue 项目的运行地址
(接口请求不再跨域)转交给 proxy 代理
替换为
devServer.proxy 属性的值,发起真正的数据请求
转发给 axios
步骤1,在 main.js
入口文件中,把 axios 的请求根路径
改造为当前 web 项目的根路径
:
// axios.defaults.baseURL = 'httts://www.escook.cn'
// 配置请求根路径
axios.defaults.baseURL = 'httts://localhost:8080'
步骤2,在项目根目录
下创建 vue.config.js
的配置文件,并声明如下的配置:
module.exports = {
devServer: {
// 当前项目在开发调试阶段,
// 会将任何未知请求(没有匹配到静态文件的请求)代理到 https://www.escook.cn
proxy: 'https://www.escook.cn',
}
}
vue create 项目名称
按需引入
、参考官方文档进行配置常见的组件
的用法
request
.use()、axios.interceptors.response
.use()vue.config.js
、devServer.proxy