@lytjs/ssr 6.4.0 → 6.6.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,591 @@
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
+ /* 路由清单 */
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)
package/dist/index.cjs CHANGED
@@ -39,11 +39,28 @@ function renderToString(vnode) {
39
39
  if (commonIs.isString(node.type)) {
40
40
  const tag = node.type;
41
41
  const props = node.props || {};
42
- const voidTags = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
42
+ const voidTags = [
43
+ "area",
44
+ "base",
45
+ "br",
46
+ "col",
47
+ "embed",
48
+ "hr",
49
+ "img",
50
+ "input",
51
+ "link",
52
+ "meta",
53
+ "param",
54
+ "source",
55
+ "track",
56
+ "wbr"
57
+ ];
43
58
  if (voidTags.includes(tag)) {
44
59
  return `<${tag}${renderAttributes(props)}>`;
45
60
  }
46
- const children = renderToString(node.children);
61
+ const children = renderToString(
62
+ node.children
63
+ );
47
64
  return `<${tag}${renderAttributes(props)}>${children}</${tag}>`;
48
65
  }
49
66
  return "";
@@ -113,6 +130,7 @@ var VirtualList = component.defineComponent({
113
130
  buffer: { type: Number, default: 5 },
114
131
  class: { type: String, default: "" }
115
132
  },
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
134
  setup(props, { slots }) {
117
135
  const scrollTop = reactivity.signal(0);
118
136
  const visibleCount = reactivity.computedSignal(() => {
@@ -146,6 +164,7 @@ var VirtualList = component.defineComponent({
146
164
  return () => {
147
165
  const items = visibleData().map((item, index) => {
148
166
  const actualIndex = startIndex() + index;
167
+ const slotContent = slots?.default?.({ item, index: actualIndex });
149
168
  return vdom.createVNode(
150
169
  "div",
151
170
  {
@@ -153,7 +172,7 @@ var VirtualList = component.defineComponent({
153
172
  style: `height: ${props.itemHeight}px;`,
154
173
  "data-index": actualIndex
155
174
  },
156
- slots.default?.({ item, index: actualIndex })
175
+ slotContent
157
176
  );
158
177
  });
159
178
  return vdom.createVNode(
@@ -297,7 +316,7 @@ function renderToStream(vnode, options) {
297
316
  errorRecovery = true
298
317
  } = options || {};
299
318
  const encoder = new TextEncoder();
300
- let flowController = maxBytesPerSecond ? new FlowController(maxBytesPerSecond) : null;
319
+ const flowController = maxBytesPerSecond ? new FlowController(maxBytesPerSecond) : null;
301
320
  let timeoutId = null;
302
321
  return new ReadableStream({
303
322
  start(controller) {
@@ -358,7 +377,7 @@ function renderToStream(vnode, options) {
358
377
  console.warn("Error occurred during stream rendering, using fallback HTML");
359
378
  controller.enqueue(encoder.encode(fallbackHtml));
360
379
  controller.close();
361
- } catch (e) {
380
+ } catch (_) {
362
381
  controller.error(error);
363
382
  }
364
383
  } else {
@@ -370,7 +389,7 @@ function renderToStream(vnode, options) {
370
389
  if (timeoutId) {
371
390
  clearTimeout(timeoutId);
372
391
  }
373
- console.log("Stream cancelled:", reason);
392
+ console.warn("Stream cancelled:", reason);
374
393
  }
375
394
  });
376
395
  function sendChunk(controller, encoder2, chunk) {
@@ -1054,9 +1073,7 @@ function createHydrationMarkers(vnode) {
1054
1073
  }
1055
1074
  const node = vnode;
1056
1075
  if (commonIs.isArray(vnode)) {
1057
- return vnode.map(
1058
- (child) => createHydrationMarkers(child)
1059
- );
1076
+ return vnode.map((child) => createHydrationMarkers(child));
1060
1077
  }
1061
1078
  if (isElementVNode(node)) {
1062
1079
  const id = generateComponentId();
@@ -1070,9 +1087,7 @@ function createHydrationMarkers(vnode) {
1070
1087
  let processedChildren = children;
1071
1088
  if (children !== null && !commonIs.isString(children) && !commonIs.isNumber(children)) {
1072
1089
  if (commonIs.isArray(children)) {
1073
- processedChildren = children.map(
1074
- (child) => createHydrationMarkers(child)
1075
- );
1090
+ processedChildren = children.map((child) => createHydrationMarkers(child));
1076
1091
  } else if (commonIs.isObject(children)) {
1077
1092
  processedChildren = [createHydrationMarkers(children)];
1078
1093
  }
@@ -1085,9 +1100,7 @@ function createHydrationMarkers(vnode) {
1085
1100
  if (commonIs.isArray(children)) {
1086
1101
  return {
1087
1102
  ...node,
1088
- children: children.map(
1089
- (child) => createHydrationMarkers(child)
1090
- )
1103
+ children: children.map((child) => createHydrationMarkers(child))
1091
1104
  };
1092
1105
  } else if (commonIs.isObject(children)) {
1093
1106
  return {