generator-mico-cli 0.2.30 → 0.2.32
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/README.md +146 -18
- package/bin/mico.js +76 -0
- package/generators/h5-react/ignore-list.json +1 -0
- package/generators/h5-react/index.js +349 -0
- package/generators/h5-react/meta.json +11 -0
- package/generators/h5-react/templates/.commitlintrc.js +7 -0
- package/generators/h5-react/templates/.cursor/rules/cicd-deploy.mdc +104 -0
- package/generators/h5-react/templates/.cursor/rules/common-intl.mdc +42 -0
- package/generators/h5-react/templates/.cursor/rules/git-hooks.mdc +40 -0
- package/generators/h5-react/templates/.cursor/rules/internal-packages.mdc +46 -0
- package/generators/h5-react/templates/.cursor/rules/monorepo.mdc +64 -0
- package/generators/h5-react/templates/.cursor/rules/package-json.mdc +52 -0
- package/generators/h5-react/templates/.cursor/rules/tailwind-umi.mdc +60 -0
- package/generators/h5-react/templates/.cursor/rules/umi-app.mdc +74 -0
- package/generators/h5-react/templates/.cursor/rules/umi-config.mdc +86 -0
- package/generators/h5-react/templates/.cursor/rules/umi-mock.mdc +80 -0
- package/generators/h5-react/templates/.cursor/rules/workspace-request.mdc +52 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/SKILL.md +213 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/evals/evals.json +23 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/cursor-rule-template.md +60 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-1-scanning.md +102 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-2-context-analysis.md +102 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-3-pattern-extraction.md +105 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-4-module-mapping.md +65 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-5-glossary.md +63 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/DEV_PATTERNS.tpl.md +77 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/GLOSSARY.tpl.md +17 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/MODULE_MAP.tpl.md +45 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/PROJECT_CONTEXT.tpl.md +155 -0
- package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/update-mode.md +116 -0
- package/generators/h5-react/templates/.env.development +5 -0
- package/generators/h5-react/templates/.env.production +5 -0
- package/generators/h5-react/templates/.env.testing +5 -0
- package/generators/h5-react/templates/.husky/commit-msg +2 -0
- package/generators/h5-react/templates/.husky/pre-commit +2 -0
- package/generators/h5-react/templates/.lintstagedrc.js +8 -0
- package/generators/h5-react/templates/.prettierrc.json +7 -0
- package/generators/h5-react/templates/CICD/before_build.sh +76 -0
- package/generators/h5-react/templates/CICD/start_dev.sh +54 -0
- package/generators/h5-react/templates/CICD/start_local.sh +30 -0
- package/generators/h5-react/templates/CICD/start_prod.sh +53 -0
- package/generators/h5-react/templates/CICD/start_test.sh +55 -0
- package/generators/h5-react/templates/CICD/wangsu_fresh_dev.sh +19 -0
- package/generators/h5-react/templates/CICD/wangsu_fresh_prod.sh +19 -0
- package/generators/h5-react/templates/CICD/wangsu_fresh_test.sh +19 -0
- package/generators/h5-react/templates/README.md +301 -0
- package/generators/h5-react/templates/_gitignore +30 -0
- package/generators/h5-react/templates/_npmrc +6 -0
- package/generators/h5-react/templates/apps/.gitkeep +0 -0
- package/generators/h5-react/templates/dev.preset.json +10 -0
- package/generators/h5-react/templates/package.json +56 -0
- package/generators/h5-react/templates/packages/common-intl/README.md +180 -0
- package/generators/h5-react/templates/packages/common-intl/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/common-intl/package.json +31 -0
- package/generators/h5-react/templates/packages/common-intl/src/index.ts +3 -0
- package/generators/h5-react/templates/packages/common-intl/src/intl.ts +100 -0
- package/generators/h5-react/templates/packages/common-intl/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/components/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/components/package.json +32 -0
- package/generators/h5-react/templates/packages/components/src/Layout/ImmersiveHeader.tsx +126 -0
- package/generators/h5-react/templates/packages/components/src/Layout/LayoutFooter.tsx +72 -0
- package/generators/h5-react/templates/packages/components/src/Layout/index.tsx +121 -0
- package/generators/h5-react/templates/packages/components/src/assets/image/back.png +0 -0
- package/generators/h5-react/templates/packages/components/src/index.ts +0 -0
- package/generators/h5-react/templates/packages/components/tsconfig.json +13 -0
- package/generators/h5-react/templates/packages/components/typings.d.ts +1 -0
- package/generators/h5-react/templates/packages/constant/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/constant/package.json +19 -0
- package/generators/h5-react/templates/packages/constant/src/index.ts +0 -0
- package/generators/h5-react/templates/packages/constant/src/member.ts +8 -0
- package/generators/h5-react/templates/packages/constant/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/deeplink/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/deeplink/package.json +18 -0
- package/generators/h5-react/templates/packages/deeplink/src/index.ts +7 -0
- package/generators/h5-react/templates/packages/deeplink/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/domain/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/domain/package.json +18 -0
- package/generators/h5-react/templates/packages/domain/src/index.ts +29 -0
- package/generators/h5-react/templates/packages/domain/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/domain/types.d.ts +11 -0
- package/generators/h5-react/templates/packages/eslint/eslint.config.base.ts +36 -0
- package/generators/h5-react/templates/packages/eslint/eslint.config.react.ts +33 -0
- package/generators/h5-react/templates/packages/eslint/package.json +22 -0
- package/generators/h5-react/templates/packages/js-bridge/eslint.config.ts +17 -0
- package/generators/h5-react/templates/packages/js-bridge/package.json +23 -0
- package/generators/h5-react/templates/packages/js-bridge/src/call.ts +126 -0
- package/generators/h5-react/templates/packages/js-bridge/src/closeH5Page.ts +9 -0
- package/generators/h5-react/templates/packages/js-bridge/src/getUserInfo.ts +96 -0
- package/generators/h5-react/templates/packages/js-bridge/src/index.ts +15 -0
- package/generators/h5-react/templates/packages/js-bridge/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/js-bridge/type.d.ts +24 -0
- package/generators/h5-react/templates/packages/request/axios.d.ts +42 -0
- package/generators/h5-react/templates/packages/request/eslint.config.ts +17 -0
- package/generators/h5-react/templates/packages/request/package.json +22 -0
- package/generators/h5-react/templates/packages/request/src/index.ts +165 -0
- package/generators/h5-react/templates/packages/request/src/interceptors.ts +126 -0
- package/generators/h5-react/templates/packages/request/src/types.ts +101 -0
- package/generators/h5-react/templates/packages/request/src/url-resolver.ts +66 -0
- package/generators/h5-react/templates/packages/request/src/utils.ts +12 -0
- package/generators/h5-react/templates/packages/request/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/request/umi.d.ts +94 -0
- package/generators/h5-react/templates/packages/typescript/package.json +11 -0
- package/generators/h5-react/templates/packages/typescript/tsconfig.base.json +23 -0
- package/generators/h5-react/templates/packages/typescript/tsconfig.react.json +7 -0
- package/generators/h5-react/templates/packages/umi-config/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/umi-config/package.json +31 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.dev.ts +34 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.prod.development.ts +17 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.prod.production.ts +42 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.prod.testing.ts +17 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.prod.ts +56 -0
- package/generators/h5-react/templates/packages/umi-config/src/config.ts +86 -0
- package/generators/h5-react/templates/packages/umi-config/src/index.ts +25 -0
- package/generators/h5-react/templates/packages/umi-config/src/plugins/apply-sentry-plugin.ts +57 -0
- package/generators/h5-react/templates/packages/umi-config/src/type.d.ts +3 -0
- package/generators/h5-react/templates/packages/umi-config/tsconfig.json +3 -0
- package/generators/h5-react/templates/packages/utils/eslint.config.ts +12 -0
- package/generators/h5-react/templates/packages/utils/package.json +27 -0
- package/generators/h5-react/templates/packages/utils/src/date.ts +21 -0
- package/generators/h5-react/templates/packages/utils/src/env.ts +40 -0
- package/generators/h5-react/templates/packages/utils/src/index.ts +3 -0
- package/generators/h5-react/templates/packages/utils/src/md5.ts +17 -0
- package/generators/h5-react/templates/packages/utils/src/mock.ts +83 -0
- package/generators/h5-react/templates/packages/utils/src/number.ts +23 -0
- package/generators/h5-react/templates/packages/utils/src/tailwind.ts +12 -0
- package/generators/h5-react/templates/packages/utils/src/url.ts +19 -0
- package/generators/h5-react/templates/packages/utils/tsconfig.json +9 -0
- package/generators/h5-react/templates/page.config.ts +1 -0
- package/generators/h5-react/templates/pnpm-workspace.yaml +17 -0
- package/generators/h5-react/templates/scripts/collect-dist.js +78 -0
- package/generators/h5-react/templates/scripts/dev-preset.js +265 -0
- package/generators/h5-react/templates/scripts/dev-preset.schema.json +39 -0
- package/generators/h5-react/templates/scripts/dev.js +133 -0
- package/generators/h5-react/templates/scripts/gateway.ts +241 -0
- package/generators/h5-react/templates/turbo.json +86 -0
- package/generators/micro-react/README.md +34 -0
- package/generators/micro-react/index.js +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +21 -1
- package/generators/micro-react/templates/apps/layout/config/config.ts +0 -15
- package/generators/micro-react/templates/apps/layout/docs/arch-/346/227/245/345/277/227/344/270/216/345/270/270/351/207/217.md +16 -8
- package/generators/micro-react/templates/apps/layout/docs/feat-/346/236/204/345/273/272define/344/270/216/345/205/215/350/256/244/350/257/201/345/210/235/345/247/213/346/200/201.md +49 -3
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +3 -1
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +4 -4
- package/generators/micro-react/templates/apps/layout/src/app.tsx +10 -4
- package/generators/micro-react/templates/apps/layout/src/common/auth/auth-check-path.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +51 -18
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +8 -5
- package/generators/micro-react/templates/package.json +1 -1
- package/generators/subapp-h5/ignore-list.json +1 -0
- package/generators/subapp-h5/index.js +424 -0
- package/generators/subapp-h5/meta.json +10 -0
- package/generators/subapp-h5/templates/.env +1 -0
- package/generators/subapp-h5/templates/.stylelintrc.js +22 -0
- package/generators/subapp-h5/templates/config/config.dev.ts +7 -0
- package/generators/subapp-h5/templates/config/config.prod.development.ts +7 -0
- package/generators/subapp-h5/templates/config/config.prod.production.ts +10 -0
- package/generators/subapp-h5/templates/config/config.prod.testing.ts +7 -0
- package/generators/subapp-h5/templates/config/config.prod.ts +7 -0
- package/generators/subapp-h5/templates/config/config.ts +6 -0
- package/generators/subapp-h5/templates/config/routes.ts +13 -0
- package/generators/subapp-h5/templates/eslint.config.ts +12 -0
- package/generators/subapp-h5/templates/mock/user.ts +34 -0
- package/generators/subapp-h5/templates/package.json +42 -0
- package/generators/subapp-h5/templates/src/app.tsx +14 -0
- package/generators/subapp-h5/templates/src/assets/yay.jpg +0 -0
- package/generators/subapp-h5/templates/src/intl.ts +37 -0
- package/generators/subapp-h5/templates/src/layouts/index.tsx +10 -0
- package/generators/subapp-h5/templates/src/pages/index.tsx +22 -0
- package/generators/subapp-h5/templates/src/services/user.ts +38 -0
- package/generators/subapp-h5/templates/tailwind.config.js +16 -0
- package/generators/subapp-h5/templates/tailwind.css +7 -0
- package/generators/subapp-h5/templates/tsconfig.json +3 -0
- package/generators/subapp-h5/templates/typings.d.ts +1 -0
- package/generators/subapp-react/README.md +43 -0
- package/generators/subapp-react/index.js +2 -0
- package/generators/subapp-react/templates/homepage/README.md +5 -1
- package/generators/subapp-react/templates/homepage/config/config.dev.ts +20 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +0 -15
- package/generators/subapp-react/templates/homepage/src/common/logger.ts +50 -18
- package/generators/subapp-umd/README.md +37 -0
- package/lib/setup-multica-desktop.js +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地开发网关脚本:读取根目录 `page.config.ts`,为每个 `apps/<key>` 分配端口并启动 `pnpm dev`,
|
|
3
|
+
* 再启动统一 HTTP/WebSocket 代理(默认从 3000 起占端口),通过路径前缀将流量转发到对应子应用。
|
|
4
|
+
*/
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process';
|
|
9
|
+
import httpProxy from 'http-proxy';
|
|
10
|
+
import portfinder from 'portfinder';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 仓库根目录的 `page.config.ts`:键名为子应用目录名(对应 `apps/<key>`)。
|
|
15
|
+
*/
|
|
16
|
+
import pageConfig from '../page.config.ts';
|
|
17
|
+
|
|
18
|
+
/** 单个子应用在网关中的运行时元数据(端口、路径前缀等)。 */
|
|
19
|
+
interface PageConfig {
|
|
20
|
+
key: string;
|
|
21
|
+
name: string;
|
|
22
|
+
fullPath: string;
|
|
23
|
+
prefixPath: string;
|
|
24
|
+
port: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 遍历 `page.config.ts` 的每个键,在 `apps/<key>` 下读取 `package.json`,
|
|
29
|
+
* 并为每个子应用从 4000 起顺序分配可用端口,生成 `PageConfig` 列表。
|
|
30
|
+
*/
|
|
31
|
+
async function formatPageConfig(): Promise<PageConfig[]> {
|
|
32
|
+
const pageConfigs: PageConfig[] = [];
|
|
33
|
+
const appPath = path.join(process.cwd(), 'apps');
|
|
34
|
+
let basePort = 4000;
|
|
35
|
+
for (const [key, value] of Object.entries(pageConfig)) {
|
|
36
|
+
const appPackagePath = path.join(appPath, key, 'package.json');
|
|
37
|
+
try {
|
|
38
|
+
const appPackage = await fs.promises.readFile(appPackagePath, 'utf8');
|
|
39
|
+
const appPackageJson = JSON.parse(appPackage);
|
|
40
|
+
const port = await portfinder.getPortPromise({ port: basePort });
|
|
41
|
+
basePort = port + 1;
|
|
42
|
+
const pageConfig: PageConfig = {
|
|
43
|
+
key,
|
|
44
|
+
name: appPackageJson.name,
|
|
45
|
+
fullPath: path.join(appPath, key),
|
|
46
|
+
prefixPath: `/${key}`,
|
|
47
|
+
port,
|
|
48
|
+
};
|
|
49
|
+
pageConfigs.push(pageConfig);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[${key}] 读取 package.json 失败: ${(error as Error).message}`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return pageConfigs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 在各自 `cwd` 下执行 `pnpm run dev -p <port>` 启动每个子应用。
|
|
60
|
+
* 监听 stdout 出现 Ready/successfully 时视为已就绪并 resolve(stderr 常含非错误日志,见下方)。
|
|
61
|
+
*/
|
|
62
|
+
async function runApp(pageConfigs: PageConfig[]) {
|
|
63
|
+
const promises = pageConfigs.map(config => {
|
|
64
|
+
return new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
|
|
65
|
+
const child = spawn('pnpm', ['run', 'dev'], {
|
|
66
|
+
cwd: config.fullPath,
|
|
67
|
+
env: { ...process.env, PORT: String(config.port) },
|
|
68
|
+
// 如需在终端直接看到子应用原始输出,可改为 stdio: 'inherit'(需自行调整就绪判断逻辑)
|
|
69
|
+
shell: process.platform === 'win32',
|
|
70
|
+
});
|
|
71
|
+
child.on('error', (err) => {
|
|
72
|
+
console.error(chalk.red(`[${config.key}] 子进程启动失败:`), err.message);
|
|
73
|
+
});
|
|
74
|
+
child.on('exit', (code, signal) => {
|
|
75
|
+
if (code !== null && code !== 0) {
|
|
76
|
+
console.error(chalk.red(`[${config.key}] 子进程退出 code=${code} signal=${signal}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
child.stdout.on('data', (data) => {
|
|
80
|
+
const msg = data.toString();
|
|
81
|
+
console.log(`${chalk.green(`[${config.key}]`)} ${msg}`);
|
|
82
|
+
const reg = /Local: http:\/\/localhost:(\d+)/
|
|
83
|
+
const match = msg.match(reg);
|
|
84
|
+
if (match) {
|
|
85
|
+
const port = match[1];
|
|
86
|
+
config.port = parseInt(port);
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
resolve(child)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
child.stderr.on('data', (data) => {
|
|
93
|
+
const msg = data.toString();
|
|
94
|
+
console.log(`${chalk.dim(`[${config.key}]`)} ${msg}`);
|
|
95
|
+
});
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
return Promise.all(promises).then((childs) => {
|
|
99
|
+
console.log(chalk.green('所有子应用已启动'))
|
|
100
|
+
pageConfigs.forEach(config => {
|
|
101
|
+
console.log(`${chalk.green(`[${config.key}]`)} 子应用已启动:${chalk.blue(`http://127.0.0.1:${config.port}`)}`)
|
|
102
|
+
})
|
|
103
|
+
return childs
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 根据当前请求解析应转发到的子应用:优先用路径匹配;无 Referer 或 Referer 为根时用请求路径;
|
|
109
|
+
* 否则用 Referer 的路径(便于静态资源等请求仍落到同一子应用)。无匹配时回退上次目标或第一个应用。
|
|
110
|
+
*/
|
|
111
|
+
function getTargetByRes(req: http.IncomingMessage, pageConfigs: PageConfig[], lastTargetPageConfig: PageConfig | null) {
|
|
112
|
+
const pathname = urlPathname(req.url) ?? '/';
|
|
113
|
+
const refererPathname = urlPathname(req.headers.referer)
|
|
114
|
+
const targetPathname = (
|
|
115
|
+
!refererPathname || refererPathname === '/'
|
|
116
|
+
? pathname
|
|
117
|
+
: refererPathname
|
|
118
|
+
)
|
|
119
|
+
const pathConfig = getTargetByPath(targetPathname, pageConfigs);
|
|
120
|
+
return pathConfig ?? lastTargetPageConfig ?? pageConfigs[0]!;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 判断 `pathname` 是否属于某子应用(等于 `prefixPath` 或以其为前缀)。 */
|
|
124
|
+
function getTargetByPath(pathname: string, pageConfigs: PageConfig[]) {
|
|
125
|
+
for (const config of pageConfigs) {
|
|
126
|
+
if (pathname === config.prefixPath || pathname.startsWith(`${config.prefixPath}/`)) {
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 从完整 URL 或相对路径中解析 pathname;无效输入返回 `null`。 */
|
|
134
|
+
function urlPathname(url: string | undefined) {
|
|
135
|
+
if (!url) return null;
|
|
136
|
+
try {
|
|
137
|
+
return new URL(url, 'http://127.0.0.1').pathname || null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 转发到子应用 dev 服务器前去掉网关层前缀(如 `/app`),使子应用收到根路径下的请求。
|
|
145
|
+
*/
|
|
146
|
+
function rewriteUrlForTarget(rawUrl: string | undefined, target: PageConfig): string {
|
|
147
|
+
if (!rawUrl) return '/';
|
|
148
|
+
const url = new URL(rawUrl, 'http://127.0.0.1');
|
|
149
|
+
url.pathname = url.pathname.replace(new RegExp(`^${target.prefixPath}`), '') || '/';
|
|
150
|
+
return url.toString();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 在可用端口(默认从 3000 起探测)上启动 HTTP 代理,支持 WebSocket;
|
|
155
|
+
* 根路径 `/` 返回各子应用入口链接,其余请求按路由规则转发到对应本地端口。
|
|
156
|
+
*/
|
|
157
|
+
async function runProxyServer(pageConfigs: PageConfig[]) {
|
|
158
|
+
const proxy = httpProxy.createProxyServer({ ws: true });
|
|
159
|
+
proxy.on('error', (err, req, res) => {
|
|
160
|
+
console.error(chalk.red('[proxy error]'), err.message);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
let lastTargetPageConfig: PageConfig | null = null;
|
|
164
|
+
|
|
165
|
+
const server = http.createServer((req, res) => {
|
|
166
|
+
const pathname = urlPathname(req.url) ?? '/';
|
|
167
|
+
if (pathname === '/' || pathname === '') {
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
169
|
+
const links = pageConfigs
|
|
170
|
+
.map((c) => ` <li><a href="${c.prefixPath}">${c.prefixPath}</a> (${c.key})</li>`)
|
|
171
|
+
.join('\n');
|
|
172
|
+
res.end(
|
|
173
|
+
`<!DOCTYPE html><html><body><h1>子应用入口</h1><ul>\n${links}\n</ul></body></html>`
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lastTargetPageConfig = getTargetByRes(req, pageConfigs, lastTargetPageConfig);
|
|
179
|
+
req.url = rewriteUrlForTarget(req.url, lastTargetPageConfig);
|
|
180
|
+
proxy.web(req, res, {
|
|
181
|
+
target: `http://127.0.0.1:${lastTargetPageConfig.port}`,
|
|
182
|
+
changeOrigin: true,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
server.on('upgrade', (req, socket, head) => {
|
|
187
|
+
lastTargetPageConfig = getTargetByRes(req, pageConfigs, lastTargetPageConfig);
|
|
188
|
+
req.url = rewriteUrlForTarget(req.url, lastTargetPageConfig);
|
|
189
|
+
proxy.ws(req, socket, head, {
|
|
190
|
+
target: `http://127.0.0.1:${lastTargetPageConfig.port}`,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
const port = await portfinder.getPortPromise({ port: 3000 });
|
|
194
|
+
server.listen(port, () => {
|
|
195
|
+
console.log(chalk.green(`[gateway] 代理服务已启动 端口:${chalk.blue(port)}`));
|
|
196
|
+
console.log(`>>> 统一通过代理访问: ${chalk.blue(`http://127.0.0.1:${port}`)}`);
|
|
197
|
+
pageConfigs.forEach(config => {
|
|
198
|
+
console.log(`${chalk.green(`[${config.key}]`)} 子应用访问路径:${chalk.blue(`http://127.0.0.1:${port}${config.prefixPath}`)}`)
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
return server;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 在收到 SIGINT(Ctrl+C)或 SIGTERM 时关闭代理 `server` 并终止所有子进程;
|
|
206
|
+
* `server.close` 若迟迟不回调,3 秒后强制 `process.exit(0)`。
|
|
207
|
+
*/
|
|
208
|
+
function listenerClose(server: http.Server, childs: ChildProcessWithoutNullStreams[]) {
|
|
209
|
+
let cleaning = false;
|
|
210
|
+
const cleanup = () => {
|
|
211
|
+
if (cleaning) return;
|
|
212
|
+
cleaning = true;
|
|
213
|
+
|
|
214
|
+
server.close(() => {
|
|
215
|
+
childs.forEach((child) => {
|
|
216
|
+
if (child.killed) return;
|
|
217
|
+
child.kill('SIGTERM');
|
|
218
|
+
});
|
|
219
|
+
process.exit(0);
|
|
220
|
+
});
|
|
221
|
+
// 若 server.close 回调未及时触发,超时后强制退出
|
|
222
|
+
setTimeout(() => process.exit(0), 3000);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
process.on('SIGINT', cleanup);
|
|
226
|
+
process.on('SIGTERM', cleanup);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 加载配置 → 拉起子应用 → 启动网关代理 → 注册退出清理。 */
|
|
230
|
+
async function main() {
|
|
231
|
+
const pageConfigs = await formatPageConfig();
|
|
232
|
+
if (pageConfigs.length === 0) {
|
|
233
|
+
console.error('未开启任何子应用,请检查 page.config.ts 配置');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
const childs = await runApp(pageConfigs);
|
|
237
|
+
const proxyServer = await runProxyServer(pageConfigs);
|
|
238
|
+
listenerClose(proxyServer, childs);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turbo.build/schema.json",
|
|
3
|
+
"globalEnv": [
|
|
4
|
+
"CDN_PUBLIC_PATH",
|
|
5
|
+
"BRANCH_OR_TAG",
|
|
6
|
+
"VERSION",
|
|
7
|
+
"SENTRY_AUTH_TOKEN"
|
|
8
|
+
],
|
|
9
|
+
"envMode": "loose",
|
|
10
|
+
"tasks": {
|
|
11
|
+
"build": {
|
|
12
|
+
"dependsOn": ["^build"],
|
|
13
|
+
"inputs": [
|
|
14
|
+
"$TURBO_DEFAULT$",
|
|
15
|
+
"!.umi/**",
|
|
16
|
+
"!.turbo/**",
|
|
17
|
+
"!src/.umi/**",
|
|
18
|
+
"!src/.umi-production/**",
|
|
19
|
+
"!src/.umi-test/**",
|
|
20
|
+
".env",
|
|
21
|
+
".env.local",
|
|
22
|
+
".env.production",
|
|
23
|
+
".env.production.local"
|
|
24
|
+
],
|
|
25
|
+
"outputs": ["dist/**"]
|
|
26
|
+
},
|
|
27
|
+
"build:development": {
|
|
28
|
+
"dependsOn": ["^build:development"],
|
|
29
|
+
"inputs": [
|
|
30
|
+
"$TURBO_DEFAULT$",
|
|
31
|
+
"!.umi/**",
|
|
32
|
+
"!.turbo/**",
|
|
33
|
+
"!src/.umi/**",
|
|
34
|
+
"!src/.umi-production/**",
|
|
35
|
+
"!src/.umi-test/**",
|
|
36
|
+
".env",
|
|
37
|
+
".env.local",
|
|
38
|
+
".env.development",
|
|
39
|
+
".env.development.local"
|
|
40
|
+
],
|
|
41
|
+
"outputs": ["dist/**"]
|
|
42
|
+
},
|
|
43
|
+
"build:testing": {
|
|
44
|
+
"dependsOn": ["^build:testing"],
|
|
45
|
+
"inputs": [
|
|
46
|
+
"$TURBO_DEFAULT$",
|
|
47
|
+
"!.umi/**",
|
|
48
|
+
"!.turbo/**",
|
|
49
|
+
"!src/.umi/**",
|
|
50
|
+
"!src/.umi-production/**",
|
|
51
|
+
"!src/.umi-test/**",
|
|
52
|
+
".env",
|
|
53
|
+
".env.local",
|
|
54
|
+
".env.testing",
|
|
55
|
+
".env.testing.local"
|
|
56
|
+
],
|
|
57
|
+
"outputs": ["dist/**"]
|
|
58
|
+
},
|
|
59
|
+
"build:production": {
|
|
60
|
+
"dependsOn": ["^build:production"],
|
|
61
|
+
"inputs": [
|
|
62
|
+
"$TURBO_DEFAULT$",
|
|
63
|
+
"!.umi/**",
|
|
64
|
+
"!.turbo/**",
|
|
65
|
+
"!src/.umi/**",
|
|
66
|
+
"!src/.umi-production/**",
|
|
67
|
+
"!src/.umi-test/**",
|
|
68
|
+
".env",
|
|
69
|
+
".env.local",
|
|
70
|
+
".env.production",
|
|
71
|
+
".env.production.local"
|
|
72
|
+
],
|
|
73
|
+
"outputs": ["dist/**"]
|
|
74
|
+
},
|
|
75
|
+
"dev": {
|
|
76
|
+
"cache": false,
|
|
77
|
+
"persistent": true
|
|
78
|
+
},
|
|
79
|
+
"lint": {
|
|
80
|
+
"outputs": []
|
|
81
|
+
},
|
|
82
|
+
"lint:fix": {
|
|
83
|
+
"outputs": []
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# micro-react 生成器
|
|
2
|
+
|
|
3
|
+
创建完整的 qiankun 微前端 Monorepo。详细 prompt、模板变量、后置动作请见 [docs/generators.md#micro-react](../../docs/generators.md#micro-react)。
|
|
4
|
+
|
|
5
|
+
## 快速索引
|
|
6
|
+
|
|
7
|
+
| 文件 | 职责 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `index.js` | Yeoman 生成器类(`initializing` / `prompting` / `writing` / `install` / `end`) |
|
|
10
|
+
| `meta.json` | `mico list` 展示的名称、描述、features |
|
|
11
|
+
| `ignore-list.json` | `collectFiles` 按路径片段过滤的清单 |
|
|
12
|
+
| `templates/` | 模板根(本仓库文档审计不覆盖此目录) |
|
|
13
|
+
|
|
14
|
+
## 使用
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
mkdir my-project && cd my-project
|
|
18
|
+
mico create micro-react
|
|
19
|
+
# 可选:--dry-run / --verbose / --force
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要求当前目录**不**是已有 monorepo(没有 `pnpm-workspace.yaml`);`--force` 可绕过此检查。
|
|
23
|
+
|
|
24
|
+
## 模板变量速查
|
|
25
|
+
|
|
26
|
+
| 变量 | 类型 | 来源 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `projectName` / `ProjectName` | string | prompt,`toKebab` / `toPascal` |
|
|
29
|
+
| `appId` | string | prompt |
|
|
30
|
+
| `packageScope` | string | prompt |
|
|
31
|
+
| `cdnPrefix` / `cdnPrefixPath` | string | prompt;`cdnPrefixPath` 为空或带尾斜杠 |
|
|
32
|
+
| `author` | string | prompt |
|
|
33
|
+
| `intlTag` | string | prompt |
|
|
34
|
+
| `micoUiVersion` / `themeVersion` | string | `npm view @mico-platform/{ui,theme} version`,前缀 `^`,失败回退 `^1.0.0` |
|
|
@@ -186,6 +186,8 @@ module.exports = class extends Generator {
|
|
|
186
186
|
const templateData = {
|
|
187
187
|
projectName: this.projectName,
|
|
188
188
|
ProjectName: this.ProjectName,
|
|
189
|
+
// 用于 MFSU mfName 等需要合法 JS 标识符的场景(不允许 `-`)
|
|
190
|
+
projectNameSnake: this.projectName.replace(/-/g, '_'),
|
|
189
191
|
appId: this.appId,
|
|
190
192
|
packageScope: this.packageScope,
|
|
191
193
|
author: this.author,
|
|
@@ -34,7 +34,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
34
34
|
// 关闭权限控制(调试用)
|
|
35
35
|
disableAuth: false,
|
|
36
36
|
// SSO 外跳地址(与 resolveExternalLoginPath 读取的 externalLoginPath 一致;生产由注入的 __MICO_CONFIG__ 提供)
|
|
37
|
-
externalLoginPath: 'https://micous-idp.cig.tencentcs.com/sso/tn-456d1d3feb5f4e09ad28ab35ee4d2e66/ai-
|
|
37
|
+
externalLoginPath: 'https://micous-idp.cig.tencentcs.com/sso/tn-456d1d3feb5f4e09ad28ab35ee4d2e66/ai-4919cf3d4883490b956b90376cfb86e7/cas',
|
|
38
38
|
// 多语言中台(可选):intl 与 common-intl 包 initIntl 合并;见 packages/common-intl/README.md
|
|
39
39
|
// 这里只是一个例子,需要根据业务在翻译中台的配置来替换
|
|
40
40
|
// intl: {
|
|
@@ -80,6 +80,26 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
80
80
|
'process.env.LOCALE_REQUEST_URL':
|
|
81
81
|
'https://api-test.micoplatform.com/lang_server/pull',
|
|
82
82
|
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @name MFSU 配置
|
|
86
|
+
* @description
|
|
87
|
+
* - mfName: 唯一名隔离微前端 runtime,避免多应用共存时 MFSU 容器冲突
|
|
88
|
+
* - exclude:
|
|
89
|
+
* - `@mico-platform/theme`:theme 子路径解析
|
|
90
|
+
* - `@mico-platform/ui`:尽量与主应用一起编译
|
|
91
|
+
* - `<%= packageScope %>/*`:workspace 包走 webpack 编译,开发态可在 DevTools 直接定位到 packages/* 源码
|
|
92
|
+
* - shared: React 单例,确保 MFSU 预打包里的组件(如 Layout)与主应用共用同一份 React,否则 useContext 报 null
|
|
93
|
+
* @doc https://umijs.org/docs/api/config#mfsu
|
|
94
|
+
*/
|
|
95
|
+
mfsu: {
|
|
96
|
+
mfName: 'mf_<%= projectNameSnake %>_layout',
|
|
97
|
+
exclude: ['@mico-platform/theme', '@mico-platform/ui', /^<%= packageScope %>\//],
|
|
98
|
+
shared: {
|
|
99
|
+
react: { singleton: true },
|
|
100
|
+
'react-dom': { singleton: true },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
83
103
|
};
|
|
84
104
|
|
|
85
105
|
export default defineConfig(config);
|
|
@@ -124,21 +124,6 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
124
124
|
*/
|
|
125
125
|
extraBabelPresets: ['@mico-platform/ui/babel-preset'],
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
* @name MFSU 配置
|
|
129
|
-
* @description
|
|
130
|
-
* - exclude: theme 子路径解析;ui 尽量与主应用一起编译
|
|
131
|
-
* - shared: React 单例,确保 MFSU 预打包里的组件(如 Layout)与主应用共用同一份 React,否则 useContext 报 null
|
|
132
|
-
* @doc https://umijs.org/docs/guides/mfsu
|
|
133
|
-
*/
|
|
134
|
-
mfsu: {
|
|
135
|
-
exclude: ['@mico-platform/theme', '@mico-platform/ui'],
|
|
136
|
-
shared: {
|
|
137
|
-
react: { singleton: true },
|
|
138
|
-
'react-dom': { singleton: true },
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
|
|
142
127
|
/**
|
|
143
128
|
* @name qiankun 微前端配置
|
|
144
129
|
* @description 作为主应用,动态加载子应用
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## Logger 工具
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
默认:**开发环境**全量输出;**生产环境**仅 `log` / `info` / `warn` 静默,`error` 仍带前缀输出。使用 `bind` 保持正确的调用栈位置。
|
|
8
|
+
|
|
9
|
+
### 生产临时全量日志(首屏 query)
|
|
10
|
+
|
|
11
|
+
首屏 URL 带 **`?debug=1`**(或 `debug=true` / `yes`,大小写不敏感)时,在**本模块首次加载**时解析一次,之后 `log` / `info` / `warn` 与开发环境行为一致;**不**随 SPA 内路由或 query 变更而动态开关(需整页刷新并带上参数)。
|
|
12
|
+
|
|
13
|
+
常量 **`DEBUG_LOGS_QUERY_KEY`**(当前为 `'debug'`)与 **`isLayoutDebugLogsEnabled`**(布尔)可从 `@/common/logger` 引用。
|
|
14
|
+
|
|
15
|
+
**安全与合规**:仅用于临时排障;控制台可能含业务或个人信息,**勿长期公开或转发**带 `debug` 的生产链接;接入日志采集时需自行脱敏。
|
|
8
16
|
|
|
9
17
|
### 使用
|
|
10
18
|
|
|
@@ -18,12 +26,12 @@ layoutLogger.error('error'); // 保持原始调用栈位置
|
|
|
18
26
|
### 实现原理
|
|
19
27
|
|
|
20
28
|
```typescript
|
|
21
|
-
// 使用 bind 而非 wrapper
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
29
|
+
// 使用 bind 而非 wrapper,保持控制台显示实际调用位置
|
|
30
|
+
// 生产且未开 debug:log/info/warn 为 noop,error 仍 bind
|
|
31
|
+
const createLogger = (prefix: string) =>
|
|
32
|
+
isLayoutDebugLogsEnabled
|
|
33
|
+
? createVerboseHandlers(`[${prefix}]`)
|
|
34
|
+
: createQuietHandlers(`[${prefix}]`);
|
|
27
35
|
```
|
|
28
36
|
|
|
29
37
|
### 预定义 Logger
|
|
@@ -100,6 +108,6 @@ export const STORAGE_KEYS = {
|
|
|
100
108
|
|
|
101
109
|
## 注意事项
|
|
102
110
|
|
|
103
|
-
-
|
|
111
|
+
- 生产默认仅 `error` 输出;首屏 `?debug=1` 等可打开全量日志(见上文),用完建议去掉参数或整页刷新无参 URL
|
|
104
112
|
- 使用 `bind` 实现,DevTools 显示实际调用位置而非 logger.ts
|
|
105
113
|
- 新增路由常量应添加到 `ROUTES` 对象
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# 构建 define 精简与免认证页初始态
|
|
2
2
|
|
|
3
|
-
> 创建时间:2026-03-24
|
|
3
|
+
> 创建时间:2026-03-24
|
|
4
|
+
> 更新:2026-05-13(补充 `defaultPath` 首屏与 `authCheckPath`、与手动跳转认证页的差异;补充从免认证页跳进认证页后 **`refresh` → `fetchUserInfo`** 的说明)
|
|
4
5
|
|
|
5
6
|
## 功能概述
|
|
6
7
|
|
|
7
8
|
1. **Umi `define`**:不再通过构建期注入 `process.env.EXTERNAL_LOGIN_PATH`(本模板从未注入 `APP_ID`);SSO 外跳等依赖运行时 `window.__MICO_CONFIG__`。
|
|
8
9
|
2. **`getInitialState`**:免认证路由下即使本地仍有 token,也不请求用户信息接口,减少无效调用。
|
|
10
|
+
3. **`/` + `defaultPath` 首屏**:若仅用 `location.pathname === '/'` 判断是否免认证,可能与「即将 `replace` 到的默认页」的认证要求不一致;见下文 **`authCheckPath`**。
|
|
9
11
|
|
|
10
12
|
## 技术方案
|
|
11
13
|
|
|
@@ -21,7 +23,46 @@
|
|
|
21
23
|
|
|
22
24
|
### 初始态
|
|
23
25
|
|
|
24
|
-
`src/app.tsx` 中仅在 `getStoredAuthToken() && !skipAuth` 时调用 `fetchUserInfoFn()`;`skipAuth
|
|
26
|
+
`src/app.tsx` 中仅在 `getStoredAuthToken() && !skipAuth` 时调用 `fetchUserInfoFn()`;`skipAuth` 由 **`isNoAuthRoute(authCheckPath) || isPageAuthFree(authCheckPath)`** 计算(见下节 **`authCheckPath`**,勿与仅看 `location.pathname` 混淆)。
|
|
27
|
+
|
|
28
|
+
### `defaultPath` 与 `authCheckPath`(首屏重定向 vs 手动进认证页)
|
|
29
|
+
|
|
30
|
+
#### 背景
|
|
31
|
+
|
|
32
|
+
访问 **`/`** 且配置了 **`window.__MICO_CONFIG__.defaultPath`**(且不为 `/`)时,`app.tsx` 的 **`onRouteChange`** 会 **`history.replace(defaultPath)`**(例如跳到工作台)。**`getInitialState` 与 `onRouteChange` 的执行顺序下,首屏拿到的 `history.location.pathname` 往往仍是 `/`**。
|
|
33
|
+
|
|
34
|
+
若 **`/`** 被配进 **`noAuthRouteList`**,或 **`/`** 对应页面 **`accessControlEnabled: false`**(页面级免认证),则仅按 **`pathname === '/'`** 会得到 **`skipAuth === true`**:不执行 **`ensureSsoSession`**、不按需认证页拉用户;随后用户马上被重定向到 **`defaultPath` 所指的需登录页**,形成「用免认证壳路径误判、绕开目标页认证」的窗口。为此在 **`getInitialState`**(约自 `app.tsx` 215 行起)对 **`defaultPath`** 做与 **`onRouteChange`** 一致的重定向预判。
|
|
35
|
+
|
|
36
|
+
#### `authCheckPath` 规则
|
|
37
|
+
|
|
38
|
+
- 当 **`pathname === '/'`** 且存在有效 **`defaultPath`**(与 `/` 不同)时,视为即将发生默认重定向,令 **`authCheckPath = defaultPath`**;
|
|
39
|
+
- 否则 **`authCheckPath = location.pathname`**。
|
|
40
|
+
|
|
41
|
+
**`isNoAuthRoute`、`isPageAuthFree` 以及由此得到的 `skipAuth`、`ensureSsoSession`、是否拉 `fetchUserInfo` 等,均基于 `authCheckPath`**,使首屏认证语义与 **重定向目标页** 一致,而不是与瞬时的 **`/`** 一致。
|
|
42
|
+
|
|
43
|
+
实现上 **`authCheckPath`** 与 **`handleAuthFailureRedirect`**(`src/common/request/sso.ts`)共用 **`resolveAuthCheckPath(pathname)`**(`src/common/auth/auth-check-path.ts`),避免无 token 时 **`getInitialState` 已按 `defaultPath` 判定需认证**,但 SSO 外跳仍按浏览器地址 **`/`** 误判为免认证而**不触发重定向**。
|
|
44
|
+
|
|
45
|
+
#### 与「从免认证页手动点到认证页」的区别
|
|
46
|
+
|
|
47
|
+
| 场景 | 典型 `location.pathname` | 为何不会误判 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| **`/` + `defaultPath` 首屏** | 首屏仍为 **`/`**,真实要去 **`defaultPath`** | 若只看 **`/`**,可能与 **`defaultPath`** 的认证要求不一致;必须用 **`authCheckPath`** 把判断「对齐到目标页」。 |
|
|
50
|
+
| **应用内从免认证页导航到认证页**(如 `/login` → `/app`) | 导航后 **`/app/...`** | 不会用旧页的免认证 pathname 算 **`skipAuth`**;且布局里 **`useRoutePermissionRefresh`** 在 pathname 变为**非** `isNoAuthRoute` 时会调用 **`@@initialState` 的 `refresh()`**,从而**再次执行 `getInitialState`**,在 **`getStoredAuthToken() && !skipAuth`** 成立时拉 **`fetchUserInfo`**(见下节)。整页刷新直达认证页时逻辑相同,仅触发时机为首屏而非路由变更。 |
|
|
51
|
+
| **外链直达或地址栏改为认证页** | 直接进入 **`/app/xxx`** | 首屏 `getInitialState` 即按认证页计算 **`skipAuth`**;有 token 则拉 **`fetchUserInfo`**。 |
|
|
52
|
+
|
|
53
|
+
#### 为何客户端从免认证跳进认证页后会拉取用户权限(`fetchUserInfo`)
|
|
54
|
+
|
|
55
|
+
1. **首屏停在免认证路径**:`getInitialState` 里 **`skipAuth === true`**,模板在 **`getStoredAuthToken() && !skipAuth`** 条件下**不会**为当前首屏去调用户信息接口,`currentUser` 可能仍为空(见 `app.tsx`)。
|
|
56
|
+
|
|
57
|
+
2. **`pathname` 变为需认证路径**:`layouts/index.tsx` 使用 **`useRoutePermissionRefresh`**(`src/hooks/useRoutePermissionRefresh.ts`)。在**跳过首次渲染**之后,每当 **`location.pathname` 变化**且新路径**不是** **`isNoAuthRoute`** 所指的静态免认证路由时,会防抖调用 Umi **`useModel('@@initialState').refresh()`**。
|
|
58
|
+
|
|
59
|
+
3. **`refresh()` 会重新执行 `getInitialState`**:此时用于计算 **`skipAuth`** 的路径已是认证页(并适用上文 **`authCheckPath`** 规则),一般 **`skipAuth === false`**;若本地仍有 **`getStoredAuthToken()`**,即进入 **`fetchUserInfoFn()`**,请求 **`GET /user/info/`** 等,更新 **`menu_perms`、`button_perms`** 等,供菜单、PermissionFilter、MicroAppLoader 注入子应用等使用。
|
|
60
|
+
|
|
61
|
+
4. **与「整页刷新且首屏已在认证页」**:走同一套 `getInitialState` 分支;差别是触发源为 **路由变更 + `refresh()`**,而非 **应用冷启动首屏**。
|
|
62
|
+
|
|
63
|
+
5. **实现细节**:`useRoutePermissionRefresh` 仅以 **`isNoAuthRoute(pathname)`** 判断是否跳过刷新;若某路由**仅**页面级 **`isPageAuthFree`** 而不在 `noAuthRouteList` 中,离开该页时 pathname 变化仍可能触发 **`refresh()`**——即「离开静态免认证名单就允许刷新权限」的策略。
|
|
64
|
+
|
|
65
|
+
**结论**:「绕过认证」风险来自 **「URL 暂时是免认证的 `/`,但实际业务要去需 SSO 的 `defaultPath`」** 这一种 **首屏 + 默认重定向** 组合;**`authCheckPath`** 专门纠偏这一种。**手动从免认证页进入认证页**时,浏览器地址已是认证路径,鉴权按认证页执行,**不依赖**与 `defaultPath` 相同的特殊分支;二者差异在于 **是否存在「pathname 与真实首屏业务路径不一致」的短暂错位**。在 **SPA** 下,进入认证页后还会经 **`useRoutePermissionRefresh` → `refresh()`** 在 **有 token** 时补拉 **`fetchUserInfo`**(见上节),避免 **`currentUser` 仍为空**、菜单与子应用权限数据落后。
|
|
25
66
|
|
|
26
67
|
## 文件清单
|
|
27
68
|
|
|
@@ -31,7 +72,11 @@
|
|
|
31
72
|
| `config/config.prod.ts` | 移除 `EXTERNAL_LOGIN_PATH` define |
|
|
32
73
|
| `config/config.prod.development.ts` | 同上 |
|
|
33
74
|
| `config/config.prod.testing.ts` | 同上 |
|
|
34
|
-
| `src/app.tsx` | `
|
|
75
|
+
| `src/app.tsx` | `getInitialState` 使用 `resolveAuthCheckPath` |
|
|
76
|
+
| `src/common/auth/auth-check-path.ts` | `resolveAuthCheckPath`:`/` + `defaultPath` 与真实鉴权路径对齐 |
|
|
77
|
+
| `src/common/request/sso.ts` | `handleAuthFailureRedirect` 使用 `resolveAuthCheckPath`,与 `getInitialState` 一致 |
|
|
78
|
+
| `src/hooks/useRoutePermissionRefresh.ts` | 非静态免认证路径的 pathname 变更时 `refresh()`,触发重跑 `getInitialState` 以拉权限 |
|
|
79
|
+
| `src/layouts/index.tsx` | 使用 `useRoutePermissionRefresh` |
|
|
35
80
|
|
|
36
81
|
## 部署注意
|
|
37
82
|
|
|
@@ -40,5 +85,6 @@
|
|
|
40
85
|
|
|
41
86
|
## 相关文档
|
|
42
87
|
|
|
88
|
+
- [菜单权限控制](./feature-菜单权限控制.md)(SSO 步骤与 pathname / `authCheckPath` 关系)
|
|
43
89
|
- [fix-SSO无限重定向](./fix-SSO无限重定向.md)
|
|
44
90
|
- [arch-请求模块](./arch-请求模块.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 菜单权限控制
|
|
2
2
|
|
|
3
|
-
> 创建时间:2026-01-24 更新时间:2026-03-27(同步 `getMenuPage`:pageId 优先,path
|
|
3
|
+
> 创建时间:2026-01-24 更新时间:2026-03-27(同步 `getMenuPage`:pageId 优先,path 兜底);2026-05-13(`getInitialState` 与 `authCheckPath` 说明链至 feat-构建define)
|
|
4
4
|
|
|
5
5
|
## 功能概述
|
|
6
6
|
|
|
@@ -84,6 +84,8 @@
|
|
|
84
84
|
└──────────────────────────────────────────────┘
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
**首屏 `getInitialState` 与步骤 1**:若 URL 为 **`/`** 且配置了 **`__MICO_CONFIG__.defaultPath`**(非 `/`),`onRouteChange` 会 **`replace`** 到默认路径,首屏瞬间 **`location.pathname` 可能仍为 `/`**。此时步骤 1 不能只看 **`pathname`**,需用 **`authCheckPath`**(见 [feat-构建define与免认证初始态](./feat-构建define与免认证初始态.md) 中「`defaultPath` 与 `authCheckPath`」)。图中步骤 1 的「pathname」在 **`getInitialState`** 语境下指 **`authCheckPath`**;从免认证页**手动**进入认证页时,地址已是认证路径,无「`/` 壳 vs `defaultPath` 目标」错位。**客户端 SPA** 下进入认证页后,还会由 **`useRoutePermissionRefresh` → `@@initialState.refresh()`** 重跑 `getInitialState`,在 **有 token** 时拉 **`fetchUserInfo`**(详见该文档「为何客户端从免认证跳进认证页后会拉取用户权限」)。
|
|
88
|
+
|
|
87
89
|
### 权限判断逻辑(详细)
|
|
88
90
|
|
|
89
91
|
```
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
1. **`routePermission`**:`BasicLayout` 中 **`isForbidden` 路由权限判定**(为何放行 / 为何 403)。
|
|
10
10
|
2. **`menuFilter`**:`filterMenuItems` 中**被隐藏的菜单项**及**隐藏原因**(侧栏不可见条目)。
|
|
11
11
|
|
|
12
|
-
日志均经 `layoutLogger`
|
|
12
|
+
日志均经 `layoutLogger` 输出;**默认仅开发环境**会在控制台看到 `log`;生产首屏带 `?debug=1`(等)时行为与开发一致,见 [日志与常量](./arch-日志与常量.md)。
|
|
13
13
|
|
|
14
14
|
## 技术方案
|
|
15
15
|
|
|
16
16
|
### 技术栈
|
|
17
17
|
|
|
18
18
|
- 框架:React 18 + @umijs/max
|
|
19
|
-
- 日志:`apps/layout/src/common/logger.ts` 的 `layoutLogger
|
|
19
|
+
- 日志:`apps/layout/src/common/logger.ts` 的 `layoutLogger`(开发环境或生产首屏 `debug` query 时输出 `log`)
|
|
20
20
|
|
|
21
21
|
### 核心实现
|
|
22
22
|
|
|
@@ -142,13 +142,13 @@ menuFilter
|
|
|
142
142
|
| 决策点 | 选择 | 理由 |
|
|
143
143
|
|--------|------|------|
|
|
144
144
|
| 日志前缀 | 固定 `'routePermission'` + 对象载荷 | 可过滤、可结构化,便于 AI/人工检索 |
|
|
145
|
-
| 生产环境 |
|
|
145
|
+
| 生产环境 | 默认静默 `log` | `layoutLogger` 在 production 下 `log`/`info`/`warn` 为 noop;首屏 `?debug=1` 等同开发 |
|
|
146
146
|
| 无 `currentRoute` 不打日志 | 跳过 | 静态路由访问频繁,避免噪音 |
|
|
147
147
|
| `menuFilter` 与超管/关权限 | 不打 | 无过滤效果时无隐藏项可记 |
|
|
148
148
|
|
|
149
149
|
## 已知限制与待改进
|
|
150
150
|
|
|
151
|
-
-
|
|
151
|
+
- **默认**仅开发环境会在控制台看到本功能的 `log`;生产首屏带 `?debug=1`(等)时亦会输出,见 [日志与常量](./arch-日志与常量.md)。线上问题仍主要依赖 Sentry 或其它监控。
|
|
152
152
|
- `renderContent` 另有 `renderContent:` 日志,与 `routePermission` 独立,排查时可同时参考。
|
|
153
153
|
|
|
154
154
|
## 注意事项
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ILang,
|
|
23
23
|
} from '<%= packageScope %>/common-intl';
|
|
24
24
|
import * as CommonIntl from '<%= packageScope %>/common-intl';
|
|
25
|
+
import { resolveAuthCheckPath } from './common/auth/auth-check-path';
|
|
25
26
|
import { getStoredAuthToken } from './common/auth/auth-manager';
|
|
26
27
|
import type { IUserInfo } from './common/auth/type';
|
|
27
28
|
import { fetchUserInfo } from './services/user';
|
|
@@ -210,16 +211,21 @@ export async function getInitialState(): Promise<{
|
|
|
210
211
|
};
|
|
211
212
|
|
|
212
213
|
const { location } = history;
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
// 如果当前路径是 / 且配置了 defaultPath(onRouteChange 会立即重定向),
|
|
215
|
+
// 用目标路径判断是否需要认证,避免用免认证的兜底页绕过目标页的认证要求(与 resolveAuthCheckPath、sso handleAuthFailureRedirect 一致)
|
|
216
|
+
const authCheckPath = resolveAuthCheckPath(location.pathname);
|
|
217
|
+
|
|
218
|
+
const noAuthRoute = isNoAuthRoute(authCheckPath);
|
|
219
|
+
const skipAuth = noAuthRoute || isPageAuthFree(authCheckPath);
|
|
215
220
|
|
|
216
221
|
// 非免认证路由:走 SSO 流程
|
|
217
222
|
if (!skipAuth) {
|
|
218
223
|
await ensureSsoSession();
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
//
|
|
222
|
-
//
|
|
226
|
+
// 仅在「需认证」路径且本地已有 token 时拉取用户信息(登录态刷新等)。
|
|
227
|
+
// 免认证 / 页面级免认证路径不强制 SSO,本地未必有 token;不进入本分支则无 currentUser,
|
|
228
|
+
// PermissionFilter、MicroAppLoader 注入子应用的 button_perms 等按无登录用户(无权限)处理。
|
|
223
229
|
if (getStoredAuthToken() && !skipAuth) {
|
|
224
230
|
const userInfo = await fetchUserInfoFn();
|
|
225
231
|
if (userInfo) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 与 `app.tsx` 中 `getInitialState` 的鉴权路径语义一致:
|
|
3
|
+
* 访问 `/` 且配置了 `window.__MICO_CONFIG__.defaultPath`(非 `/`)时,`onRouteChange` 会 `replace` 到该路径,
|
|
4
|
+
* 首屏 `location.pathname` 可能仍为 `/`,但免认证 / SSO 失败重定向等判断应以**目标路径**为准。
|
|
5
|
+
*/
|
|
6
|
+
export function resolveAuthCheckPath(pathname: string): string {
|
|
7
|
+
if (typeof window === 'undefined') {
|
|
8
|
+
return pathname;
|
|
9
|
+
}
|
|
10
|
+
const defaultPath = window.__MICO_CONFIG__?.defaultPath;
|
|
11
|
+
const willRedirectToDefault =
|
|
12
|
+
pathname === '/' && !!defaultPath && defaultPath !== '/';
|
|
13
|
+
return willRedirectToDefault ? defaultPath : pathname;
|
|
14
|
+
}
|