JavaScript 编程精解(Eloquent)第四版(四)

译者:飞龙

协议:CC BY-NC-SA 4.0

第十九章:HTTP与表单

超文本传输协议(HyperText Transfer Protocol),在第十三章中介绍,是在万维网上请求和提供数据的机制。本章更详细地描述了该协议,并解释了浏览器JavaScript如何访问它。

协议

如果你在浏览器的地址栏中输入[eloquentjavascript.net/18_http.xhtml](http://eloquentjavascript.net/18_http.xhtml),浏览器首先查找与eloquent [javascript.net](http://javascript.net)关联的服务器地址,并尝试在端口80HTTP流量的默认端口)上打开一个TCP连接。如果服务器存在并接受连接,浏览器可能会发送类似这样的内容:

GET /18_http.xhtml HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

然后服务器通过相同的连接进行响应。

HTTP/1.1 200 OK
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

<!doctype html>
--snip--

浏览器获取空行后响应的部分,即它的主体(不要与HTML 标签混淆),并将其显示为HTML文档。

客户端发送的信息称为请求。它以这一行开始:

GET /18_http.xhtml HTTP/1.1

第一个词是请求的方法GET意味着我们想要获取指定的资源。其他常见的方法有DELETE用于删除资源,PUT用于创建或替换资源,POST用于向其发送信息。请注意,服务器并不一定有义务执行它收到的每一个请求。如果你走到一个随机网站并告诉它删除其主页,它可能会拒绝。

方法名称之后的部分是请求应用于的资源的路径。在最简单的情况下,资源只是服务器上的一个文件,但协议并不要求它必须是。资源可以是任何可以被转移好像它是一个文件的东西。许多服务器生成的响应是动态生成的。例如,如果你打开[github.com/marijnh](https://github.com/marijnh),服务器会在其数据库中查找名为marijnh的用户,如果找到了,它会为该用户生成一个个人资料页面。

在资源路径之后,请求的第一行提到HTTP/1.1,以指示它使用的HTTP协议的版本。

实际上,许多网站使用HTTP版本2,它支持与版本1.1相同的概念,但更加复杂,因此可以更快。浏览器在与特定服务器交互时会自动切换到适当的协议版本,无论使用哪个版本,请求的结果都是相同的。由于版本1.1更加简单且更易于操作,我们将使用它来说明该协议。

服务器的响应也会以一个版本开头,后面跟着响应的状态,首先是一个三位数的状态代码,然后是一个可读的字符串。

HTTP/1.1 200 OK

2开头的状态码表示请求成功。以4开头的代码意味着请求存在问题。最著名的HTTP状态码可能是404,表示找不到该资源。以5开头的代码表示服务器发生了错误,请求没有错。

请求或响应的第一行后面可以跟随任意数量的头部。这些是形如name: value的行,指定有关请求或响应的额外信息。这些头部是示例响应的一部分:

Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

这告诉我们响应文档的大小和类型。在这种情况下,它是一个87,320字节的HTML文档。它还告诉我们该文档最后一次修改的时间。

客户端和服务器可以自由决定在请求或响应中包含哪些头部。但其中一些对于正常工作是必要的。例如,如果响应中没有Content-Type头部,浏览器将不知道如何显示该文档。

在头部之后,请求和响应都可能包含一个空行,后面跟着一个主体,其中包含实际发送的文档。GETDELETE请求不发送任何数据,但PUTPOST请求会发送。一些响应类型,如错误响应,也不需要主体。

浏览器和HTTP

正如我们所见,当我们在地址栏中输入URL时,浏览器会发起请求。当生成的HTML页面引用其他文件,例如图像和JavaScript文件时,它也会检索这些文件。

一个中等复杂的网站可以轻松包含10200个资源。为了能够快速获取这些资源,浏览器会同时发起多个GET请求,而不是一个个等待响应。

HTML页面可以包含表单,允许用户填写信息并将其发送到服务器。这是一个表单的示例:

<form method="GET" action="example/message.xhtml">
  <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请求,浏览器会导航到该请求的结果。

元素的method属性为GET(或被省略)时,表单中的信息将作为查询字符串添加到动作URL的末尾。浏览器可能会向这个URL发起请求:

GET /example/message.xhtml?name=Jean&message=Yes%3F HTTP/1.1

问号表示URL路径部分的结束和查询的开始。它后面跟随名称和值的对,分别对应于表单字段元素的name属性和这些元素的内容。一个&符号用于分隔这些对。

URL中编码的实际消息是“Yes?”,但问号被一个奇怪的代码替换。一些查询字符串中的字符必须被转义。问号(以%3F表示)就是其中之一。似乎有一种不成文的规则,即每种格式都需要自己的一种字符转义方式。这种称为URL编码的方式使用一个百分号,后跟两个十六进制(基数为16)数字来编码字符代码。在这种情况下,3F在十进制中是63,是问号字符的代码。JavaScript提供了encodeURIComponentdecodeURIComponent函数来编码和解码这种格式。

console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?

如果我们将之前看到的HTML表单的method属性更改为POST,提交表单时发出的HTTP请求将使用POST方法,并将查询字符串放入请求的主体中,而不是将其添加到URL中。

POST /example/message.xhtml 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可以进行HTTP请求的接口称为fetch

fetch("example/data.txt").then(response => {
  console.log(response.status);
  // → 200
 console.log(response.headers.get("Content-Type"));
  // → text/plain
});

调用fetch返回一个Promise,该Promise解析为一个Response对象,包含有关服务器响应的信息,例如其状态码和响应头。响应头被封装在一个类似Map的对象中,该对象将其键(头部名称)视为不区分大小写,因为头部名称不应区分大小写。这意味着headers.get("Content-Type")headers.get("content-TYPE")将返回相同的值。

请注意,即使服务器返回了错误代码,fetch返回的Promise仍然会成功解决。如果发生网络错误或请求所针对的服务器无法找到,Promise也可能被拒绝。

fetch的第一个参数是应该请求的URL。当该URL不以协议名称(例如http:)开头时,它被视为相对的,这意味着它相对于当前文档进行解释。当它以斜杠(/)开头时,它将替换当前路径,即服务器名称之后的部分。当它没有斜杠时,当前路径中直到最后一个斜杠的部分将放在相对URL之前。

要获取响应的实际内容,您可以使用其text方法。因为初始Promise一旦接收到响应的头部就会解决,而读取响应主体可能需要更长的时间,所以这再次返回一个Promise

fetch("example/data.txt")
  .then(resp => resp.text())
  .then(text => console.log(text));
// → This is the content of data.txt

一种类似的方法,称为json,返回一个解析为JSON时的值的promise,如果不是有效的JSON,则会拒绝。

默认情况下,fetch使用GET方法发出请求,并且不包含请求主体。你可以通过将带有额外选项的对象作为第二个参数传递来配置它。例如,这个请求试图删除example/data.txt

fetch("example/data.txt", {method: "DELETE"}).then(resp => {
  console.log(resp.status);
  // → 405
});

405状态码表示“方法不被允许”,这是HTTP服务器表明“抱歉,我无法做到”的方式。

要为PUTPOST请求添加请求主体,可以包含一个body选项。要设置头部,可以使用headers选项。例如,这个请求包含一个Range头部,指示服务器仅返回文档的一部分。

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.orgmybank.com)发出HTTP请求来保护我们。

当构建希望出于合法理由访问多个域的系统时,这可能是一个烦人的问题。幸运的是,服务器可以在其响应中包含这样的头部,以明确向浏览器指示请求可以来自另一个域:

Access-Control-Allow-Origin: *

理解HTTP

当构建一个需要在浏览器中运行的JavaScript程序(客户端)与服务器上的程序(服务器端)之间进行通信的系统时,有几种不同的方式来建模这种通信。

一种常用的模型是远程过程调用。在这个模型中,通信遵循正常函数调用的模式,只不过函数实际上是在另一台机器上运行。调用它涉及向服务器发出请求,包括函数的名称和参数。对此请求的响应包含返回的值。

在考虑远程过程调用时,HTTP只是一个通信的载体,你很可能会编写一个完全隐藏它的抽象层。

另一种方法是围绕资源和HTTP方法的概念构建你的通信。你使用PUT请求而不是远程过程addUser,针对/users/larry。你不再在函数参数中编码用户的属性,而是定义一个JSON文档格式(或使用现有格式)来表示用户。然后,用于创建新资源的PUT请求的主体就是这样的文档。通过向资源的URL(例如,/users/larry)发出GET请求来获取资源,这又会返回表示该资源的文档。

这种第二种方法使得使用HTTP提供的一些特性变得更加容易,比如支持资源缓存(在客户端保留资源副本以便快速访问)。HTTP中的概念设计良好,可以为设计你的服务器接口提供一套有用的原则。

安全性和HTTPS

在互联网上传输的数据往往要经历一条漫长而危险的道路。为了到达目的地,它必须穿越从咖啡店Wi-Fi热点到各种公司和国家控制的网络。在其路线的任何点,它都可能被检查甚至被修改。

如果某些东西保持秘密很重要,比如你的电子邮件账户密码,或者它需要在传输到目的地时不被修改,比如你通过银行网站转账的账户号码,那么普通HTTP就不够安全。

安全HTTP协议用于以https://开头的URL,以一种更难以阅读和篡改的方式封装HTTP流量。在交换数据之前,客户端通过要求服务器证明其拥有浏览器识别的证书颁发机构颁发的加密证书,来验证服务器的身份。接下来,通过连接传输的所有数据都以一种应该能防止窃听和篡改的方式进行加密。

因此,当它正常工作时,HTTPS可以防止其他人冒充你想要交流的网站并且监视你的通信。它并不完美,也发生过由于伪造或盗用证书和软件故障导致HTTPS失败的各种事件,但它比普通HTTP安全得多

表单字段

表单最初是为预JavaScript网页设计的,目的是允许网站通过HTTP请求发送用户提交的信息。这个设计假设与服务器的交互总是通过导航到新页面来进行。

然而,表单元素是DOM的一部分,就像页面的其余部分一样,表示表单字段的DOM元素支持许多其他元素所不具备的属性和事件。这使得使用JavaScript程序检查和控制这些输入字段成为可能,并且可以进行诸如向表单添加新功能或在JavaScript应用程序中将表单和字段作为构建块等操作。

一个网页表单由任意数量的输入字段组成,这些字段被分组在一个标签中。HTML允许多种不同样式的字段,从简单的开关复选框到下拉菜单和文本输入字段。本书不会试图全面讨论所有字段类型,但我们将首先提供一个粗略的概述。

许多字段类型使用标签。该标签的type属性用于选择字段的样式。以下是一些常用的类型:

text 单行文本字段
password 与文本相同,但隐藏输入的文本
checkbox 开/关切换开关
color 一种颜色
date 日历日期
radio (部分) 多选字段
file 允许用户从他们的计算机中选择一个文件

表单字段不一定要出现在标签中。你可以将它们放置在页面的任何位置。这种无表单字段不能被提交(只有整个表单可以),但在使用JavaScript响应输入时,我们通常不希望正常提交我们的字段。

<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="color" value="orange"> (color)</p>
<p><input type="date" value="2023-10-13"> (date)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

使用此HTML代码创建的字段看起来是这样的:

https://github.com/OpenDocCN/geekdoc-js-zh/raw/master/docs/elqt-js-4e/img/f0304-01.jpg

这些元素的JavaScript接口根据元素的类型而有所不同。

多行文本字段有自己的标签结束标签,并使用这两个标签之间的文本作为起始文本,而不是值属性。

<textarea>
one
two
three
</textarea>

最后,菜单会尝试移动到包含用户输入文本的选项,并通过上下箭头键移动选择。

我们可以通过JavaScript使用focusblur方法控制焦点。第一个方法将焦点移动到调用的DOM元素,第二个方法则移除焦点。document.activeElement的值对应于当前获得焦点的元素。

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  // → INPUT
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);
  // → BODY
</script>

对于某些页面,用户期望立即与表单字段进行交互。JavaScript可以在文档加载时聚焦该字段,但HTML也提供了autofocus属性,该属性在让浏览器知道我们想要实现什么的同时产生相同的效果。这让浏览器在不适当的情况下有机会禁用这一行为,比如当用户将焦点放在其他地方时。

浏览器允许用户通过按TAB移动焦点到下一个可聚焦元素,按SHIFT-TAB返回到上一个元素。默认情况下,元素按照它们在文档中出现的顺序被访问。可以使用tabindex属性来改变这个顺序。以下示例文档将允许焦点从文本输入跳转到OK按钮,而不是先经过帮助链接:

<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

默认情况下,大多数类型的HTML元素不能被聚焦。你可以给任何元素添加tabindex属性使其可聚焦。tabindex0的元素可以被聚焦而不影响聚焦顺序。

禁用字段

所有表单字段都可以通过其disabled属性被禁用。这是一个可以不带值指定的属性——只要存在,元素就会被禁用。

<button>I'm all right</button>
<button disabled>I'm out</button>

被禁用的字段无法聚焦或更改,浏览器会使其看起来呈灰色和褪色。

https://github.com/OpenDocCN/geekdoc-js-zh/raw/master/docs/elqt-js-4e/img/f0306-01.jpg

当一个程序正在处理由某个按钮或其他控件引起的可能需要与服务器通信并因此耗时的操作时,禁用该控件直到操作完成是个好主意。这样,当用户不耐烦再次点击时,他们不会不小心重复他们的操作。

整个表单

当一个字段包含在元素中时,其DOM元素将有一个form属性链接回该表单的DOM元素。反过来,元素有一个名为elements的属性,包含了内部字段的类似数组的集合。

表单字段的name属性决定了在提交表单时其值的识别方式。它也可以在访问表单的elements属性时用作属性名称,该属性既可以作为类似数组的对象(通过数字访问),也可以作为映射(通过名称访问)。

<form action="example/submit.xhtml">
  Name: <input type="text" name="name"><br>
  Password: <input type="password" name="password"><br>
  <button type="submit">Log in</button>
</form>
<script>
  let form = document.querySelector("form");
  console.log(form.elements[1].type);
  // → password
  console.log(form.elements.password.type);
  // → password
  console.log(form.elements.name.form == form);
  // → true
</script>

当一个具有提交类型属性的按钮被按下时,会导致表单被提交。在表单字段聚焦时按下ENTER键也会产生相同的效果。

正常提交表单意味着浏览器导航到由表单的action属性指示的页面,使用GETPOST请求。但在这之前,会触发一个“submit”事件。你可以用JavaScript处理这个事件,并通过在事件对象上调用preventDefault来阻止这种默认行为。

<form>
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

在JavaScript中拦截“submit”事件有多种用途。我们可以编写代码来验证用户输入的值是否合理,并立即显示错误信息,而不是提交表单。或者,我们可以完全禁用常规的提交表单方式,如示例所示,让我们的程序处理输入,可能使用fetch将其发送到服务器而无需重新加载页面。

文本字段