Fork me on GitHub

3. RESTful методы и тестирование в Expresso

Добро пожаловать в третью часть руководства по созданию веб-приложения с помощью Node.js. В рамках серии уроков будет рассказано про основные особенности и трудности, которые возникают при работе с Node.js.

Предыдущие части:

В этой части мы будем опираться на тот код, который был сгенерирован в предыдущей части. В приложение был добавлен каркас модели Document, так что пора добавить ей немного содержания. Далее будет предполагаться, что исходый код скачан из git-репозитория, так что самое время посетить notepad и скачать код.

3.1. Логирование

Для начала давайте добавим немного логирования. Express предоставляет такую возможность в блоке app.configure. Просто убедитесь, что Вы используете логирование:

app.configure(function() {
  app.use(express.logger());
  // Далее идут опции, добавленные в прошлой части
});

Хорошим тоном считается немного различающаяся конфигурация логирования в зависимости от окружения. Так я и поступил:

app.configure('development', function() {
  app.use(express.logger());
  app.use(express.errorHandler({
      dumpExceptions: true,
      showStack: true
  }));
});

app.configure('production', function() {
  app.use(express.logger());
  app.use(express.errorHandler());
});

3.2. API

Доступ к документам можно организовать по HTTP, используя CRUD-подобное (Create, Read, Update, Delete) RESTful API:

  • GET /documents - возвращает список документов
  • POST /documents/ - создает новый документ
  • GET /documents/:id - возвращает конкретный документ
  • PUT /documents/:id - изменяет конкретный документ
  • DELETE /documents/:id - удалет конкретный документ

HTTP-метод очень важен. Обратите внимание, что список документов и создание нового документа имеют один и тот же URL, а результат зависит от того, какой HTTP-метод используется (PUT или GET). Express корректно обработает каждый из этих URL и выполнить для каждого из них нужный код.

3.3. HTTP-метод имеет значение

Если Вы до сих пор не использовали в своей работе данный подход, то просто запомните - HTTP-метод имеет значение. Так, например, в прошлой части руководства мы определили следующий метод:

app.get('/', function(req, res) {
  // Ответ на GET для '/'
  // ...
});

Если Вы создадите форму, которая будет выполнять POST для того же URL, то Express будет возвращать ошибку, так как не задан соответствующий обработчик.

Так же напомню, что в прошлой части мы добавили в конфигурацию настройку express.methodOverride. Причиной этому является тот факт, что мы не можем полагаться на браузер в вопросах определения HTTP-методов (например, таких как DELETE). Но мы можем использовать некоторое соглашение, чтобы обойти эту проблему: формы могут использовать скрытые поля, которые Express будет интерпретировать как “настоящий” HTTP-метод.

Иногда этот подход к RESTful HTTP API может показаться неизящным, но плюсом данного соглашения является огромное количество веб-приложений, которые успешно используют REST.

3.4. Справочник CRUD заглушек

Вот как должны выглядеть CRUD заглушки:

// Список
app.get('/documents.:format', function(req, res) {
});

// Создать
app.post('/documents.:format?', function(req, res) {
});

// Прочитать
app.get('/documents/:id.:format?', function(req, res) {
});

// Изменить
app.put('/documents/:id.:format?', function(req, res) {
});

// Удалить
app.del('/documents/:id.:format?', function(req, res) {
});

Обратите внимание, что Express использует del вместо delete.

3.5. Асинхронные базы данных

Перед тем как мы начнем реализовывать каждый REST метод, давайте посмотрим на пример - загрузка списка документов. Вы, вероятно, привыкли работать в следующей манере:

app.get('/documents', function(req, res) {
  var documents = Document.find().all();

  // Отправляем результат как JSON
  res.send(documents);
}

В Node.js в основном используют библиотеки базы данных асинхронно. Это означает, что нам необходимо сделать так:

app.get('/documents', function(req, res) {
  Document.find().all(function(documents) {
    // 'documents' будет содержать все документы,
    // возвращенные запросом
    res.send(documents.map(function(d) {
      // Возвращаем объект в более полезном виде,
      // который res.send() сможет отправить во вне как JSON
      return d.__doc;
    }));
  });
});

Разница заключается в функции обратного вызова (callback), используемой для доступа к результату. Этот пример не очень эффективный, так как в нем каждый документ добавляется в массив. Вероятно, было бы более правильно возвращать их в виде потока клиенту, как только они станут доступными.

3.6. Форматы

Я предпочитаю поддерживать HTML и JSON где это необходимо. Для этого может быть использован следующий подход:

// :format может быть json или html
app.get('/documents.:format?', function(req, res) {
  // Подобие Mongo запроса
  Document.find().all(function(documents) {
    switch (req.params.format) {
      // Для json генерируем подходящие данные
      case 'json':
        res.send(documents.map(function(d) {
          return d.__doc;
        }));
      break;

      // Иначе - отрисовываем html-шаблон
      // (пока еще не реализовано)
      default:
        res.render('documents/index.jade');
    }
  });
});

Этот пример демонстрирует работу одной из функциональностей ядра Express/Connect: строка, описыващая маршрутизацию, использует :format для того, чтобы определить, какой тип данных ожидает клиент: JSON или HTML. Знак вопроса означает, что формат может быть не задан явно.

Обратите внимание, что этот пример оборачивает операции в базе данных кодом для ответа клиенту. Такой подход можно использовать для удаления или изменения объектов.

3.7. Переадресация

В зависимости от того, какой формат задан, метод создания документа возвращает либо JSON версию документа, либо выполняет переадресацию, если запрошен HTML:

app.post('/documents.:format?', function(req, res) {
  var document = new Document(req.body['document']);
  document.save(function() {
    switch (req.params.format) {
      case 'json':
        res.send(document.__doc);
       break;

       default:
        res.redirect('/documents');
    }
  });
});

В примере используется метод res.redirect для перенаправления браузера к списку документов. Точно так же можно перенаправлять на форму редактирования. Мы поближе познакомимся с этой возможностью, когда будем реализовывать интерфейс пользователя.

3.8. Тесты

Приложения подобные нашему, я обычно, начинаю писать с тестов для API. Таким образом гораздо проще реализовать большинство методов перед тем, как погружаться в код пользовательского интерфейса. Первым делом, необходимо добавить описание содинения к тестовой базе данных:

app.configure('test', function() {
  app.use(express.errorHandler({
    dumpExceptions: true,
    showStack: true
  }));
  db = mongoose.connect('mongodb://localhost/nodepad-test');
});

После чего в test/app.test.js я явно прописываю использование тестового окружения:

process.env.NODE_ENV = 'test';

Это означает, что тестовая база данных может быть безболезненно захламлена тестовыми данными или даже удалена.

Сами тесты требуют немного времени, чтобы начать ими пользоваться. Тесты Expresso замечательно работают для тестирования Express приложений, но выяснение тонкостей работы требуют чтения значительного объекма исходного кода и списков рассылки.

Вот показательный пример:

'POST /documents.json': function(assert) {
  assert.response(app, {
      url: '/documents.json',
      method: 'POST',
      data: JSON.stringify({ document: { title: 'Test' } }),
      headers: { 'Content-Type': 'application/json' }
    }, {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    },

    function(res) {
      var document = JSON.parse(res.body);
      assert.equal('Test', document.title);
    });
}

Названием теста (‘POST /documents.json’) может быть все, что угодно. Заголовок не анализируется. В первом параметре определяется HTTP-запрос. В данном случае, я указал заголовок Content-Type. Если этого не будет сделано, то Connect не сможет проанализировать data.

Я специально написал тесты для JSON и application/x-www-form-urlencoded, так как обычно именно на этих вещах происходит затык. Просто запомните, что Express “из коробки” не умеет работать с зашифрованными данными форм и именно поэтому мы указали methodOverride в блоке конфигурации.

С полными примерами тестов можно ознакомиться в данном коммите 39e66cb.

3.9. Заключение

Теперь вы должны уметь:

  • создавать CRUD-заглушки, указывая необходимый HTTP-метод, в Express
  • организовывать код приложения таким образом, чтобы можно было тестировать используя Express, Expresso и Mongoose
  • реализовывать простые Expresso тесты

В следующей части мы закончим с API для документов и начнем добавлять основные HTML шаблоны. Я собираюсь добавить интерфейс на основе jQuery, но будет лучше, если мы сначала закончим с тестами и API.