JavaScript 編程精解 中文第三版 二11、項目:技能分享網站

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

原文:Project: Skill-Sharing Websitejavascript

譯者:飛龍css

協議:CC BY-NC-SA 4.0html

自豪地採用谷歌翻譯java

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

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

Margaret Fullergithub

技能分享會是一個活動,其中興趣相同的人聚在一塊兒,針對他們所知的事情進行小型非正式的展現。在園藝技能分享會上,能夠解釋如何耕做芹菜。若是在編程技能分享小組中,你能夠順便給每一個人講講 Node.js。正則表達式

在計算機領域中,這類聚會每每名爲用戶小組,是開闊眼界、瞭解行業新動態或僅僅接觸興趣相同的人的好方法。許多大城市都會有 JavaScript 聚會。這類聚會每每是能夠免費參加的,並且我發現我參加過的那些聚會都很是友好熱情。apache

在最後的項目章節中,咱們的目標是創建網站,管理特定技能分享會的討論內容。假設一個小組的人會在成員辦公室中按期舉辦關於獨輪車的聚會。上一個組織者搬到了另外一個城市,而且沒人能夠站出來接下來他的任務。咱們須要一個系統,讓參與者能夠在系統中發言並相互討論,這樣就不須要一箇中心組織人員了。npm

就像上一章同樣,本章中的一些代碼是爲 Node.js 編寫的,而且直接在你正在查看的 HTML頁面中運行它不太可行。 該項目的完整代碼能夠從eloquentjavascript.net/code/skillsharing.zip下載。

設計

本項目的服務器部分爲 Node.js 編寫,客戶端部分則爲瀏覽器編寫。服務器存儲系統數據並將其提供給客戶端。它也提供實現客戶端系統的文件。

服務器保存了爲下次聚會提出的對話列表。每一個對話包括參與人員姓名、標題和該對話的相關評論。客戶端容許用戶提出新的對話(將對話添加到列表中)、刪除對話和評論已存在的對話。每當用戶作了修改時,客戶端會向服務器發送關於更改的 HTTP 請求。

咱們建立應用來展現一個實時視圖,來展現目前已經提出的對話和評論。每當某些人在某些地點提交了新的對話或添加新評論時,全部在瀏覽器中打開頁面的人都應該當即看到變化。這個特性略有挑戰,網絡服務器沒法創建到客戶端的鏈接,也沒有好方法來知道有哪些客戶端如今在查看特定網站。

該問題的一個解決方案叫做長時間輪詢,這恰巧是 Node 的設計動機之一。

長輪詢

爲了可以當即提示客戶端某些信息發生了改變,咱們須要創建到客戶端的鏈接。因爲一般瀏覽器沒法接受鏈接,並且客戶端一般在路由後面,它不管如何都會拒絕這類鏈接,所以由服務器初始化鏈接是不切實際的。

咱們能夠安排客戶端來打開鏈接並保持該鏈接,所以服務器可使用該鏈接在必要時傳送信息。

但 HTTP 請求只是簡單的信息流:客戶端發送請求,服務器返回一條響應,就是這樣。有一種名爲 WebSocket 的技術,受到現代瀏覽器的支持,是的咱們能夠創建鏈接並進行任意的數據交換。但如何正確運用這項技術是較爲複雜的。

本章咱們將會使用一種相對簡單的技術:長輪詢(Long Polling)。客戶端會連續使用定時的 HTTP 請求向服務器詢問新信息,而當沒有新信息須要報告時服務器會簡單地推遲響應。

只要客戶端確保其能夠持續不斷地創建輪詢請求,就能夠在信息可用以後,從服務器快速地接收到信息。例如,若 Fatma 在瀏覽器中打開了技能分享程序,瀏覽器會發送請求詢問是否有更新,且等待請求的響應。當 Iman 在本身的瀏覽器中提交了關於「極限降滑獨輪車」的對話以後。服務器發現 Fatma 在等待更新請求,並將新的對話做爲響應發送給待處理的請求。Fatma 的瀏覽器將會接收到數據並更新屏幕展現對話內容。

爲了防止鏈接超時(由於鏈接必定時間不活躍後會被中斷),長輪詢技術經常爲每一個請求設置一個最大等待時間,只要超過了這個時間,即便沒人有任何須要報告的信息也會返回響應,在此以後,客戶端會創建一個新的請求。按期從新發送請求也使得這種技術更具魯棒性,容許客戶端從臨時的鏈接失敗或服務器問題中恢復。

使用了長輪詢技術的繁忙的服務器,能夠有成百上千個等待的請求,所以也就有這麼多個 TCP 鏈接處於打開狀態。Node簡化了多鏈接的管理工做,而不是創建單獨線程來控制每一個鏈接,這對這樣的系統是很是合適的。

HTTP 接口

在咱們設計服務器或客戶端的代碼以前,讓咱們先來思考一下二者均會涉及的一點:雙方通訊的 HTTP 接口。

咱們會使用 JSON 做爲請求和響應正文的格式,就像第二十章中的文件服務器同樣,咱們嘗試充分利用 HTTP 方法。全部接口均以/talks路徑爲中心。不以/talks開頭的路徑則用於提供靜態文件服務,即用於實現客戶端系統的 HTML 和 JavaScript 代碼。

訪問/talksGET請求會返回以下所示的 JSON 文檔。

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

咱們能夠發送PUT請求到相似於/talks/Unituning之類的 URL 上來建立新對話,在第二個斜槓後的那部分是對話的名稱。PUT請求正文應當包含一個 JSON 對象,其中有一個presenter屬性和一個summary屬性。

由於對話標題能夠包含空格和其餘沒法正常出如今 URL 中的字符,所以咱們必須使用encodeURIComponent函數來編碼標題字符串,並構建 URL。

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

下面這個請求用於建立關於「空轉」的對話。

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 數據,或使用DELETE請求經過這些 URL 刪除對話。

爲了在對話中添加一條評論,能夠向諸如/talks/Unituning/comments的 URL 發送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?"}

爲了支持長輪詢,若是沒有新的信息可用,發送到/talksGET請求可能會包含額外的標題,通知服務器延遲響應。 咱們將使用一般用於管理緩存的一對協議頭:ETagIf-None-Match

服務器可能在響應中包含ETag(「實體標籤」)協議頭。 它的值是標識資源當前版本的字符串。 當客戶稍後再次請求該資源時,能夠經過包含一個If-None-Match頭來進行條件請求,該頭的值保存相同的字符串。 若是資源沒有改變,服務器將響應狀態碼 304,這意味着「未修改」,告訴客戶端它的緩存版本仍然是最新的。 當標籤與服務器不匹配時,服務器正常響應。

咱們須要這樣的東西,經過它客戶端能夠告訴服務器它有哪一個版本的對話列表,僅當列表發生變化時,服務器纔會響應。 但服務器不是當即返回 304 響應,它應該中止響應,而且僅當有新東西的可用,或已通過去了給定的時間時才返回。 爲了將長輪詢請求與常規條件請求區分開來,咱們給他們另外一個標頭Prefer: wait=90,告訴服務器客戶端最多等待 90 秒的響應。

服務器將保留版本號,每次對話更改時更新,並將其用做ETag值。 客戶端能夠在對話變動時通知此類要求:

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

[....]

這裏描述的協議並無任何訪問控制。每一個人均可以評論、修改對話或刪除對話。由於因特網中充滿了流氓,所以將這類沒有進一步保護的系統放在網絡上最後可能並非很好。

服務器

讓咱們開始構建程序的服務器部分。本節的代碼能夠在 Node.js 中執行。

路由

咱們的服務器會使用createServer來啓動 HTTP 服務器。在處理新請求的函數中,咱們必須區分咱們支持的請求的類型(根據方法和路徑肯定)。咱們可使用一長串的if語句完成該任務,但還存在一種更優雅的方式。

路由能夠做爲幫助把請求調度傳給能處理該請求的函數。路徑匹配正則表達式/^\/talks\/([^\/]+)$//talks/帶着對話名稱)的PUT請求,應當由指定函數處理。此外,路由能夠幫助咱們提取路徑中有意義的部分,在本例中會將對話的標題(包裹在正則表達式的括號之中)傳遞給處理器函數。

在 NPM 中有許多優秀的路由包,但這裏咱們本身編寫一個路由來展現其原理。

這裏給出router.js,咱們隨後將在服務器模塊中使用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;
  }
};

該模塊導出Router類。咱們可使用路由對象的add方法來註冊一個新的處理器,並使用resolve方法解析請求。

找處處理器以後,後者會返回一個響應,不然爲null。它會逐個嘗試路由(根據定義順序排序),當找到一個匹配的路由時返回true

路由會使用context值調用處理器函數(這裏是服務器實例),將請求對象中的字符串,與已定義分組中的正則表達式匹配。傳遞給處理器的字符串必須進行 URL 解碼,由於原始 URL 中可能包含%20風格的代碼。

文件服務

當請求沒法匹配路由中定義的任何請求類型時,服務器必須將其解釋爲請求位於public目錄下的某個文件。服務器可使用第二十章中定義的文件服務器來提供文件服務,但咱們並不須要也不想對文件支持 PUT 和 DELETE 請求,且咱們想支持相似於緩存等高級特性。所以讓咱們使用 NPM 中更爲可靠且通過充分測試的靜態文件服務器。

我選擇了ecstatic。它並非 NPM 中惟一的此類服務,但它可以完美工做且符合咱們的意圖。ecstatic模塊導出了一個函數,咱們能夠調用該函數,並傳遞一個配置對象來生成一個請求處理函數。咱們使用root選項告知服務器文件搜索位置。

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();
  }
}

它使用上一章中的文件服務器的相似約定來處理響應 - 處理器返回Promise,可解析爲描述響應的對象。 它將服務器包裝在一個對象中,它也維護它的狀態。

做爲資源的對話

已提出的對話存儲在服務器的talks屬性中,這是一個對象,屬性名稱是對話標題。這些對話會展示爲/talks/[title]下的 HTTP 資源,所以咱們須要將處理器添加咱們的路由中供客戶端選擇,來實現不一樣的方法。

獲取(GET)單個對話的請求處理器,必須查找對話並使用對話的 JSON 數據做爲響應,若不存在則返回 404 錯誤響應碼。

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`};
  }
});

刪除對話時,將其從talks對象中刪除便可。

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

咱們將在稍後定義updated方法,它通知等待有關更改的長輪詢請求。

爲了獲取請求正文的內容,咱們定義一個名爲readStream的函數,從可讀流中讀取全部內容,並返回解析爲字符串的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));
  });
}

須要讀取響應正文的函數是PUT的處理器,用戶使用它建立新對話。該函數須要檢查數據中是否有presentersummary屬性,這些屬性都是字符串。任何來自外部的數據均可能是無心義的,咱們不但願錯誤請求到達時會破壞咱們的內部數據模型,或者致使服務崩潰。

若數據看起來合法,處理器會將對話轉化爲對象,存儲在talks對象中,若是有標題相同的對話存在則覆蓋,並再次調用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來獲取請求內容,驗證請求數據,若看上去合法,則將其存儲爲評論。

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`};
  }
});

嘗試向不存在的對話中添加評論會返回 404 錯誤。

長輪詢支持

服務器中最值得探討的方面是處理長輪詢的部分代碼。當 URL 爲/talksGET請求到來時,它多是一個常規請求或一個長輪詢請求。

咱們可能在不少地方,將對話列表發送給客戶端,所以咱們首先定義一個簡單的輔助函數,它構建這樣一個數組,並在響應中包含ETag協議頭。

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標頭。 Node 在其小寫名稱下存儲協議頭,根據規定其名稱是不區分大小寫的。

router.add("GET", /^\/talks$/, async (server, request) => {
  let tag = /"(.*)"/.exec(request.headers["if-none-match"]);
  let wait = /\bwait=(\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]));
  }
});

若是沒有給出標籤,或者給出的標籤與服務器的當前版本不匹配,則處理器使用對話列表來響應。 若是請求是有條件的,而且對話沒有變化,咱們查閱Prefer標題來查看,是否應該延遲響應或當即響應。

用於延遲請求的回調函數存儲在服務器的waiting數組中,以便在發生事件時通知它們。 waitForChanges方法也會當即設置一個定時器,當請求等待了足夠長時,以 304 狀態來響應。

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註冊一個更改,會增長version屬性並喚醒全部等待的請求。

var changes = [];

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

服務器代碼這樣就完成了。 若是咱們建立一個SkillShareServer的實例,並在端口 8000 上啓動它,那麼生成的 HTTP 服務器,將服務於public子目錄中的文件,以及/ talksURL 下的一個對話管理界面。

new SkillShareServer(Object.create(null)).start(8000);

客戶端

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

HTML

在網絡服務器提供文件服務時,有一種廣爲使用的約定是:當請求直接訪問與目錄對應的路徑時,返回名爲index.html的文件。咱們使用的文件服務模塊ecstatic就支持這種約定。當請求路徑爲/時,服務器會搜索文件./public/index.html./public是咱們賦予的根目錄),若文件存在則返回文件。

所以,若咱們但願瀏覽器指向咱們服務器時展現某個特定頁面,咱們將其放在public/index.html中。這就是咱們的index文件。

<!doctype html>
<meta charset="utf-8">
<title>Skill Sharing</title>
<link rel="stylesheet" href="skillsharing.css">

<h1>Skill Sharing</h1>

<script src="skillsharing_client.js"></script>

它定義了文檔標題幷包含一個樣式表,除了其它東西,它定義了幾種樣式,確保對話之間有必定的空間。

最後,它在頁面頂部添加標題,並加載包含客戶端應用的腳本。

動做

應用狀態由對話列表和用戶名稱組成,咱們將它存儲在一個{talks, user}對象中。 咱們不容許用戶界面直接操做狀態或發送 HTTP 請求。 反之,它可能會觸發動做,它描述用戶正在嘗試作什麼。

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;
}

咱們將用戶的名字存儲在localStorage中,以便在頁面加載時恢復。

須要涉及服務器的操做使用fetch,將網絡請求發送到前面描述的 HTTP 接口。 咱們使用包裝函數fetchOK,它確保當服務器返回錯誤代碼時,拒絕返回的Promise

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

這個輔助函數用於爲某個對話,使用給定標題創建 URL。

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

當請求失敗時,咱們不但願咱們的頁面絲絕不變,不給予任何提示。所以咱們定義一個函數,名爲reportError,至少在發生錯誤時向用戶展現一個對話框。

function reportError(error) {
  alert(String(error));
}

渲染組件

咱們將使用一個方法,相似於咱們在第十九章中所見,將應用拆分爲組件。 但因爲某些組件不須要更新,或者在更新時老是徹底從新繪製,因此咱們不將它們定義爲類,而是直接返回 DOM 節點的函數。 例如,下面是一個組件,顯示用戶能夠向它輸入名稱的字段的:

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

用於構建 DOM 元素的elt函數是咱們在第十九章中使用的函數。

相似的函數用於渲染對話,包括評論列表和添加新評論的表單。

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事件處理器調用form.reset,在建立"newComment"動做後清除表單的內容。

在建立適度複雜的 DOM 片斷時,這種編程風格開始顯得至關混亂。 有一個普遍使用的(非標準的)JavaScript 擴展叫作 JSX,它容許你直接在你的腳本中編寫 HTML,這可使這樣的代碼更漂亮(取決於你認爲漂亮是什麼)。 在實際運行這種代碼以前,必須在腳本上運行一個程序,將僞 HTML 轉換爲 JavaScript 函數調用,就像咱們在這裏用的東西。

評論更容易渲染。

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

最後,用戶可使用表單建立新對話,它渲染爲這樣。

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"));
}

輪詢

爲了啓動應用,咱們須要對話的當前列表。 因爲初始加載與長輪詢過程密切相關 -- 輪詢時必須使用來自加載的ETag -- 咱們將編寫一個函數來不斷輪詢服務器的/ talks,而且在新的對話集可用時,調用回調函數。

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());
  }
}

這是一個async函數,所以循環和等待請求更容易。 它運行一個無限循環,每次迭代中,一般檢索對話列表。或者,若是這不是第一個請求,則帶有使其成爲長輪詢請求的協議頭。

當請求失敗時,函數會等待一下子,而後再次嘗試。 這樣,若是你的網絡鏈接斷了一段時間而後又恢復,應用能夠恢復並繼續更新。 經過setTimeout解析的Promise,是強制async函數等待的方法。

當服務器回覆 304 響應時,這意味着長輪詢請求超時,因此函數應該當即啓動下一個請求。 若是響應是普通的 200 響應,它的正文將當作 JSON 而讀取並傳遞給回調函數,而且它的ETag協議頭的值爲下一次迭代而存儲。

應用

如下組件將整個用戶界面結合在一塊兒。

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;
    }
  }
}

當對話改變時,這個組件從新繪製全部這些組件。 這很簡單,但也是浪費。 咱們將在練習中回顧一下。

咱們能夠像這樣啓動應用:

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();

若你執行服務器並同時爲localhost:8000/打開兩個瀏覽器窗口,你能夠看到在一個窗口中執行動做時,另外一個窗口中會當即作出反應。

習題

下面的習題涉及修改本章中定義的系統。爲了使用該系統進行工做,請確保首先下載代碼,安裝了 Node,並使用npm install安裝了項目的全部依賴。

磁盤持久化

技能分享服務只將數據存儲在內存中。這就意味着當服務崩潰或覺得任何緣由重啓時,全部的對話和評論都會丟失。

擴展服務使得其將對話數據存儲到磁盤上,並在程序重啓時自動從新加載數據。不要擔憂效率,只要用最簡單的代碼讓其能夠工做便可。

重置評論字段

因爲咱們經常沒法在 DOM 節點中找到惟一替換的位置,所以整批地重繪對話是個很好的工做機制。但這裏有個例外,若你開始在對話的評論字段中輸入一些文字,而在另外一個窗口向同一條對話添加了一條評論,那麼第一個窗口中的字段就會被重繪,會移除掉其內容和焦點。

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

相關文章
相關標籤/搜索