KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Марейн Хавербеке - Выразительный JavaScript

Марейн Хавербеке - Выразительный JavaScript

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Марейн Хавербеке, "Выразительный jаvascript" бесплатно, без регистрации.
Перейти на страницу:

var http = require("http");

var Router = require("./router");

var ecstatic = require("ecstatic");


var fileServer = ecstatic({root: "./public"});

var router = new Router();


http.createServer(function(request, response) {

  if (!router.resolve(request, response))

    fileServer(request, response);

}).listen(8000);


Функции respond и respondJSON используются в коде сервера, чтобы можно было отправлять ответы одним вызовом функции.


function respond(response, status, data, type) {

  response.writeHead(status, {

    "Content-Type": type || "text/plain"

  });

  response.end(data);

}


function respondJSON(response, status, data) {

  respond(response, status, JSON.stringify(data),

          "application/json");

}

Темы как ресурсы

Сервер хранит предложенные темы в объекте talks, у которого именами свойств являются названия тем. Они будут выглядеть как ресурсы HTTP по адресу /talks/[title], поэтому нам нужно добавить в маршрутизатор обработчиков, реализующих различные методы, которые клиенты могут использовать для работы с ними.

Обработчик для запросов GET одной темы должен найти её и либо вернуть данные в JSON, либо выдать ошибку 404.

var talks = Object.create(null);


router.add("GET", /^/talks/([^/]+)$/,

           function(request, response, title) {

  if (title in talks)

    respondJSON(response, 200, talks[title]);

  else

    respond(response, 404, "No talk '" + title + "' found");

});


Удаление темы делается удалением из объекта talks.


router.add("DELETE", /^/talks/([^/]+)$/,

           function(request, response, title) {

  if (title in talks) {

    delete talks[title];

    registerChange(title);

  }

  respond(response, 204, null);

});

Функция registerChange, которую мы определим позже, уведомляет длинные запросы об изменениях.

Чтобы было просто получать контент тел запросов, закодированных при помощи JSON, мы определяем функцию readStreamAsJSON, которая читает всё содержимое потока, разбирает его по правилам JSON и затем делает обратный вызов.

function readStreamAsJSON(stream, callback) {

  var data = "";

  stream.on("data", function(chunk) {

    data += chunk;

  });

  stream.on("end", function() {

    var result, error;

    try { result = JSON.parse(data); }

    catch (e) { error = e; }

    callback(error, result);

  });

  stream.on("error", function(error) {

    callback(error);

  });

}

Один из обработчиков, которому нужно читать ответы в JSON – это обработчик PUT, который используется для создания новых тем. Он должен проверить, есть ли у данных свойства presenter и summary, которые должны быть строками. Данные, приходящие снаружи, всегда могут оказаться мусором, и мы не хотим, чтобы из-за плохого запроса была сломана наша система.

Если данные выглядят приемлемо, обработчик сохраняет объект, представляющий новую тему, в объекте talks, при этом, возможно, перезаписывая существующую тему с таким же заголовком, и опять вызывает registerChange.

router.add("PUT", /^/talks/([^/]+)$/,

           function(request, response, title) {

  readStreamAsJSON(request, function(error, talk) {

    if (error) {

      respond(response, 400, error.toString());

    } else if (!talk ||

               typeof talk.presenter != "string" ||

               typeof talk.summary != "string") {

      respond(response, 400, "Bad talk data");

    } else {

      talks[title] = {title: title,

                      presenter: talk.presenter,

                      summary: talk.summary,

                      comments: []};

      registerChange(title);

      respond(response, 204, null);

    }

  });

});

Добавление комментария к теме работает сходным образом. Мы используем readStreamAsJSON для получения содержимого сообщения, проверяем результирующие данные и сохраняем их как комментарий, если они приемлемы.

router.add("POST", /^/talks/([^/]+)/comments$/,

           function(request, response, title) {

  readStreamAsJSON(request, function(error, comment) {

    if (error) {

      respond(response, 400, error.toString());

    } else if (!comment ||

               typeof comment.author != "string" ||

               typeof comment.message != "string") {

      respond(response, 400, "Bad comment data");

    } else if (title in talks) {

      talks[title].comments.push(comment);

      registerChange(title);

      respond(response, 204, null);

    } else {

      respond(response, 404, "No talk '" + title + "' found");

    }

  });

});

Попытка добавить комментарий к несуществующей теме должна возвращать ошибку 404.

Поддержка длинных запросов

Самый интересный аспект сервера – часть, которая поддерживает длинные запросы. Когда на адрес /talks поступает запрос GET, это может быть простой запрос всех тем, или запрос на обновления с параметром changesSince.

Есть много различных ситуаций, в которых нам нужно отправить клиенту список тем, поэтому мы сначала определим вспомогательную функцию, присоединяющую поле serverTime к таким ответам.

function sendTalks(talks, response) {

  respondJSON(response, 200, {

    serverTime: Date.now(),

    talks: talks

  });

}

Обработчик должен посмотреть на все параметры запроса в его URL, чтобы проверить, не задан ли параметр changesSince. Если дать функции parse модуля “url” второй аргумент значения true, он также распарсит вторую часть URL – query, часть запроса. У возвращаемого объекта будет свойство query, в котором будет ещё один объект, с именами и значениями параметров.

router.add("GET", /^/talks$/, function(request, response) {

  var query = require("url").parse(request.url, true).query;

  if (query.changesSince == null) {

    var list = [];

    for (var title in talks)

      list.push(talks[title]);

    sendTalks(list, response);

  } else {

    var since = Number(query.changesSince);

    if (isNaN(since)) {

      respond(response, 400, "Invalid parameter");

    } else {

      var changed = getChangedTalks(since);

      if (changed.length > 0)

         sendTalks(changed, response);

      else

        waitForChanges(since, response);

    }

  }

});

При отсутствии параметра changesSince обработчик просто строит список всех тем и возвращает его.

Иначе, сперва надо проверить параметр changeSince на предмет того, что это число. Функция getChangedTalks, которую мы вскоре определим, возвращает массив изменённых тем с некоего заданного времени. Если она возвращает пустой массив, то серверу нечего возвращать клиенту, так что он сохраняет объект response (при помощи waitForChanges), чтобы ответить попозже.

var waiting = [];


function waitForChanges(since, response) {

  var waiter = {since: since, response: response};

  waiting.push(waiter);

  setTimeout(function() {

    var found = waiting.indexOf(waiter);

    if (found > -1) {

      waiting.splice(found, 1);

      sendTalks([], response);

    }

  }, 90 * 1000);

}

Метод splice используется для вырезания куска массива. Ему задаётся индекс и количество элементов, и он изменяет массив, удаляя это количество элементов после заданного индекса. В этом случае мы удаляем один элемент – объект, ждущий ответ, чей индекс мы узнали через indexOf. Если вы передадите дополнительные аргументы в splice, их значения будут вставлены в массив на заданной позиции, и заместят удалённые элементы.

Когда объект response сохранён в массиве waiting, задаётся таймаут. После 90 секунд он проверяет, ждёт ли ещё запрос, и если да – отправляет пустой ответ и удаляет его из массива waiting.

Чтобы найти именно те темы, которые сменились после заданного времени, нам надо отслеживать историю изменений. Регистрация изменения при помощи registerChange запомнит это изменение, вместе с текущим временем, в массиве changes. Когда случается изменение, это значит – есть новые данные, поэтому всем ждущим запросам можно немедленно ответить.

var changes = [];


function registerChange(title) {

  changes.push({title: title, time: Date.now()});

  waiting.forEach(function(waiter) {

    sendTalks(getChangedTalks(waiter.since), waiter.response);

  });

  waiting = [];

}

Наконец, getChangedTalks использует массив changes, чтобы построить массив изменившихся тем, включая объекты со свойством deleted для тем, которых уже не существует. При построении массива getChangedTalks должна убедиться, что одна и та же тема не включается дважды, так как тема могла измениться несколько раз с заданного момента времени.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*