JavaScript 編程精解 中文第三版 十八、HTTP 和表單

十八、HTTP 和表單

原文:HTTP and Forms

譯者:飛龍

協議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

部分參考了《JavaScript 編程精解(第 2 版)》

通信在實質上必須是無狀態的,從客戶端到服務器的每個請求都必須包含理解請求所需的所有信息,并且不能利用服務器上存儲的任何上下文。

Roy Fielding,《Architectural Styles and the Design of Network-based Software Architectures》

image

我們曾在第 13 章中提到過超文本傳輸協議(HTTP),萬維網中通過該協議進行數據請求和傳輸。在本章中會對該協議進行詳細介紹,并解釋瀏覽器中 JavaScript 訪問 HTTP 的方式。

協議

當你在瀏覽器地址欄中輸入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: 65585
Content-Type: text/html
Last-Modified: Mon, 08 Jan 2018 10:29:45 GMT

<!doctype html>
... the rest of the document

瀏覽器會選取空行之后的響應部分,也就是正文(不要與 HTML <body>標簽混淆),并將其顯示為 HTML 文檔。

由客戶端發出的信息叫作請求。請求的第一行如下。

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/1.1 200 OK

以 2 開頭的狀態碼表示請求成功。以 4 開頭的狀態碼表示請求中有錯誤。404 是最著名的 HTTP 狀態碼了,表示找不到資源。以 5 開頭的狀態碼表示服務器端出現了問題,而請求沒有問題。

請求或響應的第一行后可能會有任意個協議頭,多個形如name: value的行表明了和請求或響應相關的更多信息。這些是示例響應中的頭信息。

Content-Length: 65585
Content-Type: text/html
Last-Modified: Thu, 04 Jan 2018 14:05:30 GMT

這些信息說明了響應文檔的大小和類型。在這個例子中,響應是一個 65585 字節的 HTML 文檔,同時也說明了該文檔最后的更改時間。

多數大多數協議頭,客戶端或服務器可以自由決定需要在請求或響應中包含的協議頭,不過也有一些協議頭是必需的。例如,指明主機名的Host頭在請求中是必須的,因為一個服務器可能在一個 IP 地址下有多個主機名服務,如果沒有Host頭,服務器則無法判斷客戶端嘗試請求哪個主機。

請求和響應可能都會在協議頭后包含一個空行,后面則是消息體,包含所發送的數據。GETDELETE請求不單獨發送任何數據,但PUTPOST請求則會。同樣地,一些響應類型(如錯誤響應)不需要有消息體。

瀏覽器和 HTTP

正如上例所示,當我們在瀏覽器地址欄輸入一個 URL 后瀏覽器會發送一個請求。當 HTML 頁面中包含有其他的文件,例如圖片和 JavaScript 文件時,瀏覽器也會一并獲取這些資源。

一個較為復雜的網站通常都會有 10 到 200 個不等的資源。為了可以很快地取得這些資源,瀏覽器會同時發送多個GET請求,而不是一次等待一個請求。此類文檔都是通過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

問號表示路徑的末尾和查詢字符串的起始。后面是多個名稱和值,這些名稱和值分別對應form輸入字段中的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,則瀏覽器會使用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 可以通過fetch接口生成 HTTP 請求。 由于它比較新,所以它很方便地使用了Promise(這在瀏覽器接口中很少見)。

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

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

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

405 狀態碼意味著“方法不允許”,這是 HTTP 服務器說“我不能這樣做”的方式。

為了添加一個請求正文,你可以包含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 方法來建立自己的通信。不同于遠程調用方法addUser,你需要發送一個PUT請求到users/larry,不同于將用戶屬性進行編碼后作為參數傳遞,你定義了一個 JSON 文檔格式(或使用一種已有的格式)來展示一個用戶。PUT請求的正文則只是這樣的一個用來建立新資源的文檔。由GET方法獲取的資源則是自愿的 URL(例如,/users/larry),該 URL 返回代表這個資源的文檔。

第二種方法使用了 HTTP 的一些特性,所以使得整體更簡潔。例如對于資源緩存的支持(在客戶端存一份副本用于快速訪問)。HTTP 中使用的概念設計良好,可以提供一組有用的原則來設計服務器接口。

安全和 HTTPS

通過互聯網傳播的數據,往往走過漫長而危險的道路。 為了到達目的地,它必須跳過任何東西,從咖啡店的 Wi-Fi 到由各個公司和國家管理的網絡。 在它的路線上的任何位置,它都可能被探測或者甚至被修改。

如果對某件事保密是重要的,例如你的電子郵件帳戶的密碼,或者它到達目的地而未經修改是重要的,例如帳戶號碼,你使用它在銀行網站上轉賬,純 HTTP 就不夠好了。

安全的 HTTP 協議,其 URL 以https://開頭,是一種難以閱讀和篡改的,HTTP 流量的封裝方式。 在交換數據之前,客戶端證實該服務器是它所聲稱的東西,通過要求它證明,它具有由瀏覽器承認的證書機構所頒發的證書。 接下來,通過連接傳輸的所有數據,都將以某種方式加密,它應該防止竊聽和篡改。

因此,當 HTTPS 正常工作時,它可以阻止某人冒充你想要與之通話的網站,以及某人窺探你的通信。 這并不完美,由于偽造或被盜的證書和損壞的軟件,存在各種 HTTPS 失敗的事故,但它比純 HTTP 更安全。

表單字段

表單最初是為 JavaScript 之前的網頁設計的,允許網站通過 HTTP 請求發送用戶提交的信息。 這種設計假定與服務器的交互,總是通過導航到新頁面實現。

但是它們的元素是 DOM 的一部分,就像頁面的其他部分一樣,并且表示表單字段的 DOM 元素,支持許多其他元素上不存在的屬性和事件。 這些使其可以使用 JavaScript 程序檢查和控制這些輸入字段,以及可以執行一些操作,例如向表單添加新功能,或在 JavaScript 應用程序中使用表單和字段作為積木。

一個網頁表單在其<form>標簽中包含若干個輸入字段。HTML 允許多個的不同風格的輸入字段,從簡單的開關選擇框到下拉菜單和進行輸入的字段。本書不會全面的討論每一個輸入字段類型,不過我們會先大概講述一下。

很多字段類型都使用<input>標簽。標簽的type屬性用來選擇字段的種類,下面是一些常用的<input>類型。

  • text:一個單行的文本輸入框。

  • password:和text相同但隱藏了輸入內容。

  • checkbox:一個復選框。

  • 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="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屬性存儲文本。

<textarea>
one
two
three
</textarea>

<select>標簽用來創造一個可以讓用戶從一些提前設定好的選項中進行選擇的字段。

<select>
  <option>Pancakes</option>
  <option>Pudding</option>
  <option>Ice cream</option>
</select>

當一個表單字段中的內容更改時會觸發change事件。

聚焦

不同于 HTML 文檔中的其他元素,表單字段可以獲取鍵盤焦點。當點擊或以某種方式激活時,他們會成為激活的元素,并接受鍵盤的輸入。

因此,只有獲得焦點時,你才能輸入文本字段。 其他字段對鍵盤事件的響應不同。 例如,<select>菜單嘗試移動到包含用戶輸入文本的選項,并通過向上和向下移動其選項來響應箭頭鍵。

我們可以通過使用 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 鍵來切換焦點。通過tabindex屬性可以改變元素接受焦點的順序。后面的例子會讓焦點從文本輸入框跳轉到 OK 按鈕而不是到幫助鏈接。

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

默認情況下,多數的 HTML 元素不能擁有焦點。但是可以通過添加tabindex屬性使任何元素可聚焦。tabindex為 -1 使 TAB 鍵跳過元素,即使它通常是可聚焦的。

禁用字段

所有的表單字段都可以通過其disable屬性來禁用。它是一個可以被指定為沒有值的屬性 - 事實上它出現在所有禁用的元素中。

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

禁用的字段不能擁有焦點或更改,瀏覽器使它們變成灰色。

當一個程序在處理一些由按鍵或其他控制方式出發的事件,并且這些事件可能要求和服務器的通信時,將元素禁用直到動作完成可能是一個很好的方法。按照這用方式,當用戶失去耐心并且再次點擊時,不會意外的重復這一動作。

作為整體的表單

當一個字段被包含在<form>元素中時,其 DOM 元素會有一個form屬性指向form的 DOM 元素。<form>元素則會有一個叫作elements屬性,包含一個類似于數據的集合,其中包含全部的字段。

一個表單字段的name屬性會決定在form提交時其內容的辨別方式。同時在獲取formelements屬性時也可以作為一種屬性名,所以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的按鈕在點擊時,會提交表單。在一個form擁有焦點時,點擊enter鍵也會有同樣的效果。

通常在提交一個表單時,瀏覽器會將頁面導航到formaction屬性指明的頁面,使用GETPOST請求。但是在這些發生之前,"submit"事件會被觸發。這個事件可以由 JavaScript 處理,并且處理器可以通過調用事件對象的preventDefault來禁用默認行為。

<form action="example/submit.html">
  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將其發送到服務器而不重新加載頁面。

文本字段

type屬性為textpassword<input>標簽和textarea標簽組成的字段有相同的接口。其 DOM 元素都有一個value屬性,保存了為字符串格式的當前內容。將這個屬性更改為另一個值將改變字段的內容。

文本字段selectionStartselectEnd屬性包含光標和所選文字的信息。當沒有選中文字時,這兩個屬性的值相同,表明當前光標的信息。例如,0 表示文本的開始,10 表示光標在第十個字符之后。當一部分字段被選中時,這兩個屬性值會不同,表明選中文字開始位置和結束位置。

和正常的值一樣,這些屬性也可以被更改。

想象你正在編寫關于 Knaseknemwy 的文章,但是名字拼寫有一些問題,后續代碼將<textarea>標簽和一個事件處理器關聯起來,當點擊F2時,插入 Knaseknemwy。

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    // The key code for F2 happens to be 113
    if (event.keyCode == 113) {
      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>

提供給querySelectorAll的 CSS 查詢中的方括號用于匹配屬性。 它選擇name屬性為"color"的元素。

選擇字段

選擇字段和單選按鈕比較相似,允許用戶從多個選項中選擇。但是,單選框的展示排版是由我們控制的,而<select>標簽外觀則是由瀏覽器控制。

選擇字段也有一個更類似于復選框列表的變體,而不是單選框。 當賦予multiple屬性時,<select>標簽將允許用戶選擇任意數量的選項,而不僅僅是一個選項。 在大多數瀏覽器中,這會顯示與正常的選擇字段不同的效果,后者通常顯示為下拉控件,僅在你打開它時才顯示選項。

每一個<option>選項會有一個值,這個值可以通過value屬性來定義。如果沒有提供,選項內的文本將作為其值。<select>value屬性反映了當前的選中項。對于一個多選字段,這個屬性用處不太大因為該屬性只會給出一個選中項。

<select>字段的<option>標簽可以通過一個類似于數組對象的options屬性訪問到。每個選項會有一個叫作selected的屬性,來表明這個選項當前是否被選中。這個屬性可以用來被設定選中或不選中。

這個例子會從多選字段中取出選中的數值,并使用這些數值構造一個二進制數字。按住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屬性。有時文本字段可以上傳多個文件,這使得同時選擇多個文件變為可能。

files對象中的對象有name(文件名)、size(文件大小,單位為字節),和type(文件的媒體類型,如text/plainimage/jpeg)等屬性。

files屬性中不包含文件內容的屬性。獲取這個內容會比較復雜。由于從硬盤中讀取文件會需要一些時間,接口必須是異步的,來避免文檔的無響應問題。

<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方法,傳入我們希望讀取的文件,一旦載入完成,readerresult屬性內容就是文件內容。

FileReader對象還會在讀取文件失敗時觸發error事件。錯誤對象本身會存在readererror屬性中。這個接口是在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的數據大小。這種限制,以及用垃圾填滿人們的硬盤并不是真正有利可圖的事實,防止該特性占用太多空間。

下面的代碼實現了一個粗糙的筆記應用。程序將用戶的筆記保存為一個對象,將筆記的標題和內容字符串相關聯。對象被編碼為 JSON 格式并存儲在localStorage中。用戶可以從<select>選擇字段中選擇筆記并在<textarea>中編輯筆記,并可以通過點擊一個按鈕來添加筆記。

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", () => {
    setState({
      notes: Object.assign({}, state.notes,
                           {[state.selected]: note.value}),
      selected: state.selected
    });
  });

  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: Object.assign({}, state.notes, {[name]: ""}),
        selected: name
      });
    });
</script>

腳本從存儲在localStorage中的"Notes"值來獲取它的初始狀態,如果其中沒有值,它會創建示例狀態,僅僅帶有一個購物列表。從localStorage中讀取不存在的字段會返回null

setState方法確保 DOM 顯示給定的狀態,并將新狀態存儲到localStorage。 事件處理器調用這個函數來移動到一個新狀態。

在這個例子中使用Object.assign,是為了創建一個新的對象,它是舊的state.notes的一個克隆,但是添加或覆蓋了一個屬性。 Object.assign選取第一個參數,向其添加所有更多參數的所有屬性。 因此,向它提供一個空對象會使它填充一個新對象。 第三個參數中的方括號表示法,用于創建名稱基于某個動態值的屬性。

還有另一個和localStorage很相似的對象叫作sessionStorage。這兩個對象之間的區別在于sessionStorage的內容會在每次會話結束時丟失,而對于多數瀏覽器來說,會話會在瀏覽器關閉時結束。

本章小結

在本章中,我們討論了 HTTP 協議的工作原理。 客戶端發送一個請求,該請求包含一個方法(通常是GET)和一個標識資源的路徑。 然后服務器決定如何處理請求,并用狀態碼和響應正文進行響應。 請求和響應都可能包含提供附加信息的協議頭。

瀏覽器 JavaScript 可以通過fetch接口生成 HTTP 請求。 像這樣生成請求:

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來禁用默認的提交事件。表單字段的元素不一定需要被包裝在<form>標簽中。

當用戶在一個文件選擇字段中選擇了本機中的一個文件時,可以用FileReader接口來在 JavaScript 中獲取文件內容。

localStoragesessionStorage對象可以用來保存頁面重載后依舊保留的信息。第一個會永久保留數據(直到用戶決定清除),第二個則會保存到瀏覽器關閉時。

習題

內容協商

HTTP 可以做的事情之一就是內容協商。 Accept請求頭用于告訴服務器,客戶端想要獲得什么類型的文檔。 許多服務器忽略這個協議頭,但是當一個服務器知道各種編碼資源的方式時,它可以查看這個協議頭,并發送客戶端首選的格式。

URL eloquentjavascript.net/author配置為響應明文,HTML 或 JSON,具體取決于客戶端要求的內容。 這些格式由標準化的媒體類型"text/plain""text/html""application/json"標識。

發送請求來獲取此資源的所有三種格式。 使用傳遞給fetchoptions對象中的headers屬性,將名為Accept的協議頭設置為所需的媒體類型。

最后,請嘗試請求媒體類型"application/rainbows+unicorns",并查看產生的狀態碼。

// Your code here.

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>

Conway 的生命游戲

Conway 的生命游戲是一個簡單的在網格中模擬生命的游戲,每一個細胞都可以生存或滅亡。對于每一代(回合),都要遵循以下規則:

  • 任何細胞,周圍有少于兩個或多于三個的活著的鄰居,都會死亡。

  • 任意細胞,擁有兩個或三個的活著的鄰居,可以生存到下一代。

  • 任何死去的細胞,周圍有三個活著的鄰居,可以再次復活。

任意一個相連的細胞都可以稱為鄰居,包括對角相連。

注意這些規則要立刻應用于整個網格,而不是一次一個網格。這表明鄰居的數目由開始的一代決定,并且鄰居在每一代時發生的變化不應該影響給定細胞新的狀態。

使用任何一個你認為合適的數據結構來實現這個游戲。使用Math.random來隨機的生成開始狀態。將其展示為一個選擇框組成的網格和一個生成下一代的按鈕。當用戶選中或取消選中一個選擇框時,其變化應該影響下一代的計算。

<div id="grid"></div>
<button id="next">Next generation</button>

<script>
  // Your code here.
</script>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容