Тест-мод для одностраничных приложений. Часть 1. Хранилище данных

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

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

Когда API серверной части нам известно, то можно попробовать эмулировать ответы сервера. Простое решение — это заготовленные JSON’ы по одному на каждый API-запрос. Более сложное и куда более функциональное решение — это автогенерируемые по определенным правилам данные, с возможностью их изменения.

Рассмотрим этот вариант. Что для него нужно:

  • Средство для хранения данных и работы с ними
  • Средство для генерации связанных данных
  • Отдельный сервер, эмулирующий реальный
  • Отдельный инструмент для внесения правок в данные на лету

Первых два пункта можно объединить в один и назвать его «Хранилище данных». Казалось бы, разных БД существует великое множество. Но под текущую задачу было написано отдельное решение — Lair. Это простое хранилище, которое все данные держит в памяти и ничего не пишет в файлы. Оно состоит из двух частей — собственно Хранилище и Фабрики.

При работе над Lair небольшое вдохновение черпалось из Ember Mirage.

Lair можно установить следующей командой:

1
npm i lair-db --save

Помимо того, что Хранилище содержит в себе все данные, оно еще и дает ряд стандартных CRUD-методов, для работы с ними. Экземпляр Хранилища можно получить так:

Здесь и далее примеры написаны на TypeScript

1
2
import {Lair} from 'lair-db';
const lair = Lair.getLair();

Работу с Хранилищем можно разделить на два этапа. Первый — это его начальное заполнение сгенерированными данными. Второй — это изменение данных в нем при запросах к нашему «серверу». Для первого этапа используются Фабрики. В них задаются правила для генерации данных при начальном заполнении Хранилища. Рассмотрим пример:

1
2
import {Factory} from 'lair-db';
const factoryInstance = Factory.create({name: 'some-name'});

factoryInstance — это экземпляр Фабрики, которая просто существует, но ничего не делает. Чтоб Хранилище могло с ней взаимодействовать, Фабрику надо зарегистрировать:

1
2
3
import {Factory} from 'lair-db';
const factoryInstance = Factory.create({name: 'some-name'});
Lair.getLair().registerFactory(factoryInstance);

Метод registerFactory принимает два параметра — собственно экземпляр Фабрики (обязательно) и имя, по которому он будет доступен в Хранилище. Если не передавать второй параметр, то в качестве имени будет использоваться значение атрибута name, заданное при создании Фабрики. Имя должно быть уникальным. В примере выше factoryInstance является «пустой». Записи, созданные из него, не будут содержать никаких атрибутов кроме id. Идентификатор создается автоматически и является автоинкрементным числом в строчной форме (проще говоря '1', '2', ..., '100500'). Изменять идентификатор нельзя.

Чтоб Записи, созданные Фабрикой, содержали какие-либо данные, надо задать атрибут attrs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {Factory} from 'lair-db';
import faker = require('faker');
const {name} = faker;
 
const unit = Factory.create({
  name: 'unit',
  attrs: {
    fistName: Factory.field({
      value() {
        return name.firstName();
      }
    }),
    lastName: Factory.field({
      value() {
        return name.lastName();
      }
    }),
    fullName: Factory.field({
      value() {
        return `${this.firstName} ${this.lastName}`;
      }
    })
  }
});

Теперь Записи unit будут с такими атрибутами — id, firstName, lastName и fullName:

1
2
3
4
const lair = Lair.getLair();
lair.registerFactory(unit);
const units = lair.createRecords('unit', 1);
console.log(units); // [{id: '1', firstName: 'Jim', lastName: 'Raynor', fullName: 'Jim Raynor'}]

Тут стоит обратить внимание на fullName. Его значение — это результат конкатенации значений firstName и lastName на момент создания Записи. То есть, последующее изменение любого из этих значений НЕ приведет к обновлению значения fullName.

В примере выше был использован метод createRecords. С его помощью создаются Записи при начальном заполнении Хранилища. В него передается имя Фабрики и количество Записей, которые надо создать.

В Хранилище можно зарегистрировать любое (в рамках разумного) количество Фабрик, главное, чтоб у них были уникальные имена. Обычно данные в приложении связаны тем или иным образом. Между Фабриками тоже можно задавать связи. Для этого есть два метода — Factory.hasOne и Factory.hasMany. Первый используется для случаев, когда связь вида «один-к-одному» или «многие-ко-одному», а второй — для «один-ко-многим» и «многие-ко-многим».

Для начальной автогенерации данных нужно задать свойство createRelated. Это объект, ключами которого являются имена Фабрик, а значениями — количество Записей, которые надо создать.

В качестве примера возьмем две Фабрики unit и squad. Один боец (юнит) может быть только в одном отряде, но в самом отряде может быть много бойцов:

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
34
import {Lair, Factory} from 'lair-db';
import faker = require('faker');
const {name} = faker;
 
const lair = Lair.getLair();
const unit = Factory.create({
  name: 'unit',
  attrs: {
    name: Factory.field({
      value() {
        return name.firstName();
      }
    }),
    squad: Factory.hasOne('squad', 'units')
  }
});
 
const squad = Factory.create({
  name: 'squad',
  attrs: {
    name: Factory.field({
      value() {
        return getRandomSquadName(); // определено где-то в другом месте
      }
    }),
    units: Factory.hasMany('unit', 'squad')
  },
  createRelated: {
    unit: 4
  }
});
lair.registerFactory(unit);
lair.registerFactory(squad);
lair.createRecords('squad', 3);

Здесь стоит обратить внимание на параметры, которые принимают методы hasOne и hasMany. Первым идет имя Фабрики, с которой устанавливается связь, а второй — это имя свойства из связываемой Фабрики для двухсторонней связи. Если вторым параметром передать null, то связь будет одностронней. Например, если в Фабрике squad задать units: Factory.hasMany('unit', null), а в Фабрике unit вообще не задавать свойство squad, то получится, что отряды по-прежднему будут связаны с юнитами, но сами юниты никак не будут связаны с отрядами.

В createRelated для Фабрики squad указано, что для каждой из созданных Записей будет создано по четыре Записи unit, при чем каждая четверка юнитов будет связана со своим отрядом, а каждый отряд будет связан с соответствующими юнитами.

В тоже время вызов lair.createRecords('unit', 2) создаст двух юнитов, которые никак не будут связаны ни с каким отрядом.

Фабрики могут быть связаны друг с другом в любых количествах и с любой вложенностью. Например, у Записей Фабрики cluster есть много Записей Фабрики host, у которых в свою очередь есть отдельная файловая структура в виде Фабрик dir и file (при чем у dir есть отношения самой к себе в виде родительской директории и вложенных поддиректорий). Здесь мы подошли к рефлексивным отношениям в рамках одной Фабрики. В Lair это реализуется очень просто:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {Lair, Factory} = require('lair-db');
const dir = Factory.create({
  name: 'dir',
  attrs: {
    parent: Factory.hasOne('dir', 'dirs'),
    dirs: Factory.hasMany('dir', 'parent', {reflexive: true, depth: 3})
  },
  createRelated: {
    dirs: 3
  }
});
const lair = Lair.getLair();
lair.registerFactory(dir);
lair.createRecords('dir', 1);

Здесь parent — это родительская директория, а dirs — дочерние каталоги. Определение parent ничем не отличается от любого другого определения связи между Фабриками. А вот у dirs в Factory.hasMany передается третий параметр — объект с дополнительными настройками. Свойство reflexive указывает на то, что поле Фабрики относится к той-же самой фабрике. Свойство depth указывает на то, насколько «вглубь» надо сгенерировать Записи dir. Другими словами, сколько уровней вложенности будет у директорий. В сочетании с createRelated.dirs можно подсчитать, сколько реально Записей dir будет создано.

Из-за lair.createRecords('dir', 1) будет создана одна директория. И это уже первый уровень вложенности. Далее, для нее будет создано три поддиректории. Это второй уровень вложенности. Наконец, для каждой из этих трех будет создано еще по три. И это последний (третий) уровень вложенности. В итоге получается, 1 + 3 + 3 * 3 = 13.

В общем виде формула получается такой:

dirsCount = dirs0 + dirs1 + … + dirsdepth-1

Можно расчитать кол-во созданных Записей для случая depth = 5 и createRelated.dirs = 4. Будет следующее: 40 + 41 + 42 + 43 + 44 = 341. Видим, что количество созданных Записей возрасло очень сильно. Если же взять depth = 6, а createRelated.dirs = 6, то общее кол-во Записей станет уже больше 9000. Об этом стоит помнить, когда вы создаете Записи с большой вложенностью других Записей.

В createRelated можно задавать не только конкретное значение, но и функцию, результат которой будет браться за требуемое значение:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {Lair, Factory} = require('lair-db');
const faker = require('faker);
const dir = Factory.create({
  name: 'dir',
  attrs: {
    parent: Factory.hasOne('dir', 'dirs'),
    dirs: Factory.hasMany('dir', 'parent', {reflexive: true, depth: 3})
  },
  createRelated: {
    dirs() {
      return faker.random.number({min: 3, max: 5});
    }
  }
});

И так, для начального заполнения Хранилища надо выполнить следуюющее:

  • Создать экземпляры Фабрик через Factory.create
  • Зарегистрировать их в Хранилище через lair.registerFactory
  • Создать нужное количество Записей через lair.createRecords (можно вызывать сколько угодно раз)

Что и как можно делать с Хранилищем после этого? Теперь Хранилище готово к тому, чтоб заменить реальную БД приложения. В Lair есть ряд методов для работы с данными:

  • createOne
  • updateOne
  • deleteOne
  • getOne
  • queryOne
  • getAll
  • queryMany

Метод createOne создает одну Записи в Хранилище и возвращает ее. Пример:

1
2
3
const newUnit = lair.createOne('unit', {
  name: 'Sarah Kerrigan'
});

В отличии от createRecords, метод createOne не использует Фабрики для генерации данных, а использует JSON, который ему передан вторым параметром. Соответственно, не будут создаваться Записи Фабрик, которые связаны с unit. В общем, createOne всегда создает только одну Запись, а createRecords, даже будучи вызванным с 1 вторым параметром, создаст как минимум одну Запись, а еще все из createRelated.

Для обновления существующей Записи в Lair есть метод updateOne. Он принимает три параметра — имя Фабрики, идентификатор Записи и JSON с новыми данными:

1
2
3
const updatedUnit = lair.updateOne('unit', '1', {
  name: 'Rory Swann'
});

Результатом вызова updateOne является обновленная Запись unit с идентификатором 1.

Удалить Запись их Хранилища можно с помощью метода deleteOne. Он принимает два параметра — имя Фабрики и идентификатор Записи. Этот метод не возвращает ничего:

1
lair.deleteOne('unit', '1');

Рассмотренные три метода могут явно или не явно влиять на связи между Записями. Начнем с метода deleteOne. Если удаляемся Запись связана с какими либо другими, то их связи будут обновлены автоматически. Если отношение было «к-одному», то значение будет null, а если было «ко-многим», то из массива будет убрана удаленная Запись. Например, если взять отряд (squad) и 4 юнита (unit) в нем, то при удалении Записи squad, у каждого связанного с ним юнита будет сброшено значение свойства squad в null. Если же не трогать squad, а удалить одного юнита, то у Записи squad из свойства units будет убран удаленный юнит. Это происходит автоматически.

В методах createOne и updateOne можно явно задать значения связей. Например, при создании Записи unit можно указать идентификатор отряда, к которому ее надо привязать:

1
2
3
4
lair.createOne('unit', {
  name: 'Jim Raynor',
  squad: '1'
});

После выполнения этой строчки в отряд с идентификатором 1 будет добавлен новосозданный юнит. Обязательное условие — связываемая Запись должна уже существовать.

При задании создании Записи squad, ей так же можно задать юнитов:

1
2
3
4
lair.createOne('squad', {
  name: 'Raynor\'s Raiders',
  units: ['1', '2', '3']
});

Каждый из юнитов с идентификаторами '1', '2', '3' будет добавлен в новый отряд, а из их старого отряда (если он был), они будут удалены.

С методом updateOne ситуация точно такая же — можно явно указать идентификаторы Записей, с которыми надо связать обновляемую. Условие про существование всех затронутых Записей тут так же в силе.

Мы рассмотрели методы, которые что-либо меняют в Хранилище, а теперь перейдем к методам чтения. Их аж четыре. Начнем с getOne. Этот метод возвращает одну Запись заданной Фабрики с требуемым идентификатором или null, если таковой нет:

1
const existingUnit = lair.getOne('unit', '1');

Метод getAll возвращает все Записи для заданной Фабрики:

1
const allUnits = lair.getAll('unit');

Метод queryOne возвращает первую Запись требуемой Фабрики, для которой функция обратного вызова вернет true:

1
const unit = lair.queryOne('unit', unit => unit.id === '1');

Метод queryMany вернет все Записи требуемой Фабрики, для которых функция обратного вызова вернет true:

1
const units = lair.queryMany('unit', unit => Number(unit.id) > 5);

Все рассмотренные методы (кроме deleteOne) возвращают одну Запись или массив Записей (случаи с null не берем). Это копии данных из Хранилища, так что их изменение никак не отразится на самом Хранилище. Для этого подходят только методы createOne, updateOne и deleteOne.

Каждая из полученных Записей содержит данные по всем связанным с ней Записям. То есть, результатом выполнения lair.getOne('squad', '1'); будет JSON вида:

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
{
  "id": "1",
  "name": "Blood Ravens",
  "units": [
    {
      "id": "1",
      "name": "Isador Akios",
      "squad": "1"
    },
    {
      "id": "2",
      "name": "Gabriel Angelos",
      "squad": "1"
    },
    {
      "id": "3",
      "name": "Anteas",
      "squad": "1"
    },
    {
      "id": "4",
      "name": "Aramus",
      "squad": "1"
    }
  ]
}

В случае, когда у Записи много связей, то в результате будет очень большой JSON, который зачастую и не нужен. Да и время его формирования будет больше. Lair дает два способа избежать этого. Первый — это возможность указать, насколько «глубоко» надо брать связанные Записи. А второй — это возможность указать, Записи каких Фабрик надо проигнорировать. Пример:

1
lair.getOne('squad', '1', {depth: 1});

В результате будет следующее:

1
2
3
4
5
{
  "id": "1",
  "name": "Blood Ravens",
  "units": ["1", "2", "3", "4"]
}
1
lair.getOne('squad', '1', {ignoreRelated: ['unit']});

В данном случае результат будет другой:

1
2
3
4
{
  "id": "1",
  "name": "Blood Ravens"
}

Такой «экономный» подход можно применять для всех методов, которые возвращают Записи:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lair.queryOne('squad', squad => squad.id === '1', {depth: 1});
// {id: '1', name: 'Blood Ravens', units: ['1', '2', '3', '4']}
lair.queryOne('squad', squad => squad.id === '1', {ignoreRelated: ['unit']});
// {id: '1', name: 'Blood Ravens'}
 
lair.queryMany('squad', squad => Number(squad.id) >= 1, {depth: 1});
// [{id: '1', name: 'Blood Ravens', units: ['1', '2', '3', '4']}]
lair.queryMany('squad', squad => Number(squad.id) >= 1, {ignoreRelated: ['unit']});
// [{id: '1', name: 'Blood Ravens'}]
 
lair.getAll('squad', {depth: 1});
// [{id: '1', name: 'Blood Ravens', units: ['1', '2', '3', '4']}]
lair.getAll('squad', {ignoreRelated: ['unit']});
// [{id: '1', name: 'Blood Ravens'}]
 
lair.updateOne('squad', '1', {name: 'White Consuls'}, {depth: 1});
// {id: '1', name: 'White Consuls', units: ['1', '2', '3', '4']}
lair.updateOne('squad', '1', {name: 'White Consuls'}, {ignoreRelated: ['unit']});
// {id: '1', name: 'White Consuls'}

Из всего вышеизложенного можно сделать вывод, что Lair подходит на роль инструмента для генерации, хранения и обработки данных для «фейк»-сервера. В ReadMe проекта находится краткая документация с примера использования, а так же несколько функций, которые остались за кадром этой заметки.

В следующей части мы поговорим про сам Сервер — каким он может быть и что от него требуется.

, ,

Тест-мод для одностраничных приложений 

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

Top ↑ | Main page | Back