@finesoft/front 0.1.16 → 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 CHANGED
@@ -1,219 +1,842 @@
1
1
  # @finesoft/front
2
2
 
3
- Full-stack framework for building content-driven web applications with SSR support.
3
+ `@finesoft/front` 是一个**聚合型全栈框架包**:
4
4
 
5
- ## Features
5
+ - 用 `@finesoft/core` 负责 **路由 / Intent / Controller / DI / Framework**
6
+ - 用 `@finesoft/browser` 负责 **客户端启动、导航、hydrate**
7
+ - 用 `@finesoft/ssr` 负责 **SSR 渲染与数据注入**
8
+ - 用 `@finesoft/server` 负责 **Hono + Vite + 部署适配器**
6
9
 
7
- - **Router** file-system-style route definitions with intent-driven navigation
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
- ## Install
12
+ 这份 README 不讲空话,直接按“**你现在要怎么接入**”来写。照着做,基本就能跑起来。
16
13
 
17
- ### Browser-only usage
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
- npm install @finesoft/front
129
+ pnpm add @finesoft/front
21
130
  ```
22
131
 
23
- ### SSR / full-stack usage
132
+ ### SSR / Vite / 部署适配
133
+
134
+ ```bash
135
+ pnpm add @finesoft/front hono @hono/node-server vite
136
+ ```
24
137
 
25
- If you use `createServer()` or other server exports, install the peer dependencies too:
138
+ ### 如果你希望 `createServer()` 自动加载 `.env`
26
139
 
27
140
  ```bash
28
- npm install @finesoft/front hono vite @hono/node-server dotenv
141
+ pnpm add dotenv
29
142
  ```
30
143
 
31
- ### Peer dependencies
144
+ ### 关于 peerDependencies
32
145
 
33
- - `hono` — required for server usage
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
- ## Package entry behavior
148
+ - `hono`
149
+ - `@hono/node-server`
150
+ - `vite`
39
151
 
40
- `@finesoft/front` ships a browser-aware export map.
152
+ 也就是说:**浏览器-only 项目**不一定都要装全;但只要你用了 SSR / server / Vite 插件,就请把它们装上。
41
153
 
42
- - In browser bundles, the `browser` condition resolves to the browser-only entry and excludes server code.
43
- - In SSR / Node environments, the default import resolves to the full entry with browser + SSR + server exports.
154
+ ---
44
155
 
45
- That means you can keep importing from:
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
- and modern bundlers will avoid pulling in `createServer`, `startServer`, and other server-only code on the client side.
171
+ 现代 bundler 会尽量避免把服务端代码拖进浏览器包里。
52
172
 
53
- ## Quick Start
173
+ ---
54
174
 
55
- ### Server
175
+ ## 最推荐的用法:Vite + SSR + Hydration + Adapter
56
176
 
57
- ```ts
58
- import { createServer } from "@finesoft/front";
177
+ 下面是一个最小可工作的接入方式。
59
178
 
60
- const { app } = await createServer({
61
- locales: ["en-US", "zh-CN"],
62
- defaultLocale: "en-US",
63
- port: 3000,
64
- ssr: { ssrEntryPath: "/src/ssr.ts" },
65
- setup(app) {
66
- // register custom routes on the Hono app
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
- Notes:
195
+ 如果你还要注册 API 代理或自定义 Hono 路由,再加一个:
72
196
 
73
- - `root` defaults to `process.cwd()`
74
- - `port` defaults to `process.env.PORT ?? 3000`
75
- - if a `.env` file exists at the project root, `createServer()` will try to load it automatically
197
+ ```text
198
+ src/proxies.ts
199
+ ```
76
200
 
77
- ### Browser
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 { startBrowserApp, Framework, defineRoutes } from "@finesoft/front";
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
- function bootstrap(framework: Framework) {
83
- defineRoutes(framework, [
84
- { path: "/", intentId: "home-page", controller: new HomeController() },
85
- {
86
- path: "/app/:id",
87
- intentId: "product-page",
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
- // mount your app (Svelte / React / Vue)
98
- return ({ page }) => {
99
- /* update on navigation */
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
- onNavigationStart: () => {},
104
- onNavigationEnd: () => {},
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
- Notes:
311
+ 这里有两个很重要的回调:
312
+
313
+ - `onNavigate(pathname)`:正常页面跳转后调用
314
+ - `onModal(page)`:当 `FlowAction` 以 modal 方式展示时调用
110
315
 
111
- - `mountId` defaults to `"app"`
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 Entry
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: (status, message) => ({ title: message }),
123
- renderApp: (page, locale) => {
124
- // render your app to HTML
125
- return { html: "", head: "", css: "" };
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()` returns a `render(url, locale)` function compatible with the SSR module shape expected by `createServer()`.
350
+ `createSSRRender()` 最终会生成一个:
132
351
 
133
- ### HTML Template
352
+ ```ts
353
+ (url: string, locale: string) => Promise<SSRRenderResult>
354
+ ```
134
355
 
135
- Your `index.html` must include SSR placeholders:
356
+ 这个签名正好就是 server / Vite 插件所需要的 SSR module 形状。
136
357
 
137
- ```html
138
- <!DOCTYPE html>
139
- <html lang="<!--ssr-lang-->">
140
- <head>
141
- <!--ssr-head-->
142
- </head>
143
- <body>
144
- <div id="app"><!--ssr-body--></div>
145
- <!--ssr-data-->
146
- <script type="module" src="/src/browser.ts"></script>
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
- | Placeholder | Replaced with |
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
- These are also available as `SSR_PLACEHOLDERS.LANG`, `.HEAD`, `.BODY`, `.DATA` constants.
372
+ `setup` 有两种传法:
159
373
 
160
- `injectSSRContent()` automatically wraps serialized server data in a `<script>` tag for the `<!--ssr-data-->` placeholder.
374
+ - 传函数:只在 `dev` / `preview` 时可用
375
+ - 传文件路径字符串:`dev` / `build` / `preview` / adapter 都可用
161
376
 
162
- ## Common patterns
377
+ 如果你要部署到 adapter 环境里,**优先传文件路径字符串**。
163
378
 
164
- ### Browser-only app
379
+ ---
165
380
 
166
- Use `startBrowserApp()` together with your framework mount callback. Browser bundlers will resolve the browser-only export automatically.
381
+ ### 7 步:配置 `vite.config.ts`
167
382
 
168
- ### Full SSR app
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
- Use the three pieces together:
400
+ `adapter` 支持:
171
401
 
172
- 1. `createServer()` — dev/prod server bootstrap
173
- 2. `createSSRRender()` — SSR render function factory
174
- 3. `startBrowserApp()` — client hydration
402
+ - `"node"`
403
+ - `"vercel"`
404
+ - `"cloudflare"`
405
+ - `"netlify"`
406
+ - `"static"`
407
+ - `"auto"`
408
+ - 或者一个自定义 `Adapter` 对象
175
409
 
176
- ## Selected exports
410
+ ---
177
411
 
178
- ### Browser
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
- - `startBrowserApp`
181
- - `History`
182
- - `registerActionHandlers`
183
- - `registerExternalUrlHandler`
184
- - `registerFlowActionHandler`
185
- - `deserializeServerData`
186
- - `createPrefetchedIntentsFromDom`
187
- - `tryScroll`
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
- - `createSSRRender`
192
- - `ssrRender`
193
- - `injectSSRContent`
194
- - `serializeServerData`
195
- - `SSR_PLACEHOLDERS`
196
-
197
- ### Server
198
-
199
- - `createServer`
200
- - `createSSRApp`
201
- - `startServer`
202
- - `parseAcceptLanguage`
203
- - `detectRuntime`
204
- - `resolveRoot`
205
-
206
- ## API Overview
207
-
208
- | Category | Key Exports |
209
- | -------- | ------------------------------------------------------------------------------- |
210
- | Core | `Framework`, `Router`, `Container`, `defineRoutes`, `BaseController` |
211
- | Actions | `ActionDispatcher`, `isFlowAction`, `isExternalUrlAction`, `makeFlowAction` |
212
- | HTTP | `HttpClient`, `HttpError` |
213
- | Browser | `startBrowserApp`, `History`, `registerActionHandlers`, `deserializeServerData` |
214
- | SSR | `createSSRRender`, `ssrRender`, `serializeServerData`, `injectSSRContent` |
215
- | Server | `createServer`, `createSSRApp`, `startServer`, `parseAcceptLanguage` |
216
- | Utils | `LruMap`, `buildUrl`, `pipe`, `pipeAsync`, `mapEach` |
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