У мультимодульній архітектурі кожен модуль може відповідати за окрему функцію, набір функцій чи компонент. Відповідно, кожна модульна команда має власний набір обов’язків, структуру та планування. Завдяки цьому розробникам у великих проєктах простіше підтримувати та оновлювати кодову базу, а продукти можна легко та швидко масштабувати.
Досвідом створення мультимодульної архітектури у великому проєкті ділиться Олександр Рябцев, Senior Mobile Engineer у Wix. Під час DOU Mobile Meetup, що відбувся у вересні, він розповів, чому вирішили будувати таку систему, як організували роботу розробників та побудували процеси.
Архітектура мультимодульного застосунку
Мультимодульну архітектуру обрали, аби реалізувати Wix One App Mobile. Це інтерпретація всього продукту Wix в мобільному застосунку. Відповідно, створити сайт, наповнити його контентом, побачити аналітику можна просто в смартфоні. Розробка подібного застосунку вимагає ретельного підходу до архітектури. Ось декілька причин, чому ми зупинилися саме на мультимодульній.
Окремі модулі — окремі команди. Кожна команда самостійно планує робочий графік, ухвалює рішення щодо моделі роботи, обирає графік релізів тощо.
Незалежні й швидкі релізи. Команди працюють автономно, не можуть блокувати процеси одна одній, і, відповідно, швидше релізять продукт або фічу.
Легке масштабування. Мультимодульна архітектура дає змогу декільком командам працювати над одним додатком одночасно. Масштаб в 4-5 команд можна виростити до 40-50 команд. Підхід дає змогу перетворити продукт на мега-апку на кшталт Telegram чи Facebook, де основні фічі проєктують «внутрішні» розробники, а «зовнішні» мають змогу робити кастомні.
Архітектуру спроєктували на базі фреймворку React Native. Найголовніше в ній — це декілька леєрів. Нижній, Native SDK — це, по суті, мобільний пристрій на iOS або Android. Верхній — це бізнес-логіка, яка написана на JavaScript + React. Обидва леєри об’єднує React Native — «обгортка» з нативними view та бібліотеками.
Подібних «обгорток» може бути багато. Наприклад, робота з камерою, запис відео чи фото, робота з Key Storage чи нотифікаціями. Ще одна перевага React Native — JS-бібліотеки, які дають змогу використати усе різноманіття вебінструментів.
Розглянемо нижче схему — Wix-інтерпретацію React Native. У верхній частині є два рівні: бізнес-логіка, фічі та sharing code. Як бачимо з назви, sharing code є спільним для бізнес-модулів і допомагає створювати фічі в єдиному стилі й з єдиною інфраструктурою.
Над продуктом працюють два типи девелоперів. Одні займаються бізнес-фічами, а другі, infra-девелопери — платформами та інструментами для розробки. Якщо об’єднати все, окрім бізнес-логіки, одержимо єдину платформу, яка називається Engine. За неї й відповідають infra-девелопери.
Загалом, завдання Infra-команди полягає у тому, щоб забезпечити:
незалежну розробку. Розробники одного модуля не мають залежати від розробників іншого. Натомість вони фокусуються на своїх фічах та незалежно обирають стиль програмування, формат тестування тощо.
незалежні релізи. Infra-команда має зробити так, щоб кожна модульна команда комфортно розробила фічу та «донесла» її до релізу.
операційну систему для бізнес-модулей. Тут буде доречно порівняти наш Engine з операційними системами iOS або Android. Це, по суті, версія платформи зі своїм SDK, яким користуються модулі.
Реалізація Business Modules
Кожен з них має такі характеристики:
маленький застосунок всередині One App;
повністю самостійна та незалежна логіка;
тільки JS, жодного нативного коду;
можуть комунікувати між собою;
реалізують інтерфейс Module.
Ось, наприклад, декілька наших модулів. Всього їх понад сотню.
mobile-apps-restaurants
mobile-apps-groups
mobile-apps-stores
mobile-apps-auth
mobile-apps-notifications
Усі модулі поділяються на два типи. Перші спрямовані на конкретний бізнес. Наприклад, модулі для ресторанів передбачають фічі, пов’язані з меню, бронюванням столиків, онлайн-замовленнями тощо. Інша група модулів — авторизація, сповіщення тощо — розроблена загалом для всього застосунку.
Нижче наведено приклад класу, який імплементує інтерфейс Module. Всі методи опціональні, тобто модуль не обов’язково буде їх реалізувати. Наприклад, в цьому класі модуль може описати, які методи або компоненти він надає для використання іншим модулям або платформі.
export default class TableReservationsOwner implements Module {
components() {
return [{
id: ScreenId.ReservationDetailsScreen,
name: 'Reservation Details',
description: 'Screen to fill contact details',
}];
}
methods(): MethodDefinition[] {
return [];
}
consumedServices(): ConsumedServicesDefinition {
return {
ownerShortcuts: () => ([
{
id: ShortcutId.ReservationsShortcut,
label: 'Table Reservations',
iconAssetPath: 'icons.general.reservations',
screenId: ScreenId.MainReservationsAdapterScreen,
},
]),
};
}
deepLinks(): DeepLinkDefinition[] {
return [];
}
data(): DataDefinition<ReservationDataModel> {
return {};
}
}
А ось приклад нашого package.json, а також dependencies, які використовує один модуль. Є два типи — dependencies та devDependencies. Перші — це бібліотеки, необхідні для цього модулю. У нашому прикладі це Redux та Formik. Під devDependencies маються на увазі dependencies на сусідні модулі, до яких дотичний один конкретний. Йдеться про authorization-модулі, notifications, engine, а також UI-бібліотеки.
{
"name": "mobile-apps-table-reservations",
"dependencies": {
"formik": "^2.0.0",
"@reduxjs/toolkit": "^1.0.0",
...
},
"devDependencies": {
"mobile-apps-stores": "^1.0.0",
"mobile-apps-owners-platform": "^2.0.0",
"mobile-apps-auth": "^1.0.0",
"mobile-apps-notifications": "^1.0.0",
"mobile-apps-ui-lib": "^7.0.0",
"mobile-apps-engine": "^30.0.0"
...
},
}
Як модулі працюють з Engine
Окремі бізнес-вертикалі, наприклад, Wix Groups, Wix Stores, Wix Blogs, можуть комунікувати між собою, але не напряму, а через Engine, який захищає їх від різноманітних помилок.
Якщо модулі незалежні, їх можна «замокати» та ефективно протестувати свій модуль. Також є змога дуже швидко збілдити локально свою вертикаль, не підвантажуючи інші 100+ модулей.
Подібна схема дозволяє застосувати lazy loading і, відповідно, не вантажити весь бандл одночасно, а почати з найнеобхіднішого. Тоді Engine підвантажує додатковий JS-бандл в оперативну пам'ять уже після того, як юзер авторизувався та обирає, на який екран перейти далі. Це суттєво пришвидшує застосунок. Без застосування lazy loading завантаження всього JS-бандлу на старті займатиме багато часу, адже React Native доволі важкий.
Також варто звернути увагу на бінарники. Їх модуль може отримати разом з Engine. Згадаємо про аналогію з операційною системою: Engine має свою версію. Коли вона створюється, бінарники компілюються й складаються окремо. Це гарантує, що кожний модуль при розробці буде використовувати одну й ту саму версію Engine, що й інші модулі, що суттєво покращує стабільність застосунку.
Одночасно з завантаженням Engine «під капотом» скачуються й бінарники. Далі розробник запускає бінарник у симуляторі, накатує свій JS-модуль — і може починати роботу. Бінарник створюється і тестується один раз, і це гарантує, що всі модулі використовують один той самий файл. Підхід пришвидшує розробку, покращує безпеку й робить проєкт стабільнішим.
Engine з точки зору файлової системи схожий на звичайний React Native-проєкт.
Найголовніший файл у проєкті — package.json.
{
"name": "wix-one-app-engine",
"main": "./main.js",
"dependencies": {
"react": "18.2.0",
"react-native": "0.71.12",
"typescript": "4.9.5",
"react-native-navigation": "7.37.1",
"react-native-webview": "11.26.1",
"@react-native-community/netinfo": "^7.1.12",
"lodash": "^4.17.21",
"react-native-mmkv": "2.7.0",
"remx": "^3.0.611"
},
"androidDependencies": {
"androidSdkVersion": 33,
"androidMinSdkVersion": 24,
"flipper": "0.191.0"
}
}
Як бачимо, Engine містить усі ключові інструменти, що за замовчуванням необхідні для розробки на React Native, такі як React, React Native, TypeScript тощо.
Також він містить бібліотеку навігації, яка необхідна кожному модулю, або WebView або Lodash. Модуль може використовувати ці інструменти або ж свої — архітектура це дозволяє. Модуль додає новий інструмент у свої dependencies — й тоді Engine додасть його до загального бандлу.
Як модулі комунікують між собою
Комунікація відбувається через Engine у три способи.
Component. В класі module.js, який імплементує інтерфейс Module, модуль A описує, що він буде доставляти в engine компонент типу BlogPost. Модуль B, своєю чергою, бере цей компонент з engine через Module Registry і перевикористовує його у своєму коді.
// Expose components module.ts
components() {
return [{
id: 'blog.components.BlogPost',
generator: () => require('./screens/BlogPost').default,
}];
}
// Use components
const BlogPostComponent = engine.moduleRegistry
.component('blog.components.BlogPost');
return (
<BlogPostComponent
componentId={componentId}
postId={postId}
/>
);
Broadcast (event). Тут так само. Модуль A оголошує, що буде робити broadcast в певному каналі, а модуль B підписується на цей broadcast у своєму коді. Коли у модулі А щось трапляється, він надсилає broadcast, який буде опрацьовувати модуль B. Все йде через engine, не напряму.
// Register broadcast from module.ts
registerBroadcasts(register) {
broadcasts.addBroadcast(
'core.newNotification',
register('core.newNotification', 'new notification'),
);
}
// send broadcast event
broadcasts.sendBroadcast({ notification });
// register to event
engine.moduleRegistry.addListener(
'wix.core.newNotification',
({ notification }) => {
// notification received
}),
);
Invoke (method). Модуль A описує в module.js метод, який він робить публічним, і який можна викликати іншим модулям. Далі модуль B може викликати цей метод через engine, передати певні props і одержати результат.
// Expose method from module.ts
methods(): MethodDefinition[] {
return [{
id: 'media.uploadMedia',
generator: () => require('./methods/uploadMedia').default,
}];
}
// invoke method from other module through Engine
const result = await engine.moduleRegistry.invoke('media.uploadMedia', {
uploadId: media.uploadId,
});
// ModuleTyped way
const result = await engine.modules.media.methods.uploadMedia({
uploadId: media.uploadId,
});
Як все збирається у One App
Нагадаю, що це мобільна інтерпретація всього Wix. Відповідно до схеми нижче, One App — це конфіг-файл, який дає інструкції engine, що потрібно взяти певні модулі, версії й скласти усе до купи.
Ось приклад package.json файлу для OneApp, де є, наприклад, platformModule. Так, у нас платформа — це також модуль, але дещо особливий, бо його найперше бачить користувач. Саме звідти юзер обирає, яким модулем користуватися далі.
{
"dependencies": {
"mobile-apps-engine": "30.0.0",
"mobile-apps-owners-platform": "1.0.0",
"mobile-apps-blog": "1.0.0",
"mobile-apps-groups": "2.0.0",
},
"oneAppEngine": {
"ownerApp": {
"platformModule": "mobile-apps-owners-platform",
"modules": [
"mobile-apps-owners-platform",
"mobile-apps-blog",
"mobile-apps-groups",
"mobile-apps-engine"
],
"tabs": [
"dashboard",
"inbox",
"manage"
],
"quickActions": [
Як результати одержали
Найперше — це можливість працювати декільком командам паралельно і незалежно. Крім того, мультимодульна архітектура дає можливість створювати окремі платформи для різних завдань. У нашому випадку маємо окремі апки для власників сайтів, для агенцій, для юзерів власників сайтів на Wix тощо.
Ще один суттєвий результат використання подібної архітектури — можливість, за аналогією зі створенням вебсайту, створювати власні застосунки, або Branded App. Такий забрендований застосунок, що складається з модулів, які обрав власник сайту, можна завантажити в App Store або Google Play Market.
Comments