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

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

Рассмотрим немного «теории», о которой умолчали ранее. Начнем со Swarm-Host. Когда вы запускаете сервер, то помимо явно зарегистрированных Роутов, в нем создается еще ряд «рабочих» Роутов, а именно:

1
2
3
4
5
6
7
GET    /lair/meta
GET    /lair/factories/:factoryName
GET    /lair/factories/:factoryName/:id
DELETE /lair/factories/:factoryName/:id
PATCH  /lair/factories/:factoryName/:id
PUT    /lair/factories/:factoryName/:id
POST   /lair/factories/:factoryName

Здесь /lair — это настраиваемый префикс:

1
2
3
import {Server} from 'swarm-host';
const server = Server.getServer();
server.lairNamespace = '/custom-lair-namespace';

Каждый из вышеописанных Роутов (кроме /meta) является «оберткой» над методами LairgetAll, getOne, deleteOne, updateOne (PATCH и PUT) и createOne.

А что по /lair/meta? У Lair есть метод getDevInfo, который возвращает «рабочую» информацию по данным в своей БД. Роут /lair/meta возвращает эти данные. Рассмотрим их подробнее. Предположим, что у нас есть хорошо знакомые Фабрики 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
import {Lair, Factory} from 'swarm-host';
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();
      },
      preferredType: 'string'
    }),
    squad: Factory.hasOne('squad', 'units')
  }
});
 
const squad = Factory.create({
  name: 'squad',
  attrs: {
    name: Factory.field({
      value() {
        return getRandomSquadName(); // определено где-то в другом месте
      },
      preferredType: 'string'
    }),
    units: Factory.hasMany('unit', 'squad')
  },
  createRelated: {
    unit: 4
  }
});

После регистрации этих Фабрик метод getDevInfo вернет следующее:

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
35
36
37
38
{
  squad: {
    count: 0,
    id: 1,
    meta: {
      name: {
        allowedValues: [],
        type: 1,
        preferredType: "string"
      },
      units: {
        factoryName: "unit",
        invertedAttrName: "squad",
        type: 3,
        reflexive: false,
        reflexiveDepth: 2
      }
    }
  },
  unit: {
    count: 0,
    id: 1,
    meta: {
      name: {
        allowedValues: [],
        type: 1,
        preferredType: "string"
      },
      squad: {
        factoryName: "squad",
        invertedAttrName: "units",
        type: 2,
        reflexive: false,
        reflexiveDepth: 2
      }
    }
  }
}

Для каждой Фабрики возвращается количество Записей на данный момент (count), идентификатор Записи (id), которая будет создана следующей, а так же описание атрибутов Фабрики (поле meta). Рассмотрим его подробнее. preferredType — это желаемый тип значения для данного атрибута. Определение этого значения никак не влияет на работу Lair. Массив allowedValues — это список значений, которые «разрешены» для данного атрибута. Опять-таки, этот список — это рекомендации, а не требования. Поле type указывает на тип атрибута. Единица — это «обычное» значение. Двойка — это отношение hasOne. Тройка — это отношение hasMany. Флаг reflexive указывает на то, является ли атрибут отношением Записи к Записям той же Фабрики. Поле factoryName содержит имя Фабрики, Запись или Записи которой находятся в значении атрибута. Поле invertedAttrName — это имя атрибута у Фабрики factoryName, которое связано с текущим атрибутом.

Этих данных, а так же Роутов, описанных выше, достаточно, чтоб сделать SPA-приложение, которое будет работать с любым сервером на основе Swarm-Host.

Так как имеется некоторый опыт работы с Ember.JS, то он и был взят для решения данной задачи. На выходе получился Swarm-Host-UI, который можно установить так:

1
2
3
git clone https://github.com/onechiporenko/swarm-host-ui
cd swarm-host-ui
npm install

Далее в файле app/adapters/application.js указываем правильные значения для свойств host и namespace.

Запускается Swarm-Host-UI следующей командой:

1
ember server

Теперь по адресу http://localhost:4200 вам доступен интерфейс, через который можно создавать, удалять и редактировать любую Запись любой Фабрики.

С точки зрения использования Swarm-Host-UI больше добавить нечего. Однако заглянем «под капот» и посмотрим, как же оно работает.

В Эмбере каждая из Архитектурных Единиц является Фабрикой, которая регистрируется в приложении Модели — это тоже Фабрики. А Фабрики можно создавать и регистрировать в приложении «на лету». Вышеописанные unit и squad В Эмбере можно записать так:

1
2
3
4
5
6
7
8
9
10
11
import DS from 'ember-data';
 
const squad = DS.Model.extend({
  name: DS.attr('string'),
  units: DS.hasMany('unit')
});
 
const unit = DS.Model.extend({
  name: DS.attr('string'),
  squad: DS.belongsTo('squad')
});

А в само приложение их можно добавить «на лету» так:

1
2
3
4
5
6
7
8
9
10
11
12
import Route from '@ember/routing/route';
import {getOwner} from '@ember/application';
 
export default Route.extend({
  beforeModel() {
    const appInstance = getOwner(this);
    const modelName = 'squad';
    appInstance.register(`model:${modelName}`, DS.Model.extend({
      // some attributes
    }));
  }
});

Функция getOwner возращает Экземпляр Приложения, у которого есть метод register. С его помощью можно зарегистрировать любую Фабрику.

На основе данных из /lair/meta можно создать все Фабрики Моделей. Так как авторы Эмбера не рекомендуют делать ничего асинхронного в Инициализаторах, то запрос на /lair/meta сделаем из Роута application:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// app/routes/application.js
import Route from '@ember/routing/route';
import {getOwner} from '@ember/application';
import {inject as service} from '@ember/service';
import {get} from '@ember/object';
import DS from 'ember-data';
 
export default Route.extend({
  /**
   * From `ember-ajax` addon
   */
  ajax: service(),
 
  beforeModel() {
    const appInstance = getOwner(this);
    const applicationAdapter = appInstance.lookup('adapter:application');
    const host = get(applicationAdapter, 'host');
    const namespace = get(applicationAdapter, 'namespace');
    const url = `${host}/${namespace}/meta`;
    return get(this, 'ajax').request(url).then(lairDevInfo => {
      Object.keys(lairDevInfo).forEach((modelName, index) => {
        const attrs = {};
        const meta = lairDevInfo[modelName].meta;
        Object.keys(meta).forEach(attrName => {
          const attrMeta = meta[attrName];
          if (attrName !== 'id') {
            if (attrMeta.type !== 2 && attrMeta.type !== 3) { // not belongsTo and not hasMany
              const type = attrMeta.preferredType || 'string';
              const attrArgs = [];
              if(['string', 'boolean', 'number', 'array', 'object'].includes(type)) {
                attrArgs.push(type);
                if (attrMeta.defaultValue) {
                  attrArgs.push({defaultValue: attrMeta.defaultValue});
                }
              }
              attrs[attrName] = DS.attr(...attrArgs);
            }
            if (attrMeta.type === 2) {
              attrs[attrName] = DS.belongsTo(attrMeta.factoryName, {
                inverse: attrMeta.invertedAttrName
              });
            }
            if (attrMeta.type === 3) {
              attrs[attrName] = DS.hasMany(attrMeta.factoryName, {
                inverse: attrMeta.invertedAttrName
              });
            }
          }
        });
        appInstance.register(`model:${modelName}`, DS.Model.extend(attrs));
      });
    });
  }
});

В beforeModel выполняется запрос на /lair/meta и полученный результат обходится в цикле, на каждом шаге которого регистрируется Фабрика Модели. Тип каждого ее атрибута берется из preferredType. Если его значения нет, то используется string. Значение по умолчанию берется из defaultValue. Связи между Моделями задаются через factoryName и invertedAttrName.

Чтоб научить Swarm-Host-UI правильно общаться с сервером надо описать Адаптер и Сериализатор Приложения. Адаптер получается очень простой:

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/adapters/application.js
import DS from 'ember-data';
 
export default DS.JSONAPIAdapter.extend({
  host: 'http://localhost:54321',
  namespace: 'lair',
  headers: {
    'Content-Type': 'application/json'
  },
  pathForType(modelName) {
    return `factories/${modelName}`;
  }
});

Значения host и namespace Адаптера используются в Роуте application.

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

В качестве заключения. Swarm-Host-UI — это относительно небольшое одностраничное приложение с одной страницей (точнее, с одним Роутом). Его настройка минимальна и не требует знаний Эмбера. Использование Swarm-Host-UI не является обязательным для решения изначальной задачи про «сервер с тестовыми данными для SPA-приложений», однако очень облегчает использование этого сервера.

, ,

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

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

Top ↑ | Main page | Back