HTTP 与表单

HTTP 与表单
人们往往难以理解这一设计的一点是,除了 URL、HTTP 和 HTML 之外,再无其他东西。没有一台中央计算机 “控制” 着网络,没有单一的网络供这些协议运行,甚至没有任何组织在 “运营” 网络。网络不是存在于某个 “地方” 的物理 “事物”,而是一个可以承载信息的 “空间”。
—— 蒂姆・伯纳斯 - 李
(插图:羊皮卷轴上的网页注册表单)
第 13 章介绍的超文本传输协议(HTTP)是万维网上请求和提供数据的机制。本章将更详细地描述该协议,并解释浏览器 JavaScript 如何访问它。
协议原理
如果你在浏览器地址栏中输入 eloquentjavascript.net/18_http.html,浏览器首先会查找与 eloquentjavascript.net 关联的服务器地址,然后尝试在端口 80(HTTP 通信的默认端口)上与该服务器建立 TCP 连接。如果服务器存在并接受连接,浏览器可能会发送如下内容:
plaintext
GET /18_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: 你的浏览器名称
然后服务器通过同一个连接做出响应:
plaintext
HTTP/1.1 200 OK
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

<!doctype html>
... 文档的其余部分
浏览器会获取响应中空行之后的部分(即响应体,不要与 HTML 的 <body> 标签混淆),并将其作为 HTML 文档显示。
客户端发送的信息称为请求。它以以下行开始:
plaintext
GET /18_http.html HTTP/1.1
第一个单词是请求的方法。GET 表示我们想要获取指定的资源。其他常见方法有 DELETE(删除资源)、PUT(创建或替换资源)和 POST(向资源发送信息)。请注意,服务器并非必须执行它收到的每个请求。如果你随便找一个网站并要求它删除主页,它很可能会拒绝。
方法名后面的部分是请求所适用的资源路径。在最简单的情况下,资源就是服务器上的一个文件,但协议并不要求必须如此。资源可以是任何能像文件一样传输的内容。许多服务器会动态生成它们的响应内容。例如,如果你打开 https://github.com/marijnh,服务器会在数据库中查找名为 “marijnh” 的用户,如果找到,就会为该用户生成一个个人资料页面。
在资源路径之后,请求的第一行会提到 HTTP/1.1,以表明它使用的 HTTP 协议版本。
实际上,许多网站使用 HTTP/2 版本,它支持与 1.1 版本相同的概念,但更复杂,因此速度更快。浏览器会在与特定服务器通信时自动切换到适当的协议版本,无论使用哪个版本,请求的结果都是相同的。由于 1.1 版本更简单直观,我们将用它来解释协议。
服务器的响应也会以版本开头, followed by the status of the response, first as a three-digit status code and then as a human-readable string.
plaintext
HTTP/1.1 200 OK
以 2 开头的状态码表示请求成功。以 4 开头的代码表示请求存在问题。最著名的 HTTP 状态码可能是 404,它表示无法找到请求的资源。以 5 开头的代码表示服务器发生错误,请求本身没有问题。
请求或响应的第一行之后可以跟任意数量的头部(headers)。这些是以 name: value 形式出现的行,用于指定关于请求或响应的额外信息。以下是示例响应中的头部:
plaintext
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT
这告诉我们响应文档的大小和类型。在这个例子中,它是一个 87,320 字节的 HTML 文档。它还告诉我们该文档的最后修改时间。
客户端和服务器可以自由决定在请求或响应中包含哪些头部。但有些头部对于正常工作是必需的。例如,如果响应中没有 Content-Type 头部,浏览器将不知道如何显示文档。
头部之后,请求和响应都可能包含一个空行, followed by a body, which contains the actual document being sent. GET 和 DELETE 请求不会发送任何数据,但 PUT 和 POST 请求会。某些响应类型(如错误响应)也不需要响应体。
浏览器与 HTTP
如前所述,当我们在浏览器地址栏中输入 URL 时,浏览器会发出请求。当生成的 HTML 页面引用其他文件(如图片和 JavaScript 文件)时,浏览器也会获取这些文件。
一个中等复杂的网站很容易包含 10 到 200 个资源。为了能够快速获取这些资源,浏览器会同时发出多个 GET 请求,而不是一次等待一个响应。
HTML 页面可能包含表单,允许用户填写信息并发送到服务器。以下是一个表单示例:
html
预览
<form method="GET" action="example/message.html">
<p>姓名:<input type="text" name="name"></p>
<p>留言:
<textarea name="message"></textarea></p>
<p><button type="submit">发送</button></p>
</form>
这段代码描述了一个包含两个字段的表单:一个小字段用于输入姓名,一个大字段用于编写留言。当你点击 “发送” 按钮时,表单会被提交,这意味着字段的内容会被打包到 HTTP 请求中,浏览器会导航到该请求的结果页面。
当 <form> 元素的 method 属性为 GET(或省略)时,表单中的信息会作为查询字符串添加到 action URL 的末尾。浏览器可能会向这个 URL 发出请求:
plaintext
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1
问号表示 URL 路径部分的结束和查询部分的开始。后面跟着成对的名称和值,分别对应表单字段元素的 name 属性和这些元素的内容。& 字符用于分隔这些对。
URL 中编码的实际消息是 “Yes?”,但问号被替换成了一个奇怪的代码。查询字符串中的某些字符必须进行转义。问号被表示为 %3F,就是其中之一。似乎有一个不成文的规则:每种格式都需要自己的字符转义方式。这种方式称为 URL 编码,它使用百分号后跟两个十六进制数字来编码字符代码。在这个例子中,3F(十进制为 63)是问号字符的代码。JavaScript 提供了 encodeURIComponent 和 decodeURIComponent 函数来编码和解码这种格式。
javascript
运行
console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?
如果我们将前面示例中 HTML 表单的 method 属性改为 POST,提交表单时发出的 HTTP 请求将使用 POST 方法,并将查询字符串放在请求体中,而不是添加到 URL 中。
plaintext
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 API
浏览器 JavaScript 发出 HTTP 请求的接口称为 fetch。
javascript
运行
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 即使服务器返回错误状态码也会成功解析。只有在发生网络错误或无法找到请求的服务器时,它才会被拒绝。
fetch 的第一个参数是要请求的 URL。当该 URL 不以协议名称(如 http:)开头时,它会被视为相对 URL,即相对于当前文档进行解析。当它以斜杠(/)开头时,它会替换当前路径(即服务器名称后的部分)。如果不以斜杠开头,则当前路径中直到并包括最后一个斜杠的部分会被添加到相对 URL 前面。
要获取响应的实际内容,可以使用其 text 方法。由于初始 Promise 在收到响应头部后就会立即解析,而读取响应体可能需要更长时间,因此该方法再次返回一个 Promise。
javascript
运行
fetch("example/data.txt")
.then(resp => resp.text())
.then(text => console.log(text));
// → This is the content of data.txt
一个类似的方法 json 返回一个 Promise,该 Promise 会解析为将响应体解析为 JSON 后的 value,或者如果 JSON 无效则会被拒绝。
默认情况下,fetch 使用 GET 方法发出请求,并且不包含请求体。你可以通过传递一个包含额外选项的对象作为第二个参数来进行不同的配置。例如,以下请求尝试删除 example/data.txt:
javascript
运行
fetch("example/data.txt", {method: "DELETE"}).then(resp => {
console.log(resp.status);
// → 405
});
405 状态码表示 “方法不允许”,这是 HTTP 服务器表示 “恐怕我不能这样做” 的方式。
要为 PUT 或 POST 请求添加请求体,可以包含 body 选项。要设置头部,可以使用 headers 选项。例如,以下请求包含 Range 头部,指示服务器仅返回文档的一部分:
javascript
运行
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 请求来保护我们。
在构建需要访问多个域名的合法系统时,这可能是一个令人烦恼的问题。幸运的是,服务器可以在响应中包含这样的头部,明确向浏览器表明来自其他域名的请求是允许的:
plaintext
Access-Control-Allow-Origin: *
认识 HTTP
当构建需要浏览器中运行的 JavaScript 程序(客户端)与服务器上的程序(服务器端)之间通信的系统时,有几种不同的通信模型可供选择。
一种常用的模型是远程过程调用(RPC)。在这种模型中,通信遵循正常函数调用的模式,不同之处在于函数实际上运行在另一台机器上。调用它需要向服务器发出包含函数名称和参数的请求。该请求的响应包含返回值。
当从远程过程调用的角度思考时,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 的 Web 设计的,用于让网站在 HTTP 请求中发送用户提交的信息。这种设计假设与服务器的交互总是通过导航到新页面来完成。
然而,表单元素和页面的其他部分一样,都是 DOM 的一部分,代表表单字段的 DOM 元素支持许多其他元素没有的属性和事件。这使得我们可以用 JavaScript 程序检查和控制这些输入字段,实现诸如为表单添加新功能,或将表单和字段用作 JavaScript 应用程序的构建块等功能。
Web 表单由若干个输入字段组成,这些字段被分组在 <form> 标签中。HTML 支持多种不同样式的字段,从简单的开关复选框到下拉菜单和文本输入字段。本书不会全面讨论所有字段类型,但我们会先进行大致概述。
许多字段类型使用 <input> 标签。该标签的 type 属性用于选择字段的样式。以下是一些常用的 <input> 类型:
text:单行文本字段
password:与 text 相同,但会隐藏输入的文本
checkbox:开关按钮
color:颜色选择器
date:日历日期选择器
radio:(多项选择字段的)单选按钮
file:允许用户从计算机中选择文件
表单字段不一定必须出现在 <form> 标签中。你可以将它们放在页面的任何位置。这种无表单的字段无法被提交(只有整个表单才能提交),但当我们用 JavaScript 响应用户输入时,通常并不希望以正常方式提交字段。
html
预览
<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>
这些元素的 JavaScript 接口因元素类型而异。
多行文本字段有自己的标签 <textarea>,这主要是因为使用属性来指定多行初始值会很不方便。<textarea> 标签需要匹配的 </textarea> 闭合标签,并使用这两个标签之间的文本(而非 value 属性)作为初始文本。
html
预览
<textarea>
one
two
three
</textarea>
最后,<select> 标签用于创建允许用户从多个预定义选项中选择的字段。
html
预览
<select>
<option>Pancakes</option>
<option>Pudding</option>
<option>Ice cream</option>
</select>
当表单字段的值发生变化时,它会触发 "change" 事件。
焦点
与 HTML 文档中的大多数元素不同,表单字段可以获得键盘焦点。当被点击、通过 Tab 键移动到或通过其他方式激活时,它们会成为当前活动元素并接收键盘输入。
因此,只有当文本字段获得焦点时,你才能在其中输入内容。其他字段对键盘事件的响应不同。例如,<select> 菜单会尝试移动到包含用户输入文本的选项,并通过箭头键上下移动选择项。
我们可以通过 focus 和 blur 方法从 JavaScript 控制焦点。前者将焦点移动到调用它的 DOM 元素上,后者则移除焦点。document.activeElement 的值对应当前获得焦点的元素。
html
预览
<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 按钮,而不是先经过帮助链接:
html
预览
<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>
默认情况下,大多数类型的 HTML 元素不能被聚焦。你可以为任何元素添加 tabindex 属性使其可聚焦。tabindex 为 0 时,元素可聚焦但不影响焦点顺序。
禁用字段
所有表单字段都可以通过其 disabled 属性禁用。这是一个可以不指定值的属性 —— 只要存在该属性,元素就会被禁用。
html
预览
<button>I'm all right</button>
<button disabled>I'm out</button>
禁用的字段不能被聚焦或修改,浏览器会将它们显示为灰色和褪色的样子。
当程序正在处理某个按钮或其他控件引发的操作(可能需要与服务器通信,因此需要一段时间)时,将控件禁用直到操作完成是个好主意。这样,当用户不耐烦并再次点击它时,他们不会意外重复操作。
表单整体
当字段包含在 <form> 元素中时,其 DOM 元素将有一个 form 属性链接回表单的 DOM 元素。反过来,<form> 元素有一个名为 elements 的属性,其中包含一个类数组集合,包含其中的所有字段。
表单字段的 name 属性决定了表单提交时其值的标识方式。访问表单的 elements 属性时,它也可以用作属性名 ——elements 既可以作为类数组对象(通过索引访问),也可以作为映射(通过名称访问)。
html
预览
<form action="example/submit.html">
Name: <input type="text" name="name">

Password: <input type="password" name="password">

<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 方法阻止默认行为。
html
预览
<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> 标签或 type 为 text 或 password 的 <input> 标签创建的字段共享一个通用接口。它们的 DOM 元素有一个 value 属性,以字符串形式保存当前内容。将此属性设置为另一个字符串会更改字段的内容。
文本字段的 selectionStart 和 selectionEnd 属性为我们提供了文本中光标和选择的信息。当没有选择内容时,这两个属性的值相同,表示光标的位置。例如,0 表示文本的开头,10 表示光标在第 10 个字符之后。当字段的一部分被选中时,这两个属性的值不同,给出所选文本的开始和结束位置。与 value 一样,这些属性也可以被写入。
假设你正在写一篇关于第二王朝末代法老哈塞海姆威的文章,但拼写他的名字有些困难。以下代码为 <textarea> 标签添加了一个事件处理程序,当你按 F2 时,会为你插入字符串 “Khasekhemwy”。
html
预览
<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);
// 将光标放在单词后面
field.selectionStart = from + word.length;
field.selectionEnd = from + word.length;
}
</script>
replaceSelection 函数将文本字段中当前选中的部分替换为给定的单词,然后将光标移动到该单词后面,以便用户可以继续输入。
文本字段的 "change" 事件不会在每次输入内容时触发。相反,它会在字段内容更改后失去焦点时触发。要立即响应用户在文本字段中的输入变化,你应该注册 "input" 事件的处理程序,该事件在用户输入字符、删除文本或以其他方式操作字段内容时都会触发。
以下示例显示了一个文本字段和一个计数器,显示字段中文本的当前长度:
html
预览
<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 属性获取或更改,该属性是一个布尔值。
html
预览
<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 属性的其他单选按钮隐式关联,因此任何时候只能有一个单选按钮处于激活状态。
html
预览
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>
传给 querySelectorAll 的 CSS 查询中的方括号用于匹配属性。它选择 name 属性为 "color" 的元素。
选择字段
选择字段在概念上类似于单选按钮 —— 它们也允许用户从一组选项中选择。但单选按钮让我们可以控制选项的布局,而 <select> 标签的外观由浏览器决定。
选择字段还有一种变体,更类似于复选框列表而非单选框。当具有 multiple 属性时,<select> 标签允许用户选择任意数量的选项,而不仅仅是一个。普通选择字段显示为下拉控件,仅在打开时显示未选中的选项,而启用了 multiple 的字段同时显示多个选项,允许用户单独启用或禁用它们。
每个 <option> 标签都有一个值。这个值可以通过 value 属性定义。如果没有给出该属性,则选项内的文本将作为其值。<select> 元素的 value 属性反映当前选中的选项。但对于多选字段,这个属性意义不大,因为它只会给出当前选中选项中的一个值。
<select> 字段的 <option> 标签可以通过字段的 options 属性作为类数组对象访问。每个选项都有一个名为 selected 的属性,表示该选项当前是否被选中。该属性也可以被写入以选中或取消选中选项。
以下示例从多选字段中提取选中的值,并使用它们从各个位组成一个二进制数。按住 Ctrl 键(或 Mac 上的 Command 键)可以选择多个选项。
html
预览
<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 程序读取此类文件的方法。该字段充当一种 “看门人” 的角色。脚本不能简单地开始读取用户计算机上的私有文件,但如果用户在这样的字段中选择了一个文件,浏览器会将该操作解释为允许脚本读取该文件。
文件字段通常看起来像一个标有 “选择文件” 或 “浏览” 之类的按钮,旁边有关于所选文件的信息。
html
预览
<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(文件大小,以字节为单位,1 字节 = 8 位)和 type(文件的媒体类型,如 text/plain 或 image/jpeg)等属性。
但它没有包含文件内容的属性。获取文件内容稍微复杂一些。由于从磁盘读取文件可能需要时间,该接口是异步的,以避免冻结窗口。
html
预览
<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 包装它:
javascript
运行
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 章看到如何做到这一点)

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容