cloud-web-corejs 1.0.54-dev.762 → 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.
@@ -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
- } from "@base/utils/menuAdapter";
14
- import { getToken } from "@base/utils/auth";
15
- import router from "@base/router";
16
- import settings from "@/settings";
17
-
18
- export const REGISTRY_PARAM_CODE = "micro_app_registry";
19
- export const DEBUG_STORAGE_KEY = "micro_app_debug";
20
-
21
- const REGISTRY_TIMEOUT = 8000;
22
- const CONTAINER_WRAPPER_ID = "Appmicro";
23
- const DEBUG_APP_CODE_REG = /^[A-Za-z][A-Za-z0-9_-]*$/;
24
-
25
- // 本机调试的约定默认值:包内兜底,各应用 settings.js 无需声明。
26
- // 需要偏离约定(改端口 / 调真实 appCode 的菜单派发)才在应用 settings.js 里覆盖,
27
- // 或用 ?microDebug=appB=http://... 单次指定。
28
- const DEBUG_DEFAULT_APP_CODE = "debugApp";
29
- const DEBUG_DEFAULT_ENTRY = "http://localhost:17527/";
30
-
31
- // 保活管理:appCode { instance, el };离开子应用路由只隐藏容器不卸载,
32
- // 切回直接显示并经 update 生命周期同步路由,页面状态(页签/表格/表单)得以保留
33
- let microAppConfigs = {};
34
- let aliveApps = {};
35
- let routeHookInstalled = false;
36
-
37
- /**
38
- * 读应用注册表(第一阶段载体:逻辑参数)。
39
- * 非法条目由 validateRegistry 跳过并在控制台显式报错;
40
- * 参数缺失 / 请求失败 / 超时一律回落空数组,不阻塞登录流程。
41
- */
42
- export function getAppRegistry() {
43
- const fetchPromise = new Promise((resolve) => {
44
- getLogicParamValue({
45
- data: { paramCode: REGISTRY_PARAM_CODE },
46
- failMsg: false,
47
- errorMsg: false,
48
- modal: false,
49
- success: (res) => {
50
- const raw = res && res.objx;
51
- if (!raw) {
52
- resolve([]);
53
- return;
54
- }
55
- const { apps, errors } = validateRegistry(raw);
56
- if (errors.length) {
57
- console.error(
58
- "[microRouter] 应用注册表存在非法条目(已跳过):",
59
- errors
60
- );
61
- }
62
- resolve(apps);
63
- },
64
- }).catch(() => resolve([]));
65
- });
66
- const timeoutPromise = new Promise((resolve) => {
67
- setTimeout(() => {
68
- resolve([]);
69
- }, REGISTRY_TIMEOUT);
70
- });
71
- return Promise.race([fetchPromise, timeoutPromise]).then((apps) =>
72
- applyDebugOverrides(apps)
73
- );
74
- }
75
-
76
- const DEBUG_URL_PARAMS = ["microDebug", "microDebugAppCode", "microDebugEntry"];
77
-
78
- /** URL 上某个查询参数(hash 前后都找),非法转义按原文处理 */
79
- function readUrlParam(href, name) {
80
- const matched = href.match(new RegExp("[?&]" + name + "=([^&#]*)"));
81
- if (!matched) return null;
82
- const raw = matched[1] || "";
83
- let value = raw;
84
- try {
85
- value = decodeURIComponent(raw);
86
- } catch (e) {
87
- // 非法转义(如值里有裸 %)会抛 URIError,按原文处理,不能让它中断注册表加载
88
- value = raw;
89
- }
90
- return value.trim();
91
- }
92
-
93
- /**
94
- * 从 URL 读调试开关并落盘 localStorage,读到后即从地址栏抹掉(避免被收藏/分享扩散)。
95
- * 支持 hash 前后两种位置:?microDebug=xx#/... #/...?microDebug=xx。
96
- * 两种写法:
97
- * ?microDebug=xx 取值同 localStorage 四档写法;off/0 清除
98
- * ?microDebugAppCode=mkweb&microDebugEntry=17527 拆成两个参数,值里不含 "="
99
- * 后者存在的原因:部分网关/WAF 对查询串里出现第二个裸 "=" 直接返 500(整页打不开),
100
- * 拆成两个参数后每个值都没有 "=",无需编码即可通过。entry 可写端口号简写。
101
- * 存在意义:settings.microDebugEnabled 是构建期常量、全体用户共用,生产构建里必须失效;
102
- * 生产环境主应用要装本机子应用,只能靠这种"按浏览器生效"的通道。
103
- */
104
- function readDebugFromUrl() {
105
- const href = window.location.href || "";
106
- const single = readUrlParam(href, "microDebug");
107
- const appCode = readUrlParam(href, "microDebugAppCode");
108
- const entry = readUrlParam(href, "microDebugEntry");
109
- let value = null;
110
- if (appCode) {
111
- // 双参数写法:entry 缺省时落到默认入口(由 getDebugOverrides 补)
112
- value = entry ? appCode + "=" + entry : appCode;
113
- } else if (single !== null) {
114
- value = single;
115
- }
116
- if (value === null) return;
117
- try {
118
- if (!value || value === "off" || value === "0") {
119
- window.localStorage.removeItem(DEBUG_STORAGE_KEY);
120
- } else {
121
- window.localStorage.setItem(DEBUG_STORAGE_KEY, value);
122
- }
123
- } catch (e) {
124
- console.error("[microRouter] 调试开关写入 localStorage 失败:", e.message);
125
- return;
126
- }
127
- try {
128
- let cleaned = href;
129
- DEBUG_URL_PARAMS.forEach((name) => {
130
- cleaned = cleaned.replace(
131
- new RegExp("([?&])" + name + "=[^&#]*&?"),
132
- "$1"
133
- );
134
- });
135
- cleaned = cleaned.replace(/[?&](?=#|$)/, ""); // 抹掉可能残留的空查询串
136
- window.history.replaceState(null, "", cleaned);
137
- } catch (e) {
138
- // replaceState 失败不影响开关本身,忽略
139
- }
140
- }
141
-
142
- /**
143
- * entry 规范化:允许只写端口号(如 "17527")代替完整地址。
144
- * 完整地址里的 ":" "//" 放在 URL 查询串上容易被地址栏或服务端重定向吃掉,
145
- * 端口简写让 ?microDebug=mkweb=17527 这种最省事的写法可用。
146
- */
147
- function normalizeDebugEntry(entry) {
148
- const value = (entry == null ? "" : String(entry)).trim();
149
- if (/^\d{2,5}$/.test(value)) {
150
- return "http://localhost:" + value + "/";
151
- }
152
- return value;
153
- }
154
-
155
- /**
156
- * 本机调试覆盖,三种触发方式(localStorage 优先于 settings 开关):
157
- * 1) URL 参数 ?microDebug=xx —— 任何环境(含生产构建)都生效,值写入 localStorage
158
- * 后自动从地址栏抹掉;?microDebug=off 清除。生产环境主应用只能用这种。
159
- * 2) localStorage.micro_app_debug —— 仅当前浏览器生效,不影响其他用户。四档写法:
160
- * "1" → settings.microDebugAppCode + microDebugEntry
161
- * "appB" → appB + 默认调试入口
162
- * "appB=http://localhost:17528/" → 指定入口;多个用逗号分隔,可混写
163
- * '[{"appCode":"..","entry":".."}]' → 完整条目数组
164
- * 3) settings.microDebugEnabled = true —— 本地起主应用时免设 localStorage;
165
- * 仅非 production 构建生效,防误留 true 发版后所有用户被指向 localhost。
166
- * 与注册表按 appCode 合并:已存在的条目覆盖 entry 并强制 enabled,不存在的追加。
167
- */
168
- function getDebugOverrides() {
169
- readDebugFromUrl();
170
- let raw = null;
171
- try {
172
- raw = window.localStorage.getItem(DEBUG_STORAGE_KEY);
173
- } catch (e) {
174
- raw = null;
175
- }
176
- raw = raw ? raw.trim() : "";
177
- // off / 0 一律视为关闭:与 URL 形态语义一致,
178
- // 否则会被当成一个名叫 off 的 appCode 注册出去
179
- if (raw === "off" || raw === "0" || raw === "false") {
180
- raw = "";
181
- }
182
- const defaults = {
183
- appCode: settings.microDebugAppCode || DEBUG_DEFAULT_APP_CODE,
184
- entry: settings.microDebugEntry || DEBUG_DEFAULT_ENTRY,
185
- };
186
- let restoreHint
187
- = "删除 localStorage." + DEBUG_STORAGE_KEY + " 后刷新恢复";
188
- let items = [];
189
- if (!raw) {
190
- if (
191
- settings.microDebugEnabled
192
- && process.env.NODE_ENV !== "production"
193
- ) {
194
- items = [defaults];
195
- restoreHint = "settings.microDebugEnabled false 恢复";
196
- } else {
197
- return { items: [], restoreHint };
198
- }
199
- } else if (raw.charAt(0) === "[") {
200
- try {
201
- const list = JSON.parse(raw);
202
- items = (Array.isArray(list) ? list : []).map((item) => ({
203
- appCode: (item && item.appCode) || "",
204
- entry: (item && item.entry) || defaults.entry,
205
- }));
206
- } catch (e) {
207
- console.error(
208
- "[microRouter] micro_app_debug JSON 解析失败,已忽略:",
209
- e.message
210
- );
211
- return { items: [], restoreHint };
212
- }
213
- } else if (raw === "1" || raw === "true") {
214
- items = [defaults];
215
- } else {
216
- items = raw.split(",").map((part) => {
217
- const idx = part.indexOf("=");
218
- const code = (idx > -1 ? part.slice(0, idx) : part).trim();
219
- const entry = idx > -1 ? part.slice(idx + 1).trim() : defaults.entry;
220
- return { appCode: code, entry };
221
- });
222
- }
223
- items = items.map((item) => ({
224
- appCode: item.appCode,
225
- entry: normalizeDebugEntry(item.entry),
226
- }));
227
- items = items.filter((item) => {
228
- if (!DEBUG_APP_CODE_REG.test(item.appCode || "") || !item.entry) {
229
- console.error(
230
- "[microRouter] micro_app_debug 非法条目已跳过:",
231
- JSON.stringify(item)
232
- );
233
- return false;
234
- }
235
- return true;
236
- });
237
- return { items, restoreHint };
238
- }
239
-
240
- function applyDebugOverrides(apps) {
241
- const { items: overrides, restoreHint } = getDebugOverrides();
242
- if (!overrides.length) return apps || [];
243
- const result = (apps || []).slice();
244
- overrides.forEach((override) => {
245
- const existing = result.find((app) => app.appCode === override.appCode);
246
- if (existing) {
247
- existing.entry = override.entry;
248
- existing.enabled = true;
249
- // 打标记:子应用据此启用"URL 直达未配菜单页面"的调试兜底路由
250
- existing.debugOverride = true;
251
- } else {
252
- result.push({
253
- appCode: override.appCode,
254
- appName: override.appCode + "(本地调试)",
255
- entry: override.entry,
256
- hasCorejs: true,
257
- enabled: true,
258
- debugOverride: true,
259
- });
260
- }
261
- console.warn(
262
- "[microRouter] 本地调试覆盖生效:"
263
- + override.appCode
264
- + " "
265
- + override.entry
266
- + "("
267
- + restoreHint
268
- + ""
269
- );
270
- });
271
- return result;
272
- }
273
-
274
- /**
275
- * activeRule 由 appCode 推导:hash 首段即归属首段,与菜单访问路径天然一致。
276
- */
277
- export function buildActiveRule(appCode) {
278
- const prefix = "#/" + appCode;
279
- return (location) => {
280
- const hash = location.hash || "";
281
- return (
282
- hash === prefix
283
- || hash.indexOf(prefix + "/") === 0
284
- || hash.indexOf(prefix + "?") === 0
285
- );
286
- };
287
- }
288
-
289
- /**
290
- * 登记子应用(保活模式):不再走 registerMicroApps/start 的 activeRule 自动装卸,
291
- * 改为路由钩子驱动的 loadMicroApp 手动装载——首次进入装载,离开只隐藏,切回显示并同步路由。
292
- * options.subAppMenus 为 menuAdapter.dispatchMenus 的切分结果。
293
- */
294
- export function registerSubApps(apps, options = {}) {
295
- const env = process.env.NODE_ENV || "development";
296
- const subAppMenus = options.subAppMenus || {};
297
- const mainAdapter = options.mainAdapter || createDefaultMainAdapter();
298
- const userInfo = options.userInfo || null;
299
- microAppConfigs = {};
300
- (apps || []).forEach((app) => {
301
- const entry = resolveEntry(app.entry, env);
302
- if (!entry) {
303
- console.error(
304
- "[microRouter] 子应用缺少当前环境 entry,已跳过:" + app.appCode
305
- );
306
- return;
307
- }
308
- microAppConfigs[app.appCode] = {
309
- name: app.appCode,
310
- entry,
311
- props: {
312
- appCode: app.appCode,
313
- menus: subAppMenus[app.appCode] || [],
314
- token: getToken(),
315
- lang: localStorage.getItem("i18n-lang") || "",
316
- userInfo,
317
- mainAdapter,
318
- // 本地调试覆盖装载的应用:允许 URL 直达未配菜单的页面(见 permission.js 调试兜底路由)
319
- microDebug: !!app.debugOverride,
320
- },
321
- };
322
- });
323
- installRouteHook();
324
- return Object.keys(microAppConfigs);
325
- }
326
-
327
- // 当前应处于激活态的子应用(等待容器期间用户可能又切走,异步续体据此丢弃过期操作)
328
- let currentActiveAppCode = null;
329
-
330
- function installRouteHook() {
331
- if (routeHookInstalled) return;
332
- routeHookInstalled = true;
333
- router.afterEach((to) => {
334
- const appCode
335
- = to.meta && to.meta.isMicroApp && microAppConfigs[to.meta.appCode]
336
- ? to.meta.appCode
337
- : null;
338
- currentActiveAppCode = appCode;
339
- Object.keys(aliveApps).forEach((code) => {
340
- if (code !== appCode) {
341
- deactivateMicroApp(code);
342
- }
343
- });
344
- if (appCode) {
345
- activateMicroApp(appCode);
346
- }
347
- });
348
- }
349
-
350
- // 刷新直接落在子应用路由时,afterEach 早于布局渲染(AppMain 懒加载 + showAppMain 延迟),
351
- // 容器尚未进 DOM——轮询等待,超时显式报错
352
- const WRAPPER_WAIT_INTERVAL = 100;
353
- const WRAPPER_WAIT_MAX_TRIES = 100;
354
-
355
- function waitForWrapper(tryCount = 0) {
356
- const wrapper = document.getElementById(CONTAINER_WRAPPER_ID);
357
- if (wrapper) return Promise.resolve(wrapper);
358
- if (tryCount >= WRAPPER_WAIT_MAX_TRIES) {
359
- console.error(
360
- "[microRouter] 等待子应用容器 #" + CONTAINER_WRAPPER_ID + " 超时"
361
- );
362
- return Promise.resolve(null);
363
- }
364
- return new Promise((resolve) => {
365
- setTimeout(() => {
366
- resolve(waitForWrapper(tryCount + 1));
367
- }, WRAPPER_WAIT_INTERVAL);
368
- });
369
- }
370
-
371
- function getContainerEl(wrapper, appCode) {
372
- const id = CONTAINER_WRAPPER_ID + "-" + appCode;
373
- let el = document.getElementById(id);
374
- if (!el) {
375
- el = document.createElement("div");
376
- el.id = id;
377
- el.style.height = "100%";
378
- wrapper.appendChild(el);
379
- }
380
- return el;
381
- }
382
-
383
- function activateMicroApp(appCode) {
384
- const config = microAppConfigs[appCode];
385
- if (!config) return;
386
- waitForWrapper().then((wrapper) => {
387
- if (!wrapper) return;
388
- if (currentActiveAppCode !== appCode) return; // 等待期间已切走
389
- const el = getContainerEl(wrapper, appCode);
390
- el.style.display = "";
391
- const alive = aliveApps[appCode];
392
- if (!alive) {
393
- aliveApps[appCode] = {
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
+ }