JavaScript程式設計精解中文第三版二十一、專案:技能分享網站
二十一、專案:技能分享網站
原文:Project: Skill-Sharing Website
譯者:飛龍
自豪地採用谷歌翻譯
部分參考了《JavaScript 程式設計精解(第 2 版)》
If you have knowledge, let others light their candles at it.
Margaret Fuller
技能分享會是一個活動,其中興趣相同的人聚在一起,針對他們所知的事情進行小型非正式的展示。在園藝技能分享會上,可以解釋如何耕作芹菜。如果在程式設計技能分享小組中,你可以順便給每個人講講 Node.js。
在計算機領域中,這類聚會往往名為使用者小組,是開闊眼界、瞭解行業新動態或僅僅接觸興趣相同的人的好方法。許多大城市都會有 JavaScript 聚會。這類聚會往往是可以免費參加的,而且我發現我參加過的那些聚會都非常友好熱情。
在最後的專案章節中,我們的目標是建立網站,管理特定技能分享會的討論內容。假設一個小組的人會在成員辦公室中定期舉辦關於獨輪車的聚會。上一個組織者搬到了另一個城市,並且沒人可以站出來接下來他的任務。我們需要一個系統,讓參與者可以在系統中發言並相互討論,這樣就不需要一箇中心組織人員了。
就像上一章一樣,本章中的一些程式碼是為 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 程式碼。
訪問/talks
的GET
請求會返回如下所示的 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?"}
為了支援長輪詢,如果沒有新的資訊可用,傳送到/talks
的GET
請求可能會包含額外的標題,通知伺服器延遲響應。 我們將使用通常用於管理快取的一對協議頭:ETag
和If-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
的處理器,使用者使用它建立新對話。該函式需要檢查資料中是否有presenter
和summary
屬性,這些屬性都是字串。任何來自外部的資料都可能是無意義的,我們不希望錯誤請求到達時會破壞我們的內部資料模型,或者導致服務崩潰。
若資料看起來合法,處理器會將對話轉化為物件,儲存在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 為/talks
的GET
請求到來時,它可能是一個常規請求或一個長輪詢請求。
我們可能在很多地方,將對話列表傳送給客戶端,因此我們首先定義一個簡單的輔助函式,它構建這樣一個陣列,並在響應中包含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-Match
和Prefer
標頭。 Node 在其小寫名稱下儲存協議頭,根據規定其名稱是不區分大小寫的。
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]));
}
});
如果沒有給出標籤,或者給出的標籤與伺服器的當前版本不匹配,則處理器使用對話列表來響應。 如果請求是有條件的,並且對話沒有變化,我們查閱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
子目錄中的檔案,以及/ talks
URL 下的一個對話管理介面。
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 節點中找到唯一替換的位置,因此整批地重繪對話是個很好的工作機制。但這裡有個例外,若你開始在對話的評論欄位中輸入一些文字,而在另一個視窗向同一條對話新增了一條評論,那麼第一個視窗中的欄位就會被重繪,會移除掉其內容和焦點。
在激烈的討論中,多人同時新增評論,這將是非常煩人的。 你能想出辦法解決它嗎?
相關文章
- JavaScript程式設計精解中文第三版零、前言JavaScript程式設計
- 18 個鍛鍊程式設計技能的網站程式設計網站
- 《JavaScript程式設計精解》--讀書筆記JavaScript程式設計筆記
- 學習程式設計常用網站分享程式設計網站
- Java 高效程式設計(Effective Java)中文第三版Java程式設計
- 網站建設網頁設計小技巧分享網站網頁
- 好程式設計師Java培訓分享Java程式設計師技能提升指南程式設計師Java
- 分享Python核心程式設計第三版PDF進一步提升程式設計水平Python程式設計
- 《JavaScript 高階程式設計》精讀筆記JavaScript程式設計筆記
- 【程式設計技巧】匯入專案出現中文亂碼程式設計
- 網站專案建設流程概述網站
- 電商網站專案總結:Vuex 帶來全新的程式設計體驗網站Vue程式設計
- 程式設計競賽中讀檔案技能程式設計
- 好程式設計師分享JavaScript六種繼承方式詳解程式設計師JavaScript繼承
- 兩岸網站大全:程式設計網站大全網站程式設計
- 程式設計師到創業,成長之路的技能分享程式設計師創業
- 程式設計師最佳網站程式設計師網站
- 好程式設計師java分享spring框架精講程式設計師JavaSpring框架
- JavaScript ES 模組:現代化前端程式設計必備技能JavaScript前端程式設計
- 蘋果推出重新設計的開源專案主題網站蘋果網站
- 長沙IT培訓:程式設計師面試專案經驗分享程式設計師面試
- 專業建站網站 網站建設 網站開發 官網開發 網頁設計 網頁網站網頁
- GO的網路程式設計分享Go程式設計
- 好程式設計師分享大資料三大必備技能程式設計師大資料
- 【VIP視訊網站專案上線】基於Nodejs的Express框架開發的VIP視訊網站專案及完整程式碼分享...網站NodeJSExpress框架
- 推薦的程式設計網站程式設計網站
- 好程式設計師分享JavaScript建立物件的方式!程式設計師JavaScript物件
- 好程式設計師分享JavaScript名稱空間模式例項詳解程式設計師JavaScript模式
- 程式設計師如何獲取新程式設計技能程式設計師
- C# 網路程式設計:.NET 開發者的核心技能C#程式設計
- 從程式設計師到專案經理(9):程式設計師加油站 -- 再牛也要合群程式設計師
- 從程式設計師到專案經理(9)程式設計師加油站 - 再牛也要合群程式設計師
- 從程式設計師到專案經理(8)程式設計師加油站 - 不要死於直率程式設計師
- 直通車主圖設計素材網站分享!附帶AI設計工具!網站AI
- 大型網站建設,一個專案幾百上千,初中生都能上手的程式設計!網站程式設計
- 程式設計師實用工具網站程式設計師網站
- IT招聘網站(程式設計師跳槽指南)網站程式設計師
- 程式設計師該知道的那些程式設計比賽網站程式設計師網站