@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 +905 -137
- 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,987 @@
|
|
|
1
1
|
# @finesoft/front
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@finesoft/front` 是一个面向 SSR Web 应用的聚合包,统一导出了以下四层能力:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `@finesoft/core`:路由、Intent、Controller、依赖注入、Framework
|
|
6
|
+
- `@finesoft/browser`:浏览器启动、导航、hydrate、prefetched data
|
|
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
|
+
它适合这样一类应用:
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
$$
|
|
13
|
+
URL \rightarrow Router \rightarrow Intent \rightarrow Controller \rightarrow Page\ Model \rightarrow SSR / Hydration
|
|
14
|
+
$$
|
|
16
15
|
|
|
17
|
-
|
|
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
|
-
|
|
127
|
+
pnpm add @finesoft/front
|
|
21
128
|
```
|
|
22
129
|
|
|
23
|
-
### SSR /
|
|
130
|
+
### SSR / Vite / Adapter 使用
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pnpm add @finesoft/front hono @hono/node-server vite
|
|
134
|
+
```
|
|
24
135
|
|
|
25
|
-
|
|
136
|
+
### 如果你希望 `createServer()` 自动加载 `.env`
|
|
26
137
|
|
|
27
138
|
```bash
|
|
28
|
-
|
|
139
|
+
pnpm add dotenv
|
|
29
140
|
```
|
|
30
141
|
|
|
31
|
-
###
|
|
142
|
+
### 当前 peer dependencies
|
|
32
143
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- `dotenv` — optional, only needed if you want `.env` auto-loading in `createServer()`
|
|
144
|
+
- `hono`
|
|
145
|
+
- `@hono/node-server`
|
|
146
|
+
- `vite`
|
|
37
147
|
|
|
38
|
-
|
|
148
|
+
如果你只使用浏览器侧能力,不一定需要全部安装;如果你使用 SSR、Server 或 Vite 插件,则建议全部安装。
|
|
39
149
|
|
|
40
|
-
|
|
150
|
+
---
|
|
41
151
|
|
|
42
|
-
|
|
43
|
-
- In SSR / Node environments, the default import resolves to the full entry with browser + SSR + server exports.
|
|
152
|
+
## 入口行为说明
|
|
44
153
|
|
|
45
|
-
|
|
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
|
-
|
|
167
|
+
现代 bundler 会根据环境自动选择更合适的入口。
|
|
52
168
|
|
|
53
|
-
|
|
169
|
+
---
|
|
54
170
|
|
|
55
|
-
|
|
171
|
+
## 推荐工作流:Vite + SSR + Hydration
|
|
56
172
|
|
|
57
|
-
|
|
58
|
-
import { createServer } from "@finesoft/front";
|
|
173
|
+
下面是一套面向公共用户的最小接入流程。
|
|
59
174
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
193
|
+
如果你需要注册 API 代理或额外 Hono 路由,可以增加:
|
|
72
194
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
195
|
+
```text
|
|
196
|
+
src/setup.ts
|
|
197
|
+
```
|
|
76
198
|
|
|
77
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
如果没有导出 `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
|
-
//
|
|
98
|
-
return ({ page }) => {
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
310
|
+
---
|
|
110
311
|
|
|
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
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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()`
|
|
342
|
+
`createSSRRender()` 最终会生成一个 `render(url, locale)` 函数,其返回值与服务端 SSR 模块契约一致。
|
|
132
343
|
|
|
133
|
-
|
|
344
|
+
---
|
|
134
345
|
|
|
135
|
-
|
|
346
|
+
### 6. 如果你有自定义 Hono 路由,编写 `src/setup.ts`
|
|
136
347
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
+
`setup` 在插件中有两种用法:
|
|
159
359
|
|
|
160
|
-
`
|
|
360
|
+
- 传入函数:适用于 `dev` / `preview`
|
|
361
|
+
- 传入文件路径字符串:适用于 `dev` / `build` / `preview` / adapter
|
|
161
362
|
|
|
162
|
-
|
|
363
|
+
如果你需要让构建产物也包含这些路由,建议传入文件路径字符串。
|
|
163
364
|
|
|
164
|
-
|
|
365
|
+
---
|
|
165
366
|
|
|
166
|
-
|
|
367
|
+
### 7. 配置 `vite.config.ts`
|
|
167
368
|
|
|
168
|
-
|
|
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
|
-
|
|
386
|
+
当前支持的 adapter:
|
|
171
387
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
388
|
+
- `"node"`
|
|
389
|
+
- `"vercel"`
|
|
390
|
+
- `"cloudflare"`
|
|
391
|
+
- `"netlify"`
|
|
392
|
+
- `"static"`
|
|
393
|
+
- `"auto"`
|
|
394
|
+
- 或自定义 `Adapter` 对象
|
|
175
395
|
|
|
176
|
-
|
|
396
|
+
---
|
|
177
397
|
|
|
178
|
-
###
|
|
398
|
+
### 8. 配置脚本
|
|
179
399
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"scripts": {
|
|
403
|
+
"dev": "vite",
|
|
404
|
+
"build": "vite build",
|
|
405
|
+
"preview": "vite preview"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
188
409
|
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|