cloud-web-corejs 1.0.54-dev.763 → 1.0.54-dev.764
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/package.json +1 -1
- package/src/index.js +0 -1
- package/src/layout/components/AppMain.vue +1 -2
- package/src/microRouter/index.js +393 -521
- package/src/store/modules/micro.js +2 -6
- package/src/store/modules/permission.js +2 -2
- package/src/utils/menuAdapter.js +140 -29
- package/src/views/user/menuFallback/index.vue +3 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -268,7 +268,6 @@ export async function mount(props = {}) {
|
|
|
268
268
|
// 非数组(null/未传)= 自治嵌入模式:子应用自拉菜单、保留自身壳(micro store 据此置 selfManaged)
|
|
269
269
|
menus: Array.isArray(props.menus) ? props.menus : null,
|
|
270
270
|
mainAdapter: props.mainAdapter || null,
|
|
271
|
-
lang: props.lang || "",
|
|
272
271
|
// 主应用经本地调试覆盖装载:允许 URL 直达未配菜单的页面
|
|
273
272
|
debug: !!props.microDebug,
|
|
274
273
|
});
|
|
@@ -84,10 +84,9 @@ export default {
|
|
|
84
84
|
// handler 存实例属性并在销毁时 $off:事件总线是应用级单例,
|
|
85
85
|
// 不解绑会在登出重建布局后累积 handler 并钉住旧实例(内存泄露)
|
|
86
86
|
this.reflushRouteViewHandler = (view) => {
|
|
87
|
-
let meta = this.$route.meta || {};
|
|
88
87
|
// 子应用页面不在本 router-view 内(由 qiankun 挂独立容器),
|
|
89
88
|
// 走本地 /redirect 重建只会隐藏再显示容器、实例不变,须转发给子应用自己重建
|
|
90
|
-
if (
|
|
89
|
+
if (refreshMicroApp(this.$route)) {
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
92
|
if(this.$route.name=="outLink"){
|
package/src/microRouter/index.js
CHANGED
|
@@ -1,521 +1,393 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* qiankun 主应用侧:应用注册表驱动的子应用注册。
|
|
3
|
-
* 注册表载体为逻辑参数 micro_app_registry(JSON 数组),
|
|
4
|
-
* 协议见 docs/qiankun微前端改造-实现方案.md 3.2 / 3.4。
|
|
5
|
-
* 由 permission store 在登录后菜单生成流程中调用(settings.microMainEnabled 门控)。
|
|
6
|
-
*/
|
|
7
|
-
import { loadMicroApp } from "qiankun";
|
|
8
|
-
import { getLogicParamValue } from "@base/api/user";
|
|
9
|
-
import {
|
|
10
|
-
getAccessPath,
|
|
11
|
-
validateRegistry,
|
|
12
|
-
resolveEntry,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*/
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*/
|
|
294
|
-
export function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
el,
|
|
395
|
-
instance: loadMicroApp({
|
|
396
|
-
name: config.name,
|
|
397
|
-
entry: config.entry,
|
|
398
|
-
container: el,
|
|
399
|
-
props: config.props,
|
|
400
|
-
}),
|
|
401
|
-
};
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
// 已保活:显示容器并通知子应用恢复路由监听、同步当前 hash(含"切到本应用另一菜单"的深导航)
|
|
405
|
-
notifyMicroApp(alive.instance, { microActive: true });
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function deactivateMicroApp(appCode) {
|
|
410
|
-
const alive = aliveApps[appCode];
|
|
411
|
-
if (!alive) return;
|
|
412
|
-
alive.el.style.display = "none";
|
|
413
|
-
// 暂停子应用路由监听,防止隐藏期间对主应用的 URL 变化做出反应(回写 URL 互相打架)
|
|
414
|
-
notifyMicroApp(alive.instance, { microActive: false });
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function notifyMicroApp(instance, props) {
|
|
418
|
-
if (!instance || typeof instance.update !== "function") return;
|
|
419
|
-
const mountPromise = instance.mountPromise || Promise.resolve();
|
|
420
|
-
mountPromise
|
|
421
|
-
.then(() => instance.update(props))
|
|
422
|
-
.catch((e) => {
|
|
423
|
-
console.error("[microRouter] 子应用 update 通知失败:", e);
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* 转发"刷新当前页"给子应用。
|
|
429
|
-
* 保活模式下主应用那套 delCachedView + /redirect 重建只作用于自己的 router-view,
|
|
430
|
-
* 子应用内容由 qiankun 挂在独立容器里,走一圈只是容器隐藏再显示、实例毫发无损,
|
|
431
|
-
* 表现为点刷新没反应——必须由子应用在自身路由内重建组件。
|
|
432
|
-
* 返回是否已转发(false 表示该应用未装载,调用方回落自身刷新逻辑)。
|
|
433
|
-
*/
|
|
434
|
-
export function refreshMicroApp(appCode) {
|
|
435
|
-
const alive = aliveApps[appCode];
|
|
436
|
-
if (!alive) return false;
|
|
437
|
-
// 带时间戳:qiankun 对相同 props 也会调用 update,但时间戳能让子应用区分每次点击
|
|
438
|
-
notifyMicroApp(alive.instance, { microRefresh: Date.now() });
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** 真正卸载全部子应用(登出等场景按需调用;常规页签切换不卸载) */
|
|
443
|
-
export function unmountAllMicroApps() {
|
|
444
|
-
Object.keys(aliveApps).forEach((code) => {
|
|
445
|
-
const alive = aliveApps[code];
|
|
446
|
-
if (alive && alive.instance) {
|
|
447
|
-
alive.instance.unmount().catch(() => {});
|
|
448
|
-
}
|
|
449
|
-
if (alive && alive.el && alive.el.parentNode) {
|
|
450
|
-
alive.el.parentNode.removeChild(alive.el);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
aliveApps = {};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* 每个子应用先按菜单生成精确路由以携带菜单标题等元数据,再追加通配兜底。
|
|
458
|
-
* 路由均不挂组件,内容由 qiankun 挂进 AppMain 的 #Appmicro 容器。
|
|
459
|
-
*/
|
|
460
|
-
export function buildSubAppRoutes(apps, subAppMenus = {}) {
|
|
461
|
-
const routes = [];
|
|
462
|
-
(apps || []).forEach((app) => {
|
|
463
|
-
const seenPaths = {};
|
|
464
|
-
(subAppMenus[app.appCode] || []).forEach((menu, index) => {
|
|
465
|
-
let path = getAccessPath(menu).split("?")[0];
|
|
466
|
-
if (!path) return;
|
|
467
|
-
if (!path.startsWith("/")) {
|
|
468
|
-
path = "/" + path;
|
|
469
|
-
}
|
|
470
|
-
if (seenPaths[path]) return;
|
|
471
|
-
seenPaths[path] = true;
|
|
472
|
-
routes.push({
|
|
473
|
-
path,
|
|
474
|
-
name: `micro-${app.appCode}-menu-${menu.id || menu.menuCode || index}`,
|
|
475
|
-
meta: {
|
|
476
|
-
title: menu.menuName || app.appName,
|
|
477
|
-
enTitle: menu.menuEnName || null,
|
|
478
|
-
treePathName: menu.treePathName,
|
|
479
|
-
repeatOpen: menu.repeatOpen,
|
|
480
|
-
isMicroApp: true,
|
|
481
|
-
appCode: app.appCode,
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
routes.push({
|
|
486
|
-
path: "/" + app.appCode + "/*",
|
|
487
|
-
name: "micro-" + app.appCode,
|
|
488
|
-
meta: {
|
|
489
|
-
title: app.appName,
|
|
490
|
-
isMicroApp: true,
|
|
491
|
-
appCode: app.appCode,
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
return routes;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// 子应用表单脚本跳主应用路由必须走该回调,不得直接摸主应用 router
|
|
499
|
-
function createDefaultMainAdapter() {
|
|
500
|
-
return {
|
|
501
|
-
navigate(path) {
|
|
502
|
-
const vueRoot = window.$vueRoot;
|
|
503
|
-
if (vueRoot && vueRoot.$router) {
|
|
504
|
-
vueRoot.$router.push(path).catch(() => {});
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
openTab(path) {
|
|
508
|
-
this.navigate(path);
|
|
509
|
-
},
|
|
510
|
-
closeTab(view) {
|
|
511
|
-
const vueRoot = window.$vueRoot;
|
|
512
|
-
if (vueRoot && vueRoot.$store) {
|
|
513
|
-
return vueRoot.$store.dispatch(
|
|
514
|
-
"tagsView/delView",
|
|
515
|
-
view || vueRoot.$route
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
return Promise.resolve();
|
|
519
|
-
},
|
|
520
|
-
};
|
|
521
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* qiankun 主应用侧:应用注册表驱动的子应用注册。
|
|
3
|
+
* 注册表载体为逻辑参数 micro_app_registry(JSON 数组),
|
|
4
|
+
* 协议见 docs/qiankun微前端改造-实现方案.md 3.2 / 3.4。
|
|
5
|
+
* 由 permission store 在登录后菜单生成流程中调用(settings.microMainEnabled 门控)。
|
|
6
|
+
*/
|
|
7
|
+
import { loadMicroApp } from "qiankun";
|
|
8
|
+
import { getLogicParamValue } from "@base/api/user";
|
|
9
|
+
import {
|
|
10
|
+
getAccessPath,
|
|
11
|
+
validateRegistry,
|
|
12
|
+
resolveEntry,
|
|
13
|
+
swapMenuNamespace,
|
|
14
|
+
parseDebugEnabled,
|
|
15
|
+
isDebugAllowed,
|
|
16
|
+
MICRO_SEGMENT,
|
|
17
|
+
MICRO_DEBUG_SEGMENT,
|
|
18
|
+
} from "@base/utils/menuAdapter";
|
|
19
|
+
import { getToken } from "@base/utils/auth";
|
|
20
|
+
import router from "@base/router";
|
|
21
|
+
|
|
22
|
+
const REGISTRY_PARAM_CODE = "micro_app_registry";
|
|
23
|
+
// 调试开关单独一条逻辑参数:与注册表生命周期不同,运维开关调试不必改注册表 JSON
|
|
24
|
+
const DEBUG_PARAM_CODE = "micro_debug_enabled";
|
|
25
|
+
|
|
26
|
+
const REGISTRY_TIMEOUT = 8000;
|
|
27
|
+
const CONTAINER_WRAPPER_ID = "Appmicro";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 调试命名空间(/micro_debug/<appCode>/...)的约定入口。
|
|
31
|
+
* 调试态不落任何存储、不读 URL 查询参数——路径本身就是开关,因此入口只能是约定值。
|
|
32
|
+
* 各子应用把 devServer 端口固定为 17527 即可零配置被主应用装载。
|
|
33
|
+
*/
|
|
34
|
+
const DEBUG_ENTRY = "http://localhost:17527/";
|
|
35
|
+
|
|
36
|
+
// 保活管理:appCode → { instance, el };离开子应用路由只隐藏容器不卸载,
|
|
37
|
+
// 切回直接显示并经 update 生命周期同步路由,页面状态(页签/表格/表单)得以保留
|
|
38
|
+
let microAppConfigs = {};
|
|
39
|
+
let aliveApps = {};
|
|
40
|
+
let routeHookInstalled = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 读一条逻辑参数的原始值;缺失 / 请求失败 / 超时一律回落空串,不阻塞登录流程。
|
|
44
|
+
*/
|
|
45
|
+
function readLogicParam(paramCode) {
|
|
46
|
+
const fetchPromise = new Promise((resolve) => {
|
|
47
|
+
getLogicParamValue({
|
|
48
|
+
data: { paramCode },
|
|
49
|
+
failMsg: false,
|
|
50
|
+
errorMsg: false,
|
|
51
|
+
modal: false,
|
|
52
|
+
success: (res) => resolve((res && res.objx) || ""),
|
|
53
|
+
}).catch(() => resolve(""));
|
|
54
|
+
});
|
|
55
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
56
|
+
setTimeout(() => resolve(""), REGISTRY_TIMEOUT);
|
|
57
|
+
});
|
|
58
|
+
return Promise.race([fetchPromise, timeoutPromise]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 读应用注册表 + 调试开关(两条逻辑参数并行取,互不依赖)。
|
|
63
|
+
* 注册表非法条目由 validateRegistry 跳过并在控制台显式报错;
|
|
64
|
+
* 调试开关取不到即视为关闭,标注在各 app 的 debugEnabled 上供下游使用。
|
|
65
|
+
*/
|
|
66
|
+
export function getAppRegistry() {
|
|
67
|
+
return Promise.all([
|
|
68
|
+
readLogicParam(REGISTRY_PARAM_CODE),
|
|
69
|
+
readLogicParam(DEBUG_PARAM_CODE),
|
|
70
|
+
]).then(([registryRaw, debugRaw]) => {
|
|
71
|
+
if (!registryRaw) return [];
|
|
72
|
+
const { apps, errors } = validateRegistry(registryRaw);
|
|
73
|
+
if (errors.length) {
|
|
74
|
+
console.error("[microRouter] 应用注册表存在非法条目(已跳过):", errors);
|
|
75
|
+
}
|
|
76
|
+
const debugSetting = parseDebugEnabled(debugRaw);
|
|
77
|
+
return apps.map((app) =>
|
|
78
|
+
Object.assign({}, app, {
|
|
79
|
+
debugEnabled: isDebugAllowed(debugSetting, app.appCode),
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 装载实例键:同一个 appCode 的正式态与调试态各自保活,可同时开着对比 */
|
|
86
|
+
function instanceKey(appCode, debug) {
|
|
87
|
+
return debug ? appCode + "@" + MICRO_DEBUG_SEGMENT : appCode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 登记子应用(保活模式):不再走 registerMicroApps/start 的 activeRule 自动装卸,
|
|
92
|
+
* 改为路由钩子驱动的 loadMicroApp 手动装载——首次进入装载,离开只隐藏,切回显示并同步路由。
|
|
93
|
+
* options.subAppMenus 为 menuAdapter.dispatchMenus 的切分结果。
|
|
94
|
+
*/
|
|
95
|
+
export function registerSubApps(apps, options = {}) {
|
|
96
|
+
const subAppMenus = options.subAppMenus || {};
|
|
97
|
+
const mainAdapter = options.mainAdapter || createDefaultMainAdapter();
|
|
98
|
+
const userInfo = options.userInfo || null;
|
|
99
|
+
microAppConfigs = {};
|
|
100
|
+
(apps || []).forEach((app) => {
|
|
101
|
+
const menus = subAppMenus[app.appCode] || [];
|
|
102
|
+
const baseProps = {
|
|
103
|
+
appCode: app.appCode,
|
|
104
|
+
token: getToken(),
|
|
105
|
+
lang: localStorage.getItem("i18n-lang") || "",
|
|
106
|
+
userInfo,
|
|
107
|
+
mainAdapter,
|
|
108
|
+
};
|
|
109
|
+
const entry = resolveEntry(app.entry);
|
|
110
|
+
if (entry) {
|
|
111
|
+
microAppConfigs[instanceKey(app.appCode, false)] = {
|
|
112
|
+
name: app.appCode,
|
|
113
|
+
entry,
|
|
114
|
+
props: Object.assign({}, baseProps, { menus, microDebug: false }),
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
console.error(
|
|
118
|
+
"[microRouter] 子应用缺少当前环境 entry,正式态已跳过:" + app.appCode
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// 调试态:入口固定为约定端口,菜单路径一并换到 /micro_debug/ 命名空间——
|
|
122
|
+
// 子应用据此生成的路由才与浏览器 hash 对得上(主子路径始终一致)。
|
|
123
|
+
// 注册表未显式 debugEnabled 的应用不建调试实例,其 /micro_debug/ 地址打不开
|
|
124
|
+
if (app.debugEnabled) {
|
|
125
|
+
microAppConfigs[instanceKey(app.appCode, true)] = {
|
|
126
|
+
name: instanceKey(app.appCode, true),
|
|
127
|
+
entry: DEBUG_ENTRY,
|
|
128
|
+
props: Object.assign({}, baseProps, {
|
|
129
|
+
menus: menus.map((menu) => swapMenuNamespace(menu, true)),
|
|
130
|
+
// 调试态才允许 URL 直达未配菜单的页面(见 permission.js 调试兜底路由)
|
|
131
|
+
microDebug: true,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
installRouteHook();
|
|
137
|
+
return Object.keys(microAppConfigs);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 当前应处于激活态的实例键(等待容器期间用户可能又切走,异步续体据此丢弃过期操作)
|
|
141
|
+
let currentActiveKey = null;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 由路由推导装载实例:正式路由 meta 直接带 microKey;
|
|
145
|
+
* 调试通配路由只带 appCode 参数,在此拼出实例键。
|
|
146
|
+
*/
|
|
147
|
+
function resolveMicroKey(to) {
|
|
148
|
+
const meta = (to && to.meta) || {};
|
|
149
|
+
if (!meta.isMicroApp) return null;
|
|
150
|
+
if (meta.microDebugCatchAll) {
|
|
151
|
+
const appCode = (to.params && to.params.appCode) || "";
|
|
152
|
+
const key = appCode ? instanceKey(appCode, true) : "";
|
|
153
|
+
if (!microAppConfigs[key]) {
|
|
154
|
+
console.error(
|
|
155
|
+
"[microRouter] 无法装载调试实例:"
|
|
156
|
+
+ (appCode || "(空 appCode)")
|
|
157
|
+
+ "。请检查该 appCode 是否在应用注册表 "
|
|
158
|
+
+ REGISTRY_PARAM_CODE
|
|
159
|
+
+ " 里,以及逻辑参数 "
|
|
160
|
+
+ DEBUG_PARAM_CODE
|
|
161
|
+
+ " 是否为该应用放开了调试(默认关闭)"
|
|
162
|
+
);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return key;
|
|
166
|
+
}
|
|
167
|
+
return microAppConfigs[meta.microKey] ? meta.microKey : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function installRouteHook() {
|
|
171
|
+
if (routeHookInstalled) return;
|
|
172
|
+
routeHookInstalled = true;
|
|
173
|
+
router.afterEach((to) => {
|
|
174
|
+
const key = resolveMicroKey(to);
|
|
175
|
+
currentActiveKey = key;
|
|
176
|
+
Object.keys(aliveApps).forEach((code) => {
|
|
177
|
+
if (code !== key) {
|
|
178
|
+
deactivateMicroApp(code);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (key) {
|
|
182
|
+
activateMicroApp(key);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 刷新直接落在子应用路由时,afterEach 早于布局渲染(AppMain 懒加载 + showAppMain 延迟),
|
|
188
|
+
// 容器尚未进 DOM——轮询等待,超时显式报错
|
|
189
|
+
const WRAPPER_WAIT_INTERVAL = 100;
|
|
190
|
+
const WRAPPER_WAIT_MAX_TRIES = 100;
|
|
191
|
+
|
|
192
|
+
function waitForWrapper(tryCount = 0) {
|
|
193
|
+
const wrapper = document.getElementById(CONTAINER_WRAPPER_ID);
|
|
194
|
+
if (wrapper) return Promise.resolve(wrapper);
|
|
195
|
+
if (tryCount >= WRAPPER_WAIT_MAX_TRIES) {
|
|
196
|
+
console.error(
|
|
197
|
+
"[microRouter] 等待子应用容器 #" + CONTAINER_WRAPPER_ID + " 超时"
|
|
198
|
+
);
|
|
199
|
+
return Promise.resolve(null);
|
|
200
|
+
}
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
resolve(waitForWrapper(tryCount + 1));
|
|
204
|
+
}, WRAPPER_WAIT_INTERVAL);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getContainerEl(wrapper, key) {
|
|
209
|
+
const id = CONTAINER_WRAPPER_ID + "-" + key.replace("@", "_");
|
|
210
|
+
let el = document.getElementById(id);
|
|
211
|
+
if (!el) {
|
|
212
|
+
el = document.createElement("div");
|
|
213
|
+
el.id = id;
|
|
214
|
+
el.style.height = "100%";
|
|
215
|
+
wrapper.appendChild(el);
|
|
216
|
+
}
|
|
217
|
+
return el;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function activateMicroApp(key) {
|
|
221
|
+
const config = microAppConfigs[key];
|
|
222
|
+
if (!config) return;
|
|
223
|
+
waitForWrapper().then((wrapper) => {
|
|
224
|
+
if (!wrapper) return;
|
|
225
|
+
if (currentActiveKey !== key) return; // 等待期间已切走
|
|
226
|
+
const el = getContainerEl(wrapper, key);
|
|
227
|
+
el.style.display = "";
|
|
228
|
+
const alive = aliveApps[key];
|
|
229
|
+
if (!alive) {
|
|
230
|
+
if (config.props.microDebug) {
|
|
231
|
+
console.warn(
|
|
232
|
+
"[microRouter] ⚠ 调试命名空间装载:"
|
|
233
|
+
+ config.name
|
|
234
|
+
+ " → "
|
|
235
|
+
+ config.entry
|
|
236
|
+
+ "(把地址里的 /"
|
|
237
|
+
+ MICRO_DEBUG_SEGMENT
|
|
238
|
+
+ "/ 改回 /"
|
|
239
|
+
+ MICRO_SEGMENT
|
|
240
|
+
+ "/ 即回到正式环境)"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
aliveApps[key] = {
|
|
244
|
+
el,
|
|
245
|
+
instance: loadMicroApp({
|
|
246
|
+
name: config.name,
|
|
247
|
+
entry: config.entry,
|
|
248
|
+
container: el,
|
|
249
|
+
props: config.props,
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// 已保活:显示容器并通知子应用恢复路由监听、同步当前 hash(含"切到本应用另一菜单"的深导航)
|
|
255
|
+
notifyMicroApp(alive.instance, { microActive: true });
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function deactivateMicroApp(key) {
|
|
260
|
+
const alive = aliveApps[key];
|
|
261
|
+
if (!alive) return;
|
|
262
|
+
alive.el.style.display = "none";
|
|
263
|
+
// 暂停子应用路由监听,防止隐藏期间对主应用的 URL 变化做出反应(回写 URL 互相打架)
|
|
264
|
+
notifyMicroApp(alive.instance, { microActive: false });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function notifyMicroApp(instance, props) {
|
|
268
|
+
if (!instance || typeof instance.update !== "function") return;
|
|
269
|
+
const mountPromise = instance.mountPromise || Promise.resolve();
|
|
270
|
+
mountPromise
|
|
271
|
+
.then(() => instance.update(props))
|
|
272
|
+
.catch((e) => {
|
|
273
|
+
console.error("[microRouter] 子应用 update 通知失败:", e);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 转发"刷新当前页"给子应用。
|
|
279
|
+
* 保活模式下主应用那套 delCachedView + /redirect 重建只作用于自己的 router-view,
|
|
280
|
+
* 子应用内容由 qiankun 挂在独立容器里,走一圈只是容器隐藏再显示、实例毫发无损,
|
|
281
|
+
* 表现为点刷新没反应——必须由子应用在自身路由内重建组件。
|
|
282
|
+
* 入参是当前路由(调试通配路由的实例键要从路由参数推导,不在 meta 里)。
|
|
283
|
+
* 返回是否已转发(false 表示该应用未装载,调用方回落自身刷新逻辑)。
|
|
284
|
+
*/
|
|
285
|
+
export function refreshMicroApp(route) {
|
|
286
|
+
const alive = aliveApps[resolveMicroKey(route)];
|
|
287
|
+
if (!alive) return false;
|
|
288
|
+
// 带时间戳:qiankun 对相同 props 也会调用 update,但时间戳能让子应用区分每次点击
|
|
289
|
+
notifyMicroApp(alive.instance, { microRefresh: Date.now() });
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 真正卸载全部子应用(登出等场景按需调用;常规页签切换不卸载) */
|
|
294
|
+
export function unmountAllMicroApps() {
|
|
295
|
+
Object.keys(aliveApps).forEach((code) => {
|
|
296
|
+
const alive = aliveApps[code];
|
|
297
|
+
if (alive && alive.instance) {
|
|
298
|
+
alive.instance.unmount().catch(() => {});
|
|
299
|
+
}
|
|
300
|
+
if (alive && alive.el && alive.el.parentNode) {
|
|
301
|
+
alive.el.parentNode.removeChild(alive.el);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
aliveApps = {};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 每个子应用先按菜单生成精确路由以携带菜单标题等元数据,再追加通配兜底。
|
|
309
|
+
* 路由均不挂组件,内容由 qiankun 挂进 AppMain 的 #Appmicro 容器。
|
|
310
|
+
*
|
|
311
|
+
* 调试命名空间不按应用展开:精确路由只为菜单元数据(标题/树路径/重复打开)而生,
|
|
312
|
+
* 装载哪个实例只取决于 appCode——做成一条参数化通配即可,
|
|
313
|
+
* 子应用再多也还是一条,代价仅是调试页签标题不显示具体菜单名。
|
|
314
|
+
*/
|
|
315
|
+
export function buildSubAppRoutes(apps, subAppMenus = {}) {
|
|
316
|
+
const routes = [];
|
|
317
|
+
(apps || []).forEach((app) => {
|
|
318
|
+
const key = instanceKey(app.appCode, false);
|
|
319
|
+
const seenPaths = {};
|
|
320
|
+
(subAppMenus[app.appCode] || []).forEach((menu, index) => {
|
|
321
|
+
let path = getAccessPath(menu).split("?")[0];
|
|
322
|
+
if (!path) return;
|
|
323
|
+
if (!path.startsWith("/")) {
|
|
324
|
+
path = "/" + path;
|
|
325
|
+
}
|
|
326
|
+
if (seenPaths[path]) return;
|
|
327
|
+
seenPaths[path] = true;
|
|
328
|
+
routes.push({
|
|
329
|
+
path,
|
|
330
|
+
name: `micro-${app.appCode}-menu-${menu.id || menu.menuCode || index}`,
|
|
331
|
+
meta: {
|
|
332
|
+
title: menu.menuName || app.appName,
|
|
333
|
+
enTitle: menu.menuEnName || null,
|
|
334
|
+
treePathName: menu.treePathName,
|
|
335
|
+
repeatOpen: menu.repeatOpen,
|
|
336
|
+
isMicroApp: true,
|
|
337
|
+
appCode: app.appCode,
|
|
338
|
+
microKey: key,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
routes.push({
|
|
343
|
+
path: "/" + MICRO_SEGMENT + "/" + app.appCode + "/*",
|
|
344
|
+
name: "micro-" + app.appCode,
|
|
345
|
+
meta: {
|
|
346
|
+
title: app.appName,
|
|
347
|
+
isMicroApp: true,
|
|
348
|
+
appCode: app.appCode,
|
|
349
|
+
microKey: key,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
// 没有任何应用开启调试时连路由都不注册——/micro_debug/ 直接落 404,
|
|
354
|
+
// 而不是进到一个装载不了任何东西的空白页
|
|
355
|
+
if ((apps || []).some((app) => app.debugEnabled)) {
|
|
356
|
+
// 整个调试命名空间共用这一条;appCode 由路由参数带出,装载实例在 afterEach 里推导
|
|
357
|
+
routes.push({
|
|
358
|
+
path: "/" + MICRO_DEBUG_SEGMENT + "/:appCode/*",
|
|
359
|
+
name: "micro-" + MICRO_DEBUG_SEGMENT,
|
|
360
|
+
meta: {
|
|
361
|
+
title: "微前端调试",
|
|
362
|
+
isMicroApp: true,
|
|
363
|
+
microDebugCatchAll: true,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return routes;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 子应用表单脚本跳主应用路由必须走该回调,不得直接摸主应用 router
|
|
371
|
+
function createDefaultMainAdapter() {
|
|
372
|
+
return {
|
|
373
|
+
navigate(path) {
|
|
374
|
+
const vueRoot = window.$vueRoot;
|
|
375
|
+
if (vueRoot && vueRoot.$router) {
|
|
376
|
+
vueRoot.$router.push(path).catch(() => {});
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
openTab(path) {
|
|
380
|
+
this.navigate(path);
|
|
381
|
+
},
|
|
382
|
+
closeTab(view) {
|
|
383
|
+
const vueRoot = window.$vueRoot;
|
|
384
|
+
if (vueRoot && vueRoot.$store) {
|
|
385
|
+
return vueRoot.$store.dispatch(
|
|
386
|
+
"tagsView/delView",
|
|
387
|
+
view || vueRoot.$route
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return Promise.resolve();
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
const state = {
|
|
7
7
|
isMicro: false,
|
|
8
8
|
appCode: "",
|
|
9
|
-
|
|
9
|
+
// 主应用回调(openTab / closeTab / navigate):包内不消费,供子应用业务代码
|
|
10
|
+
// (表单脚本跳主应用路由)读取,不得直接摸主应用 router
|
|
10
11
|
mainAdapter: null,
|
|
11
|
-
lang: "",
|
|
12
12
|
// 自治嵌入模式:主应用未下发菜单(props.menus 非数组)时置 true,
|
|
13
13
|
// 子应用自拉菜单、保留自身侧边栏/页签壳,整站运行在容器内(含登录页)
|
|
14
14
|
selfManaged: false,
|
|
@@ -26,9 +26,7 @@ const mutations = {
|
|
|
26
26
|
state.isMicro = true;
|
|
27
27
|
state.appCode = payload.appCode || state.appCode;
|
|
28
28
|
state.selfManaged = !Array.isArray(payload.menus);
|
|
29
|
-
state.menus = payload.menus || [];
|
|
30
29
|
state.mainAdapter = payload.mainAdapter || null;
|
|
31
|
-
state.lang = payload.lang || "";
|
|
32
30
|
state.debug = !!payload.debug;
|
|
33
31
|
},
|
|
34
32
|
BUMP_RELOAD_SEQ: (state) => {
|
|
@@ -40,9 +38,7 @@ const mutations = {
|
|
|
40
38
|
CLEAR_MICRO_CONTEXT: (state) => {
|
|
41
39
|
state.isMicro = false;
|
|
42
40
|
state.appCode = "";
|
|
43
|
-
state.menus = [];
|
|
44
41
|
state.mainAdapter = null;
|
|
45
|
-
state.lang = "";
|
|
46
42
|
state.selfManaged = false;
|
|
47
43
|
state.routerPaused = false;
|
|
48
44
|
state.debug = false;
|
|
@@ -247,7 +247,7 @@ const actions = {
|
|
|
247
247
|
"SET_REGISTRY_APP_CODES",
|
|
248
248
|
hostRegistryApps.map((app) => app.appCode)
|
|
249
249
|
);
|
|
250
|
-
let
|
|
250
|
+
let subAppMenus = dispatchMenus(rows, hostRegistryApps);
|
|
251
251
|
hostMicroRoutes = buildSubAppRoutes(hostRegistryApps, subAppMenus);
|
|
252
252
|
if (hostRegistryApps.length) {
|
|
253
253
|
registerSubApps(hostRegistryApps, {
|
|
@@ -285,7 +285,7 @@ function getUrlParams(url) {
|
|
|
285
285
|
return result;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
// 宿主态下归属子应用的菜单不生成本地组件路由(否则会遮住 /appCode/* 通配路由),
|
|
288
|
+
// 宿主态下归属子应用的菜单不生成本地组件路由(否则会遮住 /micro/appCode/* 通配路由),
|
|
289
289
|
// 由子应用经 qiankun 渲染;侧边栏 handleSidebarRoutes 仍保留全量
|
|
290
290
|
function isDelegatedToSubApp(route) {
|
|
291
291
|
return hostRegistryApps.length > 0 && !!resolveOwner(route, hostRegistryApps);
|
package/src/utils/menuAdapter.js
CHANGED
|
@@ -11,6 +11,23 @@ export const FORM_MENU_TYPE = 5;
|
|
|
11
11
|
// url 命名空间保留首段:/base/xxx 表示组件在 npm 包内(@base/views/xxx)
|
|
12
12
|
export const BASE_SEGMENT = "base";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 微前端固定前缀:访问路径形如 /micro/<appCode>/<来源>/<组件路径>。
|
|
16
|
+
* 用固定前缀而非"首段即 appCode",是为了让归属命名空间与主应用自身的
|
|
17
|
+
* 一级视图目录(/user、/pmd、/yx…)结构性隔离——否则注册一个与视图目录
|
|
18
|
+
* 同名的 appCode 会劫持主应用整棵子树,且新增视图目录还会反向撞掉已注册的应用。
|
|
19
|
+
*/
|
|
20
|
+
export const MICRO_SEGMENT = "micro";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 本机调试命名空间:/micro_debug/<appCode>/... 与 /micro/<appCode>/... 归属相同、
|
|
24
|
+
* 菜单相同、组件解析相同,**只有 entry 不同**(正式取注册表,调试取约定端口)。
|
|
25
|
+
* 把"调试还是正式"编码进路径而非存储,是为了让调试态无状态——不会残留在
|
|
26
|
+
* localStorage 里跨天生效,刷新/分享/多标签页的行为都由 URL 自身决定。
|
|
27
|
+
*/
|
|
28
|
+
export const MICRO_DEBUG_SEGMENT = "micro_debug";
|
|
29
|
+
|
|
30
|
+
const MICRO_SEGMENTS = [MICRO_SEGMENT, MICRO_DEBUG_SEGMENT];
|
|
14
31
|
const BASE_VIEW_PREFIX = "@base/views";
|
|
15
32
|
const APP_CODE_REG = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
16
33
|
|
|
@@ -29,19 +46,62 @@ export function getAccessPath(menu) {
|
|
|
29
46
|
/**
|
|
30
47
|
* 取路径首段(不含查询串);@ 开头的存量组件路径无首段语义,返回 ""
|
|
31
48
|
*/
|
|
32
|
-
|
|
49
|
+
function getFirstSegment(path) {
|
|
50
|
+
return getSegments(path)[0] || "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 切分路径为段数组(不含查询串);@ 开头的存量组件路径无首段语义,返回 [] */
|
|
54
|
+
function getSegments(path) {
|
|
33
55
|
if (!path || typeof path !== "string" || path.startsWith("@")) {
|
|
34
|
-
return
|
|
56
|
+
return [];
|
|
35
57
|
}
|
|
36
|
-
return path.split("?")[0].split("/").filter(Boolean)
|
|
58
|
+
return path.split("?")[0].split("/").filter(Boolean);
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
62
|
+
* 取访问路径里的归属 appCode:仅 /micro/<appCode>/... 或
|
|
63
|
+
* /micro_debug/<appCode>/... 形态才有归属,其余(主应用自身路由、
|
|
64
|
+
* /base 命名空间、裸路径)一律返回 ""。
|
|
65
|
+
*/
|
|
66
|
+
export function getOwnerSegment(path) {
|
|
67
|
+
const segs = getSegments(path);
|
|
68
|
+
if (segs.length < 2 || MICRO_SEGMENTS.indexOf(segs[0]) < 0) return "";
|
|
69
|
+
return segs[1];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 切换访问路径的微前端命名空间(/micro/ ⇄ /micro_debug/),非微前端路径原样返回。
|
|
74
|
+
* 用途:主应用在调试命名空间下装载子应用时,把下发的菜单路径一并换过去——
|
|
75
|
+
* 子应用据此生成的路由才与浏览器 hash 对得上(主子路径始终保持一致)。
|
|
76
|
+
*/
|
|
77
|
+
export function swapMicroNamespace(path, debug) {
|
|
78
|
+
const segs = getSegments(path);
|
|
79
|
+
if (segs.length < 2 || MICRO_SEGMENTS.indexOf(segs[0]) < 0) return path;
|
|
80
|
+
const target = debug ? MICRO_DEBUG_SEGMENT : MICRO_SEGMENT;
|
|
81
|
+
if (segs[0] === target) return path;
|
|
82
|
+
return "/" + target + stripFirstSegment(path);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 按调试与否重写整条菜单的访问路径命名空间(浅拷贝,不改原对象)。
|
|
87
|
+
* route 有值改 route,否则改 url——与 getAccessPath 的取值口径一致;
|
|
88
|
+
* 单字段写法里 url 同时是组件路径,但 resolveComponentPath 对两个命名空间
|
|
89
|
+
* 都会剥前缀,所以换过去不影响组件解析。
|
|
90
|
+
*/
|
|
91
|
+
export function swapMenuNamespace(menu, debug) {
|
|
92
|
+
if (!menu) return menu;
|
|
93
|
+
const field = menu.route ? "route" : "url";
|
|
94
|
+
const swapped = swapMicroNamespace(String(menu[field] || ""), debug);
|
|
95
|
+
if (swapped === menu[field]) return menu;
|
|
96
|
+
return Object.assign({}, menu, { [field]: swapped });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 解析菜单归属:访问路径的 /micro/<appCode>(或 /micro_debug/<appCode>)
|
|
101
|
+
* 命中注册表则归属该子应用,否则(无前缀 / base / 未命中)归主应用,返回 ""。
|
|
42
102
|
*/
|
|
43
103
|
export function resolveOwner(menu, registry) {
|
|
44
|
-
const seg =
|
|
104
|
+
const seg = getOwnerSegment(getAccessPath(menu));
|
|
45
105
|
if (!seg || seg === BASE_SEGMENT) return "";
|
|
46
106
|
const apps = normalizeRegistry(registry);
|
|
47
107
|
return apps.some((app) => app.appCode === seg) ? seg : "";
|
|
@@ -50,9 +110,10 @@ export function resolveOwner(menu, registry) {
|
|
|
50
110
|
/**
|
|
51
111
|
* 解析组件路径(type 0 普通菜单):
|
|
52
112
|
* - "@..." 存量旧写法:url 即组件路径,原样返回;
|
|
113
|
+
* - /micro/<渲染方 appCode>/… 或 /micro_debug/<同>:剥掉归属两段
|
|
114
|
+
* (渲染方只渲染归属自己的菜单,两个命名空间同等对待);
|
|
53
115
|
* - 首段 base:转 @base/views 命名空间;
|
|
54
|
-
* -
|
|
55
|
-
* - 其余原样(无前缀走渲染方 @/views;别家前缀加载失败落兜底组件)。
|
|
116
|
+
* - 其余原样(无前缀走渲染方 @/views;别家归属加载失败落兜底组件)。
|
|
56
117
|
*/
|
|
57
118
|
export function resolveComponentPath(menu, rendererAppCode) {
|
|
58
119
|
const url = menu && menu.url != null ? String(menu.url) : "";
|
|
@@ -60,17 +121,19 @@ export function resolveComponentPath(menu, rendererAppCode) {
|
|
|
60
121
|
if (url.startsWith("@")) return url;
|
|
61
122
|
const seg = getFirstSegment(url);
|
|
62
123
|
if (!seg) return url;
|
|
124
|
+
if (MICRO_SEGMENTS.indexOf(seg) >= 0) {
|
|
125
|
+
const owner = getOwnerSegment(url);
|
|
126
|
+
// 归属别家、或主应用误渲染子应用菜单:原样返回,加载失败后由 MenuFallback 说明归属
|
|
127
|
+
if (!owner || !rendererAppCode || owner !== rendererAppCode) return url;
|
|
128
|
+
// 单字段写法 /micro/appA/base/xx:剥掉归属两段后可能仍带 base 命名空间,递归解析一次
|
|
129
|
+
// (appCode 传空,不会把 /micro/appA/micro/appA/xx 连剥两轮)
|
|
130
|
+
const rest = stripFirstSegment(stripFirstSegment(url));
|
|
131
|
+
return rest ? resolveComponentPath({ url: rest }, "") : "";
|
|
132
|
+
}
|
|
63
133
|
if (seg === BASE_SEGMENT) {
|
|
64
134
|
const rest = stripFirstSegment(url);
|
|
65
135
|
return rest ? BASE_VIEW_PREFIX + rest : "";
|
|
66
136
|
}
|
|
67
|
-
if (rendererAppCode && seg === rendererAppCode) {
|
|
68
|
-
const rest = stripFirstSegment(url);
|
|
69
|
-
if (!rest) return "";
|
|
70
|
-
// 单字段写法 /appA/base/xx:剥掉归属前缀后可能仍带 base 命名空间,递归解析一次
|
|
71
|
-
// (appCode 传空,只剥一层归属前缀,不会把 /appA/appA/xx 连剥两层)
|
|
72
|
-
return resolveComponentPath({ url: rest }, "");
|
|
73
|
-
}
|
|
74
137
|
return url;
|
|
75
138
|
}
|
|
76
139
|
|
|
@@ -81,8 +144,9 @@ function stripFirstSegment(url) {
|
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
147
|
+
* 全量菜单按归属切分,返回 { appCode: [菜单行] }。
|
|
148
|
+
* 主应用侧不需要切分结果——它始终用原始整树渲染侧边栏,直接用入参即可;
|
|
149
|
+
* 这里只产出各子应用归属自己的扁平列表(用于 props 下发生成子应用内部路由)。
|
|
86
150
|
*/
|
|
87
151
|
export function dispatchMenus(menus, registry) {
|
|
88
152
|
const apps = normalizeRegistry(registry);
|
|
@@ -102,13 +166,15 @@ export function dispatchMenus(menus, registry) {
|
|
|
102
166
|
});
|
|
103
167
|
};
|
|
104
168
|
walk(menus || []);
|
|
105
|
-
return
|
|
169
|
+
return subAppMenus;
|
|
106
170
|
}
|
|
107
171
|
|
|
108
172
|
/**
|
|
109
173
|
* 校验应用注册表(逻辑参数 micro_app_registry 的 paramValue)。
|
|
110
174
|
* 非法条目跳过并记入 errors,绝不静默;enabled === false 的合法条目不收录也不报错。
|
|
111
|
-
*
|
|
175
|
+
* appCode 处在 /micro/ 命名空间内,与主应用一级视图目录天然隔离,无需再防目录撞名;
|
|
176
|
+
* 仅保留 base(组件来源命名空间)与 micro / micro_debug(前缀自身)三个保留字。
|
|
177
|
+
* options.reservedSegments 可追加应用自定义保留字。
|
|
112
178
|
*/
|
|
113
179
|
export function validateRegistry(raw, options = {}) {
|
|
114
180
|
const errors = [];
|
|
@@ -123,7 +189,9 @@ export function validateRegistry(raw, options = {}) {
|
|
|
123
189
|
if (!Array.isArray(list)) {
|
|
124
190
|
return { apps: [], errors: ["注册表必须是 JSON 数组"] };
|
|
125
191
|
}
|
|
126
|
-
const reserved = [BASE_SEGMENT]
|
|
192
|
+
const reserved = [BASE_SEGMENT]
|
|
193
|
+
.concat(MICRO_SEGMENTS)
|
|
194
|
+
.concat(options.reservedSegments || []);
|
|
127
195
|
const apps = [];
|
|
128
196
|
const seen = {};
|
|
129
197
|
list.forEach((item, index) => {
|
|
@@ -145,8 +213,16 @@ export function validateRegistry(raw, options = {}) {
|
|
|
145
213
|
errors.push(tag + " appCode 重复: " + appCode);
|
|
146
214
|
return;
|
|
147
215
|
}
|
|
148
|
-
if (
|
|
149
|
-
errors.push(
|
|
216
|
+
if (typeof item.entry !== "string" || !item.entry.trim()) {
|
|
217
|
+
errors.push(
|
|
218
|
+
tag
|
|
219
|
+
+ "("
|
|
220
|
+
+ appCode
|
|
221
|
+
+ ")entry 必须是非空字符串(分环境对象写法已废弃,"
|
|
222
|
+
+ "注册表本身按环境分库;本机联调用 /"
|
|
223
|
+
+ MICRO_DEBUG_SEGMENT
|
|
224
|
+
+ "/ 命名空间)"
|
|
225
|
+
);
|
|
150
226
|
return;
|
|
151
227
|
}
|
|
152
228
|
seen[appCode] = true;
|
|
@@ -154,8 +230,7 @@ export function validateRegistry(raw, options = {}) {
|
|
|
154
230
|
apps.push({
|
|
155
231
|
appCode: appCode,
|
|
156
232
|
appName: item.appName || appCode,
|
|
157
|
-
entry: item.entry,
|
|
158
|
-
hasCorejs: !!item.hasCorejs,
|
|
233
|
+
entry: item.entry.trim(),
|
|
159
234
|
enabled: true,
|
|
160
235
|
});
|
|
161
236
|
});
|
|
@@ -163,12 +238,48 @@ export function validateRegistry(raw, options = {}) {
|
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
/**
|
|
166
|
-
*
|
|
241
|
+
* 解析调试开关(逻辑参数 micro_debug_enabled 的值)→ { all, codes }。
|
|
242
|
+
* 与注册表分开承载:注册表描述"有哪些应用",本开关是环境级运维动作,
|
|
243
|
+
* 生命周期不同——运维开关调试不必改注册表 JSON,少一类把注册表改坏的风险。
|
|
244
|
+
*
|
|
245
|
+
* 空 / "0" / "false" / "off" → 关闭(默认,也是取值失败时的回落)
|
|
246
|
+
* "1" / "true" / "all" → 全部注册应用
|
|
247
|
+
* "appA,appB" → 仅列出的应用
|
|
248
|
+
*/
|
|
249
|
+
export function parseDebugEnabled(raw) {
|
|
250
|
+
const value = (raw == null ? "" : String(raw)).trim();
|
|
251
|
+
if (!value || DEBUG_OFF_VALUES.indexOf(value.toLowerCase()) >= 0) {
|
|
252
|
+
return { all: false, codes: [] };
|
|
253
|
+
}
|
|
254
|
+
if (DEBUG_ALL_VALUES.indexOf(value.toLowerCase()) >= 0) {
|
|
255
|
+
return { all: true, codes: [] };
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
all: false,
|
|
259
|
+
codes: value
|
|
260
|
+
.split(",")
|
|
261
|
+
.map((code) => code.trim())
|
|
262
|
+
.filter(Boolean),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DEBUG_OFF_VALUES = ["0", "false", "off", "no"];
|
|
267
|
+
const DEBUG_ALL_VALUES = ["1", "true", "all", "on", "yes"];
|
|
268
|
+
|
|
269
|
+
/** 某应用是否允许经 /micro_debug/ 装载本机 devServer */
|
|
270
|
+
export function isDebugAllowed(setting, appCode) {
|
|
271
|
+
if (!setting || !appCode) return false;
|
|
272
|
+
if (setting.all) return true;
|
|
273
|
+
return (setting.codes || []).indexOf(appCode) >= 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 取 entry。不再支持分环境对象写法:注册表存在后端逻辑参数里,本身已按环境分库,
|
|
278
|
+
* 主应用连哪个后端就拿到哪份注册表——再按前端 NODE_ENV 分一次是重复机制。
|
|
279
|
+
* 本机联调走 /micro_debug/ 命名空间(显式、按 URL 生效),不依赖构建环境。
|
|
167
280
|
*/
|
|
168
|
-
export function resolveEntry(entry
|
|
169
|
-
|
|
170
|
-
if (typeof entry === "string") return entry;
|
|
171
|
-
return entry[env] || entry.production || "";
|
|
281
|
+
export function resolveEntry(entry) {
|
|
282
|
+
return typeof entry === "string" ? entry.trim() : "";
|
|
172
283
|
}
|
|
173
284
|
|
|
174
285
|
function normalizeRegistry(registry) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
16
|
import settings from "@/settings";
|
|
17
|
-
import {
|
|
17
|
+
import { getOwnerSegment, BASE_SEGMENT } from "@base/utils/menuAdapter";
|
|
18
18
|
|
|
19
19
|
export default {
|
|
20
20
|
name: "menuFallback",
|
|
@@ -28,14 +28,14 @@ export default {
|
|
|
28
28
|
return meta.title || (this.$route && this.$route.path) || "";
|
|
29
29
|
},
|
|
30
30
|
ownerSegment() {
|
|
31
|
-
return
|
|
31
|
+
return getOwnerSegment((this.$route && this.$route.path) || "");
|
|
32
32
|
},
|
|
33
33
|
registryAppCodes() {
|
|
34
34
|
let permission = (this.$store && this.$store.state.permission) || {};
|
|
35
35
|
return permission.registryAppCodes || [];
|
|
36
36
|
},
|
|
37
37
|
foreignApp() {
|
|
38
|
-
//
|
|
38
|
+
// 只有 /micro/<appCode> 命中注册表里的别家 appCode 才算"归属别家应用";
|
|
39
39
|
// 未命中(含注册表未配置)一律按组件加载失败处理,不误导去主平台
|
|
40
40
|
let seg = this.ownerSegment;
|
|
41
41
|
let own = settings.appCode || "";
|