- Introduction
- Chapter 1 Values, Types, and Operators
- Chapter 2 Program Structure
- Expressions and statements
- Variables
- Keywords and reserved words
- The environment
- Functions
- The console.log function
- Return values
- prompt and confirm
- Control flow
- Conditional execution
- while and do loops
- Indenting Code
- for loops
- Breaking Out of a Loop
- Updating variables succinctly
- Dispatching on a value with switch
- Capitalization
- Comments
- Summary
- Exercises
- Chapter 3 Functions
- Chapter 4 Data Structures: Objects and Arrays
- Chapter 5 Higher-Order Functions
- Chapter 6 The Secret Life of Objects
- Chapter 7 Project: Electronic Life
- Chapter 8 Bugs and Error Handling
- Chapter 9 Regular Expressions
- Creating a regular expression
- Testing for matches
- Matching a set of characters
- Repeating parts of a pattern
- Grouping subexpressions
- Matches and groups
- The date type
- Word and string boundaries
- Choice patterns
- The mechanics of matching
- Backtracking
- The replace method
- Greed
- Dynamically creating RegExp objects
- The search method
- The lastIndex property
- Parsing an INI file
- International characters
- Summary
- Exercises
- Chapter 10 Modules
- Chapter 11 Project: A Programming Language
- Chapter 12 JavaScript and the Browser
- Chapter 13 The Document Object Model
- Chapter 14 Handling Events
- Chapter 15 Project: A Platform Game
- Chapter 16 Drawing on Canvas
- Chapter 17 HTTP
- Chapter 18 Forms and Form Fields
- Chapter 19 Project: A Paint Program
- Chapter 20 Node.js
- Chapter 21 Project: Skill-Sharing Website
- Eloquent JavaScript
- Exercise Hints
- Program Structure
- Functions
- Data Structures: Objects and Arrays
- Higher-Order Functions
- The Secret Life of Objects
- Project: Electronic Life
- Bugs and Error Handling
- Regular Expressions
- Modules
- Project: A Programming Language
- The Document Object Model
- Handling Events
- Project: A Platform Game
- Drawing on Canvas
- HTTP
- Forms and Form Fields
- Project: A Paint Program
- Node.js
- Project: Skill-Sharing Website
The client
The client-side part of the talk-managing website consists of three files: an HTML page, a style sheet, and a JavaScript file.
HTML
It is a widely used convention for web servers to try to serve a file named index.html
when a request is made directly to a path that corresponds to a directory. The file server module we use, ecstatic
, supports this convention. When a request is made to the path /
, the server looks for the file ./public/index.html
( ./public
being the root we gave it) and returns that file if found.
Thus, if we want a page to show up when a browser is pointed at our server, we should put it in public/index.html
. This is how our index file starts:
<!doctype html> <title>Skill Sharing</title> <link rel="stylesheet" href="skillsharing.css"> <h1>Skill sharing</h1> <p>Your name: <input type="text" id="name"></p> <div id="talks"></div>
It defines the document title and includes a style sheet, which defines a few styles to, among other things, add a border around talks. Then it adds a heading and a name field. The user is expected to put their name in the latter so that it can be attached to talks and comments they submit.
The <div>
element with the ID "talks"
will contain the current list of talks. The script fills the list in when it receives talks from the server.
Next comes the form that is used to create a new talk.
<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>
The script will add a "submit"
event handler to this form, from which it can make the HTTP request that tells the server about the talk.
Next comes a rather mysterious block, which has its display
style set to none
, preventing it from actually showing up on the page. Can you guess what it is for?
<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>
Creating complicated DOM structures with JavaScript code produces ugly code. You can make the code slightly better by introducing helper functions like the elt
function from Chapter 13 , but the result will still look worse than HTML, which can be thought of as a domain-specific language for expressing DOM structures.
To create DOM structures for the talks, our program will define a simple templating system, which uses hidden DOM structures included in the document to instantiate new DOM structures, replacing the placeholders between double braces with the values of a specific talk.
Finally, the HTML document includes the script file that contains the client-side code.
<script src="skillsharing_client.js"></script>
Starting up
The first thing the client has to do when the page is loaded is ask the server for the current set of talks. Since we are going to make a lot of HTTP requests, we will again define a small wrapper around XMLHttpRequest
, which accepts an object to configure the request as well as a callback to call when the request finishes.
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); }
The initial request displays the talks it receives on the screen and starts the long-polling process by calling waitForChanges
.
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(); } });
The lastServerTime
variable is used to track the time of the last update that was received from the server. After the initial request, the client’s view of the talks corresponds to the view that the server had when it responded to that request. Thus, the serverTime
property included in the response provides an appropriate initial value for lastServerTime
.
When the request fails, we don’t want to have our page just sit there, doing nothing without explanation. So we define a simple function called reportError
, which at least shows the user a dialog that tells them something went wrong.
function reportError(error) { if (error) alert(error.toString()); }
The function checks whether there is an actual error, and it alerts only when there is one. That way, we can also directly pass this function to request
for requests where we can ignore the response. This makes sure that if the request fails, the error is reported to the user.
Displaying talks
To be able to update the view of the talks when changes come in, the client must keep track of the talks that it is currently showing. That way, when a new version of a talk that is already on the screen comes in, the talk can be replaced (in place) with its updated form. Similarly, when information comes in that a talk is being deleted, the right DOM element can be removed from the document.
The function displayTalks
is used both to build up the initial display and to update it when something changes. It will use the shownTalks
object, which associates talk titles with DOM nodes, to remember the talks it currently has on the screen.
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; } }); }
Building up the DOM structure for talks is done using the templates that were included in the HTML document. First, we must define instantiateTemplate
, which looks up and fills in a template.
The name
parameter is the template’s name. To look up the template element, we search for an element whose class name matches the template name, which is a child of the element with ID "template"
. Using the querySelector
method makes this easy. There were templates named "talk"
and "comment"
in the HTML page.
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); }
The cloneNode
method, which all DOM nodes have, creates a copy of a node. It won’t copy the node’s child nodes unless true
is given as a first argument. The instantiate
function recursively builds up a copy of the template, filling in the template as it goes.
The second argument to instantiateTemplate
should be an object, whose properties hold the strings that are to be filled into the template. A placeholder like {{title}}
will be replaced with the value of values
’ title
property.
This is a crude approach to templating, but it is enough to implement drawTalk
.
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; }
After instantiating the "talk"
template, there are various things that need to be patched up. First, the comments have to be filled in by repeatedly instantiating the "comment"
template and appending the results to the node with class "comments"
. Next, event handlers have to be attached to the button that deletes the task and the form that adds a new comment.
Updating the server
The event handlers registered by drawTalk
call the function deleteTalk
and addComment
to perform the actual actions required to delete a talk or add a comment. These will need to build up URLs that refer to talks with a given title, for which we define the talkURL
helper function.
function talkURL(title) { return "talks/" + encodeURIComponent(title); }
The deleteTalk
function fires off a DELETE
request and reports the error when that fails.
function deleteTalk(title) { request({pathname: talkURL(title), method: "DELETE"}, reportError); }
Adding a comment requires building up a JSON representation of the comment and submitting that as part of a POST
request.
function addComment(title, comment) { var comment = {author: nameField.value, message: comment}; request({pathname: talkURL(title) + "/comments", body: JSON.stringify(comment), method: "POST"}, reportError); }
The nameField
variable used to set the comment’s author
property is a reference to the <input>
field at the top of the page that allows the user to specify their name. We also wire up that field to localStorage
so that it does not have to be filled in again every time the page is reloaded.
var nameField = document.querySelector("#name"); nameField.value = localStorage.getItem("name") || ""; nameField.addEventListener("change", function() { localStorage.setItem("name", nameField.value); });
The form at the bottom of the page, for proposing a new talk, gets a "submit"
event handler. This handler prevents the event’s default effect (which would cause a page reload), clears the form, and fires off a PUT
request to create the talk.
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(); });
Noticing changes
I should point out that the various functions that change the state of the application by creating or deleting talks or adding a comment do absolutely nothing to ensure that the changes they make are visible on the screen. They simply tell the server and rely on the long-polling mechanism to trigger the appropriate updates to the page.
Given the mechanism that we implemented in our server and the way we defined displayTalks
to handle updates of talks that are already on the page, the actual long polling is surprisingly simple.
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(); } }); }
This function is called once when the program starts up and then keeps calling itself to ensure that a polling request is always active. When the request fails, we don’t call reportError
since popping up a dialog every time we fail to reach the server would get annoying when the server is down. Instead, the error is written to the console (to ease debugging), and another attempt is made 2.5 seconds later.
When the request succeeds, the new data is put onto the screen, and lastServerTime
is updated to reflect the fact that we received data corresponding to this new point in time. The request is immediately restarted to wait for the next update.
If you run the server and open two browser windows for localhost:8000/ next to each other, you can see that the actions you perform in one window are immediately visible in the other.
This is a book about getting computers to do what you want them to do. Computers are about as common as screwdrivers today, but they contain a lot more hidden complexity and thus are harder to operate and understand. To many, they remain alien, slightly threatening things.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论