diff --git a/18.md b/18.md new file mode 100644 index 0000000..c38e834 --- /dev/null +++ b/18.md @@ -0,0 +1,203 @@ +## 十八、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 + + +... the rest of the document +``` + +浏览器会选取空行之后的响应部分,也就是正文(不要与 HTML ``标签混淆),并将其显示为 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开头的状态码表示服务器端出现了问题,而请求没有问题。 + +请求或响应的第一行后可能会有任意个协议头,多个形如“name:value”的行表明了和请求或响应相关的更多信息。这些是示例响应中的头信息。 + +```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 +
+

Name:

+

Message:

+

+
+``` + +这段代码描述了一个有两个输入域的表单:较小的输入域要求用户输入姓名,较大的要求用户输入一条消息。当点击发送按钮时,表单就提交了,这意味着其字段的内容被打包到 HTTP 请求中,并且浏览器跳转到该请求的结果。 + +当`
`元素的`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 更安全。