成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript 編程精解 中文第三版 二十一、項(xiàng)目:技能分享網(wǎng)站

remcarpediem / 611人閱讀

摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會(huì)是一個(gè)活動(dòng),其中興趣相同的人聚在一起,針對他們所知的事情進(jìn)行小型非正式的展示。所有接口均以路徑為中心。

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

原文:Project: Skill-Sharing Website

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

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

If you have knowledge, let others light their candles at it.

Margaret Fuller

技能分享會(huì)是一個(gè)活動(dòng),其中興趣相同的人聚在一起,針對他們所知的事情進(jìn)行小型非正式的展示。在園藝技能分享會(huì)上,可以解釋如何耕作芹菜。如果在編程技能分享小組中,你可以順便給每個(gè)人講講 Node.js。

在計(jì)算機(jī)領(lǐng)域中,這類聚會(huì)往往名為用戶小組,是開闊眼界、了解行業(yè)新動(dòng)態(tài)或僅僅接觸興趣相同的人的好方法。許多大城市都會(huì)有 JavaScript 聚會(huì)。這類聚會(huì)往往是可以免費(fèi)參加的,而且我發(fā)現(xiàn)我參加過的那些聚會(huì)都非常友好熱情。

在最后的項(xiàng)目章節(jié)中,我們的目標(biāo)是建立網(wǎng)站,管理特定技能分享會(huì)的討論內(nèi)容。假設(shè)一個(gè)小組的人會(huì)在成員辦公室中定期舉辦關(guān)于獨(dú)輪車的聚會(huì)。上一個(gè)組織者搬到了另一個(gè)城市,并且沒人可以站出來接下來他的任務(wù)。我們需要一個(gè)系統(tǒng),讓參與者可以在系統(tǒng)中發(fā)言并相互討論,這樣就不需要一個(gè)中心組織人員了。

就像上一章一樣,本章中的一些代碼是為 Node.js 編寫的,并且直接在你正在查看的 HTML頁面中運(yùn)行它不太可行。 該項(xiàng)目的完整代碼可以從eloquentjavascript.net/code/skillsharing.zip下載。

設(shè)計(jì)

本項(xiàng)目的服務(wù)器部分為 Node.js 編寫,客戶端部分則為瀏覽器編寫。服務(wù)器存儲(chǔ)系統(tǒng)數(shù)據(jù)并將其提供給客戶端。它也提供實(shí)現(xiàn)客戶端系統(tǒng)的文件。

服務(wù)器保存了為下次聚會(huì)提出的對話列表。每個(gè)對話包括參與人員姓名、標(biāo)題和該對話的相關(guān)評論??蛻舳嗽试S用戶提出新的對話(將對話添加到列表中)、刪除對話和評論已存在的對話。每當(dāng)用戶做了修改時(shí),客戶端會(huì)向服務(wù)器發(fā)送關(guān)于更改的 HTTP 請求。

我們創(chuàng)建應(yīng)用來展示一個(gè)實(shí)時(shí)視圖,來展示目前已經(jīng)提出的對話和評論。每當(dāng)某些人在某些地點(diǎn)提交了新的對話或添加新評論時(shí),所有在瀏覽器中打開頁面的人都應(yīng)該立即看到變化。這個(gè)特性略有挑戰(zhàn),網(wǎng)絡(luò)服務(wù)器無法建立到客戶端的連接,也沒有好方法來知道有哪些客戶端現(xiàn)在在查看特定網(wǎng)站。

該問題的一個(gè)解決方案叫作長時(shí)間輪詢,這恰巧是 Node 的設(shè)計(jì)動(dòng)機(jī)之一。

長輪詢

為了能夠立即提示客戶端某些信息發(fā)生了改變,我們需要建立到客戶端的連接。由于通常瀏覽器無法接受連接,而且客戶端通常在路由后面,它無論如何都會(huì)拒絕這類連接,因此由服務(wù)器初始化連接是不切實(shí)際的。

我們可以安排客戶端來打開連接并保持該連接,因此服務(wù)器可以使用該連接在必要時(shí)傳送信息。

但 HTTP 請求只是簡單的信息流:客戶端發(fā)送請求,服務(wù)器返回一條響應(yīng),就是這樣。有一種名為 WebSocket 的技術(shù),受到現(xiàn)代瀏覽器的支持,是的我們可以建立連接并進(jìn)行任意的數(shù)據(jù)交換。但如何正確運(yùn)用這項(xiàng)技術(shù)是較為復(fù)雜的。

本章我們將會(huì)使用一種相對簡單的技術(shù):長輪詢(Long Polling)??蛻舳藭?huì)連續(xù)使用定時(shí)的 HTTP 請求向服務(wù)器詢問新信息,而當(dāng)沒有新信息需要報(bào)告時(shí)服務(wù)器會(huì)簡單地推遲響應(yīng)。

只要客戶端確保其可以持續(xù)不斷地建立輪詢請求,就可以在信息可用之后,從服務(wù)器快速地接收到信息。例如,若 Fatma 在瀏覽器中打開了技能分享程序,瀏覽器會(huì)發(fā)送請求詢問是否有更新,且等待請求的響應(yīng)。當(dāng) Iman 在自己的瀏覽器中提交了關(guān)于“極限降滑獨(dú)輪車”的對話之后。服務(wù)器發(fā)現(xiàn) Fatma 在等待更新請求,并將新的對話作為響應(yīng)發(fā)送給待處理的請求。Fatma 的瀏覽器將會(huì)接收到數(shù)據(jù)并更新屏幕展示對話內(nèi)容。

為了防止連接超時(shí)(因?yàn)檫B接一定時(shí)間不活躍后會(huì)被中斷),長輪詢技術(shù)常常為每個(gè)請求設(shè)置一個(gè)最大等待時(shí)間,只要超過了這個(gè)時(shí)間,即使沒人有任何需要報(bào)告的信息也會(huì)返回響應(yīng),在此之后,客戶端會(huì)建立一個(gè)新的請求。定期重新發(fā)送請求也使得這種技術(shù)更具魯棒性,允許客戶端從臨時(shí)的連接失敗或服務(wù)器問題中恢復(fù)。

使用了長輪詢技術(shù)的繁忙的服務(wù)器,可以有成百上千個(gè)等待的請求,因此也就有這么多個(gè) TCP 連接處于打開狀態(tài)。Node簡化了多連接的管理工作,而不是建立多帶帶線程來控制每個(gè)連接,這對這樣的系統(tǒng)是非常合適的。

HTTP 接口

在我們設(shè)計(jì)服務(wù)器或客戶端的代碼之前,讓我們先來思考一下兩者均會(huì)涉及的一點(diǎn):雙方通信的 HTTP 接口。

我們會(huì)使用 JSON 作為請求和響應(yīng)正文的格式,就像第二十章中的文件服務(wù)器一樣,我們嘗試充分利用 HTTP 方法。所有接口均以/talks路徑為中心。不以/talks開頭的路徑則用于提供靜態(tài)文件服務(wù),即用于實(shí)現(xiàn)客戶端系統(tǒng)的 HTML 和 JavaScript 代碼。

訪問/talksGET請求會(huì)返回如下所示的 JSON 文檔。

[{"title": "Unituning",
  "presenter": "Jamal",
  "summary": "Modifying your cycle for extra style",
  "comment": []}]

我們可以發(fā)送PUT請求到類似于/talks/Unituning之類的 URL 上來創(chuàng)建新對話,在第二個(gè)斜杠后的那部分是對話的名稱。PUT請求正文應(yīng)當(dāng)包含一個(gè) JSON 對象,其中有一個(gè)presenter屬性和一個(gè)summary屬性。

因?yàn)閷υ挊?biāo)題可以包含空格和其他無法正常出現(xiàn)在 URL 中的字符,因此我們必須使用encodeURIComponent函數(shù)來編碼標(biāo)題字符串,并構(gòu)建 URL。

console.log("/talks/" + encodeURIComponent("How to Idle"));
// → /talks/How%20to%20Idle

下面這個(gè)請求用于創(chuàng)建關(guān)于“空轉(zhuǎn)”的對話。

PUT /talks/How%20to%20Idle HTTP/1.1
Content-Type: application/json
Content-Length: 92

{"presenter": "Maureen",
 "summary": "Standing still on a unicycle"}

我們也可以使用GET請求通過這些 URL 獲取對話的 JSON 數(shù)據(jù),或使用DELETE請求通過這些 URL 刪除對話。

為了在對話中添加一條評論,可以向諸如/talks/Unituning/comments的 URL 發(fā)送POST請求,JSON 正文包含author屬性和message屬性。

POST /talks/Unituning/comments HTTP/1.1
Content-Type: application/json
Content-Length: 72

{"author": "Iman",
 "message": "Will you talk about raising a cycle?"}

為了支持長輪詢,如果沒有新的信息可用,發(fā)送到/talksGET請求可能會(huì)包含額外的標(biāo)題,通知服務(wù)器延遲響應(yīng)。 我們將使用通常用于管理緩存的一對協(xié)議頭:ETagIf-None-Match

服務(wù)器可能在響應(yīng)中包含ETag(“實(shí)體標(biāo)簽”)協(xié)議頭。 它的值是標(biāo)識資源當(dāng)前版本的字符串。 當(dāng)客戶稍后再次請求該資源時(shí),可以通過包含一個(gè)If-None-Match頭來進(jìn)行條件請求,該頭的值保存相同的字符串。 如果資源沒有改變,服務(wù)器將響應(yīng)狀態(tài)碼 304,這意味著“未修改”,告訴客戶端它的緩存版本仍然是最新的。 當(dāng)標(biāo)簽與服務(wù)器不匹配時(shí),服務(wù)器正常響應(yīng)。

我們需要這樣的東西,通過它客戶端可以告訴服務(wù)器它有哪個(gè)版本的對話列表,僅當(dāng)列表發(fā)生變化時(shí),服務(wù)器才會(huì)響應(yīng)。 但服務(wù)器不是立即返回 304 響應(yīng),它應(yīng)該停止響應(yīng),并且僅當(dāng)有新東西的可用,或已經(jīng)過去了給定的時(shí)間時(shí)才返回。 為了將長輪詢請求與常規(guī)條件請求區(qū)分開來,我們給他們另一個(gè)標(biāo)頭Prefer: wait=90,告訴服務(wù)器客戶端最多等待 90 秒的響應(yīng)。

服務(wù)器將保留版本號,每次對話更改時(shí)更新,并將其用作ETag值。 客戶端可以在對話變更時(shí)通知此類要求:

GET /talks HTTP/1.1
If-None-Match: "4"
Prefer: wait=90

(time passes)

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "5"
Content-Length: 295

[....]

這里描述的協(xié)議并沒有任何訪問控制。每個(gè)人都可以評論、修改對話或刪除對話。因?yàn)橐蛱鼐W(wǎng)中充滿了流氓,因此將這類沒有進(jìn)一步保護(hù)的系統(tǒng)放在網(wǎng)絡(luò)上最后可能并不是很好。

服務(wù)器

讓我們開始構(gòu)建程序的服務(wù)器部分。本節(jié)的代碼可以在 Node.js 中執(zhí)行。

路由

我們的服務(wù)器會(huì)使用createServer來啟動(dòng) HTTP 服務(wù)器。在處理新請求的函數(shù)中,我們必須區(qū)分我們支持的請求的類型(根據(jù)方法和路徑確定)。我們可以使用一長串的if語句完成該任務(wù),但還存在一種更優(yōu)雅的方式。

路由可以作為幫助把請求調(diào)度傳給能處理該請求的函數(shù)。路徑匹配正則表達(dá)式/^/talks/([^/]+)$//talks/帶著對話名稱)的PUT請求,應(yīng)當(dāng)由指定函數(shù)處理。此外,路由可以幫助我們提取路徑中有意義的部分,在本例中會(huì)將對話的標(biāo)題(包裹在正則表達(dá)式的括號之中)傳遞給處理器函數(shù)。

在 NPM 中有許多優(yōu)秀的路由包,但這里我們自己編寫一個(gè)路由來展示其原理。

這里給出router.js,我們隨后將在服務(wù)器模塊中使用require獲取該模塊。

const {parse} = require("url");

module.exports = class Router {
  constructor() {
    this.routes = [];
  }
  add(method, url, handler) {
    this.routes.push({method, url, handler});
  }
  resolve(context, request) {
    let path = parse(request.url).pathname;

    for (let {method, url, handler} of this.routes) {
      let match = url.exec(path);
      if (!match || request.method != method) continue;
      let urlParts = match.slice(1).map(decodeURIComponent);
      return handler(context, ...urlParts, request);
    }
    return null;
  }
};

該模塊導(dǎo)出Router類。我們可以使用路由對象的add方法來注冊一個(gè)新的處理器,并使用resolve方法解析請求。

找到處理器之后,后者會(huì)返回一個(gè)響應(yīng),否則為null。它會(huì)逐個(gè)嘗試路由(根據(jù)定義順序排序),當(dāng)找到一個(gè)匹配的路由時(shí)返回true

路由會(huì)使用context值調(diào)用處理器函數(shù)(這里是服務(wù)器實(shí)例),將請求對象中的字符串,與已定義分組中的正則表達(dá)式匹配。傳遞給處理器的字符串必須進(jìn)行 URL 解碼,因?yàn)樵?URL 中可能包含%20風(fēng)格的代碼。

文件服務(wù)

當(dāng)請求無法匹配路由中定義的任何請求類型時(shí),服務(wù)器必須將其解釋為請求位于public目錄下的某個(gè)文件。服務(wù)器可以使用第二十章中定義的文件服務(wù)器來提供文件服務(wù),但我們并不需要也不想對文件支持 PUT 和 DELETE 請求,且我們想支持類似于緩存等高級特性。因此讓我們使用 NPM 中更為可靠且經(jīng)過充分測試的靜態(tài)文件服務(wù)器。

我選擇了ecstatic。它并不是 NPM 中唯一的此類服務(wù),但它能夠完美工作且符合我們的意圖。ecstatic模塊導(dǎo)出了一個(gè)函數(shù),我們可以調(diào)用該函數(shù),并傳遞一個(gè)配置對象來生成一個(gè)請求處理函數(shù)。我們使用root選項(xiàng)告知服務(wù)器文件搜索位置。

const {createServer} = require("http");
const Router = require("./router");
const ecstatic = require("ecstatic");

const router = new Router();
const defaultHeaders = {"Content-Type": "text/plain"};

class SkillShareServer {
  constructor(talks) {
    this.talks = talks;
    this.version = 0;
    this.waiting = [];

    let fileServer = ecstatic({root: "./public"});
    this.server = createServer((request, response) => {
      let resolved = router.resolve(this, request);
      if (resolved) {
        resolved.catch(error => {
          if (error.status != null) return error;
          return {body: String(error), status: 500};
        }).then(({body,
                  status = 200,
                  headers = defaultHeaders}) => {
          response.writeHead(status, headers);
          response.end(body);
        });
      } else {
        fileServer(request, response);
      }
    });
  }
  start(port) {
    this.server.listen(port);
  }
  stop() {
    this.server.close();
  }
}

它使用上一章中的文件服務(wù)器的類似約定來處理響應(yīng) - 處理器返回Promise,可解析為描述響應(yīng)的對象。 它將服務(wù)器包裝在一個(gè)對象中,它也維護(hù)它的狀態(tài)。

作為資源的對話

已提出的對話存儲(chǔ)在服務(wù)器的talks屬性中,這是一個(gè)對象,屬性名稱是對話標(biāo)題。這些對話會(huì)展現(xiàn)為/talks/[title]下的 HTTP 資源,因此我們需要將處理器添加我們的路由中供客戶端選擇,來實(shí)現(xiàn)不同的方法。

獲取(GET)單個(gè)對話的請求處理器,必須查找對話并使用對話的 JSON 數(shù)據(jù)作為響應(yīng),若不存在則返回 404 錯(cuò)誤響應(yīng)碼。

const talkPath = /^/talks/([^/]+)$/;

router.add("GET", talkPath, async (server, title) => {
  if (title in server.talks) {
    return {body: JSON.stringify(server.talks[title]),
            headers: {"Content-Type": "application/json"}};
  } else {
    return {status: 404, body: `No talk "${title}" found`};
  }
});

刪除對話時(shí),將其從talks對象中刪除即可。

router.add("DELETE", talkPath, async (server, title) => {
  if (title in server.talks) {
    delete server.talks[title];
    server.updated();
  }
  return {status: 204};
});

我們將在稍后定義updated方法,它通知等待有關(guān)更改的長輪詢請求。

為了獲取請求正文的內(nèi)容,我們定義一個(gè)名為readStream的函數(shù),從可讀流中讀取所有內(nèi)容,并返回解析為字符串的Promise

function readStream(stream) {
  return new Promise((resolve, reject) => {
    let data = "";
    stream.on("error", reject);
    stream.on("data", chunk => data += chunk.toString());
    stream.on("end", () => resolve(data));
  });
}

需要讀取響應(yīng)正文的函數(shù)是PUT的處理器,用戶使用它創(chuàng)建新對話。該函數(shù)需要檢查數(shù)據(jù)中是否有presentersummary屬性,這些屬性都是字符串。任何來自外部的數(shù)據(jù)都可能是無意義的,我們不希望錯(cuò)誤請求到達(dá)時(shí)會(huì)破壞我們的內(nèi)部數(shù)據(jù)模型,或者導(dǎo)致服務(wù)崩潰。

若數(shù)據(jù)看起來合法,處理器會(huì)將對話轉(zhuǎn)化為對象,存儲(chǔ)在talks對象中,如果有標(biāo)題相同的對話存在則覆蓋,并再次調(diào)用updated。

router.add("PUT", talkPath,
           async (server, title, request) => {
  let requestBody = await readStream(request);
  let talk;
  try { talk = JSON.parse(requestBody); }
  catch (_) { return {status: 400, body: "Invalid JSON"}; }

  if (!talk ||
      typeof talk.presenter != "string" ||
      typeof talk.summary != "string") {
    return {status: 400, body: "Bad talk data"};
  }
  server.talks[title] = {title,
                         presenter: talk.presenter,
                         summary: talk.summary,
                         comments: []};
  server.updated();
  return {status: 204};
});

在對話中添加評論也是類似的。我們使用readStream來獲取請求內(nèi)容,驗(yàn)證請求數(shù)據(jù),若看上去合法,則將其存儲(chǔ)為評論。

router.add("POST", /^/talks/([^/]+)/comments$/,
           async (server, title, request) => {
  let requestBody = await readStream(request);
  let comment;
  try { comment = JSON.parse(requestBody); }
  catch (_) { return {status: 400, body: "Invalid JSON"}; }

  if (!comment ||
      typeof comment.author != "string" ||
      typeof comment.message != "string") {
    return {status: 400, body: "Bad comment data"};
  } else if (title in server.talks) {
    server.talks[title].comments.push(comment);
    server.updated();
    return {status: 204};
  } else {
    return {status: 404, body: `No talk "${title}" found`};
  }
});

嘗試向不存在的對話中添加評論會(huì)返回 404 錯(cuò)誤。

長輪詢支持

服務(wù)器中最值得探討的方面是處理長輪詢的部分代碼。當(dāng) URL 為/talksGET請求到來時(shí),它可能是一個(gè)常規(guī)請求或一個(gè)長輪詢請求。

我們可能在很多地方,將對話列表發(fā)送給客戶端,因此我們首先定義一個(gè)簡單的輔助函數(shù),它構(gòu)建這樣一個(gè)數(shù)組,并在響應(yīng)中包含ETag協(xié)議頭。

SkillShareServer.prototype.talkResponse = function() {
  let talks = [];
  for (let title of Object.keys(this.talks)) {
    talks.push(this.talks[title]);
  }
  return {
    body: JSON.stringify(talks),
    headers: {"Content-Type": "application/json",
              "ETag": `"${this.version}"`}
  };
};

處理器本身需要查看請求頭,來查看是否存在If-None-MatchPrefer標(biāo)頭。 Node 在其小寫名稱下存儲(chǔ)協(xié)議頭,根據(jù)規(guī)定其名稱是不區(qū)分大小寫的。

router.add("GET", /^/talks$/, async (server, request) => {
  let tag = /"(.*)"/.exec(request.headers["if-none-match"]);
  let wait = /wait=(d+)/.exec(request.headers["prefer"]);
  if (!tag || tag[1] != server.version) {
    return server.talkResponse();
  } else if (!wait) {
    return {status: 304};
  } else {
    return server.waitForChanges(Number(wait[1]));
  }
});

如果沒有給出標(biāo)簽,或者給出的標(biāo)簽與服務(wù)器的當(dāng)前版本不匹配,則處理器使用對話列表來響應(yīng)。 如果請求是有條件的,并且對話沒有變化,我們查閱Prefer標(biāo)題來查看,是否應(yīng)該延遲響應(yīng)或立即響應(yīng)。

用于延遲請求的回調(diào)函數(shù)存儲(chǔ)在服務(wù)器的waiting數(shù)組中,以便在發(fā)生事件時(shí)通知它們。 waitForChanges方法也會(huì)立即設(shè)置一個(gè)定時(shí)器,當(dāng)請求等待了足夠長時(shí),以 304 狀態(tài)來響應(yīng)。

SkillShareServer.prototype.waitForChanges = function(time) {
  return new Promise(resolve => {
    this.waiting.push(resolve);
    setTimeout(() => {
      if (!this.waiting.includes(resolve)) return;
      this.waiting = this.waiting.filter(r => r != resolve);
      resolve({status: 304});
    }, time * 1000);
  });
};

使用updated注冊一個(gè)更改,會(huì)增加version屬性并喚醒所有等待的請求。

var changes = [];

SkillShareServer.prototype.updated = function() {
  this.version++;
  let response = this.talkResponse();
  this.waiting.forEach(resolve => resolve(response));
  this.waiting = [];
};

服務(wù)器代碼這樣就完成了。 如果我們創(chuàng)建一個(gè)SkillShareServer的實(shí)例,并在端口 8000 上啟動(dòng)它,那么生成的 HTTP 服務(wù)器,將服務(wù)于public子目錄中的文件,以及/ talksURL 下的一個(gè)對話管理界面。

new SkillShareServer(Object.create(null)).start(8000);
客戶端

技能分享網(wǎng)站的客戶端部分由三個(gè)文件組成:微型 HTML 頁面、樣式表以及 JavaScript 文件。

HTML

在網(wǎng)絡(luò)服務(wù)器提供文件服務(wù)時(shí),有一種廣為使用的約定是:當(dāng)請求直接訪問與目錄對應(yīng)的路徑時(shí),返回名為index.html的文件。我們使用的文件服務(wù)模塊ecstatic就支持這種約定。當(dāng)請求路徑為/時(shí),服務(wù)器會(huì)搜索文件./public/index.html./public是我們賦予的根目錄),若文件存在則返回文件。

因此,若我們希望瀏覽器指向我們服務(wù)器時(shí)展示某個(gè)特定頁面,我們將其放在public/index.html中。這就是我們的index文件。



Skill Sharing


Skill Sharing

它定義了文檔標(biāo)題并包含一個(gè)樣式表,除了其它東西,它定義了幾種樣式,確保對話之間有一定的空間。

最后,它在頁面頂部添加標(biāo)題,并加載包含客戶端應(yīng)用的腳本。

動(dòng)作

應(yīng)用狀態(tài)由對話列表和用戶名稱組成,我們將它存儲(chǔ)在一個(gè){talks, user}對象中。 我們不允許用戶界面直接操作狀態(tài)或發(fā)送 HTTP 請求。 反之,它可能會(huì)觸發(fā)動(dòng)作,它描述用戶正在嘗試做什么。

function handleAction(state, action) {
  if (action.type == "setUser") {
    localStorage.setItem("userName", action.user);
    return Object.assign({}, state, {user: action.user});
  } else if (action.type == "setTalks") {
    return Object.assign({}, state, {talks: action.talks});
  } else if (action.type == "newTalk") {
    fetchOK(talkURL(action.title), {
      method: "PUT",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({
        presenter: state.user,
        summary: action.summary
      })
    }).catch(reportError);
  } else if (action.type == "deleteTalk") {
    fetchOK(talkURL(action.talk), {method: "DELETE"})
      .catch(reportError);
  } else if (action.type == "newComment") {
    fetchOK(talkURL(action.talk) + "/comments", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({
        author: state.user,
        message: action.message
      })
    }).catch(reportError);
  }
  return state;
}

我們將用戶的名字存儲(chǔ)在localStorage中,以便在頁面加載時(shí)恢復(fù)。

需要涉及服務(wù)器的操作使用fetch,將網(wǎng)絡(luò)請求發(fā)送到前面描述的 HTTP 接口。 我們使用包裝函數(shù)fetchOK,它確保當(dāng)服務(wù)器返回錯(cuò)誤代碼時(shí),拒絕返回的Promise。

function fetchOK(url, options) {
  return fetch(url, options).then(response => {
    if (response.status < 400) return response;
    else throw new Error(response.statusText);
  });
}

這個(gè)輔助函數(shù)用于為某個(gè)對話,使用給定標(biāo)題建立 URL。

function talkURL(title) {
  return "talks/" + encodeURIComponent(title);
}

當(dāng)請求失敗時(shí),我們不希望我們的頁面絲毫不變,不給予任何提示。因此我們定義一個(gè)函數(shù),名為reportError,至少在發(fā)生錯(cuò)誤時(shí)向用戶展示一個(gè)對話框。

function reportError(error) {
  alert(String(error));
}
渲染組件

我們將使用一個(gè)方法,類似于我們在第十九章中所見,將應(yīng)用拆分為組件。 但由于某些組件不需要更新,或者在更新時(shí)總是完全重新繪制,所以我們不將它們定義為類,而是直接返回 DOM 節(jié)點(diǎn)的函數(shù)。 例如,下面是一個(gè)組件,顯示用戶可以向它輸入名稱的字段的:

function renderUserField(name, dispatch) {
  return elt("label", {}, "Your name: ", elt("input", {
    type: "text",
    value: name,
    onchange(event) {
      dispatch({type: "setUser", user: event.target.value});
    }
  }));
}

用于構(gòu)建 DOM 元素的elt函數(shù)是我們在第十九章中使用的函數(shù)。

類似的函數(shù)用于渲染對話,包括評論列表和添加新評論的表單。

function renderTalk(talk, dispatch) {
  return elt(
    "section", {className: "talk"},
    elt("h2", null, talk.title, " ", elt("button", {
      type: "button",
      onclick() {
        dispatch({type: "deleteTalk", talk: talk.title});
      }
    }, "Delete")),
    elt("div", null, "by ",
        elt("strong", null, talk.presenter)),
    elt("p", null, talk.summary),
    ...talk.comments.map(renderComment),
    elt("form", {
      onsubmit(event) {
        event.preventDefault();
        let form = event.target;
        dispatch({type: "newComment",
                  talk: talk.title,
                  message: form.elements.comment.value});
        form.reset();
      }
    }, elt("input", {type: "text", name: "comment"}), " ",
       elt("button", {type: "submit"}, "Add comment")));
}

submit事件處理器調(diào)用form.reset,在創(chuàng)建"newComment"動(dòng)作后清除表單的內(nèi)容。

在創(chuàng)建適度復(fù)雜的 DOM 片段時(shí),這種編程風(fēng)格開始顯得相當(dāng)混亂。 有一個(gè)廣泛使用的(非標(biāo)準(zhǔn)的)JavaScript 擴(kuò)展叫做 JSX,它允許你直接在你的腳本中編寫 HTML,這可以使這樣的代碼更漂亮(取決于你認(rèn)為漂亮是什么)。 在實(shí)際運(yùn)行這種代碼之前,必須在腳本上運(yùn)行一個(gè)程序,將偽 HTML 轉(zhuǎn)換為 JavaScript 函數(shù)調(diào)用,就像我們在這里用的東西。

評論更容易渲染。

function renderComment(comment) {
  return elt("p", {className: "comment"},
             elt("strong", null, comment.author),
             ": ", comment.message);
}

最后,用戶可以使用表單創(chuàng)建新對話,它渲染為這樣。

function renderTalkForm(dispatch) {
  let title = elt("input", {type: "text"});
  let summary = elt("input", {type: "text"});
  return elt("form", {
    onsubmit(event) {
      event.preventDefault();
      dispatch({type: "newTalk",
                title: title.value,
                summary: summary.value});
      event.target.reset();
    }
  }, elt("h3", null, "Submit a Talk"),
     elt("label", null, "Title: ", title),
     elt("label", null, "Summary: ", summary),
     elt("button", {type: "submit"}, "Submit"));
}
輪詢

為了啟動(dòng)應(yīng)用,我們需要對話的當(dāng)前列表。 由于初始加載與長輪詢過程密切相關(guān) -- 輪詢時(shí)必須使用來自加載的ETag -- 我們將編寫一個(gè)函數(shù)來不斷輪詢服務(wù)器的/ talks,并且在新的對話集可用時(shí),調(diào)用回調(diào)函數(shù)。

async function pollTalks(update) {
  let tag = undefined;
  for (;;) {
    let response;
    try {
      response = await fetchOK("/talks", {
        headers: tag && {"If-None-Match": tag,
                         "Prefer": "wait=90"}
      });
    } catch (e) {
      console.log("Request failed: " + e);
      await new Promise(resolve => setTimeout(resolve, 500));
      continue;
    }
    if (response.status == 304) continue;
    tag = response.headers.get("ETag");
    update(await response.json());
  }
}

這是一個(gè)async函數(shù),因此循環(huán)和等待請求更容易。 它運(yùn)行一個(gè)無限循環(huán),每次迭代中,通常檢索對話列表?;蛘?,如果這不是第一個(gè)請求,則帶有使其成為長輪詢請求的協(xié)議頭。

當(dāng)請求失敗時(shí),函數(shù)會(huì)等待一會(huì)兒,然后再次嘗試。 這樣,如果你的網(wǎng)絡(luò)連接斷了一段時(shí)間然后又恢復(fù),應(yīng)用可以恢復(fù)并繼續(xù)更新。 通過setTimeout解析的Promise,是強(qiáng)制async函數(shù)等待的方法。

當(dāng)服務(wù)器回復(fù) 304 響應(yīng)時(shí),這意味著長輪詢請求超時(shí),所以函數(shù)應(yīng)該立即啟動(dòng)下一個(gè)請求。 如果響應(yīng)是普通的 200 響應(yīng),它的正文將當(dāng)做 JSON 而讀取并傳遞給回調(diào)函數(shù),并且它的ETag協(xié)議頭的值為下一次迭代而存儲(chǔ)。

應(yīng)用

以下組件將整個(gè)用戶界面結(jié)合在一起。

class SkillShareApp {
  constructor(state, dispatch) {
    this.dispatch = dispatch;
    this.talkDOM = elt("div", {className: "talks"});
    this.dom = elt("div", null,
                   renderUserField(state.user, dispatch),
                   this.talkDOM,
                   renderTalkForm(dispatch));
    this.setState(state);
  }

  setState(state) {
    if (state.talks != this.talks) {
      this.talkDOM.textContent = "";
      for (let talk of state.talks) {
        this.talkDOM.appendChild(
          renderTalk(talk, this.dispatch));
      }
      this.talks = state.talks;
    }
  }
}

當(dāng)對話改變時(shí),這個(gè)組件重新繪制所有這些組件。 這很簡單,但也是浪費(fèi)。 我們將在練習(xí)中回顧一下。

我們可以像這樣啟動(dòng)應(yīng)用:

function runApp() {
  let user = localStorage.getItem("userName") || "Anon";
  let state, app;
  function dispatch(action) {
    state = handleAction(state, action);
    app.setState(state);
  }

  pollTalks(talks => {
    if (!app) {
      state = {user, talks};
      app = new SkillShareApp(state, dispatch);
      document.body.appendChild(app.dom);
    } else {
      dispatch({type: "setTalks", talks});
    }
  }).catch(reportError);
}

runApp();

若你執(zhí)行服務(wù)器并同時(shí)為localhost:8000/打開兩個(gè)瀏覽器窗口,你可以看到在一個(gè)窗口中執(zhí)行動(dòng)作時(shí),另一個(gè)窗口中會(huì)立即做出反應(yīng)。

習(xí)題

下面的習(xí)題涉及修改本章中定義的系統(tǒng)。為了使用該系統(tǒng)進(jìn)行工作,請確保首先下載代碼,安裝了 Node,并使用npm install安裝了項(xiàng)目的所有依賴。

磁盤持久化

技能分享服務(wù)只將數(shù)據(jù)存儲(chǔ)在內(nèi)存中。這就意味著當(dāng)服務(wù)崩潰或以為任何原因重啟時(shí),所有的對話和評論都會(huì)丟失。

擴(kuò)展服務(wù)使得其將對話數(shù)據(jù)存儲(chǔ)到磁盤上,并在程序重啟時(shí)自動(dòng)重新加載數(shù)據(jù)。不要擔(dān)心效率,只要用最簡單的代碼讓其可以工作即可。

重置評論字段

由于我們常常無法在 DOM 節(jié)點(diǎn)中找到唯一替換的位置,因此整批地重繪對話是個(gè)很好的工作機(jī)制。但這里有個(gè)例外,若你開始在對話的評論字段中輸入一些文字,而在另一個(gè)窗口向同一條對話添加了一條評論,那么第一個(gè)窗口中的字段就會(huì)被重繪,會(huì)移除掉其內(nèi)容和焦點(diǎn)。

在激烈的討論中,多人同時(shí)添加評論,這將是非常煩人的。 你能想出辦法解決它嗎?

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/105071.html

相關(guān)文章

  • JavaScript 編程精解 中文三版 二十、Node.js

    摘要:在這樣的程序中,異步編程通常是有幫助的。最初是為了使異步編程簡單方便而設(shè)計(jì)的。在年設(shè)計(jì)時(shí),人們已經(jīng)在瀏覽器中進(jìn)行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風(fēng)格。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Node.js 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)...

    qqlcbb 評論0 收藏0
  • JavaScript 編程精解 中文三版 十三、瀏覽器中的 JavaScript

    摘要:在本例中,使用屬性指定鏈接的目標(biāo),其中表示超文本鏈接。您應(yīng)該認(rèn)為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒有實(shí)際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...

    zhiwei 評論0 收藏0
  • JavaScript 編程精解 中文三版 十九、項(xiàng)目:像素藝術(shù)編輯器

    摘要:相反,當(dāng)響應(yīng)指針事件時(shí),它會(huì)調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分?;卣{(diào)函數(shù)可能會(huì)返回另一個(gè)回調(diào)函數(shù),以便在按下按鈕并且將指針移動(dòng)到另一個(gè)像素時(shí)得到通知。它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Pixel Art Editor 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...

    Meils 評論0 收藏0
  • JavaScript 編程精解 中文三版 零、前言

    摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲(chǔ)在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...

    sanyang 評論0 收藏0
  • JavaScript 編程精解 中文三版 二、程序結(jié)構(gòu)

    摘要:為了運(yùn)行包裹的程序,可以將這些值應(yīng)用于它們。在瀏覽器中,輸出出現(xiàn)在控制臺中。在英文版頁面上運(yùn)行示例或自己的代碼時(shí),會(huì)在示例之后顯示輸出,而不是在瀏覽器的控制臺中顯示。這被稱為條件執(zhí)行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Program Structure 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...

    ThinkSNS 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<