Home

ONEDECK (Микросервисный фронтенд)

Фреймворк ONEDECK позволяет легко писать микросервисы во фронтенде.
В рамках данного фреймворка микросервис для фронтенда называется модуль.

ONEDECK позволяет легко делить ваше приложени на модули и собирать приложение как конструктор из набора модулей. В дальнейшем модули легко переиспользовать.

Пример приложения Git
Пример собранного приложения EXAMPLE
Документация ESDoc

Модуль (микросервис, фрагмент)

Модуль - независимая, логически законченная единица приложения.

  • Может быть написан c помощью любого фреймворка (Vue, React, Angular, Webix ... );

  • Можно легко собрать отдельно от всего приложения;

  • Динамически импортируется;

Виды модулей

Все модули, за исключением Root модуля наследуются от базового класса onedeck.Module. Root модуль наследуется от базового класса onedeck.RootModule.

  • Root Module Главный модуль приложения. Инициализируется один раз при старте приложения и инициализирует все модули приложения. Все модули общаются между собой через Root модуль с помощью событий. Реализует паттерн Медиатор.
  • Page Module Модуль страницы. Инициализируется при переходе на страницу модуля. Каждый модуль страницы соответствует конкретному url адресу. Может встраиваться в модуль макета (Layout module) и содержать в себе встраиваемые модули (Embed Module)
  • Global Module Глобальный модуль (глобальные модульные окна, нотификации). Инициализируется один раз при старте приложения. Может вызываться из любой точки приложения. Может содержать в себе Embed модули.
  • Layout Module Модуль макета (header, footer). Инициализируется при переходе на страницу, если еще не был проинициализирован на предыдущей странице. Представляет собой контейнер для встраивания Page модуля и для встраивания Embed модулей страницы.
  • Embed Module Встраиваемый модуль. Инициализируется при переходе на страницу модуля Page либо при инициализации Global модуля. Представляет собой логически законченную часть, которая может быть встроена в различные модули приложения.

Пример внутреннего устройства

|--project_name_dir
    |--src
        |--modules
            |--Root
                |--root.js
                ...
            |--Module1
                |--module.js
                ...
            |--Module2
                |--module.js
                ...
            ...

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

Конфигурация приложения

Alias в webpack.config

Для того, чтобы пути в модуле были относительно модуля, следует прописать директиву alias в webpack.config

resolve: {
   alias: {
     ModuleName: path.resolve(__dirname, src/modules/ModuleName),
     ...
   }
 },

Можно автоматизировать данную процедуру (alias в webpack.config):

const { readdirSync } = require('fs')

const modules = {}
try {
    readdirSync(path.resolve(__dirname, "src/modules/")).forEach(m => {
        modules[m] = path.resolve(__dirname, src/modules/${m})
        console.info(\x1b[37m Module: \x1b[33m ${m})
    })
} catch (e) {
    console.error('\x1b[31m', e.toString())
    process.exit()
}
module.exports = {
    .....
    resolve: {
        alias: modules
    }
  },

Конфиг файл

import Root from 'Root/module';

export default {
  // роутинг с помощю history Api или hash
  historyApi: false,
  // корневой путь для приложения ('/example/path/')
  rootPath: '/',
  // класс Root модуля
  rootModule: Root,
  // название модуля главной страницы
  mainModule: 'main',
  // названия модуля страницы 404
  module404: 'notfound',
  // функция для динамического импорта модуля
  // module - название модуля и название директории модуля
  import: async (module) => await import(./modules/${module}/module),
  modules: {
    auth: {
      module: 'ExampleAuth',
    },
    main: {
      layout: 'ExampleLayoutWebix',
      module: 'ExampleWebix',
      embed: {
        example: {
          module: 'ExampleEmbed',
        },
      },
    },
    notfound: {
      layout: 'ExampleLayoutWebix',
      module: 'ExampleError404',
    },
    globalwnd: {
      global: true,
      module: 'ExampleGlobalWnd',
      embed: {
        example: {
          module: 'ExampleEmbedGlobal',
        },
      },
    },
    globalnotification: {
      global: true,
      module: 'ExampleNotification',
    },
  },
};

Конфигурация может содержать любые поля, в зависимости от необходимости.

Обязательные поля конфигурации приложения
  • historyApi: Bool - вид роутинга в приложении
  • rootPath: String - начальный роут. Если наше приложение стартует от пути http://localhost:3000/example/path/, в rootPath необходимо указать /example/path/;
  • rootModule: Class - класс Root модуля
  • mainModule: String - название модуля главной страницы
  • module404: String - название модуля страницы 404
  • import: Function - асинхронная функция, которая динамически импортирует все модули
  • modules: Object - объект, который содержит настройки всех модулей
Конфигурация модуля
main: {
  layout: 'ExampleLayoutWebix',
  module: 'ExampleWebix',
  embed: {
    example: {
      module: 'ExampleEmbed',
    },
  },
},

Конфигурация модуля может содержать любые поля, в зависимости от необходимости.

  • ключ объекта конфигурации main- название модуля. Данное название соответсвует url модуля http://localhost:3000/main/
  • module: String - Обязательное поле. Содержит название модуля. Модуль должен находиться в директории с соответствующим названием.
  • layout: String - Необязательное поле. Содержит название модуля Layout. Модуль должен находиться в директории с соответствующим названием.
  • embed: Object - Необязательное поле. Содержит Embed модули. Модуль должен находиться в директории с соответствующим названием, которое указано в поле module .
Мы можем для различных условий использовать разные модули:
main: {
  layout: window.innerWidth < 1000 ? 'Layout1' : 'Layout2' ,
  module: window.innerWidth < 1000 ? 'Module1' : 'Module2' ,
  embed: {
    example: {
      module: window.innerWidth < 1000 ? 'Embed1' : 'Embed2' ,
    },
  },
},

Старт приложения (index.js):

import Config from './conf';
new Config.rootModule(Config);

При старте приложнеия нам нужно инициализировать ROOT модуль, он в свою очередь проинициализирует нужные нам модули.

Пример Root модуля

import Onedeck from 'onedeck';
import ExampleNotification from 'ExampleNotification/module';
import ExampleGlobalWnd from 'ExampleGlobalWnd/module';

export default class Root extends Onedeck.RootModule {
  init (path) {
    console.log('init', this.constructor.name, path);
    this.eventHandler();
  }

  eventHandler () {
    this.$$on('examplEvent', (exampleData) => {
      this.exampleAction(exampleData);
    });

    this.$$on('showGlobalWnd', () => {
      const wnd = new ExampleGlobalWnd();
      wnd.show();
    });

    this.$$on('notify', (text) => {
      const notifyObj = new ExampleNotification();
      notifyObj.notify(text);
    });
  }

  dispatcher (path, state) {
    console.log('dispatcher', this.constructor.name, path, state);
  }

  mounted (module, layout) {
    console.log('mounted', this.constructor.name, module, layout);
  }
}

Root module - является ядром всего приложения, наследуется от Onedeck.RootModule

Модуль описывает следующие методы

  • init (path) - инициализация Root модуля. При инициализации нужно вызвать метод this.eventHandler();
  • eventHandler () - в этом методе мы описываем все события уровня приложения (глобальные)
    this.$$on('examplEvent', (exampleData) => {
      this.exampleAction(exampleData);
    });

после этого объявления (this.$$on) в каждом модуле можно вызвать это событие module.$$gemit('examplEvent', data)

  • dispatcher (path, state) - метод вызывается при переходе на новый url адрес.
   module.$$rout({
       path: '/module_name/item/1',
       state: {
           id: 1,
           name: "Example"
       },
   });

каждый модуль имеет метод $$rout. После перехода на новый роут в каждом модуле вызывается метод dispatcher Метод dispatcher принимает path - массив ['module_name', 'item', '1'] и state - {id: 1, name: "Example"}

  • mounted (module, layout) - метод вызывается после инициализации всех модулей. mounted принимает объекты module - текущий Page модуль и layout - текущий Layout модуль

Пример Модуля

import Onedeck from 'onedeck';
import App from 'ExampleModule/App.vue';
import Vue from 'vue';

/**
 * Class ExampleModule
 * module use Vue
 */
export default class ExampleModule extends Onedeck.Module {
  init (path, state) {
    console.log('init', this.constructor.name, path, state);

    this.VueApp = new Vue(App);
    this.eventHandler();
  }

  eventHandler () {
    this.$$on('onAuth', () => this.$$rout({
      path: '/main/',
      state: null,
    }));
  }

  dispatcher (path, state) {
    console.log('dispatcher', this.constructor.name, path, state);
  }

  mounted (module, layout) {
    console.log('mounted', this.constructor.name, module, layout);
  }

  destroy () {
    this.$$offAll()
    this.VueApp.$destroy();
    document.getElementById('ROOT').innerHTML = '';
  }
}

Module - наследуется от Onedeck.RootModule

Хуки жизненного цикла

init (path, state) - инициализация модуля. В этом методе необходимо вызвать eventHandler метод для обработки событий

@param {Array} path - массив с элементами url адреса. ['module_name', 'item', '1'] @param {Object} state - данные переданные с url.

init срабатывает в следующем порядке:
  • Для Root и Global модулей срабатывает 1 раз при инициализации приложения.
  • Для Embed модуля который встраивается в Global - 1 раз при инициализации.
  • Для Layout модуля - каждый раз при смене Layout.
  • Для Page модуля и для Embed модуля - каждый раз при смене Page модуля.
dispatcher(path, state) - диспетчер, в этом методе необходимо описать дейсвия при смене url.

@param {Array} path - массив с элементами url адреса. ['module_name', 'item', '1'] @param {Object} state - данные переданные с url.

dispatcher вызывается в следующем порядке:
  1. Вызывается dispatcher Root моудля.
  2. Вызывается dispatcher Globla моудля.
  3. Вызываются dispatcher Embed моудлей котрые встраиваются в Globla модуль.
  4. Вызывается dispatcher Layout моудля.
  5. Вызывается dispatcher Page моудля.
  6. Вызываются dispatcher Embed моудлей котрые встраиваются в Page модуль.
mounted(currentModule, currentLayout) - вызывается после того как все модули смонтированы в DOM дерево

@param {Object} currentModule - текущий Page модуль. @param {Object} currentLayout - текущий Layout модуль.

mounted вызывается в следующем порядке:
  1. Вызывается mounted Root моудля.
  2. Вызывается mounted Globla моудля.
  3. Вызываются mounted Embed моудлей котрые встраиваются в Globla модуль.
  4. Вызывается mounted Layout моудля.
  5. Вызывается mounted Page моудля.
  6. Вызываются mounted Embed моудлей котрые встраиваются в Page модуль.
destroy () - деструктор модуля. В деструкторе необходимо уничножить объект модуля, почистить DOM дерево, отписаться от событий модуля.
destroy срабатывает в следующем порядке:
  • Для Root, Global и Embed (который встраивается в Global) модулей не вызывается, данные модули активны на всем протяжении работы приложения.
  • Для Layout модуля - каждый раз при смене Layout.
  • Для Page модуля и для Embed модуля - каждый раз при смене Page модуля.

События

ONEDECK предоставляет два типа событий: события уровня модуля $$emit и события уровня приложения $$gemit

Cобытия уровня модуля $$emit

Каждый модуль имеет в себе реализацию паттерна observer Модуль может создовать события c помощью методов $$on или $$onOnce Желательно события создавать в методе eventHandler, но возможны и другие варианты. Пример создания события:

  eventHandler () {
    // Можно так
    this.$$on('onExample1', (data) => {
        console.log(data)
    });
    // Но лучше так
    this.$$on('onExample2', this.onExample2Listener);
  }

После того как мы создали событие мы можем его вызвать в любом месте модуля. (прмиер Vue.js):

<script>
import store from 'Example/store';
import { mapState, mapMutations } from 'vuex';
import Module from 'Example/module';

export default {
  el: '#Embed',
  name: 'EmbedApp',
  store,
  computed: {
    ...mapState({
      data: (state) => state.data,
    }),
  },
  methods: {
    ...mapMutations(['setData']),
    notify() {
      // Так как каждый модуль реализует Singleton мы получим текущий экземпляр данного модуля.
      const module = new Module()
      module.$$emit('onExample1', this.data)
    }
  },
};
</script>
import Module from 'Example/module';
 // Так как каждый модуль реализует Singleton мы получим текущий экземпляр данного модуля.
const module = new Module()
module.$$emit('onExample1', this.data)

Так же мы можем отписаться от события:

  1. this.$$off('onExample2', this.onExample2Listener)
  2. this.$$offAll()

События уровня приложения $$gemit

В приложении есть глобальные события. Они необходимы для общения между модулями. Мудуль ROOT реализует паттерн Mediator. Все глобальные события создает модуль ROOT. События создются точно так-же как и в обчном модуле, помощью методов $$on или $$onOnce.

import ExampleNotification from 'ExampleNotification/module';
import ExampleGlobalWnd from 'ExampleGlobalWnd/module';
import axios from 'axios';

export default class Root extends Onedeck.RootModule {
  init (initObj) {
    ...
    this.eventHandler();
  }

  eventHandler () {
    this.$$on('onExampleEvent', (exampleData) => {
      this.exampleAction(exampleData);
    });

    this.$$on('onShowGlobalWnd', () => {
      const wnd = new ExampleGlobalWnd();
      wnd.show();
    });

    this.$$on('onNotify', (text) => {
      const notifyObj = new ExampleNotification();
      notifyObj.notify(text);
    });

    axios.interceptors.response.use(undefined, (error) => {
        const notifyObj = new ExampleNotification();
        notifyObj.ajaxError(text);
        return Promise.reject(error);
    });
  }
}

Теперь каждый модуль может вызвать необходимое нам событие с помощью метода $$gemit.

import Module from 'Example/module';
// Так как каждый модуль реализует Singleton мы получим текущий экземпляр данного модуля.
const module = new Module()
module.$$gemit('onNotify', this.data)

Роутинг

ONEDECK позволяет использовать 2 вида роутинга HISTORY API и хэш роутинг (с помощью #) Чтобы переключиться между этими двумя режимами необходимо в конфиге задать параметр historyApi. true - роутинг с помщью HISTORY API false - хэш роутинг (с помощью #)

Метод $$rout

Каждый модуль имеет метод $$rout.

import Module from 'Example/module';
const module = new Module()
module.$$rout({
 path: '/module_name/item/1',
 state: {id: 1, name: 'example'},
})

state: {id: 1, name: 'example'} - данные которые мы передаем по этому url адресу, (см. раздел Хуки жизненного цикла, метод init и метод dispatcher) path: '/module_name/item/1' - перевый элемент url адреса module_name должен совпадать с ключем конфига моудля (см. раздел Конфиг файл)

module_name: {
  layout: 'ExampleLayout',
  module: 'ExampleModule',
},