1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-24 04:22:20 +00:00
wizardforcel 6d30957199 18.
2018-05-13 22:52:52 +08:00

204 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 十八、HTTP 和表单
> 通信在实质上必须是无状态的,从客户端到服务器的每个请求都必须包含理解请求所需的所有信息,并且不能利用服务器上存储的任何上下文。
>
> Roy Fielding《Architectural Styles and the Design of Network-based Software Architectures》
我们曾在第 13 章中提到过超文本传输协议HTTP万维网中通过该协议进行数据请求和传输。在本章中会对该协议进行详细介绍并解释浏览器中JavaScript访问HTTP的方式。
### 协议
当你在浏览器地址栏中输入`eloquentjavascript.net/18_http.html`时,浏览器会首先找到和`eloquentjavascript.net`相关的服务器的地址,然后尝试通过 80 端口建立 TCP 连接,其中 80 端口是 HTTP 的默认通信端口。如果该服务器存在并且接受了该连接,浏览器可能发送如下内容。
```http
GET /18_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name
```
然后服务器会通过同一个链接返回如下内容。
```http
HTTP/1.1 200 OK
Content-Length: 65585
Content-Type: text/html
Last-Modified: Mon, 08 Jan 2018 10:29:45 GMT
```
浏览器会选取空行之后的响应部分,也就是正文(不要与 HTML `<body>`标签混淆),并将其显示为 HTML 文档。
由客户端发出的信息叫作请求。请求的第一行如下。
```http
GET /17_http.html HTTP/1.1
```
请求中的第一个单词是请求方法。GET表示我们希望得到一个我们指定的资源。其他常用方式还有DELETE用于删除一个资源PUT用于替换资源POST用于发送消息。需要注意的是服务器并不需要处理所有收到的请求。如果你随机访问一个网站并请求删除主页服务器很有可能会拒绝你的请求。
方法名后的请求部分是所请求的资源的路径。在最简单的情况下,一个资源只是服务器中的一个文件。不过,协议并没有要求资源一定是实际文件。一个资源可以是任何可以像文件一样传输的东西。很多服务器会实时地生成这些资源。例如,如果你打开`github.com/marijnh`,服务器会在数据库中寻找名为`marijnjh`的用户,如果找到了则会为该用户的生成介绍页面。
请求的第一行中位于资源路径后面的HTTP/1.1用来表明所使用的HTTP协议的版本。
在实践中,许多网站使用 HTTP v2它支持与版本 1.1 相同的概念,但是要复杂得多,因此速度更快。 浏览器在与给定服务器通信时,会自动切换到适当的协议版本,并且无论使用哪个版本,请求的结果都是相同的。 由于 1.1 版更直接,更易于使用,因此我们将专注于此。
服务器的响应也是以版本号开始的。版本号后面是应答状态,首先是一个三位的状态码,然后是一个可读的字符串。
```http
HTTP/1.1 200 OK
```
以2开头的状态码表示请求成功。以4开头的状态码表示请求中有错误。404是最著名的HTTP状态码了表示找不到资源。以5开头的状态码表示服务器端出现了问题而请求没有问题。
请求或响应的第一行后可能会有任意个协议头多个形如“namevalue”的行表明了和请求或响应相关的更多信息。这些是示例响应中的头信息。
```http
Content-Length: 65585
Content-Type: text/html
Last-Modified: Thu, 04 Jan 2018 14:05:30 GMT
```
这些信息说明了响应文档的大小和类型。在这个例子中响应是一个65585字节的HTML文档同时也说明了该文档最后的更改时间。
多数大多数协议头客户端或服务器可以自由决定需要在请求或响应中包含的协议头不过也有一些协议头是必需的。例如指明主机名的Host头在请求中是必须的因为一个服务器可能在一个IP地址下有多个主机名服务如果没有Host头服务器则无法判断客户端尝试请求哪个主机。
请求和应答可能都会在协议头后包含一个空行后面则是消息体包含所发送的数据。GET和DELETE请求不单独发送任何数据但PUT和POST请求则会。同样地一些响应类型如错误应答不需要有消息体。
### 浏览器和 HTTP
正如上例所示当我们在浏览器地址栏输入一个URL后浏览器会发送一个请求。当HTML页面中包含有其他的文件例如图片和JavaScript文件时浏览器也会一并获取这些资源。
一个较为复杂的网站通常都会有10到200个不等的资源。为了可以很快地取得这些资源浏览器会同时发送多个`GET`请求而不是一次等待一个请求。此类文档都是通过GET方法来获取的。
HTML页面可能包含表单用户可以在表单中填入一些信息然后由浏览器将其发送到服务器。如下是一个表单的例子。
```html
<form method="GET" action="example/message.html">
<p>Name: <input type="text" name="name"></p>
<p>Message:<br><textarea name="message"></textarea></p>
<p><button type="submit">Send</button></p>
</form>
```
这段代码描述了一个有两个输入域的表单:较小的输入域要求用户输入姓名,较大的要求用户输入一条消息。当点击发送按钮时,表单就提交了,这意味着其字段的内容被打包到 HTTP 请求中,并且浏览器跳转到该请求的结果。
`<form>`元素的`method`属性是`GET`(或省略)时,表单中的信息将作为查询字符串添加到`action` URL 的末尾。 浏览器可能会向此 URL 发出请求:
```http
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1
```
问号表示路径的末尾和查询字符串的起始。后面是多个名称和值,这些名称和值分别对应`form`输入字段中的`name`属性和这些元素的内容。`&`字符用来分隔不同的名称对。
在这个 URL 中经过编码的消息实际原本是“Yes只不过浏览器用奇怪的代码替换了问号。我们必须替换掉请求字符串中的一些字符。使用%3F替换的问号就是其中之一。这样看似乎有一个不成文的规定每种格式都会有自己的转义字符。这里的编码格式叫作URL编码使用一个百分号和16进制的数字来对字符进行编码。在这个例子中3F十进制为63是问号字符的编码。JavaScript提供了encodeURIComponent和decodeURIComponent函数来按照这种格式进行编码和解码。
```js
console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?
```
如果我们将本例HTML表单中的method属性更改为POST则浏览器会使用POST方法发送该表单并将请求字符串放到请求体中而不是添加到URL中。
```http
POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded
name=Jean&message=Yes%3F
```
`GET`请求应该用于没有副作用的请求,而仅仅是询问信息。 可以改变服务器上的某些内容的请求,例如创建一个新帐户或发布消息,应该用其他方法表示,例如`POST`。 诸如浏览器之类的客户端软件,知道它不应该盲目地发出`POST`请求,但通常会隐式地发出`GET`请求 - 例如预先获取一个它认为用户很快需要的资源。
我们将在本章后面的回到表单,以及如何与 JavaScript 交互。
### Fetch
浏览器 JavaScript 可以通过`Fetch`接口生成 HTTP 请求。 由于它比较新,所以它很方便地使用了`Promise`(这在浏览器接口中很少见)。
```js
fetch("example/data.txt").then(response => {
console.log(response.status);
// → 200
console.log(response.headers.get("Content-Type"));
// → text/plain
});
```
调用`fetch`返回一个`Promise`,它解析为一个`Response`对象,该对象包含服务器响应的信息,例如状态码和协议头。 协议头被封装在类`Map`的对象中,该对象不区分键(协议头名称)的大小写,因为协议头名称不应区分大小写。 这意味着`header.get("Content-Type")``headers.get("content-TYPE")`将返回相同的值。
请注意,即使服务器使用错误代码进行响应,由`fetch`返回的`Promise`也会成功解析。 如果存在网络错误或找不到请求的服务器,它也可能被拒绝。
`fetch`的第一个参数是请求的 URL。 当该 URL 不以协议名称(例如`http:`)开头时,它被视为相对路径,这意味着它解释为相对于当前文档的路径。 当它以斜线(`/`)开始时,它将替换当前路径,即服务器名称后面的部分。 否则,当前路径直到并包括最后一个斜杠的部分,放在相对 URL 前面。
为了获取响应的实际内容,可以使用其`text`方法。 由于初始`Promise`在收到响应头文件后立即解析,并且读取响应正文可能需要一段时间,这又会返回一个`Promise`
```js
fetch("example/data.txt")
.then(resp => resp.text())
.then(text => console.log(text));
// → This is the content of data.txt
```
有一种类似的方法,名为`json`,它返回一个`Promise`,它将解析为,将正文解析为 JSON 时得到的值,或者不是有效的 JSON则被拒绝。
默认情况下,`fetch`使用`GET`方法发出请求,并且不包含请求正文。 你可以通过传递一个带有额外选项的对象作为第二个参数,来进行不同的配置。 例如,这个请求试图删除`example/data.txt`
```js
fetch("example/data.txt", {method: "DELETE"}).then(resp => {
console.log(resp.status);
// → 405
});
```
405 状态码意味着“方法不允许”,这是 HTTP 服务器说“我不能这样做”的方式。
为了添加一个请求正文,你可以包含`body`选项。 为了设置标题,存在`headers`选项。 例如,这个请求包含`Range`协议,它指示服务器只返回一部分响应。
```js
fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
.then(resp => resp.text())
.then(console.log);
// → the content
```
览器将自动添加一些请求头,例如`"Host"`和服务器需要的协议头,来确定正文的大小。 但是对于包含认证信息或告诉服务器想要接收的文件格式,添加自己的协议头通常很有用。
### HTTP 沙箱
在网页脚本中发出 HTTP 请求,再次引发了安全性的担忧。 控制脚本的人的兴趣可能不同于正在运行的计算机的所有者。 更具体地说,如果我访问`themafia.org`,我不希望其脚本能够使用来自我的浏览器的身份向`mybank.com`发出请求,并且下令将我所有的钱转移到某个随机帐户。
出于这个原因,浏览器通过禁止脚本向其他域(如`themafia.org``mybank.com`等名称)发送 HTTP 请求来保护我们。
在构建希望因合法原因访问多个域的系统时,这可能是一个恼人的问题。 幸运的是,服务器可以在响应中包含这样的协议头,来明确地向浏览器表明,请求可以来自另一个域:
```http
Access-Control-Allow-Origin: *
```
### 运用 HTTP
当构建一个需要让浏览器客户端的JavaScript程序和服务器端的程序进行通信的系统时有一些不同的方式可以实现这个功能。
一个常用的方法是远程过程调用,通信遵从正常的方法调用方式,不过调用的方法实际运行在另一台机器中。调用包括向服务器发送包含方法名和参数的请求。响应的结果则包括函数的返回值。
当考虑远程过程调用时HTTP只是通信的载体并且你很可能会写一个抽象层来隐藏细节。
另一个方法是使用一些资源和HTTP方法来建立自己的通信。不同于远程调用方法addUser你需要发送一个PUT请求到users/larry不同于将用户属性进行编码后作为参数传递你定义了一个 JSON 文档格式或使用一种已有的格式来展示一个用户。PUT请求的正文则只是这样的一个用来建立新资源的文档。由GET方法获取的资源则是自愿的URL例如/users/larry该URL返回代表这个资源的文档。
第二种方法使用了HTTP的一些特性所以使得整体更简洁。例如对于资源缓存的支持在客户端存一份副本用于快速访问。HTTP中使用的概念设计良好可以提供一组有用的原则来设计服务器接口。
### 安全和 HTTPS
通过互联网传播的数据,往往走过漫长而危险的道路。 为了到达目的地,它必须跳过任何东西,从咖啡店的 Wi-Fi 到由各个公司和国家管理的网络。 在它的路线上的任何位置,它都可能被探测或者甚至被修改。
如果对某件事保密是重要的,例如您的电子邮件帐户的密码,或者它到达目的地而未经修改是重要的,例如帐户号码,您使用它在银行网站上转账,纯 HTTP 就不够好了。
安全的 HTTP 协议,其 URL 以`https://`开头是一种难以阅读和篡改的HTTP 流量的封装方式。 在交换数据之前,客户端证实该服务器是它所声称的东西,通过要求它证明,它具有由浏览器承认的证书机构所颁发的证书。 接下来,通过连接传输的所有数据,都将以某种方式加密,它应该防止窃听和篡改。
因此,当 HTTPS 正常工作时,它可以阻止某人冒充您想要与之通话的网站,以及某人窥探您的通信。 这并不完美,由于伪造或被盗的证书和损坏的软件,存在各种 HTTPS 失败的事故,但它比纯 HTTP 更安全。