返回介绍

The client

发布于 2025-02-27 23:45:57 字数 14822 浏览 0 评论 0 收藏 0

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 valuestitle 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文