@lytjs/ssr 6.5.0 → 6.7.0
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 +591 -589
- package/dist/index.cjs +22 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +22 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/dist/index.d.cts +0 -612
- package/dist/index.d.ts +0 -612
package/README.md
CHANGED
|
@@ -1,589 +1,591 @@
|
|
|
1
|
-
# @lytjs/ssr
|
|
2
|
-
|
|
3
|
-
> LytJS 服务端渲染(SSR)支持,提供同构渲染、流式 SSR、静态站点生成等功能。
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@lytjs/ssr)
|
|
6
|
-
[](https://gitee.com/lytjs/lytjs/blob/main/LICENSE)
|
|
7
|
-
|
|
8
|
-
## 简介
|
|
9
|
-
|
|
10
|
-
`@lytjs/ssr` 是 LytJS 框架的服务端渲染扩展包,提供了完整的 SSR 支持能力。它允许开发者使用 LytJS 构建同构应用,同时享受服务端渲染的性能优势和客户端渲染的开发体验。
|
|
11
|
-
|
|
12
|
-
### 核心特性
|
|
13
|
-
|
|
14
|
-
- **同构渲染**:一份代码,同时运行在服务端和客户端
|
|
15
|
-
- **流式 SSR**:支持流式渲染,提升首屏加载速度
|
|
16
|
-
- **静态站点生成(SSG)**:预渲染静态页面,适合内容型网站
|
|
17
|
-
- **增量静态再生成(ISR)**:按需重新生成特定页面
|
|
18
|
-
- **数据预取**:服务端组件数据预取和脱水/水合
|
|
19
|
-
- **水合优化**:智能水合策略,减少 hydration 开销
|
|
20
|
-
- **组件虚拟列表**:服务端友好的虚拟列表实现
|
|
21
|
-
|
|
22
|
-
## 安装
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npm install @lytjs/ssr
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
或使用 pnpm:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pnpm add @lytjs/ssr
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## 依赖关系
|
|
35
|
-
|
|
36
|
-
`@lytjs/ssr` 依赖以下 LytJS 核心包:
|
|
37
|
-
|
|
38
|
-
- `@lytjs/reactivity` - 响应式系统
|
|
39
|
-
- `@lytjs/component` - 组件系统
|
|
40
|
-
- `@lytjs/vdom` - 虚拟 DOM
|
|
41
|
-
- `@lytjs/common-is` - 工具函数
|
|
42
|
-
- `@lytjs/common-env` - 环境检测
|
|
43
|
-
- `@lytjs/common-dom` - DOM 工具函数
|
|
44
|
-
|
|
45
|
-
## 快速开始
|
|
46
|
-
|
|
47
|
-
### 服务端渲染基础
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import { renderToString } from '@lytjs/ssr';
|
|
51
|
-
import { createApp } from './app';
|
|
52
|
-
|
|
53
|
-
async function render(url: string) {
|
|
54
|
-
const app = createApp({
|
|
55
|
-
url,
|
|
56
|
-
context: { url }
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const html = await renderToString(app);
|
|
60
|
-
return html;
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### 获取完整的 HTML
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { renderToHtml } from '@lytjs/ssr';
|
|
68
|
-
import { createApp } from './app';
|
|
69
|
-
|
|
70
|
-
async function renderPage(url: string) {
|
|
71
|
-
const app = createApp({ url });
|
|
72
|
-
|
|
73
|
-
const { html, state } = await renderToHtml(app, {
|
|
74
|
-
title: 'LytJS SSR App',
|
|
75
|
-
baseUrl: 'https://example.com'
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return html;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## 主要 API
|
|
83
|
-
|
|
84
|
-
### 渲染函数
|
|
85
|
-
|
|
86
|
-
#### `renderToString(app)`
|
|
87
|
-
|
|
88
|
-
将应用渲染为 HTML 字符串。
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { renderToString } from '@lytjs/ssr';
|
|
92
|
-
import { createSSRApp } from 'lytjs';
|
|
93
|
-
|
|
94
|
-
const app = createSSRApp(App, { props: { initialData } });
|
|
95
|
-
const html = await renderToString(app);
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
#### `renderToHtml(app, options)`
|
|
99
|
-
|
|
100
|
-
获取完整的 HTML 页面,包含 DOCTYPE、head 和 body。
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { renderToHtml } from '@lytjs/ssr';
|
|
104
|
-
|
|
105
|
-
const { html, state } = await renderToHtml(app, {
|
|
106
|
-
title: '我的应用',
|
|
107
|
-
lang: 'zh-CN',
|
|
108
|
-
baseUrl: 'https://example.com',
|
|
109
|
-
scripts: ['/client.js'],
|
|
110
|
-
styles: ['/styles.css']
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### 流式渲染
|
|
115
|
-
|
|
116
|
-
#### `renderToStream(app, options)`
|
|
117
|
-
|
|
118
|
-
流式渲染,边生成边输出。
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { renderToStream } from '@lytjs/ssr';
|
|
122
|
-
|
|
123
|
-
const stream = await renderToStream(app, {
|
|
124
|
-
onShellReady() {
|
|
125
|
-
response.setHeader('Content-Type', 'text/html');
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
stream.pipe(response);
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
#### `renderToStreamAsync(app, options)`
|
|
133
|
-
|
|
134
|
-
带异步数据预取的流式渲染。
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
import { renderToStreamAsync } from '@lytjs/ssr';
|
|
138
|
-
|
|
139
|
-
const stream = await renderToStreamAsync(app, {
|
|
140
|
-
context: { data: await fetchInitialData() },
|
|
141
|
-
onAllReady() {
|
|
142
|
-
response.setHeader('Content-Type', 'text/html');
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### `renderToStreamEnhanced(app, options)`
|
|
148
|
-
|
|
149
|
-
增强型流式渲染,支持 Suspense 边界。
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
import { renderToStreamEnhanced } from '@lytjs/ssr';
|
|
153
|
-
|
|
154
|
-
const enhancedStream = await renderToStreamEnhanced(app, {
|
|
155
|
-
onComplete() {
|
|
156
|
-
console.log('渲染完成');
|
|
157
|
-
},
|
|
158
|
-
onError(error) {
|
|
159
|
-
console.error('渲染错误:', error);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### 流式渲染选项
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
interface StreamRenderOptions {
|
|
168
|
-
context?: Record<string, any>;
|
|
169
|
-
onShellReady?: () => void;
|
|
170
|
-
onComplete?: () => void;
|
|
171
|
-
onError?: (error: Error) => void;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
interface EnhancedStreamRenderOptions extends StreamRenderOptions {
|
|
175
|
-
timeout?: number;
|
|
176
|
-
streamOptions?: {
|
|
177
|
-
flush?: 'sync' | 'async' | 'deferred';
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## 静态站点生成(SSG)
|
|
183
|
-
|
|
184
|
-
### 静态页面生成
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
import { generateStaticPages } from '@lytjs/ssr';
|
|
188
|
-
import { createApp } from './app';
|
|
189
|
-
import { getAllRoutes } from './routes';
|
|
190
|
-
|
|
191
|
-
async function buildStaticSite() {
|
|
192
|
-
const routes = await getAllRoutes();
|
|
193
|
-
|
|
194
|
-
await generateStaticPages(routes, {
|
|
195
|
-
outputDir: './dist',
|
|
196
|
-
render: async (url) => {
|
|
197
|
-
const app = createApp({ url });
|
|
198
|
-
return renderToHtml(app, { title: 'My Site' });
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### 生成路由清单
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
import { generateRouteManifest } from '@lytjs/ssr';
|
|
208
|
-
|
|
209
|
-
const manifest = await generateRouteManifest(routes, {
|
|
210
|
-
basePath: '/pages',
|
|
211
|
-
includePatterns: ['*.html'],
|
|
212
|
-
excludePatterns: ['**/404.html']
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
console.log(manifest);
|
|
216
|
-
// { routes: [...], total: 100, generated: Date }
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### 验证静态页面
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
import { validatePages } from '@lytjs/ssr';
|
|
223
|
-
|
|
224
|
-
const results = await validatePages('./dist', {
|
|
225
|
-
checkLinks: true,
|
|
226
|
-
checkImages: true,
|
|
227
|
-
checkScripts: true
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (results.errors.length > 0) {
|
|
231
|
-
console.error('页面验证失败:', results.errors);
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### 写入静态文件
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
import { writeStaticFiles } from '@lytjs/ssr';
|
|
239
|
-
|
|
240
|
-
await writeStaticFiles('./dist', {
|
|
241
|
-
manifest: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
registerServerComponent
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
import
|
|
414
|
-
import {
|
|
415
|
-
import
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
app
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
import
|
|
444
|
-
import {
|
|
445
|
-
import
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
app
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
registerServerComponent
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1
|
+
# @lytjs/ssr
|
|
2
|
+
|
|
3
|
+
> LytJS 服务端渲染(SSR)支持,提供同构渲染、流式 SSR、静态站点生成等功能。
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@lytjs/ssr)
|
|
6
|
+
[](https://gitee.com/lytjs/lytjs/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## 简介
|
|
9
|
+
|
|
10
|
+
`@lytjs/ssr` 是 LytJS 框架的服务端渲染扩展包,提供了完整的 SSR 支持能力。它允许开发者使用 LytJS 构建同构应用,同时享受服务端渲染的性能优势和客户端渲染的开发体验。
|
|
11
|
+
|
|
12
|
+
### 核心特性
|
|
13
|
+
|
|
14
|
+
- **同构渲染**:一份代码,同时运行在服务端和客户端
|
|
15
|
+
- **流式 SSR**:支持流式渲染,提升首屏加载速度
|
|
16
|
+
- **静态站点生成(SSG)**:预渲染静态页面,适合内容型网站
|
|
17
|
+
- **增量静态再生成(ISR)**:按需重新生成特定页面
|
|
18
|
+
- **数据预取**:服务端组件数据预取和脱水/水合
|
|
19
|
+
- **水合优化**:智能水合策略,减少 hydration 开销
|
|
20
|
+
- **组件虚拟列表**:服务端友好的虚拟列表实现
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @lytjs/ssr
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
或使用 pnpm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @lytjs/ssr
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 依赖关系
|
|
35
|
+
|
|
36
|
+
`@lytjs/ssr` 依赖以下 LytJS 核心包:
|
|
37
|
+
|
|
38
|
+
- `@lytjs/reactivity` - 响应式系统
|
|
39
|
+
- `@lytjs/component` - 组件系统
|
|
40
|
+
- `@lytjs/vdom` - 虚拟 DOM
|
|
41
|
+
- `@lytjs/common-is` - 工具函数
|
|
42
|
+
- `@lytjs/common-env` - 环境检测
|
|
43
|
+
- `@lytjs/common-dom` - DOM 工具函数
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
### 服务端渲染基础
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { renderToString } from '@lytjs/ssr';
|
|
51
|
+
import { createApp } from './app';
|
|
52
|
+
|
|
53
|
+
async function render(url: string) {
|
|
54
|
+
const app = createApp({
|
|
55
|
+
url,
|
|
56
|
+
context: { url },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const html = await renderToString(app);
|
|
60
|
+
return html;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 获取完整的 HTML
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { renderToHtml } from '@lytjs/ssr';
|
|
68
|
+
import { createApp } from './app';
|
|
69
|
+
|
|
70
|
+
async function renderPage(url: string) {
|
|
71
|
+
const app = createApp({ url });
|
|
72
|
+
|
|
73
|
+
const { html, state } = await renderToHtml(app, {
|
|
74
|
+
title: 'LytJS SSR App',
|
|
75
|
+
baseUrl: 'https://example.com',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return html;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 主要 API
|
|
83
|
+
|
|
84
|
+
### 渲染函数
|
|
85
|
+
|
|
86
|
+
#### `renderToString(app)`
|
|
87
|
+
|
|
88
|
+
将应用渲染为 HTML 字符串。
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { renderToString } from '@lytjs/ssr';
|
|
92
|
+
import { createSSRApp } from 'lytjs';
|
|
93
|
+
|
|
94
|
+
const app = createSSRApp(App, { props: { initialData } });
|
|
95
|
+
const html = await renderToString(app);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `renderToHtml(app, options)`
|
|
99
|
+
|
|
100
|
+
获取完整的 HTML 页面,包含 DOCTYPE、head 和 body。
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { renderToHtml } from '@lytjs/ssr';
|
|
104
|
+
|
|
105
|
+
const { html, state } = await renderToHtml(app, {
|
|
106
|
+
title: '我的应用',
|
|
107
|
+
lang: 'zh-CN',
|
|
108
|
+
baseUrl: 'https://example.com',
|
|
109
|
+
scripts: ['/client.js'],
|
|
110
|
+
styles: ['/styles.css'],
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 流式渲染
|
|
115
|
+
|
|
116
|
+
#### `renderToStream(app, options)`
|
|
117
|
+
|
|
118
|
+
流式渲染,边生成边输出。
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { renderToStream } from '@lytjs/ssr';
|
|
122
|
+
|
|
123
|
+
const stream = await renderToStream(app, {
|
|
124
|
+
onShellReady() {
|
|
125
|
+
response.setHeader('Content-Type', 'text/html');
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
stream.pipe(response);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `renderToStreamAsync(app, options)`
|
|
133
|
+
|
|
134
|
+
带异步数据预取的流式渲染。
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { renderToStreamAsync } from '@lytjs/ssr';
|
|
138
|
+
|
|
139
|
+
const stream = await renderToStreamAsync(app, {
|
|
140
|
+
context: { data: await fetchInitialData() },
|
|
141
|
+
onAllReady() {
|
|
142
|
+
response.setHeader('Content-Type', 'text/html');
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### `renderToStreamEnhanced(app, options)`
|
|
148
|
+
|
|
149
|
+
增强型流式渲染,支持 Suspense 边界。
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { renderToStreamEnhanced } from '@lytjs/ssr';
|
|
153
|
+
|
|
154
|
+
const enhancedStream = await renderToStreamEnhanced(app, {
|
|
155
|
+
onComplete() {
|
|
156
|
+
console.log('渲染完成');
|
|
157
|
+
},
|
|
158
|
+
onError(error) {
|
|
159
|
+
console.error('渲染错误:', error);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 流式渲染选项
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
interface StreamRenderOptions {
|
|
168
|
+
context?: Record<string, any>;
|
|
169
|
+
onShellReady?: () => void;
|
|
170
|
+
onComplete?: () => void;
|
|
171
|
+
onError?: (error: Error) => void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface EnhancedStreamRenderOptions extends StreamRenderOptions {
|
|
175
|
+
timeout?: number;
|
|
176
|
+
streamOptions?: {
|
|
177
|
+
flush?: 'sync' | 'async' | 'deferred';
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 静态站点生成(SSG)
|
|
183
|
+
|
|
184
|
+
### 静态页面生成
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { generateStaticPages } from '@lytjs/ssr';
|
|
188
|
+
import { createApp } from './app';
|
|
189
|
+
import { getAllRoutes } from './routes';
|
|
190
|
+
|
|
191
|
+
async function buildStaticSite() {
|
|
192
|
+
const routes = await getAllRoutes();
|
|
193
|
+
|
|
194
|
+
await generateStaticPages(routes, {
|
|
195
|
+
outputDir: './dist',
|
|
196
|
+
render: async (url) => {
|
|
197
|
+
const app = createApp({ url });
|
|
198
|
+
return renderToHtml(app, { title: 'My Site' });
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 生成路由清单
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { generateRouteManifest } from '@lytjs/ssr';
|
|
208
|
+
|
|
209
|
+
const manifest = await generateRouteManifest(routes, {
|
|
210
|
+
basePath: '/pages',
|
|
211
|
+
includePatterns: ['*.html'],
|
|
212
|
+
excludePatterns: ['**/404.html'],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log(manifest);
|
|
216
|
+
// { routes: [...], total: 100, generated: Date }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 验证静态页面
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { validatePages } from '@lytjs/ssr';
|
|
223
|
+
|
|
224
|
+
const results = await validatePages('./dist', {
|
|
225
|
+
checkLinks: true,
|
|
226
|
+
checkImages: true,
|
|
227
|
+
checkScripts: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (results.errors.length > 0) {
|
|
231
|
+
console.error('页面验证失败:', results.errors);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 写入静态文件
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { writeStaticFiles } from '@lytjs/ssr';
|
|
239
|
+
|
|
240
|
+
await writeStaticFiles('./dist', {
|
|
241
|
+
manifest: {
|
|
242
|
+
/* 路由清单 */
|
|
243
|
+
},
|
|
244
|
+
copyAssets: ['./public/**/*'],
|
|
245
|
+
minify: true,
|
|
246
|
+
sitemap: true,
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 增量静态再生成(ISR)
|
|
251
|
+
|
|
252
|
+
### 创建 ISR 中间件
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { createISRMiddleware } from '@lytjs/ssr';
|
|
256
|
+
|
|
257
|
+
const isr = createISRMiddleware({
|
|
258
|
+
revalidate: '/blog/:slug',
|
|
259
|
+
revalidateInterval: 60,
|
|
260
|
+
maxRetries: 3,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
app.use(isr);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 按需重新验证
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { revalidateOnDemand } from '@lytjs/ssr';
|
|
270
|
+
|
|
271
|
+
await revalidateOnDemand('/blog/my-post', {
|
|
272
|
+
secret: process.env.REVALIDATE_SECRET,
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### ISR 缓存管理
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { getISRCacheStats, clearISRCache } from '@lytjs/ssr';
|
|
280
|
+
|
|
281
|
+
const stats = getISRCacheStats();
|
|
282
|
+
console.log(stats);
|
|
283
|
+
// { pages: 100, size: '2.5MB', lastUpdate: Date }
|
|
284
|
+
|
|
285
|
+
await clearISRCache();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 服务端组件
|
|
289
|
+
|
|
290
|
+
### 注册服务端组件
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { registerServerComponent } from '@lytjs/ssr';
|
|
294
|
+
|
|
295
|
+
registerServerComponent('UserProfile', {
|
|
296
|
+
fetchData: async (context) => {
|
|
297
|
+
return await api.getUser(context.params.id);
|
|
298
|
+
},
|
|
299
|
+
serverOnly: true,
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 收集预取组件
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { collectPrefetchComponents } from '@lytjs/ssr';
|
|
307
|
+
|
|
308
|
+
const components = await collectPrefetchComponents(app);
|
|
309
|
+
console.log(components);
|
|
310
|
+
// ['UserProfile', 'ProductList', 'CommentSection']
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 预取所有组件
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { prefetchAllComponents } from '@lytjs/ssr';
|
|
317
|
+
|
|
318
|
+
await prefetchAllComponents(components, {
|
|
319
|
+
context: { userId: '123' },
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 构建脱水状态
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { buildDehydratedState } from '@lytjs/ssr';
|
|
327
|
+
|
|
328
|
+
const state = buildDehydratedState(app);
|
|
329
|
+
const serialized = safeSerializeState(state);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 安全序列化
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { safeSerializeState, safeDeserializeState } from '@lytjs/ssr';
|
|
336
|
+
|
|
337
|
+
const serialized = safeSerializeState(data);
|
|
338
|
+
const deserialized = safeDeserializeState(serialized);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 水合优化
|
|
342
|
+
|
|
343
|
+
### 创建水合标记
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { createHydrationMarkers } from '@lytjs/ssr';
|
|
347
|
+
|
|
348
|
+
const markers = createHydrationMarkers(app, {
|
|
349
|
+
includeTimestamps: true,
|
|
350
|
+
includeVersions: true,
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 获取水合策略
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { getHydrationStrategy } from '@lytjs/ssr';
|
|
358
|
+
|
|
359
|
+
const strategy = getHydrationStrategy(app, {
|
|
360
|
+
mode: 'eager',
|
|
361
|
+
delay: 100,
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### 序列化水合状态
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { serializeHydrationState } from '@lytjs/ssr';
|
|
369
|
+
|
|
370
|
+
const state = serializeHydrationState(markers);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### 创建脱水状态
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { createDehydratedState } from '@lytjs/ssr';
|
|
377
|
+
|
|
378
|
+
const dehydrated = createDehydratedState(app, {
|
|
379
|
+
includeSignals: true,
|
|
380
|
+
includeComponents: true,
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## 虚拟列表
|
|
385
|
+
|
|
386
|
+
### VirtualList 组件
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { VirtualList } from '@lytjs/ssr';
|
|
390
|
+
|
|
391
|
+
export function ProductList({ products }) {
|
|
392
|
+
return () => (
|
|
393
|
+
<VirtualList
|
|
394
|
+
items={products}
|
|
395
|
+
itemHeight={80}
|
|
396
|
+
overscan={3}
|
|
397
|
+
renderItem={(product) => (
|
|
398
|
+
<div class="product-item">
|
|
399
|
+
<img src={product.image} />
|
|
400
|
+
<span>{product.name}</span>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## 服务端渲染集成
|
|
409
|
+
|
|
410
|
+
### Express 集成
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import express from 'express';
|
|
414
|
+
import { renderToHtml } from '@lytjs/ssr';
|
|
415
|
+
import { createSSRApp } from 'lytjs';
|
|
416
|
+
import { createRouter, createMemoryHistory } from '@lytjs/router';
|
|
417
|
+
import App from './App';
|
|
418
|
+
|
|
419
|
+
const app = express();
|
|
420
|
+
|
|
421
|
+
app.get('*', async (req, res) => {
|
|
422
|
+
const router = createRouter({
|
|
423
|
+
history: createMemoryHistory(req.url),
|
|
424
|
+
routes: getRoutes(),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const lytApp = createSSRApp(App, { router });
|
|
428
|
+
|
|
429
|
+
const html = await renderToHtml(lytApp, {
|
|
430
|
+
title: 'LytJS SSR',
|
|
431
|
+
lang: 'zh-CN',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
res.send(html);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
app.listen(3000);
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Koa 集成
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import Koa from 'koa';
|
|
444
|
+
import { renderToStream } from '@lytjs/ssr';
|
|
445
|
+
import { createSSRApp } from 'lytjs';
|
|
446
|
+
import { createRouter, createMemoryHistory } from '@lytjs/router';
|
|
447
|
+
import App from './App';
|
|
448
|
+
|
|
449
|
+
const app = new Koa();
|
|
450
|
+
|
|
451
|
+
app.use(async (ctx) => {
|
|
452
|
+
const router = createRouter({
|
|
453
|
+
history: createMemoryHistory(ctx.url),
|
|
454
|
+
routes: getRoutes(),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const lytApp = createSSRApp(App, { router });
|
|
458
|
+
const stream = await renderToStream(lytApp);
|
|
459
|
+
|
|
460
|
+
ctx.type = 'text/html';
|
|
461
|
+
ctx.body = stream;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
app.listen(3000);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## 类型定义
|
|
468
|
+
|
|
469
|
+
### SSG 配置
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
interface SSGPage {
|
|
473
|
+
url: string;
|
|
474
|
+
outputPath: string;
|
|
475
|
+
content: string;
|
|
476
|
+
metadata?: {
|
|
477
|
+
title?: string;
|
|
478
|
+
description?: string;
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
interface SSGOptions {
|
|
483
|
+
outputDir?: string;
|
|
484
|
+
basePath?: string;
|
|
485
|
+
concurrency?: number;
|
|
486
|
+
onProgress?: (current: number, total: number) => void;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 数据预取上下文
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
interface DataPrefetchContext {
|
|
494
|
+
request: Request;
|
|
495
|
+
params: Record<string, string>;
|
|
496
|
+
query: Record<string, string>;
|
|
497
|
+
cookies: Record<string, string>;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
interface PrefetchResult<T = any> {
|
|
501
|
+
data: T;
|
|
502
|
+
ttl?: number;
|
|
503
|
+
tags?: string[];
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## 最佳实践
|
|
508
|
+
|
|
509
|
+
### 服务端渲染优化
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { renderToStream } from '@lytjs/ssr';
|
|
513
|
+
|
|
514
|
+
const stream = await renderToStream(app, {
|
|
515
|
+
onShellReady() {
|
|
516
|
+
response.setHeader('Content-Type', 'text/html');
|
|
517
|
+
response.setHeader('Transfer-Encoding', 'chunked');
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
stream.pipe(response);
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 数据缓存策略
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { registerServerComponent } from '@lytjs/ssr';
|
|
528
|
+
|
|
529
|
+
registerServerComponent('ProductList', {
|
|
530
|
+
fetchData: async (context) => {
|
|
531
|
+
const cacheKey = `products:${context.query.category}`;
|
|
532
|
+
const cached = await cache.get(cacheKey);
|
|
533
|
+
|
|
534
|
+
if (cached) return cached;
|
|
535
|
+
|
|
536
|
+
const data = await api.getProducts(context.query);
|
|
537
|
+
await cache.set(cacheKey, data, { ttl: 300 });
|
|
538
|
+
|
|
539
|
+
return data;
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### 水合性能优化
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { getHydrationStrategy } from '@lytjs/ssr';
|
|
548
|
+
|
|
549
|
+
const strategy = getHydrationStrategy(app, {
|
|
550
|
+
mode: 'lazy',
|
|
551
|
+
threshold: 0.1,
|
|
552
|
+
priority: ['above-fold', 'critical'],
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## 常见问题
|
|
557
|
+
|
|
558
|
+
### 如何处理客户端专有 API?
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { isClient } from '@lytjs/common-env';
|
|
562
|
+
|
|
563
|
+
if (isClient) {
|
|
564
|
+
localStorage.setItem('key', 'value');
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### 如何在 SSR 中使用 window 对象?
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { isServer } from '@lytjs/common-env';
|
|
572
|
+
|
|
573
|
+
if (!isServer) {
|
|
574
|
+
window.addEventListener('resize', handleResize);
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## 浏览器兼容性
|
|
579
|
+
|
|
580
|
+
服务端渲染无需考虑浏览器兼容性。客户端水合代码支持所有现代浏览器。
|
|
581
|
+
|
|
582
|
+
## 许可证
|
|
583
|
+
|
|
584
|
+
MIT License - [查看许可证](https://gitee.com/lytjs/lytjs/blob/main/LICENSE)
|
|
585
|
+
|
|
586
|
+
## 贡献指南
|
|
587
|
+
|
|
588
|
+
欢迎提交 Issue 和 Pull Request!
|
|
589
|
+
|
|
590
|
+
- [Gitee 仓库](https://gitee.com/lytjs/lytjs)
|
|
591
|
+
- [问题反馈](https://gitee.com/lytjs/lytjs/issues)
|