2022-05-29 ajax,axios,promise,async和await,bootstrap,npm,node,webpack,git
本文介绍了AJAX技术的基础概念与应用方法。主要内容包括:1) AJAX原理与特点,通过XMLHttpRequest对象实现异步通信;2) URL组成与查询参数使用;3) axios库的核心配置(请求方法、参数处理、错误捕获);4) HTTP请求/响应报文结构解析;5) 接口文档的作用与Form-serialize插件快速收集表单数据;6) Bootstrap模态框的两种控制方式。最后演示了非项目
一.AJAX
1.什么是AJAX?
简答:浏览器与服务器通信的技术
原理:通过XMLHttpRequest
对象来和服务器通信,可以使用text,JSON,XML,HTML
等格式发送和接收数据
特点:异步,即不刷新页面的情况下和服务器通信
2.如何学习ajax?
先下载和使用axios
库,和服务器进行数据通信,
再回头学习XMLHTTPRequest
对象的使用,从而了解ajax的底层原理
3.URL的组成和每个部分的作用,查询参数
网页地址简称网址,本质是统一资源定位符(Uniform Resource Locator)
分类:网页资源(xxx.com/index.html),图片资源(xxx.com/logo.png),数据资源(xxx.com/api/province)
URL有三个部分组成:协议+域名+资源路径
http://hmajax.itheima.net/api/provice
http协议:超文本传输协议,规定浏览器和服务器之间传输数据的格式
4.axios的核心配置:查询参数,请求方法,错误处理等
- 查询参数:
http://hmajax.itheima.net/api/city?pname=河北省
params:{
pname:'河北省'
}
案例:地区查询(略)
- 请求方法:
get,post,put
methods:'post'
data:{
//提交的数据
}
- 错误处理
.then()
.catch(err){
alert(err.data.message)
}
5.http的请求报文
5.1.什么是请求报文?
浏览器按照http协议要求的格式,发送给服务器的内容
5.2.请求报文长这样
POST /login HTTP/1.1----------1.请求行=请求方法+URL+协议
Host: www.example.com-----------2.请求头:多行键值对,即 键:值
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Content-Type: application/x-www-form-urlencoded 重点关注这个请求头,它规定了请求体的格式:application/x-www-form-urlencoded(表单数据)或 application/json(JSON数据)。
Content-Length: 28
Accept: text/html,application/xhtml+xml
Connection: keep-alive
--------------3.空行:分隔请求头,空行后面是发送给服务器的资源
username=john&password=123456-----------4.请求体(可选):发送给服务器的资源
- 请求行:位于报文第一行,格式为
[HTTP方法] [资源路径] [HTTP版本]
。例如,GET /index.html HTTP/1.1
- 请求头:多行键值对,提供额外信息如客户端类型、内容格式等。常见头部包括
Host
(服务器域名)、User-Agent
(客户端标识)、Content-Type
(请求体格式)和Content-Length
(请求体大小) - 空行:一个空白行,用于分隔头部和请求体。
- 请求体:可选部分,用于POST等方法的表单提交或数据上传。格式取决于
Content-Type
5.3.在哪里可以看到请求报文?
请求头:F12>网络>fetch/XML>标头>请求标头>查看源代码
请求体:F12>网络>fetch/XML>载荷
5.4.请求报文的错误排查
目的是通过请求报文排查错误原因并修复
登录报错的示例:
6.http的响应报文
6.1.什么是响应报文?
http协议:规定浏览器发送服务器返回内容的格式
响应报文:服务器按照http协议要求的格式返回给浏览器的内容
6.2.响应报文长这样
HTTP/1.1 200 OK -------状态行=协议版本+状态码+状态描述
Content-Type: text/html; charset=UTF-8 -------响应头:以键值对格式携带的附加信息
Content-Length: 132
Connection: keep-alive
Date: Tue, 15 May 2024 10:00:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Cache-Control: max-age=3600
--------空行:分隔响应头和响应体
<!DOCTYPE html> -------------响应体:服务器返回的资源(实际数据部分)
<html> <head>
<title>示例页面</title>
</head> <body> <h1>欢迎访问!</h1> </body>
</html>
7.什么是接口文档?
描述接口的文档
接口:使用ajax与服务器通信是使用的UTL,请求方法,参数等
一个有效的接口文档:apifox.cn/apidoc/project-1937884/doc-1695440
8.form-serialize插件
作用:快速表单收集元素的值
下载:略
使用:const data=serialize(form,{hash:true,empty:true})
hash:true==>data是JS对象
hash:false==>data是查询参数
emtpy==>是否返回空值
注意事项:input
框要设置name
属性
9.引入Bootstrap模态框
9.1.通过属性控制显隐
- 在点击按钮上绑定
data-bs-toggle='modal-backdrop' data-bs-target='myModal
’来控制模态框的显示 - 在点击按钮上绑定
data-bs-dismiss='modal'
来控制隐藏
<!-- 模态框 -->
<div id="bookModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">添加图书</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="bookTitle">书名</label>
<input type="text" id="bookTitle" placeholder="请输入书名">
</div>
<div class="form-group">
<label for="bookAuthor">作者</label>
<input type="text" id="bookAuthor" placeholder="请输入作者">
</div>
<div class="form-group">
<label for="bookPublisher">出版社</label>
<input type="text" id="bookPublisher" placeholder="请输入出版社">
</div>
</div>
<div class="modal-footer">
<button id="cancelBtn" class="btn btn-secondary">取消</button>
<button id="confirmBtn" class="btn btn-primary">确认</button>
</div>
</div>
</div>
9.2.通过JS控制显隐
//html
<button id="addBtn">
<i class="fas fa-plus"></i><span id="addBtn">添加</span>
</button>
//js
var myModel=document.getElementById("myModal");
var model=new bootstrap.Modal(myModel);//实例化模态框对象
document.querySelector('#addBtn').addEventListener('click',function(){
model.show(); //显示模态框
})
10.非项目环境如何调用接口并动态渲染页面?
//获取文章列表
const creator='张三三'
function getBookList() {
axios({
url: "https://hmajax.itheima.net/api/books",
method: "get",
params: {
creator
}
})
.then(res => {
const bookList = res.data.data;
// 尝试打印bookList
console.log("bookList:", bookList);
const htmlStr = bookList.map((book, index) => {
return `<tr>
<td>${index + 1}</td>
<td>${book.bookname}</td>
<td>${book.author}</td>
<td>${book.publisher}</td>
<td><a href="#">删除</a><a href="#" id="editBtn" class="btn btn-warning" data-bs-toggle="modal"
data-bs-target="#myModal">编辑</a></td>
</tr>`
}).join("");
document.querySelector("table>tbody").innerHTML = htmlStr;
})
}
getBookList();
11.如何实现图片上传功能?
接口文档:https://apifox.cn/apidoc/docs-site/1937884/api-49760221
接口地址: https://hmajax.itheima.net/api/uploadimg
步骤:
- 获取图片文件对象
- 使用FormData携带图片文件
- 提交表单数据到服务器,使用图片url网址
示例:
//html
<input type="file" class="upload">
//js
// 1.获取文件对象
const upload = document.querySelector('.upload');
// 监听文件变化事件
upload.addEventListener('change', function (e) {
//target中的files属性是一个FileList对象(伪数组),包含了用户选择的文件列表
// console.log(e.target.files[0]);//0:File {name: 'JSAPI2.png', lastModified: 1751373447138
const file = e.target.files[0];
//2.使用FormData携带图片文件
const fd = new FormData();//创建一个FormData对象,用于存储要发送到服务器的内容。
fd.append('img', file); //这里的'file'要和后端接收的字段名一致
//3.提交表单数据到服务器,使用图片url网址
axios({
method: 'post',
url: 'https://hmajax.itheima.net/api/uploadimg', // 这里要和后端接口一致
data: fd,
}).then(res => {//图片回显
const img = document.createElement('img');
img.src = res.data.data.url;
document.body.appendChild(img);
console.log(res);
});
});
12.如何使用上传图片功能更换网站背景?
步骤:
- 选择图片上传,设置body背景
- 上传成功后保存url网址
- 网页运行后获取url网址
示例:
//微调上例代码
axios({
method: "post",
url: 'https://hmajax.itheima.net/api/uploadimg', // 这里要和后端接口一致
data: fd,
}).then(res => {
localStorage.setItem('bg', res.data.data.url)
})
......
const bg = localStorage.getItem('bg')
if (bg) {
document.body.style.backgroundImage = `url(${bg})`
}
二.XMLHttpRequest对象
XHR对象可以在不刷新页面的情况下请求URL获取数据,特点是局部刷新
axios是XHR对象的封装函数,但并不等同于XHR
学习XHR对象可以了解与服务器通信的方式,了解axios内部原理,同时也有其使用场景:小型静态网页可以不引入axios以缩小体积
1.使用步骤
// 1.创建XHR对象
const xhr = new XMLHttpRequest();
// 2.配置请求方法和URL地址
xhr.open('get', 'https://hmajax.itheima.net/api/province');
// 3.监听loadend事件(加载完成事件),接收响应
xhr.addEventListener('loadend', () => {
console.log(xhr.response)
const data=JSON.parse(xhr.response);//JSON.parse()将字符串转换为对象
console.log(data.list.join('
'))
});
// 4.发起请求
xhr.send()
2.XHR的查询参数
是浏览器提供给服务器的额外信息,使其返回浏览器想要的数据
xhr.open('get', 'https://hmajax.itheima.net/api/city?pname=辽宁省');
"?"
号后面即为查询参数,多个查询参数之间用"&"
号隔开
3.示例:获取城市列表
要求使用https://hmajax.itheima.net/api/area?${queryString}
获取城市区域数据,用于后续渲染到页面中,其中queryString
是用户在省份和城市的input框中输入的pname
和cname
组成的对象
//HTML部分略
var provinceInput = document.getElementById('province');
var cityInput = document.getElementById('city');
var queryBtn = document.getElementById('query-btn');
//1.获取用户输入的省份和城市
const pname=provinceInput.value;
const cname=cityInput.value;
// 2.把省份和城市拼接成字符串----字符串=>对象=>URLSearchParams实例=>字符串
//字符串=>对象
const obj={
pname,
cname,
}
//对象=>URLSearchParams实例
const queryObj=new URLSearchParams(obj);
//URLSearchParams实例=>字符串
const queryString=queryObj.toString();//pname=湖南省&cname=长沙市
// console.log(pname,cname,obj,queryObj,queryString);
// 点击获取后台数据,并渲染到页面
queryBtn.addEventListener('click',function(){
alert('点击了查询按钮')
// 声明xhr对象
const xhr=new XMLHttpRequest();
// 配置请求参数和url
xhr.open('GET',`https://hmajax.itheima.net/api/area?${queryString}`,true);
xhr.addEventListener('loadend',function(){
// console.log(xhr.response);
if(xhr.status===200){
// 请求成功
const result=JSON.parse(xhr.response);
console.log(result);
// 渲染到页面
result.list.forEach(item=>{
//思路:创建option元素,设置value和innerText属性,添加到select中
const option=document.createElement('option');
option.value=item;
option.innerText=item;
document.getElementById('district-select').appendChild(option);
});
// 显示select标签
document.getElementById('district-select').style.display='block';
// 隐藏提示信息
document.getElementById('result-hint').style.display='none';
}
})
// 发送请求
xhr.send();
})
三.Promise
1.定义
Promise对象用于表示一个异步操作的最终完成或失败和其对应的结果值
2.优点
- 逻辑更清晰
- 了解axios函数内部的运作机制
- 解决回调地狱
3.步骤
//1.创建Promise对象
const p = new Promise((resolve, reject) => {
//2.写异步代码
setTimeout(() => {
// resolve("成功了");
reject(new Error("失败了"))
}, 1000)
});
//3.成功的回调
p.then(res => {
console.log("成功的回调:", res);
})
//4.失败的回调
p.catch(err => {
console.log("失败的回调:", err);
})
4.Promise的三种状态
-
new Promise()得到一个对象,此时该对象处于
pending
状态(待定) -
后续处理异步任务有了成功的结果,调用了
resolve
时,Promise对象的状态会修改为fulfilled
(已兑现)
此时会把resolve中的参数传给.then的回调函数中 -
否则会调用
reject
,传出一个错误的对象,状态会变成rejected
(已拒绝)
Promise的状态一旦兑现或拒绝就不能再改变(可以通过再调用一次resolve()
看看)
5.示例:使用Promise优化获取城市区域的渲染
// 点击获取后台数据,并渲染到页面
queryBtn.addEventListener('click', function () {
// 1.创建Promise对象
const p = new Promise(function (resolve, reject) {
// 2.异步操作:XHR对象
// 声明xhr对象
const xhr = new XMLHttpRequest();
// 配置请求参数和url
xhr.open('GET', `https://hmajax.itheima.net/api/area?${queryString}`, true);
xhr.addEventListener('loadend', function () {
// console.log(xhr.response);
if (xhr.status === 200) {
// 把异步操作的结果封装到resolve中
resolve(xhr.response);
}
})
// 发送请求
xhr.send();
})
// 3.关联回调函数:把XHR请求的结果封装到resolve中,并把回调函数封装到then方法中
p.then(res=>{
// 请求成功
// const result = JSON.parse(xhr.response);
console.log(JSON.parse(res));
// 渲染到页面
JSON.parse(res).list.forEach(item => {
//思路:创建option元素,设置value和innerText属性,添加到select中
const option = document.createElement('option');
option.value = item;
option.innerText = item;
document.getElementById('district-select').appendChild(option);
});
// 显示select标签
document.getElementById('district-select').style.display = 'block';
// 隐藏提示信息
document.getElementById('result-hint').style.display = 'none';
})
})
6.基于Promise对象和XMLHttpRequest对象,封装一个myAxios函数
目的是模拟axios函数的封装,了解axios的内部原理
- 封装myAxios函数
// 封装一个myAxios函数
function myAxios(config){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
if(xhr.params){
const queryObj = new URLSearchParams(config.params);//对象=>URLSearchParams实例
const queryString = queryObj.toString();//URLSearchParams实例=>字符串
config.url+=`?${queryString}`
}
xhr.open(config.method||"GET", config.url)
xhr.onload = function () {
if (xhr.status>=200&&xhr.status<300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
}
if(config.data){//判断配置对象config是否传入data选项
// 若有:配置请求头信息,发送请求体数据
xhr.setRequestHeader("Content-Type", "application/json")
xhr.send(JSON.stringify(config.data))
}else{
// 若无:直接发送请求
xhr.send()
}
})
}
- 应用到上例
//字符串=>对象
const params = {
pname,
cname,
}
// //对象=>URLSearchParams实例
// const queryObj = new URLSearchParams(params);
// //URLSearchParams实例=>字符串
// const queryString = queryObj.toString();//pname=湖南省&cname=长沙市
// // console.log(pname,cname,obj,queryObj,queryString);
// 点击获取后台数据,并渲染到页面
queryBtn.addEventListener('click', function () {
myAxios({
url: `https://hmajax.itheima.net/api/area`,
method: 'GET',
params
}).then(res => {
// 请求成功
// const result = JSON.parse(xhr.response);
console.log(res);
// 渲染到页面
res.list.forEach(item => {
//思路:创建option元素,设置value和innerText属性,添加到select中
const option = document.createElement('option');
option.value = item;
option.innerText = item;
document.getElementById('district-select').appendChild(option);
});
// 显示select标签
document.getElementById('district-select').style.display = 'block';
// 隐藏提示信息
document.getElementById('result-hint').style.display = 'none';
})
})
- 应用二:使用封装的myAxios函数携带请求体数据并用于注册账号上
document.querySelector("button").addEventListener("click", function () {
myAxios({
url: "https://hmajax.itheima.net/api/register",
method: "POST",
data: {
username: "zhangsan2",
password: "123457"
}
}).then(res => {
console.log(res.message)
}).catch(err => {
console.dir(err)
});
});
7.同步代码和异步代码
- 同步代码:逐行执行,原地等待结果后才继续向下执行
- 异步代码:调用后不阻塞代码向下执行,将来完成后触发回调函数(定时器,事件,AJAX),通过回调接收结果
8.回调函数地狱
一直向下嵌套回调函数,形成回调函数地狱,缺点是可读性差,捕获异常难度大,耦合性高
解决方法:链式调用
9.链式调用
使用then
函数返回新的Promise
对象,一直串联下去then
的回调函数中,return
的值会传给then
函数生成的新的Promise
对象
10.Promise.all
作用:
把几个Promise对象合并成一个大的Promise对象,这个大Promise对象的then回调要等待所有子Promise对象全部成功之后才会触发,任意一个子Promise对象就执行大Promise的catch回调
语法:const p=new Promise.all([Promise对象1,Promise对象2,...]);p.then().catch()
示例:
const url='https://hmajax.itheima.net/api/weather'
const bjPromise=axios({url,params:{city:'110100'}});
const shPromise=axios({url,params:{city:'310100'}});
const gzPromise=axios({url,params:{city:'440100'}});
const szPromise=axios({url,params:{city:'440300'}});
const p=Promise.all([bjPromise,shPromise,gzPromise,szPromise]);
p.then(res=>{
console.log(res);//得到一个数组,顺序与和并子Promise的顺序一致
}).catch(err=>{
console.log(err)
});
四.async和await关键字
1.定义
用更简洁的方式写出基于Promise的异步代码,无需刻意地链式调用Promise
示例:
async function getNewsList() {
const {data:{data}} = await axios('https://hmajax.itheima.net/api/news')
const newsList = data
console.log(newsList)
}
getNewsList()
2.try和catch语句
语法:用try把可能会产生错误的异步代码包起来,在catch中接收错误信息
try{
//要执行的代码
//若某行代码发生错误,剩余代码不再往下执行
}catch(error){
//接收错误信息,来自try中代码发生的错误
}
上例优化:
async function getNewsList() {
try{
const {data:{data}} = await axios('https://hmajax.itheima.net/api/news')
const newsList = data111
console.log(newsList)
}catch(err){
console.log("出错了:",err)//出错了: ReferenceError: data111 is not defined
}
getNewsList()
3.事件循环(Event Loop)
3.1.事件循环的模型分为三个部分:调用栈,宿主环境(浏览器)和任务队列
- 调用栈:JS代码在运行时的一个调用环境
- 宿主环境:就是浏览器,它是多线程的
- 任务队列:内存中开辟的一块空间
3.2.什么是事件循环?
执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里的回调函数执行机制
3.3.为什么会有事件循环?
JS时单线程的,为了不阻塞JS其他代码,设计执行代码的模型
3.4.事件循环的执行过程
- 执行同步代码,遇到异步代码就交给**宿主环境(浏览器)**执行
- 异步有了结果后把回调函数放入任务队列中排队
- 所谓循环是指:调用栈反复去任务队列中拿取任务
4.事件循环的练习
测试题一:微任务>宏任务
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
结果:1 4 3 2,
原因:异步代码中的微任务Promise的回调优先于宏任务setTimeout执行
测试题二:嵌套异步
console.log('A');
setTimeout(() => {
console.log('B');
Promise.resolve().then(() => console.log('C'));
}, 0);
Promise.resolve().then(() => {
console.log('D');
setTimeout(() => console.log('E'), 0);
});
console.log('F');
结果:A F D B C E,
原因:(异步代码部分)
step1:微任务先输出D,再添加一个宏任务E(第二个宏任务,最后输出);
step2:第一个宏任务:先执行B再执行微任务C
step3:第二个宏任务:执行E
测试题三:Vue的$nextTick
<script>
export default {
mounted() {
console.log('1');
this.$nextTick(() => {
console.log('2');
});
Promise.resolve().then(() => console.log('3'));
setTimeout(() => console.log('4'), 0);
console.log('5');
}
}
</script>
结果:1 5 2 3 4
同步:1 5
微任务:2 3----$nextTick
是微任务
宏任务:4
如果把$nextTick
包在一层微任务里,那么执行顺序要后延:
mounted() {
console.log('1');
Promise.resolve().then(() => {
this.$nextTick(() => {
console.log('2');
});
});
Promise.resolve().then(() => console.log('3'));
setTimeout(() => console.log('4'), 0);
console.log('5');
}
变成:1 5 3 2 4
第一个微任务(外层 Promise)执行,将 $nextTick 的回调放入微任务队列
第二个微任务(内层 Promise)输出 3
执行新加入的微任务($nextTick 回调)输出 2
测试题四:混合任务层级
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout in Promise'), 0);
});
new Promise(resolve => {
console.log('Promise creator');
resolve();
}).then(() => console.log('Promise 2'));
console.log('End');
结果:
同步:Start Promise creator End
微任务:Promise1 Promise2
宏任务:Timeout1 PromiseInTimeout TimeoutInPromise
测试题五:Vue响应更新+$nextTick
export default {
data: () => ({ count: 0 }),
mounted() {
console.log('A');
this.count = 1; // 触发异步更新
this.$nextTick(() => {
console.log('B');
});
Promise.resolve().then(() => {
console.log('C');
this.count = 2; // 再次触发更新
});
setTimeout(() => {
console.log('D');
this.$nextTick(() => console.log('E'));
}, 0);
console.log('F');
},
watch: {
count() {console.log('Count updated:', this.count);}
}
}
结果:A F Count updated:1 B C Count updated:2 D E
全过程:
同步:
console.log('A'); // 同步输出 A
this.count = 1; // 触发响应式更新(加入 Vue 更新队列)
this.$nextTick(() => { console.log('B'); }); // 加入 nextTick 队列
Promise.resolve().then(() => { /* ... */ }); // 加入微任务队列
setTimeout(() => { /* ... */ }, 0); // 加入宏任务队列
console.log('F'); // 同步输出 F
输出:A → F
* 此时 Vue 更新队列中有 count 更新任务
* nextTick 队列中有输出 B 的回调
* 微任务队列中有输出 C 的回调
* 宏任务队列中有输出 D 的回调
微任务:
// 1. Vue 更新队列任务(优先执行)
watcher.run() // 触发 watch 回调 → 输出 "Count updated: 1"
// 2. Vue 的 nextTick 回调
() => console.log('B') // 输出 B
// 3. Promise 回调
() => {
console.log('C'); // 输出 C
this.count = 2; // 再次触发更新
}
输出:Count updated: 1 → B → C,当 this.count = 2 执行时,继续输出:Count updated: 2
宏任务:
() => {
console.log('D'); // 输出 D
this.$nextTick(() => console.log('E')); // 加入 nextTick 队列,最后输出 E
}
输出:D E
测试题六:
输出:1 5 3 2 4 6
原因:延时器和XML对象请求的回调函数都是宏任务
测试题七
console.log('1. 同步开始');
setTimeout(() => {
console.log('2. 宏任务1');
Promise.resolve().then(() => console.log('3. 微任务1 (宏任务1内)'));
}, 0);
Promise.resolve().then(() => {
console.log('4. 微任务1');
setTimeout(() => console.log('5. 宏任务2 (微任务1内)'), 0);
});
new Promise(resolve => {
console.log('6. Promise构造器同步');
resolve();
}).then(() => console.log('7. 微任务2'));
queueMicrotask(() => console.log('8. 微任务3'));
setTimeout(() => {
console.log('9. 宏任务3');
queueMicrotask(() => console.log('10. 微任务4 (宏任务3内)'));
}, 0);
console.log('11. 同步结束');
结果:
同步:1 6 11
微任务:4 7 8
宏任务:2 3 9 10 5
为什么5在最后面?
这个宏任务是执行到微任务时在添加到任务列表上的,
在其他所有宏任务执行完前,5不会执行
总结
常见同步代码:输出语句console.log,同步函数,new Promise构造器
常见微任务:Promise的回调,$nextTick
常见宏任务:延时器,XHR对象的回调
5.微任务和宏任务
ES6引入的Promise对象使得JS引擎也可以发起异步任务
- 宏任务:由浏览器环境执行的异步代码
- 常见宏任务:JS脚本执行事件,setTimeout,AJAX请求事件,用户交互事件
- 微任务:由JS引擎环境执行的异步代码
- 常见微任务:
Promise.resolve().then()
- 常见微任务:
*注意Promise本身是同步的,但是then和catch回调函数是异步的
五.node.js
1.什么是Node.js?
定义:Node.js是一个跨平台的JS开发环境,使开发者可以搭建服务器端的JS应用程序
作用:Node可以使用JS语法编写服务器端的程序
- 编写数据接口,提供网页资源浏览功能等
- 前端工程化:为学习vue和react等框架做铺垫
2.什么是前端工程化?
前端工程化就是开发项目知道上线的过程中集成的所有工具和技术
- 压缩工具:空格回车变量名等进行压缩处理,使项目体积更小,加载速度更快
- 格式化工具:代码风格的统一
- 转换工具:less,scss,ts==>转换成原生的css,js
- 打包工具:webpack是静态模块的打包工具,对代码进行转换压缩整合
- 脚手架工具:(如Vue-Cli)把开发项目遇到的所有过程准备好
前端工程化的过程离不开node的参与,node可以主动读取前端代码的内容
3.node.js为何能执行js?
浏览器能执行JS代码依靠的是内核中的V8引擎,它是C++编写的程序
node把v8引擎都拿出来,然后基于v8引擎,在node环境,脱离浏览器独立执行JS代码
4.node的安装和使用
略
5.node的核心模块:使用fs模块读写文件
模块:类似插件,封装了方法和属性
fs模块:封装了雨本机文件系统进行交互的方法或属性
语法和示例:
//index.js
//1.加载fs模块对象
const fs=require('fs')
//2.写入文件
//fs.writeFile('文件路径','写入内容',err=>{})
fs.writeFile('1.txt','hello world',(err)=>{})
//3.读取文件
//fs.readFile('文件路径',(err,data)=>{})//data:文件内容的Buffer数据流
fs.readFile('1.txt','utf8',(err,data)=>{
console.log(data)
})
命令:node index.js
(若文件路径不存在,则自动新建一个1.txt
)
6.node的核心模块:使用path模块处理路径
6.1.介绍
- 在node代码中
相对路径
是根据终端所在路径来查找的,可能无法找到,此时可以加载path模块防止相对路径不准确
*建议在node中使用绝对路径
__dirname
内置变量:获取当前模块所在目录的绝对路径path:join()
:使用特定于平台的分隔符作为界定符,把所有给定的路径片段连接起来
path.join('03','dist/js','index.js')
- 语法:
const fs=require('fs')
// 1.引入path模块
const path=require('path')
//__dirname 获取当前文件所在的目录
console.log(__dirname)
// 2.使用path.join()拼接路径
fs.readFile(path.join(__dirname,'../1.txt'),(err,data)=>{
if(err) return console.log(err)
else console.log(data.toString())
})
/*总结:path拼接的是txt文件的绝对路径,
然后js文件不管放在哪里,node这个js文件都能读取到txt文件中的内容*/
6.2.案例:压缩和读取HTML网页的内容
- 思路:把当前(任意)HTML文件的回车符和换行符去掉形成一个新的HTML文件
- 步骤:读取HTML文件内容>正则替换>写入到新HTML文件
- 代码:
const fs=require('fs')
const path=require('path')
// 读取当前HTML文件
fs.readFile(path.join(__dirname,'../index.html'),(err,data)=>{
{
// console.log(data.toString())
// 正则:去除html文件的空格和回车换行
let resultStr=data.toString().replace(/[\r\n]/g,'')
console.log(resultStr)
// 写入到一个新的文件newIndex.html中
fs.writeFile(path.join(__dirname,'newIndex.html'),resultStr,(err)=>{
console.log('写入成功')
})
}
})
结果:生成一个没有换行的newIndex.html
文件
7.node的核心模块:使用http模块创建web服务
7.1.什么是URL中的端口号?
- URL:统一资源定位符,简称网址,用于访问服务器中的资源
- 端口号之于网址,类似于房间号之于房间
若想匹配到一个房间物品必须:使用钥匙打开房子中对应的房间号,在该房间中找到物品位置
协议 域名 端口号(默认80) 资源路径
http://hmajax.itheima.net:80/api/province
端口号范围:0-65535之间任意整数,默认80,0-1023之前的都是系统占用的端口号
- 常见端口号:
80--web服务
3000--web服务
8080--Web服务
3306--数据库服务
7.2.介绍http模块
需求:创建web服务并响应内容内浏览器
步骤:
// 1.加载引入http模块,设置server 变量
const http = require('http');
const server = http.createServer()
// 2.监听request请求事件,设置响应头和响应体
server.on('request', (req, res) => {
// 设置响应头:内容类型为普通文本,格式是中文
res.setHeader('Content-Type', 'text/plain;charset=uth-8');
// 设置响应体:返回内容并结束本次请求和响应
res.end('Hello World');
});
// 3.设置端口号,启动web服务
server.listen(3000, () => {
console.log('服务器启动成功,端口号:3000......')
});
// 4.浏览器请求http://localhost:3000
7.3.案例:基于web服务开发提供网页资源的功能
// 导入所需模块
const http = require('http');
const fs = require('fs');
const path = require('path');
// 创建HTTP服务
const server = http.createServer((req, res) => {
// 处理根路径和newIndex.html请求
if (req.url === '/' || req.url === '/newIndex.html') {
const filePath = path.join(__dirname, 'newIndex.html');
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('服务器内部错误');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
}
// 处理其他路径请求
else {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>资源不存在</h1>
<p>请求的路径 ${req.url} 不存在,请检查URL</p>
</body>
</html>
`);
}
});
// 启动服务监听3000端口
const PORT = 3000;
server.listen(PORT, () => {
console.log(`服务已启动,访问 http://localhost:${PORT}`);
});
8.模块化
8.1.定义
node的模块化是一种将代码分割为独立可复用单元的机制,它遵循CommonJS规范,每个文件被视为一个独立模块,具有自己的作用域
模块化解决了代码组织,依赖管理,作用域隔离的问题
8.2.优势
- 代码复用
- 作用域隔离
- 依赖管理
- 可维护性
- 按需加载
8.3.node的模块类型
- 核心模块:node的内置模块(fs,path.http等)
- 文件模块:用户自定义的模块
- 第三方模块:通过npm安装的模块
8.4.导入和导出模块
//math.js
//导出单个功能
modules.exports=function add(a,b){return a+b}
//index.js
//导入核心模块
const fs=require('fs')
//导入自定义模块
const math=require('./math.js')
//使用导入的模块
console.log(math.add(2,3))
8.5.在vue项目中的使用场景
- 配置管理
// config.js
module.exports = {
apiBaseUrl: process.env.VUE_APP_API_URL || 'https://api.example.com',
maxRetry: 3,
timeout: 5000
};
// vue.config.js 略
- 工具函数模块
// utils/dateFormatter.js
exports.formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
- 封装api接口
exports.getUser = async (id) => {
try {
const response = await axios.get(`${config.apiBaseUrl}/users/${id}`);
return response.data;
} catch (error) {
console.error('API请求失败:', error);
throw error;
}
};
9.ECMAScript标准
ESM是JS官方标准模块系统,由ECMAScript6引入
现代浏览器和构建工具(如vite)原生支持
9.1.默认导入和默认导出
导出:export default{}
导入:import 变量名 from "路径/模块名"
*node默认支持的是CommonJS标准的语法,如要使用ECMAScript标准的语法,需要在所在文件夹新建package.json
文件并设置{"type":"module"}
//a.js
//默认导出
const baseUrl="xxxxxx"
export default={
url:baseUrl//可以设置对外暴露的属性名为url
}
//index.js
//默认导入
import obj from './a.js'
console.log(obj.url)
//创建package.json
{
"type":"module"
}
9.2.命名导入和命名导出
导出:export修饰定义语句(export const getCartList=()=>{})
导入:import {getCartList} from "路径/模块名"
六.npm包管理器
1.什么是包?
包:把模块,代码,其他资料聚合成的一个文件夹
包分类:
* 项目包:用于编写项目和业务逻辑
* 软件包:封装工具和方法来进行使用
要求:
根目录中必须有package.json
文件(记录包的清单信息)
2.封装一个软件包
//utils/lib/arr.js
// 获取数组元素数量
const getArrayNum = (arr) => {
return arr.length;
};
// CommonJS 导出
module.exports = {
getArrayNum
};
//utils/lib/str.js
// 检查用户名格式
const checkUser = (username) => {
return username
};
// 检查密码强度
const checkPwd = (password) => {
return password
};
// CommonJS 导出
module.exports = {
checkUser,
checkPwd
};
//utils/index.js
// CommonJS 导入子模块
const arrUtils = require('./lib/arr');
const strUtils = require('./lib/str');
// 合并导出所有工具方法
module.exports = {
...arrUtils,
...strUtils
};
//server.js
// 导入utils包
const utils=require("./index")
// 控制台输出验证
console.log('数组长度:', utils.getArrayNum([1, 2, 3, 4]));
console.log('用户验证:', utils.checkUser('test_user'));
console.log('密码验证:', utils.checkPwd('Password123'));
3.npm软件包管理器
npm是Node.js标准的软件包管理器,作用是下载软件包和管理版本
初始化或新建清单文件package.json的命令:npm init -y
下载包:npm install 包名
下载包的位置:当前项目的node_modules
文件夹下
4.npm安装依赖
下载的(别人的)项目中,不包含node_modules,不能正常运行项目,
原因是缺少依赖的本地软件包
这样做的理由是:用户自己用npm下载相关依赖,比放一大堆依赖包在项目中更快
解决方法:在项目终端输入命令npm i
效果:一键下载在package.json中记录的所有软件包
5.下载一个全局软件包:nodemon
-
本地软件包:只能作用于当前项目,一般封装属性和方法,存在于node_modules
-
全局软件包:作用于本机所有项目,一般封装命令和工具,存在系统设置的文件夹下
-
nodemon的作用
取代node命令,可以检测代码的更改,自动重启程序
(每次更改xxx.js不用再输入一遍node xxx.js)
安装nodemon:npm i nodemon -g
启动服务器:nodemon xxx.js
七.Webpack
1.什么是webpack?
静态模块:编写代码过程中html,css,js,图片等固定内容的文件
打包:把静态模块的内容压缩,整合,编译等,即前端工程化
webpack:用于JS应用程序的静态模块打包工具
当webpack处理应用程序时,它会在内部,从一个或多个入口点构建一个依赖图,然后将你项目中所需的每一个模块组合成一个或多个bundle,它们均为静态资源,用于展示你的内容
vite:和webpack一样,是新型的打包工具,主要用于vue3项目中
2.如何使用webpack打包项目?(默认只打包js文件)
- step1–生成
webpackStudy/package.json
文件:npm init -y
- step2–新建
webpack/src
文件夹和项目代码
//新建src/utils/check.js
export const checkPhone=phone=>phone.length===11
export const checkCode=code=>code.length===6
//新建src/index.js
import {checkPhone,checkCode} from './utils/check.js'
console.log(checkPhone('13800138000'),checkCode('123456'))
- step3–安装webpack:
npm - webpack-cli --save -dev
- step4–配置局部自定义命令
//package.json
"script":{
"build":"webpack"
}
- step5–打包:
npm run build
打包结果:生成webpackStudy/dist
文件夹,其中的main.js
就是压缩之后的index.js
3.修改打包的入口和出口
当前默认打包入口:webpackStudy/src/index.js
当前默认打包出口:webpackStudy/dist/main.js
如何把默认打包入口改成自定义?
- 新建
webpackStudy/src/login
目录下的同名html,css,js
文件 - 新建
webpackStudy/webpack.config.js
const path = require('path');
module.exports = {
entry:path.resolve(__dirname,'./src/login/login'),//入口文件路径
output: {
path: path.resolve(__dirname, 'dist'),//输出目录路径
filename:'./myLogin.js',//输出文件路径
clean:true//重新生成dist目录
},
}
结果:生成dist/myLogin.js
*注:webpack默认只识别js代码,后续通过插件把css代码也插进去
4.使用webpack打包html文件
使用到了HtmlWebpackPlugin插件,该插件会自动生成html文件
- 下载
HtmlWebpackPlugin
插件:npm i html-webpack-plugin --save-dev
- 配置
webpack.config.js
如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口
entry: path.resolve(__dirname, './src/login/login.js'),//入口文件路径
// 出口
output: {
path: path.resolve(__dirname, 'dist'),//输出目录路径
filename: './login/index.js',//输出文件路径
clean: true//重新生成dist目录
},
// 插件:给webpack提供更多功能
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/login/login.html'),//模板文件
filename: path.resolve(__dirname, 'dist/login/index.html')//输出的文件
}),
],
}
- 效果:重新打包后,不仅生成了
login/index.js文件
还有index.html文件
*还剩css还没压缩打包
5.使用webpack打包css文件
5.1.方法一:把在js文件中引入css文件,把解析后的css代码插入到DOM中
使用到了加载器css-loader,作用是解析css代码,使得webpack能识别(webpack默认只识别js代码)
使用到了加载器style-loader,作用是把解析后的css代码插入到DOM先解析,再插入到DOM
步骤:
- 安装插件:
npm install --save-dev css-loader npm install --save-dev style-loader
- 引入
//src/login/login.js
//import 'bootstrap/dist/css/bootstrap.min.css'
import "./login.css"
- 配置
webpack.config.js
如下:
module.exports = {
......
// 加载器:让webpack识别更多模块文件的内容
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']//use从后往前加载的,注意顺序
},
],
}
}
效果:dist/login/index.js
中新增css样式
如果压缩前的login.css
使用到了bootstrap模块包
- 需要下载:
npm i bootstrap
- 然后在
login.js
中引入
//src/login/index.js
import 'bootstrap/dist/css/bootstrap.min.css'
import "./index.css"
5.2.方法二:提取css代码文件
提取成css文件可以被浏览器缓存,减小JS文件的体积
浏览器可以并行缓存css文件和js文件的代码,可以让网页更快加载出来
使用到了插件mini-css-extract-plugin,作用是提取css代码,本质是把css代码打包到js中,并插入到DOM上,显然,它不能和插件mini-css-extract-plugin
一起使用
提取css文件
use: [MiniCssExtractPlugin.loader, "css-loader"]
- 安装插件:
npm i mini-css-extract-plugin --save -dev
- 配置
webpack.config.js
如下:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
......
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
成功生成dist/login/index.css
压缩css文件代码
此时生成的dist/login/index.css
未被压缩,使用插件css-minimizer-webpack-plugin,该插件使用 cssnano 优化和压缩 CSS。
- 安装插件:
npm install css-minimizer-webpack-plugin --save-dev
- 配置
webpack.config.js
如下:’
// (生产环境)
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
......
// 优化
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
`...`,//此处就是保留js文件的压缩配置,然后在此基础上再进行自定义css文件的压缩配置
new CssMinimizerPlugin(),
],
},
}
//(开发环境)
module.exports = {
optimization: {
// [...]
minimize: true,
},
};
此时dist/login/index.css
压缩成功
6.webpack打包less代码
使用到了加载器less-loader,作用是把less代码编译css代码
步骤:
- 安装less和less-loader:
npm install less less-loader --save-dev
- 配置
webpack.config.js
如下
// 加载器:让webpack识别更多模块文件的内容
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
//新增对less文件的匹配
{
test: /\.less$/i,
use: [
// compiles Less to CSS
MiniCssExtractPlugin.loader,//此处替换掉原来的style-loader,让其生成单独的css文件
'css-loader',
'less-loader',
],
},
],
},
- 原项目新建
login.less
替换掉login.css
,禁用VSCode中的easy less
插件,最后在login.js
中引入如下
// import "./login.css"
import "./login01.less"
打包后生成了dist/login/index.css
,功能不变
7.webpack打包图片
[了解]webpack5之前需要下载如下loader:
raw-loader:将文件导入为字符串
url-loader:将文件作为data URL内敛到bundle中
file-loader:将文件发送到输出目录
[重点]webpack5之后直接使用内置资源模块,配置webpack.config.js
如下即可
(官网位置:http://webpack.docschina.org/guides/asset-modules#root)
//webpack.config.js
rules: [
......
{
//使用内置属性打包图片资源
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',//内置属性,打包图片资源--发送一个单独的文件并导出 URL
generator: {
filename: 'images/[hash][ext][query]',
publicPath: '../'//此处配置图片的路径,相对于css文件的路径(dist下,与login同级目录下)
//生成两个png文件:dist/images/9ec024e6a149b3778d4f.png和b4d13a877e7b08485a89.png
}
}
],
在原项目中准备两个图片资源:网页背景图和用户头像
//login/login.js
import "./login.css"
//添加头像图片logo.png
import imgObj from './assets/logo.png'
const img=document.createElement('img')
img.src=imgObj
img.style.width='30px'
img.style.height='30px'
loginForm.appendChild(img)
//login/login.css
//设置背景图bg.jpg
body {
background-image: url('./assets/bg.jpg');
}
效果:
- 生成两个png文件:
dist/images/9ec024e6a149b3778d4f.png
和b4d13a877e7b08485a89.png
- 打包后的css文件中,引入图片路径变成:
background-image:url(../images/b4d13a877e7b08485a89.jpg)
8.webpack中搭建开发环境
为什么要搭建开发环境?
每次都要npm run build
,很麻烦,搭建开发环境可以解决此类问题
如何搭建开发环境?
配置webpack-dev-server快速开发应用程序,它的作用是启动web服务,自动检测源代码变化并热更新到网页
步骤:
- 下载
webpack-dev-server
插件:npm install --save-dev webpack-dev-server
- 配置
webpack.config.js
module.exports = {
mode: 'development',//development是开发模式,production是生产模式
......
// devtool是后续内容,先不用了解
devtool:'inline-source-map',
// 开发服务器
devServer: {
static: './dist/login',//静态资源目录,相对于当前配置文件的位置
port: 8081,//指定端口号
open: true,//自动打开浏览器
},
}
- 配置
package.json
"scripts": {
"dev": "webpack serve --open",//多了serve --open,它会使得npm run dev命令持续监听项目代码的变化,并自动打开浏览器
"build": "webpack"
},
效果:
先npm run build 生成dist打包文件夹
再npm run dev,自动打开浏览器
默认网址是http://localhost:8081/login
至此已实现我们想要的效果:
在打包前的项目文件login.js下改变输出语句,控制台实时更新,而不用重新打包
9.webpack打包模式的介绍和使用
9.1.两种打包模式的特点
打包模式可以告知webpack,使用相应模式的内置优化,分为开发模式(development)和生产模式(production)两种
- 开发模式的特点是:调试代码(不会压缩代码),实时加载变化,模式热替换,本地开发时使用
- 生产模式的特点是:压缩代码,资源优化,更轻量,打包上线时使用
9.2.如何设置开发模式?
两种方法:
//webpack.config.js
mode:'development'
//package.json
"scripts": {
"dev": "webpack serve --open",
"build": "webpack --mode=production"
},
优先级:package.json命令行
>webpack.config.js
9.3.应用场景1:在开发模式下使用style-loader,在生产模式下提取css文件
思路
在开发模式下使用 style-loader
(CSS 注入 DOM),在生产模式下提取 CSS 文件(使用 MiniCssExtractPlugin
如何区分当前处于什么开发环境?
- 名词提前解释
1.NODE_ENV 应用程序的"工作模式"
类比手机:省电模式/性能模式
告诉程序现在是开发development/测试test/生产环境production--即NODE_ENV的值
2.cross-env 跨平台设置环境变量的"万能遥控器"
解决不同操作系统设置环境变量的差异
这是一个让环境变量命令栏在所有电脑上都能用的工具
示例:
"build": "cross-env NODE_ENV=production webpack",其中的cross-env NODE_ENV=production
在windows平台上等价于:
set NODE_ENV=production
set:Windows 设置环境变量的命令
NODE_ENV:环境变量名称(通常表示Node.js运行环境)
production:环境变量的值(表示生产环境)
3.process.env 程序可以查看的个人信息档案,包含所有的环境设置
如:process.env.NODE_ENV==='development'
三者联系:
用cross-env设置NODE_ENV的值(production/development/test),
程序通过process.env查看NODE_ENV的值,并根据这个值决定如何工作
- 下载
cross-env
包,设置参数区分环境,安装命令:npm i cross-env --save-dev
- 配置自定义命令.传入参数名和参数值绑定待process.env对象下
//package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
//生产模式:npm run build
"build": "cross-env NODE_ENV=production webpack serve --open --mode=production",
//开发模式:npm run dev
"dev": "cross-env NODE_ENV=development webpack --mode=development"
},
//webpack.config.js
rules: [
{
test: /\.css$/i,
//开发模式:css在js中引入,不生成独立css文件;生产模式:生成css文件
use: [process.env.NODE_ENV==='development'?'style-loader':MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/i,
use: [
//开发模式:css在js中引入,不生成独立css文件;生产模式:生成css文件
process.env.NODE_ENV==='development'?'style-loader':MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
],
},
],
- 验证
npm run build,生成独立的css文件
若改成:
//"build": "cross-env NODE_ENV=development webpack --mode=development"
"build": "cross-env NODE_ENV=production webpack --mode=production"
则输出结果是css内嵌在js文件中,而不会生成独立的css文件
10.webpack向前端注入一个环境变量
10.1.应用场景2:在开发模式下打印console.log语句,在生产模式下不打印
问题:cross-env
设置的NODE_ENV
只能在node环境下生效,在前端代码中无法访问process.env.NODE_ENV
解决:使用webpack内置插件DefinePlugin,该插件允许在编译时将代码中的变量替换成其他值或表达式
步骤:
- step1:在前端代码注入环境变量
//webpack.config.js
const webpack = require('webpack');
......
new webpack.DefinePlugin({
process.env.NODE_ENV:JSON.stringify(process.env.NODE_ENV)
})
*注:为什么要进行JSON类型转换?
此处是判断production还是development,但直接写会导致字符串被认为变量,需要JSON强制定义为字符串
即:字符串会当成变量传递,需要字符串转成json格式
(在编译时,把前端代码中匹配的变量名替换成值或表达式)
- step2:准备不同的输出语句
//src/login/login.js
if(process.env.NODE_ENV==='production){
//生产环境:打印语句失效
console.log=function(){}
}else{
//开发环境:打印输出语句
console.log(666)
}
结果:
npm run dev 开发环境下,输出666
npm run build 生产环境下,什么也不输出
11.webpack开发环境调试错误
打包后的代码被压缩成一行,无法正确定位源代码的位置(行列)
devtool选项控制着webpack如何生成源映射source maps
*source-map:源映射
是连接编译后代码和原始源代码之间的关键工具,可以准确追踪error和warning在原始代码的位置
常用devtool选项和含义
略,问问deekseek:webpack有哪些常用的devtool选项?
讲一下即将使用的inline-source-map选项:
写法:devtool='inline-source-map'
含义:把source mpa作为DataURL嵌入bundle
特点:
-不需要额外.map文件
-显著增加bundle体积
-开发服务器热更新较慢
步骤:
//打包前的login.js:写错了
consolee.log(666)
//webpack.config.js:只在开发模式时使用devtool选项
//把配置对象{}单独放在一个常量中
const config={};
//仅开发环境下使用devtool选项
if(process.env.NODE_ENV==='development'){
config.devtool='inline-source-map';
}
module.exports=config
测试:
启动开发模式:npm run dev
结果:成功定位到原始源代码中错误的文件和具体行数
启动生产模式:npm run build
结果:也报错,但没有记录报错信息的原始代码
12.解析别名(alias)
解析别名是指对配置模块进行解析,创建import引入路径的别名,来使得模块的引入变得更简单
//webpack.config.js
const config={
resolve:{//解析
alias:{//别名
'@':path.resolve(__dirname,'src')
}
}
}
//src/login/login.js
// import imgObj from './assets/logo.png'
路径太长且相对路径不安全,改成绝对路径+解析别名如下:
import imgObj from "@/login/assets/logo.png"
13.优化:生产模式下的使用CDN
- CDN是什么:内容分发网络,指的是一组分布在各个地区的服务器
- CDN起到的作用:把静态资源文件或第三方库放在CDN网络中各个服务器中供用户请求获取
- 使用CDN的目的:减轻自己服务器的请求压力,就进请求物理延迟低,配套缓存策略
13.1.应用场景3:开发模式下使用本地的第三方库,生产模式下使用CDN加载引入
步骤:
- step1:在HTML中引入第三方库的CDN地址,并用模板语法判断
(模板语法:在html文件中区分当前是什么环境)
//src/login/login.html
<% if (htmlWebpackPlugin.options.useCdn) { %>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js"></script>
<%} %>
使用到了自定义属性useCdn,该属性的设置如下:
//webpack.config.js
const config={
// 配置
plugins: [
// 生成html文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/login/login.html'),//模板文件
filename: path.resolve(__dirname, 'dist/login/index.html'),//输出的文件
//自定义属性useCdn
useCdn:process.env.NODE_ENV==='production'//生产模式时使用CDN
}),
}
- step2:配置webpack.config.js中externals外部拓展选项(防止某些import的包如boostrape,axios被打包)
//webpack.config.js
//生产模式
if(process.env.NODE_ENV==='production'){
config.externals={//外部拓展选项:防止import的包被打包进来
//key:import 'bootstrap/dist/css/bootstrap.min.css'或import axios from 'axios'
//value:留在原地的全局变量,最好和CDN导入时暴露的变量名一致
'bootstrap/dist/css/bootstrap.min.css':'bootstrap',
'axios':'axios'
}
}
*留在原地:打包后在引入的原位置留一个axios和bootstrap的全局变量
- step3:在开发模式和生产模式下分别打包,看效果
开发环境:npm run dev ,浏览器>右键"检查网页源代码"(CTRL+U),没有CDN引入
生产模式:npm run build,有CDN引入
14.webpack打包多页面
14.1.核心步骤
- 1.多入口配置:每个页面对应一个文件
- 2.生成html文件:使用
HtmlWebpackPlugin
为每个页面创建HTML - 3.拆分代码:通过
chunks
字段指定每个页面加载的JS文件 - 4.输出配置:使用
[name]
占位符,来动态生成文件名
打包前的项目结构:
src/
├── pages/
│ ├── home/
│ │ ├── index.js
│ │ └── style.css
│ └── about/
│ ├── index.js
│ └── style.css
public/
├── home.html
└── about.html
打包后的项目结构:
dist/
├── js/
│ ├── home.bundle.js
│ └── about.bundle.js
├── css/
│ ├── home.styles.css
│ └── about.styles.css
├── home.html
└── about.html
*注:js文件中需要有如下引入语句
import './style.css';
安装依赖:
npm install webpack webpack-cli html-webpack-plugin mini-css-extract-plugin css-loader --save-dev
14.2.配置webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports={
/***1.多入口配置:每个页面对应一个文件***/
entry:{
home:'./src/pages/home/index.js',
about:'./src/pages/about/index.js'
},
output:{
path:path.resolve(__dirname,'dist'),
/***4.输出配置:使用[name]占位符,来动态生成文件名***/
filename:'js/[name].bundle.js'//[name]是占位符,会被替换成entry中定义的键名(home,about)
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 提取CSS到文件
'css-loader'
]
}
]
},
plugins:[
// 提取css文件
new MiniCssExtractPlugin({
filename:'css/[name].style.css'
}),
/***2.生成html文件:使用`HtmlWebpackPlugin`为每个页面创建HTML***/
// 为home页面生成html文件
new HtmlWebpackPlugin({
template:'./public/home.html',
filename:'home.html',
/***3.拆分代码:通过chunks字段指定每个页面加载的JS文件***/
chunks:['home']//指定该页面要引入的入口chunk(js文件和css文件)
}),
// 为about页面生成html文件
new HtmlWebpackPlugin({
template:'./public/about.html',
filename:'about.html',
chunks:['about']//指定该页面要引入的入口chunk(js文件和css文件)
}),
]
}
最终效果:
访问home.html时自动加载home.bundle.js和home.bundle.css
访问about.html时自动加载about.bundle.js和about.bundle.css
补充说明:
chunks['home']:
控制每个HTML页面加载哪些JS文件,避免所有页面加载全部JS
[name]占位符:
filename:'js/[name].bundle.js'//自动生成home.bundle.js和about.bundle.js
webpack多页面打包的最终目的是:
像乐高一样生产网页,把零散的HTML/css/js按网页需求组装成独立产品包,让浏览器只下载当前页面专属套餐,避免全家桶式的资源浪费,实现加载速度的提升
14.3.优化:新增一个带有图片资源的login页一起进行多页面打包
- 项目目录和图片资源引用详情
打包前:
project/
├── public/
│ ├── home.html
│ ├── about.html
│ └── login.html # 新增的login页面模板
├── src/
│ ├── pages/
│ │ ├── home/ # 原有首页
│ │ ├── about/ # 原有关于页
│ │ └── login/ # 新增login页
│ │ ├── index.js
│ │ ├── style.css
│ │ └── images/ # login页的图片资源
│ │ ├── bg.jpg
│ │ └── logo.png
│ └── assets/ # 公共资源目录(可选)
└── webpack.config.js # 修改后的配置文件
//login/index.js:
import "./style.css"
import imgObj from './images/logo.png'
const img=document.createElement('img')
img.src=imgObj
img.style.width='30px'
img.style.height='30px'
const loginForm = document.getElementById('loginForm');
loginForm.appendChild(img)
//login/style.css
body {
background-image: url('./images/bg.jpg');
}
打包后:
dist/
├── css/
│ ├── home.styles.css
│ ├── about.styles.css
│ └── login.styles.css # 新增的login样式
├── js/
│ ├── home.bundle.js
│ ├── about.bundle.js
│ └── login.bundle.js # 新增的login脚本
├── images/ # 图片资源目录
│ ├── bg.jpg # 压缩后的背景图
│ └── logo.png # 压缩后的logo
├── home.html
├── about.html
└── login.html # 新增的login页面
- 新增
webpack.config.js
配置如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports={
/***1.多入口配置:每个页面对应一个文件***/
entry:{
home:'./src/pages/home/index.js',
about:'./src/pages/about/index.js',
denglu:'./src/pages/denglu/index.js',
},
output:{
path:path.resolve(__dirname,'dist'),
filename:'js/[name].bundle.js',//[name]是占位符,会被替换成entry中定义的键名(home,about)
/***4.输出配置:使用[name]占位符,来动态生成文件名***/
assetModuleFilename:'images/[name][hash:8][ext]'//打包图片资源时,文件名和路径的设置规则 [name]是文件名,[hash:8]是文件内容的哈希值,取前8位,[ext]是文件的扩展名
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 提取CSS到文件
'css-loader'
]
},
// 新增:使用内置属性打包图片资源
{
test:/\.(png|jpe?g|gif|svg)/i,
type:'asset/resource',//内置属性,打包图片资源
generator:{
filename:'images/[name][hash:8][ext]',
publicPath:'../'//此处配置图片的路径,相对于css文件的路径(dist下,与login同级目录下)
}
}
]
},
plugins:[
// 提取css文件
new MiniCssExtractPlugin({
filename:'css/[name].style.css'
}),
// 为home页面生成html文件
new HtmlWebpackPlugin({
template:'./public/home.html',
filename:'home.html',
chunks:['home']//指定该页面要引入的入口chunk(js文件和css文件)
}),
// 为about页面生成html文件
new HtmlWebpackPlugin({
template:'./public/about.html',
filename:'about.html',
chunks:['about']//指定该页面要引入的入口chunk(js文件和css文件)
}),
// 新增:为denglu页面生成html文件
/***2.生成html文件:使用`HtmlWebpackPlugin`为每个页面创建HTML***/
new HtmlWebpackPlugin({
template:'./public/denglu.html',
filename:'denglu.html',//生成的页面文件名
/***3.拆分代码:通过chunks字段指定每个页面加载的JS文件***/
chunks:['denglu']//指定该页面要引入的入口chunk(js文件和css文件)
})
]
}
14.4.优化:提取公共代码,把多个页面都引用的公共样式或函数工具库等提取出来
- 新增公共文件
src/common/utils.js
src/common/api.js
src/common/styles/base.css
src/common/styles/theme.css
而上述公共文件被多个页面所引用,以src/pages/home/index.js为例
// 引入公共代码
import { formatDate, debounce } from '../../common/utils';
import { fetchData } from '../../common/api';
import '../../common/styles/base.css';
import '../../common/styles/theme.css';
// 页面特定代码
import './style.css';
......
- 新增
webpack.config.js
配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
.....
// 关键:使用splitChunks拆分公共代码
splitChunks: {
/*step1:通过cacheGroups配置识别公共模块*/
cacheGroups: {
/*step2.1.commonJS组:提取所有位于src/common/下的JS文件*/
commonJS: {
test: /[\\/]src[\\/]common[\\/].*\.js$/,
name: 'common',
chunks: 'all',
enforce: true,
priority: 20
},
/*step2.2.commonCSS组:提取所有位于src/common/styles/下的CSS文件*/
commonCSS: {
test: /[\\/]src[\\/]common[\\/]styles[\\/].*\.css$/,
name: 'common',
chunks: 'all',
enforce: true,
priority: 30
},
/*step2.3.vendor组:提取所有node_modules中的第三方库*/
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
}
}
},
// 提取运行时代码
runtimeChunk: {
name: 'runtime'
}
},
};
- 打包后的项目结构
dist/
├── css/
│ ├── home.[hash].css # 首页特有样式
│ ├── about.[hash].css # 关于页特有样式
│ ├── login.[hash].css # 登录页特有样式
│ └── common.[hash].css # 公共样式
├── js/
│ ├── home.[hash].bundle.js # 首页特有JS
│ ├── about.[hash].bundle.js # 关于页特有JS
│ ├── login.[hash].bundle.js # 登录页特有JS
│ ├── common.[hash].bundle.js # 公共JS代码
│ ├── vendors.[hash].bundle.js # 第三方库
│ └── runtime.[hash].js # 运行时代码
├── images/
└── *.html
八.Git
1.基本概念
定义:git是开源的分布式的代码版本控制系统
安装和成功标志:鼠标右键有bash终端,命令git -v
出现版本号
配置用户名和邮箱:每次提交代码产生版本时标明身份所用
git config --global user.name 'gaara'
git config --golbal user.email '850432549@qq.com'
验证设置是否成功:
git config --list
什么是git仓库?
仓库(repository)是记录文件状态内容的地方,存着修改所有过的历史记录
位置:隐藏文件夹/.git
如何获取一个git仓库?
方式一:把本地文件夹转换为Git仓库,输入命令git init
方式二:从其他服务器上克隆一个Git仓库(后续)
2.Git的三个区域
工作区:实际开发时操作的项目代码文件夹/gitStudy
暂存区:存放改动后的文件(相比之前的版本),可以部分提交上传,也可以把暂存区的所有改动文件提交上传/.git/index
版本库:提交和保存暂存区中的内容/.git/objects
命令:
git add
:把工作区的代码存放到暂存区
(git ls-files:查看当前项目往暂存区存放了哪些文件)git commit -m 'version 1.1.0 登录页'
:把寄存区中的内容提交到版本库,生成一个版本记录
3.文件状态
Git文件有以下状态:
- 未跟踪
(U)
–新文件,从没被Git管理过 - 已跟踪
(A)
–Git已经知道和管理的文件 - 未修改
( )
–三个区域统一 - 已修改
(M)
–工作区内容发生变化
准备一个新文件index.html,左上角默认显示U
git add ./index.html添加到暂存区,文件状态变成A
git status -s,输出:A (空格) index.html
注:第一列是暂存区的状态,第二列是工作区的状态
改动indx.html并再次git status -s,输出: AM index.html
提交暂存区中的内容到版本库git commit -m "第一次提交" ,输出: (空格)(空格)index.html
*git status命令的作用是显示当前仓库状态
4.对暂存区文件的操作命令
把暂存区中的内容重新恢复到工作区中:git restore 目标文件
移除不想要的暂存区文件:git rm --cached 目标文件
5.版本回退
是指把版本库中的每一版本恢复到工作区git log --oneline
查看历史版本记录git reset --soft 版本号
工作区内容回退到版本号的版本
其中,
soft:保留,保留其他文件,但这些文件会变成未跟踪状态(U)
hard:覆盖,工作区只能存在该版本的这几个文件
mixed:默认,工作区保留其他文件(文件状态变成U),但暂存区只能存在该版本的几个文件
6.文件删除
在工作区和暂存区中删掉该文件,并提交一个不含该文件的版本到版本库中
步骤:
- 删除工作区文件:手动删除
index.js
- 删除暂存区文件:再存一次,不含该文件
git add ./
或单独删掉暂存区中的文件git rm index.js
- 提交到版本库:
git commit -m '删除index.js'
*注:不能把版本库中含有index.js
中的版本删除,只能产生一个不含该文件的版本
7.忽略文件.gitignore
.gitignore
文件能让git彻底忽略对指定文件的跟踪
(这样做的目的是:让git仓库更小更快,避免重复无义的文件管理)
常见的需要git忽略的文件:
- 系统或软件自动生成的文件,如:下载node生成的node_modules
- 编译时产生的文件,如:打包后生成的dist
- 运行时生成的日志文件/缓存文件/临时文件等,如:*.log,
- 涉密/密码/秘钥文件,如:*.pem,*cer
一个.gitignore
忽略文件可能长这样:
//.gitignore
#我是注释
node_modules
dist
.vscode
*.pem
*.cer
*.log
666.txt
8.分支[重要]
什么是分支?
git 分支是git版本控制系统的核心功能,它允许开发者在同一代码库中创建独立的工作线
分支就像平行宇宙,在不影响主线(main或master分支)的情况下进行开发新功能,修复bug
特点: 并行开发,隔离风险,版本管理
命令
查看所有分支:git branch
创建新分支:git branch user001
切换到新分支:git checkout user001
(新版本:git switch user001)
一步到位,创建并立即切换到新分支:git checkout -b user002
合并分支(在master分支下把user001分支的内容合并过来):git merge user001
删除分支:git branch -d user001
合并冲突
合并冲突发生在Git无法自动合并两个分支的修改时
常见场景:
- 相同文件相同位置的修改:同文件同行代码被不同分支修改
- 文件删除冲突:分支A删除文件,分支B修改文件
- 二进制文件冲突:图片/文章等文件被不同分支修改
- 重构冲突:分支A重命名了文件,分支B修改原文件内容
9.常用git命令小结
git init 初始化一个git仓库,生成.git
git add . 把工作区(项目代码)存进暂存区
验证:git ls-files 查看暂存区中的文件
git commit -m 'xxxx' 把暂存区的文件提交到版本库,生成一个版本记录
验证:git log --oneline 查看历史版本
git reset --soft 9b8632f 工作区回退到版本号为"9b8632f"的历史版本
soft:保留,保留其他文件,但这些文件会变成未跟踪状态(U)
hard:覆盖,工作区只能存在该版本的这几个文件
mixed:默认,工作区保留其他文件(文件状态变成U),但暂存区只能存在该版本的几个文件
git branch user001 创建新分支
git checkout user001 切换分支
(新版本是 git switch user001)
git checkout -b user002 创建并切换新分支
git merge user001 合并分支(把user001提交的版本库合并过来)
git branch -d user001 删除分支
10.远程仓库[重要]
把项目的版本库托管到因特网或其他地方,即远程仓库,常用的远程仓库有github和gitee
在远程仓库上只要保存好版本库的历史记录,就可以实现多人协作开发同一项目
10.1.如何把代码上传到git远程仓库?
- 创建远程版本库
- 把本地Git仓库推送上去保存
- 本地Git仓库添加远程仓库原点地址
语法:git remote add 远程仓库别名 远程仓库地址
如:git remote add origin http://gitee.com/xxxxxxxxxxxx/webpackStudy.git
- 本地Git仓库推送版本记录到远程仓库
语法:git push -u 远程仓库别名 本地和远程分支名(都是master可以简写)
如:git push -u origin master
完整写法:git push --set upstream origin master master
10.2.上传代码时的常见报错
- 报错如下
/webpackStudy (master)$ git push -u origin master
git: 'credential-manager' is not a git command. See 'git --help'.
The most similar command is
credential-manager-core
To https://gitee.com/xxxxxxxxxx/webpack-study.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://gitee.com/veizgenq/webpack-study.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
- 错误原因:
合并冲突,本地仓库和远程仓库没有共同祖先,无法自动合并
出现这种情况的原因是初始化远程仓库的时候勾选了自动生成初始化文件,如 README.md,.gitignore
等文件
- 解决方法:先拉取到本地进行合并,再一起推送上传
1.拉取合并:git pull --rebase origin master
等价于:
git fetch origin master # 获取远程最新代码但不合并
git rebase origin/master # 将本地提交"重放"在远程提交之上
2.推送上传:git push -u origin master
10.3.多人协同开发的流程是什么样的?
----开发者A:存暂存区然后提交到本地版本库后,先拉取(以保持代码同步),在推送上去
----开发者B:拉取最新的代码
拉取合并--看到别人同步上去的最新内容:
git pull origin master --rebase
这条命令等价于:
get fetch origin master #获取远程分支记录到本地,但未合并
git merge origin/master #把远程分支记录合并到所在分支下
*注:跟上述bug一样,错误原因就是因为提交提前没有先拉取与合并
更多推荐
所有评论(0)