HTTP 和 表单
人们对该设计难以理解的一点是,除了 URL、HTTP 和 HTML 之外,别无他物。并没有中央计算机“控制”着网络,也没有这些协议工作的单一网络,甚至没有一个“运行”网络的组织。网络不是存在于特定“地点”的物理“事物”。它是一个信息可以存在的“空间”。
超文本传输协议(HTTP)在第 13 章中介绍,是万维网上请求和提供数据的机制。本章将更详细地介绍该协议,并解释浏览器 JavaScript 如何访问它。
该协议
如果您在浏览器的地址栏中输入eloquentjavascript.net/18_http.html,浏览器首先会查找与eloquentjavascript.net关联的服务器地址,并尝试在端口 80 上与其建立 TCP 连接,端口 80 是 HTTP 流量的默认端口。如果服务器存在并接受连接,浏览器可能会发送如下内容
GET /18_http.html 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> ... the rest of the document
浏览器获取响应中空行后的部分,即其主体(不要与 HTML <body>
标签混淆),并将其显示为 HTML 文档。
GET /18_http.html HTTP/1.1
第一个词是请求的方法。GET
表示我们想要获取指定的资源。其他常见方法包括DELETE
用于删除资源,PUT
用于创建或替换资源,以及POST
用于向资源发送信息。请注意,服务器没有义务执行收到的每个请求。如果您走到一个随机的网站并告诉它DELETE
它的主页,它很可能会拒绝。
方法名后面的部分是请求所应用的资源的路径。在最简单的情况下,资源只是服务器上的一个文件,但协议不要求它必须是文件。资源可以是任何可以像文件一样传输的东西。许多服务器动态生成它们生成的响应。例如,如果您打开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
标头,浏览器将不知道如何显示文档。
在标头之后,请求和响应都可能包含一个空行,后面跟着一个主体,其中包含正在发送的实际文档。GET
和DELETE
请求不会发送任何数据,但PUT
和POST
请求会。某些响应类型,例如错误响应,也不需要主体。
浏览器和 HTTP
正如我们所见,当我们在浏览器的地址栏中输入 URL 时,浏览器会发出请求。当生成的 HTML 页面引用其他文件(如图像和 JavaScript 文件)时,它也会检索这些文件。
一个中等复杂的网站很容易包含 10 到 200 个资源。为了能够快速获取这些资源,浏览器会同时发出多个GET
请求,而不是一次等待一个响应。
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 的请求
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1
问号表示 URL 的路径部分的结束和查询的开始。后面跟着名称和值的对,分别对应表单字段元素的name
属性和这些元素的内容。使用一个“&”字符(&
)来分隔这些对。
URL 中编码的实际消息是“Yes?”,但问号被一个奇怪的代码替换了。查询字符串中的一些字符必须转义。问号,表示为%3F
,就是其中之一。似乎有一个不成文的规定,每个格式都需要它自己的转义字符方式。这种方式,称为URL 编码,使用百分号后跟两个十六进制(16 进制)数字来编码字符代码。在本例中,3F(在十进制表示法中为 63)是问号字符的代码。JavaScript 提供了encodeURIComponent
和decodeURIComponent
函数来对这种格式进行编码和解码。
console.log(encodeURIComponent("Yes?")); // → Yes%3F console.log(decodeURIComponent("Yes%3F")); // → Yes?
如果我们将前面示例中 HTML 表单的method
属性更改为POST
,提交表单时发出的 HTTP 请求将使用POST
方法,并将查询字符串放在请求主体中,而不是将其添加到 URL 中。
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 可以用来发出 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.
和headers.
将返回相同的值。
请注意,即使服务器返回了错误代码,fetch
返回的 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
,返回一个 promise,该 promise 解析为您在将主体解析为 JSON 时获得的值,或者如果它不是有效的 JSON,则拒绝。
默认情况下,fetch
使用 GET
方法进行请求,并且不包含请求主体。您可以通过将包含额外选项的对象作为第二个参数传递来对其进行不同的配置。例如,此请求尝试删除 example/data.txt
fetch("example/data.txt", {method: "DELETE"}).then(resp => { console.log(resp.status); // → 405 });
405 状态代码表示“方法不允许”,这是 HTTP 服务器用来表示“我恐怕无法做到”的方式。
要为 PUT
或 POST
请求添加请求主体,您可以包含 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.org和mybank.com之类的名称)发出 HTTP 请求来保护我们。
在构建想要访问多个域以实现合法目的的系统时,这可能是一个令人烦恼的问题。幸运的是,服务器可以在其对浏览器的响应中包含这样的标头,以明确地指示浏览器请求可以来自另一个域,这是可以接受的。
Access-Control-Allow-Origin: *
欣赏 HTTP
在构建需要在浏览器中运行的 JavaScript 程序(客户端)和服务器上的程序(服务器端)之间进行通信的系统时,有多种不同的方式来模拟这种通信。
一种常用的模型是远程过程调用。在此模型中,通信遵循普通函数调用的模式,只是该函数实际上在另一台机器上运行。调用它涉及向服务器发出请求,该请求包含函数的名称和参数。对该请求的响应包含返回的值。
当以远程过程调用的角度思考时,HTTP 只是通信的载体,您很可能会编写一个完全隐藏它的抽象层。
另一种方法是围绕资源和 HTTP 方法的概念构建您的通信。与其使用名为addUser
的远程过程调用,不如使用PUT
请求到/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 应用程序中使用表单和字段作为构建块等操作。
Web 表单包含任意数量的输入字段,这些字段分组在<form>
标签中。HTML 允许多种不同样式的字段,从简单的开/关复选框到下拉菜单和用于文本输入的字段。本书不会试图全面讨论所有字段类型,但我们会从粗略概述开始。
许多字段类型使用<input>
标签。该标签的type
属性用于选择字段的样式。以下是一些常用的<input>
类型
text | 单行文本字段 |
password | 与text 相同,但隐藏了输入的文本 |
checkbox | 开/关开关 |
color | 颜色 |
date | 日历日期 |
radio | (部分)多选字段 |
file | 允许用户从计算机中选择文件 |
表单字段不一定要出现在<form>
标签中。您可以将它们放在页面的任何位置。这些无表单的字段无法提交(只有表单作为一个整体才能提交),但是当使用 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>
多行文本字段有自己的标签<textarea>
,主要是因为使用属性来指定多行起始值会很麻烦。<textarea>
标签需要一个匹配的</
闭合标签,它使用这两个标签之间的文本作为起始文本,而不是value
属性。
<textarea> one two three </textarea>
最后,<select>
标签用于创建一个字段,允许用户从多个预定义选项中进行选择。
<select> <option>Pancakes</option> <option>Pudding</option> <option>Ice cream</option> </select>
每当表单字段的值发生变化时,它都会触发一个"change"
事件。
焦点
与 HTML 文档中的大多数元素不同,表单字段可以获得键盘焦点。当单击、使用tab键移动到或以其他方式激活它们时,它们将成为当前活动元素并接收键盘输入。
因此,只有当文本字段获得焦点时,您才能在其中键入。其他字段对键盘事件的响应方式不同。例如,<select>
菜单尝试移动到包含用户键入文本的选项,并通过向上和向下移动选择来响应箭头键。
我们可以使用focus
和blur
方法从 JavaScript 控制焦点。第一个方法将焦点移动到调用它的 DOM 元素,第二个方法将焦点移除。document.
中的值对应于当前获得焦点的元素。
<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
属性更改此顺序。以下示例文档将使焦点从文本输入跳到“确定”按钮,而不是首先经过帮助链接
<input type="text" tabindex=1> <a href=".">(help)</a> <button onclick="console.log('ok')" tabindex=2>OK</button>
默认情况下,大多数类型的 HTML 元素无法获得焦点。您可以向任何元素添加tabindex
属性以使其可获得焦点。tabindex
为 0 使元素可获得焦点,而不会影响焦点顺序。
禁用字段
所有表单字段都可以通过它们的 `disabled` 属性设置为禁用状态。这是一个可以不带值指定的属性 - 只要它存在,就会禁用该元素。
<button>I'm all right</button> <button disabled>I'm out</button>
禁用的字段无法获得焦点或更改,浏览器会使它们看起来灰暗且褪色。
当程序正在处理某个按钮或其他控件触发的操作时,该操作可能需要与服务器通信,因此需要一段时间,此时禁用该控件直到操作完成是一个好主意。这样,当用户变得不耐烦并再次点击它时,他们不会意外地重复他们的操作。
整个表单
当一个字段包含在 `<form>` 元素中时,它的 DOM 元素将有一个 `form` 属性,指向该表单的 DOM 元素。反过来,`<form>` 元素有一个名为 `elements` 的属性,它包含一个类似数组的集合,其中包含表单中的所有字段。
表单字段的 `name` 属性决定了表单提交时它的值将如何被识别。它也可以用作属性名,用于访问表单的 `elements` 属性,该属性既可以像数组一样访问(通过数字访问),也可以像映射一样访问(通过名称访问)。
<form action="example/submit.html"> 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>
当按下 `type` 属性为 `submit` 的按钮时,表单将被提交。当表单字段获得焦点时,按下 enter 键具有相同的效果。
提交表单通常意味着浏览器会导航到表单 `action` 属性中指定的页面,使用 `GET` 或 `POST` 请求。但在此之前,会触发一个 `"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` 将其发送到服务器,而不会重新加载页面。
文本字段
由 `<textarea>` 标签创建的字段,或者 `<input>` 标签,其类型为 `text` 或 `password`,共享一个公共接口。它们的 DOM 元素有一个 `value` 属性,它以字符串形式保存它们当前的内容。将此属性设置为另一个字符串会更改字段的内容。
文本字段的 `selectionStart` 和 `selectionEnd` 属性为我们提供了有关文本中光标和选区的的信息。当没有任何内容被选中时,这两个属性包含相同的数字,表示光标的位置。例如,0 表示文本的开头,10 表示光标在第 10 个字符之后。当部分字段被选中时,这两个属性将不同,为我们提供所选文本的开始和结束位置。与 `value` 一样,这些属性也可以被写入。
想象一下,你正在写一篇关于卡塞克姆威的的文章,他是第二王朝的最后一位法老王,但在拼写他的名字时遇到了一些麻烦。下面的代码连接了一个 `<textarea>` 标签,并添加了一个事件处理程序,当你按下 F2 时,它会为你插入字符串“Khasekhemwy”。
<textarea></textarea> <script> let textarea = document.querySelector("textarea"); textarea.addEventListener("keydown", event => { if (event.key == "F2") { replaceSelection(textarea, "Khasekhemwy"); event.preventDefault(); } }); function replaceSelection(field, word) { let from = field.selectionStart, to = field.selectionEnd; field.value = field.value.slice(0, from) + word + field.value.slice(to); // Put the cursor after the word field.selectionStart = from + word.length; field.selectionEnd = from + word.length; } </script>
函数 `replaceSelection` 会用给定的单词替换文本字段中当前选中的部分内容,然后将光标移动到该单词之后,以便用户可以继续输入。
文本字段的 `"change"` 事件不会在每次输入时触发。相反,它会在字段失去焦点后,并且其内容已更改时触发。要立即响应文本字段中的更改,应该为 `"input"` 事件注册一个处理程序,它在用户输入字符、删除文本或以其他方式操作字段内容时触发。
下面的示例显示了一个文本字段和一个计数器,它显示了该字段中当前文本的长度
<input type="text"> length: <span id="length">0</span> <script> let text = document.querySelector("input"); let output = document.querySelector("#length"); text.addEventListener("input", () => { output.textContent = text.value.length; }); </script>
复选框和单选按钮
复选框字段是一个二进制开关。它的值可以通过它的 `checked` 属性提取或更改,该属性保存一个布尔值。
<label> <input type="checkbox" id="purple"> Make this page purple </label> <script> let checkbox = document.querySelector("#purple"); checkbox.addEventListener("change", () => { document.body.style.background = checkbox.checked ? "mediumpurple" : ""; }); </script>
`<label>` 标签将文档的一部分与输入字段关联起来。单击标签上的任何位置都会激活该字段,使该字段获得焦点,并切换其值(如果它是复选框或单选按钮)。
单选按钮类似于复选框,但它隐式地链接到其他具有相同 `name` 属性的单选按钮,因此一次只有一个单选按钮可以处于活动状态。
Color: <label> <input type="radio" name="color" value="orange"> Orange </label> <label> <input type="radio" name="color" value="lightgreen"> Green </label> <label> <input type="radio" name="color" value="lightblue"> Blue </label> <script> let buttons = document.querySelectorAll("[name=color]"); for (let button of Array.from(buttons)) { button.addEventListener("change", () => { document.body.style.background = button.value; }); } </script>
CSS 查询中传递给 `querySelectorAll` 的方括号用于匹配属性。它选择 `name` 属性为 `"color"` 的元素。
选择字段
选择字段在概念上类似于单选按钮 - 它们也允许用户从一组选项中进行选择。但单选按钮将选项的布局置于我们的控制之下,而 `<select>` 标签的外观由浏览器决定。
选择字段还有一种变体,更像是复选框列表,而不是单选框。当给定 `multiple` 属性时,`<select>` 标签将允许用户选择任意数量的选项,而不是仅选择单个选项。常规选择字段以下拉控件的形式绘制,该控件仅在打开时才会显示未激活的选项,而具有 `multiple` 属性的字段会同时显示多个选项,允许用户单独启用或禁用它们。
每个 `<option>` 标签都有一个值。此值可以通过 `value` 属性定义。如果没有给出 `value` 属性,则选项内部的文本将作为其值。`<select>` 元素的 `value` 属性反映当前选中的选项。然而,对于 `multiple` 字段,这个属性意义不大,因为它只会给出一个当前选中的选项的值。
`<select>` 字段的 `<option>` 标签可以通过字段的 `options` 属性以类似数组的形式访问。每个选项都有一个名为 `selected` 的属性,它表示该选项当前是否被选中。该属性也可以被写入以选择或取消选择一个选项。
这个示例从 `multiple` 选择字段中提取选中的值,并用它们从各个位组成一个二进制数。按住 ctrl(或 Mac 上的 command)键可以选择多个选项。
<select multiple> <option value="1">0001</option> <option value="2">0010</option> <option value="4">0100</option> <option value="8">1000</option> </select> = <span id="output">0</span> <script> let select = document.querySelector("select"); let output = document.querySelector("#output"); select.addEventListener("change", () => { let number = 0; for (let option of Array.from(select.options)) { if (option.selected) { number += Number(option.value); } } output.textContent = number; }); </script>
文件字段
文件字段最初被设计为一种通过表单上传用户计算机上文件的方式。在现代浏览器中,它们还提供了一种从 JavaScript 程序中读取此类文件的方式。该字段充当一种守门员。脚本不能简单地开始从用户的计算机读取私有文件,但如果用户在这样的字段中选择了一个文件,浏览器会将该操作解释为脚本可以读取该文件的意思。
文件字段通常看起来像一个按钮,上面标着“选择文件”或“浏览”,旁边是有关所选文件的信息。
<input type="file"> <script> let input = document.querySelector("input"); input.addEventListener("change", () => { if (input.files.length > 0) { let file = input.files[0]; console.log("You chose", file.name); if (file.type) console.log("It has type", file.type); } }); </script>
文件字段元素的 `files` 属性是一个类似数组的对象(再次,不是真正的数组),包含在该字段中选择的多个文件。它最初为空。没有简单地使用 `file` 属性的原因是,文件字段还支持 `multiple` 属性,这使得可以选择多个文件。
`files` 中的对象具有 `name`(文件名)、`size`(文件大小,以字节为单位,字节是 8 位的块)和 `type`(文件的媒体类型,例如 `text/plain` 或 `image/jpeg`)等属性。
它没有的属性是包含文件内容的属性。获取文件内容需要更多步骤。由于从磁盘读取文件可能需要时间,因此该接口是异步的,以避免冻结窗口。
<input type="file" multiple> <script> let input = document.querySelector("input"); input.addEventListener("change", () => { for (let file of Array.from(input.files)) { let reader = new FileReader(); reader.addEventListener("load", () => { console.log("File", file.name, "starts with", reader.result.slice(0, 20)); }); reader.readAsText(file); } }); </script>
读取文件是通过创建一个 `FileReader` 对象、为其注册一个 `"load"` 事件处理程序,并调用其 `readAsText` 方法完成的,该方法将我们想要读取的文件传递给它。加载完成后,读取器中的 `result` 属性将包含文件的内容。
`FileReader` 在读取文件失败时也会触发 `"error"` 事件。错误对象本身最终会出现在读取器的 `error` 属性中。这个接口是在 Promise 成为语言的一部分之前设计的。可以用 Promise 将其包装起来,像这样
function readFileText(file) { return new Promise((resolve, reject) => { let reader = new FileReader(); reader.addEventListener( "load", () => resolve(reader.result)); reader.addEventListener( "error", () => reject(reader.error)); reader.readAsText(file); }); }
在客户端存储数据
包含少量 JavaScript 的简单 HTML 页面可以作为“迷你应用程序”的绝佳格式 - 小型辅助程序,可以自动化基本任务。通过将几个表单字段与事件处理程序连接起来,你可以执行任何操作,从厘米和英寸之间的转换到从主密码和网站名称计算密码。
当这样的应用程序需要在会话之间记住一些内容时,你不能使用 JavaScript 绑定——它们在每次页面关闭时都会被丢弃。你可以设置一个服务器,将其连接到互联网,并让你的应用程序在那里存储一些东西(我们将在 第 20 章 中看到如何做到这一点)。但这会增加很多额外的工作和复杂性。有时,将数据保存在浏览器中就足够了。
localStorage
对象可用于以在页面重新加载后仍然存在的方式存储数据。此对象允许你将字符串值存储在名称下。
localStorage.setItem("username", "marijn"); console.log(localStorage.getItem("username")); // → marijn localStorage.removeItem("username");
localStorage
中的值将一直存在,直到它被覆盖、使用 removeItem
删除或用户清除其本地数据。
来自不同域的站点会获得不同的存储区。这意味着原则上,由给定网站存储在 localStorage
中的数据只能由该站点上的脚本读取(和覆盖)。
浏览器确实会对站点可以存储在 localStorage
中的数据的大小施加限制。这种限制,以及用垃圾填充人们硬盘并不真正有利可图的事实,可以防止此功能占用过多空间。
以下代码实现了一个简单的记事应用程序。它保存一组命名笔记,并允许用户编辑笔记和创建新笔记。
Notes: <select></select> <button>Add</button><br> <textarea style="width: 100%"></textarea> <script> let list = document.querySelector("select"); let note = document.querySelector("textarea"); let state; function setState(newState) { list.textContent = ""; for (let name of Object.keys(newState.notes)) { let option = document.createElement("option"); option.textContent = name; if (newState.selected == name) option.selected = true; list.appendChild(option); } note.value = newState.notes[newState.selected]; localStorage.setItem("Notes", JSON.stringify(newState)); state = newState; } setState(JSON.parse(localStorage.getItem("Notes")) ?? { notes: {"shopping list": "Carrots\nRaisins"}, selected: "shopping list" }); list.addEventListener("change", () => { setState({notes: state.notes, selected: list.value}); }); note.addEventListener("change", () => { let {selected} = state; setState({ notes: {...state.notes, [selected]: note.value}, selected }); }); document.querySelector("button") .addEventListener("click", () => { let name = prompt("Note name"); if (name) setState({ notes: {...state.notes, [name]: ""}, selected: name }); }); </script>
该脚本从存储在 "Notes"
中的值中获取其初始状态 localStorage
或,如果该值缺失,则创建一个仅包含购物清单的示例状态。从 localStorage
读取不存在的字段将返回 null
。将 null
传递给 JSON.parse
将使其解析字符串 "null"
并返回 null
。因此,??
运算符可用于在这种情况下提供默认值。
setState
方法确保 DOM 显示给定状态并将新状态存储到 localStorage
中。事件处理程序调用此函数以进入新状态。
示例中的 ...
语法用于创建一个新对象,它是旧 state.notes
的克隆,但添加或覆盖了一个属性。它使用扩展语法首先添加旧对象中的属性,然后设置新属性。对象文字中的方括号表示法用于创建名称基于某个动态值的属性。
还有一个类似于 localStorage
的对象,称为 sessionStorage
。这两者的区别在于,sessionStorage
的内容在每个会话结束时都会被遗忘,对于大多数浏览器来说,这意味着在每次关闭浏览器时都会被遗忘。
总结
在本章中,我们讨论了 HTTP 协议的工作原理。客户端发送一个请求,其中包含一个方法(通常为 GET
)和一个标识资源的路径。服务器然后决定如何处理该请求并以状态代码和响应主体进行响应。请求和响应都可能包含提供附加信息的标头。
浏览器 JavaScript 可以使用 HTTP 请求的接口称为 fetch
。发出请求如下所示
fetch("/18_http.html").then(r => r.text()).then(text => { console.log(`The page starts with ${text.slice(0, 15)}`); });
浏览器发出 GET
请求以获取显示网页所需的资源。页面也可能包含表单,允许用户输入的信息在提交表单时作为新页面的请求发送。
HTML 可以表示各种类型的表单字段,例如文本字段、复选框、多选字段和文件选择器。这些字段可以用 JavaScript 检查和操作。它们在更改时会触发 "change"
事件,在键入文本时会触发 "input"
事件,并在获得键盘焦点时接收键盘事件。value
(用于文本和选择字段)或 checked
(用于复选框和单选按钮)等属性用于读取或设置字段的内容。
当提交表单时,会在其上触发 "submit"
事件。JavaScript 处理程序可以调用 preventDefault
以禁用浏览器的默认行为。表单字段元素也可能出现在表单标签之外。
当用户从本地文件系统中的文件选择器字段中选择文件时,可以使用 FileReader
接口从 JavaScript 程序访问此文件的内容。
localStorage
和 sessionStorage
对象可用于以在页面重新加载后仍然存在的方式保存信息。第一个对象永远保存数据(或直到用户决定清除它),第二个对象保存数据直到浏览器关闭。
练习
内容协商
HTTP 可以执行的一项操作称为内容协商。Accept
请求标头用于告诉服务器客户端希望获取哪种类型的文档。许多服务器会忽略此标头,但当服务器知道各种编码资源的方式时,它可以查看此标头并发送客户端首选的资源。
URL https://eloquent.javascript.ac.cn/author 配置为根据客户端的要求以纯文本、HTML 或 JSON 响应。这些格式由标准化媒体类型 text/plain
、text/html
和 application/json
标识。
发送请求以获取此资源的所有三种格式。使用传递给 fetch
的选项对象中的 headers
属性将名为 Accept
的标头设置为所需的媒体类型。
最后,尝试请求媒体类型 application/
并查看它产生的状态代码。
// Your code here.
显示提示...
根据本章前面的fetch
示例编写你的代码。
请求虚假媒体类型将返回一个代码为 406(“不可接受”)的响应,这是服务器在无法满足 Accept
标头时应该返回的代码。
JavaScript 工作台
构建一个允许用户键入和运行 JavaScript 代码片段的界面。
在 <textarea>
字段旁边放置一个按钮,当按下时,使用我们在 第 10 章 中看到的 Function
构造函数将文本包装在函数中并调用它。将函数的返回值或它引发的任何错误转换为字符串,并在文本字段下方显示它。
<textarea id="code">return "hi";</textarea> <button id="button">Run</button> <pre id="output"></pre> <script> // Your code here. </script>
显示提示...
使用 document.
或 document.
获取对 HTML 中定义的元素的访问权限。按钮的 "click"
或 "mousedown"
事件的事件处理程序可以获取文本字段的 value
属性并在其上调用 Function
。
确保你将对 Function
的调用和对结果的调用都包装在一个 try
块中,以便你可以捕获它产生的异常。在这种情况下,我们真的不知道我们正在寻找哪种异常,所以捕获所有异常。
输出元素的 textContent
属性可用于用字符串消息填充它。或者,如果你想保留旧内容,可以使用 document.
创建一个新的文本节点并将其附加到元素。请记住在末尾添加一个换行符,以便所有输出都不会出现在同一行上。
康威生命游戏
康威生命游戏是一个简单的模拟,它在网格上创建人工“生命”,网格的每个单元格要么活着,要么死亡。在每一代(回合)中,将应用以下规则
请注意,这些规则是对整个网格同时应用的,而不是逐个方格应用的。这意味着邻居的计数基于一代开始时的状况,发生在这一代期间的邻居单元格的变化不应影响给定单元格的新状态。
使用你认为合适的任何数据结构来实现此游戏。使用 Math.random
在初始时用随机图案填充网格。将其显示为复选框字段的网格,旁边有一个按钮用于前进到下一代。当用户选中或取消选中复选框时,这些更改应在计算下一代时包含在内。
<div id="grid"></div> <button id="next">Next generation</button> <script> // Your code here. </script>
显示提示...
为了解决更改在概念上同时发生的这个问题,请尝试将一代的计算视为一个纯函数,该函数接受一个网格并产生一个代表下一回合的新网格。
可以使用宽度 × 高度元素的单个数组来表示矩阵,按行存储值,因此,例如,第五行中的第三个元素(使用基于零的索引)存储在位置 4 × 宽度 + 2。你可以用两个嵌套循环来计算活邻居,在两个维度上循环遍历相邻坐标。注意不要计算场外的单元格,并且忽略中心单元格,因为我们正在计算其邻居。
确保对复选框的更改在下一代生效可以通过两种方式实现。事件处理程序可以注意到这些更改并更新当前网格以反映它们,或者你可以在计算下一回合之前从复选框中的值生成一个新的网格。
如果你选择使用事件处理程序,你可能想要添加属性来标识每个复选框对应的位置,以便轻松确定要更改哪个单元格。
要绘制复选框网格,你可以使用 <table>
元素(参见 第 14 章)或者简单地将它们全部放在同一个元素中,并在行之间放置 <br>
(换行符)元素。