bl-common-vue3 3.8.110 → 3.8.111

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/index.js CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  getGlobalContainer,
10
10
  } from "./src/common/utils/popupContainer";
11
11
  import popupContainer from "./src/common/utils/popupContainer";
12
+ import RenderCustomApp from "./src/Directives/RenderCustomApp/directive.js";
12
13
 
13
14
  export const install = function (app) {
14
15
  Object.keys(components).map((key) => {
@@ -27,4 +28,7 @@ export default {
27
28
  commonLocale,
28
29
  commonAllLocale,
29
30
  ...popupContainer,
31
+ directives: {
32
+ RenderCustomApp: RenderCustomApp,
33
+ },
30
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bl-common-vue3",
3
- "version": "3.8.110",
3
+ "version": "3.8.111",
4
4
  "main": "index.js",
5
5
  "module": "index.js",
6
6
  "description": "bailing vue3 common components lib",
@@ -0,0 +1,184 @@
1
+ import RenderCustomApp from "./index.vue";
2
+ import { createApp, defineComponent, h, reactive } from "vue";
3
+ import utils from "../../common/utils/util.js";
4
+
5
+ // 从 basicConfig 中获取微应用加载需求配置。
6
+ const getCustomizationAppsConfig = () => {
7
+ return utils.getCommonLocal("basicConfig")?.org_extra_config?.customization_apps || {};
8
+ };
9
+
10
+ // 记录当前宿主元素上挂载的内部 Vue 应用实例,便于更新和卸载。
11
+ const MICRO_APP_INSTANCE = "__microAppInst__";
12
+ // 记录宿主元素被指令隐藏前的 display,key 恢复有效时用于还原。
13
+ const MICRO_APP_HOST_DISPLAY = "__microAppHostDisplay__";
14
+ // 标记宿主元素是否由当前指令隐藏,避免误恢复业务自身控制的 display。
15
+ const MICRO_APP_HOST_HIDDEN = "__microAppHostHidden__";
16
+ // 对外派发的统一事件名,使用冒号命名避免与原生或业务事件冲突。
17
+ const MICRO_APP_DATA_CHANGE_EVENT = "micro:change";
18
+
19
+ // 统一指令入参,避免 binding.value 为空时后续访问报错。
20
+ const getDirectiveValue = (binding = {}, vnode = {}) => {
21
+ const props = vnode?.props || {};
22
+ return {
23
+ key: binding.value || "",
24
+ // microAppTagName 传入micro-app自定义tagName
25
+ microAppTagName: props.microAppTagName || "",
26
+ // microData 传入业务入参,默认值为空对象
27
+ microData: props.microData || {},
28
+ };
29
+ };
30
+
31
+ // key 失效后若宿主元素已无 DOM 子元素,则隐藏宿主,避免空容器继续占位。
32
+ const hideEmptyHostElement = (el) => {
33
+ if (el.children.length) {
34
+ return;
35
+ }
36
+ if (!el[MICRO_APP_HOST_HIDDEN]) {
37
+ el[MICRO_APP_HOST_DISPLAY] = el.style.display || "";
38
+ }
39
+ el.style.display = "none";
40
+ el[MICRO_APP_HOST_HIDDEN] = true;
41
+ };
42
+
43
+ // key 重新有效时恢复由指令隐藏的宿主元素。
44
+ const showHostElement = (el) => {
45
+ if (!el[MICRO_APP_HOST_HIDDEN]) {
46
+ return;
47
+ }
48
+ el.style.display = el[MICRO_APP_HOST_DISPLAY] || "";
49
+ delete el[MICRO_APP_HOST_DISPLAY];
50
+ delete el[MICRO_APP_HOST_HIDDEN];
51
+ };
52
+
53
+ // 将微应用数据变更透传给指令调用方,统一通过 DOM 事件接收。
54
+ const dispatchDatachange = (el, binding, data) => {
55
+ const directiveValue = getDirectiveValue(binding);
56
+ const detail = {
57
+ key: directiveValue.key || "",
58
+ data,
59
+ };
60
+
61
+ // 派发 DOM 事件,兼容 addEventListener 和 Vue 模板监听。
62
+ [MICRO_APP_DATA_CHANGE_EVENT].forEach((eventName) => {
63
+ el.dispatchEvent(
64
+ new CustomEvent(eventName, {
65
+ detail,
66
+ bubbles: true,
67
+ composed: true,
68
+ })
69
+ );
70
+ });
71
+ };
72
+
73
+ // 清空宿主原始子元素前记录当前占位高度,避免 loading 因容器无固高而定位到错误区域。
74
+ const getHostStyle = (el) => {
75
+ const height = Math.ceil(el.getBoundingClientRect?.().height || 0);
76
+
77
+ if (!height) {
78
+ return {};
79
+ }
80
+
81
+ return {
82
+ minHeight: `${height}px`,
83
+ };
84
+ };
85
+
86
+ // 在指令宿主元素内创建独立 Vue 应用,用组件方式渲染真实 micro-app。
87
+ const mountMicroApp = (el, binding, vnode) => {
88
+ showHostElement(el);
89
+ const state = reactive({
90
+ bindValue: getDirectiveValue(binding, vnode),
91
+ customizationAppsConfig: getCustomizationAppsConfig(),
92
+ hostStyle: getHostStyle(el),
93
+ });
94
+
95
+ const app = createApp(
96
+ defineComponent({
97
+ name: "RenderCustomAppDirectiveRoot",
98
+ setup() {
99
+ return () =>
100
+ h(RenderCustomApp, {
101
+ microId: state.bindValue.key || "",
102
+ microAppTagName: state.bindValue.microAppTagName || "",
103
+ microData: state.bindValue.microData || {},
104
+ hostStyle: state.hostStyle,
105
+ customizationAppsConfig: state.customizationAppsConfig,
106
+ "onMicro:change": (data) => {
107
+ const instance = el[MICRO_APP_INSTANCE];
108
+ dispatchDatachange(el, instance?.binding || binding, data);
109
+ },
110
+ });
111
+ },
112
+ })
113
+ );
114
+
115
+ // 继承宿主应用上下文,确保内部组件可以继续使用全局插件、store、i18n 等能力。
116
+ const hostAppContext =
117
+ vnode?.appContext ||
118
+ binding?.instance?.$?.appContext ||
119
+ binding?.instance?.$?.vnode?.appContext ||
120
+ null;
121
+ if (hostAppContext) {
122
+ Object.assign(app._context, hostAppContext);
123
+ }
124
+ // 清空宿主元素原内容,再挂载微应用包装组件。
125
+ el.innerHTML = "";
126
+ app.mount(el);
127
+ el[MICRO_APP_INSTANCE] = {
128
+ app,
129
+ binding,
130
+ state,
131
+ };
132
+ };
133
+
134
+ // 响应指令参数变化:无 key 时卸载,有实例时只更新响应式入参。
135
+ const updateMicroApp = (el, binding, vnode) => {
136
+ const instance = el[MICRO_APP_INSTANCE];
137
+ const bindingValue = getDirectiveValue(binding, vnode) || {};
138
+ const customizationAppsConfig = getCustomizationAppsConfig();
139
+ if (!bindingValue.key || !customizationAppsConfig?.[bindingValue.key]) {
140
+ unmountMicroApp(el, {
141
+ hideEmptyHost: true,
142
+ });
143
+ return;
144
+ }
145
+ showHostElement(el);
146
+ if (!instance) {
147
+ mountMicroApp(el, binding, vnode);
148
+ return;
149
+ }
150
+
151
+ instance.state.bindValue = bindingValue;
152
+ instance.state.customizationAppsConfig = customizationAppsConfig;
153
+ // 已挂载后若宿主尺寸变化,保留最新占位信息供 loading 居中使用。
154
+ instance.state.hostStyle = getHostStyle(el);
155
+ instance.binding = binding;
156
+ };
157
+
158
+ // 宿主组件卸载或 key 失效时,销毁内部 Vue 应用并移除实例引用。
159
+ const unmountMicroApp = (el, options = {}) => {
160
+ const instance = el[MICRO_APP_INSTANCE];
161
+
162
+ if (instance?.app) {
163
+ instance.app.unmount();
164
+ delete el[MICRO_APP_INSTANCE];
165
+ }
166
+ if (options.hideEmptyHost) {
167
+ hideEmptyHostElement(el);
168
+ }
169
+ };
170
+
171
+ export default {
172
+ // 指令挂载时初始化微应用实例。
173
+ mounted(el, binding, vnode) {
174
+ updateMicroApp(el, binding, vnode);
175
+ },
176
+ // 指令参数变化时更新微应用实例。
177
+ updated(el, binding, vnode) {
178
+ updateMicroApp(el, binding, vnode);
179
+ },
180
+ // 指令卸载时卸载微应用实例。
181
+ beforeUnmount(el) {
182
+ unmountMicroApp(el);
183
+ },
184
+ };
@@ -0,0 +1,401 @@
1
+ <template>
2
+ <template v-if="renderAppConfigList.length">
3
+ <template v-for="renderAppConfig in renderAppConfigList" :key="renderAppConfig.name">
4
+ <div
5
+ class="use-micro-app-wrap"
6
+ :style="[renderHostStyle, renderAppConfig.style]"
7
+ >
8
+ <component
9
+ v-if="renderAppConfig.type === RENDER_APP_TYPE.MICRO"
10
+ :is="renderMicroAppTagName"
11
+ :key="`${renderMicroAppTagName}_${renderAppConfig.name}`"
12
+ class="use-micro-app"
13
+ router-mode="state"
14
+ :name="renderAppConfig.name"
15
+ :url="renderAppConfig.entry"
16
+ :default-page="renderAppConfig.defaultPage"
17
+ :data="childAppData"
18
+ @mounted="handleRenderAppLoadEnd(renderAppConfig.name)"
19
+ @error="handleRenderAppLoadEnd(renderAppConfig.name)"
20
+ @datachange="handleDataChange"
21
+ destroy
22
+ />
23
+ <iframe
24
+ v-else
25
+ class="use-micro-app use-iframe-app"
26
+ :src="renderAppConfig.src"
27
+ frameborder="0"
28
+ @load="handleRenderAppLoadEnd(renderAppConfig.name)"
29
+ @error="handleRenderAppLoadEnd(renderAppConfig.name)"
30
+ ></iframe>
31
+ <div
32
+ v-if="loadingAppMap[renderAppConfig.name]"
33
+ class="use-micro-app-loading"
34
+ >
35
+ <a-spin />
36
+ </div>
37
+ </div>
38
+ </template>
39
+ </template>
40
+ </template>
41
+
42
+ <script>
43
+ import { computed, defineComponent, getCurrentInstance, onBeforeUnmount, reactive, watch } from "vue";
44
+ import { Spin } from "ant-design-vue";
45
+ import utils from "../../common/utils/util";
46
+ import microApp from "kj-micro-app";
47
+
48
+ const RENDER_APP_TYPE = {
49
+ MICRO: "micro",
50
+ IFRAME: "iframe",
51
+ };
52
+ const DEFAULT_MICRO_APP_TAG_NAME = "micro-app";
53
+ const IFRAME_DEFAULT_STYLE = {
54
+ height: "inherit",
55
+ };
56
+ const LOADING_MAX_WAIT = 15000;
57
+
58
+ // 从 basicConfig 中获取定制应用渲染配置。
59
+ const getCustomizationAppsConfig = () => {
60
+ return utils.getCommonLocal("basicConfig")?.org_extra_config?.customization_apps || {};
61
+ };
62
+
63
+ // 新配置固定为 key -> Array
64
+ const getRenderConfigList = (config) => {
65
+ return Array.isArray(config) ? config.filter(Boolean) : [];
66
+ };
67
+
68
+ // micro-app 实例名必须稳定且唯一,iframe 也复用该 name 作为渲染 key。
69
+ const getRenderAppName = (microId, type, index) => {
70
+ return `${microId}_${type}_${index}`;
71
+ };
72
+
73
+ // 根据当前运行环境选择应用入口,开发环境使用 path_dev,生产环境使用 path_prod。
74
+ const getRenderEntry = (config) => {
75
+ return utils.isEnv() ? config?.path_dev || "" : config?.path_prod || "";
76
+ };
77
+
78
+ // 统一序列化 query,过滤空值,避免 iframe 和 micro-app 两套逻辑重复处理。
79
+ const stringifyQuery = (query = {}) => {
80
+ return Object.keys(query)
81
+ .filter((key) => query[key] !== undefined && query[key] !== null && query[key] !== "")
82
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`)
83
+ .join("&");
84
+ };
85
+
86
+ // micro-app 使用 hash 路由,iframe 同样拼到入口后作为页面地址。
87
+ const formatDefaultPage = (url, type, query = {}) => {
88
+ if (!url) {
89
+ return "";
90
+ }
91
+
92
+ const queryParams = stringifyQuery(query);
93
+ let pageUrl = url;
94
+ if(type === RENDER_APP_TYPE.MICRO) {
95
+ pageUrl = url.indexOf("#") === 0 ? url : `#${url}`;
96
+ }
97
+
98
+ if (queryParams) {
99
+ return pageUrl.indexOf("?") === -1 ? `${pageUrl}?${queryParams}` : `${pageUrl}&${queryParams}`;
100
+ }
101
+
102
+ return pageUrl;
103
+ };
104
+
105
+ // iframe 没有 default-page 概念,需要把入口和 hash 页面路径拼成完整 src。
106
+ const formatIframeSrc = (entry, defaultPage) => {
107
+ if (!defaultPage) {
108
+ return "";
109
+ }
110
+
111
+ // defaultPage包含http协议,直接返回
112
+ if(defaultPage.indexOf("http") === 0) {
113
+ return defaultPage;
114
+ }
115
+
116
+ return `${entry.replace(/\/?$/, "/")}${defaultPage}`;
117
+ };
118
+
119
+ // 样式只使用定制应用配置,避免继续接收旧指令对象里的 styles。
120
+ const getRenderStyle = (config, type) => {
121
+ const configStyle = config?.style || {};
122
+ const style = {
123
+ ...configStyle,
124
+ };
125
+
126
+ if (type === RENDER_APP_TYPE.IFRAME) {
127
+ return {
128
+ ...IFRAME_DEFAULT_STYLE,
129
+ ...style,
130
+ };
131
+ }
132
+
133
+ return style;
134
+ };
135
+
136
+ // 合并默认下发数据和 microData,业务入参固定放在 data.microData 中。
137
+ const mergeChildAppData = (baseData = {}, microData = {}, microId) => {
138
+ return {
139
+ ...baseData,
140
+ props: {
141
+ ...(baseData.props || {}),
142
+ },
143
+ microData: {
144
+ key: microId,
145
+ ...microData,
146
+ },
147
+ };
148
+ };
149
+
150
+ export default defineComponent({
151
+ name: "RenderCustomApp",
152
+ components: {
153
+ "a-spin": Spin,
154
+ },
155
+ props: {
156
+ // 定制应用 id。
157
+ microId: {
158
+ type: String,
159
+ default: "",
160
+ },
161
+ // 定制应用数据。
162
+ microData: {
163
+ type: Object,
164
+ default: () => ({}),
165
+ },
166
+ // 宿主应用注册 micro-app.start 时配置的 tagName,由 v-microId 指令参数显式传入。
167
+ microAppTagName: {
168
+ type: String,
169
+ default: "",
170
+ },
171
+ // 指令清空原始子元素前记录的宿主占位样式,用于让 loading 在原卡片区域内居中。
172
+ hostStyle: {
173
+ type: Object,
174
+ default: () => ({}),
175
+ },
176
+ // 定制应用渲染配置。
177
+ customizationAppsConfig: {
178
+ type: Object,
179
+ default: () => ({}),
180
+ },
181
+ },
182
+ emits: ["micro:change"],
183
+ setup(props, { emit }) {
184
+ const currentInstance = getCurrentInstance();
185
+ // 从继承到的宿主上下文中读取全局能力,用于补全路由参数和下发公共数据。
186
+ const hostStore = computed(() => currentInstance?.proxy?.$store);
187
+ const hostRoute = computed(() => currentInstance?.proxy?.$route);
188
+ const hostUtils = computed(() => currentInstance?.proxy?.$utils);
189
+ // 宿主应用配置的全局 tagName。
190
+ const hostMicroAppTagName = computed(() => currentInstance?.proxy?.$microAppTagName);
191
+ const loadingAppMap = reactive({});
192
+ const loadingTimeoutTimerMap = {};
193
+
194
+ // 从 props 中获取 microId,用于从 customization_apps 中定位需要渲染的应用列表。
195
+ const microId = computed(() => props.microId || "");
196
+ const renderMicroAppTagName = computed(() => {
197
+ return props.microAppTagName || hostMicroAppTagName.value || DEFAULT_MICRO_APP_TAG_NAME;
198
+ });
199
+ const renderHostStyle = computed(() => props.hostStyle || {});
200
+
201
+ // 优先使用指令显式传入的配置,否则从公共缓存 basicConfig 中读取。
202
+ const customizationAppsConfig = computed(() => {
203
+ return Object.keys(props.customizationAppsConfig || {}).length
204
+ ? props.customizationAppsConfig
205
+ : getCustomizationAppsConfig();
206
+ });
207
+
208
+ // 将新格式配置转换为模板可直接渲染的 micro-app / iframe 配置。
209
+ const renderAppConfigList = computed(() => {
210
+ const configs = getRenderConfigList(customizationAppsConfig.value?.[microId.value]);
211
+
212
+ return configs
213
+ .map((config, index) => {
214
+ const type = config?.type || "";
215
+ const entry = getRenderEntry(config);
216
+ const defaultPage = formatDefaultPage(config?.url || "", type, hostRoute.value?.query || {});
217
+ const name = getRenderAppName(microId.value, type, index);
218
+ const style = getRenderStyle(config, type);
219
+ const renderConfig = {
220
+ ...config,
221
+ type,
222
+ name,
223
+ entry,
224
+ defaultPage,
225
+ style,
226
+ };
227
+
228
+ // iframe 渲染需要完整 src,micro-app 仍然使用 entry + defaultPage。
229
+ if (type === RENDER_APP_TYPE.IFRAME) {
230
+ renderConfig.src = formatIframeSrc(entry, defaultPage);
231
+ }
232
+
233
+ return renderConfig;
234
+ })
235
+ .filter((config) => {
236
+ if (config.type === RENDER_APP_TYPE.MICRO) {
237
+ return config.name && config.entry && config.defaultPage;
238
+ }
239
+ if (config.type === RENDER_APP_TYPE.IFRAME) {
240
+ return config.name && config.src;
241
+ }
242
+ return false;
243
+ });
244
+ });
245
+
246
+ // 默认下发宿主公共数据,业务入参只从 v-microId 宿主节点的 microData 读取。
247
+ const childAppData = computed(() => {
248
+ const baseData =
249
+ hostUtils.value?.getAppToChildData?.({}) ||
250
+ (hostStore.value?.getters?.mainAppInfo ? { props: hostStore.value.getters.mainAppInfo } : {});
251
+
252
+ return mergeChildAppData(baseData, props?.microData || {}, microId.value);
253
+ });
254
+
255
+ // 只记录 micro-app 实例名,iframe 不需要走 microApp.unmountApp 销毁流程。
256
+ const microAppNameList = computed(() => {
257
+ return renderAppConfigList.value
258
+ .filter((item) => item.type === RENDER_APP_TYPE.MICRO)
259
+ .map((item) => item.name);
260
+ });
261
+ const renderAppNameList = computed(() => {
262
+ return renderAppConfigList.value.map((item) => item.name);
263
+ });
264
+
265
+ // 切换 key 或组件卸载时主动销毁仍处于激活状态的 micro-app 实例。
266
+ const unmountApp = (name) => {
267
+ if (!name) {
268
+ return;
269
+ }
270
+
271
+ const activeApps = microApp.getActiveApps?.() || [];
272
+ if (!activeApps.includes(name)) {
273
+ return;
274
+ }
275
+
276
+ Promise.resolve(
277
+ microApp.unmountApp(name, {
278
+ destroy: true,
279
+ clearAliveState: true,
280
+ })
281
+ ).catch(() => {});
282
+ };
283
+
284
+ const clearLoadingTimer = (name) => {
285
+ if (!name) {
286
+ return;
287
+ }
288
+
289
+ if (loadingTimeoutTimerMap[name]) {
290
+ clearTimeout(loadingTimeoutTimerMap[name]);
291
+ delete loadingTimeoutTimerMap[name];
292
+ }
293
+ };
294
+
295
+ // micro-app mounted/error 或 iframe load/error 后关闭当前实例 loading。
296
+ const handleRenderAppLoadEnd = (name) => {
297
+ const loadingInfo = loadingAppMap[name];
298
+ if (!loadingInfo) {
299
+ return;
300
+ }
301
+
302
+ clearLoadingTimer(name);
303
+ delete loadingAppMap[name];
304
+ };
305
+
306
+ // 使用 Ant Design Vue 的 Spin 表示加载中,等待真实事件或兜底超时结束。
307
+ const startRenderAppLoading = (name) => {
308
+ if (!name || loadingAppMap[name]) {
309
+ return;
310
+ }
311
+
312
+ loadingAppMap[name] = true;
313
+ // 防止自定义标签异常未派发 mounted/error 时 loading 永久停留,正常场景会被真实事件提前清理。
314
+ loadingTimeoutTimerMap[name] = setTimeout(() => {
315
+ handleRenderAppLoadEnd(name);
316
+ }, LOADING_MAX_WAIT);
317
+ };
318
+
319
+ // 将 micro-app 子应用派发的数据事件重新抛给使用指令的宿主元素。
320
+ const handleDataChange = (event) => {
321
+ emit("micro:change", event?.detail?.data);
322
+ };
323
+
324
+ // 配置列表变化时,同步 micro-app 和 iframe 的 loading 生命周期。
325
+ watch(
326
+ () => renderAppNameList.value,
327
+ (names, oldNames = []) => {
328
+ names.forEach((name) => {
329
+ startRenderAppLoading(name);
330
+ });
331
+ oldNames
332
+ .filter((name) => name && !names.includes(name))
333
+ .forEach((name) => {
334
+ clearLoadingTimer(name);
335
+ delete loadingAppMap[name];
336
+ });
337
+ },
338
+ {
339
+ immediate: true,
340
+ }
341
+ );
342
+
343
+ // 配置列表变化时,只销毁已移除的 micro-app,保留仍在使用的实例。
344
+ watch(
345
+ () => microAppNameList.value,
346
+ (names, oldNames = []) => {
347
+ oldNames
348
+ .filter((name) => name && !names.includes(name))
349
+ .forEach((name) => {
350
+ unmountApp(name);
351
+ });
352
+ }
353
+ );
354
+
355
+ // 宿主组件卸载时统一释放由指令创建的 micro-app 实例。
356
+ onBeforeUnmount(() => {
357
+ Object.keys(loadingTimeoutTimerMap).forEach(clearLoadingTimer);
358
+ microAppNameList.value.forEach(unmountApp);
359
+ });
360
+
361
+ return {
362
+ RENDER_APP_TYPE,
363
+ childAppData,
364
+ handleDataChange,
365
+ handleRenderAppLoadEnd,
366
+ loadingAppMap,
367
+ renderHostStyle,
368
+ renderMicroAppTagName,
369
+ renderAppConfigList,
370
+ };
371
+ },
372
+ });
373
+ </script>
374
+
375
+ <style lang="less" scoped>
376
+ .use-micro-app {
377
+ display: block;
378
+ width: 100%;
379
+ height: 100%;
380
+ }
381
+
382
+ .use-micro-app-wrap {
383
+ position: relative;
384
+ width: 100%;
385
+ height: 100%;
386
+ }
387
+
388
+ .use-micro-app-loading {
389
+ position: absolute;
390
+ inset: 0;
391
+ z-index: 1;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ background-color: #fff;
396
+ }
397
+
398
+ .use-iframe-app {
399
+ border: 0;
400
+ }
401
+ </style>