2022-05-29 ajax,axios,promise,async和await,bootstrap,npm,node,webpack,git,express,CORS跨域,fetch

前端核心技术全解析

文章目录

一.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框中输入的pnamecname组成的对象

//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
输出:AF
* 此时 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: 1BC,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.pngb4d13a877e7b08485a89.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.jsexternals外部拓展选项(防止某些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一样,错误原因就是因为提交提前没有先拉取与合并
10.4.克隆别人的代码后上传到自己的远程仓库需要注意什么?
	* 查看当前远程仓库和本地仓库的链接情况:git remote -v,发现链接的是别人的gitee 仓库(报错: remote origin already exists)
	* 移除现有的origin远程:git remote remove origin
	* 链接自己的远程仓库:git remote add origin https://gitee.com/v***/x****n.git
	* 上传:git push -u origin master
	* (如果报错git: 'credential-manager' is not a git command,
就配置正确的凭据助手:git config --global credential.helper manager-core)

九.Express

0.学习目标
  • 使用express.static()快速托管静态资源
  • 使用express路由精简项目结构
  • 使用常见的express中间件
  • 使用express创建API接口
  • express中使用cors跨域资源共享
1.什么是Express?

express是Node.js平台最流行的web应用框架,提供简介灵活的API和丰富的功能,用于构建Web应用和API接口,有快速,开发,极简的优点

1.1.核心优势
  • 极简高效:简化HTTP请求处理流程
  • 中间件架构:通过中间件链处理请求
  • 路由系统:提供强大的URL路由功能
  • 扩展性强:支持大量的第三方中间件
1.2.Express和node内置属性http的关系

通俗理解:express和node的内置http模块类似,是专门用来创建web服务器

express的本质:一个npm上的第三方包,提供了快速创建web服务器的方法

有了http内置模块为什么还要用express?
http内置模块用起来很复杂,开发效率低,express是基于内置http模块进一步封装出来的,能极大提高开发效率
它们之间的关系类似于web apijQuery之间的关系,后者也是前者进一步封装出来的

2.基本使用
2.1.安装和使用步骤

安装:npm i express@4.17.1
使用步骤:导入>创建web服务器>启动服务器

//server.js
// 1.导入express
const express=require('express');
// 2.创建web服务器
const app=express();
// 3.调用app.listen,启动服务器
app.listen(80,()=>{
  console.log('服务器启动成功....')
})
终端输入命令:node server.js
2.2.监听GET和POST请求
  • 通过app.get()app.post()方法分别监听客户端的GET和POST请求,语法:
app.get("/*URL*/",(req,res)=>{/*处理函数*/})
其中,req是请求对象,res是响应对象
同理,
app.post("/*URL*/",(req,res)=>{/*处理函数*/})
  • 通过res.send()方法,把处理好的内容发送给客户端:
// 1.导入express 
const express = require('express')
// 2.创建web服务器 
const app = express()
// 3.挂载中间件和路由
app.get('/user', (req, res) => {
  res.send({
    name : 'zhangsan',
    age : 18,
    sex : 'male'
  })
})
app.post('/user', (req, res) => {
  res.send('post请求')
})
// 4.启动服务器
app.listen(3000, () => {
  console.log('服务器启动成功,端口号:3000')
})
  • 通过postman模拟客户端发起get和post请求:

URL输入栏中输入:(http://127.0.0.1:80/user),测试server.js服务器的响应数据

2.3.获取URL携带的查询参数和动态参数
2.3.1.通过req.query对象,获取到客户端发送过来的查询参数:
app.get('/user',(req,res)=>{
    res.send({name:'zs',age:20,gender:'男'});//向客户端发送JSON对象
    console.log(req.query)//默认是一个空对象
    res.send(req.query)
})

如何让req.query非空?在postman中输入URL时携带查询参数,如:?name=zs&age=20
示例:

// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app=express();
// 3.配置中间件和路由
app.get('/',(req,res)=>{
    res.send(req.query.name);//把客户端(postman)输入的查询参数返回去
})
// 4. 监听端口,启动服务器
app.listen(3000,()=>{
    console.log('服务器启动......');
});
  • postman
输入:http://127.0.0.1:3000?name=zs&sex=male
输出:zs

在这里插入图片描述

2.3.2.通过req.params对象,获取客户端发送过来的动态参数

示例:

// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app=express();
// 3.配置中间件和路由
// 查询参数用req.query获取
app.get('/',(req,res)=>{
    res.send(req.query.name);//zs
})
// 动态参数用req.params获取
app.post('/user/:id',(req,res)=>{
    res.send(req.params);//{id:1003}
})
// 4. 监听端口,启动服务器
app.listen(3000,()=>{
    console.log('服务器启动......');
});

postman输入POST请求的URL:http://127.0.0.1:3000/user/1003
2.4.托管静态资源express.static()
// 1.导入express
const express=require('express');
// 2.创建web服务器
const app=express();
//app.use(express.static('public'))//http://localhost:3000/hello.html
app.use('/public',express.static('public'))//http://localhost:3000/public/hello.html
app.listen(3000,()=>{
  console.log('服务器启动成功...')
})

此时public文件夹下的所有文件就可以在浏览器中访问如下:
在地址栏中输入http://localhost:3000/public/hello.html,输入相应的网页内容

3.Express中的路由

路由:就是一种映射关系,现实中的路由比如10086客服通话中按键数字和相应业务办理的映射关系就是一种路由

Express中的路由:客户端的请求和服务器处理函数之间的映射关系
一个路由有三个部分组成:请求类型,请求URL,处理函数,语法:app.Method(Path,Handle)
示例:

//挂载一个post路由
app.post('/',(req,res)=>{
    res.send("hello")
})
3.1.路由的匹配过程

每当一个请求到达服务器后,会先经过路由的匹配,再调用相应函数
匹配过程是按照路由的顺序从上到下,若请求类型和URL同时匹配成功,express会将这次请求转交相应的function函数处理

3.2.路由的模块化

express不建议直接把路由挂载到app(开启服务器的express实例)上,推荐做法是把路由抽离为单独的模块
实现步骤:

  • 创建模块js文件(如login.js)
  • 调用express.Router()函数,创建路由实例对象
区别:
	const app=express()//返回的是express的实例
	const router=express.Router()//返回的是路由的实例对象
  • 往路由实例对象上挂载具体的路由
  • 使用modules.exports向外共享卢布对象
  • 使用app.use()函数注册路由模块

示例:

//apiRouter.js
// 1.导入express
const express=require('express');
// 2.创建路由的实例对象
const router=express.Router();
// 3.挂载路由
router.get('/api/test',(req,res)=>{
    console.log('test路由被访问了');
    res.send('Hello express!')
})
// 4.导出路由
module.exports=router;


//server.js
// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app=express();
// 3.引入APIRouter模块
const apiRouter=require('./apiRouter');
// 4. 注册路由模块
app.use(apiRouter);
// 5. 监听端口,启动服务器
app.listen(3000,()=>{
    console.log('服务器启动......');
});

postman的GET请求输入URL:http://127.0.0.1:3000/api/test
输出:Hello express!
控制台打印:
	服务器启动......
	test路由被访问了
4.Express中的中间件
4.1.中间件的概念和格式
  • 中间件: 业务逻辑的中间处理环节,上一级的输出就是下一级的输入
  • 调用流程:当一个请求到达Express的服务器后可以连续调用多个中间件进行预处理
  • 格式:本质上是一个处理函数
//这是一个中间件
app.get('/',function(req,res,next){
	console.log(666);
	res.send("hello")
    next()---必须调用next(),表示把流转关系转交给下一个中间件或路由
});
//这是一个路由
app.post('/',(req,res)=>{
    res.send("hello")
})
中间件和路由的区别:
    中间件函数的形参列表中,必须包含next参数
    路由处理函数中形参列表中,只包含req和res
4.2.全局生效的中间件

定义:
    客户端发起的任何请求,到达服务器后都会触发的中间件
作用:
     多个中间件之间共享同一份req和res,基于此特性,可以在上游的中间件中统一为req和res对象添加自定义属性和方法,供下游中间件或路由使用

  • 定义单个中间件
// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app=express();
// 3.配置中间件和路由
// 配置一个全局中间件,为req添加一个属性
app.use((req,res,next)=>{
  const time=Date.now();
  req.startTime=time;
  next();//必须调用next(),否则后续的路由中间件将不会执行
});
// 路由中调用req,看看是否可以拿到startTime属性
app.get('/',(req,res)=>{
    console.log('startTime:',req.startTime);
    res.send('hello express');//发送响应给客户端
});
// 4. 监听端口,启动服务器
app.listen(3000,()=>{
    console.log('服务器启动......');
});


写法二:此例中,桩件件中的回调可以单独先定义好再引用函数名
// 配置一个全局中间件,为req添加一个属性
function mw(req,res,next){
    const startTime=Date.now();
    // 把startTime挂载到req上,供后面的路由使用
    req.startTime=startTime;
    next();//调用下一个中间件或者路由的处理器函数
}
app.use(mw);
  • 定义多个中间件
function mw1(req,res,next){
    // 给req添加一个属性startTime1
    req.startTime1=Date.now();
    next();
}
function mw2(req,res,next){
    // 给req添加一个属性startTime2
    req.startTime2=Date.now();
    next();
}
app.use(mw1);
app.use(mw2);
4.3.局部生效的中间件

定义:
    不使用app.use定义的中间件,叫做局部生效的中间件
定义多个局部中间件

写法一:app.get('/',mw1,mw2,function(){})
写法二:app.get('/',[mw1,mw2],function(){})
4.4.全局中间件和局部中间件的对比总结示例
//server.js
// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app=express();
// 3.配置中间件和路由
// ================= 全局中间件 =================
// 全局中间件1:请求时间戳记录器
app.use((req, res, next) => {
  req.startTime = Date.now(); // 添加请求开始时间
  console.log(`[全局中间件1] 请求开始: ${req.method} ${req.path}`);
  next();
});
// 全局中间件2:请求计数器
let globalCounter = 0;
app.use((req, res, next) => {
  globalCounter++;
  req.requestId = globalCounter; // 添加全局唯一ID
  console.log(`[全局中间件2] 当前总请求数: ${globalCounter}`);
  next();
});

// ================= 局部中间件 =================
// 局部中间件1:用户认证
const authMiddleware = (req, res, next) => {
  console.log(`[局部中间件1] 执行用户认证: ${req.path}`);
  req.user = { id: 123, name: "测试用户" }; // 添加用户信息
  next();
};
// 局部中间件2:API版本检查
const versionCheck = (version) => (req, res, next) => {
  console.log(`[局部中间件2] 验证API版本: v${version}`);
  req.apiVersion = version; // 添加API版本
  next();
};

// ================= 路由定义 =================
// 路由1:用户资料(使用局部中间件)
app.get('/user',
  authMiddleware, // 局部中间件1
  versionCheck(1.2), // 局部中间件2(带参数)
  (req, res) => {
    // 验证全局属性的共享性
    const response = {
      message: "用户资料",
      global: {
        requestId: req.requestId, // 来自全局中间件2
        startTime: req.startTime  // 来自全局中间件1
      },
      local: {
        user: req.user,        // 来自局部中间件1
        apiVersion: req.apiVersion // 来自局部中间件2
      }
    };
    console.log(response);
    res.json(response);
  }
);
// 路由2:商品列表(不使用任何局部中间件)
app.get('/products', (req, res) => {
  const response = {
    message: "商品列表",
    global: {
      requestId: req.requestId, // 来自全局中间件2
      startTime: req.startTime  // 来自全局中间件1
    },
    local: {
      // 未使用局部中间件,这些属性不存在
      user: req.user,
      apiVersion: req.apiVersion
    }
  };
  console.log(response);
  res.json(response);
});

// 4. 监听端口,启动服务器
app.listen(3000,()=>{
    console.log('服务器启动......');
});
  • 输出结果:
postman:http://127.0.0.1:3000/user

控制台输出:
服务器启动......
[全局中间件1] 请求开始: GET /user
[全局中间件2] 当前总请求数: 1
[局部中间件1] 执行用户认证: /user
[局部中间件2] 验证API版本: v1.2
{
  message: '用户资料',
  global: { requestId: 1, startTime: 1756789675147 },
  local: { user: { id: 123, name: '测试用户' }, apiVersion: 1.2 }
}
postman BODY输出:
{"message":"用户资料","global":{"requestId":1,"startTime":1756789675147},"local":{"user":{"id":123,"name":"测试用户"},"apiVersion":1.2}}

postman:http://127.0.0.1:3000/products 略
  • 总结:
* 中间件别放路由后面,此时中间件不会生效(不管局部和全局)
* next()函数不能省略,且next()之后不要再写额外的代码
* 连续调用多个中间件时,它们共享req对象和res对象
4.5.中间件的分类

Express把常见的中间件的用法分为五大类:应用级别中间件,路由级别中间件,错误级别中间件,Express内置中间件,第三方中间件

4.5.1.应用级别的中间件
通过`app.use()或app.get(),app.post()`绑定到app实例上的中间件(全局中间件和局部中间件)
4.5.2.路由级别的中间件

绑定在express.Router()实例上的中间件
用法和应用级别中间件一样,区别仅仅在于把app替换成router(定义为express.Router()的实例对象)

4.5.3.错误级别的中间件
  • 作用:捕获整个项目中发生的异常错误,绑定项目发生异常崩溃
    - 格式:处理函数中必须有四个形参function(err,req,res,next)(){}
  • 示例:
// 1.引入express
const express = require('express');
// 2.创建express的服务器实例
const app = express();
// 3.配置中间件和路由
//挂载一个get路由
app.get('/', (req, res) => {
  throw new Error('错误了...')//人为抛出一个错误
  res.send("home page")//不会被执行
})
//定义错误级别的中间件:捕获整个项目的异常错误,避免整个项目的崩溃
app.use(function (err, req, res, next) {
  console.log('发生了错误!' + err.message)//发生了错误!错误了...
  res.send('Error:' + err.message)//Error:错误了...
})
// 4. 监听端口,启动服务器
app.listen(3000, () => {
  console.log('服务器启动......');
});
*错误中间件必须注册在所有路由之后(一般中间件都是写在路由之前)
4.5.4.Express内置中间件

Express4.16.0后内置了三个常用中间件

  • express.static:快速托管静态资源
  • express.json:解析JSON格式的请求体数据(表单数据)
  • express.urlencoded:解析URL-encoded格式的请求体数据

示例:通过postman发送JSON格式的数据和urlencoded数据的数据

postman:
body>raw>JSON
    上面输入:POST localhost:3000/user   
    下面输入:{"name":"张三","age":18}
body>x-www-form-urlcoded
    上面输入:POST localhost:3000/book
    下面输入:bookname    水浒传

//server.js
......
// 配置解析JSON数据的中间件
app.use(express.json())
app.post('/user', (req, res) => {
  console.log("使用req.body接收客户端postman发送过来的数据:",req.body);
  res.send('OJBK~');
})

// 配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
app.post('/login', (req, res) => {
  console.log("使用req.body接收客户端表单发送过来的数据:",req.body);
  res.send('欧了~');
})

在这里插入图片描述
在这里插入图片描述

4.5.5.常用的第三方中间件body-server

body-parser:解析请求体数据
使用步骤:

  • 安装:npm i body-parser
    - 导入:const bodyParser=require(body-parser)
    - 注册:app.use(bodyParser.urlencoded(extended:false))
    使用效果:
app.post('/book',(req,res)=>{
    console.log(req.body)//效果:不再打印undefined
    res.send('ok!')
})
4.6.自定义一个中间件

需求:手动模拟一个类似express.urlencoded的中间件,使其可以解析POST提交到服务器server.js的表单数据
知识点:

  • req.on:触发相应的事件
  • querystring模块:node.js的内置模块,用于处理查询字符串,其parse()函数可以将查询字符串解析成对象格式

步骤:

  • step1.定义全局中间键
  • step2.监听req对象的data事件,获取发送到服务器的数据
  • step3.使用querystring模拟解析请求体数据
  • step4.把解析出来的数据对象挂载为req.body
  • step5.把自定义中间件封装为模块
/**自定义中间件将实现express.urlencoded中间件功能,即:解析表单数据**/
// 定义全局中间键
app.use((req, res, next) => {
  let str='';//用来存客户端发送过来的请求体数据
  //使用req.on监听req对象的data事件,获取发送到服务器的数据
  req.on('data', (chunk) => {
    str+=chunk;//拼接请求体数据(请求体数据可能是分批多次发送的)
  })
  //使用req.on监听end事件:触发就说明数据接收完毕
  req.on('end', () => {
    //console.log("打印请求体数据:",str)
    const body=qs.parse(str);//使用querystring模拟解析请求体数据
    req.body=body;//把解析出来的数据对象挂载为req.body
    next();//调用下一个中间件
  })
})

// 对比express.urlcoded中间件
// app.use(express.urlencoded({extended:false}))//配置解析urlencoded格式的数据的中间件

// 挂载一个post路由
app.post('/login', (req, res) => {
  console.log(req.body); // 打印请求体数据
  res.send('登录成功');//给客户端发送响应
})

把中间件单独提取到文件中再在server.js中引入

//custom-body-parser.js
// 引入node的内置模块querystring
const qs=require('querystring');

function bodyParser(req, res, next)  {
  let str='';//用来存客户端发送过来的请求体数据
  //使用req.on监听req对象的data事件,获取发送到服务器的数据
  req.on('data', (chunk) => {
    str+=chunk;//拼接请求体数据
  })
  //使用req.on监听end事件:触发就说明数据接收完毕
  req.on('end', () => {
    //console.log("打印请求体数据:",str)
    const body=qs.parse(str);//使用querystring模拟解析请求体数据
    req.body=body;//把解析出来的数据对象挂载为req.body
    next();//调用下一个中间件
  })
}

//导出
module.exports=bodyParser


//server.js
.....
// 引入自定义中间件
const bodyParser = require('./custom-body-parse');
app.use(bodyParser)
// 对比:app.use(express.urlencoded({extended:false}))
5.使用express写一个接口

知识点:

  • req.query:获取查询字符串
5.1.创建服务器

步骤:
- step1:创建服务器 略
- step2:创建API路由模块

//apiRouter.js 路由模块
const express = require('express')
const apiRouter=express.Router()
module.exports=apiRouter
  • step3:导入路由模块
//server.js
//创建服务器
const express = require('express')
//导入路由模块
const router=require('./apiRouter.js')
//注册路由模块
app.use('/api',router)//router可视为一个全局中间件
app.listen(80,()=>{
    console.log('开启服务器...')
})
  • step4:注册路由模块 (见上面代码)
5.2.编写get接口
//apiRouter.js 路由模块
const express = require('express')
const apiRouter=express.Router()
apiRouter.get('/get',(req,res)=>{//拼接后就是:localhost/api/get?name=zs
    const query=req.query
    res.send({
        status:0,//状态:0表示成功,1表示失败
        msg:'GET请求成功',//状态描述
        data:query//需要响应给客户端的数据
    })
})
module.exports=apiRouter
5.3.编写post接口
//apiRouter.js 路由模块
const express = require('express')
const apiRouter=express.Router()
apiRouter.post('/post',(req,res)=>{//拼接后就是:localhost/api/post
    const body=req.body//获取请求体中包含的urlencoded格式的数据
    res.send({//向客户端响应结果
        status:0,//状态:0表示成功,1表示失败
        msg:'POST请求成功',//状态描述
        data:body//需要响应给客户端的数据
    })
})
module.exports=apiRouter

//server.js
//创建服务器
const express = require('express')
//配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
//导入路由模块
const router=require('./apiRouter.js')
//注册路由模块
app.use('/api',router)//router可视为一个全局中间件
app.listen(80,()=>{
    console.log('开启服务器...')
})
6.CORS跨域资源共享
6.1.接口的跨域问题

编写一个网页使用jQuery进行get请求,出现跨域报错,原因是协议不同

//index.html
  $(document).ready(function () {
    $('#getButton').click(function () {
      $.ajax({
        url: 'http://127.0.0.1:3000/api/get', // 这里替换为你的API地址
        type: 'GET',
        success: function (data) {
          console.log('GET响应数据:', data);
        },
        error: function (xhr, status, error) {
          console.error('Error in GET request:', xhr.responseText);
        }
      });
    });
})

在这里插入图片描述
跨域的解决方法有两种:CORS(主流,推荐)和jsonp(只能针对GET请求)

6.2.什么是CORS?

CORS:跨域资源共享:Cross Origin Resource Sharing,由一系列http响应头组成,这些响应头决定浏览器是否阻止前端JS代码跨域获取资源
它是Express的一个第三方中间件

6.3.使用CORS解决跨域问题
  • 安装:npm i cors
  • 导入:const cors=require('cors')
  • 配置中间件:app.use(core())(在挂载路由之前)
    示例:
客户端index.html(代码不变,)

服务器server.js,代码如下:
// 1.引入express
const express = require('express');
// 引入cors
const cors = require('cors');
// 2.创建express的服务器实例
const app = express();
app.use(cors())// 允许跨域访问
// 3.配置中间件和路由
// 挂载一个html网页客户端访问的get路由
app.get('/api/get', (req, res) => {
  res.send('跨域成功了...');
})
// 4. 监听端口,启动服务器
app.listen(3000, () => {
  console.log('服务器启动......');
});

结果:客户端点击GET按钮,输出"GET响应数据: 跨域成功了..."
6.4.注意事项
  • 1.CORS主要在服务器端进行配置,客户端(浏览器)啥都不用做,
  • 2.CORS在浏览器中有兼容性,需要浏览器(IE10+,CHrome4+,FireFox3.5+)支持XMLHttpRequest Level2
6.5.cors相关的三个响应头

Access-Control-Allow-xxxx是固定搭配

6.5.1.Access-Control-Allow-Origin
作用:控制哪些网页或网站来请求我们的服务器接口
语法:`Access-Control-Allow-Origin:<origin>|*(*代表任何网站都能请求接口)`

其中,origin参数的值指定了允许访问该资源的外域URL
示例:

res.setHeader('Access-Control-Allow-Origin','http://xxxx.cn')
//表示只支持http://xxxx.cn域名下所有网页的跨域请求
6.5.2.Access-Control-Allow-Headers

默认情况下CORS仅支持客户端向服务器发送如下9个请求头

Accept、Accept-Language、Content-Language、
DPR、Downlink、Save-Data、Viewport-Width、Width、
Content-Type(值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)

而该响应头可以对额外的请求头进行声明,让这次请求不会失败
如:X-Custom-Header请求头不在上述9个请求头之中
因此需要配置如下:

res.setHeader('Access-Control-Allow-Headers','Content-Type','X-Custom-Header')
6.5.3.Access-Control-Allow-Methods

默认情况下CORS仅支持客户端发起GET,POST,HEAD请求,其他请求如PUT.DELETE需配置此请求头才能请求成功

res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
    或直接:
res.setHeader('Access-Control-Allow-Methods','*')
CORS请求的分类

根据请求方式和请求头的不同,分为两类:简单请求和预检请求

    1.简单请求    
        -请求方式:GET/POST/HEAD
        -请求头:9种之一
     特点:客户端和服务器之间只会发生一次请求

    2.预检请求
        -请求方式:上述三种请求之外
        -请求头:自定义头部字段
        -向服务器发送了application/json格式的数据
     特点:客户端和服务器之间会发生两次请求,第一次是OPTION请求
       
预检:浏览器和服务器正式通信之前,浏览器回显发送OPTION请求进行预检,以获知服务器是否允许该实际请求
  该次的OPTION请求称为预检请求
服务器响应预检请求后,再发送响应的请求(携带真实数据

在这里插入图片描述

7.编写一个jsonp接口
7.1.JSONP的概念和特点

概念:浏览器通过script标签的src属性请求服务器上的数据,同时服务器返回一个函数的调用,这样的请求方式叫做JSONP
特点:
- 不属于AJAX请求,没有使用XMLHttpRequest对象
- 仅支持GET请求

7.2.创建JSONP接口的注意事项

若已配置CORS,必须在CORS中间件之前声明JSONP接口

    app.get('api/jsonp',()=>{})//jsonp接口,不会被处理成cors接口
    app.use(cors)//配置cors中间件
    app.get('api/get',()=>{})//开启cors的接口
7.3.JSONP接口的实现示例
客户端:index.html
$(function() {
    $('#btn-jsonp').click(function() {
        $.ajax({
            url: 'http://localhost:3000/api/jsonp', // 服务器地址
            type: 'GET',
            dataType: 'jsonp', // 指定为jsonp
            jsonpCallback: 'success', // 指定回调函数名(可选,如果不指定,jQuery会随机生成一个)
            success: function(response) {
                console.log('JSONP请求成功,返回数据:', response);
            }
        });
    });
});

服务器:serverjs
const express = require('express');
const app = express();

// 配置JSONP接口(注意:必须写在cors中间件之前,否则会被处理成CORS接口)
app.get('/api/jsonp', (req, res) => {
    const callback = req.query.callback;// 1. 获取客户端发送的回调函数名
    const data = { name: '张三', age: 22 };// 2. 定义要发送的数据
    const str = `${callback}(${JSON.stringify(data)})`;// 3. 拼接函数调用的字符串:例如,回调函数名(JSON.stringify(data))
    res.send(str);// 4. 将拼接的字符串返回给客户端(script标签执行)
});

// 启动服务器
app.listen(3000, () => {
    console.log(`Server running at http://localhost:${port}`);
});

为什么没有显式使用script标签的src属性?

因为jQuery的$.ajax方法在type为’jsonp’时,会自动动态创建一个标签,
并将其src属性设置为请求的URL,同时附加一个回调函数名(由jQuery随机生成或指定)。
当服务器返回时,会执行这个回调函数。请求完成后,jQuery又会自动移除这个标签。
所以,我们不需要手动创建。

上例如果不使用jQuery,那么index.html代码如下:

//服务器server.js不变

//客户端index.html
// 全局回调函数(服务端将调用此函数)
function handleJsonpResponse(data) {
    // 1.信息输出
    document.getElementById('result').innerHTML = `
        <p>姓名: ${data.name}</p>
        <p>年龄: ${data.age}</p>
    `;
    console.log('JSONP响应数据:', data);
    // 2.清理:移除script标签
    const script = document.getElementById('jsonpScript');
    if (script) script.remove();
}

// 绑定按钮点击事件
document.getElementById('jsonpButton').addEventListener('click', function() {
    // 1.创建唯一的回调函数名(避免缓存问题)
    const callbackName = 'jsonpCallback_' + Date.now();

    // 2.动态创建script标签
    const script = document.createElement('script');
    script.id = 'jsonpScript';
    // 3.设置src属性(包含回调函数名参数)
    script.src = `http://localhost:3000/api/jsonp?callback=${callbackName}`;
   
    // 4.将回调函数挂载到全局对象window
    window[callbackName] = function(data) {
        // 调用处理函数
        handleJsonpResponse(data);
        // 清理:删除全局回调函数
        delete window[callbackName];
    }
   
    // 5.将script标签添加到文档
    document.body.appendChild(script);
});
7.4.面试题:JSONP的原理是什么?为什么JSONP不是ajax?

原理:jsonp=json+padding,它的原理是利用script标签不受同源策略的限制的特性,实现跨域数据的获取
步骤:

  • step1:前端–定义回调函数handleData,创建script标签
  • step2:服务器–接收请求,把数据包装为handleData({...})的形式
  • step3:浏览器–执行返回的JS代码,自动调用handleData函数处理数据
    示例:
	<script>
		function handleData(data){
			console.log('收到的数据是:',data)
		}
	</script>
	<script src="https://xxxx.com/data?callback=handleData></script>

JSONP和AJAX的区别:

1).技术本质和数据传输
	jsonp基于script标签动态加载,获取的是JS脚本,ajax基于XHR对象,直接获取原始数据(JSON/XML)
2).请求方式
	jsonp只能用GET请求
3).错误处理
	jsonp只能通过超时机制检测错误,ajax可通过HTTP状态码精确捕获错误
4).跨域能力
	jsonp原生支持跨域,ajax需配置CORS或代理进行跨域

总结:尽管jQuery等库把jsonp封装为$ajax,但只是语法糖,本质上只是把jsonp跨域的步骤封装了进去

// 伪代码实现
$.jsonp = function(url) {
  const script = document.createElement('script');
  script.src = url + '?callback=jsonpCallback';
  document.body.appendChild(script);
}

十.fetch api

1.什么是fetch?

fetch被称为下一代ajax技术,内部采用Promise方式处理数据(它没有用XMLHttpRequest发送请求,而axios的底层是基于XMLHttpRequest对象实现的)
优点: fetch API语法简洁明了,比XMLHttpRequest更简单易用,除了不兼容IE,主流浏览器已经兼容

2.基本用法

fetch()的功能和XMLHttpRequest()对象相同,只有三处不同:

  • fetch不使用回调函数
  • fetch采用模块化设计,API分散于多个对象中(如:Request对象,Response对象,Header对象)
  • fetch通过数据流(Stream对象)处理数据,可以分开读取,有利于提高网站性能,对于大文件或网速慢的场景很有用

用法:fetch(url).then().catch()

示例:从服务器获取json数据

  • 写法一:
  //  1.使用fetch+.then()方法发起GET请求,获取数据并打印到控制台(不常用)
fetch("https://hmajax.itheima.net/api/books?creator=老李")// 发起请求
   .then(response => response.text())// 解析文本数据
   .then(result => console.log(result))// 打印结果到控制台
   .catch(error => console.log('error', error));// 捕获错误并打印到控制台

上例可以用asyncawait改写为

  • 写法二:async和await,try...catch,查询参数
  // 3.使用try...catch用于捕获错误,fetch要放在try里面,否则捕获不到错误
  async function getData() {
    try {
      // 设置查询参数:?creator=老李
      const res = await fetch("https://hmajax.itheima.net/api/books?creator=老李");// 发起请求
      const result = await res.text();// 解析文本数据
      console.log(result); // 打印结果到控制台
    } catch (error) {
      console.log('error', error);// 捕获错误并打印到控制台
    }
  }
  getData();
//await语句必须放在try...catch里面,这样才能捕捉异步操作中可能发生的错误
后文都采用await的写法,不使用.then()的写法。

其中,
resStream对象,res.json( )异步操作取出所有内容,并转为JSON对象

3.Response对象的常用属性和方法

fetch成功后,得到的是一个Response对象,它对应服务器的HTTP响应

  let res=await fetch('https://hmajax.itheima.net/api/books?creator=老李')
  console.log(res);// Response对象

Response对象的常用属性

res.ok--请求是否成功
res.status--响应的HTTP状态码(:200)
res.statusText--响应的HTTP状态信息
res.url--请求的URL
res.type--响应的类型

Response对象的常用方法

res.json()--解析json对象
res.text()--解析文本数据,得到字符串
res.blob()--解析二进制数据,得到二进制Blob对象
res.FormData()--解析表单数据,得到FormData对象
res.arrayBuffer()--解析二进制数据,得到ArrayBuffer对象
4.fetch的配置参数

fetch的第一个参数是URL,此外还能接收第二个参数作为配置对象,通过配置对象可以自定义发出http请求
语法:fetch(url, options)

fetch 发送GET请求
  //语法: fetch(url, options)
  /**
   * 配置参数介绍:
   * method: 请求方法,默认为'get'
   * headers: 请求头信息
   * body: 请求体信息,通常用于POST和PUT请求
   */
  fetch("https://hmajax.itheima.net/api/books?creator=老李", {
    method: "GET", // 请求方法,默认为'get'
    headers: {
      "Content-Type": "application/json;charset=utf-8", // 设置请求头信息
    },
  })
   .then(res => res.text())// 解析文本数据
   .then(result => console.log(result))// 打印结果到控制台
   .catch(error => console.log('error', error));// 捕获错误并打印到控制台
fetch发送POST请求
//fetch发送post请求:新增图书
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

var raw = JSON.stringify({
   "bookname": "《黑马程序员》",
   "author": "小马",
   "publisher": "北京出版社",
   "creator": "老李"
});

var requestOptions = {// 配置对象
   method: 'POST',
   headers: myHeaders,
   body: raw,
   redirect: 'follow'
};

fetch("https://hmajax.itheima.net/api/books", requestOptions)
   .then(response => response.text())
   .then(result => console.log(result))
5.fetch的二次封装

原生的fetch支持Promise,但是参数还需自己处理,比较麻烦
如:

  • get/delete的请求参数需要在地址栏拼接
  • put/patch/posth的请求参数需要转为json格式设置请求头

因此一般会对fetch进行二次封装
期望达到的效果:不同的请求只要调用同一个封装函数就够了

思路:

  • 1.封装一个函数,接收两个参数:url和options
  • 2.在函数内部,使用fetch发起请求
  • 3.返回一个Promise对象,让外部可以继续使用then和catch
  • 4.在函数内部,根据options的不同,发起不同的请求

封装函数如下:

//封装http函数
async function http(options) {
  // 1.封装一个函数,接收两个参数:url和options
  let { url, method = 'GET', params = {}, data = {} } = options;
  // 2.在函数内部,使用fetch发起请求
  if (method === 'GET') {
    // 处理get请求的params参数
    const queryString = Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&');
    url += `?${queryString}`;
    // 发起get请求
    const response = await fetch(url);
    // 3.返回一个Promise对象,让外部可以继续使用then和catch
    return response.json();
    // 4.在函数内部,根据options的不同,发起不同的请求
    // 发起post请求
  } else {
    const response = await fetch(url, {
      method: method,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });
    return response.json();
  }
}

调用封装函数,发起不同类型的请求:

// 使用封装后的函数发起请求
  // get请求
  async function getFn() {
    let result =await http({
      url: 'https://hmajax.itheima.net/api/books',
      method: 'GET',
      params: {
        creator: '老李'
      }
    }).then(result => console.log("get了吗:", result));
    return result;
  }
  getFn()
  // post请求
  http({
    url: 'https://hmajax.itheima.net/api/books',
    method: 'POST',
    data: {
      bookname: "《黑马程序员》",
      author: "小马",
      publisher: "北京出版社",
      creator: "老李"
    }
  }).then(result => console.log("post了吗:", result));

优化:把封装函数单独提取成一个文件,然后引用该文件(略)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值