@finesoft/front 0.1.17 → 0.1.18

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.
Files changed (2) hide show
  1. package/README.md +408 -263
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,41 +1,42 @@
1
1
  # @finesoft/front
2
2
 
3
- `@finesoft/front` 是一个**聚合型全栈框架包**:
3
+ `@finesoft/front` 是一个面向 SSR Web 应用的聚合包,统一导出了以下四层能力:
4
4
 
5
- - `@finesoft/core` 负责 **路由 / Intent / Controller / DI / Framework**
6
- - `@finesoft/browser` 负责 **客户端启动、导航、hydrate**
7
- - `@finesoft/ssr` 负责 **SSR 渲染与数据注入**
8
- - `@finesoft/server` 负责 **Hono + Vite + 部署适配器**
5
+ - `@finesoft/core`:路由、IntentController、依赖注入、Framework
6
+ - `@finesoft/browser`:浏览器启动、导航、hydrate、prefetched data
7
+ - `@finesoft/ssr`:SSR 渲染与服务端数据注入
8
+ - `@finesoft/server`:Hono 服务端集成、Vite 插件、部署适配器
9
9
 
10
- 如果你想做一个“**URL → Intent → Controller → Page Model → SSR + Hydration**”的内容型站点,这个包就是把整条链路打包好了。
10
+ 它适合这样一类应用:
11
11
 
12
- 这份 README 不讲空话,直接按“**你现在要怎么接入**”来写。照着做,基本就能跑起来。
12
+ $$
13
+ URL \rightarrow Router \rightarrow Intent \rightarrow Controller \rightarrow Page\ Model \rightarrow SSR / Hydration
14
+ $$
15
+
16
+ 也就是说:URL 决定页面语义,Controller 负责取数和组装页面模型,UI 层只负责渲染页面模型。
13
17
 
14
18
  ---
15
19
 
16
- ## 先搞清楚:这个包适合什么项目
20
+ ## 适用场景
17
21
 
18
- 它更适合下面这类应用:
22
+ `@finesoft/front` 更适合以下类型的项目:
19
23
 
20
- - 内容展示站点
21
- - App Store / Media / 资讯 / 榜单 / 搜索 / 详情页
22
- - 需要 SSR 的站点
23
- - URL 和页面状态关系清晰的项目
24
- - 希望把“页面逻辑”收敛到 Controller,而不是散落在 UI 组件里的项目
24
+ - 需要 SSR 的内容型站点
25
+ - 有明确 URL 语义的多页面 Web 应用
26
+ - 希望将页面获取逻辑集中在 Controller 中的项目
27
+ - 需要同一套页面模型同时服务 SSR 和客户端导航的项目
25
28
 
26
- 如果你的项目是:
29
+ 例如:
27
30
 
28
- - 纯客户端小工具
29
- - 页面非常少
30
- - 不需要 SSR / SEO
31
+ - 内容聚合站点
32
+ - 应用商店、媒体展示、排行榜、搜索、详情页
33
+ - 需要 SEO 的展示型前端
31
34
 
32
- 也能用,但可能有点“杀鸡用框架刀”。
35
+ 如果你的项目非常轻量、完全不需要 SSR,也可以只使用其中的 Browser/Core 能力。
33
36
 
34
37
  ---
35
38
 
36
- ## 这个包实际导出了什么
37
-
38
- `@finesoft/front` 不是单一模块,而是下面几个能力的统一入口。
39
+ ## 主要导出
39
40
 
40
41
  ### Core
41
42
 
@@ -88,48 +89,45 @@
88
89
 
89
90
  ---
90
91
 
91
- ## 三种接入方式,怎么选
92
+ ## 选择哪种接入方式
92
93
 
93
- ### 方案 A:**推荐**,直接用 Vite 插件一把梭
94
+ ### 方式 A:使用 `finesoftFrontViteConfig()`
94
95
 
95
- 适合绝大多数项目。
96
+ 这是推荐方式,适合大多数项目。
96
97
 
97
- 你会得到:
98
+ 优点:
98
99
 
99
- - `vite`:开发
100
- - `vite build`:客户端 + SSR + adapter 一起构建
101
- - `vite preview`:本地预览 SSR 产物
100
+ - `vite` 可直接用于开发
101
+ - `vite build` 会同时完成客户端与 SSR 构建
102
+ - 可以直接接入平台适配器输出部署产物
103
+ - `vite preview` 可用于本地预览 SSR 构建结果
102
104
 
103
- 入口是:`finesoftFrontViteConfig()`
105
+ ### 方式 B:手动使用 `createServer()`
104
106
 
105
- > 如果你没有特殊原因,优先选这个。它是现在这套代码里最顺手的工作流。
107
+ 适合以下情况:
106
108
 
107
- ### 方案 B:手动用 `createServer()`
109
+ - 你需要完全控制服务启动流程
110
+ - 你已经有自定义的 Hono / Node 集成方式
111
+ - 你不希望依赖 Vite 插件生命周期
108
112
 
109
- 适合:
113
+ ### 方式 C:仅使用 Browser/Core
110
114
 
111
- - 你不想依赖 Vite 插件生命周期
112
- - 你已经有自己的服务启动逻辑
113
- - 你要手工控制 Hono/Vite/SSR 的集成方式
115
+ 适合以下情况:
114
116
 
115
- ### 方案 C:纯浏览器使用
116
-
117
- 适合:
118
-
119
- - 不做 SSR
120
- - 只想复用 Router / Framework / Action / Controller 这套模型
117
+ - 你不需要 SSR
118
+ - 你只希望复用 Router / Intent / Controller / Framework 模型
121
119
 
122
120
  ---
123
121
 
124
122
  ## 安装
125
123
 
126
- ### 只做浏览器端
124
+ ### 仅浏览器端使用
127
125
 
128
126
  ```bash
129
127
  pnpm add @finesoft/front
130
128
  ```
131
129
 
132
- ### SSR / Vite / 部署适配
130
+ ### SSR / Vite / Adapter 使用
133
131
 
134
132
  ```bash
135
133
  pnpm add @finesoft/front hono @hono/node-server vite
@@ -141,44 +139,42 @@ pnpm add @finesoft/front hono @hono/node-server vite
141
139
  pnpm add dotenv
142
140
  ```
143
141
 
144
- ### 关于 peerDependencies
145
-
146
- 这个包当前依赖这些 peer:
142
+ ### 当前 peer dependencies
147
143
 
148
144
  - `hono`
149
145
  - `@hono/node-server`
150
146
  - `vite`
151
147
 
152
- 也就是说:**浏览器-only 项目**不一定都要装全;但只要你用了 SSR / server / Vite 插件,就请把它们装上。
148
+ 如果你只使用浏览器侧能力,不一定需要全部安装;如果你使用 SSR、Server Vite 插件,则建议全部安装。
153
149
 
154
150
  ---
155
151
 
156
- ## 浏览器与服务端入口行为
152
+ ## 入口行为说明
157
153
 
158
- `@finesoft/front` 带有 `browser` export condition。
154
+ `@finesoft/front` 提供了 `browser` export condition。
159
155
 
160
156
  这意味着:
161
157
 
162
- - 浏览器构建时,导入 `@finesoft/front` 会自动走 **browser-only entry**
163
- - Node / SSR 环境下,导入 `@finesoft/front` 会走 **完整入口**
158
+ - 浏览器构建时,会优先解析 browser-only entry,避免引入服务端实现
159
+ - Node / SSR 环境下,会解析完整入口,包含 Browser、SSR、Server 全部导出
164
160
 
165
- 所以大多数情况下你可以放心写:
161
+ 因此大多数情况下你可以直接这样写:
166
162
 
167
163
  ```ts
168
164
  import { startBrowserApp } from "@finesoft/front";
169
165
  ```
170
166
 
171
- 现代 bundler 会尽量避免把服务端代码拖进浏览器包里。
167
+ 现代 bundler 会根据环境自动选择更合适的入口。
172
168
 
173
169
  ---
174
170
 
175
- ## 最推荐的用法:Vite + SSR + Hydration + Adapter
171
+ ## 推荐工作流:Vite + SSR + Hydration
176
172
 
177
- 下面是一个最小可工作的接入方式。
173
+ 下面是一套面向公共用户的最小接入流程。
178
174
 
179
- ### 1 步:准备文件结构
175
+ ### 1. 准备目录结构
180
176
 
181
- 建议至少有这些文件:
177
+ 建议至少包含以下文件:
182
178
 
183
179
  ```text
184
180
  src/
@@ -186,23 +182,25 @@ src/
186
182
  ssr.ts
187
183
  lib/
188
184
  bootstrap.ts
185
+ models/
186
+ page.ts
189
187
  controllers/
190
188
  home-controller.ts
191
189
  index.html
192
190
  vite.config.ts
193
191
  ```
194
192
 
195
- 如果你还要注册 API 代理或自定义 Hono 路由,再加一个:
193
+ 如果你需要注册 API 代理或额外 Hono 路由,可以增加:
196
194
 
197
195
  ```text
198
- src/proxies.ts
196
+ src/setup.ts
199
197
  ```
200
198
 
201
199
  ---
202
200
 
203
- ### 2 步:写 `index.html`
201
+ ### 2. 编写 `index.html`
204
202
 
205
- 你的 HTML 模板**必须包含**这四个 SSR 占位符:
203
+ 你的 HTML 模板必须包含以下 SSR 占位符:
206
204
 
207
205
  ```html
208
206
  <!doctype html>
@@ -220,27 +218,24 @@ src/proxies.ts
220
218
  </html>
221
219
  ```
222
220
 
223
- 它们的含义如下:
224
-
225
- | 占位符 | 会被替换成什么 |
221
+ | 占位符 | 用途 |
226
222
  | --- | --- |
227
- | `<!--ssr-lang-->` | 当前语言,如 `en` / `zh` |
228
- | `<!--ssr-head-->` | `<head>` 内容和样式 |
223
+ | `<!--ssr-lang-->` | 当前语言 |
224
+ | `<!--ssr-head-->` | SSR `<head>` 内容与样式 |
229
225
  | `<!--ssr-body-->` | 服务端渲染后的 HTML |
230
- | `<!--ssr-data-->` | 序列化后的 server data 脚本 |
231
-
232
- 如果少一个,SSR 体验就会开始闹脾气。
226
+ | `<!--ssr-data-->` | 序列化后的服务端数据 |
233
227
 
234
228
  ---
235
229
 
236
- ### 3 步:写路由和 Controller 注册
230
+ ### 3. 编写路由与 Controller 注册
237
231
 
238
- 推荐用 `defineRoutes()`,把 URL Controller 写在同一个数组里。
232
+ 推荐使用 `defineRoutes()`,把 URL Controller 声明放在同一处。
239
233
 
240
234
  `src/lib/bootstrap.ts`:
241
235
 
242
236
  ```ts
243
237
  import {
238
+ BasePage,
244
239
  BaseController,
245
240
  Container,
246
241
  Framework,
@@ -248,11 +243,16 @@ import {
248
243
  type RouteDefinition,
249
244
  } from "@finesoft/front";
250
245
 
251
- class HomeController extends BaseController<Record<string, string>, { title: string }> {
246
+ class HomeController extends BaseController<Record<string, string>, BasePage> {
252
247
  readonly intentId = "home-page";
253
248
 
254
249
  async execute(_params: Record<string, string>, _container: Container) {
255
- return { title: "Home" };
250
+ return {
251
+ id: "page-home",
252
+ pageType: "home",
253
+ title: "Home",
254
+ description: "A page rendered by @finesoft/front",
255
+ };
256
256
  }
257
257
  }
258
258
 
@@ -264,31 +264,30 @@ export function bootstrap(framework: Framework): void {
264
264
  defineRoutes(framework, routes);
265
265
  }
266
266
 
267
- // 如果你会使用 static adapter,请务必把 routes 导出出来
267
+ // 如果你计划使用 static adapter,建议导出 routes
268
268
  export { routes };
269
269
  ```
270
270
 
271
- #### 为什么 `static` 模式一定要导出 `routes`
271
+ #### 为什么 `static` 模式建议导出 `routes`
272
272
 
273
- 因为 `staticAdapter()` 会在构建时去读取你的路由定义,自动预渲染无参数页面。
273
+ `staticAdapter()` 会在构建期读取你的路由定义,并自动预渲染无参数路由。
274
274
 
275
- 如果不导出,静态构建就没法知道应该预渲染哪些 URL。
275
+ 如果没有导出 `routes`,静态导出时将无法自动发现这些页面。
276
276
 
277
277
  ---
278
278
 
279
- ### 4 步:写浏览器入口 `src/browser.ts`
279
+ ### 4. 编写浏览器入口 `src/browser.ts`
280
280
 
281
281
  ```ts
282
282
  import { startBrowserApp } from "@finesoft/front";
283
- import { bootstrap } from "$lib/bootstrap";
283
+ import { bootstrap } from "./lib/bootstrap";
284
284
 
285
285
  startBrowserApp({
286
286
  bootstrap,
287
287
  defaultLocale: "en",
288
288
  mountId: "app",
289
289
  mount: (target, { framework, locale }) => {
290
- // 这里接入你的 UI 框架(Svelte / React / Vue 都行)
291
- // 返回一个 update 函数,后续导航时会调用它
290
+ // 在这里接入你的 UI 框架(Svelte / React / Vue
292
291
  return ({ page, isFirstPage }) => {
293
292
  void target;
294
293
  void framework;
@@ -299,27 +298,22 @@ startBrowserApp({
299
298
  },
300
299
  callbacks: {
301
300
  onNavigate(pathname) {
302
- console.log("navigated:", pathname);
301
+ console.log("navigate:", pathname);
303
302
  },
304
303
  onModal(page) {
305
- console.log("open modal:", page);
304
+ console.log("modal:", page);
306
305
  },
307
306
  },
308
307
  });
309
308
  ```
310
309
 
311
- 这里有两个很重要的回调:
312
-
313
- - `onNavigate(pathname)`:正常页面跳转后调用
314
- - `onModal(page)`:当 `FlowAction` 以 modal 方式展示时调用
315
-
316
310
  ---
317
311
 
318
- ### 5 步:写 SSR 入口 `src/ssr.ts`
312
+ ### 5. 编写 SSR 入口 `src/ssr.ts`
319
313
 
320
314
  ```ts
321
315
  import { createSSRRender, serializeServerData } from "@finesoft/front";
322
- import { bootstrap } from "$lib/bootstrap";
316
+ import { bootstrap } from "./lib/bootstrap";
323
317
 
324
318
  export const render = createSSRRender({
325
319
  bootstrap,
@@ -332,8 +326,6 @@ export const render = createSSRRender({
332
326
  };
333
327
  },
334
328
  renderApp(page, locale) {
335
- // 这里接入你的 SSR 渲染函数
336
- // 比如 Svelte 的 render() / ReactDOMServer / Vue SSR
337
329
  void page;
338
330
  void locale;
339
331
  return {
@@ -347,17 +339,11 @@ export const render = createSSRRender({
347
339
  export { serializeServerData };
348
340
  ```
349
341
 
350
- `createSSRRender()` 最终会生成一个:
351
-
352
- ```ts
353
- (url: string, locale: string) => Promise<SSRRenderResult>
354
- ```
355
-
356
- 这个签名正好就是 server / Vite 插件所需要的 SSR module 形状。
342
+ `createSSRRender()` 最终会生成一个 `render(url, locale)` 函数,其返回值与服务端 SSR 模块契约一致。
357
343
 
358
344
  ---
359
345
 
360
- ### 6 步:如果你有 API 代理,写 `src/proxies.ts`
346
+ ### 6. 如果你有自定义 Hono 路由,编写 `src/setup.ts`
361
347
 
362
348
  ```ts
363
349
  import type { Hono } from "hono";
@@ -367,18 +353,18 @@ export default function setup(app: Hono) {
367
353
  }
368
354
  ```
369
355
 
370
- 这里推荐导出 `default` 函数。
356
+ 建议优先导出 `default` 函数。
371
357
 
372
- `setup` 有两种传法:
358
+ `setup` 在插件中有两种用法:
373
359
 
374
- - 传函数:只在 `dev` / `preview` 时可用
375
- - 传文件路径字符串:`dev` / `build` / `preview` / adapter 都可用
360
+ - 传入函数:适用于 `dev` / `preview`
361
+ - 传入文件路径字符串:适用于 `dev` / `build` / `preview` / adapter
376
362
 
377
- 如果你要部署到 adapter 环境里,**优先传文件路径字符串**。
363
+ 如果你需要让构建产物也包含这些路由,建议传入文件路径字符串。
378
364
 
379
365
  ---
380
366
 
381
- ### 7 步:配置 `vite.config.ts`
367
+ ### 7. 配置 `vite.config.ts`
382
368
 
383
369
  ```ts
384
370
  import { finesoftFrontViteConfig } from "@finesoft/front";
@@ -390,14 +376,14 @@ export default defineConfig({
390
376
  locales: ["zh", "en"],
391
377
  defaultLocale: "en",
392
378
  ssr: { entry: "src/ssr.ts" },
393
- setup: "src/proxies.ts",
379
+ setup: "src/setup.ts",
394
380
  adapter: "node",
395
381
  }),
396
382
  ],
397
383
  });
398
384
  ```
399
385
 
400
- `adapter` 支持:
386
+ 当前支持的 adapter
401
387
 
402
388
  - `"node"`
403
389
  - `"vercel"`
@@ -405,11 +391,11 @@ export default defineConfig({
405
391
  - `"netlify"`
406
392
  - `"static"`
407
393
  - `"auto"`
408
- - 或者一个自定义 `Adapter` 对象
394
+ - 或自定义 `Adapter` 对象
409
395
 
410
396
  ---
411
397
 
412
- ### 8 步:配置脚本
398
+ ### 8. 配置脚本
413
399
 
414
400
  ```json
415
401
  {
@@ -423,7 +409,9 @@ export default defineConfig({
423
409
 
424
410
  ---
425
411
 
426
- ### 9 步:启动项目
412
+ ### 9. 启动与构建
413
+
414
+ 开发:
427
415
 
428
416
  ```bash
429
417
  pnpm dev
@@ -435,7 +423,7 @@ pnpm dev
435
423
  pnpm build
436
424
  ```
437
425
 
438
- 本地 SSR 预览:
426
+ 本地预览:
439
427
 
440
428
  ```bash
441
429
  pnpm preview
@@ -443,9 +431,239 @@ pnpm preview
443
431
 
444
432
  ---
445
433
 
446
- ## Adapter 指南:构建后到底会产出什么
434
+ ## 完整 Svelte 示例
435
+
436
+ 下面是一套不依赖任何仓库内约定、可以独立理解的 Svelte 示例。
437
+
438
+ ### `src/lib/models/page.ts`
439
+
440
+ ```ts
441
+ import type { BasePage } from "@finesoft/front";
442
+
443
+ export interface HomePage extends BasePage {
444
+ pageType: "home";
445
+ body: string;
446
+ }
447
+
448
+ export interface ErrorPage extends BasePage {
449
+ pageType: "error";
450
+ errorMessage: string;
451
+ statusCode: number;
452
+ }
453
+
454
+ export type Page = HomePage | ErrorPage;
455
+ ```
456
+
457
+ ### `src/lib/bootstrap.ts`
458
+
459
+ ```ts
460
+ import {
461
+ BaseController,
462
+ Container,
463
+ Framework,
464
+ defineRoutes,
465
+ type RouteDefinition,
466
+ } from "@finesoft/front";
467
+ import type { ErrorPage, HomePage, Page } from "./models/page";
468
+
469
+ class HomeController extends BaseController<Record<string, string>, Page> {
470
+ readonly intentId = "home-page";
471
+
472
+ async execute(
473
+ _params: Record<string, string>,
474
+ _container: Container,
475
+ ): Promise<HomePage> {
476
+ return {
477
+ id: "page-home",
478
+ pageType: "home",
479
+ title: "Hello Svelte + Finesoft Front",
480
+ description: "A minimal SSR page rendered by Svelte",
481
+ body: "This page is rendered on the server first and hydrated on the client.",
482
+ };
483
+ }
484
+
485
+ override fallback(
486
+ _params: Record<string, string>,
487
+ error: Error,
488
+ ): ErrorPage {
489
+ return {
490
+ id: "page-error",
491
+ pageType: "error",
492
+ title: "Error",
493
+ errorMessage: error.message,
494
+ statusCode: 500,
495
+ };
496
+ }
497
+ }
498
+
499
+ const routes: RouteDefinition[] = [
500
+ { path: "/", intentId: "home-page", controller: new HomeController() },
501
+ ];
502
+
503
+ export function bootstrap(framework: Framework): void {
504
+ defineRoutes(framework, routes);
505
+ }
506
+
507
+ export { routes };
508
+ ```
509
+
510
+ ### `src/browser.ts`
511
+
512
+ ```ts
513
+ import { startBrowserApp } from "@finesoft/front";
514
+ import App from "./App.svelte";
515
+ import { bootstrap } from "./lib/bootstrap";
516
+ import type { Page } from "./lib/models/page";
517
+
518
+ startBrowserApp({
519
+ bootstrap,
520
+ defaultLocale: "en",
521
+ mount: (target, { framework, locale }) => {
522
+ const app = new App({
523
+ target,
524
+ hydrate: true,
525
+ props: {
526
+ locale,
527
+ framework,
528
+ },
529
+ });
530
+
531
+ return (props) => {
532
+ app.$set(
533
+ props as {
534
+ page: Promise<Page> | Page;
535
+ isFirstPage?: boolean;
536
+ },
537
+ );
538
+ };
539
+ },
540
+ callbacks: {
541
+ onNavigate(pathname) {
542
+ console.log("navigate:", pathname);
543
+ },
544
+ onModal(page) {
545
+ console.log("modal:", page);
546
+ },
547
+ },
548
+ });
549
+ ```
550
+
551
+ ### `src/ssr.ts`
552
+
553
+ ```ts
554
+ import { createSSRRender, serializeServerData } from "@finesoft/front";
555
+ import App from "./App.svelte";
556
+ import { bootstrap } from "./lib/bootstrap";
557
+ import type { ErrorPage, Page } from "./lib/models/page";
558
+
559
+ export { serializeServerData };
560
+
561
+ function getErrorPage(status: number, message: string): ErrorPage {
562
+ return {
563
+ id: `page-error-${status}`,
564
+ pageType: "error",
565
+ title: "Error",
566
+ errorMessage: message,
567
+ statusCode: status,
568
+ };
569
+ }
570
+
571
+ export const render = createSSRRender({
572
+ bootstrap,
573
+ getErrorPage,
574
+ renderApp(page, locale) {
575
+ const result = (App as any).render({
576
+ page: page as Page,
577
+ isFirstPage: true,
578
+ locale,
579
+ });
580
+
581
+ return {
582
+ html: result.html ?? "",
583
+ head: result.head ?? "",
584
+ css: result.css?.code ?? "",
585
+ };
586
+ },
587
+ });
588
+ ```
589
+
590
+ ### `src/App.svelte`
591
+
592
+ ```svelte
593
+ <script lang="ts">
594
+ import type { Framework } from "@finesoft/front";
595
+ import type { ErrorPage, Page } from "./lib/models/page";
596
+
597
+ export let page: Promise<Page> | Page = new Promise(() => {});
598
+ export let isFirstPage = true;
599
+ export let locale = "en";
600
+ export let framework: Framework | undefined = undefined;
601
+
602
+ $: safePage = normalizePage(page);
603
+
604
+ function normalizePage(value: Promise<Page> | Page): Promise<Page> | Page {
605
+ if (!(value instanceof Promise)) return value;
606
+
607
+ return value.catch(
608
+ (err): ErrorPage => ({
609
+ id: "page-error-runtime",
610
+ pageType: "error",
611
+ title: "Error",
612
+ errorMessage:
613
+ err instanceof Error ? err.message : "Failed to load page",
614
+ statusCode: 500,
615
+ }),
616
+ );
617
+ }
618
+
619
+ function getMessage(resolved: Page): string {
620
+ return resolved.pageType === "home"
621
+ ? resolved.body
622
+ : resolved.errorMessage;
623
+ }
624
+ </script>
625
+
626
+ <svelte:head>
627
+ <title>Finesoft Front Svelte Example</title>
628
+ <meta
629
+ name="description"
630
+ content="Minimal Svelte SSR example powered by @finesoft/front"
631
+ />
632
+ </svelte:head>
633
+
634
+ {#await safePage}
635
+ <main>
636
+ <h1>Loading...</h1>
637
+ <p>{isFirstPage ? "Preparing first page" : "Navigating"}</p>
638
+ </main>
639
+ {:then resolved}
640
+ <main>
641
+ <p>locale: {locale}</p>
642
+ <h1>{resolved.title}</h1>
643
+ <p>{getMessage(resolved)}</p>
644
+
645
+ <nav>
646
+ <a href="/">Home</a>
647
+ </nav>
648
+
649
+ {#if framework}
650
+ <p>Framework is available on the client and can be passed to child components.</p>
651
+ {/if}
652
+ </main>
653
+ {/await}
654
+ ```
655
+
656
+ ### 这个 Svelte 示例的关键点
657
+
658
+ 1. `browser.ts` 中使用 `hydrate: true`,让客户端接管 SSR HTML。
659
+ 2. `ssr.ts` 中使用 `App.render(...)`,并将 `{ html, head, css }` 返回给 `createSSRRender()`。
660
+ 3. `App.svelte` 的 `page` 同时支持 `Page` 与 `Promise<Page>`,兼容首屏渲染与客户端导航。
661
+ 4. 对 rejected promise 做兜底转换,可以将运行时错误转成可控的错误页面。
662
+ 5. 如果你希望子组件直接访问 `Framework`,可以再封装一层 Svelte context 工具。
663
+
664
+ ---
447
665
 
448
- 这一节非常重要,因为很多人一到部署阶段就开始“咦,我文件呢?”。
666
+ ## Adapter 输出说明
449
667
 
450
668
  ### `adapter: "node"`
451
669
 
@@ -461,7 +679,7 @@ node dist/server/index.mjs
461
679
 
462
680
  适合:
463
681
 
464
- - 自己管 Node 进程
682
+ - Node 服务器
465
683
  - Docker
466
684
  - VPS
467
685
  - PM2
@@ -476,13 +694,12 @@ node dist/server/index.mjs
476
694
  - `.vercel/output/static/`
477
695
  - `.vercel/output/functions/ssr.func/`
478
696
 
479
- 注意:
697
+ 说明:
480
698
 
481
- - **它不在 `dist/` 里面,这是正常的**
482
- - 这是 Vercel Build Output API v3 的约定目录
483
- - 不是框架任性,是平台规定就这么放
699
+ - 它不在 `dist/` 中,这是平台约定
700
+ - 对应 Vercel Build Output API v3
484
701
 
485
- 建议把 `.vercel/` 加进 `.gitignore`
702
+ 建议将 `.vercel/` 加入 `.gitignore`。
486
703
 
487
704
  ---
488
705
 
@@ -493,13 +710,12 @@ node dist/server/index.mjs
493
710
  - `.netlify/functions-internal/ssr/index.mjs`
494
711
  - `dist/client/_redirects`
495
712
 
496
- 注意:
713
+ 说明:
497
714
 
498
- - **`.netlify/` 在 `dist/` 外面也是正常的**
499
- - 这是 Netlify Functions 的约定目录
500
- - 真正的 publish 目录通常是 `dist/client/`
715
+ - `.netlify/` 在 `dist/` 外同样属于平台约定
716
+ - 常见发布目录是 `dist/client/`
501
717
 
502
- 同样建议把 `.netlify/` 加进 `.gitignore`
718
+ 建议将 `.netlify/` 加入 `.gitignore`。
503
719
 
504
720
  ---
505
721
 
@@ -510,10 +726,10 @@ node dist/server/index.mjs
510
726
  - `dist/cloudflare/_worker.js`
511
727
  - `dist/cloudflare/assets/`
512
728
 
513
- 注意:
729
+ 说明:
514
730
 
515
- - Cloudflare Workers 环境不是完整 Node.js
516
- - 如果你的 `setup` 或运行时代码依赖 Node API,可能需要 `nodejs_compat`
731
+ - Cloudflare Workers 不是完整 Node.js 环境
732
+ - 如果运行时代码依赖 Node API,可能需要额外兼容配置
517
733
 
518
734
  ---
519
735
 
@@ -526,32 +742,32 @@ node dist/server/index.mjs
526
742
  适合:
527
743
 
528
744
  - 纯静态托管
529
- - CDN / OSS / Pages
745
+ - CDN / 对象存储 / Pages 类平台
530
746
  - 不依赖运行时服务端逻辑的页面
531
747
 
532
- 它会做的事:
748
+ 它会执行:
533
749
 
534
- 1. 从你的 `routes` 导出中读取路由
535
- 2. 自动预渲染**无参数路由**
750
+ 1. 读取导出的路由配置
751
+ 2. 自动预渲染无参数路由
536
752
  3. 复制客户端静态资源
537
- 4. 输出纯 HTML/CSS/JS 站点
753
+ 4. 输出纯 HTML / CSS / JS 文件
538
754
 
539
- #### `static` 模式的三个重要限制
755
+ #### `static` 模式的三个注意点
540
756
 
541
757
  ##### 1)只会自动预渲染无参数路由
542
758
 
543
- 比如这些会自动生成:
759
+ 例如这些通常会自动生成:
544
760
 
545
761
  - `/`
546
762
  - `/search`
547
- - `/games`
763
+ - `/about`
548
764
 
549
- 这些不会自动生成:
765
+ 这些通常不会自动生成:
550
766
 
551
767
  - `/product/:id`
552
768
  - `/list/:category`
553
769
 
554
- 如果你要预渲染动态 URL,请补充具体地址,例如:
770
+ 如果你要预渲染动态地址,请补充具体 URL
555
771
 
556
772
  ```ts
557
773
  import { staticAdapter } from "@finesoft/front";
@@ -563,26 +779,20 @@ finesoftFrontViteConfig({
563
779
  });
564
780
  ```
565
781
 
566
- ##### 2)构建时必须能拿到页面数据
567
-
568
- `static` 预渲染是**构建期执行 Controller**。
782
+ ##### 2)构建时必须能够拿到页面数据
569
783
 
570
- 这意味着:
784
+ `static` 预渲染会在构建期执行 Controller。
571
785
 
572
- - 如果你的 Controller `fetch` API
573
- - 而构建时没有 API 服务
574
- - 那么页面就会渲染失败或退化成错误页
786
+ 如果 Controller 依赖外部 API,而构建时这些 API 不可访问,页面可能构建失败或退化为错误页。
575
787
 
576
- 解决方式有两个:
788
+ 常见解决方式:
577
789
 
578
- - 构建时保证 API 可访问
579
- - Controller `fallback()` 提供 mock 数据回退
790
+ - 构建期确保 API 可访问
791
+ - Controller 提供 fallback / mock 数据
580
792
 
581
- ##### 3)预览静态产物时,要看的是 `dist/static/`
793
+ ##### 3)验证静态产物时,应直接查看 `dist/static/`
582
794
 
583
- `vite preview` 更适合看标准 Vite 构建产物和 SSR 预览。
584
-
585
- 如果你要验证 `static` 最终结果,请直接服务 `dist/static/`,例如:
795
+ 如果你要验证静态导出的最终结果,可以直接服务这个目录:
586
796
 
587
797
  ```bash
588
798
  cd dist/static
@@ -593,20 +803,20 @@ python3 -m http.server 3000
593
803
 
594
804
  ### `adapter: "auto"`
595
805
 
596
- 会根据环境变量自动选择:
806
+ 自动识别顺序:
597
807
 
598
808
  - `VERCEL` → `vercel`
599
809
  - `CF_PAGES` → `cloudflare`
600
810
  - `NETLIFY` → `netlify`
601
811
  - 默认 → `node`
602
812
 
603
- 适合 CI / 平台自动识别场景。
813
+ 适合 CI 或平台自动识别场景。
604
814
 
605
815
  ---
606
816
 
607
- ## 如果你想自己写 Adapter
817
+ ## 自定义 Adapter
608
818
 
609
- 可以直接传对象:
819
+ 你也可以直接传入自定义 `Adapter` 对象:
610
820
 
611
821
  ```ts
612
822
  import type { Adapter } from "@finesoft/front";
@@ -614,13 +824,13 @@ import type { Adapter } from "@finesoft/front";
614
824
  const customAdapter: Adapter = {
615
825
  name: "my-platform",
616
826
  async build(ctx) {
617
- // ctx 里有 root / vite / fs / path / templateHtml
618
- // 也有 generateSSREntry / buildBundle / copyStaticAssets 等工具
827
+ // ctx 中包含 root / vite / fs / path / templateHtml
828
+ // 以及 generateSSREntry / buildBundle / copyStaticAssets 等工具方法
619
829
  },
620
830
  };
621
831
  ```
622
832
 
623
- 然后:
833
+ 使用方式:
624
834
 
625
835
  ```ts
626
836
  finesoftFrontViteConfig({
@@ -630,9 +840,9 @@ finesoftFrontViteConfig({
630
840
 
631
841
  ---
632
842
 
633
- ## 手动模式:直接用 `createServer()`
843
+ ## 手动模式:`createServer()`
634
844
 
635
- 如果你不想依赖 Vite 插件,也可以直接起服务器。
845
+ 如果你不希望通过 Vite 插件接入,也可以直接使用 `createServer()`。
636
846
 
637
847
  ```ts
638
848
  import { createServer } from "@finesoft/front";
@@ -655,28 +865,28 @@ void vite;
655
865
  void runtime;
656
866
  ```
657
867
 
658
- ### `createServer()` 会帮你做什么
868
+ ### `createServer()` 会处理的内容
659
869
 
660
- 1. 解析根目录
661
- 2. 如果存在 `.env`,尝试加载它
870
+ 1. 解析项目根目录
871
+ 2. 如果存在 `.env`,尝试自动加载
662
872
  3. 检测当前运行时
663
- 4. 开发模式下创建 Vite middleware server
873
+ 4. 在开发模式下创建 Vite middleware server
664
874
  5. 创建 Hono app
665
- 6. 先挂你的业务路由
666
- 7. 再挂 SSR catch-all
875
+ 6. 先注册业务路由
876
+ 7. 再挂载 SSR catch-all
667
877
  8. 启动服务
668
878
 
669
- ### `createServer()` 默认行为
879
+ 默认行为:
670
880
 
671
- - `root` 默认是 `process.cwd()`
672
- - `port` 默认是 `process.env.PORT ?? 3000`
673
- - 如果根目录存在 `.env` 且安装了 `dotenv`,会自动尝试加载
881
+ - `root` 默认值:`process.cwd()`
882
+ - `port` 默认值:`process.env.PORT ?? 3000`
883
+ - 根目录存在 `.env` 且安装了 `dotenv` 时,会尝试自动加载
674
884
 
675
885
  ---
676
886
 
677
887
  ## 纯浏览器模式
678
888
 
679
- 如果你只想要 Framework / Router / Action / Controller,不做 SSR,也能单独使用。
889
+ 如果你只需要 Router / Intent / Controller / Framework,也可以单独使用浏览器侧能力。
680
890
 
681
891
  ```ts
682
892
  import { Framework, defineRoutes, startBrowserApp } from "@finesoft/front";
@@ -704,46 +914,17 @@ startBrowserApp({
704
914
 
705
915
  ---
706
916
 
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`:浏览器导航状态管理
721
-
722
- ### SSR
723
-
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
- ## 常见坑位(强烈建议先看)
917
+ ## 常见问题
737
918
 
738
- ### 1. `index.html` 没放 SSR 占位符
919
+ ### 1. `index.html` 少了 SSR 占位符
739
920
 
740
- 症状:
921
+ 现象:
741
922
 
742
- - 页面不 hydrate
743
- - SSR 内容丢失
744
- - server data 不进页面
923
+ - 页面未正常 hydrate
924
+ - SSR 内容缺失
925
+ - 服务端数据未注入
745
926
 
746
- 解决:检查这四个占位符是否都在:
927
+ 请检查以下四个占位符是否全部存在:
747
928
 
748
929
  - `<!--ssr-lang-->`
749
930
  - `<!--ssr-head-->`
@@ -752,16 +933,16 @@ startBrowserApp({
752
933
 
753
934
  ---
754
935
 
755
- ### 2. `setup` 传了函数,但部署产物里没有生效
936
+ ### 2. `setup` 传了函数,但构建产物中没有生效
756
937
 
757
938
  原因:
758
939
 
759
- - 直接传函数只适合 `dev/preview`
760
- - adapter 构建期没法可靠地把内联函数带进去
940
+ - 直接传函数主要适用于 `dev` / `preview`
941
+ - 构建期更适合通过文件路径构建 `setup` 模块
761
942
 
762
- 解决:
943
+ 建议:
763
944
 
764
- - 用文件路径字符串,例如 `setup: "src/proxies.ts"`
945
+ - 使用文件路径字符串,例如 `setup: "src/setup.ts"`
765
946
 
766
947
  ---
767
948
 
@@ -771,70 +952,34 @@ startBrowserApp({
771
952
 
772
953
  - `staticAdapter()` 只会自动预渲染无参数路由
773
954
 
774
- 解决:
955
+ 解决方式:
775
956
 
776
- - `dynamicRoutes` 传入具体 URL
957
+ - 使用 `dynamicRoutes` 提供具体 URL
777
958
 
778
959
  ---
779
960
 
780
- ### 4. `static` 模式构建出来是 500 错误页
961
+ ### 4. `static` 模式构建出来的是错误页
781
962
 
782
- 原因:
963
+ 原因通常是:
783
964
 
784
- - 构建时 Controller API 失败
785
- - 但 `fallback()` 只返回错误页,没有 mock 数据
965
+ - 构建期 Controller 访问外部 API 失败
966
+ - 但 fallback 没有提供可用的本地数据
786
967
 
787
- 解决:
968
+ 解决方式:
788
969
 
789
- - 构建时保证 API 可访问,或者
790
- - 给 Controller 增加 mock fallback
970
+ - 保证构建期 API 可访问,或
971
+ - 给 Controller 提供 fallback / mock 数据
791
972
 
792
973
  ---
793
974
 
794
975
  ### 5. `.vercel/` 和 `.netlify/` 为什么不在 `dist/`
795
976
 
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
- 如果你第一次接这个包,建议按下面顺序推进:
977
+ 这是平台约定,不是框架异常:
830
978
 
831
- 1. 先只做一个 `/` 首页
832
- 2. 跑通 `browser.ts` + `ssr.ts` + `index.html`
833
- 3. 再接 `finesoftFrontViteConfig()`
834
- 4. 开发阶段先用 `adapter: "node"`
835
- 5. 最后再切 `vercel` / `netlify` / `cloudflare` / `static`
979
+ - Vercel 使用 `.vercel/output/`
980
+ - Netlify 使用 `.netlify/functions-internal/`
836
981
 
837
- 别一上来就同时搞多语言、动态路由、SSR、静态预渲染、平台部署。那样很容易把自己卷成一个调试陀螺。
982
+ 建议将这些目录加入 `.gitignore`。
838
983
 
839
984
  ---
840
985
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finesoft/front",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Full-stack framework: router, DI, actions, SSR, and server — all in one package",
5
5
  "license": "MIT",
6
6
  "type": "module",