@lytjs/ssr 6.4.0 → 6.5.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 ADDED
@@ -0,0 +1,589 @@
1
+ # @lytjs/ssr
2
+
3
+ > LytJS 服务端渲染(SSR)支持,提供同构渲染、流式 SSR、静态站点生成等功能。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@lytjs/ssr.svg)](https://www.npmjs.com/package/@lytjs/ssr)
6
+ [![license](https://img.shields.io/npm/l/@lytjs/ssr.svg)](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
+ copyAssets: ['./public/**/*'],
243
+ minify: true,
244
+ sitemap: true
245
+ });
246
+ ```
247
+
248
+ ## 增量静态再生成(ISR)
249
+
250
+ ### 创建 ISR 中间件
251
+
252
+ ```typescript
253
+ import { createISRMiddleware } from '@lytjs/ssr';
254
+
255
+ const isr = createISRMiddleware({
256
+ revalidate: '/blog/:slug',
257
+ revalidateInterval: 60,
258
+ maxRetries: 3
259
+ });
260
+
261
+ app.use(isr);
262
+ ```
263
+
264
+ ### 按需重新验证
265
+
266
+ ```typescript
267
+ import { revalidateOnDemand } from '@lytjs/ssr';
268
+
269
+ await revalidateOnDemand('/blog/my-post', {
270
+ secret: process.env.REVALIDATE_SECRET
271
+ });
272
+ ```
273
+
274
+ ### ISR 缓存管理
275
+
276
+ ```typescript
277
+ import { getISRCacheStats, clearISRCache } from '@lytjs/ssr';
278
+
279
+ const stats = getISRCacheStats();
280
+ console.log(stats);
281
+ // { pages: 100, size: '2.5MB', lastUpdate: Date }
282
+
283
+ await clearISRCache();
284
+ ```
285
+
286
+ ## 服务端组件
287
+
288
+ ### 注册服务端组件
289
+
290
+ ```typescript
291
+ import { registerServerComponent } from '@lytjs/ssr';
292
+
293
+ registerServerComponent('UserProfile', {
294
+ fetchData: async (context) => {
295
+ return await api.getUser(context.params.id);
296
+ },
297
+ serverOnly: true
298
+ });
299
+ ```
300
+
301
+ ### 收集预取组件
302
+
303
+ ```typescript
304
+ import { collectPrefetchComponents } from '@lytjs/ssr';
305
+
306
+ const components = await collectPrefetchComponents(app);
307
+ console.log(components);
308
+ // ['UserProfile', 'ProductList', 'CommentSection']
309
+ ```
310
+
311
+ ### 预取所有组件
312
+
313
+ ```typescript
314
+ import { prefetchAllComponents } from '@lytjs/ssr';
315
+
316
+ await prefetchAllComponents(components, {
317
+ context: { userId: '123' }
318
+ });
319
+ ```
320
+
321
+ ### 构建脱水状态
322
+
323
+ ```typescript
324
+ import { buildDehydratedState } from '@lytjs/ssr';
325
+
326
+ const state = buildDehydratedState(app);
327
+ const serialized = safeSerializeState(state);
328
+ ```
329
+
330
+ ### 安全序列化
331
+
332
+ ```typescript
333
+ import { safeSerializeState, safeDeserializeState } from '@lytjs/ssr';
334
+
335
+ const serialized = safeSerializeState(data);
336
+ const deserialized = safeDeserializeState(serialized);
337
+ ```
338
+
339
+ ## 水合优化
340
+
341
+ ### 创建水合标记
342
+
343
+ ```typescript
344
+ import { createHydrationMarkers } from '@lytjs/ssr';
345
+
346
+ const markers = createHydrationMarkers(app, {
347
+ includeTimestamps: true,
348
+ includeVersions: true
349
+ });
350
+ ```
351
+
352
+ ### 获取水合策略
353
+
354
+ ```typescript
355
+ import { getHydrationStrategy } from '@lytjs/ssr';
356
+
357
+ const strategy = getHydrationStrategy(app, {
358
+ mode: 'eager',
359
+ delay: 100
360
+ });
361
+ ```
362
+
363
+ ### 序列化水合状态
364
+
365
+ ```typescript
366
+ import { serializeHydrationState } from '@lytjs/ssr';
367
+
368
+ const state = serializeHydrationState(markers);
369
+ ```
370
+
371
+ ### 创建脱水状态
372
+
373
+ ```typescript
374
+ import { createDehydratedState } from '@lytjs/ssr';
375
+
376
+ const dehydrated = createDehydratedState(app, {
377
+ includeSignals: true,
378
+ includeComponents: true
379
+ });
380
+ ```
381
+
382
+ ## 虚拟列表
383
+
384
+ ### VirtualList 组件
385
+
386
+ ```typescript
387
+ import { VirtualList } from '@lytjs/ssr';
388
+
389
+ export function ProductList({ products }) {
390
+ return () => (
391
+ <VirtualList
392
+ items={products}
393
+ itemHeight={80}
394
+ overscan={3}
395
+ renderItem={(product) => (
396
+ <div class="product-item">
397
+ <img src={product.image} />
398
+ <span>{product.name}</span>
399
+ </div>
400
+ )}
401
+ />
402
+ );
403
+ }
404
+ ```
405
+
406
+ ## 服务端渲染集成
407
+
408
+ ### Express 集成
409
+
410
+ ```typescript
411
+ import express from 'express';
412
+ import { renderToHtml } from '@lytjs/ssr';
413
+ import { createSSRApp } from 'lytjs';
414
+ import { createRouter, createMemoryHistory } from '@lytjs/router';
415
+ import App from './App';
416
+
417
+ const app = express();
418
+
419
+ app.get('*', async (req, res) => {
420
+ const router = createRouter({
421
+ history: createMemoryHistory(req.url),
422
+ routes: getRoutes()
423
+ });
424
+
425
+ const lytApp = createSSRApp(App, { router });
426
+
427
+ const html = await renderToHtml(lytApp, {
428
+ title: 'LytJS SSR',
429
+ lang: 'zh-CN'
430
+ });
431
+
432
+ res.send(html);
433
+ });
434
+
435
+ app.listen(3000);
436
+ ```
437
+
438
+ ### Koa 集成
439
+
440
+ ```typescript
441
+ import Koa from 'koa';
442
+ import { renderToStream } from '@lytjs/ssr';
443
+ import { createSSRApp } from 'lytjs';
444
+ import { createRouter, createMemoryHistory } from '@lytjs/router';
445
+ import App from './App';
446
+
447
+ const app = new Koa();
448
+
449
+ app.use(async (ctx) => {
450
+ const router = createRouter({
451
+ history: createMemoryHistory(ctx.url),
452
+ routes: getRoutes()
453
+ });
454
+
455
+ const lytApp = createSSRApp(App, { router });
456
+ const stream = await renderToStream(lytApp);
457
+
458
+ ctx.type = 'text/html';
459
+ ctx.body = stream;
460
+ });
461
+
462
+ app.listen(3000);
463
+ ```
464
+
465
+ ## 类型定义
466
+
467
+ ### SSG 配置
468
+
469
+ ```typescript
470
+ interface SSGPage {
471
+ url: string;
472
+ outputPath: string;
473
+ content: string;
474
+ metadata?: {
475
+ title?: string;
476
+ description?: string;
477
+ };
478
+ }
479
+
480
+ interface SSGOptions {
481
+ outputDir?: string;
482
+ basePath?: string;
483
+ concurrency?: number;
484
+ onProgress?: (current: number, total: number) => void;
485
+ }
486
+ ```
487
+
488
+ ### 数据预取上下文
489
+
490
+ ```typescript
491
+ interface DataPrefetchContext {
492
+ request: Request;
493
+ params: Record<string, string>;
494
+ query: Record<string, string>;
495
+ cookies: Record<string, string>;
496
+ }
497
+
498
+ interface PrefetchResult<T = any> {
499
+ data: T;
500
+ ttl?: number;
501
+ tags?: string[];
502
+ }
503
+ ```
504
+
505
+ ## 最佳实践
506
+
507
+ ### 服务端渲染优化
508
+
509
+ ```typescript
510
+ import { renderToStream } from '@lytjs/ssr';
511
+
512
+ const stream = await renderToStream(app, {
513
+ onShellReady() {
514
+ response.setHeader('Content-Type', 'text/html');
515
+ response.setHeader('Transfer-Encoding', 'chunked');
516
+ }
517
+ });
518
+
519
+ stream.pipe(response);
520
+ ```
521
+
522
+ ### 数据缓存策略
523
+
524
+ ```typescript
525
+ import { registerServerComponent } from '@lytjs/ssr';
526
+
527
+ registerServerComponent('ProductList', {
528
+ fetchData: async (context) => {
529
+ const cacheKey = `products:${context.query.category}`;
530
+ const cached = await cache.get(cacheKey);
531
+
532
+ if (cached) return cached;
533
+
534
+ const data = await api.getProducts(context.query);
535
+ await cache.set(cacheKey, data, { ttl: 300 });
536
+
537
+ return data;
538
+ }
539
+ });
540
+ ```
541
+
542
+ ### 水合性能优化
543
+
544
+ ```typescript
545
+ import { getHydrationStrategy } from '@lytjs/ssr';
546
+
547
+ const strategy = getHydrationStrategy(app, {
548
+ mode: 'lazy',
549
+ threshold: 0.1,
550
+ priority: ['above-fold', 'critical']
551
+ });
552
+ ```
553
+
554
+ ## 常见问题
555
+
556
+ ### 如何处理客户端专有 API?
557
+
558
+ ```typescript
559
+ import { isClient } from '@lytjs/common-env';
560
+
561
+ if (isClient) {
562
+ localStorage.setItem('key', 'value');
563
+ }
564
+ ```
565
+
566
+ ### 如何在 SSR 中使用 window 对象?
567
+
568
+ ```typescript
569
+ import { isServer } from '@lytjs/common-env';
570
+
571
+ if (!isServer) {
572
+ window.addEventListener('resize', handleResize);
573
+ }
574
+ ```
575
+
576
+ ## 浏览器兼容性
577
+
578
+ 服务端渲染无需考虑浏览器兼容性。客户端水合代码支持所有现代浏览器。
579
+
580
+ ## 许可证
581
+
582
+ MIT License - [查看许可证](https://gitee.com/lytjs/lytjs/blob/main/LICENSE)
583
+
584
+ ## 贡献指南
585
+
586
+ 欢迎提交 Issue 和 Pull Request!
587
+
588
+ - [Gitee 仓库](https://gitee.com/lytjs/lytjs)
589
+ - [问题反馈](https://gitee.com/lytjs/lytjs/issues)
package/dist/index.cjs CHANGED
@@ -113,6 +113,7 @@ var VirtualList = component.defineComponent({
113
113
  buffer: { type: Number, default: 5 },
114
114
  class: { type: String, default: "" }
115
115
  },
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
117
  setup(props, { slots }) {
117
118
  const scrollTop = reactivity.signal(0);
118
119
  const visibleCount = reactivity.computedSignal(() => {
@@ -146,6 +147,7 @@ var VirtualList = component.defineComponent({
146
147
  return () => {
147
148
  const items = visibleData().map((item, index) => {
148
149
  const actualIndex = startIndex() + index;
150
+ const slotContent = slots?.default?.({ item, index: actualIndex });
149
151
  return vdom.createVNode(
150
152
  "div",
151
153
  {
@@ -153,7 +155,7 @@ var VirtualList = component.defineComponent({
153
155
  style: `height: ${props.itemHeight}px;`,
154
156
  "data-index": actualIndex
155
157
  },
156
- slots.default?.({ item, index: actualIndex })
158
+ slotContent
157
159
  );
158
160
  });
159
161
  return vdom.createVNode(
@@ -297,7 +299,7 @@ function renderToStream(vnode, options) {
297
299
  errorRecovery = true
298
300
  } = options || {};
299
301
  const encoder = new TextEncoder();
300
- let flowController = maxBytesPerSecond ? new FlowController(maxBytesPerSecond) : null;
302
+ const flowController = maxBytesPerSecond ? new FlowController(maxBytesPerSecond) : null;
301
303
  let timeoutId = null;
302
304
  return new ReadableStream({
303
305
  start(controller) {
@@ -358,7 +360,7 @@ function renderToStream(vnode, options) {
358
360
  console.warn("Error occurred during stream rendering, using fallback HTML");
359
361
  controller.enqueue(encoder.encode(fallbackHtml));
360
362
  controller.close();
361
- } catch (e) {
363
+ } catch (_) {
362
364
  controller.error(error);
363
365
  }
364
366
  } else {
@@ -370,7 +372,7 @@ function renderToStream(vnode, options) {
370
372
  if (timeoutId) {
371
373
  clearTimeout(timeoutId);
372
374
  }
373
- console.log("Stream cancelled:", reason);
375
+ console.warn("Stream cancelled:", reason);
374
376
  }
375
377
  });
376
378
  function sendChunk(controller, encoder2, chunk) {