November 2015

Volume 30 Number 12

Работающий программист - MEAN: маршрутизация в Express

By Тэд Ньюард | November 2015

Ted NewardС возвращением, «нодисты». (Понятия не имею, является ли официальным такое обращение к тем, кто регулярно пользуется Node.js, но на мой вкус обращение «нодисты» звучит лучше, чем «нодхеды» [«Nodeheads»], «нодерати» [«Noderati»] или «нодферату» [«Nodeferatu»].)

В прошлой статье (msdn.com/magazine/mt573719) стек приложения перестал быть просто «N» (только Node) и превратился в «EN» благодаря установке Express совместно с Node.js. Как бы ни было соблазнительно перейти прямо к другим вещам, есть еще несколько средств, связанных с Express (и его вспомогательными пакетами и библиотеками), которые заслуживают исследования и более глубокого обсуждения. Ранее вы уже получили представление об одном из них — о маршрутизации (routing) в Express, когда писали код для функции, которая выводит «Hello World» в ответ на HTTP-запрос к относительному URL. Теперь я немного углублюсь в мир Express и покажу, как использовать его более эффективно.

Кстати, те, кто заинтересован в новейшем и лучшем коде, который пишется как часть этой серии статей, могут посетить сайт Microsoft Azure, где хранится самый последний код для этой серии (msdn-mean.azurewebsites.net). Весьма вероятно, что информация в этой рубрике уже отстала от выложенной на данном сайте, учитывая графики публикаций, а сайт позволяет читателям заглянуть немного вперед.

Снова о маршрутизации

Напомню файл app.js из прошлой статьи в моей рубрике, демонстрирующий природу приложения с единственной конечной точкой, созданной на текущем этапе (рис. 1), — простая, но необходимая дань Богам Компьютерной Науки.

Рис. 1. Код для «Hello World» в Express

// Загрузка модулей
var express = require('express');
var debug = require('debug')('app');

// Создание экземпляра express
var app = express();

// Настройка простого маршрута (route)
app.get('/', function (req, res) {
  debug("/ requested");
  res.send('Hello World!');
});

// Запуск сервера
var port = process.env.PORT || 3000;
debug("We picked up",port,"for the port");
var server = app.listen(port, function () {

  var host = server.address().address;
  var port = server.address().port;

  console.log(
    'Example app listening at http://%s:%s', host, port);
});

Интересующая нас часть — раздел кода под комментарием «Настройка простого маршрута»; здесь вы устанавливаете единственную конечную точку с помощью HTTP-команды get и конечной точки относительного URL («/», передаваемой в качестве первого аргумента методу get).

Довольно легко логически вывести шаблон для других HTTP-команд: для запроса POST используется метод post, для PUT — метод put, а для DELETE — метод delete. Express поддерживает и другие команды, но по достаточно очевидным причинам эти четыре команды применяются чаще всего. Каждый из этих методов также принимает второй аргумент — функцию, которая в примере на рис. 1 является литералом функции, обрабатывающей входящий HTTP-запрос.

Буква «E» в MEAN

Зачастую, когда «нодисты» пишут приложения на основе Express, они делают это в той же манере, что и мы, «нетеры» («.NETers»), пишем приложения ASP.NET. Сервер генерирует HTML-документ, содержащий представление (HTML), тесно переплетенное с данными, и отсылает браузеру, после чего пользователь заполняет форму и командой POST передает введенные данные обратно Express (или щелкает ссылку и генерирует команду GET для отправки в Express, чтобы снова выполнить полный цикл на серверной стороне). А поскольку написание HTML вручную в Node.js столь же увлекательно, как и в Visual Basic или C#, в мире Node.js появился ряд инструментов, предназначенных для той же цели, что и синтаксис Razor в традиционном приложении ASP.NET. Это упрощает написание презентационного уровня без слишком сильного перемешивания данных и кода.

Однако AngularJS в приложении на основе MEAN будет образовывать полноценную среду на клиентской стороне, поэтому Express играет ту же роль, что и ASP.NET MVC — это просто транспортный уровень, который принимает исходные данные (обычно в формате JSON) от клиента, что-то делает с этими данными (обычно либо сохраняет их, либо модифицирует, либо находит сопоставленные или связанные данные) и отправляет исходные данные (и вновь, как правило, в формате JSON) обратно на клиентский уровень. В связи с этим при нашем пребывании в Express мы будем избегать тематики, относящейся к инфраструктурам формирования шаблонов (templating frameworks) (которых в мире Node.js есть несколько, причем handlebars и jade — две самые популярные). Я сосредоточусь на простой передаче JSON туда и обратно. Некоторые назовут это конечной точкой RESTful, но, честно говоря, REST включает куда больше, чем просто HTTP и JSON, а создание RESTful-систем выходит далеко за рамки этой серии статей.

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

Привет в формате JSON

Обычно Web API следует довольно нежесткой структуре для получения данных.

  • GET-запрос к ресурсу данного типа (например, «persons») даст JSON-результат, являющийся массивом объектов, каждый из которых содержит, как минимум, уникальный идентификатор (для индивидуальной выборки) и, как правило, какой-то краткий описательный текст, пригодный для отображения в списке вариантов.
  • GET-запрос к ресурсу данного типа с идентификатором как частью URL («persons/1234», где 1234 — уникальный идентификатор персоны, которая нас интересует) даст JSON-результат, являющийся (обычно) единственным JSON-объектом, который описывает ресурс с каким-то уровнем детализации.

Web API также будут использовать PUT, POST и DELETE, но пока что я сосредоточусь только на получении данных.

Итак, предполагая, что тип ресурса — «persons», вы создадите две конечные точки: одну с меткой «/persons», а другую с «/persons/<unique identifier>». Для начала достаточно небольшой «базы данных» персон, с которыми вы будете работать — хватит имен, фамилий и их текущего статуса (того, чем они занимаются в данный момент) (рис. 2).

Рис. 2. Создание небольшой базы данных персон

var personData = [
  {
    "id": 1,
    "firstName": "Ted",
    "lastName": "Neward",
    "status": "MEANing"
  },
  {
    "id": 2,
    "firstName": "Brian",
    "lastName": "Randell",
    "status": "TFSing"
  }
];

Не совсем SQL Server, но пока сойдет.

Далее вам нужна конечная точка для полного набора persons:

var getAllPersons = function(req, res) {
  var response = personData;

  res.send(JSON.stringify(response));
};
app.get('/persons', getAllPersons);

Заметьте, что в этом случае механизм сопоставления маршрутов использует автономную функцию (getAllPersons), что более распространено, так как это немного помогает поддержанию принципа разделения обязанностей: данная функция действует как контроллер (в Model-View-Controller). Пока что я использую JSON.stringify для сериализации массива JavaScript-объектов в JSON-представление, но впоследствии применю что-нибудь более элегантное.

Затем вам понадобится конечная точка для индивидуальных объектов person, но это потребует дополнительной работы, поскольку идентификатор персоны нужно извлекать как параметр, а в Express это делается определенным образом. Один из способов (пожалуй, более простой на первый взгляд) — задействовать объект params объекта request (параметр req для функции, используемой в карте маршрутов) для получения параметра, указанного в маршруте, но Node.js позволяет добиться большего от параметра-функции: это разновидность фильтра, вызываемого, когда обнаруживается параметр с определенным шаблоном именования:

app.get('/persons/:personId', getPerson);

app.param('personId', function (req, res, next, personId) {
  debug("personId found:",personId);
  var person = _.find(personData, function(it) {
    return personId == it.id;
  });
  debug("person:", person);
  req.person = person;
  next();
});

Когда маршрут вызывается, все, что следует за «/persons» (как в «/persons/1»), будет связано в параметр с именем personId — то же самое вы могли бы найти в ASP.NET MVC. Но потом, когда используется функция param (которая вызывается при вызове любого маршрута с «:personId»), запускается связанная функция. Эта функция ищет нужное в крошечной базе данных personData (используя функцию find из пакета lodash, как показано в предыдущем фрагменте кода). Но потом это добавляется к объекту req (поскольку JavaScript-объекты всегда типизируются динамически, такая операция тривиальна), чтобы он был доступен в данном случае функции getPerson, — теперь это тоже довольно тривиально, так как объект, который вы хотите возвратить, уже получен:

var getPerson = function(req, res) {
  if (req.person) {
    res.send(200, JSON.stringify(req.person));
  }
  else {
    res.send(400, { message: "Unrecognized identifier: " +
      identifier });
  }
};

Видите, что я подразумеваю под тривиальностью?

Заключение

Мне нужно сделать с помощью Express кое-что еще, но, хотя я только-только разошелся, на сегодня места у меня уже больше нет, так что… удачи в кодировании!


Тэд Ньюард (Ted Neward) — директор iTrellis по технологиям (эта компания предоставляет консалтинговые услуги). Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com или ted@itrellis.com.

Выражаю благодарность за рецензирование статьи эксперту Шону Уайлдермуту (Shawn Wildermuth).