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

來源: ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目

原文:HTTP and Formsjavascript

譯者:飛龍html

協議:CC BY-NC-SA 4.0java

自豪地採用谷歌翻譯git

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

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

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

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

協議

當你在瀏覽器地址欄中輸入eloquentjavascript.net/18_http.html時,瀏覽器會首先找到和eloquentjavascript.net相關的服務器的地址,而後嘗試經過 80 端口創建 TCP 鏈接,其中 80 端口是 HTTP 的默認通訊端口。若是該服務器存在而且接受了該鏈接,瀏覽器可能發送以下內容。json

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>
相關文章
相關標籤/搜索