@finesoft/front 0.1.15 → 0.1.17
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 +758 -135
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,219 +1,842 @@
|
|
|
1
1
|
# @finesoft/front
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@finesoft/front` 是一个**聚合型全栈框架包**:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 用 `@finesoft/core` 负责 **路由 / Intent / Controller / DI / Framework**
|
|
6
|
+
- 用 `@finesoft/browser` 负责 **客户端启动、导航、hydrate**
|
|
7
|
+
- 用 `@finesoft/ssr` 负责 **SSR 渲染与数据注入**
|
|
8
|
+
- 用 `@finesoft/server` 负责 **Hono + Vite + 部署适配器**
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- **DI Container** — lightweight dependency injection for controllers and services
|
|
9
|
-
- **Actions & Intents** — declarative navigation model (FlowAction, ExternalUrlAction)
|
|
10
|
-
- **SSR** — server-side rendering pipeline with data serialization
|
|
11
|
-
- **Server** — one-shot factory for Hono + Vite + SSR (Node / Deno / Bun)
|
|
12
|
-
- **Browser Runtime** — app bootstrap, action handlers, history management
|
|
13
|
-
- **Browser Export Condition** — browser builds exclude server-only code automatically
|
|
10
|
+
如果你想做一个“**URL → Intent → Controller → Page Model → SSR + Hydration**”的内容型站点,这个包就是把整条链路打包好了。
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
这份 README 不讲空话,直接按“**你现在要怎么接入**”来写。照着做,基本就能跑起来。
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 先搞清楚:这个包适合什么项目
|
|
17
|
+
|
|
18
|
+
它更适合下面这类应用:
|
|
19
|
+
|
|
20
|
+
- 内容展示站点
|
|
21
|
+
- App Store / Media / 资讯 / 榜单 / 搜索 / 详情页
|
|
22
|
+
- 需要 SSR 的站点
|
|
23
|
+
- URL 和页面状态关系清晰的项目
|
|
24
|
+
- 希望把“页面逻辑”收敛到 Controller,而不是散落在 UI 组件里的项目
|
|
25
|
+
|
|
26
|
+
如果你的项目是:
|
|
27
|
+
|
|
28
|
+
- 纯客户端小工具
|
|
29
|
+
- 页面非常少
|
|
30
|
+
- 不需要 SSR / SEO
|
|
31
|
+
|
|
32
|
+
也能用,但可能有点“杀鸡用框架刀”。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 这个包实际导出了什么
|
|
37
|
+
|
|
38
|
+
`@finesoft/front` 不是单一模块,而是下面几个能力的统一入口。
|
|
39
|
+
|
|
40
|
+
### Core
|
|
41
|
+
|
|
42
|
+
- `Framework`
|
|
43
|
+
- `Router`
|
|
44
|
+
- `Container`
|
|
45
|
+
- `BaseController`
|
|
46
|
+
- `defineRoutes`
|
|
47
|
+
- `ActionDispatcher`
|
|
48
|
+
- `IntentDispatcher`
|
|
49
|
+
- `HttpClient`
|
|
50
|
+
- `LruMap`
|
|
51
|
+
- `buildUrl`
|
|
52
|
+
|
|
53
|
+
### Browser
|
|
54
|
+
|
|
55
|
+
- `startBrowserApp`
|
|
56
|
+
- `History`
|
|
57
|
+
- `registerActionHandlers`
|
|
58
|
+
- `registerFlowActionHandler`
|
|
59
|
+
- `registerExternalUrlHandler`
|
|
60
|
+
- `deserializeServerData`
|
|
61
|
+
- `createPrefetchedIntentsFromDom`
|
|
62
|
+
- `tryScroll`
|
|
63
|
+
|
|
64
|
+
### SSR
|
|
65
|
+
|
|
66
|
+
- `createSSRRender`
|
|
67
|
+
- `ssrRender`
|
|
68
|
+
- `injectSSRContent`
|
|
69
|
+
- `serializeServerData`
|
|
70
|
+
- `SSR_PLACEHOLDERS`
|
|
71
|
+
|
|
72
|
+
### Server / Deployment
|
|
73
|
+
|
|
74
|
+
- `createServer`
|
|
75
|
+
- `createSSRApp`
|
|
76
|
+
- `startServer`
|
|
77
|
+
- `parseAcceptLanguage`
|
|
78
|
+
- `detectRuntime`
|
|
79
|
+
- `resolveRoot`
|
|
80
|
+
- `finesoftFrontViteConfig`
|
|
81
|
+
- `nodeAdapter`
|
|
82
|
+
- `vercelAdapter`
|
|
83
|
+
- `cloudflareAdapter`
|
|
84
|
+
- `netlifyAdapter`
|
|
85
|
+
- `staticAdapter`
|
|
86
|
+
- `autoAdapter`
|
|
87
|
+
- `resolveAdapter`
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 三种接入方式,怎么选
|
|
92
|
+
|
|
93
|
+
### 方案 A:**推荐**,直接用 Vite 插件一把梭
|
|
94
|
+
|
|
95
|
+
适合绝大多数项目。
|
|
96
|
+
|
|
97
|
+
你会得到:
|
|
98
|
+
|
|
99
|
+
- `vite`:开发
|
|
100
|
+
- `vite build`:客户端 + SSR + adapter 一起构建
|
|
101
|
+
- `vite preview`:本地预览 SSR 产物
|
|
102
|
+
|
|
103
|
+
入口是:`finesoftFrontViteConfig()`
|
|
104
|
+
|
|
105
|
+
> 如果你没有特殊原因,优先选这个。它是现在这套代码里最顺手的工作流。
|
|
106
|
+
|
|
107
|
+
### 方案 B:手动用 `createServer()`
|
|
108
|
+
|
|
109
|
+
适合:
|
|
110
|
+
|
|
111
|
+
- 你不想依赖 Vite 插件生命周期
|
|
112
|
+
- 你已经有自己的服务启动逻辑
|
|
113
|
+
- 你要手工控制 Hono/Vite/SSR 的集成方式
|
|
114
|
+
|
|
115
|
+
### 方案 C:纯浏览器使用
|
|
116
|
+
|
|
117
|
+
适合:
|
|
118
|
+
|
|
119
|
+
- 不做 SSR
|
|
120
|
+
- 只想复用 Router / Framework / Action / Controller 这套模型
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 安装
|
|
125
|
+
|
|
126
|
+
### 只做浏览器端
|
|
18
127
|
|
|
19
128
|
```bash
|
|
20
|
-
|
|
129
|
+
pnpm add @finesoft/front
|
|
21
130
|
```
|
|
22
131
|
|
|
23
|
-
### SSR /
|
|
132
|
+
### 做 SSR / Vite / 部署适配
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pnpm add @finesoft/front hono @hono/node-server vite
|
|
136
|
+
```
|
|
24
137
|
|
|
25
|
-
|
|
138
|
+
### 如果你希望 `createServer()` 自动加载 `.env`
|
|
26
139
|
|
|
27
140
|
```bash
|
|
28
|
-
|
|
141
|
+
pnpm add dotenv
|
|
29
142
|
```
|
|
30
143
|
|
|
31
|
-
###
|
|
144
|
+
### 关于 peerDependencies
|
|
32
145
|
|
|
33
|
-
|
|
34
|
-
- `vite` — required for development SSR server usage
|
|
35
|
-
- `@hono/node-server` — required for Node.js server startup
|
|
36
|
-
- `dotenv` — optional, only needed if you want `.env` auto-loading in `createServer()`
|
|
146
|
+
这个包当前依赖这些 peer:
|
|
37
147
|
|
|
38
|
-
|
|
148
|
+
- `hono`
|
|
149
|
+
- `@hono/node-server`
|
|
150
|
+
- `vite`
|
|
39
151
|
|
|
40
|
-
|
|
152
|
+
也就是说:**浏览器-only 项目**不一定都要装全;但只要你用了 SSR / server / Vite 插件,就请把它们装上。
|
|
41
153
|
|
|
42
|
-
|
|
43
|
-
- In SSR / Node environments, the default import resolves to the full entry with browser + SSR + server exports.
|
|
154
|
+
---
|
|
44
155
|
|
|
45
|
-
|
|
156
|
+
## 浏览器与服务端入口行为
|
|
157
|
+
|
|
158
|
+
`@finesoft/front` 带有 `browser` export condition。
|
|
159
|
+
|
|
160
|
+
这意味着:
|
|
161
|
+
|
|
162
|
+
- 浏览器构建时,导入 `@finesoft/front` 会自动走 **browser-only entry**
|
|
163
|
+
- Node / SSR 环境下,导入 `@finesoft/front` 会走 **完整入口**
|
|
164
|
+
|
|
165
|
+
所以大多数情况下你可以放心写:
|
|
46
166
|
|
|
47
167
|
```ts
|
|
48
168
|
import { startBrowserApp } from "@finesoft/front";
|
|
49
169
|
```
|
|
50
170
|
|
|
51
|
-
|
|
171
|
+
现代 bundler 会尽量避免把服务端代码拖进浏览器包里。
|
|
52
172
|
|
|
53
|
-
|
|
173
|
+
---
|
|
54
174
|
|
|
55
|
-
|
|
175
|
+
## 最推荐的用法:Vite + SSR + Hydration + Adapter
|
|
56
176
|
|
|
57
|
-
|
|
58
|
-
import { createServer } from "@finesoft/front";
|
|
177
|
+
下面是一个最小可工作的接入方式。
|
|
59
178
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
179
|
+
### 第 1 步:准备文件结构
|
|
180
|
+
|
|
181
|
+
建议至少有这些文件:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
src/
|
|
185
|
+
browser.ts
|
|
186
|
+
ssr.ts
|
|
187
|
+
lib/
|
|
188
|
+
bootstrap.ts
|
|
189
|
+
controllers/
|
|
190
|
+
home-controller.ts
|
|
191
|
+
index.html
|
|
192
|
+
vite.config.ts
|
|
69
193
|
```
|
|
70
194
|
|
|
71
|
-
|
|
195
|
+
如果你还要注册 API 代理或自定义 Hono 路由,再加一个:
|
|
72
196
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
197
|
+
```text
|
|
198
|
+
src/proxies.ts
|
|
199
|
+
```
|
|
76
200
|
|
|
77
|
-
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### 第 2 步:写 `index.html`
|
|
204
|
+
|
|
205
|
+
你的 HTML 模板**必须包含**这四个 SSR 占位符:
|
|
206
|
+
|
|
207
|
+
```html
|
|
208
|
+
<!doctype html>
|
|
209
|
+
<html lang="<!--ssr-lang-->">
|
|
210
|
+
<head>
|
|
211
|
+
<meta charset="utf-8" />
|
|
212
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
213
|
+
<!--ssr-head-->
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
<div id="app"><!--ssr-body--></div>
|
|
217
|
+
<!--ssr-data-->
|
|
218
|
+
<script type="module" src="/src/browser.ts"></script>
|
|
219
|
+
</body>
|
|
220
|
+
</html>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
它们的含义如下:
|
|
224
|
+
|
|
225
|
+
| 占位符 | 会被替换成什么 |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `<!--ssr-lang-->` | 当前语言,如 `en` / `zh` |
|
|
228
|
+
| `<!--ssr-head-->` | `<head>` 内容和样式 |
|
|
229
|
+
| `<!--ssr-body-->` | 服务端渲染后的 HTML |
|
|
230
|
+
| `<!--ssr-data-->` | 序列化后的 server data 脚本 |
|
|
231
|
+
|
|
232
|
+
如果少一个,SSR 体验就会开始闹脾气。
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### 第 3 步:写路由和 Controller 注册
|
|
237
|
+
|
|
238
|
+
推荐用 `defineRoutes()`,把 URL 和 Controller 写在同一个数组里。
|
|
239
|
+
|
|
240
|
+
`src/lib/bootstrap.ts`:
|
|
78
241
|
|
|
79
242
|
```ts
|
|
80
|
-
import {
|
|
243
|
+
import {
|
|
244
|
+
BaseController,
|
|
245
|
+
Container,
|
|
246
|
+
Framework,
|
|
247
|
+
defineRoutes,
|
|
248
|
+
type RouteDefinition,
|
|
249
|
+
} from "@finesoft/front";
|
|
250
|
+
|
|
251
|
+
class HomeController extends BaseController<Record<string, string>, { title: string }> {
|
|
252
|
+
readonly intentId = "home-page";
|
|
253
|
+
|
|
254
|
+
async execute(_params: Record<string, string>, _container: Container) {
|
|
255
|
+
return { title: "Home" };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
81
258
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
controller: new ProductController(),
|
|
89
|
-
},
|
|
90
|
-
]);
|
|
259
|
+
const routes: RouteDefinition[] = [
|
|
260
|
+
{ path: "/", intentId: "home-page", controller: new HomeController() },
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
export function bootstrap(framework: Framework): void {
|
|
264
|
+
defineRoutes(framework, routes);
|
|
91
265
|
}
|
|
92
266
|
|
|
267
|
+
// 如果你会使用 static adapter,请务必把 routes 导出出来
|
|
268
|
+
export { routes };
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### 为什么 `static` 模式一定要导出 `routes`
|
|
272
|
+
|
|
273
|
+
因为 `staticAdapter()` 会在构建时去读取你的路由定义,自动预渲染无参数页面。
|
|
274
|
+
|
|
275
|
+
如果不导出,静态构建就没法知道应该预渲染哪些 URL。
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### 第 4 步:写浏览器入口 `src/browser.ts`
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { startBrowserApp } from "@finesoft/front";
|
|
283
|
+
import { bootstrap } from "$lib/bootstrap";
|
|
284
|
+
|
|
93
285
|
startBrowserApp({
|
|
94
286
|
bootstrap,
|
|
287
|
+
defaultLocale: "en",
|
|
95
288
|
mountId: "app",
|
|
96
289
|
mount: (target, { framework, locale }) => {
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
290
|
+
// 这里接入你的 UI 框架(Svelte / React / Vue 都行)
|
|
291
|
+
// 返回一个 update 函数,后续导航时会调用它
|
|
292
|
+
return ({ page, isFirstPage }) => {
|
|
293
|
+
void target;
|
|
294
|
+
void framework;
|
|
295
|
+
void locale;
|
|
296
|
+
void page;
|
|
297
|
+
void isFirstPage;
|
|
100
298
|
};
|
|
101
299
|
},
|
|
102
300
|
callbacks: {
|
|
103
|
-
|
|
104
|
-
|
|
301
|
+
onNavigate(pathname) {
|
|
302
|
+
console.log("navigated:", pathname);
|
|
303
|
+
},
|
|
304
|
+
onModal(page) {
|
|
305
|
+
console.log("open modal:", page);
|
|
306
|
+
},
|
|
105
307
|
},
|
|
106
308
|
});
|
|
107
309
|
```
|
|
108
310
|
|
|
109
|
-
|
|
311
|
+
这里有两个很重要的回调:
|
|
312
|
+
|
|
313
|
+
- `onNavigate(pathname)`:正常页面跳转后调用
|
|
314
|
+
- `onModal(page)`:当 `FlowAction` 以 modal 方式展示时调用
|
|
110
315
|
|
|
111
|
-
|
|
112
|
-
- `locale` is resolved from `document.documentElement.lang` first, then falls back to `defaultLocale`
|
|
113
|
-
- `startBrowserApp()` automatically reads prefetched server data from the DOM and performs the initial route action
|
|
316
|
+
---
|
|
114
317
|
|
|
115
|
-
### SSR
|
|
318
|
+
### 第 5 步:写 SSR 入口 `src/ssr.ts`
|
|
116
319
|
|
|
117
320
|
```ts
|
|
118
321
|
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
322
|
+
import { bootstrap } from "$lib/bootstrap";
|
|
119
323
|
|
|
120
324
|
export const render = createSSRRender({
|
|
121
325
|
bootstrap,
|
|
122
|
-
getErrorPage
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
326
|
+
getErrorPage(status, message) {
|
|
327
|
+
return {
|
|
328
|
+
id: `error-${status}`,
|
|
329
|
+
pageType: "error",
|
|
330
|
+
title: message,
|
|
331
|
+
statusCode: status,
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
renderApp(page, locale) {
|
|
335
|
+
// 这里接入你的 SSR 渲染函数
|
|
336
|
+
// 比如 Svelte 的 render() / ReactDOMServer / Vue SSR
|
|
337
|
+
void page;
|
|
338
|
+
void locale;
|
|
339
|
+
return {
|
|
340
|
+
html: "",
|
|
341
|
+
head: "",
|
|
342
|
+
css: "",
|
|
343
|
+
};
|
|
126
344
|
},
|
|
127
345
|
});
|
|
346
|
+
|
|
128
347
|
export { serializeServerData };
|
|
129
348
|
```
|
|
130
349
|
|
|
131
|
-
`createSSRRender()`
|
|
350
|
+
`createSSRRender()` 最终会生成一个:
|
|
132
351
|
|
|
133
|
-
|
|
352
|
+
```ts
|
|
353
|
+
(url: string, locale: string) => Promise<SSRRenderResult>
|
|
354
|
+
```
|
|
134
355
|
|
|
135
|
-
|
|
356
|
+
这个签名正好就是 server / Vite 插件所需要的 SSR module 形状。
|
|
136
357
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
</body>
|
|
148
|
-
</html>
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### 第 6 步:如果你有 API 代理,写 `src/proxies.ts`
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import type { Hono } from "hono";
|
|
364
|
+
|
|
365
|
+
export default function setup(app: Hono) {
|
|
366
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
367
|
+
}
|
|
149
368
|
```
|
|
150
369
|
|
|
151
|
-
|
|
152
|
-
| ----------------- | ------------------------------------------ |
|
|
153
|
-
| `<!--ssr-lang-->` | Locale string (e.g. `en`) |
|
|
154
|
-
| `<!--ssr-head-->` | `<head>` content + CSS |
|
|
155
|
-
| `<!--ssr-body-->` | Server-rendered HTML |
|
|
156
|
-
| `<!--ssr-data-->` | `<script>` tag with serialized server data |
|
|
370
|
+
这里推荐导出 `default` 函数。
|
|
157
371
|
|
|
158
|
-
|
|
372
|
+
`setup` 有两种传法:
|
|
159
373
|
|
|
160
|
-
`
|
|
374
|
+
- 传函数:只在 `dev` / `preview` 时可用
|
|
375
|
+
- 传文件路径字符串:`dev` / `build` / `preview` / adapter 都可用
|
|
161
376
|
|
|
162
|
-
|
|
377
|
+
如果你要部署到 adapter 环境里,**优先传文件路径字符串**。
|
|
163
378
|
|
|
164
|
-
|
|
379
|
+
---
|
|
165
380
|
|
|
166
|
-
|
|
381
|
+
### 第 7 步:配置 `vite.config.ts`
|
|
167
382
|
|
|
168
|
-
|
|
383
|
+
```ts
|
|
384
|
+
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
385
|
+
import { defineConfig } from "vite";
|
|
386
|
+
|
|
387
|
+
export default defineConfig({
|
|
388
|
+
plugins: [
|
|
389
|
+
finesoftFrontViteConfig({
|
|
390
|
+
locales: ["zh", "en"],
|
|
391
|
+
defaultLocale: "en",
|
|
392
|
+
ssr: { entry: "src/ssr.ts" },
|
|
393
|
+
setup: "src/proxies.ts",
|
|
394
|
+
adapter: "node",
|
|
395
|
+
}),
|
|
396
|
+
],
|
|
397
|
+
});
|
|
398
|
+
```
|
|
169
399
|
|
|
170
|
-
|
|
400
|
+
`adapter` 支持:
|
|
171
401
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
402
|
+
- `"node"`
|
|
403
|
+
- `"vercel"`
|
|
404
|
+
- `"cloudflare"`
|
|
405
|
+
- `"netlify"`
|
|
406
|
+
- `"static"`
|
|
407
|
+
- `"auto"`
|
|
408
|
+
- 或者一个自定义 `Adapter` 对象
|
|
175
409
|
|
|
176
|
-
|
|
410
|
+
---
|
|
177
411
|
|
|
178
|
-
###
|
|
412
|
+
### 第 8 步:配置脚本
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"scripts": {
|
|
417
|
+
"dev": "vite",
|
|
418
|
+
"build": "vite build",
|
|
419
|
+
"preview": "vite preview"
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
### 第 9 步:启动项目
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
pnpm dev
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
构建:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
pnpm build
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
本地 SSR 预览:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
pnpm preview
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Adapter 指南:构建后到底会产出什么
|
|
447
|
+
|
|
448
|
+
这一节非常重要,因为很多人一到部署阶段就开始“咦,我文件呢?”。
|
|
449
|
+
|
|
450
|
+
### `adapter: "node"`
|
|
451
|
+
|
|
452
|
+
输出:
|
|
453
|
+
|
|
454
|
+
- `dist/server/index.mjs`
|
|
455
|
+
|
|
456
|
+
运行方式:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
node dist/server/index.mjs
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
适合:
|
|
463
|
+
|
|
464
|
+
- 自己管 Node 进程
|
|
465
|
+
- Docker
|
|
466
|
+
- VPS
|
|
467
|
+
- PM2
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### `adapter: "vercel"`
|
|
472
|
+
|
|
473
|
+
输出:
|
|
474
|
+
|
|
475
|
+
- `.vercel/output/config.json`
|
|
476
|
+
- `.vercel/output/static/`
|
|
477
|
+
- `.vercel/output/functions/ssr.func/`
|
|
478
|
+
|
|
479
|
+
注意:
|
|
179
480
|
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
481
|
+
- **它不在 `dist/` 里面,这是正常的**
|
|
482
|
+
- 这是 Vercel Build Output API v3 的约定目录
|
|
483
|
+
- 不是框架任性,是平台规定就这么放
|
|
484
|
+
|
|
485
|
+
建议把 `.vercel/` 加进 `.gitignore`
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### `adapter: "netlify"`
|
|
490
|
+
|
|
491
|
+
输出:
|
|
492
|
+
|
|
493
|
+
- `.netlify/functions-internal/ssr/index.mjs`
|
|
494
|
+
- `dist/client/_redirects`
|
|
495
|
+
|
|
496
|
+
注意:
|
|
497
|
+
|
|
498
|
+
- **`.netlify/` 在 `dist/` 外面也是正常的**
|
|
499
|
+
- 这是 Netlify Functions 的约定目录
|
|
500
|
+
- 真正的 publish 目录通常是 `dist/client/`
|
|
501
|
+
|
|
502
|
+
同样建议把 `.netlify/` 加进 `.gitignore`
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### `adapter: "cloudflare"`
|
|
507
|
+
|
|
508
|
+
输出:
|
|
509
|
+
|
|
510
|
+
- `dist/cloudflare/_worker.js`
|
|
511
|
+
- `dist/cloudflare/assets/`
|
|
512
|
+
|
|
513
|
+
注意:
|
|
514
|
+
|
|
515
|
+
- Cloudflare Workers 环境不是完整 Node.js
|
|
516
|
+
- 如果你的 `setup` 或运行时代码依赖 Node API,可能需要 `nodejs_compat`
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### `adapter: "static"`
|
|
521
|
+
|
|
522
|
+
输出:
|
|
523
|
+
|
|
524
|
+
- `dist/static/`
|
|
525
|
+
|
|
526
|
+
适合:
|
|
527
|
+
|
|
528
|
+
- 纯静态托管
|
|
529
|
+
- CDN / OSS / Pages
|
|
530
|
+
- 不依赖运行时服务端逻辑的页面
|
|
531
|
+
|
|
532
|
+
它会做的事:
|
|
533
|
+
|
|
534
|
+
1. 从你的 `routes` 导出中读取路由
|
|
535
|
+
2. 自动预渲染**无参数路由**
|
|
536
|
+
3. 复制客户端静态资源
|
|
537
|
+
4. 输出纯 HTML/CSS/JS 站点
|
|
538
|
+
|
|
539
|
+
#### `static` 模式的三个重要限制
|
|
540
|
+
|
|
541
|
+
##### 1)只会自动预渲染无参数路由
|
|
542
|
+
|
|
543
|
+
比如这些会自动生成:
|
|
544
|
+
|
|
545
|
+
- `/`
|
|
546
|
+
- `/search`
|
|
547
|
+
- `/games`
|
|
548
|
+
|
|
549
|
+
这些不会自动生成:
|
|
550
|
+
|
|
551
|
+
- `/product/:id`
|
|
552
|
+
- `/list/:category`
|
|
553
|
+
|
|
554
|
+
如果你要预渲染动态 URL,请补充具体地址,例如:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
import { staticAdapter } from "@finesoft/front";
|
|
558
|
+
|
|
559
|
+
finesoftFrontViteConfig({
|
|
560
|
+
adapter: staticAdapter({
|
|
561
|
+
dynamicRoutes: ["/product/123", "/list/games"],
|
|
562
|
+
}),
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
##### 2)构建时必须能拿到页面数据
|
|
567
|
+
|
|
568
|
+
`static` 预渲染是**构建期执行 Controller**。
|
|
569
|
+
|
|
570
|
+
这意味着:
|
|
571
|
+
|
|
572
|
+
- 如果你的 Controller 会 `fetch` API
|
|
573
|
+
- 而构建时没有 API 服务
|
|
574
|
+
- 那么页面就会渲染失败或退化成错误页
|
|
575
|
+
|
|
576
|
+
解决方式有两个:
|
|
577
|
+
|
|
578
|
+
- 构建时保证 API 可访问
|
|
579
|
+
- 给 Controller 的 `fallback()` 提供 mock 数据回退
|
|
580
|
+
|
|
581
|
+
##### 3)预览静态产物时,要看的是 `dist/static/`
|
|
582
|
+
|
|
583
|
+
`vite preview` 更适合看标准 Vite 构建产物和 SSR 预览。
|
|
584
|
+
|
|
585
|
+
如果你要验证 `static` 最终结果,请直接服务 `dist/static/`,例如:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
cd dist/static
|
|
589
|
+
python3 -m http.server 3000
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
### `adapter: "auto"`
|
|
595
|
+
|
|
596
|
+
会根据环境变量自动选择:
|
|
597
|
+
|
|
598
|
+
- `VERCEL` → `vercel`
|
|
599
|
+
- `CF_PAGES` → `cloudflare`
|
|
600
|
+
- `NETLIFY` → `netlify`
|
|
601
|
+
- 默认 → `node`
|
|
602
|
+
|
|
603
|
+
适合 CI / 平台自动识别场景。
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## 如果你想自己写 Adapter
|
|
608
|
+
|
|
609
|
+
可以直接传对象:
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
import type { Adapter } from "@finesoft/front";
|
|
613
|
+
|
|
614
|
+
const customAdapter: Adapter = {
|
|
615
|
+
name: "my-platform",
|
|
616
|
+
async build(ctx) {
|
|
617
|
+
// ctx 里有 root / vite / fs / path / templateHtml
|
|
618
|
+
// 也有 generateSSREntry / buildBundle / copyStaticAssets 等工具
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
然后:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
finesoftFrontViteConfig({
|
|
627
|
+
adapter: customAdapter,
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## 手动模式:直接用 `createServer()`
|
|
634
|
+
|
|
635
|
+
如果你不想依赖 Vite 插件,也可以直接起服务器。
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
import { createServer } from "@finesoft/front";
|
|
639
|
+
|
|
640
|
+
const { app, vite, runtime } = await createServer({
|
|
641
|
+
root: process.cwd(),
|
|
642
|
+
locales: ["zh", "en"],
|
|
643
|
+
defaultLocale: "en",
|
|
644
|
+
port: 3000,
|
|
645
|
+
setup(app) {
|
|
646
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
647
|
+
},
|
|
648
|
+
ssr: {
|
|
649
|
+
ssrEntryPath: "/src/ssr.ts",
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
void app;
|
|
654
|
+
void vite;
|
|
655
|
+
void runtime;
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### `createServer()` 会帮你做什么
|
|
659
|
+
|
|
660
|
+
1. 解析根目录
|
|
661
|
+
2. 如果存在 `.env`,尝试加载它
|
|
662
|
+
3. 检测当前运行时
|
|
663
|
+
4. 开发模式下创建 Vite middleware server
|
|
664
|
+
5. 创建 Hono app
|
|
665
|
+
6. 先挂你的业务路由
|
|
666
|
+
7. 再挂 SSR catch-all
|
|
667
|
+
8. 启动服务
|
|
668
|
+
|
|
669
|
+
### `createServer()` 默认行为
|
|
670
|
+
|
|
671
|
+
- `root` 默认是 `process.cwd()`
|
|
672
|
+
- `port` 默认是 `process.env.PORT ?? 3000`
|
|
673
|
+
- 如果根目录存在 `.env` 且安装了 `dotenv`,会自动尝试加载
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## 纯浏览器模式
|
|
678
|
+
|
|
679
|
+
如果你只想要 Framework / Router / Action / Controller,不做 SSR,也能单独使用。
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
import { Framework, defineRoutes, startBrowserApp } from "@finesoft/front";
|
|
683
|
+
|
|
684
|
+
function bootstrap(framework: Framework) {
|
|
685
|
+
defineRoutes(framework, [
|
|
686
|
+
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
687
|
+
]);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
startBrowserApp({
|
|
691
|
+
bootstrap,
|
|
692
|
+
mount: (target) => {
|
|
693
|
+
return ({ page }) => {
|
|
694
|
+
void target;
|
|
695
|
+
void page;
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
callbacks: {
|
|
699
|
+
onNavigate() {},
|
|
700
|
+
onModal() {},
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## 你最常会用到的 API
|
|
708
|
+
|
|
709
|
+
### 路由与 Intent
|
|
710
|
+
|
|
711
|
+
- `defineRoutes()`:声明式注册路由
|
|
712
|
+
- `Framework.routeUrl()`:把 URL 匹配成 Intent
|
|
713
|
+
- `Framework.dispatch()`:执行 Intent
|
|
714
|
+
- `BaseController`:页面控制器基类
|
|
715
|
+
|
|
716
|
+
### 浏览器启动
|
|
717
|
+
|
|
718
|
+
- `startBrowserApp()`:一站式 hydration 启动
|
|
719
|
+
- `registerActionHandlers()`:注册 action handlers
|
|
720
|
+
- `History`:浏览器导航状态管理
|
|
188
721
|
|
|
189
722
|
### SSR
|
|
190
723
|
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
724
|
+
- `createSSRRender()`:生成 `render(url, locale)`
|
|
725
|
+
- `serializeServerData()`:序列化服务端数据
|
|
726
|
+
- `injectSSRContent()`:把 SSR 内容注入 HTML 模板
|
|
727
|
+
|
|
728
|
+
### 服务端 / 部署
|
|
729
|
+
|
|
730
|
+
- `createServer()`:手动启动 SSR 服务
|
|
731
|
+
- `finesoftFrontViteConfig()`:推荐用的 Vite 插件入口
|
|
732
|
+
- `nodeAdapter()` / `vercelAdapter()` / `cloudflareAdapter()` / `netlifyAdapter()` / `staticAdapter()` / `autoAdapter()`
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## 常见坑位(强烈建议先看)
|
|
737
|
+
|
|
738
|
+
### 1. `index.html` 没放 SSR 占位符
|
|
739
|
+
|
|
740
|
+
症状:
|
|
741
|
+
|
|
742
|
+
- 页面不 hydrate
|
|
743
|
+
- SSR 内容丢失
|
|
744
|
+
- server data 不进页面
|
|
745
|
+
|
|
746
|
+
解决:检查这四个占位符是否都在:
|
|
747
|
+
|
|
748
|
+
- `<!--ssr-lang-->`
|
|
749
|
+
- `<!--ssr-head-->`
|
|
750
|
+
- `<!--ssr-body-->`
|
|
751
|
+
- `<!--ssr-data-->`
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
### 2. `setup` 传了函数,但部署产物里没有生效
|
|
756
|
+
|
|
757
|
+
原因:
|
|
758
|
+
|
|
759
|
+
- 直接传函数只适合 `dev/preview`
|
|
760
|
+
- adapter 构建期没法可靠地把内联函数带进去
|
|
761
|
+
|
|
762
|
+
解决:
|
|
763
|
+
|
|
764
|
+
- 用文件路径字符串,例如 `setup: "src/proxies.ts"`
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
### 3. `static` 模式下动态路由没有页面
|
|
769
|
+
|
|
770
|
+
原因:
|
|
771
|
+
|
|
772
|
+
- `staticAdapter()` 只会自动预渲染无参数路由
|
|
773
|
+
|
|
774
|
+
解决:
|
|
775
|
+
|
|
776
|
+
- 用 `dynamicRoutes` 传入具体 URL
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
### 4. `static` 模式构建出来是 500 错误页
|
|
781
|
+
|
|
782
|
+
原因:
|
|
783
|
+
|
|
784
|
+
- 构建时 Controller 调 API 失败
|
|
785
|
+
- 但 `fallback()` 只返回错误页,没有 mock 数据
|
|
786
|
+
|
|
787
|
+
解决:
|
|
788
|
+
|
|
789
|
+
- 构建时保证 API 可访问,或者
|
|
790
|
+
- 给 Controller 增加 mock fallback
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
### 5. `.vercel/` 和 `.netlify/` 为什么不在 `dist/`
|
|
795
|
+
|
|
796
|
+
答案:
|
|
797
|
+
|
|
798
|
+
- **这是平台规范,不是 bug**
|
|
799
|
+
- Vercel 要求 `.vercel/output/`
|
|
800
|
+
- Netlify 要求 `.netlify/functions-internal/`
|
|
801
|
+
|
|
802
|
+
建议:
|
|
803
|
+
|
|
804
|
+
- 把它们加入 `.gitignore`
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## 一个更完整的推荐心智模型
|
|
809
|
+
|
|
810
|
+
你可以把它理解成下面这条链路:
|
|
811
|
+
|
|
812
|
+
$$
|
|
813
|
+
URL \rightarrow Router \rightarrow Intent \rightarrow Controller \rightarrow Page\ Model \rightarrow SSR / Browser\ UI
|
|
814
|
+
$$
|
|
815
|
+
|
|
816
|
+
也就是说:
|
|
817
|
+
|
|
818
|
+
- URL 负责定位页面语义
|
|
819
|
+
- Intent 负责描述“我要什么页面”
|
|
820
|
+
- Controller 负责拉数据和组装页面模型
|
|
821
|
+
- UI 层只负责把 `Page` 渲染出来
|
|
822
|
+
|
|
823
|
+
这也是这套框架最值钱的地方:**页面逻辑和 UI 框架解耦**。
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## 最后给一个实战建议
|
|
828
|
+
|
|
829
|
+
如果你第一次接这个包,建议按下面顺序推进:
|
|
830
|
+
|
|
831
|
+
1. 先只做一个 `/` 首页
|
|
832
|
+
2. 跑通 `browser.ts` + `ssr.ts` + `index.html`
|
|
833
|
+
3. 再接 `finesoftFrontViteConfig()`
|
|
834
|
+
4. 开发阶段先用 `adapter: "node"`
|
|
835
|
+
5. 最后再切 `vercel` / `netlify` / `cloudflare` / `static`
|
|
836
|
+
|
|
837
|
+
别一上来就同时搞多语言、动态路由、SSR、静态预渲染、平台部署。那样很容易把自己卷成一个调试陀螺。
|
|
838
|
+
|
|
839
|
+
---
|
|
217
840
|
|
|
218
841
|
## License
|
|
219
842
|
|