mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-28 15:12:20 +00:00
701 lines
37 KiB
Markdown
701 lines
37 KiB
Markdown
## 十八、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开头的状态码表示服务器端出现了问题,而请求没有问题。
|
||
|
||
请求或响应的第一行后可能会有任意个协议头,多个形如“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
|
||
<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 更安全。
|
||
|
||
## 表单字段
|
||
|
||
一个网页表单在其<form>标签中包含若干个输入字段。HTML允许一定数量的不同风格的输入字段,从简单的开/关选择框到下拉菜单和进行输入的字段。本书不会全面的讨论每一个输入字段类型,不过我们会先大概讲述一下。
|
||
|
||
很多字段类型都使用<input>标签。标签的type属性用来选择字段的种类,下面是一些常用的<input>类型。
|
||
|
||
+ text:一个单行的文本输入框。
|
||
|
||
+ password:和text相同但隐藏了输入内容。
|
||
|
||
+ checkbox:一个复选框。
|
||
|
||
+ 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="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事件。
|
||
|
||
## 18.2 聚焦
|
||
|
||
不同于HTML文档中的其他元素,表单字段可以获取键盘焦点。当点击或以某种方式激活时,他们会成为激活的元素,并接受键盘的输入。
|
||
|
||
如果一个文档有文本字段,只有在该字段被聚焦时才能有文本输入进去。其他字段以不同的方式来响应键盘事件。例如,一个<select>菜单会尝试移动到和用户输入相匹配的内容的选项,并可以响应方向按键,将选择器上下移动。
|
||
|
||
我们可以通过使用JavaScript的focus和blur方法来控制聚焦。第一个会聚焦到某一个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属性,可以实现相同的效果,并让浏览器知道我们正在尝试实现的事情。这可以让浏览器来禁用一些错误的操作,例如用户希望将焦点置于其他地方。
|
||
|
||
```html
|
||
<input type="text" autofocus>
|
||
```
|
||
|
||
浏览器也允许用户通过TAB键来切换焦点。通过tabindex属性可以改变元素接受焦点的顺序。后面的例子会让焦点从文本输入框之间跳转到OK按钮而不是到帮助链接。
|
||
|
||
```html
|
||
<input type="text" tabindex=1> <a href=".">(help)</a>
|
||
<button onclick="console.log('ok')" tabindex=2>OK</button>
|
||
```
|
||
|
||
默认情况下,多数的HTML元素不能被聚焦。但是可以通过添加tabindex属性使任何元素可以对焦。
|
||
|
||
## 18.3 禁用字段
|
||
|
||
所有的表单字段都可以通过其disable属性来禁用,disable也作为一个元素的文档对象的属性。
|
||
|
||
```html
|
||
<button>I'm all right</button>
|
||
<button disabled>I'm out</button>
|
||
```
|
||
|
||
禁用的字段不能被聚焦或更改,不同于激活的字段,它们会变成灰色的。
|
||
|
||
当一个程序在处理一些由按键或其他控制方式出发的事件,并且这些事件可能要求和服务器的通信时,将元素禁用直到动作完成可能是一个很好的方法。按照这用方式,当用户失去耐心并且再次点击时,不会意外的重复这一动作。
|
||
|
||
## 18.4 作为整体的表单
|
||
|
||
当一个字段被包含在<form>元素中时,其DOM元素会有一个form属性指向form的DOM元素。<form>元素则会有一个叫作elements属性,包含一个类似于数据的集合,其中包含全部的字段。
|
||
|
||
一个表单字段的name属性会决定在form提交时其内容的辨别方式。同时在获取form的elements属性时也可以作为一种属性名,所以elements属性既可以像数组(由编号来访问)一样使用也可以像映射一样访问(通过名字访问)。
|
||
|
||
```html
|
||
<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>
|
||
var 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的按钮在点击时,会提交表单。在一个form被聚焦时,点击enter键也会有同样的效果。
|
||
|
||
通常在提交一个表单时,浏览器会将页面导航到form的action属性指明的页面,使用GET或POST请求。但是在这些发生之前,“submit”事件会被触发。这个事件可以由JavaScript处理,并且处理函数可以通过调用事件对象的preventDefault来禁用默认行为。
|
||
|
||
```html
|
||
<form action="example/submit.html">
|
||
Value: <input type="text" name="value">
|
||
<button type="submit">Save</button>
|
||
</form>
|
||
<script>
|
||
var form = document.querySelector("form");
|
||
form.addEventListener("submit", function(event) {
|
||
console.log("Saving value", form.elements.value.value);
|
||
event.preventDefault();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
在JavaScript中submit事件有多种用途。我们可以编写代码来检测用户输入是否正确并且立刻做出错误信息提示,当输入值错误时中止提交表单。或者我们可以禁用正常的提交方式,正如在之前的例子中,使用自定义的程序来处理请求,使用XMLHttpRequest来将其发送到服务器且不重新载入页面。
|
||
|
||
## 18.5 文本字段
|
||
|
||
由type属性为text或password的<input>标签和textarea标签组成的字段有相同的接口。其DOM元素都有一个value属性,保存了为字符串格式的当前内容。将这个属性更改为另一个值将改变字段的内容。
|
||
|
||
文本字段selectionStart和selectEnd属性包含光标和所选文字的信息。当没有选中文字时,这两个属性的值相同,表明当前光标的信息。例如,0表示文本的开始,10表示光标在第十个字符之后。当一部分字段被选中时,这两个属性值会不同,表明选中文字开始位置和结束位置。
|
||
|
||
和正常的值一样,这些属性也可以被更改。
|
||
|
||
下面的例子中,有一个关于Knaseknemwy的文章但是名字拼写有一些问题,后续代码将<textarea>标签和一个事件处理程序关联起来,当点击F2时,插入Knaseknemwy。
|
||
|
||
```html
|
||
<textarea></textarea>
|
||
<script>
|
||
var textarea = document.querySelector("textarea");
|
||
textarea.addEventListener("keydown", function(event) {
|
||
// The key code for F2 happens to be 113
|
||
if (event.keyCode == 113) {
|
||
replaceSelection(textarea, "Khasekhemwy");
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
function replaceSelection(field, word) {
|
||
var 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 = field.selectionEnd =
|
||
from + word.length;
|
||
}
|
||
</script>
|
||
```
|
||
|
||
replaceSelection函数用给定的字符串替换当前选中的文本字段内容,并将光标移动到替换内容后让用户可以继续输入。Change事件不会在每次有输入时都被调用,而是在内容在改变并失焦后触发。为了及时的响应文本字段的改变,则需要为input事件注册一个处理函数,每当用户有输入或更改时就被触发。
|
||
|
||
下面的例子展示一个文本字段和一个展示当前输入文字长度的计数器。
|
||
|
||
```html
|
||
<input type="text"> length: <span id="length">0</span>
|
||
<script>
|
||
var text = document.querySelector("input");
|
||
var output = document.querySelector("#length");
|
||
text.addEventListener("input", function() {
|
||
output.textContent = text.value.length;
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 18.6 选择框和单选框
|
||
|
||
一个选择框只是一个简单的双选切换。其值可以通过其包含一个布尔值的checked属性来获取和更改。
|
||
|
||
```html
|
||
<input type="checkbox" id="purple">
|
||
<label for="purple">Make this page purple</label>
|
||
<script>
|
||
var checkbox = document.querySelector("#purple");
|
||
checkbox.addEventListener("change", function() {
|
||
document.body.style.background =
|
||
checkbox.checked ? "mediumpurple" : "";
|
||
});
|
||
</script>
|
||
```
|
||
|
||
<label>标签用来将部分文本和一个输入字段关联。其for属性应该关联到一个字段的id属性。点击label会激活相关联的字段,当字段为选择框或单选框时,会聚焦到选择或单选框并切换其值。
|
||
|
||
单选框和选择框类似,不过单选框可以通过相同的name属性隐式的和其他几个单选框关联,保证只能选择其中一个。
|
||
|
||
```html
|
||
Color:
|
||
<input type="radio" name="color" value="mediumpurple"> Purple
|
||
<input type="radio" name="color" value="lightgreen"> Green
|
||
<input type="radio" name="color" value="lightblue"> Blue
|
||
<script>
|
||
var buttons = document.getElementsByName("color");
|
||
function setColor(event) {
|
||
document.body.style.background = event.target.value;
|
||
}
|
||
for (var i = 0; i < buttons.length; i++)
|
||
buttons[i].addEventListener("change", setColor);
|
||
</script>
|
||
```
|
||
|
||
Document.getElementsByName方法可以返回全部name属性为给定值的元素。上例中循环遍历这些元素(通过正常的for循环而不是forEach,因为返回的集合不是一个真正的数据)并且为每一个元素添加事件处理。事件对象有一个target属性,关联到触发该事件的元素。在这样的事件处理中这个方法非常有用,不同的元素会调用这个事件处理,然后使用相同的方式访问到当前的目标。
|
||
|
||
## 18.7 选择字段
|
||
|
||
选择字段和单选按钮比较相似,允许用户从多个选项中选择。但是,单选框的展示排版是由我们控制的,而<select>标签外观则是由浏览器控制。
|
||
|
||
选择字段的变体会和一系列的选择框很相似,但不同于单选框的是当给出multiple属性时,<select>标签会允许用户选择任意数量的选项而不是只有一个。
|
||
|
||
```html
|
||
<select multiple>
|
||
<option>Pancakes</option>
|
||
<option>Pudding</option>
|
||
<option>Ice cream</option>
|
||
</select>
|
||
```
|
||
|
||
在多数浏览器中,现实样式会和单选选择字段有所区别,单选选择字段有一个下拉控制按钮且只会在点击时打开。
|
||
|
||
<select>标签的size属性用来设置同时可展示的选项,可以使得你对下拉菜单的外观进行控制。例如,设置size属性为3会让这个字段显示三行,不论是否有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>
|
||
var select = document.querySelector("select");
|
||
var output = document.querySelector("#output");
|
||
select.addEventListener("change", function() {
|
||
var number = 0;
|
||
for (var i = 0; i < select.options.length; i++) {
|
||
var option = select.options[i];
|
||
if (option.selected)
|
||
number += Number(option.value);
|
||
}
|
||
output.textContent = number;
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 18.8 文件字段
|
||
|
||
文件字段最初是用于通过表单来上传从浏览器机器中获取的文件。在现代浏览器中,也可以从JavaScript程序中读取文件。该字段则作为一个看门人角色。脚本不能简单地直接从用户的电脑中读取文件,但是如果用户在这个字段中选择了一个文件,浏览器会将这个行为解释为脚本,便可以访问该文件。
|
||
|
||
一个文本字段是一个类似于“选择文件”或“浏览”标签的按钮,后面跟着所选文件的信息。
|
||
|
||
```html
|
||
<input type="file">
|
||
<script>
|
||
var input = document.querySelector("input");
|
||
input.addEventListener("change", function() {
|
||
if (input.files.length > 0) {
|
||
var file = input.files[0];
|
||
console.log("You chose", file.name);
|
||
if (file.type)
|
||
console.log("It has type", file.type);
|
||
}
|
||
});
|
||
</script>
|
||
```
|
||
|
||
文本字段的files属性是一个类似数组的对象(当然,不是一个真正的数组),包含在字段中所选择的文件。开始时是空的。因此文本字段属性不仅仅是file属性。有时文本字段可以上传多个文件,这使得同时选择多个文件变为可能。
|
||
|
||
files属性中的对象有name(文件名)、size(文件大小,单位为字节),和type(文件的媒体类型,如text/plain,image/jped)等属性。
|
||
|
||
而files属性中不包含文件内容的属性。获取这个内容会比较复杂。由于从硬盘中读取文件会需要一些时间,接口需要使用异步方法来放置文档的无响应问题。你可以将FileReader构造器看作XMLHttpRequest,但是FileReader是用于文件的。
|
||
|
||
```html
|
||
<input type="file" multiple>
|
||
<script>
|
||
var input = document.querySelector("input");
|
||
input.addEventListener("change", function() {
|
||
Array.prototype.forEach.call(input.files, function(file) {
|
||
var reader = new FileReader();
|
||
reader.addEventListener("load", function() {
|
||
console.log("File", file.name, "starts with",
|
||
reader.result.slice(0, 20));
|
||
});
|
||
reader.readAsText(file);
|
||
});
|
||
});
|
||
</script>
|
||
```
|
||
|
||
读取文件是通过FileReader对象实现的,注册一个load事件处理程序,然后调用readAsText方法,传入我们希望读取的文件,一旦载入完成,reader的result属性内容就是文件内容。
|
||
|
||
下面的例子中使用Array.prototype.forEach来迭代遍历数组,因为通过正常的循环来从事件处理函数中获取正确的文件和reader对象会很不方便。变量可以被循环的全部迭代器获取到。
|
||
|
||
FileReader对象还会在读取文件失败时触发error事件。错误对象本身会存在reader的error属性中。如果你不想去记住另一个奇怪的异步接口的细节,你可以将其封装在Promise(见17章)中:
|
||
|
||
```html
|
||
function readFile(file) {
|
||
return new Promise(function(succeed, fail) {
|
||
var reader = new FileReader();
|
||
reader.addEventListener("load", function() {
|
||
succeed(reader.result);
|
||
});
|
||
reader.addEventListener("error", function() {
|
||
fail(reader.error);
|
||
});
|
||
reader.readAsText(file);
|
||
});
|
||
}
|
||
```
|
||
|
||
可以通过调用slice方法来获取文件的一部分并将结果(一个叫作blob的对象)传递给文件读取器。
|
||
|
||
## 18.9 客户端保存数据
|
||
|
||
采用JavaScript代码的简单HTML页面可以作为实现一些小应用的很好的途径。可以采用小的帮助程序来自动化处理一些日常事务。通过关联一些表单字段和事件处理函数,你可以实现华氏度与摄氏度的转换。也可以实现由主密码和网站名来生成密码等各种任务。
|
||
|
||
当一个应用需要存储一些东西以便于跨对话使用时,则不能使用JavaScript变量因为每当页面关闭时这些值就会丢失。你可以搭建一个服务器,连接到因特网,将一些服务数据存储到其中。在第20章中将会介绍如何实现这些,当然这需要很多的工作,也有一定的复杂度。有时只要将数据存储在浏览器中即可,但是如何实现呢?
|
||
|
||
你可以将字符串数据存储在localStorage对象中,使得其可以在页面重新载入时继续保留。这个对象允许你将字符串存储在某个名字(也是字符串)下,下面是具体示例。
|
||
|
||
```js
|
||
localStorage.setItem("username", "marijn");
|
||
console.log(localStorage.getItem("username"));
|
||
// → marijn
|
||
localStorage.removeItem("username");
|
||
```
|
||
|
||
一个在localStorage中的值会保留到其被重写时,它也可以通过removeItem来清除,或者由用户清除本地数据。
|
||
|
||
不同字段名的站点的数据会存在不同的地方。这也表明原则上由localStorage存储的数据只可以由相同站点的脚本编辑。
|
||
|
||
浏览器也会限制一个站点可以存储的localStorage的数据的大小,通常只有几兆。这个可以限制用户的硬盘被垃圾文件填满,防止了过多占用用户空间。
|
||
|
||
下面的代码实现了一个简单的记笔记的应用。程序将用户的笔记保存为一个对象,将笔记的标题和内容字符串相关联。对象被编码为JSON格式并存储在localStorage中。用户可以从<select>选择字段中选择笔记并在<textarea>中编辑笔记,并可以通过点击一个按钮来添加笔记。
|
||
|
||
```html
|
||
Notes: <select id="list"></select>
|
||
<button onclick="addNote()">new</button><br>
|
||
<textarea id="currentnote" style="width: 100%; height: 10em">
|
||
</textarea>
|
||
|
||
<script>
|
||
var list = document.querySelector("#list");
|
||
function addToList(name) {
|
||
var option = document.createElement("option");
|
||
option.textContent = name;
|
||
list.appendChild(option);
|
||
}
|
||
|
||
// Initialize the list from localStorage
|
||
var notes = JSON.parse(localStorage.getItem("notes")) ||
|
||
{"shopping list": ""};
|
||
for (var name in notes)
|
||
if (notes.hasOwnProperty(name))
|
||
addToList(name);
|
||
|
||
function saveToStorage() {
|
||
localStorage.setItem("notes", JSON.stringify(notes));
|
||
}
|
||
|
||
var current = document.querySelector("#currentnote");
|
||
current.value = notes[list.value];
|
||
|
||
list.addEventListener("change", function() {
|
||
current.value = notes[list.value];
|
||
});
|
||
current.addEventListener("change", function() {
|
||
notes[list.value] = current.value;
|
||
saveToStorage();
|
||
});
|
||
|
||
function addNote() {
|
||
var name = prompt("Note name", "");
|
||
if (!name) return;
|
||
if (!notes.hasOwnProperty(name)) {
|
||
notes[name] = "";
|
||
addToList(name);
|
||
saveToStorage();
|
||
}
|
||
list.value = name;
|
||
current.value = notes[name];
|
||
}
|
||
</script>
|
||
```
|
||
|
||
脚本通过存储在localStorage中的值来初始化notes,如果其中没有值,则会使用一个简单的只有一个购物单的笔记对象来初始化。从localStorage中读取不存在的字段会返回null。
|
||
|
||
将null值传递给JSON.parse会使得该方法对字符串null进行解析,并返回null。因此,对于这种情况,||运算符可以用来提供一个默认值。
|
||
|
||
当笔记数据更改时(新的笔记添加或已有笔记更改时),saveToStorage函数会被调用,来更新存储字段。如果这个应用想要处理上千个笔记,而不是几个的话这种做法的代价将会过大,我们则需要用更复杂的方式来存储,例如给每个笔记独有的存储空间。
|
||
|
||
当用户添加一个新的笔记时,代码必须明确的更新文本字段,尽管<select>字段有一个change处理函数进行同样的事情,但代码实现这个功能依旧是必须的,因为change事件只有在用户更改字段的数据时才会触发,而不是脚本更改时。
|
||
|
||
还有另一个和localStorage很相似的对象叫作sessionStorage。这两个对象之间的区别在于sessionStorage的内容会在每次会话结束时丢失,而对于多数浏览器来说,会话会在浏览器关闭时结束。
|
||
|
||
## 18.10 本章小结
|
||
|
||
HTML可以表示多种表单字段,例如文本字段、选择框、多选字段和文件选取。
|
||
|
||
这些字段可以用JavaScript进行控制和读取。当内容改变时会触发change事件,当文本有输入时会触发input时间,也还有多种键盘事件。这些事件允许我们获知用户与这些字段产生交互的时间。像value这样的属性(对于文本和选择字段)或checked属性(对于选择框和单选框)是用来读取或设置字段的值。
|
||
|
||
当一个表单被提交时,会触发其submit事件,JavaScript处理器可以通过调用preventDefault来禁用默认的提交事件。表单字段的元素不一定需要被包装在<form>标签中。
|
||
|
||
当用户在一个文件选择字段中选择了本机中的一个文件时,可以用FileReader接口来在JavaScript中获取文件内容。
|
||
|
||
localStorage和sessionStorage对象可以用来保存页面重载后依旧保留的信息。第一个会永久保留数据(直到用户决定清楚),第二个则会保存到浏览器关闭时。
|
||
|
||
## 18.11 习题
|
||
|
||
### 18.11.1 一个JavaScript工作台
|
||
|
||
构建一个接口,用于让用户输入并运行一段JavaScript代码。
|
||
|
||
在一个<textarea>后面放置一个按钮,当点击按钮时,使用第10章中讲述过的Function构造器来将这些文本封装到一个函数中并调用。将函数运行结果或错误信息转换为字符串显示在<textarea>后面。
|
||
|
||
```html
|
||
<textarea id="code">return "hi";</textarea>
|
||
<button id="button">Run</button>
|
||
<pre id="output"></pre>
|
||
|
||
<script>
|
||
// Your code here.
|
||
</script>
|
||
```
|
||
|
||
### 18.11.2 自动补全
|
||
|
||
扩展一个文本字段,当用户输入时,一个建议值的列表显示在这个字段下面。提供一个可能的输入值数组,并展示出以当前输入的文本开头的值。当点击一个建议项时,将当前文本中的值用该值替换。
|
||
|
||
```html
|
||
<input type="text" id="field">
|
||
<div id="suggestions" style="cursor: pointer"></div>
|
||
|
||
<script>
|
||
// Builds up an array with global variable names, like
|
||
// 'alert', 'document', and 'scrollTo'
|
||
var terms = [];
|
||
for (var name in window)
|
||
terms.push(name);
|
||
|
||
// Your code here.
|
||
</script>
|
||
```
|
||
|
||
### 18.11.3 Conway的生命游戏
|
||
|
||
Conway的生命游戏是一个简单的在方格中模拟生命的游戏,每一个小格都可以生存或灭亡。对于每一代生命都要遵循以下条件:
|
||
|
||
+ 任何小格周围有少于两个或多于三个的活着的邻居都会死亡。
|
||
|
||
+ 任意两个或三个的活着的邻居的小格可以生存到下一代。
|
||
|
||
+ 任何死去的、有三个活着的邻居的小格可以再次复活。
|
||
|
||
任意一个相连的小格都可以称为邻居,包括对角相连。
|
||
|
||
注意这些规则要立刻应用于整个网格,而不是一次一个方格。这表明邻居的数目由开始的一代决定,并且邻居在每一代时发生的变化不应该影响给定小格新的状态。
|
||
|
||
使用任何一个你认为合适的数据结构来实现这个游戏。使用Math.random来随机的生成开始状态。将其展示为一个选择框组成的网格和一个生成下一代的按钮。当用户选中或取消选中一个选择框时,其变化应该影响下一代的计算。
|
||
|
||
```html
|
||
<div id="grid"></div>
|
||
<button id="next">Next generation</button>
|
||
|
||
<script>
|
||
// Your code here.
|
||
</script>
|
||
```
|