@nebula-skills/nebula-code-standards 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/bin/cli.cjs +50 -0
- package/package.json +45 -0
- package/scripts/postinstall.cjs +18 -0
- package/skill/SKILL.md +133 -0
- package/skill/backend-standards.md +611 -0
- package/skill/boot-components-catalog.md +546 -0
- package/skill/db-standards.md +232 -0
- package/skill/framework-standards.md +610 -0
- package/skill/frontend-standards.md +310 -0
- package/skill/full-crud-example.md +608 -0
- package/skill/init-new-project.md +842 -0
- package/skill/microapp-guide.md +510 -0
- package/skill/pitfalls-checklist.md +188 -0
- package/skill/rpc-api-reference.md +313 -0
- package/skill/upgrade-decision.md +151 -0
- package/skill/workspace-overview.md +194 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# 独立仓库微前端子应用接入指南
|
|
2
|
+
|
|
3
|
+
> 业务前端要不要作为独立仓库?怎么接入 nebula-web 主应用?本文给标准答案,**规则自闭环**,不依赖任何具体业务样板项目。
|
|
4
|
+
|
|
5
|
+
主应用:`nebula-web/apps/nebula-web-main`(端口 `:3000`)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、两种子应用形态对比
|
|
10
|
+
|
|
11
|
+
| 维度 | monorepo 内置 | 独立仓库(推荐业务域) |
|
|
12
|
+
|------|-------------|--------------------|
|
|
13
|
+
| 位置 | `nebula-web/apps/nebula-web-{domain}` | 独立 Git 仓库 `nebula-{domain}-web` |
|
|
14
|
+
| `@nebula-web/*` 来源 | `workspace:*`(monorepo 软链) | `latest`(GitLab Package Registry) |
|
|
15
|
+
| 部署管道 | 主应用统一 build | 自带 `deploy/build-deploy.sh` |
|
|
16
|
+
| 适用场景 | 平台公共域(auth / system) | 业务域 |
|
|
17
|
+
| 修改 `@nebula-web/*` | 直接改源码即时生效 | 需在 nebula-web 改源码 → CI 发布 → `pnpm update @nebula-web/*` |
|
|
18
|
+
| 多团队协作 | 单仓库,统一 PR 流程 | 各业务团队独立仓库,权限隔离 |
|
|
19
|
+
|
|
20
|
+
> 业务子应用 **默认走独立仓库形态**。仅平台公共能力(与认证 / 系统配置绑定的)放进 monorepo。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 二、独立仓库子应用必备文件清单
|
|
25
|
+
|
|
26
|
+
按本清单创建文件,缺一个都可能导致接入失败。
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
nebula-{domain}-web/
|
|
30
|
+
├── package.json # private:true, name=nebula-{domain}-web, scripts: dev/dev:remote/build/typecheck
|
|
31
|
+
├── pnpm-lock.yaml
|
|
32
|
+
├── vite.config.ts # base/双入口/iceStarkHtmlFixPlugin/proxy
|
|
33
|
+
├── tsconfig.json
|
|
34
|
+
├── index.html # <div id="app"></div> + <script src="/src/main.ts">
|
|
35
|
+
├── .env.development # 本地内网 IP 后端
|
|
36
|
+
├── .env.remote # 联调线上域名后端
|
|
37
|
+
├── .env.production # 生产 API_BASE
|
|
38
|
+
├── .npmrc # GitLab Package Registry + auth token
|
|
39
|
+
├── .gitignore # 见 [[init-new-project]] §六-.gitignore
|
|
40
|
+
├── deploy/
|
|
41
|
+
│ ├── build-deploy.sh # macOS/Linux
|
|
42
|
+
│ ├── build-deploy.bat # Windows CMD
|
|
43
|
+
│ └── build-deploy.ps1 # Windows PowerShell
|
|
44
|
+
├── README.md
|
|
45
|
+
└── src/
|
|
46
|
+
├── main.ts # setLibraryName + mount/unmount + isInIcestark
|
|
47
|
+
├── App.vue # 仅 RouterView
|
|
48
|
+
├── api/index.ts # createRequest('/nebula-{domain}')
|
|
49
|
+
├── router/routes.ts # 路由(不含 /{domain}app 前缀,由 getBasename 注入)
|
|
50
|
+
├── pages/
|
|
51
|
+
├── stores/
|
|
52
|
+
└── styles/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 三、关键配置详解
|
|
58
|
+
|
|
59
|
+
### 3.1 package.json
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"name": "nebula-{domain}-web",
|
|
64
|
+
"version": "0.1.0",
|
|
65
|
+
"private": true,
|
|
66
|
+
"type": "module",
|
|
67
|
+
"scripts": {
|
|
68
|
+
"dev": "vite --mode development --host 0.0.0.0 --port {PORT}",
|
|
69
|
+
"dev:remote": "vite --mode remote --host 0.0.0.0 --port {PORT}",
|
|
70
|
+
"build": "vite build",
|
|
71
|
+
"typecheck": "vue-tsc --noEmit",
|
|
72
|
+
"update:nebula": "pnpm update @nebula-web/components @nebula-web/types @nebula-web/utils"
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@ice/stark-app": "^1.5.0",
|
|
76
|
+
"@ice/stark-data": "^0.1.3",
|
|
77
|
+
"@nebula-web/components": "latest",
|
|
78
|
+
"@nebula-web/types": "latest",
|
|
79
|
+
"@nebula-web/utils": "latest",
|
|
80
|
+
"axios": "^1.7.0",
|
|
81
|
+
"element-plus": "^2.8.0",
|
|
82
|
+
"pinia": "^2.2.0",
|
|
83
|
+
"vue": "^3.5.0",
|
|
84
|
+
"vue-router": "^4.4.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
关键约束:
|
|
90
|
+
- ✅ `"private": true` 防止意外发布
|
|
91
|
+
- ✅ `"name": "nebula-{domain}-web"`(kebab-case 全小写)
|
|
92
|
+
- ✅ 端口必须避开 `3000` / 已被占用的子应用端口(参考主应用 `.env.development`)
|
|
93
|
+
|
|
94
|
+
### 3.2 vite.config.ts 三大要素
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
98
|
+
import { defineConfig, loadEnv } from 'vite'
|
|
99
|
+
import { resolve } from 'path'
|
|
100
|
+
import { readFileSync, writeFileSync } from 'fs'
|
|
101
|
+
import vue from '@vitejs/plugin-vue'
|
|
102
|
+
import AutoImport from 'unplugin-auto-import/vite'
|
|
103
|
+
import Components from 'unplugin-vue-components/vite'
|
|
104
|
+
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
|
105
|
+
import compression from 'vite-plugin-compression'
|
|
106
|
+
import type { Plugin } from 'vite'
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 修复 IceStark loadScriptMode:'import' 时 HTML 引用错误 facade 的问题。
|
|
110
|
+
* 同时把 index.html 和 src/main.ts 作为 Rollup 入口时,
|
|
111
|
+
* - index-xxx.js:HTML 用的 facade(无 mount/unmount 导出)
|
|
112
|
+
* - main-xxx.js :main.ts 用的 facade(有 export { mount, unmount })
|
|
113
|
+
* 默认 HTML 注入 index-xxx.js,IceStark import() 该文件拿不到导出 → 白屏。
|
|
114
|
+
* 此插件在 writeBundle 阶段把 HTML 里的 index facade 替换为 main facade。
|
|
115
|
+
*/
|
|
116
|
+
function iceStarkHtmlFixPlugin(): Plugin {
|
|
117
|
+
return {
|
|
118
|
+
name: 'icestark-html-fix',
|
|
119
|
+
apply: 'build',
|
|
120
|
+
writeBundle(options, bundle) {
|
|
121
|
+
let indexFacade: string | null = null
|
|
122
|
+
let mainFacade: string | null = null
|
|
123
|
+
for (const chunk of Object.values(bundle)) {
|
|
124
|
+
if (chunk.type === 'chunk' && chunk.isEntry) {
|
|
125
|
+
if (chunk.name === 'index') indexFacade = chunk.fileName
|
|
126
|
+
if (chunk.name === 'main') mainFacade = chunk.fileName
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!indexFacade || !mainFacade) return
|
|
130
|
+
const outDir = options.dir ?? 'dist'
|
|
131
|
+
const htmlPath = resolve(outDir, 'index.html')
|
|
132
|
+
try {
|
|
133
|
+
let html = readFileSync(htmlPath, 'utf-8')
|
|
134
|
+
html = html.replace(indexFacade, mainFacade)
|
|
135
|
+
writeFileSync(htmlPath, html)
|
|
136
|
+
console.log(`[icestark-html-fix] ${indexFacade} → ${mainFacade}`)
|
|
137
|
+
} catch (_e) { /* ignore */ }
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default defineConfig(({ mode }) => {
|
|
143
|
+
const env = loadEnv(mode, process.cwd(), '')
|
|
144
|
+
const isProd = mode === 'production'
|
|
145
|
+
const needRewrite = env.VITE_PROXY_REWRITE !== 'false'
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
// 要素一:base 与 Nginx location 对齐
|
|
149
|
+
base: isProd ? '/{domain}app/' : '/',
|
|
150
|
+
plugins: [
|
|
151
|
+
vue(),
|
|
152
|
+
// 要素二:iceStarkHtmlFixPlugin
|
|
153
|
+
iceStarkHtmlFixPlugin(),
|
|
154
|
+
...(isProd ? [compression({ ext: '.gz', algorithm: 'gzip', threshold: 10240, level: 9 })] : []),
|
|
155
|
+
AutoImport({
|
|
156
|
+
imports: ['vue', 'vue-router', 'pinia'],
|
|
157
|
+
resolvers: [ElementPlusResolver()],
|
|
158
|
+
dts: 'src/types/auto-imports.d.ts',
|
|
159
|
+
}),
|
|
160
|
+
Components({
|
|
161
|
+
resolvers: [ElementPlusResolver()],
|
|
162
|
+
dts: 'src/types/components.d.ts',
|
|
163
|
+
}),
|
|
164
|
+
],
|
|
165
|
+
resolve: {
|
|
166
|
+
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
|
|
167
|
+
},
|
|
168
|
+
// @ice/stark-data 为 CommonJS,预构建后才能正确解析 named export
|
|
169
|
+
optimizeDeps: { include: ['@ice/stark-data'] },
|
|
170
|
+
server: {
|
|
171
|
+
port: {PORT},
|
|
172
|
+
host: '127.0.0.1',
|
|
173
|
+
cors: true,
|
|
174
|
+
// 允许主应用跨域加载子应用资源
|
|
175
|
+
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
176
|
+
proxy: {
|
|
177
|
+
'/nebula-{domain}': {
|
|
178
|
+
target: env.VITE_{DOMAIN}_BACKEND || 'http://localhost:8080',
|
|
179
|
+
changeOrigin: true,
|
|
180
|
+
...(needRewrite ? { rewrite: (p: string) => p.replace(/^\/nebula-{domain}/, '') } : {}),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
build: {
|
|
185
|
+
target: 'es2015',
|
|
186
|
+
chunkSizeWarningLimit: 1000,
|
|
187
|
+
rollupOptions: {
|
|
188
|
+
// 要素三:双 Rollup 入口 + preserveEntrySignatures
|
|
189
|
+
input: {
|
|
190
|
+
index: fileURLToPath(new URL('./index.html', import.meta.url)),
|
|
191
|
+
main: fileURLToPath(new URL('./src/main.ts', import.meta.url)),
|
|
192
|
+
},
|
|
193
|
+
preserveEntrySignatures: 'exports-only',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**iceStarkHtmlFixPlugin 原理**:
|
|
201
|
+
- Vite 把 `index.html` 作为入口时,会生成 `index-{hash}.js`(**无导出**)和 `main-{hash}.js`(**有 mount/unmount 导出**)两个 facade
|
|
202
|
+
- 默认 HTML 引用了无导出的 `index-xxx.js`,IceStark `import()` 拿不到 mount/unmount → 白屏
|
|
203
|
+
- 此插件在 `writeBundle` 阶段把 HTML 里的 index facade 替换为 main facade
|
|
204
|
+
|
|
205
|
+
### 3.3 src/main.ts 四个钩子
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { createApp, type App as VueApp } from 'vue'
|
|
209
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
210
|
+
import { createPinia } from 'pinia'
|
|
211
|
+
import ElementPlus from 'element-plus'
|
|
212
|
+
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
213
|
+
import NebulaComponents from '@nebula-web/components'
|
|
214
|
+
import isInIcestark from '@ice/stark-app/lib/isInIcestark'
|
|
215
|
+
import getBasename from '@ice/stark-app/lib/getBasename'
|
|
216
|
+
import setLibraryName from '@ice/stark-app/lib/setLibraryName'
|
|
217
|
+
import App from './App.vue'
|
|
218
|
+
import routes from './router/routes'
|
|
219
|
+
|
|
220
|
+
// 钩子一:库名(与主应用 microAppConfig.name 完全一致)
|
|
221
|
+
setLibraryName('nebula{Domain}App')
|
|
222
|
+
|
|
223
|
+
let vueApp: VueApp | null = null
|
|
224
|
+
|
|
225
|
+
function createVueApp(basename: string) {
|
|
226
|
+
const router = createRouter({ history: createWebHistory(basename), routes })
|
|
227
|
+
const app = createApp(App)
|
|
228
|
+
app.use(createPinia())
|
|
229
|
+
.use(router)
|
|
230
|
+
.use(ElementPlus, { locale: zhCn })
|
|
231
|
+
.use(NebulaComponents)
|
|
232
|
+
return { app, router }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 钩子二:safeUnmount(处理 HMR 双挂载 / DOM 残留)
|
|
236
|
+
function safeUnmount(container?: HTMLElement) {
|
|
237
|
+
if (vueApp) { vueApp.unmount(); vueApp = null }
|
|
238
|
+
if (container && (container as any).__vue_app__) {
|
|
239
|
+
;(container as any).__vue_app__.unmount()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 钩子三:mount
|
|
244
|
+
export function mount({ container }: { container: HTMLElement }) {
|
|
245
|
+
safeUnmount(container)
|
|
246
|
+
const { app } = createVueApp(getBasename()) // → '/{domain}app'
|
|
247
|
+
vueApp = app
|
|
248
|
+
app.mount(container)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 钩子四:unmount
|
|
252
|
+
export function unmount() { safeUnmount() }
|
|
253
|
+
|
|
254
|
+
// 生产 Docker 环境 fallback
|
|
255
|
+
;(window as any).nebula{Domain}App = { mount, unmount }
|
|
256
|
+
|
|
257
|
+
// 独立运行模式(本地纯 UI 调试,不依赖主应用)
|
|
258
|
+
if (!isInIcestark()) {
|
|
259
|
+
const { app } = createVueApp('/')
|
|
260
|
+
app.mount('#app')
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 3.4 三套 .env
|
|
265
|
+
|
|
266
|
+
**.env.development**(pnpm dev,本地内网 IP 后端)
|
|
267
|
+
```env
|
|
268
|
+
VITE_{DOMAIN}_API_BASE=
|
|
269
|
+
VITE_{DOMAIN}_BACKEND=http://localhost:8080
|
|
270
|
+
VITE_PROXY_REWRITE=true
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**.env.remote**(pnpm dev:remote,联调线上)
|
|
274
|
+
```env
|
|
275
|
+
VITE_{DOMAIN}_API_BASE=
|
|
276
|
+
VITE_{DOMAIN}_BACKEND=https://{your-host-domain}
|
|
277
|
+
VITE_PROXY_REWRITE=false
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**.env.production**
|
|
281
|
+
```env
|
|
282
|
+
VITE_{DOMAIN}_API_BASE=/nebula-{domain}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**.npmrc**
|
|
286
|
+
```ini
|
|
287
|
+
@nebula-web:registry=https://gitlab.{your-gitlab-host}/api/v4/packages/npm/
|
|
288
|
+
//gitlab.{your-gitlab-host}/api/v4/packages/npm/:_authToken=${GITLAB_TOKEN}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 3.5 src/api/index.ts
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { createRequest } from '@nebula-web/utils'
|
|
295
|
+
|
|
296
|
+
// 空字符串时回退到 /nebula-{domain},由 proxy 转发
|
|
297
|
+
export const {domain}Request = createRequest(
|
|
298
|
+
import.meta.env.VITE_{DOMAIN}_API_BASE || '/nebula-{domain}'
|
|
299
|
+
)
|
|
300
|
+
export const systemRequest = createRequest(
|
|
301
|
+
import.meta.env.VITE_{DOMAIN}_API_BASE || '/nebula-system'
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`createRequest` 内置:JWT 注入、`X-Tenant-Id` 注入、401 静默刷新、`R<T>` 解包。**不要自己 axios.create**。
|
|
306
|
+
|
|
307
|
+
### 3.6 index.html(极简)
|
|
308
|
+
|
|
309
|
+
```html
|
|
310
|
+
<!DOCTYPE html>
|
|
311
|
+
<html lang="zh-CN">
|
|
312
|
+
<head>
|
|
313
|
+
<meta charset="UTF-8" />
|
|
314
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
315
|
+
<title>nebula-{domain}-web</title>
|
|
316
|
+
</head>
|
|
317
|
+
<body>
|
|
318
|
+
<div id="app"></div>
|
|
319
|
+
<script type="module" src="/src/main.ts"></script>
|
|
320
|
+
</body>
|
|
321
|
+
</html>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 四、主应用注册步骤
|
|
327
|
+
|
|
328
|
+
修改 `nebula-web/apps/nebula-web-main/src/layout/tabs/microAppConfig.ts`,追加:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const microApps = [
|
|
332
|
+
// ... 已有
|
|
333
|
+
{
|
|
334
|
+
name: '{domain}app', // ← 与 setLibraryName 去掉前缀和大写后一致
|
|
335
|
+
activePath: '/{domain}app',
|
|
336
|
+
title: '{业务中文名}',
|
|
337
|
+
entry: 'VITE_MICRO_APP_{DOMAIN}',
|
|
338
|
+
},
|
|
339
|
+
]
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
修改 `nebula-web/apps/nebula-web-main/.env.development`:
|
|
343
|
+
```env
|
|
344
|
+
VITE_MICRO_APP_{DOMAIN}={PORT}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
修改 `nebula-web/apps/nebula-web-main/.env.production`:
|
|
348
|
+
```env
|
|
349
|
+
VITE_MICRO_APP_{DOMAIN}=/{domain}app/
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 五、本地联调(两个仓库同时启动)
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# 终端 1:主应用
|
|
358
|
+
cd /path/to/nebula-web
|
|
359
|
+
pnpm dev # 主应用 :3000 + 内置 auth/system 子应用
|
|
360
|
+
|
|
361
|
+
# 终端 2:业务子应用
|
|
362
|
+
cd /path/to/nebula-{domain}-web
|
|
363
|
+
pnpm dev # :{PORT}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
打开 `http://127.0.0.1:3000/` → 登录 → 菜单点击「{业务中文名}」→ 主应用通过 IceStark `import('http://127.0.0.1:{PORT}/')` 加载子应用。
|
|
367
|
+
|
|
368
|
+
**两端 mode 必须一致**:主应用 `pnpm dev:remote` 时子应用也必须 `pnpm dev:remote`,否则后端代理目标不一致。
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## 六、与主应用通信
|
|
373
|
+
|
|
374
|
+
### 6.1 共享状态(store)
|
|
375
|
+
|
|
376
|
+
主应用在登录 / 切换租户后通过 `@ice/stark-data` 推送:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// 主应用(已实现,子应用直接读即可)
|
|
380
|
+
import { store } from '@ice/stark-data'
|
|
381
|
+
store.set('nebulaToken', accessToken)
|
|
382
|
+
store.set('nebulaUser', userInfo)
|
|
383
|
+
store.set('nebulaPermissions', ['xxx:read', 'xxx:write'])
|
|
384
|
+
store.set('nebulaRoles', ['admin'])
|
|
385
|
+
store.set('nebulaTenantId', tenantId)
|
|
386
|
+
store.set('nebulaAuthBaseURL', '/nebula-auth')
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
子应用读取:
|
|
390
|
+
```typescript
|
|
391
|
+
import { store } from '@ice/stark-data'
|
|
392
|
+
const token = store.get('nebulaToken')
|
|
393
|
+
const perms = store.get('nebulaPermissions') ?? []
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
> 实际开发中,子应用基本不需要直接读 store —— `@nebula-web/utils` 的 `createRequest` 已经在拦截器里自动从 store 拿 token。
|
|
397
|
+
|
|
398
|
+
### 6.2 事件(event)
|
|
399
|
+
|
|
400
|
+
子应用主动通知主应用:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { event } from '@ice/stark-data'
|
|
404
|
+
|
|
405
|
+
event.emit('401ToLogin', 'token 已失效') // 跳登录
|
|
406
|
+
event.emit('toLogout') // 主动登出
|
|
407
|
+
event.emit('routerPush', '/dashboard') // 主应用路由跳转
|
|
408
|
+
event.emit('setGlobalLoading', true) // 全局 loading
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 七、命名一致性约束(最易翻车)⚠️
|
|
414
|
+
|
|
415
|
+
下面五处命名必须严格对齐,**任一不一致都会导致 IceStark 加载白屏**:
|
|
416
|
+
|
|
417
|
+
| 位置 | 取值规则 | 示例(`{domain}=mes`) |
|
|
418
|
+
|------|---------|-----------------------|
|
|
419
|
+
| 子应用 `src/main.ts` | `setLibraryName('nebula{PascalDomain}App')` | `setLibraryName('nebulaMesApp')` |
|
|
420
|
+
| 主应用 `microAppConfig.ts` | `name: '{domain}app'`(剥除前缀 / 全小写) | `name: 'mesapp'` |
|
|
421
|
+
| 主应用 `microAppConfig.ts` | `activePath: '/{domain}app'` | `activePath: '/mesapp'` |
|
|
422
|
+
| 子应用 `vite.config.ts` | `base: isProd ? '/{domain}app/' : '/'` | `base: '/mesapp/'` |
|
|
423
|
+
| 生产 Nginx | `location /{domain}app/ { ... }` | `location /mesapp/` |
|
|
424
|
+
| 主应用 `.env.production` | `VITE_MICRO_APP_{DOMAIN}=/{domain}app/` | `VITE_MICRO_APP_MES=/mesapp/` |
|
|
425
|
+
|
|
426
|
+
派生约定:
|
|
427
|
+
- `{PascalDomain}` = 首字母大写(`mes` → `Mes`,`scm` → `Scm`)
|
|
428
|
+
- `{domain}app` = 全小写
|
|
429
|
+
- `{DOMAIN}` = 全大写
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 八、部署
|
|
434
|
+
|
|
435
|
+
### 8.1 deploy/build-deploy.sh 骨架
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
#!/bin/bash
|
|
439
|
+
# 编译并发布到 Nginx 服务器
|
|
440
|
+
set -e
|
|
441
|
+
|
|
442
|
+
SERVER_HOST="${SERVER_HOST:-}" # 必填:通过环境变量传入
|
|
443
|
+
SERVER_USER="${SERVER_USER:-ecs-user}"
|
|
444
|
+
SERVER_PORT="${SERVER_PORT:-22}"
|
|
445
|
+
REMOTE_HTML_BASE="${REMOTE_HTML_BASE:-/data/service/nginx/html/nebula-web/{domain}}"
|
|
446
|
+
|
|
447
|
+
if [ -z "$SERVER_HOST" ]; then
|
|
448
|
+
echo "❌ 请设置 SERVER_HOST 环境变量"
|
|
449
|
+
exit 1
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# 1. 检查工具
|
|
453
|
+
command -v pnpm >/dev/null || { echo "❌ pnpm 未安装"; exit 1; }
|
|
454
|
+
command -v scp >/dev/null || { echo "❌ scp 未安装"; exit 1; }
|
|
455
|
+
command -v ssh >/dev/null || { echo "❌ ssh 未安装"; exit 1; }
|
|
456
|
+
|
|
457
|
+
# 2. 安装依赖(可选 -u 更新 @nebula-web/*)
|
|
458
|
+
if [ "$1" = "-u" ]; then
|
|
459
|
+
export GITLAB_TOKEN="${GITLAB_TOKEN:?需要设置 GITLAB_TOKEN}"
|
|
460
|
+
pnpm update @nebula-web/components @nebula-web/types @nebula-web/utils
|
|
461
|
+
else
|
|
462
|
+
pnpm install
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
# 3. 类型检查(可选 -f 跳过)
|
|
466
|
+
[ "$1" != "-f" ] && pnpm typecheck
|
|
467
|
+
|
|
468
|
+
# 4. 编译
|
|
469
|
+
pnpm build
|
|
470
|
+
|
|
471
|
+
# 5. 上传
|
|
472
|
+
ssh -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "mkdir -p $REMOTE_HTML_BASE"
|
|
473
|
+
scp -P "$SERVER_PORT" -r dist/* "$SERVER_USER@$SERVER_HOST:$REMOTE_HTML_BASE/"
|
|
474
|
+
|
|
475
|
+
echo "✅ 已发布到 $SERVER_HOST:$REMOTE_HTML_BASE"
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 8.2 Nginx 配置
|
|
479
|
+
|
|
480
|
+
```nginx
|
|
481
|
+
location /{domain}app/ {
|
|
482
|
+
alias /data/service/nginx/html/nebula-web/{domain}/;
|
|
483
|
+
try_files $uri $uri/ /{domain}app/index.html;
|
|
484
|
+
gzip_static on;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# 后端代理(与开发 proxy 路径一致)
|
|
488
|
+
location /nebula-{domain}/ {
|
|
489
|
+
proxy_pass http://{backend-upstream}/;
|
|
490
|
+
proxy_set_header Authorization $http_authorization;
|
|
491
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 九、避坑清单(本主题专属)
|
|
498
|
+
|
|
499
|
+
- ⚠️ `setLibraryName` 与 `microAppConfig.name` 不一致 → 主应用 `import()` 后拿不到 mount/unmount → 白屏
|
|
500
|
+
- ⚠️ `vite.config.ts` 漏掉双 Rollup 入口或 `preserveEntrySignatures: 'exports-only'` → 同上
|
|
501
|
+
- ⚠️ 漏掉 `iceStarkHtmlFixPlugin` → 生产构建后 HTML 引用错误 facade → 白屏
|
|
502
|
+
- ⚠️ `optimizeDeps.include` 没加 `@ice/stark-data` → 开发模式 named export 解析失败
|
|
503
|
+
- ⚠️ `main.ts` 的 `mount` 没 safeUnmount → HMR 后双挂载 / DOM 残留
|
|
504
|
+
- ⚠️ 业务代码直接用 `axios.create` → 应使用 `@nebula-web/utils` 的 `createRequest`
|
|
505
|
+
- ⚠️ 直接读 `localStorage.getItem('token')` → 应通过 `@ice/stark-data` store 或 `@nebula-web/utils` 的 token helper
|
|
506
|
+
- ⚠️ `.npmrc` 没配 `GITLAB_TOKEN` → `pnpm install` 拉不到 `@nebula-web/*`
|
|
507
|
+
- ⚠️ 主子应用 mode 不一致(一边 dev,一边 dev:remote)→ 接口请求落到错误后端
|
|
508
|
+
- ⚠️ 子应用 `index.html` 标题、`README.md`、`package.json` 描述、UI 字面量中残留其他业务样板字眼 → 必须替换为当前业务名
|
|
509
|
+
|
|
510
|
+
详尽避坑参考 [pitfalls-checklist.md](pitfalls-checklist.md) §G。
|