@finesoft/front 0.1.16 → 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.
package/README.md CHANGED
@@ -1,219 +1,987 @@
1
1
  # @finesoft/front
2
2
 
3
- Full-stack framework for building content-driven web applications with SSR support.
3
+ `@finesoft/front` 是一个面向 SSR Web 应用的聚合包,统一导出了以下四层能力:
4
4
 
5
- ## Features
5
+ - `@finesoft/core`:路由、Intent、Controller、依赖注入、Framework
6
+ - `@finesoft/browser`:浏览器启动、导航、hydrate、prefetched data
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
+ 它适合这样一类应用:
14
11
 
15
- ## Install
12
+ $$
13
+ URL \rightarrow Router \rightarrow Intent \rightarrow Controller \rightarrow Page\ Model \rightarrow SSR / Hydration
14
+ $$
16
15
 
17
- ### Browser-only usage
16
+ 也就是说:URL 决定页面语义,Controller 负责取数和组装页面模型,UI 层只负责渲染页面模型。
17
+
18
+ ---
19
+
20
+ ## 适用场景
21
+
22
+ `@finesoft/front` 更适合以下类型的项目:
23
+
24
+ - 需要 SSR 的内容型站点
25
+ - 有明确 URL 语义的多页面 Web 应用
26
+ - 希望将页面获取逻辑集中在 Controller 中的项目
27
+ - 需要同一套页面模型同时服务 SSR 和客户端导航的项目
28
+
29
+ 例如:
30
+
31
+ - 内容聚合站点
32
+ - 应用商店、媒体展示、排行榜、搜索、详情页
33
+ - 需要 SEO 的展示型前端
34
+
35
+ 如果你的项目非常轻量、完全不需要 SSR,也可以只使用其中的 Browser/Core 能力。
36
+
37
+ ---
38
+
39
+ ## 主要导出
40
+
41
+ ### Core
42
+
43
+ - `Framework`
44
+ - `Router`
45
+ - `Container`
46
+ - `BaseController`
47
+ - `defineRoutes`
48
+ - `ActionDispatcher`
49
+ - `IntentDispatcher`
50
+ - `HttpClient`
51
+ - `LruMap`
52
+ - `buildUrl`
53
+
54
+ ### Browser
55
+
56
+ - `startBrowserApp`
57
+ - `History`
58
+ - `registerActionHandlers`
59
+ - `registerFlowActionHandler`
60
+ - `registerExternalUrlHandler`
61
+ - `deserializeServerData`
62
+ - `createPrefetchedIntentsFromDom`
63
+ - `tryScroll`
64
+
65
+ ### SSR
66
+
67
+ - `createSSRRender`
68
+ - `ssrRender`
69
+ - `injectSSRContent`
70
+ - `serializeServerData`
71
+ - `SSR_PLACEHOLDERS`
72
+
73
+ ### Server / Deployment
74
+
75
+ - `createServer`
76
+ - `createSSRApp`
77
+ - `startServer`
78
+ - `parseAcceptLanguage`
79
+ - `detectRuntime`
80
+ - `resolveRoot`
81
+ - `finesoftFrontViteConfig`
82
+ - `nodeAdapter`
83
+ - `vercelAdapter`
84
+ - `cloudflareAdapter`
85
+ - `netlifyAdapter`
86
+ - `staticAdapter`
87
+ - `autoAdapter`
88
+ - `resolveAdapter`
89
+
90
+ ---
91
+
92
+ ## 选择哪种接入方式
93
+
94
+ ### 方式 A:使用 `finesoftFrontViteConfig()`
95
+
96
+ 这是推荐方式,适合大多数项目。
97
+
98
+ 优点:
99
+
100
+ - `vite` 可直接用于开发
101
+ - `vite build` 会同时完成客户端与 SSR 构建
102
+ - 可以直接接入平台适配器输出部署产物
103
+ - `vite preview` 可用于本地预览 SSR 构建结果
104
+
105
+ ### 方式 B:手动使用 `createServer()`
106
+
107
+ 适合以下情况:
108
+
109
+ - 你需要完全控制服务启动流程
110
+ - 你已经有自定义的 Hono / Node 集成方式
111
+ - 你不希望依赖 Vite 插件生命周期
112
+
113
+ ### 方式 C:仅使用 Browser/Core
114
+
115
+ 适合以下情况:
116
+
117
+ - 你不需要 SSR
118
+ - 你只希望复用 Router / Intent / Controller / Framework 模型
119
+
120
+ ---
121
+
122
+ ## 安装
123
+
124
+ ### 仅浏览器端使用
18
125
 
19
126
  ```bash
20
- npm install @finesoft/front
127
+ pnpm add @finesoft/front
21
128
  ```
22
129
 
23
- ### SSR / full-stack usage
130
+ ### SSR / Vite / Adapter 使用
131
+
132
+ ```bash
133
+ pnpm add @finesoft/front hono @hono/node-server vite
134
+ ```
24
135
 
25
- If you use `createServer()` or other server exports, install the peer dependencies too:
136
+ ### 如果你希望 `createServer()` 自动加载 `.env`
26
137
 
27
138
  ```bash
28
- npm install @finesoft/front hono vite @hono/node-server dotenv
139
+ pnpm add dotenv
29
140
  ```
30
141
 
31
- ### Peer dependencies
142
+ ### 当前 peer dependencies
32
143
 
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()`
144
+ - `hono`
145
+ - `@hono/node-server`
146
+ - `vite`
37
147
 
38
- ## Package entry behavior
148
+ 如果你只使用浏览器侧能力,不一定需要全部安装;如果你使用 SSR、Server Vite 插件,则建议全部安装。
39
149
 
40
- `@finesoft/front` ships a browser-aware export map.
150
+ ---
41
151
 
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.
152
+ ## 入口行为说明
44
153
 
45
- That means you can keep importing from:
154
+ `@finesoft/front` 提供了 `browser` export condition。
155
+
156
+ 这意味着:
157
+
158
+ - 浏览器构建时,会优先解析 browser-only entry,避免引入服务端实现
159
+ - Node / SSR 环境下,会解析完整入口,包含 Browser、SSR、Server 全部导出
160
+
161
+ 因此大多数情况下你可以直接这样写:
46
162
 
47
163
  ```ts
48
164
  import { startBrowserApp } from "@finesoft/front";
49
165
  ```
50
166
 
51
- and modern bundlers will avoid pulling in `createServer`, `startServer`, and other server-only code on the client side.
167
+ 现代 bundler 会根据环境自动选择更合适的入口。
52
168
 
53
- ## Quick Start
169
+ ---
54
170
 
55
- ### Server
171
+ ## 推荐工作流:Vite + SSR + Hydration
56
172
 
57
- ```ts
58
- import { createServer } from "@finesoft/front";
173
+ 下面是一套面向公共用户的最小接入流程。
59
174
 
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
- });
175
+ ### 1. 准备目录结构
176
+
177
+ 建议至少包含以下文件:
178
+
179
+ ```text
180
+ src/
181
+ browser.ts
182
+ ssr.ts
183
+ lib/
184
+ bootstrap.ts
185
+ models/
186
+ page.ts
187
+ controllers/
188
+ home-controller.ts
189
+ index.html
190
+ vite.config.ts
69
191
  ```
70
192
 
71
- Notes:
193
+ 如果你需要注册 API 代理或额外 Hono 路由,可以增加:
72
194
 
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
195
+ ```text
196
+ src/setup.ts
197
+ ```
76
198
 
77
- ### Browser
199
+ ---
200
+
201
+ ### 2. 编写 `index.html`
202
+
203
+ 你的 HTML 模板必须包含以下 SSR 占位符:
204
+
205
+ ```html
206
+ <!doctype html>
207
+ <html lang="<!--ssr-lang-->">
208
+ <head>
209
+ <meta charset="utf-8" />
210
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
211
+ <!--ssr-head-->
212
+ </head>
213
+ <body>
214
+ <div id="app"><!--ssr-body--></div>
215
+ <!--ssr-data-->
216
+ <script type="module" src="/src/browser.ts"></script>
217
+ </body>
218
+ </html>
219
+ ```
220
+
221
+ | 占位符 | 用途 |
222
+ | --- | --- |
223
+ | `<!--ssr-lang-->` | 当前语言 |
224
+ | `<!--ssr-head-->` | SSR `<head>` 内容与样式 |
225
+ | `<!--ssr-body-->` | 服务端渲染后的 HTML |
226
+ | `<!--ssr-data-->` | 序列化后的服务端数据 |
227
+
228
+ ---
229
+
230
+ ### 3. 编写路由与 Controller 注册
231
+
232
+ 推荐使用 `defineRoutes()`,把 URL 与 Controller 声明放在同一处。
233
+
234
+ `src/lib/bootstrap.ts`:
78
235
 
79
236
  ```ts
80
- import { startBrowserApp, Framework, defineRoutes } from "@finesoft/front";
237
+ import {
238
+ BasePage,
239
+ BaseController,
240
+ Container,
241
+ Framework,
242
+ defineRoutes,
243
+ type RouteDefinition,
244
+ } from "@finesoft/front";
245
+
246
+ class HomeController extends BaseController<Record<string, string>, BasePage> {
247
+ readonly intentId = "home-page";
248
+
249
+ async execute(_params: Record<string, string>, _container: Container) {
250
+ return {
251
+ id: "page-home",
252
+ pageType: "home",
253
+ title: "Home",
254
+ description: "A page rendered by @finesoft/front",
255
+ };
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
+ 如果没有导出 `routes`,静态导出时将无法自动发现这些页面。
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
+ return ({ page, isFirstPage }) => {
292
+ void target;
293
+ void framework;
294
+ void locale;
295
+ void page;
296
+ void isFirstPage;
100
297
  };
101
298
  },
102
299
  callbacks: {
103
- onNavigationStart: () => {},
104
- onNavigationEnd: () => {},
300
+ onNavigate(pathname) {
301
+ console.log("navigate:", pathname);
302
+ },
303
+ onModal(page) {
304
+ console.log("modal:", page);
305
+ },
105
306
  },
106
307
  });
107
308
  ```
108
309
 
109
- Notes:
310
+ ---
110
311
 
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
114
-
115
- ### SSR Entry
312
+ ### 5. 编写 SSR 入口 `src/ssr.ts`
116
313
 
117
314
  ```ts
118
315
  import { createSSRRender, serializeServerData } from "@finesoft/front";
316
+ import { bootstrap } from "./lib/bootstrap";
119
317
 
120
318
  export const render = createSSRRender({
121
319
  bootstrap,
122
- getErrorPage: (status, message) => ({ title: message }),
123
- renderApp: (page, locale) => {
124
- // render your app to HTML
125
- return { html: "", head: "", css: "" };
320
+ getErrorPage(status, message) {
321
+ return {
322
+ id: `error-${status}`,
323
+ pageType: "error",
324
+ title: message,
325
+ statusCode: status,
326
+ };
327
+ },
328
+ renderApp(page, locale) {
329
+ void page;
330
+ void locale;
331
+ return {
332
+ html: "",
333
+ head: "",
334
+ css: "",
335
+ };
126
336
  },
127
337
  });
338
+
128
339
  export { serializeServerData };
129
340
  ```
130
341
 
131
- `createSSRRender()` returns a `render(url, locale)` function compatible with the SSR module shape expected by `createServer()`.
342
+ `createSSRRender()` 最终会生成一个 `render(url, locale)` 函数,其返回值与服务端 SSR 模块契约一致。
132
343
 
133
- ### HTML Template
344
+ ---
134
345
 
135
- Your `index.html` must include SSR placeholders:
346
+ ### 6. 如果你有自定义 Hono 路由,编写 `src/setup.ts`
136
347
 
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>
348
+ ```ts
349
+ import type { Hono } from "hono";
350
+
351
+ export default function setup(app: Hono) {
352
+ app.get("/api/health", (c) => c.json({ ok: true }));
353
+ }
149
354
  ```
150
355
 
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 |
356
+ 建议优先导出 `default` 函数。
157
357
 
158
- These are also available as `SSR_PLACEHOLDERS.LANG`, `.HEAD`, `.BODY`, `.DATA` constants.
358
+ `setup` 在插件中有两种用法:
159
359
 
160
- `injectSSRContent()` automatically wraps serialized server data in a `<script>` tag for the `<!--ssr-data-->` placeholder.
360
+ - 传入函数:适用于 `dev` / `preview`
361
+ - 传入文件路径字符串:适用于 `dev` / `build` / `preview` / adapter
161
362
 
162
- ## Common patterns
363
+ 如果你需要让构建产物也包含这些路由,建议传入文件路径字符串。
163
364
 
164
- ### Browser-only app
365
+ ---
165
366
 
166
- Use `startBrowserApp()` together with your framework mount callback. Browser bundlers will resolve the browser-only export automatically.
367
+ ### 7. 配置 `vite.config.ts`
167
368
 
168
- ### Full SSR app
369
+ ```ts
370
+ import { finesoftFrontViteConfig } from "@finesoft/front";
371
+ import { defineConfig } from "vite";
372
+
373
+ export default defineConfig({
374
+ plugins: [
375
+ finesoftFrontViteConfig({
376
+ locales: ["zh", "en"],
377
+ defaultLocale: "en",
378
+ ssr: { entry: "src/ssr.ts" },
379
+ setup: "src/setup.ts",
380
+ adapter: "node",
381
+ }),
382
+ ],
383
+ });
384
+ ```
169
385
 
170
- Use the three pieces together:
386
+ 当前支持的 adapter:
171
387
 
172
- 1. `createServer()` — dev/prod server bootstrap
173
- 2. `createSSRRender()` — SSR render function factory
174
- 3. `startBrowserApp()` — client hydration
388
+ - `"node"`
389
+ - `"vercel"`
390
+ - `"cloudflare"`
391
+ - `"netlify"`
392
+ - `"static"`
393
+ - `"auto"`
394
+ - 或自定义 `Adapter` 对象
175
395
 
176
- ## Selected exports
396
+ ---
177
397
 
178
- ### Browser
398
+ ### 8. 配置脚本
179
399
 
180
- - `startBrowserApp`
181
- - `History`
182
- - `registerActionHandlers`
183
- - `registerExternalUrlHandler`
184
- - `registerFlowActionHandler`
185
- - `deserializeServerData`
186
- - `createPrefetchedIntentsFromDom`
187
- - `tryScroll`
400
+ ```json
401
+ {
402
+ "scripts": {
403
+ "dev": "vite",
404
+ "build": "vite build",
405
+ "preview": "vite preview"
406
+ }
407
+ }
408
+ ```
188
409
 
189
- ### SSR
410
+ ---
411
+
412
+ ### 9. 启动与构建
413
+
414
+ 开发:
415
+
416
+ ```bash
417
+ pnpm dev
418
+ ```
419
+
420
+ 构建:
421
+
422
+ ```bash
423
+ pnpm build
424
+ ```
425
+
426
+ 本地预览:
427
+
428
+ ```bash
429
+ pnpm preview
430
+ ```
431
+
432
+ ---
433
+
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
+ ---
665
+
666
+ ## Adapter 输出说明
667
+
668
+ ### `adapter: "node"`
669
+
670
+ 输出:
671
+
672
+ - `dist/server/index.mjs`
673
+
674
+ 运行方式:
675
+
676
+ ```bash
677
+ node dist/server/index.mjs
678
+ ```
679
+
680
+ 适合:
681
+
682
+ - Node 服务器
683
+ - Docker
684
+ - VPS
685
+ - PM2
686
+
687
+ ---
688
+
689
+ ### `adapter: "vercel"`
690
+
691
+ 输出:
692
+
693
+ - `.vercel/output/config.json`
694
+ - `.vercel/output/static/`
695
+ - `.vercel/output/functions/ssr.func/`
696
+
697
+ 说明:
698
+
699
+ - 它不在 `dist/` 中,这是平台约定
700
+ - 对应 Vercel Build Output API v3
701
+
702
+ 建议将 `.vercel/` 加入 `.gitignore`。
703
+
704
+ ---
705
+
706
+ ### `adapter: "netlify"`
707
+
708
+ 输出:
709
+
710
+ - `.netlify/functions-internal/ssr/index.mjs`
711
+ - `dist/client/_redirects`
712
+
713
+ 说明:
714
+
715
+ - `.netlify/` 在 `dist/` 外同样属于平台约定
716
+ - 常见发布目录是 `dist/client/`
717
+
718
+ 建议将 `.netlify/` 加入 `.gitignore`。
719
+
720
+ ---
721
+
722
+ ### `adapter: "cloudflare"`
723
+
724
+ 输出:
725
+
726
+ - `dist/cloudflare/_worker.js`
727
+ - `dist/cloudflare/assets/`
728
+
729
+ 说明:
730
+
731
+ - Cloudflare Workers 不是完整 Node.js 环境
732
+ - 如果运行时代码依赖 Node API,可能需要额外兼容配置
733
+
734
+ ---
735
+
736
+ ### `adapter: "static"`
737
+
738
+ 输出:
739
+
740
+ - `dist/static/`
741
+
742
+ 适合:
743
+
744
+ - 纯静态托管
745
+ - CDN / 对象存储 / Pages 类平台
746
+ - 不依赖运行时服务端逻辑的页面
747
+
748
+ 它会执行:
749
+
750
+ 1. 读取导出的路由配置
751
+ 2. 自动预渲染无参数路由
752
+ 3. 复制客户端静态资源
753
+ 4. 输出纯 HTML / CSS / JS 文件
754
+
755
+ #### `static` 模式的三个注意点
756
+
757
+ ##### 1)只会自动预渲染无参数路由
758
+
759
+ 例如这些通常会自动生成:
760
+
761
+ - `/`
762
+ - `/search`
763
+ - `/about`
764
+
765
+ 这些通常不会自动生成:
766
+
767
+ - `/product/:id`
768
+ - `/list/:category`
769
+
770
+ 如果你要预渲染动态地址,请补充具体 URL:
771
+
772
+ ```ts
773
+ import { staticAdapter } from "@finesoft/front";
774
+
775
+ finesoftFrontViteConfig({
776
+ adapter: staticAdapter({
777
+ dynamicRoutes: ["/product/123", "/list/games"],
778
+ }),
779
+ });
780
+ ```
781
+
782
+ ##### 2)构建时必须能够拿到页面数据
783
+
784
+ `static` 预渲染会在构建期执行 Controller。
785
+
786
+ 如果 Controller 依赖外部 API,而构建时这些 API 不可访问,页面可能构建失败或退化为错误页。
787
+
788
+ 常见解决方式:
789
+
790
+ - 构建期确保 API 可访问
791
+ - 为 Controller 提供 fallback / mock 数据
792
+
793
+ ##### 3)验证静态产物时,应直接查看 `dist/static/`
794
+
795
+ 如果你要验证静态导出的最终结果,可以直接服务这个目录:
796
+
797
+ ```bash
798
+ cd dist/static
799
+ python3 -m http.server 3000
800
+ ```
801
+
802
+ ---
803
+
804
+ ### `adapter: "auto"`
805
+
806
+ 自动识别顺序:
807
+
808
+ - `VERCEL` → `vercel`
809
+ - `CF_PAGES` → `cloudflare`
810
+ - `NETLIFY` → `netlify`
811
+ - 默认 → `node`
812
+
813
+ 适合 CI 或平台自动识别场景。
814
+
815
+ ---
816
+
817
+ ## 自定义 Adapter
818
+
819
+ 你也可以直接传入自定义 `Adapter` 对象:
820
+
821
+ ```ts
822
+ import type { Adapter } from "@finesoft/front";
823
+
824
+ const customAdapter: Adapter = {
825
+ name: "my-platform",
826
+ async build(ctx) {
827
+ // ctx 中包含 root / vite / fs / path / templateHtml
828
+ // 以及 generateSSREntry / buildBundle / copyStaticAssets 等工具方法
829
+ },
830
+ };
831
+ ```
832
+
833
+ 使用方式:
834
+
835
+ ```ts
836
+ finesoftFrontViteConfig({
837
+ adapter: customAdapter,
838
+ });
839
+ ```
840
+
841
+ ---
842
+
843
+ ## 手动模式:`createServer()`
844
+
845
+ 如果你不希望通过 Vite 插件接入,也可以直接使用 `createServer()`。
846
+
847
+ ```ts
848
+ import { createServer } from "@finesoft/front";
849
+
850
+ const { app, vite, runtime } = await createServer({
851
+ root: process.cwd(),
852
+ locales: ["zh", "en"],
853
+ defaultLocale: "en",
854
+ port: 3000,
855
+ setup(app) {
856
+ app.get("/api/health", (c) => c.json({ ok: true }));
857
+ },
858
+ ssr: {
859
+ ssrEntryPath: "/src/ssr.ts",
860
+ },
861
+ });
862
+
863
+ void app;
864
+ void vite;
865
+ void runtime;
866
+ ```
867
+
868
+ ### `createServer()` 会处理的内容
869
+
870
+ 1. 解析项目根目录
871
+ 2. 如果存在 `.env`,尝试自动加载
872
+ 3. 检测当前运行时
873
+ 4. 在开发模式下创建 Vite middleware server
874
+ 5. 创建 Hono app
875
+ 6. 先注册业务路由
876
+ 7. 再挂载 SSR catch-all
877
+ 8. 启动服务
878
+
879
+ 默认行为:
880
+
881
+ - `root` 默认值:`process.cwd()`
882
+ - `port` 默认值:`process.env.PORT ?? 3000`
883
+ - 根目录存在 `.env` 且安装了 `dotenv` 时,会尝试自动加载
884
+
885
+ ---
886
+
887
+ ## 纯浏览器模式
888
+
889
+ 如果你只需要 Router / Intent / Controller / Framework,也可以单独使用浏览器侧能力。
890
+
891
+ ```ts
892
+ import { Framework, defineRoutes, startBrowserApp } from "@finesoft/front";
893
+
894
+ function bootstrap(framework: Framework) {
895
+ defineRoutes(framework, [
896
+ { path: "/", intentId: "home", controller: new HomeController() },
897
+ ]);
898
+ }
899
+
900
+ startBrowserApp({
901
+ bootstrap,
902
+ mount: (target) => {
903
+ return ({ page }) => {
904
+ void target;
905
+ void page;
906
+ };
907
+ },
908
+ callbacks: {
909
+ onNavigate() {},
910
+ onModal() {},
911
+ },
912
+ });
913
+ ```
914
+
915
+ ---
916
+
917
+ ## 常见问题
918
+
919
+ ### 1. `index.html` 少了 SSR 占位符
920
+
921
+ 现象:
922
+
923
+ - 页面未正常 hydrate
924
+ - SSR 内容缺失
925
+ - 服务端数据未注入
926
+
927
+ 请检查以下四个占位符是否全部存在:
928
+
929
+ - `<!--ssr-lang-->`
930
+ - `<!--ssr-head-->`
931
+ - `<!--ssr-body-->`
932
+ - `<!--ssr-data-->`
933
+
934
+ ---
935
+
936
+ ### 2. `setup` 传了函数,但构建产物中没有生效
937
+
938
+ 原因:
939
+
940
+ - 直接传函数主要适用于 `dev` / `preview`
941
+ - 构建期更适合通过文件路径构建 `setup` 模块
942
+
943
+ 建议:
944
+
945
+ - 使用文件路径字符串,例如 `setup: "src/setup.ts"`
946
+
947
+ ---
948
+
949
+ ### 3. `static` 模式下动态路由没有页面
950
+
951
+ 原因:
952
+
953
+ - `staticAdapter()` 只会自动预渲染无参数路由
954
+
955
+ 解决方式:
956
+
957
+ - 使用 `dynamicRoutes` 提供具体 URL
958
+
959
+ ---
960
+
961
+ ### 4. `static` 模式构建出来的是错误页
962
+
963
+ 原因通常是:
964
+
965
+ - 构建期 Controller 访问外部 API 失败
966
+ - 但 fallback 没有提供可用的本地数据
967
+
968
+ 解决方式:
969
+
970
+ - 保证构建期 API 可访问,或
971
+ - 给 Controller 提供 fallback / mock 数据
972
+
973
+ ---
974
+
975
+ ### 5. `.vercel/` 和 `.netlify/` 为什么不在 `dist/`
976
+
977
+ 这是平台约定,不是框架异常:
978
+
979
+ - Vercel 使用 `.vercel/output/`
980
+ - Netlify 使用 `.netlify/functions-internal/`
981
+
982
+ 建议将这些目录加入 `.gitignore`。
190
983
 
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` |
984
+ ---
217
985
 
218
986
  ## License
219
987