@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 +591 -0
- package/dist/index.cjs +28 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +28 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +50 -50
- package/dist/index.d.cts +0 -612
- package/dist/index.d.ts +0 -612
package/README.md
ADDED
|
@@ -0,0 +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
|
+
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 = [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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 {
|