mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-27 22:52:20 +00:00
21.
This commit is contained in:
parent
52538421aa
commit
97fba152c2
498
21.md
498
21.md
@ -381,220 +381,80 @@ new SkillShareServer(Object.create(null)).start(8000);
|
||||
|
||||
### 21.5 客户端
|
||||
|
||||
对话管理网站的客户端部分由三个文件组成:HTML页面、样式表以及JavaScript文件。
|
||||
技能分享网站的客户端部分由三个文件组成:微型 HTML 页面、样式表以及 JavaScript 文件。
|
||||
|
||||
#### 21.5.1 HTML
|
||||
|
||||
在网络服务器提供文件服务时,有一种广为使用的约定是:当请求直接访问与目录对应的路径时,返回名为index.html的文件。我们使用的文件服务模块ecstatic就支持这种约定。当请求路径为/时,服务器会搜索文件./public/index.html(./public是我们赋予的根目录),若文件存在则返回文件。
|
||||
|
||||
因此,若我们希望浏览器指向我们服务器时展示某个特定页面,我们将其放在public/index.html中。这就是我们最初的index文件。
|
||||
因此,若我们希望浏览器指向我们服务器时展示某个特定页面,我们将其放在`public/index.html`中。这就是我们的`index`文件。
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<title>Skill Sharing</title>
|
||||
<link rel="stylesheet" href="skillsharing.css">
|
||||
|
||||
<h1>Skill sharing</h1>
|
||||
<h1>Skill Sharing</h1>
|
||||
|
||||
<p>Your name: <input type="text" id="name"></p>
|
||||
|
||||
<div id="talks"></div>
|
||||
```
|
||||
|
||||
该文件中定义了文档标题,并包含了样式表,样式表中定义了一些样式,除了别的元素之外,还给每条对话添加了边框。随后我们添加标题和表示姓名的字段。我们希望用户将名字填写到该字段中,这样我们可以将姓名附加到用户提交的对话和评论中。
|
||||
|
||||
其中ID为”talks”的<div>元素包含了当前的对话列表。当脚本从服务器接收到任务列表后会填充该列表。
|
||||
|
||||
接下来我们编写创建新对话的表单。
|
||||
|
||||
```html
|
||||
<form id="newtalk">
|
||||
<h3>Submit a talk</h3>
|
||||
Title: <input type="text" style="width: 40em" name="title">
|
||||
<br>
|
||||
Summary: <input type="text" style="width: 40em" name="summary">
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
脚本文件将会在表单的submit按钮上添加事件处理器,通过这个事件向服务器发送创建会话的HTTP请求。
|
||||
|
||||
接下来看一个更为神秘的模块,我们将其显示样式设置为none,防止该模块演示在页面上。你能猜到这是为什么设计的吗?
|
||||
|
||||
```html
|
||||
<div id="template" style="display: none">
|
||||
<div class="talk">
|
||||
<h2>{{title}}</h2>
|
||||
<div>by <span class="name">{{presenter}}</span></div>
|
||||
<p>{{summary}}</p>
|
||||
<div class="comments"></div>
|
||||
<form>
|
||||
<input type="text" name="comment">
|
||||
<button type="submit">Add comment</button>
|
||||
<button type="button" class="del">Delete talk</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="comment">
|
||||
<span class="name">{{author}}</span>: {{message}}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
使用JavaScript代码创建DOM结构会产生丑陋的代码。你可以通过引入辅助函数,例如第13章中的elt函数来使得代码稍微优雅一点,但其结果依然不如HTML,可以把HTML看成一种用于表达DOM结构的领域特定语言。
|
||||
|
||||
为了创建会话的DOM结构,我们的程序会定义一个简单的模板系统,使用文档中隐藏的DOM结构来初始化新的DOM结构,并使用特定会话中的值替换占位符。占位符包裹在两个大括号之间。
|
||||
|
||||
最后,HTML文档包含脚本文件,脚本文件中包含客户端代码。
|
||||
|
||||
```html
|
||||
<script src="skillsharing_client.js"></script>
|
||||
```
|
||||
|
||||
#### 21.5.2 启动
|
||||
它定义了文档标题并包含一个样式表,除了其它东西,它定义了几种样式,确保对话之间有一定的空间。
|
||||
|
||||
页面加载后客户端首先向服务器请求当前的对话集。由于我们打算建立许多HTTP请求,我们再次定义了XMLHttpRequest的简单包装器,该函数接受一个对象,用于配置请求,并在请求结束时调用回调函数。
|
||||
最后,它在页面顶部添加标题,并加载包含客户端应用的脚本。
|
||||
|
||||
### 动作
|
||||
|
||||
应用程序状态由对话列表和用户名称组成,我们将它存储在一个`{talks, user}`对象中。 我们不允许用户界面直接操作状态或发送 HTTP 请求。 反之,它可能会触发动作,它描述用户正在尝试做什么。
|
||||
|
||||
```js
|
||||
function request(options, callback) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open(options.method || "GET", options.pathname, true);
|
||||
req.addEventListener("load", function() {
|
||||
if (req.status < 400)
|
||||
callback(null, req.responseText);
|
||||
else
|
||||
callback(new Error("Request failed: " + req.statusText));
|
||||
});
|
||||
req.addEventListener("error", function() {
|
||||
callback(new Error("Network error"));
|
||||
});
|
||||
req.send(options.body || null);
|
||||
}
|
||||
```
|
||||
|
||||
最初的请求将接收到的对话显示在屏幕上,并调用waitForChanges来启动长轮询过程。
|
||||
|
||||
```js
|
||||
var lastServerTime = 0;
|
||||
|
||||
request({pathname: "talks"}, function(error, response) {
|
||||
if (error) {
|
||||
reportError(error);
|
||||
} else {
|
||||
response = JSON.parse(response);
|
||||
displayTalks(response.talks);
|
||||
lastServerTime = response.serverTime;
|
||||
waitForChanges();
|
||||
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);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
变量lastServerTime用于跟踪最后从服务器接收更新信息的时间。在第一个请求之后,客户端的对话视图需要在请求响应到来时与服务器的视图同步,因此响应中的serverTime属性给lastServerTime提供了合适初值。
|
||||
|
||||
当请求失败时,我们不希望我们的页面丝毫不变,不给予任何提示。因此我们定义一个函数,名为reportError,至少在发生错误时向用户展示一个对话框。
|
||||
|
||||
```js
|
||||
function reportError(error) {
|
||||
if (error)
|
||||
alert(error.toString());
|
||||
return state;
|
||||
}
|
||||
```
|
||||
|
||||
该函数检查是否有实际的错误,只有当确实发生错误时才弹出警告框。这样我们可以忽略掉响应直接将该函数传递给request。这确保请求失败时会向用户报告错误信息。
|
||||
我们将用户的名字存储在`localStorage`中,以便在页面加载时恢复。
|
||||
|
||||
#### 21.5.3 显示会话
|
||||
|
||||
为了能够在数据修改时更新对话视图,客户端必须持续跟踪当前显示的对话。这样,当屏幕上已存在的对话的新版本到来时,客户端可以使用更新后的对话替换原来的版本。类似地,当对话删除的信息到来时,必须从文档中删除当时的DOM元素。
|
||||
|
||||
函数displayTalks用于显示最初的对话,且在某些信息发生改动时更新对话。该函数使用shownTalks对象记录当前在屏幕上的对话,该对象保存了对话和DOM节点的关系。
|
||||
需要涉及服务器的操作使用`fetch`,将网络请求发送到前面描述的 HTTP 接口。 我们使用包装函数`fetchOK`,它确保当服务器返回错误代码时,拒绝返回的`Promise`。
|
||||
|
||||
```js
|
||||
var talkDiv = document.querySelector("#talks");
|
||||
var shownTalks = Object.create(null);
|
||||
|
||||
function displayTalks(talks) {
|
||||
talks.forEach(function(talk) {
|
||||
var shown = shownTalks[talk.title];
|
||||
if (talk.deleted) {
|
||||
if (shown) {
|
||||
talkDiv.removeChild(shown);
|
||||
delete shownTalks[talk.title];
|
||||
}
|
||||
} else {
|
||||
var node = drawTalk(talk);
|
||||
if (shown)
|
||||
talkDiv.replaceChild(node, shown);
|
||||
else
|
||||
talkDiv.appendChild(node);
|
||||
shownTalks[talk.title] = node;
|
||||
}
|
||||
function fetchOK(url, options) {
|
||||
return fetch(url, options).then(response => {
|
||||
if (response.status < 400) return response;
|
||||
else throw new Error(response.statusText);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
我们使用HTML文档中的模板来构建对话的DOM结构。首先我们必须定义instantiateTemplate,用于查找并填充模板。
|
||||
|
||||
模板中的name参数是模板名称。为了查找模板元素,我们搜索一个类名与模板名称匹配的元素,该元素是ID为template的元素的孩子节点。使用querySelector方法可以轻松完成该任务。在HTML页面中有名为talk和comment的模板。
|
||||
|
||||
```js
|
||||
function instantiateTemplate(name, values) {
|
||||
function instantiateText(text) {
|
||||
return text.replace(/\{\{(\w+)\}\}/g, function(_, name) {
|
||||
return values[name];
|
||||
});
|
||||
}
|
||||
function instantiate(node) {
|
||||
if (node.nodeType == document.ELEMENT_NODE) {
|
||||
var copy = node.cloneNode();
|
||||
for (var i = 0; i < node.childNodes.length; i++)
|
||||
copy.appendChild(instantiate(node.childNodes[i]));
|
||||
return copy;
|
||||
} else if (node.nodeType == document.TEXT_NODE) {
|
||||
return document.createTextNode(
|
||||
instantiateText(node.nodeValue));
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
var template = document.querySelector("#template ." + name);
|
||||
return instantiate(template);
|
||||
}
|
||||
```
|
||||
|
||||
每个DOM节点都有cloneNode方法,用于创建节点的拷贝。除非该函数的第一个参数为true,否则该函数不会拷贝节点的子节点。instantiate函数则用于递归构建模板的拷贝,并根据值填充模板。
|
||||
|
||||
instantiateTemplate的第二个参数是一个对象,其属性是想要填充到模板中的字符串。模板中像{{title}}这样的占位符会被替换为value的title属性的值。
|
||||
|
||||
这是非常原始的模板方法,但这对于实验drawTalk来说已经足够了。
|
||||
|
||||
```js
|
||||
function drawTalk(talk) {
|
||||
var node = instantiateTemplate("talk", talk);
|
||||
var comments = node.querySelector(".comments");
|
||||
talk.comments.forEach(function(comment) {
|
||||
comments.appendChild(
|
||||
instantiateTemplate("comment", comment));
|
||||
});
|
||||
|
||||
node.querySelector("button.del").addEventListener(
|
||||
"click", deleteTalk.bind(null, talk.title));
|
||||
|
||||
var form = node.querySelector("form");
|
||||
form.addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
addComment(talk.title, form.elements.comment.value);
|
||||
form.reset();
|
||||
});
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
在实例化talk模板后,还需要修补一些数据。首先,我们需要反复实例化comment模板,并将实例化结果添加到类为comments的节点之中。接下来,必须要在删除对话的按钮上附加对应的事件处理器,并在表单上附加创建新评论的事件处理器。
|
||||
|
||||
#### 21.5.4 更新服务器
|
||||
|
||||
通过drawTalk函数调用注册的事件处理器会调用deleteTalk和addComment来执行实际的动作,以删除对话或添加评论。这些处理器需要根据指定的标题构建指向对话的URL,我们之前定义的talkURL辅助函数可以完成该任务。
|
||||
这个辅助函数用于为某个对话,使用给定标题建立 URL。
|
||||
|
||||
```js
|
||||
function talkURL(title) {
|
||||
@ -602,88 +462,190 @@ function talkURL(title) {
|
||||
}
|
||||
```
|
||||
|
||||
deleteTalk函数会发送DELETE请求,并在失败时报告错误信息。
|
||||
当请求失败时,我们不希望我们的页面丝毫不变,不给予任何提示。因此我们定义一个函数,名为reportError,至少在发生错误时向用户展示一个对话框。
|
||||
|
||||
```js
|
||||
function deleteTalk(title) {
|
||||
request({pathname: talkURL(title), method: "DELETE"},
|
||||
reportError);
|
||||
function reportError(error) {
|
||||
alert(String(error));
|
||||
}
|
||||
```
|
||||
|
||||
添加评论需要构建用于表示评论的JSON数据,并使用POST请求提交该数据。
|
||||
#### 渲染组件
|
||||
|
||||
我们将使用一个方法,类似于我们在第十九章中所见,将应用程序拆分为组件。 但由于某些组件不需要更新,或者在更新时总是完全重新绘制,所以我们不将它们定义为类,而是直接返回 DOM 节点的函数。 例如,下面是一个组件,显示用户可以向它输入名称的字段的:
|
||||
|
||||
```js
|
||||
function addComment(title, comment) {
|
||||
var comment = {author: nameField.value, message: comment};
|
||||
request({pathname: talkURL(title) + "/comments",
|
||||
body: JSON.stringify(comment),
|
||||
method: "POST"},
|
||||
reportError);
|
||||
}
|
||||
```
|
||||
|
||||
变量nameField用于设置评论的author属性,该属性对应于页面顶端的<input>字段,允许用户指定他们的姓名。我们也可以将该字段填写到localStorage中,这样每次重新载入页面时用户不必再次填写姓名。
|
||||
|
||||
```js
|
||||
var nameField = document.querySelector("#name");
|
||||
|
||||
nameField.value = localStorage.getItem("name") || "";
|
||||
|
||||
nameField.addEventListener("change", function() {
|
||||
localStorage.setItem("name", nameField.value);
|
||||
});
|
||||
```
|
||||
|
||||
页面底端的表单通过submit的事件处理器发表新对话。事件处理器阻止了时间的默认效果(会导致重新加载页面),清空表单,并发送创建对话的PUT请求。
|
||||
|
||||
```js
|
||||
var talkForm = document.querySelector("#newtalk");
|
||||
|
||||
talkForm.addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
request({pathname: talkURL(talkForm.elements.title.value),
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
presenter: nameField.value,
|
||||
summary: talkForm.elements.summary.value
|
||||
})}, reportError);
|
||||
talkForm.reset();
|
||||
});
|
||||
```
|
||||
|
||||
#### 21.5.5 提示更改
|
||||
|
||||
这里需要指出的是不同修改应用程序状态的函数(创建删除任务以及添加评论)都不必确保他们的修改对屏幕可见。这些函数只需要告知服务器,并依赖长轮询机制来触发页面的适当更新。
|
||||
|
||||
考虑到我们在服务器中实现的机制,以及我们定义displayTalks来处理会话更新的方式,实际的长轮询代码简单得令人惊讶。
|
||||
|
||||
```js
|
||||
function waitForChanges() {
|
||||
request({pathname: "talks?changesSince=" + lastServerTime},
|
||||
function(error, response) {
|
||||
if (error) {
|
||||
setTimeout(waitForChanges, 2500);
|
||||
console.error(error.stack);
|
||||
} else {
|
||||
response = JSON.parse(response);
|
||||
displayTalks(response.talks);
|
||||
lastServerTime = response.serverTime;
|
||||
waitForChanges();
|
||||
function renderUserField(name, dispatch) {
|
||||
return elt("label", {}, "Your name: ", elt("input", {
|
||||
type: "text",
|
||||
value: name,
|
||||
onchange(event) {
|
||||
dispatch({type: "setUser", user: event.target.value});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
该函数在程序启动时调用一次,然后不断调用该函数确保长连接请求一直处于活跃状态。当请求失败时,我们不需要调用reportError,因为若每次请求无法到达服务器,都弹出一个对话框,会让用户感到厌烦。相反,我们将错误信息写到控制台中(易于调试),并在2.5秒之后再次进行尝试。
|
||||
用于构建DOM元素的`elt`函数是我们在第十九章中使用的函数。
|
||||
|
||||
每当请求成功后,客户端会将新数据展示在屏幕中并根据接收到的数据更新lastServerTime,确保该属性与我们接收到数据的新时间点相一致。接着立即重新发起请求来等待下一轮更新。
|
||||
类似的函数用于渲染对话,包括评论列表和添加新评论的表单。
|
||||
|
||||
若你执行服务器并同时打开两个浏览器窗口,都输入localhost:8000/,你可以看到在一个窗口中执行动作时,另一个窗口中会立即做出反应。
|
||||
```js
|
||||
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 函数调用,就像我们在这里用的东西。
|
||||
|
||||
评论更容易渲染。
|
||||
|
||||
```js
|
||||
function renderComment(comment) {
|
||||
return elt("p", {className: "comment"},
|
||||
elt("strong", null, comment.author),
|
||||
": ", comment.message);
|
||||
}
|
||||
```
|
||||
|
||||
最后,用户可以用于创建新对话的表现,渲染为这样。
|
||||
|
||||
```js
|
||||
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`,并且在新的对话集可用时,调用回调函数。
|
||||
|
||||
```js
|
||||
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`协议头的值为下一次迭代而存储。
|
||||
|
||||
### 应用
|
||||
|
||||
以下组件将整个用户界面结合在一起。
|
||||
|
||||
```js
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当对话改变时,这个组件重新绘制所有这些组件。 这很简单,但也是浪费。 我们将在练习中回顾一下。
|
||||
|
||||
我们可以像这样启动应用:
|
||||
|
||||
```js
|
||||
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/`打开两个浏览器窗口,你可以看到在一个窗口中执行动作时,另一个窗口中会立即做出反应。
|
||||
|
||||
### 21.6 习题
|
||||
|
||||
下面的习题涉及修改本章中定义的系统。为了使用该系统进行工作,请确保首先下载代码([http://eloquentjavascript.net/code/skillshare.zip](http://eloquentjavascript.net/code/skillshare.zip))并安装了Node([http://nodejs.org/](http://nodejs.org/))。
|
||||
下面的习题涉及修改本章中定义的系统。为了使用该系统进行工作,请确保首先下载[代码](http://eloquentjavascript.net/code/skillshare.zip),安装了 [Node](http://nodejs.org/),并使用`npm install`安装了项目的所有依赖。
|
||||
|
||||
#### 21.6.1 磁盘持久化
|
||||
|
||||
@ -695,36 +657,4 @@ function waitForChanges() {
|
||||
|
||||
由于我们常常无法在DOM节点中找到唯一替换的位置,因此整批地重绘对话是个很好的工作机制。但这里有个例外,若你开始在对话的评论字段中输入一些文字,而在另一个窗口向同一条对话添加了一条评论,那么第一个窗口中的字段就会被重绘,会移除掉其内容和焦点。
|
||||
|
||||
在热烈的讨论中,当多个人向同一条对话中添加评论时,这种情况会非常烦人。你能想出一种方法来避免这个问题吗?
|
||||
|
||||
#### 21.6.3 更好的模板
|
||||
|
||||
许多模板系统除了填充一些字符串外还能完成更多工作。至少这些模板系统支持条件包含某部分模板,类似于if语句;或者可以重复一部分模板,类似于循环。
|
||||
|
||||
若我们可以为数组中的每个元素重复一段模板,我们就不需要第二个模板(“comment”)了。而可以指定talk模板循环扫描talk持有的comments属性,并逐个渲染节点,这些节点组成了数组中的每个元素所对应的评论。
|
||||
|
||||
模板如下所示。
|
||||
|
||||
```html
|
||||
<div class="comments">
|
||||
<div class="comment" template-repeat="comments">
|
||||
<span class="name">{{author}}</span>: {{message}}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
其思想是:在模板实例化时,每当发现某个节点有template-repeat属性,实例化代码就会认为标签中属性名在对象中对应的属性是数组,并循环遍历该属性。对于数组中的每个元素都会添加一个节点的实例。模板的上下文(instantiateTemplate中的values变量)在循环中会指向数组中的每个元素,因此{{author}}将会查找comment对象而非原来的上下文(talk对象)。
|
||||
|
||||
重写instantiateTemplate来实现该特性并修改模板,使用该特性来移除掉drawTalk中显示渲染评论的代码。
|
||||
|
||||
为了能在特定值为true或者false时可以省略模板中的一部分,你应该如何在节点中添加条件实例化呢?
|
||||
|
||||
#### 21.6.4 无脚本化
|
||||
|
||||
当某些人使用浏览器访问我们的网站时,若浏览器禁用了JavaScript或只是无法显示JavaScript,他们将会看到一个完全损坏,无法使用的页面。这并不是一件好事。
|
||||
|
||||
某些类型的应用程序不使用JavaScript是无法完成的。对于其他应用,你不需要关心客户端是否支持运行脚本。但对于有大量用户的网站,支持无脚本用户是必要的。
|
||||
|
||||
尝试思考一下,如何在不使用JavaScript下建立一个技能分享网站,保留其基本功能。这个版本不需要加入自动更新,人们可以使用传统的方式来刷新页面。但查看存在的对话,创建新节点,并提交评论等功能需要完好。
|
||||
|
||||
本书并不要求读者实际实现该网站,画出大致解决方案就足够了。是不是修订过的方法比最初的方法更吸引你呢?
|
||||
在激烈的讨论中,多人同时添加评论,这将是非常烦人的。 你能想出办法解决它吗?
|
||||
|
Loading…
x
Reference in New Issue
Block a user