arui-scripts 15.8.1 → 15.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/modules.md CHANGED
@@ -14,306 +14,248 @@
14
14
  то модули приложений - это его реализация в рамках arui-scripts, с дополнительным уровнем абстракции, который, в том числе,
15
15
  позволяет использовать модули без самого module-federation.
16
16
 
17
- ## Общие принципы работы модулей
18
- С точки зрения кода модуль представляет собой простой js-объект, который может быть _каким то образом_ подключен в другое приложение.
17
+ # Как использовать
19
18
 
20
- `arui-scripts` предоставляет решение для сборки таких модулей, а также отдельную библиотеку для упрощения их подключения в другие приложения.
19
+ Предположим у вас есть два приложения, `foo-app` и `bar-app`. Вы хотите предоставлять модуль из `foo-app` и использовать
20
+ его в `bar-app`.
21
21
 
22
- ## Режимы подключения модулей
22
+ ## 1. Добавляем конфигурацию в arui-scripts.config.ts (в foo-app)
23
23
 
24
- В `arui-scripts` есть два способа сборки модулей:
25
- - `default` - Это стандартные модули, которые подключаются с помощью [webpack module federation](https://webpack.js.org/concepts/module-federation/).
26
- - `compat` - Это модули, которые подключаются просто добавлением нужных скриптов на страницу.
27
-
28
- Основная проблема, которую решает ModuleFederation - это возможность не загружать на хост-приложение код библиотек уже подключенных в него.
29
- Например, хост-приложение уже использует `react`, модуль так же написан на `react`. ModuleFederation дает нам легко "переиспользовать"
30
- уже загруженный в браузер код `react` в модуле, не загружая его еще раз.
31
-
32
- ### Сравнение
33
-
34
- `default` модули:
35
- - **+++** Простой способ для переиспользования библиотек между модулем и приложением-хостом.
36
- - **+++** Возможность использовать разные версии общих библиотек в разных модулях/хостах (речь про те библиотеки, которые будут шарится).
37
- - **---** Нет встроенной изоляции стилей. Стили модуля будут применены к хост-приложению.
38
- - **---** Нет возможности использовать модуль в приложении, которое не использует webpack.
39
-
40
- Проблема изоляции стилей может быть решена с помощью [shadow dom](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM),
41
- или с помощью css modules. Но это накладывает некоторые ограничения либо на поддерживаемые браузеры (shadow dom), либо на
42
- существующую кодовую базу (css modules должны использоваться везде, если у вас будет две версии arui-feather на странице - будет не очень приятно).
43
-
44
- `compat` модули:
45
- - **+++** Встроенная изоляция стилей. Стили модуля не будут применены к хост-приложению, если только вы не захотите этого.
46
- - **---** Нет возможности использовать разные версии общих библиотек в разных модулях/хостах, если вы хотите их шарить.
47
-
48
- *Как понять какой режим использовать?*
49
- В целом, если ваше приложение и модули используют только css-modules, то можно использовать `default` режим. Конфликты в стилях
50
- вам в таком случае не грозят. Если же вы используете обычный css, или ваши библиотеки используют обычный css, то лучше
51
- использовать `compat` режим.
52
-
53
- ## Возможность управления модулями с сервера
54
- Сами модули используются только на клиентской части приложения. Но, в некоторых случаях, может быть полезно иметь возможность
55
- управлять тем, какой модуль должен быть подключен на странице с сервера, или же иметь модуль, который будет при загрузке
56
- иметь доступ к данным, доступным только на сервере (аналогично тому, как мы передаем серверный стейт в приложения при SSR).
57
-
58
- Поэтому `arui-scripts` предоставляет возможность создать специальный эндпоинт на вашем сервере, из которого вы сможете управлять
59
- состоянием модуля.
24
+ ```ts
25
+ // ./arui-scripts.config.ts
26
+ import type { PackageSettings } from 'arui-scripts';
60
27
 
61
- Модули с такой возможностью мы называем _модулями с серверным состоянием_ (_server state_).
28
+ const aruiScriptsConfig: PackageSettings = {
29
+ modules: {
30
+ // если хост приложение шарит эти библиотеки, и они совпадают по версии
31
+ shared: {
32
+ 'react': '^17.0.0', // так же поддерживаются более сложные версии например { requiredVersion: '^17.0.0', singleton: true }
33
+ 'react-dom': '^17.0.0',
34
+ },
35
+ exposes: {
36
+ 'SomeModule': './src/modules/some-module/index',
37
+ 'AnotherModule': './src/modules/another-module/index',
38
+ }
39
+ }
40
+ }
62
41
 
63
- ## Особые типы модулей
64
- Несмотря на то, что сами по себе модули представляют собой простой js-объект, мы определяем один особый тип модулей - _монтируемые модули_.
42
+ export default aruiScriptsConfig;
43
+ ```
65
44
 
66
- ### Монтируемые модули
67
- Монтируемые модули - это модули, основное предназначение которых - отрендерить какой-то компонент внутри хост-приложения.
68
- Монтируемые модули могут быть как клиентскими, так и серверными.
45
+ Эта конфигурация объявляет два модуля, `SomeModule` и `AnotherModule`. Эти модули смогут получать react и react-dom из
46
+ подключившего их приложения, если оно содержит конфигурацию для `modules.shared`, и версии библиотек подходят по semver.
69
47
 
70
- Такие модули должны экспортировать две функции:
48
+ ## 2. Создаем входную точку модуля (в foo-app)
71
49
 
72
50
  ```tsx
73
- import { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
51
+ // src/modules/some-module/index
52
+ import type { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
74
53
  export const mount: ModuleMountFunction = (targetNode, runParams, serverState) => {
75
- // здесь происходит монтирование модуля в хост-приложение
76
- // targetNode - это DOM-нода, в которую нужно отрендерить модуль
77
- // runParams - это параметры, которые были переданы при запуске модуля
78
- // serverState - это состояние, которое было передано с сервера
54
+ console.log( // Вы можете передать эти переменные в ваши компоненты, подробнее ниже
55
+ runParams, // undefined
56
+ serverState // { baseUrl: "https://example.com/foo-app", hostAppId: "bar-app" } // Эти данные определяются тем, что было передано в загрузчик в приложении-хосте
57
+ );
79
58
 
80
- // Скорее всего это будет что-то вроде:
81
- ReactDOM.render(<App preparedState={serverState} runParams={runParams} />, targetNode);
59
+ ReactDOM.render(
60
+ <div>Hello from module!</div>,
61
+ targetNode,
62
+ );
82
63
  }
83
64
 
84
65
  export const unmount: ModuleUnmountFunction = (targetNode) => {
85
- // здесь происходит демонтирование модуля из хост-приложения
86
- // Скорее всего это будет что-то вроде:
87
66
  ReactDOM.unmountComponentAtNode(targetNode);
88
67
  }
89
68
  ```
90
69
 
91
- ### Модули-фабрики
92
- Модули-фабрики - это модули, которые поставляют фабрики, которые в свою очередь вызываются в рантайме со стейтом (клиентским или серверным, в зависимости от типа поставляемого модуля).
70
+ Пример для React@18
93
71
 
94
- Такие модули должны экспортировать фабрику:
72
+ ```tsx
73
+ import type { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
74
+ import ReactDOM from 'react-dom';
95
75
 
96
- Для mf(default) модулей:
76
+ let root: ReturnType<typeof ReactDOM.createRoot>
97
77
 
98
- ```tsx
99
- import type { FactoryModule } from '@alfalab/scripts-modules';
78
+ export const mount: ModuleMountFunction = (targetNide, runParams, serverState) => {
79
+ root = ReactDOM.createRoot(targetNode);
100
80
 
101
- const factory: FactoryModule = function (runParams, serverState) {
102
- // serverState - это состояние, которое подготовлено на сервере модуля
103
- // runParams - это параметры, которые были переданы при запуске модуля клиентом
104
- // в фабрике можно на основе стейта вернуть готовый модуль
105
- return {
106
- serverState,
107
- doSomething: () => {
108
- fetch(serverState.baseUrl + '/api/getData')
109
- }
110
- };
81
+ root.render(<App preparedState={serverState} runParams={runParams} />);
111
82
  }
112
83
 
113
- export default factory;
114
- // или export { factory };
84
+ export const unmount: ModuleUnmountFunction = () => {
85
+ root?.unmount();
86
+ }
115
87
  ```
116
88
 
117
- для compat модулей:
118
- ```ts
119
- import type { FactoryModule } from '@alfalab/scripts-modules';
89
+ ## 3. Подключите модуль в другом приложении (в bar-app)
120
90
 
121
- const factory: FactoryModule = function (runParams, serverState) {
122
- // в фабрике можно на основе стейта вернуть готовый модуль
123
- return {
124
- serverState,
125
- doSomething: () => {
126
- fetch(serverState.baseUrl + '/api/getData')
127
- }
128
- };
129
- }
91
+ Если предположить что приложение с модулем уже развернуто по адресу https://examle.com/foo-app
92
+ ```tsx
93
+ import {
94
+ createModuleLoader,
95
+ createModuleFetcher,
96
+ useModuleMounter,
97
+ MountableModule,
98
+ } from '@alfalab/scripts-modules';
99
+
100
+ const loader = createModuleLoader<MountableModule>({
101
+ hostAppId: 'bar-app',
102
+ moduleId: 'test',
103
+ getModuleResources: createModuleFetcher({
104
+ baseUrl: 'https://examle.com/foo-app',
105
+ }),
106
+ });
130
107
 
131
- window.ModuleCompat = factory;
108
+ export const MyAwesomeComponent = () => {
109
+ const { loadingState, targetElementRef } = useModuleMounter({ loader });
110
+
111
+ return (
112
+ <div>
113
+ {loadingState === 'pending' && <div>pending...</div>}
114
+ {loadingState === 'rejected' && <div>Error</div>}
115
+ <div ref={targetElementRef} /> {/* сюда будет монтироваться модуль */}
116
+ </div>
117
+ );
118
+ }
132
119
  ```
133
120
 
134
- ## Как создать модуль
121
+ На этом этапе вы получите подгружающееся в bar-app модуль.
135
122
 
136
- ### Описать модуль в настройках arui-scripts
137
- Для того чтобы ваше приложение начало предоставлять модули, вам необходимо добавить настройки в `arui-scripts.config.ts`:
123
+ ## 4. (Опционально). Определить shared-библиотеки в приложении потребителе (в bar-app)
138
124
 
139
125
  ```ts
140
- import { PackageSettings } from 'arui-scripts';
126
+ // ./arui-scripts.config.ts
127
+ import type { PackageSettings } from 'arui-scripts';
141
128
 
142
129
  const aruiScriptsConfig: PackageSettings = {
143
- compatModules: {
144
- exposes: {
145
- 'ClientModuleCompat': { // имя модуля, будет использоваться приложениями-потребителями
146
- entry: './src/modules/module-compat/index', // точка входа модуля
147
- },
148
- 'ServerStateModuleCompat': {
149
- entry: './src/modules/server-state-module-compat/index',
150
- // этот модуль будет ожидать на странице глобальные переменные react и reactDOM,
151
- // он будет использовать их вместо библиотек из своего node_modules
152
- compatConfig: {
153
- react: 'react',
154
- 'react-dom': 'reactDOM',
155
- }
156
- }
157
- }
158
- },
159
130
  modules: {
160
- // модули тут смогут переиспользовать react и react-dom из хост-приложения,
161
- // если хост приложение шарит эти библиотеки, и они совпадают по версии
162
131
  shared: {
163
- 'react': '^17.0.0', // так же поддерживаются более сложные версии например { requiredVersion: '^17.0.0', singleton: true }
132
+ 'react': '^17.0.0',
164
133
  'react-dom': '^17.0.0',
165
134
  },
166
- exposes: {
167
- 'module': './src/modules/module/index',
168
- 'ServerStateModule': './src/modules/server-state-module/index',
169
- }
170
135
  }
171
136
  }
172
137
 
173
138
  export default aruiScriptsConfig;
174
139
  ```
140
+ Эта конфигурация даст возможность использовать react и react-dom библиотеки из bar-app, не загружая их из foo-app.
175
141
 
176
- Все параметры конфигурации описаны [ниже](#Конфигурация-модулей).
142
+ ## Готово!
177
143
 
178
- ### Создать модуль
179
- Модуль является простым js/ts файлом. Он может использовать любой код вашего проекта, и любые библиотеки из node_modules.
144
+ Эта минимальная конфигурация, которая нужна для работы с модулями. Далее идет информация об advanced настройках работы с модулями.
145
+ Рекомендуется с ней ознакомиться хотя бы верхнеуровнево, для того, чтобы понимать какие возможности есть у модулей.
180
146
 
181
- В зависимости от режима подключения модуля, входная точка будет выглядеть по-разному.
147
+ # Передача параметров в модуль из приложения-потребителя
148
+ Зачастую модули должны получать какую-то информацию из приложения потребителя при своей инициализации.
182
149
 
183
- #### Default модуль
184
- Входная точка модуля должна экспортировать все поля модуля через `export`.
150
+ На стороне модуля эти параметры приходят в параметр `runParams` функции mount. Предположим ваш модуль должен получать из
151
+ приложения-потребителя тему (`theme`) и ширину (`width`). Тогда входная точка будет выглядеть примерно так:
185
152
 
186
- ```ts
187
- // src/modules/module/index.ts
153
+ ```tsx
154
+ import type { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
188
155
 
189
- export const doSomething = () => {
190
- console.log('Hello from module!');
156
+ type ModuleRunParams = {
157
+ theme: stirng;
158
+ width: number;
191
159
  };
192
160
 
193
- export const publicConstant = 3.14;
194
- ```
195
-
196
- #### Compat модуль
197
- Входная точка compat модуля должна писать в глобальную переменную `window` объект с ключом `{НазваниеМодуля}`.
198
- Все поля этого объекта по сути и будут являться модулем, ваши потребители смогут использовать их.
199
-
200
- ```ts
201
- // src/modules/module-compat/index.ts
202
-
203
- window.ModuleCompat = {
204
- doSomething: () => {
205
- console.log('Hello from compat module!');
206
- },
207
- publicConstant: 3.14,
208
- // ...
209
- };
161
+ export const mount: ModuleMountFunction<ModuleRunParams> = (targetNode, runParams) => {
162
+ ReactDOM.render(
163
+ <MyAwesomeComponent theme={runParams.theme} width={runParams.width} />,
164
+ targetNode,
165
+ );
166
+ }
167
+ // далее unmount как и раньше
210
168
  ```
211
169
 
212
- <details>
213
- <summary>Писать в window? Вы что, с дуба рухнулись?</summary>
214
- Да, конечно, это может создать определенные проблемы (конфликты имен модулей, определенные ограничения на используемые названия),
215
- но по сути это единственный способ передать код модуля в хост-приложение.
216
-
217
- Webpack module federation делает абсолютно то же самое, просто прячет работу с глобальными переменными за собой.
218
- </details>
219
-
220
- #### Создание модулей предопределенного типа
221
-
222
- **Монтируемый модуль, default**
170
+ На стороне потребителя:
223
171
 
224
172
  ```tsx
225
- // src/modules/module/index.ts
173
+ import { createModuleLoader, MountableModule, useModuleMounter } from '@alfalab/scripts-modules';
226
174
 
227
- import React from 'react';
228
- import ReactDOM from 'react-dom';
229
- import type { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
230
- import { Module } from './Module';
175
+ // У нас возможности передать типы из приложения-модуля в приложение-хост
176
+ // При желании вы можете вынести эти типы в общую библиотеку и использовать оттуда
177
+ type ModuleRunParams = {
178
+ theme: string;
179
+ width: number;
180
+ };
231
181
 
232
- export const mount: ModuleMountFunction<any, any> = (targetNode, runParams, serverState) => {
233
- console.log('Module: mount', { runParams, serverState });
234
- if (!targetNode) {
235
- throw new Error(`Target node is not defined for module`);
236
- }
182
+ type ModuleType = MountableModule<ModuleRunParams>;
237
183
 
238
- ReactDOM.render(<Module />, targetNode);
239
- };
240
- export const unmount: ModuleUnmountFunction = (targetNode) => {
241
- console.log('Module: unmount');
242
- if (!targetNode) {
243
- return;
244
- }
184
+ const loader = createModuleLoader<ModuleType>(/*...*/);
185
+ export const MyAwesomeComponent = () => {
186
+ const { loadingState, targetElementRef } = useModuleMounter({
187
+ loader,
188
+ runParams: { theme: 'blue', width: 300 },
189
+ });
245
190
 
246
- ReactDOM.unmountComponentAtNode(targetNode);
247
- };
191
+ return (
192
+ <div>
193
+ { loadingState === 'pending' && <div>pending...</div> }
194
+ { loadingState === 'rejected' && <div>Error</div> }
195
+ <div ref={ targetElementRef }/>
196
+ </div>
197
+ );
198
+ }
248
199
  ```
249
200
 
250
- **Монтируемый модуль, compat**
201
+ :warning: **Внимание!** Модуль не будет обновляться при изменении runParams. Это осознанное решение, вы не должны относится к
202
+ параметрам тут с той же легкостью, что и к prop-ам react-компонентов. Очень легко провести параллели между эти двумя
203
+ концепциями, но использование runParams специально сделано менее удобным - чем меньше вы их используете, тем реже вы будете
204
+ их менять, и тем меньше вероятность привнести обратно несовместимые изменения. Старайтесь передавать в runParams только
205
+ примитивы, не пытаться передавать там объекты сущностей (например профиль пользователя).
206
+ В целом - минимизируйте их использование.
207
+ Если вам важно чтобы модуль перемонтировался каждый раз при изменении runParams - вы можете сделать это самостоятельно, например добавив
208
+ key в ваш компонент-обертку вокруг модуля.
251
209
 
252
- ```tsx
253
- // src/modules/module-compat/index.ts
254
- import React from 'react';
255
- import ReactDOM from 'react-dom';
256
- import type { ModuleMountFunction, ModuleUnmountFunction, WindowWithMountableModule } from '@alfalab/scripts-modules';
257
- import { ModuleCompat } from './ModuleCompat';
258
-
259
- const mount: ModuleMountFunction<any, any> = (targetNode, runParams, serverState) => {
260
- console.log('ModuleCompat: mount', { runParams, serverState });
261
- ReactDOM.render(<ModuleCompat />, targetNode);
262
- };
263
- const unmount: ModuleUnmountFunction = (targetNode) => {
264
- console.log('ModuleCompat: unmount');
210
+ # Возможность управления модулями с сервера
211
+ Сами модули используются только на клиентской части приложения. Но, в некоторых случаях, может быть полезно иметь возможность
212
+ управлять тем, какой модуль должен быть подключен на странице с сервера, или же иметь модуль, который будет при загрузке
213
+ иметь доступ к данным, доступным только на сервере (аналогично тому, как мы передаем серверный стейт в приложения при SSR).
265
214
 
266
- ReactDOM.unmountComponentAtNode(targetNode);
267
- };
215
+ По умолчанию, даже в клиентские модули в mount-функцию будет передан параметр `serverState`, содержащий:
216
+ - `baseUrl` - тот адрес, который использовался при загрузке модуля с помощью `createModuleFetcher`. Вы можете использовать
217
+ этот адрес например для определения того, где искать API вашего модуля
218
+ - `hostAppId` - идентификатор приложения, которое загружает модуль. Может быть полезен если вы хотите менять поведение
219
+ модуля в зависимости от того, кто его потребляет.
268
220
 
269
- (window as WindowWithMountableModule).ModuleCompat = {
270
- mount: mountModule,
271
- unmount: unmountModule,
272
- };
273
- ```
221
+ Так же `arui-scripts` предоставляет возможность создать специальный эндпоинт на вашем сервере, из которого вы сможете управлять
222
+ состоянием модуля.
274
223
 
224
+ Модули с такой возможностью мы называем _модулями с серверным состоянием_ (_server state_).
275
225
 
276
- ### (Опционально) Определить серверный эндпоинт для модуля
277
- Если вы хотите, чтобы ваш модуль имел серверную часть, которая сможет подготовить данные для модуля, то вам необходимо
278
- определить серверный эндпоинт для модуля. Для этого вам нужно определить объект, описывающий ваши модули:
226
+ Для удобного создания таких эндпоинтов сделаны хелперы в пакете `@alfalab/scripts-server`. Например, для `hapi@20`:
279
227
 
280
228
  ```ts
281
- import type { ModulesConfig } from '@alfalab/scripts-server';
282
-
283
- const modules: ModulesConfig = {
284
- 'ServerStateModuleCompat': {
285
- mountMode: 'compat',
286
- version: '1.0.0',
287
- getRunParams: async (getResourcesRequest) => ({
288
- // getResouresRequest - это объект, который будет передан из хост-приложения
289
-
290
- // данные, которые вернет эта будут доступны при инициализации модуля
291
- paramFromServer: 'This can be any data from server',
292
- asyncData: 'It can be constructed from async data, so you may perform some service calls here',
293
- contextRoot: 'http://localhost:8081',
294
- }),
295
- },
296
- 'ServerModule': {
297
- mountMode: 'default',
298
- version: '1.0.0',
299
- getRunParams: async () => ({
300
- paramFromServer: 'This can be any data from server',
301
- asyncData: 'It can be constructed from async data, so you may perform some service calls here',
302
- contextRoot: 'http://localhost:8081',
303
- }),
304
- },
305
- };
306
- ```
307
-
308
- Подробнее о `getResourcesRequest` и `getRunParams` рассказано в разделе [Подключение модулей](#Подключение-модулей).
229
+ import { createGetModulesHapi20Plugin } from '@alfalab/scripts-server/build/hapi-20';
230
+
231
+ const plugin = createGetModulesHapi20Plugin(
232
+ {
233
+ 'SomeModule': {
234
+ mountMode: 'default',
235
+ version: '1.0.0',
236
+ getModuleState: async (getResourcesRequest, request) => {
237
+ console.log(getResourcesRequest.moduleId); // "SomeModule"
238
+ console.log(getResourcesRequest.hostAppId); // В зависимости от того, что за приложение запросило модуль
239
+ console.log(getResourcesRequest.params); // параметры запроса за модулем, подробнее ниже
240
+ console.log(request); // Для каждого фреймворка это будет специфичный для него объект запроса
241
+ const answerFromAwesomeService = await getSomethingFromBackend();
242
+ return {
243
+ baseUrl: '/awesome-app',
244
+ answerFromAwesomeService, // эти данные попадут в параметр serverState mount-функции модуля
245
+ };
246
+ },
247
+ }
248
+ }
249
+ );
309
250
 
310
- Далее, в зависимости от того, какой серверный фреймворк вы используете, вам нужно будет подключить ваши модули в
311
- соответствующий хендлер. Например, для express это будет выглядеть так:
251
+ server.register(plugin);
252
+ ```
312
253
 
254
+ Для других фреймворков это будет выглядеть аналогично, для `express`
313
255
  ```ts
314
256
  import { createGetModulesExpress } from '@alfalab/scripts-server/build/express';
315
257
 
316
- const modulesRouter = createGetModulesExpress(modules);
258
+ const modulesRouter = createGetModulesExpress({/*...*/});
317
259
 
318
260
  app.use(modulesRouter);
319
261
  ```
@@ -323,17 +265,7 @@ app.use(modulesRouter);
323
265
  ```ts
324
266
  import { createGetModulesHapi16Plugin } from '@alfalab/scripts-server/build/hapi16';
325
267
 
326
- const modulesPlugin = createGetModulesHapi16Plugin(modules);
327
-
328
- server.register(modulesPlugin);
329
- ```
330
-
331
- Для `hapi@20`:
332
-
333
- ```ts
334
- import { createGetModulesHapi20Plugin } from '@alfalab/scripts-server/build/hapi20';
335
-
336
- const modulesPlugin = createGetModulesHapi20Plugin(modules);
268
+ const modulesPlugin = createGetModulesHapi16Plugin({/*...*/});
337
269
 
338
270
  server.register(modulesPlugin);
339
271
  ```
@@ -343,7 +275,7 @@ server.register(modulesPlugin);
343
275
  ```ts
344
276
  import { createGetModulesMethod } from '@alfalab/scripts-server';
345
277
 
346
- const getModules = createGetModulesMethod(modules);
278
+ const getModules = createGetModulesMethod({/*...*/});
347
279
 
348
280
  // getModules будет иметь следующую сигнатуру:
349
281
  type ModulesMethod = {
@@ -357,191 +289,330 @@ type ModulesMethod = {
357
289
  // вы можете посмотреть примеры реализации тких методов для express, hapi@16 и hapi@20.
358
290
  ```
359
291
 
360
- ### (Опционально) Разобраться с изоляцией стилей
292
+ ## Подключение модулей с серверным состоянием в приложении потребителе
293
+ Приложение-потребитель должно знать, что модуль имеет серверное состояние, поскольку это требует
294
+ небольшого изменения механизма подключения модуля:
361
295
 
362
- #### Compat модули
363
- В случае с compat модулями, стили модуля будут применены только к элементам, которые находятся внутри элемента
364
- с классом `module-{имя модуля}`. Это позволяет изолировать стили модуля от стилей хост-приложения.
296
+ ```tsx
297
+ import {
298
+ createModuleLoader,
299
+ createServerStateModuleFetcher,
300
+ useModuleMounter,
301
+ MountableModule,
302
+ } from '@alfalab/scripts-modules';
303
+
304
+ type RequestParams = { // Опционально, вы можете определить параметры которые попадут на серверную часть модуля, в getResourcesRequest.params
305
+ name: string;
306
+ }
365
307
 
366
- Вашей ответственностью будет добавить к рут-элементу модуля класс .module-nameOfModule. Вы должны сделать это в самом верхнем компоненте/элементе вашего модуля.
308
+ const loader = createModuleLoader<MountableModule, RequestParams>({
309
+ hostAppId: 'bar-app',
310
+ moduleId: 'test',
311
+ getModuleResources: createServerStateModuleFetcher({ // !!! другой метод подключения
312
+ baseUrl: 'http://localhost:8082',
313
+ headers: { 'X-Auth': 'bla-bla' } // опционально вы можете передать дополнительные заголовки для запроса
314
+ }),
315
+ });
367
316
 
368
- Вы можете переопределить префикс для css классов модуля, в `arui-scripts.config.ts`, подробнее в [конфигурации модулей](#Конфигурация-модулей).
317
+ export const MyAwesomeComponent = () => {
318
+ const { loadingState, targetElementRef } = useModuleMounter({
319
+ loader,
320
+ loaderParams: { name: 'Ivan' }, // Если модуль не принимает кастомных параметров на сервере - этого можно не делать!
321
+ });
369
322
 
370
- Если ваше react-приложение использует порталы, вам так же надо не забыть добавить префикс к элементу-порталу.
323
+ return (
324
+ <div>
325
+ {loadingState === 'pending' && <div>pending...</div>}
326
+ {loadingState === 'rejected' && <div>Error</div>}
327
+ <div ref={targetElementRef} /> {/* сюда будет монтироваться модуль */}
328
+ </div>
329
+ );
330
+ }
331
+ ```
371
332
 
372
- **Важно** - изоляция стилей работает только в одном направлении - стили модуля не будут применены к элементам
373
- хост-приложения. Но стили хост-приложения могут быть применены к элементам модуля.
333
+ # Изоляция стилей
334
+ Если ваши приложения активно используют глобальные стили (то есть не с css-modules или css-in-js), вы вполне
335
+ можете столкнуться с проблемой конфликтов стилей между модулями и приложением-потребителем.
374
336
 
375
- #### Стандартные модули
376
- Никакого встроенного механизма изоляции стилей для стандартных модулей нет. Если хост-приложение и модуль используют css-modules,
377
- то конфликтов возникнуть не должно. Если же это не так - вы можете попробовать решить эту проблему используя shadow-dom.
337
+ Для решения конфликтов стилей вы можете попробовать перевести проект на css-modules, но это может быть довольно
338
+ трудоемкой задачей, особенно если у вас уже есть большая кодовая база.
378
339
 
379
- ### Тестирование модулей
380
- Поскольку в общем случае модули представляют собой простой js код - для тестирования вы можете пользоваться любыми привычными вам инструментами.
340
+ Простое решение, которое предлагает arui-scripts - это использование другого типа модулей, основанного не
341
+ на module-federation. Эти модули мы называем _compat_ модулями.
381
342
 
382
- Для тестирования модулей в cypress или playwright вы можете создать отдельный эндпоинт в вашем приложении, который будет
383
- подключать модуль в ваше же приложение.
343
+ Суть метода заключается в том, что ко всем стилям модуля будет добавляться префикс, который позволит изолировать
344
+ стили модуля от стилей приложения-потребителя.
384
345
 
346
+ :warning: **Внимание!** - изоляция стилей работает только в одном направлении - стили модуля не будут применены к элементам
347
+ хост-приложения. Но стили хост-приложения могут быть применены к элементам модуля.
385
348
 
386
- # Подключение модулей
349
+ Для того чтобы использовать этот метод, вам нужно:
387
350
 
388
- ## Создание загрузчика
389
- Базовый способ подключение модулей - это использование `createModuleLoader` из `@alfalab/scripts-modules`. Этот метод
390
- вернет вам функцию, которая позволит подключить модуль в ваше приложение.
351
+ 1. Изменить конфигурацию модуля в `arui-scripts.config.ts`:
391
352
 
392
353
  ```ts
393
- import { createModuleLoader } from '@alfalab/scripts-modules';
394
-
395
- const loader = createModuleLoader({
396
- hostAppId: 'my-app', // id вашего приложения, оно будет передаваться в серверную ручку модуля
397
- moduleId: 'test', // id модуля, который вы хотите подключить
398
- // функция, которая должна вернуть описание модуля.
399
- getModuleResources: async ({ moduleId, hostAppId, params }) => ({
400
- scripts: ['http://localhost:8081/static/js/main.js'], // скрипты модуля
401
- styles: ['http://localhost:8081/static/css/main.css'], // стили модуля
402
- moduleVersion: '1.0.0', // версия модуля
403
- appName: 'moduleSourceAppName', // имя приложения, которое является источником модуля
404
- mountMode: 'compat', // режим монтирования модуля
405
- moduleRunParams: { // параметры, которые будут доступны при инициализации модуля
406
- baseUrl: 'http://localhost:8081',
354
+ // ./arui-scripts.config.ts
355
+ import type { PackageSettings } from 'arui-scripts';
356
+
357
+ const aruiScriptsConfig: PackageSettings = {
358
+ compatModules: {
359
+ // Тут ключ - название библиотеки, значение - имя переменной в window, которая будет использоваться для получения библиотеки
360
+ // Это те библиотеки, которые этот проект будет предоставлять модулям, подключаемым в него
361
+ shared: {
362
+ 'react': 'react',
363
+ 'react-dom': 'reactDOM',
407
364
  },
408
- }),
409
- });
365
+ exposes: {
366
+ 'SomeModule': {
367
+ entry: './src/modules/some-module/index',
368
+ // Это те библиотеки, которые модуль будет пытаться получить из window
369
+ externals: {
370
+ react: 'react',
371
+ 'react-dom': 'reactDOM',
372
+ },
373
+ },
374
+ 'AnotherModule': {
375
+ entry: './src/modules/another-module/index',
376
+ },
377
+ }
378
+ }
379
+ }
380
+
381
+ export default aruiScriptsConfig;
410
382
  ```
411
383
 
412
- Вам вовсе не обязательно руками описывать функцию `getModuleResources`. В зависимости от типа модуля, вы можете
413
- использовать один из готовых хелперов:
384
+ 2. Изменить входную точку модуля:
414
385
 
415
- Для модулей без серверного стейта:
416
- ```ts
417
- import { createModuleLoader, createModuleFetcher } from '@alfalab/scripts-modules';
386
+ ```tsx
387
+ // ./src/modules/some-module/index
388
+ import type {
389
+ ModuleMountFunction,
390
+ ModuleUnmountFunction,
391
+ WindowWithMountableModule,
392
+ } from '@alfalab/scripts-modules';
418
393
 
419
- const loader = createModuleLoader({
420
- hostAppId: 'my-app',
421
- moduleId: 'test',
422
- getModuleResources: createModuleFetcher({
423
- baseUrl: 'http://localhost:8081',
424
- }),
425
- });
426
- ```
394
+ const CSS_PREFIX = 'module-SomeModule'; // префикс, который будет добавлен к классам модуля. По умолчанию - module-<moduleId>
427
395
 
428
- `createModuleFetcher` сам сделает запрос за манифестом приложения, и правильным образом сформирует описание модуля.
396
+ // Если ваше react-приложение использует порталы, вам так же надо не забыть добавить префикс к элементу-порталу.
397
+ export const mount: ModuleMountFunction = (targetNode, runParams, serverState) => {
398
+ ReactDOM.render(
399
+ <div className={ CSS_PREFIX }>Hello from module!</div>,
400
+ targetNode,
401
+ );
402
+ }
429
403
 
430
- Для модулей с серверным стейтом:
431
- ```ts
432
- import { createModuleLoader, createServerStateModuleFetcher } from '@alfalab/scripts-modules';
404
+ export const unmount: ModuleUnmountFunction = (targetNode) => {
405
+ ReactDOM.unmountComponentAtNode(targetNode);
406
+ };
433
407
 
434
- const loader = createModuleLoader({
435
- hostAppId: 'my-app',
436
- moduleId: 'test',
437
- getModuleResources: createServerStateModuleFetcher({
438
- baseUrl: 'http://localhost:8081',
439
- headers: { 'X-Auth': 'bla-bla' } // опционально вы можете передать дополнительные заголовки для запроса
440
- }),
441
- });
408
+ (window as WindowWithMountableModule).SomeModule = { // имя переменной в window должно соответствовать имени модуля в exposes
409
+ mount: mountModule,
410
+ unmount: unmountModule,
411
+ };
442
412
  ```
443
413
 
444
- `createServerStateModuleFetcher` сам сделает запрос к ручке, которая отдает описание модуля.
414
+ Как видите, изменения в сравнении с обычными модулями минимальны. На стороне потребителя при
415
+ этом не меняется ничего - `@alfalab/script-modules` сам поймет как загружать такой модуль и будет подключать его
416
+ нужным способом.
445
417
 
446
- В случае же совсем кастомных требований, вы можете реализовать функцию `getModuleResources` самостоятельно.
418
+ <details>
419
+ <summary>Писать в window? Вы что, с дуба рухнулись?</summary>
420
+ Да, конечно, это может создать определенные проблемы (конфликты имен модулей, определенные ограничения на используемые названия),
421
+ но по сути это единственный способ передать код модуля в хост-приложение.
447
422
 
448
- ## Использование загрузчика
449
- После того как вы создали `loader` - вы легко можете получить доступ к модулю:
423
+ Webpack module federation делает абсолютно то же самое, просто прячет работу с глобальными переменными за собой.
424
+ </details>
450
425
 
451
- ```ts
452
- const { module, unmount, moduleResources } = await loader({
453
- getResourcesParams: { foo: 'bar' }, // параметры, которые будут переданы в getModuleResources
454
- });
426
+ ### Сравнение способов подключения модулей
455
427
 
456
- console.log(module); // модуль, который вы загрузили. Тут будут доступны всё, что было экспортировано из модуля
457
- console.log(moduleResources); // полный ответ от getModuleResources
428
+ `default` модули:
429
+ - **+++** Простой способ для переиспользования библиотек между модулем и приложением-хостом.
430
+ - **+++** Возможность использовать разные версии общих библиотек в разных модулях/хостах (речь про те библиотеки, которые будут шарится).
431
+ - **---** Нет встроенной изоляции стилей. Стили модуля будут применены к хост-приложению.
432
+ - **---** Нет возможности использовать модуль в приложении, которое не использует webpack.
458
433
 
459
- // вызов этой функции отмонтирует модуль из вашего приложения - удалит скрипты и стили модуля, а так же удалит
460
- // все глобальные переменные, которые были определены в модуле.
461
- unmount();
462
- ```
434
+ Проблема изоляции стилей может быть решена с помощью [shadow dom](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM),
435
+ или с помощью css modules. Но это накладывает некоторые ограничения либо на поддерживаемые браузеры (shadow dom), либо на
436
+ существующую кодовую базу (css modules должны использоваться везде, если у вас будет две версии arui-feather на странице - будет не очень приятно).
437
+
438
+ `compat` модули:
439
+ - **+++** Встроенная изоляция стилей. Стили модуля не будут применены к хост-приложению, если только вы не захотите этого.
440
+ - **---** Нет возможности использовать разные версии общих библиотек в разных модулях/хостах, если вы хотите их шарить.
463
441
 
464
- При вызове `loader` вы можете передать параметры, которые попадут в функцию `getModuleResources`. Это может быть полезно,
465
- если вы хотите передать какие-то параметры на сервер модуля.
442
+ *Как понять какой режим использовать?*
443
+ В целом, если ваше приложение и модули используют только css-modules, то можно использовать `default` режим. Конфликты в стилях
444
+ вам в таком случае не грозят. Если же вы используете обычный css, или ваши библиотеки используют обычный css, то лучше
445
+ использовать `compat` режим.
466
446
 
467
- `getModuleResources` будет вызвана со следующими параметрами:
468
- ```ts
469
- const getModuleResourcesParams = {
470
- moduleId: 'test', // id модуля, который вы хотите подключить
471
- hostAppId: 'my-app', // id вашего приложения
472
- params: { foo: 'bar' }, // параметры, которые вы передали в loader как `getResourcesParams`
447
+ # Другие типы модулей
448
+
449
+ Помимо создания монтируемых модулей, есть возможность создавать и другие типы модулей, более подходящие для некоторых вариантов использования.
450
+
451
+
452
+ ## Модули-фабрики
453
+ Модули-фабрики - это модули, которые поставляют фабрики, которые в свою очередь вызываются в рантайме со стейтом (клиентским или серверным, в зависимости от типа поставляемого модуля).
454
+
455
+ Такие модули должны экспортировать фабрику:
456
+
457
+ Для default модулей:
458
+
459
+ ```tsx
460
+ import type { FactoryModule } from '@alfalab/scripts-modules';
461
+
462
+ const factory: FactoryModule = function (runParams, serverState) {
463
+ // serverState - это состояние, которое подготовлено на сервере модуля
464
+ // runParams - это параметры, которые были переданы при запуске модуля клиентом
465
+ // в фабрике можно на основе стейта вернуть готовый модуль
466
+ return {
467
+ serverState,
468
+ doSomething: () => {
469
+ fetch(serverState.baseUrl + '/api/getData')
470
+ }
471
+ };
473
472
  }
473
+
474
+ export default factory;
475
+ // или export { factory };
474
476
  ```
475
477
 
476
- При использовании `createServerStateModuleFetcher` именно эти данные будут отправлены на сервер и будут доступны в функции `getRunParams` модуля.
478
+ для compat модулей:
479
+ ```ts
480
+ import type { FactoryModule } from '@alfalab/scripts-modules';
477
481
 
478
- При использовании `createModuleFetcher` вам не нужно беспокоиться о том, какие параметры вы передаете в `getModuleResources` - они
479
- никак не используются в клиентских модулях.
482
+ const factory: FactoryModule = function (runParams, serverState) {
483
+ // в фабрике можно на основе стейта вернуть готовый модуль
484
+ return {
485
+ serverState,
486
+ doSomething: () => {
487
+ fetch(serverState.baseUrl + '/api/getData')
488
+ }
489
+ };
490
+ }
491
+
492
+ window.ModuleCompat = factory;
493
+ ```
480
494
 
481
- ## Использования загрузчика в реакт-приложении
495
+ Серверный эндпоинт для таких модулей создается абсолютно так же, как и для монтируемых модулей.
482
496
 
483
- Для того чтобы упростить работу с загрузчиком в реакт-приложении, мы предоставляем хук `useModuleLoader`:
497
+ ### Подключение модулей-фабрик в приложении потребителе
484
498
 
485
- ```tsx
486
- import { createModuleLoader, useModuleLoader, createModuleFetcher } from '@alfalab/scripts-modules';
499
+ Для подключения модулей-фабрик можно использовать хук `useModuleFactory`:
487
500
 
488
- const loader = createModuleLoader({
501
+ ```tsx
502
+ import {
503
+ createModuleLoader,
504
+ createModuleFetcher,
505
+ FactoryModule,
506
+ useModuleFactory,
507
+ } from '@alfalab/scripts-modules';
508
+
509
+ const loader = createModuleLoader<FactoryModule>({
510
+ hostAppId: 'bar-app',
489
511
  moduleId: 'test',
490
- getModuleResources: createModuleFetcher({
491
- baseUrl: 'http://localhost:8081',
512
+ getModuleResources: createModuleFetcher({ // или createServerStateModuleFetcher для модулей с серверным состоянием
513
+ baseUrl: 'https://examle.com/foo-app',
492
514
  }),
493
515
  });
494
516
 
495
- const MyComponent = () => {
496
- const { loadingState, module, resources } = useModuleLoader(loader); // вторым параметром можно передать параметры, которые будут переданы в getModuleResources
517
+ export const MyAwesomeComponent = () => {
518
+ const { loadingState, module } = useModuleFactory({ loader });
497
519
 
498
520
  return (
499
521
  <div>
500
- {loadingState === 'loading' && <div>Loading...</div>}
501
- {loadingState === 'error' && <div>Error</div>}
502
- {loadingState === 'success' && (
503
- <div>
504
- <div>Module loaded</div>
505
- <div>{module}</div> {/* модуль, который вы загрузили. Тут будет доступно всё, что было экспортировано из модуля */}
506
- <div>{resources}</div>
507
- </div>
508
- )}
522
+ { loadingState === 'pending' && <div>pending...</div> }
523
+ { loadingState === 'rejected' && <div>Error</div> }
524
+ <pre>{ JSON.stringify(module) }</pre> {/* модуль будет содержать то, что вернула фабрика */}
509
525
  </div>
510
526
  );
511
- };
527
+ }
528
+ ```
529
+
530
+ Если вы хотите использовать модуль-фабрику вне реакт-компонента, вы можете использовать другой метод:
531
+
532
+ ```ts
533
+ import {
534
+ createModuleLoader,
535
+ createModuleFetcher,
536
+ FactoryModule,
537
+ executeModuleFactory,
538
+ } from '@alfalab/scripts-modules';
539
+
540
+ const loader = createModuleLoader<FactoryModule>(/*...*/);
541
+
542
+ (async () => {
543
+ const loaderResult = await loader();
544
+
545
+ const executionResult = executeModuleFactory(
546
+ result.module,
547
+ result.moduleResources.moduleState,
548
+ {}, // опциональные run-параметры модуля
549
+ );
550
+
551
+ console.log(executionResult); // Тут будет то, что возвращает модуль-фабрика
552
+ })();
512
553
  ```
513
554
 
514
- ### Использование монтируемых модулей
555
+ ## Абстрактные модули
556
+
557
+ Для совсем сложных кейсов, когда вам нужен полный контроль над всем тем, что делает код из модуля, вы можете использовать
558
+ абстрактные модули. Они могут иметь абсолютно любую структуру, экспортировать из себя любые методы и константы.
559
+
560
+ ```tsx
561
+ // src/modules/abstract-module/index.ts
562
+ export const doSomething = () => {
563
+ console.log('Hello from abstract module!');
564
+ };
565
+
566
+ export const publicConstant = 3.14;
567
+
568
+ window.AbstractModule = { // для compat модулей
569
+ doSomething,
570
+ publicConstant,
571
+ };
572
+ ```
515
573
 
516
- Для работы с монтируемыми модулями так же есть готовый хук `useModuleMounter`:
574
+ Для подключения вы можете использовать хук `useModuleLoader`:
517
575
 
518
576
  ```tsx
519
- import { createModuleLoader, useModuleMounter, createModuleFetcher } from '@alfalab/scripts-modules';
577
+ import {
578
+ createModuleLoader,
579
+ createModuleFetcher,
580
+ useModuleLoader,
581
+ } from '@alfalab/scripts-modules';
582
+
583
+ type CustomModule = {
584
+ doSomething: () => void;
585
+ publicConstant: number;
586
+ }
520
587
 
521
- const loader = createModuleLoader({
588
+ const loader = createModuleLoader<CustomModule>({
589
+ hostAppId: 'bar-app',
522
590
  moduleId: 'test',
523
591
  getModuleResources: createModuleFetcher({
524
- baseUrl: 'http://localhost:8081',
592
+ baseUrl: 'https://examle.com/foo-app',
525
593
  }),
526
594
  });
527
595
 
528
- const MyComponent = () => {
529
- const { loadingState, targetElementRef } = useModuleMounter({
530
- loader,
531
- loaderParams: {}, // параметры, которые будут переданы в getModuleResources, опционально
532
- runParams: {}, // параметры, которые будут переданы в mount функцию модуля, опционально
533
- });
596
+ export const MyAwesomeComponent = () => {
597
+ const { loadingState, module } = useModuleLoader({ loader });
534
598
 
535
599
  return (
536
600
  <div>
537
- {loadingState === 'loading' && <div>Loading...</div>}
538
- {loadingState === 'error' && <div>Error</div>}
539
- <div ref={targetElementRef} /> {/* сюда будет монтироваться модуль */}
601
+ { loadingState === 'pending' && <div>pending...</div> }
602
+ { loadingState === 'rejected' && <div>Error</div> }
603
+ <pre>{ JSON.stringify(module) }</pre> {/* модуль будет содержать то, что экспортирует модуль, то есть doSomething и publicConstant */}
540
604
  </div>
541
605
  );
542
- };
606
+ }
543
607
  ```
544
608
 
609
+ # Тестирование модулей
610
+ Поскольку в общем случае модули представляют собой простой js код - для тестирования вы можете пользоваться любыми привычными вам инструментами.
611
+
612
+ Для тестирования модулей в cypress или playwright вы можете создать отдельный эндпоинт в вашем приложении, который будет
613
+ подключать модуль в ваше же приложение.
614
+
615
+
545
616
  # Документация API
546
617
 
547
618
  ## Конфигурация модулей