Миграция с Express 2.x на Express 3.x

Express 3 вышел в 2012 году. Эта статья разбирает конкретный переход с 2.x на 3.x и полезна при чтении старого проекта; она не предлагает устанавливать Express 3 для нового приложения. Обе ветки не поддерживаются. Изменения сверяются с официальной миграцией 2.x → 3.x; для действующего приложения нужен дальнейший переход на поддерживаемую ветку.

Удалено

  • res.render() опция "status" (используйте res.statusCode= или res.status(code).render(...))
  • res.render() опция "charset" (используйте res.charset=)
  • res.local(name, value) (используйте res.locals.name = value или res.locals({ name: value }))
  • app.dynamicHelpers() (используйте middleware + res.locals)
  • app.helpers() (используйте app.locals)
  • концепция "layout" (теперь это задача шаблонизатора)
  • partial() (теперь это задача шаблонизатора)
  • res.partial()
  • настройка "view options", используйте app.locals
  • настройка "hints"
  • req.isXMLHttpRequest (используйте req.xhr)
  • app.error() (используйте middleware с аргументами (err, req, res, next))
  • req.flash() (просто используйте сессии: req.session.messages = ['foo'] или как-то так)
  • connect-flash может использоваться вместо req.flash()
  • настройка jsonp callback была удалена (используйте res.jsonp())

Изменилось

Здесь перечислены изменения именно Express 3. В текущем Express сессии и парсер cookies подключаются отдельными пакетами, а значения секретов берутся из конфигурации окружения. Не копируйте старую строку express.cookieParser('secret') как рабочую конфигурацию.

  • req.header(field[, defaultValue]) изменили на req.get(field) (оставлено для обратной совместимости)
  • res.header(field[, value]) изменили на res.set(field, value) / res.get(field) (оставлено для обратной совместимости)
  • переименовали app.register() на app.engine()
  • шаблонизатор теперь должен соответствовать не engine.compile(str, options) => Function, а engine.__express(filename, options, callback)
  • express.createServer() теперь просто express() (но для обратной совместимости сохранено).
    • помните, что express() теперь не возвращает экземпляр http.Server. (См. раздел Фабрика приложения, там детальнее)
  • Используйте express.cookieParser('secret') вместо того, чтобы передавать "секрет" в middleware express.session(). Опция 'secret' в middleware express.session() была удалена.

View options

Настройка "view options" больше не нужна, app.locals - это локальные переменные, которые смешиваются с теми, что мы передаём в res.render(), поэтому app.locals.pretty = true эквивалентно res.render(view, { pretty: true }).

Фабрика приложения

express() возвращает функцию-обработчик HTTP-запросов, а не «единичный такт» приложения. HTTP-сервер вызывает её для входящего запроса. Это позволяет передать одно приложение в http.createServer() или https.createServer():

...
var app = express();
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);

Для удобства и простоты метод app.listen() принимает те же аргументы, оборачивая их в HTTP сервер. Следующие примеры эквивалентны:

var app = express();
app.listen(3000);

и

var app = express()
  , http = require('http');

http.createServer(app).listen(3000);

Это означает, что методы, присутствующие в http.Server.prototype, более не присутствуют в app, например, app.address() теперь должен вызываться у экземпляра сервера, который возвращается из app.listen() или того, который вы создадите с помощью http.createServer(app).

Совместимость с Socket.IO

Ниже сохранён исторический способ подключения Socket.IO. У современных версий API отличается: следуйте инструкции сервера Socket.IO, где используется new Server(httpServer). Не смешивайте версии в одном примере.

Метод .listen() Socket.IO принимает аргументом экземпляр http.Server. С версии 3.x, express() не возвращает экземпляр http.Server. (См. раздел Фабрика приложения выше.) Чтобы Socket.IO заработал с Express 3.x, создайте вручную и передайте экземпляр http.Server в метод .listen() Socket.IO.

Примечание переводчика. Я, например, передаю в Socket.IO то, что вернуло мне app.listen(), это ведь тоже http.Server

var app = express()
  , http = require('http')
  , server = http.createServer(app)
  , io = require('socket.io').listen(server);

server.listen(3000);

Интеграция шаблонизаторов

Для совместимости с Express 2.x шаблонизатор должен экспортировать следующее:

exports.compile = function(templateString, options) {
  return a Function;
};

Совместимые с Express 3.x шаблонизаторы должны экспортировать следующее:

exports.__express = function(filename, options, callback) {
  callback(err, string);
};

Если шаблонизатор не экспортирует этот метод, все равно есть способ - метод app.engine() позволяет сопоставить любую функцию определённому расширению файла. Допустим, есть библиотека markdown, и нужно рендерить .md-файлы, но эта библиотека не поддерживает Express, тогда ваш вызов app.engine() будет выглядеть примерно так:

var markdown = require('some-markdown-library');

app.engine('md', function(path, options, fn){
  fs.readFile(path, 'utf8', function(err, str){
    if (err) return fn(err);
    str = markdown.parse(str).toString();
    fn(null, str);
  });
});

Изменения системы шаблонов

После удаления концепции layouts и partials в Express 3.x шаблонизаторы теперь имеют больше контроля над операциями файлового ввода-вывода. Интеграция с шаблонизаторами теперь существенно упростилась, также упростились внутренние механизмы системы шаблонов.

Поскольку layouts и partials вышли из Express, наследование реализует шаблонизатор. В Pug для этого есть extends и block, а для повторяемых частей — include и mixin. Устаревшая статья на devthought больше не нужна как инструкция по настройке.

Возврат старого partial() отдельным расширением — исторический вариант совместимости, а не обязательный шаг миграции. Сначала определите, какие шаблоны действительно используют старое поведение.

В старых проектах встречаются пакеты express-partials и ejs-locals. Они названы здесь для распознавания существующей зависимости; это не рекомендация добавлять их в новый проект.

Middleware для обработки ошибок

В версии 2.x метод app.error(callback) был на практике эквивалентен следующему:

app.error = function(fn){
  this.use(function(err, req, res, next){
    fn.apply(this, arguments);
  });
};

Причина в том, что Connect различает "обычное" middleware, и middleware для обработки ошибок через fn.length. У обычного fn.length <= 3, что соответствует (req, res, next), тогда как middleware для обработки ошибок должно иметь ровно 4 - (err, req, res, next).

В 2.x эта функциональность была обёрнута в синтаксический сахар, чтобы упростить API.

Проще говоря, всё, что нужно для перехвата ошибок, это определить другое middleware, но с четырьмя аргументами. Это middleware должно быть определено ниже всех остальных, чтобы они могли вызывать next(err) для передачи ошибки, как показано ниже:

app.use(express.bodyParser())
app.use(express.cookieParser())
app.use(express.session())
app.use(app.router) // the router itself (app.get(), app.put() etc)
app.use(function(err, req, res, next){
  // if an error occurs Connect will pass it down
  // through these "error-handling" middleware
  // allowing you to respond however you like
  res.send(500, { error: 'Sorry something bad happened!' });
})

Локальные переменные уровня запроса и приложения

app.locals содержит значения уровня приложения, например название сервиса. res.locals существует в рамках одного ответа и подходит для данных текущего пользователя. Не сохраняйте пользователя в app.locals: он станет общей переменной для запросов.

app.locals.siteName = 'JSman';
app.use(function (req, res, next) {
  res.locals.currentUser = req.user || null;
  next();
});
app.get('/profile', function (req, res) {
  res.render('profile');
});

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

Заметки и перевод: @jsmarkus