Тест-мод для одностраничных приложений. Часть 2. Сервер

В первой части мы рассмотрели решение для хранения данных, их обработки и генерации. Теперь пришел через самого сервера.

Изначально перед нами есть два пути:

  • Можно перехватывать запросы приложения прямо в нем. Примером готовых решений тут может быть Pretender и его «производные»
  • Можно сделать отдельный сервер, который доступен по отдельному адресу. В этом случае можно брать все, что построено на базе express.js

В любом случае, при запуске выбранного Сервера, Lair будет генерировать исходные данные и заполнять ими свою БД.

Вариант с отдельным сервером для меня предпочтительнее, так как при перезагрузке странички с приложением, БД не будет сбрасываться в изначальное состояние.

Поверх express.js написано очень много решений под разные задачи. Добавим еше одно — свое. Краткий список требований (без учета того, что Экспресс умеет сам по себе):

  • Интеграция Lair в сам Сервер и обработчики Роутов
  • Предопределенные обработчики Роутов для типовых операций
  • Готовые Роуты для работы с Lair «напрямую»
  • Возможность запускать «долгоживущие» задачи на Сервере

Исходя из таких требований и был написан Swarm-Host. Его можно установить следующей командой:

1
npm i swarm-host --save

Swarm-Host состоит из нескольких архитектурных единиц — Server, Route, Cron и Job. Рассмотрим их все по порядку.

Экземпляр класса Server — это надстройка над express()-приложением. Его можно получить выполнив:

1
2
import {Server} from 'swarm-host';
const server = Server.getServer();

Запустить и остановить его можно вызвав соответствующие методы:

1
2
server.startServer();
server.stopServer();

Для Сервера можно задать ряд настроек, таких как порт, пространство имен для Роутов, задержку отправки ответа и уровень «разговорчивости»:

1
2
3
4
server.port = '12345';
server.namespace = '/api/v100500/';
server.delay = 100;
server.verbose = true;

Последнее указывает на то, будут ли в консоли выводиться логи работы Swarm-host’а.

Сервер, который не обрабатывает никакие Роуты — это не очень полезный Сервер. Перейдем к ним. Роуты — это экземпляры класса Route. Их можно получить несколькими способами. Самый простой — это вызвать статический метод createRoute:

1
2
import {Route} from 'swarm-host';
const route = Route.createRoute();

Полученный route будет обрабатывать GET-запрос на / (учитывая namespace Сервера) и возвращать {} в ответе.

Метод createRoute принимает три параметра — тип запроса, путь и функцию-обработчик. То есть, чтоб сделать Роут для PUT-запроса на /some/path, надо выполнить следующее:

1
2
import {Route} from 'swarm-host';
const route = Route.createRoute('put', '/some/path');

Функция-обработчик, используемая по-умолчанию, не делает ничего, а только передает пустой объект ({}) на выход. В нее передается четыре параметра. Первых три — это стандартные Экспрессовские req, res и next (#). Четвертым параметром передается экземпляр Lair, проинициализированный на момент запуска Сервера (об этом — далее). То есть, у нас обычный express-обработчик с одним дополнительным параметром:

1
2
3
4
5
import {Route} from 'swarm-host';
const route = Route.createRoute('put', '/some/path', (req, res, next, lair) => {
  const data = lair.getAll('squad');
  res.json({data});
});

Помимо createRoute есть еще ряд методов для создания Роутов с предопределенным функционалом.

Для случая, когда ваш Роут возвращает все Записи определенного типа, есть метод get:

1
2
3
import {Route} from 'swarm-host';
const allSquads = Route.get('/squads', 'squad', {depth: 1});
const singleSquad = Route.get('/squads/:id', 'squad', {depth: 1});

Первый параметр — это путь, второй — имя Фабрики, третий — это дополнительные параметры, которые передаются в lair.getAll. Если же полученные данные надо еще как-то дополнительно обработать перед отправкой пользователю, то можно использовать функцию обработного вызова, которая передается четвертым параметров в get:

1
2
3
4
5
6
7
8
9
10
11
12
import {Route} from 'swarm-host';
const allSquadsRoute = Route.get('/squads', 'squad', {ignoreRelated: ['unit']}, (req, res, squads, lair) => {
  res.json({data: squads.map(squad => {
    const id = squad.id;
    delete squad.id;
    return {
      id,
      type: 'squads',
      attributes: squad
    };
  })});
});

В примере выше полученный массив squads переводится в формат JSON-API.

Функция обратного вызова получает четыре параметра, которые очень похожи на те, что есть у функции-callback у createRoute. Единственное отличие в третьем параметре — вместо next передается результат выполнения lair.getAll.

Метод Route.get можно использовать и для создания Роутов, которые должны возвращать одну запись:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Route} from 'swarm-host';
const singleSquadRoute = Route.get('/squads/:id', 'squad', {});
const customSingleSquadRoute = Route.get(
  '/squads/:id',
  'squad',
  {ignoreRelated: ['unit']},
  (req, res, squad, lair) => {
    const id = squad.id;
    delete squad.id;
    res.json({
      data: {
        id,
        type: 'squads',
        attributes: squad
      }
    });
  });

Для методов post, put/patch, delete тоже есть соответствующие статические методы у класса Route:

1
2
3
4
5
6
7
import {Route} from 'swarm-host';
const createSquadRoute = Route.post('/squads', 'squad', {});
const customCreateSquadRoute = Route.post(
  '/squads',
  'squad',
  {ignoreRelated: ['unit']},
  (req, res, createdSquad, lair) => res.json({data: createdSquad}));
1
2
3
4
5
6
7
import {Route} from 'swarm-host';
const updateSquadRoute = Route.patch('/squads/:id', 'squad', {});
const customUpdateSquadRoute = Route.post(
  '/squads/:id',
  'squad',
  {ignoreRelated: ['unit']},
  (req, res, updatedSquad, lair) => res.json({data: updatedSquad}));
1
2
3
4
5
6
7
import {Route} from 'swarm-host';
const updateSquadRoute = Route.put('/squads/:id', 'squad', {});
const customUpdateSquadRoute = Route.put(
  '/squads/:id',
  'squad',
  {ignoreRelated: ['unit']},
  (req, res, updatedSquad, lair) => res.json({data: updatedSquad}));
1
2
3
4
5
6
7
import {Route} from 'swarm-host';
const deleteSquadRoute = Route.delete('/squads/:id', 'squad', {});
const customDeleteSquadRoute = Route.delete(
  '/squads/:id',
  'squad',
  {},
  (req, res, resultOfLairDeleteOne, lair) => res.json({meta: {}}));

Создавать Роуты мы научились, но вот Сервер все равно про них ничего не знает. Есть несколько методов, с помощью которых можно зарегистрировать Роуты в Сервере. Начнем с самого простого server.addRoute:

1
2
3
4
import {Route, Server} from 'swarm-host';
const server = Server.getServer();
const route = Route.get('/squads', 'squad');
server.addRoute(route);

Данный метод принимает один параметр (сам Роут) и добавляет его как обработчик для GET-запроса на /squads. Вызывать addRoute стоит до того, как будет выполнен server.startServer.

Если у вас есть массив с Роутами, то можно воспользоваться методом server.addRoutes:

1
2
3
4
5
6
7
import {Route, Server} from 'swarm-host';
const server = Server.getServer();
const routes = [
  Route.get('/squads', 'squad'),
  Route.get('/squads/:id', 'squad')
];
server.addRoutes(routes);

Два вышеописанных метода хороши, когда Роутов не много. А вот когда их наберется десятка полтора-два, то захочется их вынести куда-то отдельно. Swarm-Host предлагает решение для таких случаев. Оно состоит в следующем:

  • Все Роуты хранятся в отдельной папке (например, ./routes)
  • Каждый Роут находится в отдельном файле.
  • Каждый Роут экспортируется из файла в виде export default Route.createRoute(...); (или любой другой метод, который возвращает Роут)
  • Роуты добавляются через метод server.addRoutesFromDir(`${__dirname}/routes`);

Метод addRoutesFromDir рекурсивно пройдет по указанной директории, возьмет все Роуты, которые найдет, и добавит их.

Так как Swarm-Host в своей работе использует Lair, то процесс создания и добавления в него Фабрик все еще актуален. По аналогии с Роутами есть три метода:

  • addFactory
  • addFactories
  • addFactoriesFromDir

Первый в качестве параметра принимает одни экземпляр Фабрики, второй — массив с экземплярами, а в третий передается путь до папки с Фабриками. Подробнее о самых Фабриках можно прочитать в первой части.

Так как Lair идет «встроенным» в Swarm-Host, то доступ к нему и Фабрикам можно получить так:

1
import {Lair, Factory} from 'swarm-host';

Как мы помним, в Lair есть метод createRecords, который изначально заполняет БД сгенерированными данными. В Swarm-Host есть метод с таким же названием и делает он точно тоже самое:

1
2
3
4
import {Route, Server} from 'swarm-host';
const server = Server.getServer();
server.addFactoriesFromDir(`${__dirname}/factories`);
server.createRecords('squad', 10);

Данный метод может быть вызван только до запуска сервера.

Так как в основе Swarm-Host находится express.js, то было бы неправильным забрать возможность добавлять middleware к обработчикам Роутов. Метод server.addMiddleware как раз и создан для этого. В него передается один параметр — функция обратного вызова, которая принимает три параметра (req, res и next). Знакомо, правда? Вот несколько примеров ее использования:

1
2
3
4
import compression = require('compression');
import {Server} from 'swarm-host';
const server = Server.getServer();
server.addMiddleware(compression());

В этом случае добавляется использование compression для каждого ответа на запросы.

1
2
3
4
5
6
7
8
import {Server} from 'swarm-host';
const server = Server.getServer();
server.addMiddleware((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, PUT, PATCH');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  next();
});

В данном примере к каждому ответу сервера добавляются несколько заголовков, разрешающие выполнять кросс-серверные запросы. Так как наш сервер используется только для тестирования, то никаких проблем от этого не будет.

Метод addMiddleware должен использоваться только до вызова server.startServer.

Метод startServer — это такая черта, после которой менять что-либо в сервере уже не имеет смысла. Более того, именно на момент его вызова начинают реально выполняться действия на сервере. То есть, только в этот момент регистрируются Роуты (в express-приложении) и Фабрики (в Lair), заполняется данными БД, добавляются middleware и, собственно, запускается express-приложение. В startServer можно передать функцию обратного вызова, которая выполнится после старта сервера.

Последним пунктом в списке «задач» для сервера значилась возможность запускать долгоживущие процессы. Что вкладывается в это понятие? Рассмотрим пример. Предположим, что в вашем приложении есть процесс репликации данных, который происходит раз в минуту и после своего завершения он в БД записывает небольшой «отчет». Вот такие процессы Swarm-Host умеет эмулировать. Для этого есть два класса — Cron и Job. Начнем с последнего. Экземпляры класса Job — это те самые процессы, которые выполняются с какой-то периодичностью. Их можно создать вызвав статический метод Job.createJob. В него передается один параметр — это JSON с опциями для создаваемого процесса. Таких опций аж девять:

  • id — идентификатор (строка) нового процесса. Должен быть уникальным в рамках сервера.
  • frequency — как часто должен выполняться процесс. Строка в формате cron.
  • ticksCount — количество «тактов» процесса.
  • ticksDelay — время между «тактами».
  • endTime — временная метка в будущем, после которой процесс будет остановлен.
  • immediateStart — должен ли процесс быть запущен сразу после создания.
  • firstTick — функция обратного вызова, выполняемая в начале каждого запуска процесса.
  • lastTick — функция обратного вызова, выполняемая в конце каждого запуска процесса.
  • tick — функция обратного вызова, выполняемая между firstTick и lastTick. Количество вызовов tick равно значению ticksCount.

Рассмотрим реализацию вышеописанного примера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {Job, Lair, Factory} from 'swarm-host';
const lair = Lair.getLair();
lair.registerFactory(Factory.create({
  name: 'replication',
  attrs: {
    progress: Factory.field({value: 0})
  }
}));
const ticksCount = 2;
const job = Job.createJob({
  id: 'custom-replication-job-id',
  frequency: '* * * * *',
  ticksCount,
  ticksDelay: 5,
  immediateStart: true,
  firstTick() {
    return lair.createOne('replication', {progress: 0});
  },
  tick(replication, tickNumber) {
    return lair.updateOne(
      'replication',
      replication.id,
      {progress: 100 * tickNumber / (ticksCount + 1)}
    );
  },
  lastTick(replication) {
    return lair.updateOne(
      'replication',
      replication.id,
      {progress: 100}
    );
  }
});

Что же тут такое получилось? В самом начале регистрируется Фабрика replication с одним полем progress. Далее создается экземпляр класса Job, который выполняется каждую минуту (см. frequency) с момента создания (см. immediateStart) и занимает около 15 секунд. Такое время получается из-за того, что firstTick выполняется на нулевой секунде каждой минуты, далее с интервалом в 5 секунд выполняются два (см. ticksCount) раза tick и один раз lastTick. Теперь о параметрах, которые принимают tick и lastTick. И так, каждый tick принимает два параметра. Первый — это то, что вернется из предыдущего вызова tick или же firstTick. Второй — это порядковый номер текущего tick (начинается с единицы). lastTick принимает только один параметр и это результат выполнения последнего tick.

В примере выше в firstTick в Lair создается новая Запись replication с значением progress = 0. Далее эта Запись передается в tick. В каждом их них обновляется значение поля progress. В lastTick это значение ставится в 100, что значит, что процесс завершился.

У каждого экземпляра класса Job есть три метода:

  • start — запускает процесс
  • stop — останавливает процесс
  • destroy — удаляет процесс

После вызова job.stop перестанут каждую минуту создаваться Записи replication в БД. И так до момента, пока не будет вызван job.start. После вызова job.destroy восстановить процесс уже нельзя — он удаляется безвозвратно.

Для управления процессов совершенно не обязательно хранить ссылку на job. Достаточно знать идентификатор процесса и использовать класс Cron:

1
2
3
4
5
6
import {Cron} from 'swarm-host';
const cron = Cron.getCron();
const id = 'custom-replication-job-id';
cron.stop(id);
cron.start(id);
cron.destroy(id);

При вызове Job.createJob созданный экземпляр автоматически регистрируется в Cron используя заданный ему идентификатор. В случае колизий, запись в Cron будет перезатерта (настоятельно не рекомендуется допускать таких ситуаций).

На данный момент это все, что надо знать о функционировании Swarm-Host. В третьей части речь пойдет о способе управления БД сервера «на лету».

, , , ,

Оставить комментарий

Top ↑ | Main page | Back