Архитектура приложения

Wasaby / Изоморфное приложение. Application

Для того, чтобы приложение SSR (server-side rendering) было проще поддерживать и быстрее создавать, необходимо, чтобы было как можно больше общего кода и меньше кода, который работает только лишь на сервере или на клиенте.

Основные причины в написании раздельного кода это то, что на сервере нет браузерного API и нельзя использовать глобальные объекты-одиночки(singleton). Так как построение страниц на сервере происходит в одном и том же процессе асинхронно, и несколько построений могут пересекаться по времени и использовать одни и те же глобальные объекты. И, соответственно, данные одной страницы могут затереть данные для другой страницы.

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

Окружение. Application/Env

Окружение — это набор модулей (свойства окружения) для изоморфной работы с браузерным API, которое доступно и на сервер. А также модули для хранения данных в рамках запроса построения.

  1. CookieApplication/Env.ICookie
  2. Location — аналог window.location
  3. Логгером — Application/Env.IConsole
  4. StoreApplication/Env.IStore
  5. StateRecieverApplication/Env.IStateReceiver

Как работать с окружением

require(['Application/Env'], ({ logger, cookie, location, getStore }) => {
    logger.info('myCookie: ', cookie.get('myCookie'));
    logger.info('hostname: ', location.hostname);

    const myStore = getStore('myStore');
    myStore.set('someKey', 'someValue');
    logger.info('some store Key: ', myStore.get('someKey'));
});

Store

Это такие объекты, где можно потоко-безопасно хранить данные между асинхронными вызовами методов компонента (в том числе и контрола). Проблема с асинхронными вызовами функции встречается при построении страницы на сервере, где несколько запросов на построение могут выполняться асинхронно/параллельно в одной и той же, глобальной области видимости.

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

/**
* NB! Со Store всегда работаем через свой синглтон.
*/
define('MySingletone', ['Application/Env', 'Types/entity'], (Env, Entity) => {
   var cacheStore = Entity.Record();
   const STORE_KEY = 'MySingletone';
   // Инициализируем хранилище
   Env.setStore('MySingletone', cacheStore);

   // NB! Наружу отдаем объект, который всегда работает с данными через Env::getStore
   return {
      read(key) {
         return Env.getStore('MySingletone').get(key);
      },
      write(key, value) {
         return Env.getStore('MySingletone').set(key, value);
      }
   }
});

require(['MySingletone'], (single) => {
   return function run() {
      const ourValue = Math.random();
      single.write("key", ourValue);
      setTimeout(() => {

         // Тут мы уверены, что в асинхронной операции получим ourValue.
         console.log("store value:", single.read("key"));
      }, 1000);
   }
});

StateReciever: Восстановление состояния объектов построенных на сервере

Для восстановления состояния модулей используется объект StateReciever, который берёт на себя ответственность о сериализации данных и размещении их в теле ответа на сервере и восстановления его на клиенте.

Момент сохранения состояния происходит в конце построения страницы, а восстановление в момент регистрации модуля.

Для того чтобы компонент мог восстанавливать свое состояние, он должен поддерживать интерфейс ISerializableState и регистрироваться в StateReciever.

Пример восстановления состояния в браузере для не визуальных модулей:

require(['Application/Env', 'Core'], ({ getStateReceiver }) => {
   class MyComponent {
      data = {};
      getState() {
         return this.data;
      }
      setState(data) {
         this.data = data;
      }
   }
   const component = new MyComponent();

   // Регистрируем компонент, для восстановление состояния с сервера.
   getStateReceiver().register("uuid", component);
    
   console.log('data from server: ', component.getState().someKey); // "someValue"
});

Настройки приложения. Application/Config

Для получения системных настроек приложения используется модуль Application/Config.

require(['Application/Config', 'Core'], (config) => {
   console.log('LogLevel: ', config.get('Application/Env.LogLevel'));
});
<!-- [Список стандартных системных настроек приложения](https://online.sbis.ru/doc/3e7cd71c-3ed9-480a-a7a5-a42b4fd8e145). -->

Wasaby / Продвинутое использование

Инициализация окружения. Application/Initializer

Для инициализации одиночек используется вызов метода init из модуля Application/Initializer. После синхронного вызова метода init можно использовать все вызовы из области Application/*.

require(['Application/Initializer', 'Application/Config'], ({ default: init }, config) => {
   const error = 2;
   const initConfigValue = {
      'Application/Env.LogLevel': error
   };
   init(initConfigValue);
   console.log('LogLevel: ', config.get('Application/Env.LogLevel'));
});

Фабрики окружения

При создании окружения используется фабрика, которая возвращает объекты, каждый из которых ответственен за API каждого из свойства окружения. По умолчанию в браузере используется Application/Env.EnvBrowser.

На сервисе представления свойства создает фабрика SbisEnv/PresentationService.

require(['Application/Initializer', 'SbisEnv/PresentationService'],
   ({ default: init }, { default: PresentationService }) => {
      const initConfigValue = {};
       
      let environmentFactory;
   if (typeof window === 'undefined') {
      environmentFactory = PresentationService;
   }
   init(initConfigValue, environmentFactory);
});

Передача данных с сервера на клиент

Для передачи состояния модулей с сервера на клиент, необходимо их сериализовать и разместить в теле страницы, а затем в момент инициализации передать их в StateReceiver.deserialize(data).

// Значение пришедшее с сервера с состоянием компонентов.
const serverValue = '{ \
   "uuid": { "someKey": "someValue" } \
}';
 
define('Core', ['Application/Initializer', 'Application/Env'], ({ default: init }, { StateReceiver }) => {
   const serverState = new StateReceiver();
   serverState.deserialize(serverValue);

   // Инициализируем приложение с состоянием с сервера.
   init({}, undefined, serverState);
});

Настройки приложения. Application/Config

При инициализации приложения первым аргументом передается объект, который является настройками приложения и доступен через объект Application/Config.

Для того, чтобы настройки приложения в любой точке времени были одинаковые, и не было проблем гонок состояния конфигурации, её можно изменять только целиком, и только на двух этапах жизненного цикла:

  • при инициализации приложения — указываются настройки по умолчанию;
  • при восстановлении конфигурации из StateReciever.

Демонстрационный пример