@finesoft/front 0.1.17 → 0.1.19
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 +408 -263
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- 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
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
5
|
+
- `@finesoft/core`:路由、Intent、Controller、依赖注入、Framework
|
|
6
|
+
- `@finesoft/browser`:浏览器启动、导航、hydrate、prefetched data
|
|
7
|
+
- `@finesoft/ssr`:SSR 渲染与服务端数据注入
|
|
8
|
+
- `@finesoft/server`:Hono 服务端集成、Vite 插件、部署适配器
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
它适合这样一类应用:
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- 希望把“页面逻辑”收敛到 Controller,而不是散落在 UI 组件里的项目
|
|
24
|
+
- 需要 SSR 的内容型站点
|
|
25
|
+
- 有明确 URL 语义的多页面 Web 应用
|
|
26
|
+
- 希望将页面获取逻辑集中在 Controller 中的项目
|
|
27
|
+
- 需要同一套页面模型同时服务 SSR 和客户端导航的项目
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
例如:
|
|
27
30
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
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
|
-
###
|
|
94
|
+
### 方式 A:使用 `finesoftFrontViteConfig()`
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
这是推荐方式,适合大多数项目。
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
优点:
|
|
98
99
|
|
|
99
|
-
- `vite
|
|
100
|
-
- `vite build
|
|
101
|
-
-
|
|
100
|
+
- `vite` 可直接用于开发
|
|
101
|
+
- `vite build` 会同时完成客户端与 SSR 构建
|
|
102
|
+
- 可以直接接入平台适配器输出部署产物
|
|
103
|
+
- `vite preview` 可用于本地预览 SSR 构建结果
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
### 方式 B:手动使用 `createServer()`
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
适合以下情况:
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
- 你需要完全控制服务启动流程
|
|
110
|
+
- 你已经有自定义的 Hono / Node 集成方式
|
|
111
|
+
- 你不希望依赖 Vite 插件生命周期
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
### 方式 C:仅使用 Browser/Core
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
- 你已经有自己的服务启动逻辑
|
|
113
|
-
- 你要手工控制 Hono/Vite/SSR 的集成方式
|
|
115
|
+
适合以下情况:
|
|
114
116
|
|
|
115
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
145
|
-
|
|
146
|
-
这个包当前依赖这些 peer:
|
|
142
|
+
### 当前 peer dependencies
|
|
147
143
|
|
|
148
144
|
- `hono`
|
|
149
145
|
- `@hono/node-server`
|
|
150
146
|
- `vite`
|
|
151
147
|
|
|
152
|
-
|
|
148
|
+
如果你只使用浏览器侧能力,不一定需要全部安装;如果你使用 SSR、Server 或 Vite 插件,则建议全部安装。
|
|
153
149
|
|
|
154
150
|
---
|
|
155
151
|
|
|
156
|
-
##
|
|
152
|
+
## 入口行为说明
|
|
157
153
|
|
|
158
|
-
`@finesoft/front`
|
|
154
|
+
`@finesoft/front` 提供了 `browser` export condition。
|
|
159
155
|
|
|
160
156
|
这意味着:
|
|
161
157
|
|
|
162
|
-
-
|
|
163
|
-
- Node / SSR
|
|
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
|
-
##
|
|
171
|
+
## 推荐工作流:Vite + SSR + Hydration
|
|
176
172
|
|
|
177
|
-
|
|
173
|
+
下面是一套面向公共用户的最小接入流程。
|
|
178
174
|
|
|
179
|
-
###
|
|
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
|
-
|
|
193
|
+
如果你需要注册 API 代理或额外 Hono 路由,可以增加:
|
|
196
194
|
|
|
197
195
|
```text
|
|
198
|
-
src/
|
|
196
|
+
src/setup.ts
|
|
199
197
|
```
|
|
200
198
|
|
|
201
199
|
---
|
|
202
200
|
|
|
203
|
-
###
|
|
201
|
+
### 2. 编写 `index.html`
|
|
204
202
|
|
|
205
|
-
你的 HTML
|
|
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-->` |
|
|
228
|
-
| `<!--ssr-head-->` | `<head>`
|
|
223
|
+
| `<!--ssr-lang-->` | 当前语言 |
|
|
224
|
+
| `<!--ssr-head-->` | SSR `<head>` 内容与样式 |
|
|
229
225
|
| `<!--ssr-body-->` | 服务端渲染后的 HTML |
|
|
230
|
-
| `<!--ssr-data-->` |
|
|
231
|
-
|
|
232
|
-
如果少一个,SSR 体验就会开始闹脾气。
|
|
226
|
+
| `<!--ssr-data-->` | 序列化后的服务端数据 |
|
|
233
227
|
|
|
234
228
|
---
|
|
235
229
|
|
|
236
|
-
###
|
|
230
|
+
### 3. 编写路由与 Controller 注册
|
|
237
231
|
|
|
238
|
-
|
|
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>,
|
|
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 {
|
|
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
|
-
//
|
|
267
|
+
// 如果你计划使用 static adapter,建议导出 routes
|
|
268
268
|
export { routes };
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
#### 为什么 `static`
|
|
271
|
+
#### 为什么 `static` 模式建议导出 `routes`
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
`staticAdapter()` 会在构建期读取你的路由定义,并自动预渲染无参数路由。
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
如果没有导出 `routes`,静态导出时将无法自动发现这些页面。
|
|
276
276
|
|
|
277
277
|
---
|
|
278
278
|
|
|
279
|
-
###
|
|
279
|
+
### 4. 编写浏览器入口 `src/browser.ts`
|
|
280
280
|
|
|
281
281
|
```ts
|
|
282
282
|
import { startBrowserApp } from "@finesoft/front";
|
|
283
|
-
import { bootstrap } from "
|
|
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
|
-
//
|
|
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("
|
|
301
|
+
console.log("navigate:", pathname);
|
|
303
302
|
},
|
|
304
303
|
onModal(page) {
|
|
305
|
-
console.log("
|
|
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
|
-
###
|
|
312
|
+
### 5. 编写 SSR 入口 `src/ssr.ts`
|
|
319
313
|
|
|
320
314
|
```ts
|
|
321
315
|
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
322
|
-
import { bootstrap } from "
|
|
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
|
-
###
|
|
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
|
-
|
|
356
|
+
建议优先导出 `default` 函数。
|
|
371
357
|
|
|
372
|
-
`setup`
|
|
358
|
+
`setup` 在插件中有两种用法:
|
|
373
359
|
|
|
374
|
-
-
|
|
375
|
-
-
|
|
360
|
+
- 传入函数:适用于 `dev` / `preview`
|
|
361
|
+
- 传入文件路径字符串:适用于 `dev` / `build` / `preview` / adapter
|
|
376
362
|
|
|
377
|
-
|
|
363
|
+
如果你需要让构建产物也包含这些路由,建议传入文件路径字符串。
|
|
378
364
|
|
|
379
365
|
---
|
|
380
366
|
|
|
381
|
-
###
|
|
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/
|
|
379
|
+
setup: "src/setup.ts",
|
|
394
380
|
adapter: "node",
|
|
395
381
|
}),
|
|
396
382
|
],
|
|
397
383
|
});
|
|
398
384
|
```
|
|
399
385
|
|
|
400
|
-
|
|
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
|
-
-
|
|
394
|
+
- 或自定义 `Adapter` 对象
|
|
409
395
|
|
|
410
396
|
---
|
|
411
397
|
|
|
412
|
-
###
|
|
398
|
+
### 8. 配置脚本
|
|
413
399
|
|
|
414
400
|
```json
|
|
415
401
|
{
|
|
@@ -423,7 +409,9 @@ export default defineConfig({
|
|
|
423
409
|
|
|
424
410
|
---
|
|
425
411
|
|
|
426
|
-
###
|
|
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
|
-
|
|
426
|
+
本地预览:
|
|
439
427
|
|
|
440
428
|
```bash
|
|
441
429
|
pnpm preview
|
|
@@ -443,9 +431,239 @@ pnpm preview
|
|
|
443
431
|
|
|
444
432
|
---
|
|
445
433
|
|
|
446
|
-
##
|
|
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
|
-
-
|
|
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
|
-
-
|
|
482
|
-
-
|
|
483
|
-
- 不是框架任性,是平台规定就这么放
|
|
699
|
+
- 它不在 `dist/` 中,这是平台约定
|
|
700
|
+
- 对应 Vercel Build Output API v3
|
|
484
701
|
|
|
485
|
-
|
|
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
|
-
-
|
|
499
|
-
-
|
|
500
|
-
- 真正的 publish 目录通常是 `dist/client/`
|
|
715
|
+
- `.netlify/` 在 `dist/` 外同样属于平台约定
|
|
716
|
+
- 常见发布目录是 `dist/client/`
|
|
501
717
|
|
|
502
|
-
|
|
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
|
|
516
|
-
-
|
|
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 /
|
|
745
|
+
- CDN / 对象存储 / Pages 类平台
|
|
530
746
|
- 不依赖运行时服务端逻辑的页面
|
|
531
747
|
|
|
532
|
-
|
|
748
|
+
它会执行:
|
|
533
749
|
|
|
534
|
-
1.
|
|
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
|
-
- `/
|
|
763
|
+
- `/about`
|
|
548
764
|
|
|
549
|
-
|
|
765
|
+
这些通常不会自动生成:
|
|
550
766
|
|
|
551
767
|
- `/product/:id`
|
|
552
768
|
- `/list/:category`
|
|
553
769
|
|
|
554
|
-
|
|
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
|
-
|
|
573
|
-
- 而构建时没有 API 服务
|
|
574
|
-
- 那么页面就会渲染失败或退化成错误页
|
|
786
|
+
如果 Controller 依赖外部 API,而构建时这些 API 不可访问,页面可能构建失败或退化为错误页。
|
|
575
787
|
|
|
576
|
-
|
|
788
|
+
常见解决方式:
|
|
577
789
|
|
|
578
|
-
-
|
|
579
|
-
-
|
|
790
|
+
- 构建期确保 API 可访问
|
|
791
|
+
- 为 Controller 提供 fallback / mock 数据
|
|
580
792
|
|
|
581
|
-
##### 3
|
|
793
|
+
##### 3)验证静态产物时,应直接查看 `dist/static/`
|
|
582
794
|
|
|
583
|
-
|
|
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
|
-
##
|
|
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
|
|
618
|
-
//
|
|
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
|
-
##
|
|
843
|
+
## 手动模式:`createServer()`
|
|
634
844
|
|
|
635
|
-
|
|
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.
|
|
873
|
+
4. 在开发模式下创建 Vite middleware server
|
|
664
874
|
5. 创建 Hono app
|
|
665
|
-
6.
|
|
666
|
-
7.
|
|
875
|
+
6. 先注册业务路由
|
|
876
|
+
7. 再挂载 SSR catch-all
|
|
667
877
|
8. 启动服务
|
|
668
878
|
|
|
669
|
-
|
|
879
|
+
默认行为:
|
|
670
880
|
|
|
671
|
-
- `root`
|
|
672
|
-
- `port`
|
|
673
|
-
-
|
|
881
|
+
- `root` 默认值:`process.cwd()`
|
|
882
|
+
- `port` 默认值:`process.env.PORT ?? 3000`
|
|
883
|
+
- 根目录存在 `.env` 且安装了 `dotenv` 时,会尝试自动加载
|
|
674
884
|
|
|
675
885
|
---
|
|
676
886
|
|
|
677
887
|
## 纯浏览器模式
|
|
678
888
|
|
|
679
|
-
|
|
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
|
-
##
|
|
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`
|
|
919
|
+
### 1. `index.html` 少了 SSR 占位符
|
|
739
920
|
|
|
740
|
-
|
|
921
|
+
现象:
|
|
741
922
|
|
|
742
|
-
-
|
|
743
|
-
- SSR
|
|
744
|
-
-
|
|
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
|
-
-
|
|
760
|
-
-
|
|
940
|
+
- 直接传函数主要适用于 `dev` / `preview`
|
|
941
|
+
- 构建期更适合通过文件路径构建 `setup` 模块
|
|
761
942
|
|
|
762
|
-
|
|
943
|
+
建议:
|
|
763
944
|
|
|
764
|
-
-
|
|
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
|
-
-
|
|
957
|
+
- 使用 `dynamicRoutes` 提供具体 URL
|
|
777
958
|
|
|
778
959
|
---
|
|
779
960
|
|
|
780
|
-
### 4. `static`
|
|
961
|
+
### 4. `static` 模式构建出来的是错误页
|
|
781
962
|
|
|
782
|
-
|
|
963
|
+
原因通常是:
|
|
783
964
|
|
|
784
|
-
-
|
|
785
|
-
- 但
|
|
965
|
+
- 构建期 Controller 访问外部 API 失败
|
|
966
|
+
- 但 fallback 没有提供可用的本地数据
|
|
786
967
|
|
|
787
|
-
|
|
968
|
+
解决方式:
|
|
788
969
|
|
|
789
|
-
-
|
|
790
|
-
- 给 Controller
|
|
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
|
-
|
|
832
|
-
|
|
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
|
-
|
|
982
|
+
建议将这些目录加入 `.gitignore`。
|
|
838
983
|
|
|
839
984
|
---
|
|
840
985
|
|