前端学习笔记005:数据传输 + AJAX + axios

本文要学习的东西又多又杂,包含 JSON、XML、HTTP 协议、AJAX 请求、Promise、axios。(大家也可以看到我已经努力把题目缩到最短了(T_T))但这些知识对后面前端框架的学习是很有帮助的,所以马上开始~

目录

1. 开始之前

2. 数据传输:数据

2.1 XML 简介

2.2 XML 语法

2.3 JSON 简介

2.4 JSON 语法

2.5 JSON 与 JS 对象的转化

3. 数据传输:传输

3.1 HTTP 简介

3.2 HTTP 报文

3.3 HTTP 请求类型

3.4 HTTP Cookie

3.5 HTTP 状态码

4. AJAX

4.1 AJAX 简介

4.2 AJAX XHR

4.3 AJAX 获取 XML 与 JSON

4.4 AJAX 发送 POST 请求

5. Promise

5.1 Promise 简介

5.2 Promise 的基本使用

5.3 Promise 错误处理 

5.4 Promise all 与 race

6. axios

6.1 axios 简介及安装

6.2 axios 的使用

7. 小结


1. 开始之前

其实我们之所以要学这么多东西,还不是因为 AJAX(T_T)

AJAX(Asynchronous JavaScript and XML,异步的 JavaScript 和 XML)是一种请求数据的新型方式,它能在不刷新页面的情况下向服务器请求数据,而且速度极其快。AJAX 已经被我们广泛使用。

但是 AJAX 需要用到什么?首先就是要向服务器请求数据。要请求数据就需要涉及到数据传输,就牵扯出了 XML,JSON 与 HTTP 协议。然后我们还需要学习 AJAX 的一个二次封装实现:Axios.js(我们一般说 axios 就行了),它体积小,功能强大,并且现代化。但 axios 最“坑”的一点就是它使用了 Promise 实现,然后我们还需要多学一个 Promise(T_T)

当然同学们不要为其感到香菇蓝受,因为这些知识我们即使不学 AJAX,也是要用到的,比如我们以后每天都要碰面的 HTTP 协议。所以大家就放平心态,好好学就行了,因为你如果学了这个,之后学习后面的前端框架你会感到极其的轻松~

下面列举了一下本文需要学习的东西的一个学习链,帮助大家更好理解~

标红的是要学的(绘图:Gitmind)

2. 数据传输:数据

这个部分主要要学习的是 XML 与 JSON,它们是目前数据存储与传输里使用最普遍的两种格式。废话不多说,马上开始~

2.1 XML 简介

XML(eXtensible Markup Language,可扩展标记语言),是一种数据传输格式。大家可能注意到了,XML 与 HTML 都是以 ML(Markup Language,标记语言)结尾的,它们的语法也极其相似。我们经常使用 XML 进行数据的交换。

XML 是以树形的结构存储的,这棵树通常被称作 DOM 树。什么?DOM 不是 HTML 里的内容吗?没错,XML 里也有 DOM 树,而且最让人不可思议的是,这两棵树,是同一个品种的~ 下一小节你就会知道这是为什么~

2.2 XML 语法

我们知道每个 HTML 文件里都会有一个 <!DOCTYPE html> 来说明这是 HTML 文件,XML 里也有一行代码来说明这是 XML 文件:

<?xml version="1.0" encoding="utf-8"?>

看到这个前后尖括号,你是不是想起了 HTML?不仅如此,当你看完 一整个 XML 文件的时候,你会直接大呼 XML 就是 HTML:

<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>

啥呀?这就是 HTML 呀!但是 XML 与 HTML 里也有一些不同的地方。不知道大家有没有注意到,上面的 XML 标签 to form heading 我们都没有见过。这就说明了一个很重要的点:HTML 里的标签全部都是预定义的,XML 里的标签全部都是自定义的。在 XML 里,你可以随意定义标签,标签的名字仅仅只是一个名字没有别的其他作用。因为 XML 是一种数据传输格式,它仅仅只是纯文本。就比如上面那个 body 标签,它仅仅只是一个 body 标签,传输数据时它不管你这是 body 标签还是 head 标签全部一股脑儿往需要传输的地方来传。

除此之外 XML 就没有什么特殊之处了。是不是很简单~

当然需要注意的一个点是 XML 不是保存在 .html 文件里的,是保存在 .xml 文件里的,要不然文件给解析成了 HTML 可不要来找我~

2.3 JSON 简介

JSON(JavaScript Object Notation,JavaScript 对象表示法),是存储数据的一种新方式,它采用类似 JS 对象(字典)的方式存储并传输格式。JSON 轻量级,表达清晰易懂,支持多种编程语言,支持与 JS 交互,使它成为了一种传输数据的新型方式,并且有取代笨重的 XML 的趋势。

2.4 JSON 语法

新建一个 .json 文件,然后就可以往里面写 JSON 了。JSON 的语法像极了 JS 键值对的形式:

{    
    "Student1": {
        "name": "小明", 
                      
        "age": 3,
        "score": { 
            "语文": 60,
            "数学": 100,
            "英语": 99,
            "前端": 100000000000 
        },        
        "schools": [ 
            "幼儿园",
            "小学",
            "中学"
        ]
    },
    
    "studentCount": 1  
}

大家也看到了,JSON 里可以写对象,写键值对,写数组,这些东西还可以嵌套。但是最外层需要使用一个花括号 {} 包裹。几个同级的元素之间需要用到 , 隔开。JSON 与 JS 对象最大的差别就是键值对里的每一个键都需要用双引号包裹,比如 "Student1"。里面的值只有对象,数字,字符串与数组,其他什么都没有(别指望着里面能写 class 与 function……)因为 JSON 也是纯文本,与 XML 一样。它只起到一个传输数据的作用。但是 JSON 最大的优势在于,可以使用 JS 把 JS 对象与 JSON 进行转化。下节就介绍转化方式~

2.5 JSON 与 JS 对象的转化

这小姐我们来聊一聊 JSON 与 JS 对象的两个转化方法,即 JSON.parse 方法与 JSON.stringify 方法。JSON 是 JS 里内置的一个类。

先来聊聊 parse 方法。它的作用是把 JSON 字符串转化为 JS 对象。比如下面这些代码:

// 模板字符串的妙用:可以在里面写引号
let jsonText = `{ "student1": { "name": "小明", "age": 3}, "studentCount"}`;
console.log(JSON.parse(jsonText));

它会打印出 jsonText 被转化为的 JS 对象:

然后是 stringify 方法。这个方法大家应该也能推测出功能了把,就是把 JS 对象转化为 JSON。下面我们再把上面的 jsonText 转化为 JS 对象然后再转化回 JSON~(这娃子,无聊到这种地步了……)

let parsedJSON = JSON.parse(jsonText);
console.log(JSON.stringify(parsedJSON));

如果你作死想用 Stringify 转化函数,它什么也不会给你传;

但如果你想转化类的实例对象,那么 JSON 会把这个类转化为对象,但是里面的函数会没~

如果你真的想转化函数,可以先把它转化为字符串,事后再将它转化为函数~ 

当然很多人就想着:我们总不可能总是转化字符串吧?我如果想获取一个 JSON 文件再转化怎么办呢?这是后面 AJAX 的内容~

3. 数据传输:传输

这一小节是数据传输中的传输部分,即介绍数据的传输方式。我们将要学习前端非常重要的一个知识点:HTTP 协议。这个协议是前端代码与后端服务器连接的重要桥梁~

3.1 HTTP 简介

HTTP(Hyper Text Transport Protocol,超文本传输协议),它是我们普遍在使用的网络协议。它是进行前后端数据请求与传输必要的协议。简单来说,比如前端页面需要一堆数据,就通过 HTTP 协议向服务器申请,服务器收到了前端页面的请求,就处理数据然后再通过 HTTP 协议发回去。

那 HTTP 具体的实现流程是怎么样的呢?下面开始具体分析~

3.2 HTTP 报文

首先我们把上图变得详细一点,变成这样:

可以看到我们把请求数据的豹纹(打错了~ 是报文~)给列了出来。上面的是前端页面发给服务器的报文,下面的是服务器发给前端页面的报文~

下面逐行解析~

请求(request)部分 

请求数据的操作称为请求(request),而请求数据时发送的报文称作请求报文~

第一行:GET /data.html

前面的 GET 是请求类型,GET 指获取数据,具体的请求类型我们后面会讲~ 

后面的 /data.html 指我们要获取服务器下的 data.html 文件的内容~

第二行:HTTP/1.1

前面的 HTTP 表示我们是使用 HTTP 协议进行数据传输的,后面的 1.1 指 HTTP 协议的版本~

第三行:Host: baidu.com 

前面我们只说明了服务器下的什么文件,并没说需要获取什么服务器的内容。Host 就是来指定服务器的。这里的 baidu.com 说明我们要获取 baidu.com 这个服务器的内容~

响应(response)部分

响应(response),就是服务器收到请求后再次发给前端页面的回应。这个部分有点长,我们先来拆~

第一行:HTTP/1.1 200 OK

前面的 HTTP/1.1 说明了使用的传输协议与使用的版本,后面的 200 OK 指状态码,后面会介绍。200 OK 指成功接收到请求~

第二行:Date: Wed, 5 oct 2022 22:34:00 GMT

这行说明了响应的时间,没啥好说的~

第三行:Content-Length:250 

这行说明了内容的字节长度,不要在意那么多细节~

第四行:Content-Type: text/html 

 这行说明了内容的类型,这里的 text/html 指 HTML 文档~

内容部分:<html> ...

这里就是响应的内容了,这些就是前端页面需要的内容~

一般来说,HTTP 请求与响应的报文结构如下所示:

请求:

请求行:包括了客户端的请求方法,链接、HTTP版本。即上面的请求报文。

请求首部字段:包括了请求的附加信息。

通用首部字段:请求报文和响应报文都会使用的报文。

实体首部字段:补充了与实体有关的资源信息。由于主体内容缺失所以我们见见不到~

报文主体:即请求的主体内容。那为什么我们上面没有见到呢?因为 GET 请求类型没有主体~

响应:

响应报文状态行:包括HTTP版本、状态码。即上面的第一行;

响应报文首部字段:包括响应报文发回去时的附加信息。

通用首部字段:请求报文和响应报文都会使用的报文。

实体首部字段:补充了与实体有关的资源信息。这些其实就是上面的 Date 到 Content-Type 这些内容~

报文主体:响应回来的数据内容,即前端页面请求的内容。

一般来说,就只有请求行,实体首部字段与报文主体比较有用。当然这些只是说明了 HTTP 报文的内容,具体发的是什么,怎么发,后面 AJAX 会讲解~

3.3 HTTP 请求类型

前面的报文说到了 HTTP 的请求类型。HTTP 的请求类型,它的作用是说明我们需要让服务器进行什么操作。比如 GET 请求就是获取数据,PUT 请求就是发送数据给服务器等等等等~

GET 请求

GET 请求是最简单且最常用的请求类型了。它代表向服务器获取某个文件资源。

POST 请求

其实与 GET 请求差不多,都是请求数据。只不过 POST 请求需要前端页面给服务器传数据,再让服务器返回处理之后的结果。一般用于表单提交信息这些场合。

PUT 请求 

这个请求的作用是前端页面给服务器传资源。也很好理解~

DELETE 请求 

这个请求的作用是让服务器删除文件,说的简单一点就是叫服务器帮你删除文件~

HEAD 请求

HEAD 请求用于获取报文首部。简单来说,就是一个 GET 请求,在返回数据时临时把报文主体删掉。它一般的作用就是获取实体首部字段,验证关于实体的信息。

OPTIONS 请求

如果你在敲代码的时候突然忘了你可以发送什么请求,那你可以使用 OPTIONS 请求来获取服务器支持的请求。粽锁粥汁,这个功能一般用不上……

3.4 HTTP Cookie

HTTP 里有一个重点,就是大名鼎鼎的 Cookie。虽然它的名字叫 Cookie,但它和饼干没有半毛钱关系~

在学习 Cookie 之前,我们先来认识一下 HTTP 协议的无状态(stateless)特性。简单来说,就是服务器不保存前端页面的任何请求数据。你今天向一个服务器请求,服务器在给你发完响应的时候立马就忘记了你是谁你在哪你居然曾经给我发送过请求而且我居然还响应了你。既然 HTTP 协议无状态,那服务器接收到那么多请求,怎么知道谁是谁?于是,Cookie 就出现了。简单来说,Cookie 是为了让服务器知道这个请求是哪个人发的。

这里我们以一个注册案例进行讲解。在发送完注册请求之后,服务器会发回给我们一个报文,其中有一个 Set-Cookie 信息。浏览器收到响应之后就会把 Set-Cookie 里面的信息保存在浏览器内。

 然后下一次再请求的时候只需要把上次服务器给你的 Cookie 信息连同请求发给服务器,服务器一看这个信息就知道是你啦~ 这个发 Cookie 的操作是浏览器帮忙完成的,不用自己动手也能丰衣足食~

3.5 HTTP 状态码

请求不可能总是一帆风顺地得到需要的返回信息,如果出了啥错那就完蛋了。为了让我们出错的时候知道自己错在哪里从而让我们改正(修复 Bug),HTTP 设置了状态码(Status Code)这个属性。默认如果你没出错状态码是 200 OK~

状态码由两部分构成:状态码与状态短语。比如上面的 200 OK,200 是状态码,OK 是状态短语,代表没问题,很 OK~

可以访问这个链接来查看 HTTP 状态码,一般常用的就是 200,404 那几个~

4. AJAX

AJAX 是我们今天的重头戏,也是这篇文章的核心。AJAX 是一种请求服务器的方式,它的优势在于不需要刷新网页就可以获取到服务器信息~

4.1 AJAX 简介

(在这里浅浅 Copy 一下第一节的内容)

AJAX(Asynchronous JavaScript and XML,异步的 JavaScript 和 XML)是一种请求数据的新型方式,它能在不刷新页面的情况下向服务器请求数据,而且速度极其快。AJAX 已经被我们广泛使用。

这里我们举一个例子。传统的网页请求服务器数据时,总是要刷新一下,拖慢网页速度。(就是下图红箭头那个地方要转圈圈)

而如果你不想刷新网页,又想获取网页的内容怎么办?AJAX 可以帮你。AJAX 可以在你运行 JS 代码的时候异步地获取数据,很方便。

下面举一个 AJAX 实际使用的例子。比如我们在百度搜索框里搜索 AJAX,它下面就跳出了很多提示词。这些数据怎么来的?服务器请求来的呗!但是你的网页有刷新吗?有影响用户体验吗?没有对吧。所以 AJAX 这是一个特别方便而且增强用户体验的东西。

那 AJAX 具体怎么实现呢?下面会先讲原生的实现方式,后面会讲一个二次封装 axios~ jQuery 由于过时了所以我们不讲~

4.2 AJAX XHR

AJAX 的核心其实就是 XHR(XML HTTP Request,XML HTTP 请求),从名字上我们也可以知道,这是一个做 HTTP 请求的库,它可以请求 XML(但是又不止可以请求 XML)。而他有什么不同之处吗?就是可以实现 AJAX。所有的库你想使用 AJAX,就得用 XHR,包括后面的 axios 封装,jQuery 等等,其实底层都是用的 XHR,只不过使用原生的 XHR 比较麻烦,使用别人封好的会简单一些,但是 XHR 也要会~ 接下来我们会通过一个小案例来讲解 XHR。

在真正发送请求之前,我们先要使用 Node.js + Express.js 在 localhost:6888 搭建一个服务器,只要有人请求就发送 Hello World 给请求的人。没学过 Node.js 与 NPM 的看这里。首先你要先在一个目录下把 Express 安装好,然后在这个目录下创建 server.js,内容如下:

// 学过 HTTP 之后重温一下这段代码
let express = require('express');
let app = express();

// 这里的服务器地址是 http://127.0.0.1:6888/ajaxServer
app.get('/ajaxServer', (request, response) => { // 这个函数有两个参数,分别是收到的请求报文与准备发送的响应报文
                                                // 这个函数每收到请求都会执行一次
  console.log("有人请求 ajaxServer 了"); // 每收到一次请求打印一次这句话
  response.send('Hello World!'); // 响应一个字符串 Hello World!
});

app.listen(6888, () => console.log("服务器已开始监听 6888 端口") );

使用 node.js 运行程序,打开浏览器 http://127.0.0.1:6888/ajaxServer,一切正常,并且服务器有提示,而且网页打开几次就输出几次。其实浏览器也对这个服务器进行了请求,但是是同步的,不属于 AJAX 请求。但是 ajaxServer 不管你用的同步还是异步只要你请求了就给你返回响应。

 

 接着我们需要创建一个 Live Server,做一个 HTML + JS 的小 Demo(示例)。首先先确保服务器正在监听,并且 Live Server 扩展已经安装;

 然后再同一目录下创建两个文件 index.html 与 script.js。index.html 源代码如下所示:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>AJAX Demo</title>
    </head>
    <body>
        <button onclick="sendAjax()">点击我发送 AJAX 请求</button><br><br>
        收到的数据:<span id="text-span"></span>
        <script src="./script.js"></script>
    </body>
</html>

大家应该都看得懂什么意思吧~ 然后是 script.js,这里我们会创建一个 XHR,当用户点击按钮时获取服务器数据,并把数据显示在 span 标签上。代码的注释一定要看!~

// 刚刚有人可能已经注意到了我们给 button 绑了一个点击事件
// 下面这个函数就是处理点击事件的
function sendAjax(){
    // 新建一个 XHR 对象
    let xhr = new XMLHttpRequest();
    // xhr.open 方法初始化这个 AJAX 请求的 HTTP 请求报文
    // 第一个参数指定这个请求的请求类型,一般为 GET 与 POST。
    // 第二个参数指定请求的链接,我们把 Host 与文件合在一起了
    // 第三个参数指定是否异步,我们使用的时候都为 true 没什么好讲的~
    xhr.open("GET","http://127.0.0.1:6888/ajaxServer",true);
    // xhr.send 方法发送请求,在 GET 请求里,这个方法没有参数
    xhr.send();
    // 获取收到的信息使用 xhr.responseText
    let respText = xhr.responseText;
    console.log(respText);
    // 把这个信息放入 span 中
    document.getElementById("text-span").innerHTML = respText;
}

VS Code 文件管理器里右键 index.html,选择“Open with Live Server”; 

可以看到 Live Server 在浏览器里打开了你的 HTML,外观都正常,但是为什么点击按钮没反应?看了看控制台,发现报错了:

在解决问题之前我们要先看看服务器有没有被请求,很明显是有的,而且按钮点几次服务器打印几次。

 那就说明我们的代码没问题。其实,这个问题就是前端界“臭名昭著”的跨域问题。HTTP 请求不知道那个人规定的,必须要协议、地址、端口号全部一样的两个文件才能发送 HTTP 请求,不然就会报错说跨域了(其实这也是为了安全考虑)。我们的服务器与前端网页的协议都是 HTTP,地址都是 127.0.0.1(不信你看地址栏),就是最后一项:端口号不一样,一个是 Live Server 的 5500,一个是服务器监听的 6888。所以我们要解决这个问题才能保证 AJAX 请求成功。

很多同学第一个想到的就是:我们服务器也监听 5500 不就好了吗?各位请检查一下自己的网络功底,一个端口是不能两个服务器同时监听的啊喂!!!

其实只需要给你的浏览器安装一个支持跨域的插件就行。假如你用的是 Edge,打开扩展商店,把上面的第一个扩展安装一下就行~

然后在扩展页面中打开即可。看到图标变成彩色说明开启成功。在后面前端框架的开发中我们会使用另一种方式:代理,所以不需要担心用户有没有装这个插件~

然后再点击这个按钮,错也不报了,但是为啥没有打印出来信息呢?服务器都还没有给我们发信息我们就获取了,那自然就获取不到了啊!这就好比给一个人发信,一把信发出去我们就去查看信箱里对方的回信,那肯定只能两手空空地回家。所以我们要在收到信的时候才获取信息,即收到响应时才获取返回的信息。那怎么做呢?这里我们就需要涉及到 XHR 对象里的 readyState 了。

readyState,说的简单一点就是 XHR 接收响应的属性。XHR 对象里有三个与 readyState 有关的属性:readyState(当这个值为 4 时说明已收到响应),status(收到响应的状态码,200 为正常,404 为未发现文件),onreadystatechange(当 readyState 发生改变时需要做的函数)。然后我们就可以改进一下 script.js 了。把它改成下面这样:

// 刚刚有人可能已经注意到了我们给 button 绑了一个点击事件
// 下面这个函数就是处理点击事件的
function sendAjax(){
    // 新建一个 XHR 对象
    let xhr = new XMLHttpRequest();
    // xhr.open 方法初始化这个 AJAX 请求的 HTTP 请求报文
    // 第一个参数指定这个请求的请求类型,一般为 GET 与 POST。
    // 第二个参数指定请求的链接,我们把 Host 与文件合在一起了
    // 第三个参数指定是否异步,我们使用的时候都为 true 没什么好讲的~
    xhr.open("GET","http://127.0.0.1:6888/ajaxServer",true);
    // xhr.send 方法发送请求,在 GET 请求里,这个方法没有参数
    xhr.send();
    // 设置 xhr.onreadystatechange 函数,当 readyState 的值改变时触发
    xhr.onreadystatechange = () => {
        if (xhr.readyState == 4 && xhr.status == 200){ // 判断响应是否已经正常返回
            // 在这里处理收到的响应
            let responseText = xhr.responseText;
            console.log(responseText);
            document.getElementById("text-span").innerHTML = responseText;
        }
    }
}

保存 JS,刷新网页。再次点击按钮,你会发现请求已经成功了~

4.3 AJAX 获取 XML 与 JSON

既然我们掌握了使用 AJAX 发送 HTTP 请求的方式,那我们不是也可以使用 AJAX 获取 XML 文档与 JSON 文档了?事实证明就是可以~

先来 XML。首先我们需要新建一个 hello.xml 文件,文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<container>
  <text id="text-label">Hello, There!</text>
</container>

保存,然后接下来我们使用 AJAX 请求它。由于我们不需要开启服务器,只需要请求 Live Server 上的文件。Live Server 的页面请求 Live Server 的文件自然不会发生跨域问题~

新建 index.html 与 script.js。index.html 除了引入 script.js 之外什么也不需要做。script.js 写入以下内容:

let xhr = new XMLHttpRequest();
// 请求 Live Server 服务器上的 hello.xml 文件,127.0.0.1:5500 是 Live Server 服务器的根目录。
xhr.open("GET","http://127.0.0.1:5500/hello.xml",true);
xhr.send();
xhr.onreadystatechange = () => {
    if (xhr.readyState == 4 && xhr.status == 200){
        // xhr.responseXML 可以获取收到的 XML
        let respXML = xhr.responseXML;
        // 没错,XML 也可以使用 getElementById 等一系列获取 DOM 的操作
        console.log(respXML.getElementById("text-label"));
    }
}

使用 Live Server 打开,你会看到控制台清晰地打印出了刚刚 XML 里的那个 DOM 标签~

你甚至可以获取到它的属性与 innerHTML,没错,innerHTML。

接下来是 JSON。我们也是创建一个 hello.json,写入如下代码(实际上就是上面讲 JSON 时的):

{    
    "Student1": {
        "name": "小明", 
                      
        "age": 3,
        "score": { 
            "chinese": 60,
            "maths": 100,
            "english": 99,
            "frontend": 100000000000 
        },        
        "schools": [ 
            "幼儿园",
            "小学",
            "中学"
        ]
    },
    
    "studentCount": 1  
}

 保存,然后使用 AJAX 获取它。把 script.js 更改成如下样子。

let xhr = new XMLHttpRequest();
// 请求 Live Server 服务器上的 hello.json 文件
xhr.open("GET","http://127.0.0.1:5500/hello.json",true);
xhr.send();
xhr.onreadystatechange = () => {
    if (xhr.readyState == 4 && xhr.status == 200){
        let respJSONText = xhr.responseText;
        console.log('收到的 JSON 文本:');
        console.log(respJSONText);
        console.log('转化成的 JS 对象:');
        console.log(JSON.parse(respJSONText));
    }
}

非常 nice~

4.4 AJAX 发送 POST 请求

前面我们说了 GET 请求的发送方法,那 POST 请求呢?下面我们就以一个简单的不刷新网页注册功能来讲解 AJAX 的 POST 请求

还是那两个文件 index.html 与 script.js,index.html 写入如下内容:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>AJAX Demo</title>
    </head>
    <body>
        <h1>注册</h1>
        用户名:<input type="text" name="username" id="username"><br>
        密码:<input type="password" name="password" id="password"><br>
        <button onclick="register()">提交注册信息</button>
        <!-- hr 标签指的是分割线 -->
        <hr>
        <h2 id="bottom-label">等待提交注册</h2>
        <script src="./script.js"></script>
    </body>
</html>

然后写 script.js,注释不看等于白干:

// 点击按钮的回调
function register(){
    // 先把我们输入的用户名与密码拿出来
    let inputUsername = document.getElementById("username").value;
    let inputPassword = document.getElementById("password").value;

    // 创建 xhr 没啥区别
    let xhr = new XMLHttpRequest();
    
    /*
    最关键的一步来了,open 里的 url 后面要传入用户输入的内容
    这也是 POST 里最大的难点
    我们使用 querystring 来进行传递
    一般的,形如 a=3&b=4 之类的字符串就是 querystring 字符串
    它可以被解析为 {a: 3, b: 4} 这样的对象
    那怎么在 url 后面接 querystring 呢?
    只需要在 url 后面加一个 ? 号后面就可以写 querystring 了
    比如 https://www.baidu.com/api?a=3&b=4
    */

    // 里面的 127.0.0.1:6888/postServer 是我们等会儿要创建服务器的路径
    xhr.open("POST",`http://127.0.0.1:6888/postServer?username=${inputUsername}&password=${inputPassword}`,true);

    // 然后发请求
    xhr.send();
    xhr.onreadystatechange = () => {
        if (xhr.readyState==4 && xhr.status==200){
            // 把获取到的字符串放到 id 为 bottom-label 的标签上去
            document.getElementById("bottom-label").innerHTML = xhr.responseText;
        }
    }
}

现在还不能运行 HTML,因为服务器还没有搭建好。在确保 express 已经成功安装之后创建一个文件叫做 postServer.js 写入以下内容:

let express = require('express');
let app = express();

// 请注意这里的方法名为 app.post
// 因为我们需要响应的是 POST 请求
app.post('/postServer', (request, response) => {
  // request.query 读取我们 POST 时发送的 querystring
  // express 已经帮我们解析好了
  let searchQS = request.query;
  // 做一些存入数据库的操作……
  // 发回去一个文本
  response.send(`${searchQS.username},欢迎您!`);
});

app.listen(6888, () => console.log("服务器已开始监听 6888 端口") );

把文件全部保存。使用 node 运行服务器脚本 postServer.js,然后使用 Live Server 打开 HTML 文件。你应该会看到下图界面:

 把用户名密码填上去然后点击按钮(记得把解决跨域的插件打开要不然报错),你应该会看见这个样子:

 恭喜你成功发送了第一个 POST 请求!

5. Promise

接下来我们开始学习一个新的知识点:Promise。它也是 ES6 新增的语法,准确来说是新增的类。由于后面我们学习 axios 时需要用到,所以这里简单学习一下~

5.1 Promise 简介

既然 Promise 是一个类,那它是干嘛的呢?按照官网上的描述,Promise 是被用于执行异步操作的。

首先聊聊异步操作。其实我们没有必要把它理解地有多复杂。其实异步操作就是需要一定的时间的操作,比如 setTimeout,需要时间等待吧?那他就是一个异步操作。

// 异步操作
setTimeout(() => { console.log("这是一个异步操作") }, 1000);

那异步操作你执行就执行呗,用什么 Promise 啊?下面有一个需求让大家完成一下。比如下面有三个异步操作:吃饭、睡觉、打代码,分别需要 2 秒,4 秒与 8 秒的时间。

function eat(){
    setTimeout(() => {
        console.log("吃饭");
    }, 2000);
}

function sleep(){
    setTimeout(() => {
        console.log("睡觉");
    }, 4000);
}

function writeCode(){
    setTimeout(() => {
        console.log("打代码");
    }, 8000);
}

那如果你想先打代码,再睡觉,再吃饭,那应该怎么写?那不简单嘛,直接先写 writeCode() 再写 sleep() 再写 eat() 不就行了吗?可事实并不是这样的。下面是执行这一操作的结果:

你会发现它先停了两秒,然后打印吃饭,再过两秒打印睡觉,再过四秒打印打代码。很明显这不是我们想要的结果。我们明明先写的打代码再写的睡觉再写的吃饭啊,为什么到了输出这里就变成先吃饭再睡觉再打代码了?

这是因为浏览器默认的多线程(Multithreading,这里可能解释地有些不准确,理解万岁)机制。你以为你的异步操作是同时做的,实际上浏览器为了提高代码的运行效率,让这三个异步操作同时运行,简单来说就是你的浏览器让你一边吃饭一边睡觉一边打代码(T_T)由于吃饭耗时短,所以自然吃饭就先打印出来了,其次是睡觉,最后才是打代码。

但是我们就是想先打代码再睡觉再吃饭该怎么办啊?有些 JS 功底好的同学可能就会说了:把它们嵌套起来不就行了吗?像下面这样:

setTimeout(() => {
    console.log("打代码");
    setTimeout(() => {
        console.log("睡觉");
        setTimeout(() => {
            console.log("吃饭");
        }, 2000);
    }, 4000);
}, 8000);

大家可以尝试一下。这段代码确实可以实现需要的功能。但他有一个明显的缺点,就是代码非常不灵活,而且可读性极差,甚至当异步操作多起来的时候,会产生“回调地狱”。所以我们才需要 Promise 来完成这些操作。其实官方对 Promise 的描述并不准确,按照我的理解来说:Promise 是被用于更优雅地进行有序的异步操作的。

5.2 Promise 的基本使用

前面卖了那么多关子,那 Promise 到底该怎么用呢?先来看看下面这个示例,有点长,照例注释一定要看:

// 先定义三个函数,分别吃饭睡觉打代码
// 返回值是一个 Promise 对象
function eat(){
    // Promise 对象的构造函数接收一个参数,参数的类型是函数
    // Promise 对象在 new 时会调用你传的函数
    // 所以我们尽量把 Promise 对象的 new 操作包在一个函数里,在需要的时候再调用它拿到 Promise 对象
    return new Promise((resolve, reject) => { // 函数的参数有两个
                                            // 前面一个 resolve 是成功情况的回调
                                            // 反之 reject 就是失败情况的回调
                                            // 后面再详细讲解
        // 函数里面放需要执行的异步操作
        setTimeout(() => {
            console.log("吃饭");
            resolve("");                    // resolve 需要调用一下说明异步操作执行成功
                                            // 里面传的参数可以给下一个异步操作用
                                            // 由于我们不用所以传一个空字符串
        }, 2000)
    })
}

// 同理定义接下来的几个函数
function sleep(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("睡觉");
            resolve(""); 
        }, 4000)
    })
}

function writeCode(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("打代码");
            resolve(""); 
        }, 8000)
    })
}

// 下面开始用 Promise 来有序执行异步操作
// 首先我们想先打代码所以先调 writeCode 拿到 Promise 对象

writeCode().then((data) => { // then 方法传递的参数是一个函数,在 Promise 构造函数中的异步操作执行完之后调这个函数
                             // 这个 then 就是有序执行异步操作的精髓
                             // data 参数就是接到了 resolve 传进来的数据
                             // 看着还挺好用的,后面会讲
    return sleep();          // 这个函数的返回值将会成为一整个 then 函数的返回值
}).then((data) => {          // 由于返回值也是 Promise 所以可以接着调用 then 方法
    return eat();
})

执行出来的效果非常 nice:

有没有 get 到 Promise 的好用?如果你还没 get 到,那对比一下这两串代码,你或许就懂了:

使用原生方式定义的:

 使用 Promise 定义的:

大家可能会说:前面我们不是耗费那么多时间去定义了那么多函数吗?大家不用担心,在真实开发中我们根本不需要去定义这些 Promise 函数(鹅鹅鹅……)

下面再简单来说说 resolve 的使用。刚刚我们也看到了 resolve 可以传参数给 then 当中的方法,我们来实验一下:

function resolveTest(){
    return new Promise((resolve, reject) => {
        setTimeout(() => { 
            console.log("异步操作执行完成")
            resolve("一些数据");
        }, 1000);
    })
}

resolveTest().then((data) => {
    console.log(data);
    // 其实 then 里的函数不一定要写异步操作,是一个函数就行
    // 如果后面再继续 then 下去函数将会接到这个函数的返回值
    // 因为如果你没有返回东西或是返回一个不是 Promise 的东西,Promise 会帮你自动生成一个
})

 可以看到在等了一秒之后打印出了“异步操作执行完成”,随后立即打印出了“一些数据”,说明 then 里的函数是可以收到 resolve 的。

那 reject 又怎么接到呢?当我们在异步操作执行失败的时候,就可以调用 reject,reject 函数的参数是一个字符串。那它怎么被接到呢?其实在 then 中是可以接到两个参数的,一个对应 resolve 的回调,一个对应 reject 的回调。下面再来个例子:

// 可以改变 i 的值,会输出不同的结果
function rejectTest(){
    return new Promise((resolve, reject) => {
        let i = 11;
        if (i > 10){
            console.log("异步操作处理失败");
            reject("数字太大了");
        } else {
            console.log("异步操作处理成功");
            resolve(i);
        }
    })
}

rejectTest().then(
// resolve 的回调
data => {
    console.log("resolve");
    console.log(data);
}, // 这个逗号漏了就 BBQ 了
errMessage => { // errMessage 接到 reject 的内容
    console.log("reject");
    console.log("原因:" + errMessage);
});

运行结果:

这里还需要知道 then 里函数的一种其他用法。返回值不一定要是 Promise 对象,普通变量也可以。在接下来的 then 中还可以继续接 data,只不过接收到的不是什么 resolve,而是上一个操作的返回值。

function getPromise(){
    return new Promise(( resolve, reject ) => {
        setTimeout(() => resolve(""), 1000); 
    })
}

getPromise().then(data => {
    console.log("第一个 then 开始执行");
    return "一些数据";
}).then(data => {
    console.log("第二个 then 收到的 data:"+data);
})

运行效果:

  

5.3 Promise 错误处理 

这里提一个小问题:如果在执行 then 里的函数的时候报错了,怎么办呢?可以使用 Promise 的 catch 方法。它紧跟在 then 后面, 参数是一个函数,当你的代码报错就会进到这个 catch 里的函数里。这个函数有参数,参数是报错信息。比如下面一个例子:

function catchTest(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("") 
        }, 1000)
    })
}

catchTest().then(data => {
    console.log(somedata); // somedata 没定义肯定报错
}).catch( reason => { // reason 接到了报错信息
    console.log("报错了,原因:"+reason);
})

运行效果:

请注意这个 catch 只对 then 里的处理有用,如果你在一开始 new Promise 那个里面的函数就出错,天王老子也救不了你(=_=)

catch 有一个妙用,就是当你不想再继续执行后面的代码的时候,即想跳出 Promise 的时候,可以抛一个小错误,然后在 catch 里面接到它,并且不做任何处理,你就成功跳出 Promise 了~

function catchTest2(){
    return new Promise(( resolve, reject ) => {
        setTimeout(() => resolve(""), 1000);
    })
}

catchTest2().then(() => {
    console.log("打印一句话");
    console.log(somedata); // somedata 未定义就会跳进 catch 里
    console.log("本来应该也打印这句"); // 由于前面报错这个就不执行了
}).catch(reason => {}); // catch 又是空的

运行结果:

 除此之外我们还需要认识一个方法叫做 finally。它通常用于异步操作的收尾。不管有没有报错,finally 始终会执行。由于这个方法我们用的不多所以不上示例。

5.4 Promise all 与 race

这两个方法都是用于同时进行多个 Promise 异步操作的。先来认识 all,它可以收到一个参数,参数类型是数组,数组里放着一个个返回 Promise 的函数。这个函数会将这些 Promise 同时运行起来,最后将所有 Promise 的 resolve 收集起来成为一整个 all 方法的 Resolve。

function eat(){
    return new Promise((resolve, reject) => { 
        setTimeout(() => {
            console.log("吃饭");
            resolve("吃完饭了");                    
        }, 2000)
    })
}

function sleep(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("睡觉");
            resolve("睡完觉了"); 
        }, 4000)
    })
}

function writeCode(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("打代码");
            resolve("代码没打完"); 
        }, 8000)
    })
}

// 请注意这个方法是 Promise 身上的静态方法
Promise.all([eat(),sleep(),writeCode()]).then( data => {
    console.log(data);
})

运行一下,可以看见吃饭睡觉打代码同时在运行在输出,并且最后的 then 方法打印出了所有的 resolve。

race 方法就不一样了。race 方法有点类似于赛跑,比如你传了三个 Promise 进去,哪个 Promise 最快完成就把这个 promise 的 resolve 传给 then,不等待剩下两个 Promise 运行完。当然 then 运行完了剩下的两个 promise 也一样会跑完的。

function eat(){
    return new Promise((resolve, reject) => { 
        setTimeout(() => {
            console.log("吃饭");
            resolve("吃完饭了");                    
        }, 2000)
    })
}

function sleep(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("睡觉");
            resolve("睡完觉了"); 
        }, 4000)
    })
}

function writeCode(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("打代码");
            resolve("代码没打完"); 
        }, 8000)
    })
}

// 请注意这个方法也是 Promise 身上的静态方法
Promise.race([eat(),sleep(),writeCode()]).then( data => {
    console.log(data);
})

运行效果很明显:

6. axios

axios 是本节最重要的内容,前面的 AJAX 与 Promise 都是为其做铺垫。因为它在前端开发中的使用率可不是一般的多~ 下面开始学习~

6.1 axios 简介及安装

axios 是一个实现前端 AJAX 请求的软件包,是对原生 XHR 的二次封装。那原生的 XHR 用得好好的,为什么还要来一个 axios 呢?那得先从 AJAX 的异步说起。AJAX 是一个异步操作,因为把请求发给服务器,服务器处理数据已经返回数据这些操作都需要时间。为了让我们在合适的时间里获取服务器返回的数据,XHR 的实现方法是使用 onreadystatechange 回调函数。但是看着非常不美观,结构也很松散。那怎么让这个异步操作执行完之后再来执行其他操作呢?肯定是要用到上文所述的 Promise 了。但在 XHR 出现的时代,Promise 是啥都不知道。所以当 ES6 Promise 出现之后,axios 的作者就把 Promise 应用在了 AJAX 领域,写出了 axios,造就了前端行业不朽的传奇。

通过上文的叙述,大家应该也知道了 axios 是一个什么东西。由于 axios 是做 AJAX 请求的库,所以服务器要先跑起来。确保 express 安装的情况下使用 Node.js 运行下面代码:

// 保存在一个文件里然后使用 node 文件名运行
// 一个最迷你的 express get 服务器,应该都看得懂什么意思了吧
let express = require('express');
let app = express();

app.get('/server', (request, response) => {
  response.send("Hello World!");
});

app.listen(6888, () => console.log("服务器已开始监听 6888 端口") );

6.2 axios 的使用

上面也说过了 axios 使用了 Promise 技术,那 axios 不会就是一个返回 Promise 对象的函数吧?没错,就是这样滴~ 先新建一个 index.html,再把 script.js 文件引进来,script.js 写如下代码:

axios({ // axios 方法有一个参数,参数是一个对象,对象里面写请求的信息
    method: "get", // method 指定请求的类型
    url: "http://127.0.0.1:6888/server" // url 指定请求的链接,这里指刚刚开的服务器
}).then( response => { // 把信息请求到了就 resolve,response 接住响应的数据
    console.log("response 收到的数据是:"+response.data); // response.data 获取响应的内容
})

使用 axios 做请求的过程就结束了,简不简单~ 但是目前还有一个问题:axios 不是第三方库吗?不引进来怎么办啊?在实际开发中我们肯定会使用 npm 引进来,但是由于我们在这里使用 npm 会产生一些问题,所以这里采用 CDN 的方式引入 axios。在 index.html 里加入如下代码把 axios 引进来:

<script src="https://cdn.staticfile.org/axios/1.1.2/axios.min.js" type="text/javascript"></script>

用 Live Server 打开 index.html,可以很明显看到请求已经成功了(需要你开启跨域插件):

axios 不仅可以 get,它还可以 post put delete options 与 head,只需要改 method 里面的值,只不过后面几种不常用所以我们这里只演示一下 post。使用 axios 发送 POST 请求与 get 差不多,但是 axios 给了我们一个小功能:我们不需要自己手动去写 querystring,使用 axios 参数中的 params 参数即可快速传参,具体看注释。当然首先先把 POST 服务器开起来。把服务器代码更改成这样然后停止服务器再重新开起来:

let express = require('express');
let app = express();

app.post('/server', (request, response) => {
  response.send(`${request.query.username},欢迎您!`);
});

app.listen(6888, () => console.log("服务器已开始监听 6888 端口") );

然后接着更改 axios 的内容。把 Script.js 更改成这样:

axios({
    method: "post",
    url: "http://127.0.0.1:6888/server",
    params: { // params 里为一个对象,里面写需要传的参数
        username: "小明",
        password: "12345678"
    }
}).then( response => {
    console.log("response 收到的数据是:"+response.data); 
})

重新运行 index.html,你可以看到数据成功发回~ 

如果你就是懒,不想写 method,你也可以使用类似 axios.get 这种方式来请求数据,这也是一个能返回 Promise 的函数。它的参数可以只接一个字符串,即 url,也可以接一个对象,里面写参数。axios 已经把诸如 axios.get axios.post 等等的各种方法都封装起来了,就是怕程序员的手太累~

// 这里只能使用 querystring 形式,要不然会出现一些问题
axios.post("http://127.0.0.1:6888/server?username=小红&password=34567890")
.then( response => console.log(response.data) );

效果如下:

 除此之外如果你想实现多个 axios 同时请求怎么办呢?很多人就会脱口而出:Promise.all 啊!但是我们这里用的不是 Promise.all,而是经过 axios 封装过的 axios.all,语法与 Promise.all 完全一样。

// axios.all 传入一个数据,里面是需要同时执行的 ajax 请求
// 下面是一行代码,有点长所以把它分成了三行
axios.all([axios.post("http://127.0.0.1:6888/server?username=小红&password=34567890"),
axios.post("http://127.0.0.1:6888/server?username=小明&password=12345678")]) 
.then(responses => console.log(responses[0].data,responses[1].data));
// responses 收集到了所有请求的返回结果,与 Promise 一样一样的

 结果很喜人:

感觉学完 axios 之后才发现:原来这才是 AJAX 请求的正确打开方式~ axios 正式完结撒花(这应该是有史以来最短的小节了吧~)

7. 小结

(不知不觉又肝了一篇两万字长文)

本文的知识点还是很多的,这里简单地做一个总结:首先我们学习了 XML JSON 与 HTTP,即数据传输的两个部分。然后就是 AJAX,这是前端网页在不刷新页面之下请求数据的必备方法。然后我们学习了 Promise 与 axios,了解了有序异步编程与前端异步请求两个知识点(其实后面的才是重点,前面是给后面做铺垫的~)总的来说这些都属于前端向后端请求数据的知识~

恭喜大家学会了这么多知识点,也恭喜大家离前端框架更进了一步~ 下一篇就是大结局,即最难也最重要的前端框架:React 了,敬请期待!

猜你喜欢

转载自blog.csdn.net/raspi_fans/article/details/127142046