@esmx/core 3.0.0-rc.10
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 +29 -0
- package/dist/app.d.ts +84 -0
- package/dist/app.mjs +34 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.mjs +73 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/gez.d.ts +703 -0
- package/dist/gez.mjs +842 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +19 -0
- package/dist/manifest-json.d.ts +48 -0
- package/dist/manifest-json.mjs +21 -0
- package/dist/module-config.d.ts +185 -0
- package/dist/module-config.mjs +79 -0
- package/dist/pack-config.d.ts +251 -0
- package/dist/pack-config.mjs +26 -0
- package/dist/render-context.d.ts +1212 -0
- package/dist/render-context.mjs +1010 -0
- package/dist/utils/cache.d.ts +49 -0
- package/dist/utils/cache.mjs +16 -0
- package/dist/utils/import-map.d.ts +8 -0
- package/dist/utils/import-map.mjs +32 -0
- package/dist/utils/middleware.d.ts +57 -0
- package/dist/utils/middleware.mjs +51 -0
- package/dist/utils/path-without-index.d.ts +1 -0
- package/dist/utils/path-without-index.mjs +9 -0
- package/dist/utils/resolve-path.d.ts +2 -0
- package/dist/utils/resolve-path.mjs +4 -0
- package/dist/utils/static-import-lexer.d.ts +26 -0
- package/dist/utils/static-import-lexer.mjs +43 -0
- package/package.json +103 -0
- package/src/app.ts +144 -0
- package/src/cli/cli.ts +99 -0
- package/src/cli/index.ts +5 -0
- package/src/gez.ts +1026 -0
- package/src/index.ts +38 -0
- package/src/manifest-json.ts +80 -0
- package/src/module-config.ts +282 -0
- package/src/pack-config.ts +301 -0
- package/src/render-context.ts +1326 -0
- package/src/utils/cache.ts +68 -0
- package/src/utils/import-map.ts +47 -0
- package/src/utils/middleware.ts +118 -0
- package/src/utils/path-without-index.ts +9 -0
- package/src/utils/resolve-path.ts +33 -0
- package/src/utils/static-import-lexer.ts +85 -0
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import serialize from "serialize-javascript";
|
|
3
|
+
export class RenderContext {
|
|
4
|
+
esmx;
|
|
5
|
+
/**
|
|
6
|
+
* 重定向地址
|
|
7
|
+
* @description
|
|
8
|
+
* - 默认为 null,表示不进行重定向
|
|
9
|
+
* - 设置后,服务端可以根据此值进行 HTTP 重定向
|
|
10
|
+
* - 常用于登录验证、权限控制等场景
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // 1. 登录验证示例
|
|
15
|
+
* export default async (rc: RenderContext) => {
|
|
16
|
+
* if (!isLoggedIn()) {
|
|
17
|
+
* rc.redirect = '/login';
|
|
18
|
+
* rc.status = 302;
|
|
19
|
+
* return;
|
|
20
|
+
* }
|
|
21
|
+
* // 继续渲染页面...
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* // 2. 权限控制示例
|
|
25
|
+
* export default async (rc: RenderContext) => {
|
|
26
|
+
* if (!hasPermission()) {
|
|
27
|
+
* rc.redirect = '/403';
|
|
28
|
+
* rc.status = 403;
|
|
29
|
+
* return;
|
|
30
|
+
* }
|
|
31
|
+
* // 继续渲染页面...
|
|
32
|
+
* };
|
|
33
|
+
*
|
|
34
|
+
* // 3. 服务端处理示例
|
|
35
|
+
* app.use(async (req, res) => {
|
|
36
|
+
* const rc = await esmx.render({
|
|
37
|
+
* params: {
|
|
38
|
+
* url: req.url
|
|
39
|
+
* }
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // 处理重定向
|
|
43
|
+
* if (rc.redirect) {
|
|
44
|
+
* res.statusCode = rc.status || 302;
|
|
45
|
+
* res.setHeader('Location', rc.redirect);
|
|
46
|
+
* res.end();
|
|
47
|
+
* return;
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* // 设置状态码
|
|
51
|
+
* if (rc.status) {
|
|
52
|
+
* res.statusCode = rc.status;
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* // 响应 HTML 内容
|
|
56
|
+
* res.end(rc.html);
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
redirect = null;
|
|
61
|
+
/**
|
|
62
|
+
* HTTP 响应状态码
|
|
63
|
+
* @description
|
|
64
|
+
* - 默认为 null,表示使用 200 状态码
|
|
65
|
+
* - 可以设置任意有效的 HTTP 状态码
|
|
66
|
+
* - 常用于错误处理、重定向等场景
|
|
67
|
+
* - 通常与 redirect 属性配合使用
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* // 1. 404 错误处理示例
|
|
72
|
+
* export default async (rc: RenderContext) => {
|
|
73
|
+
* const page = await findPage(rc.params.url);
|
|
74
|
+
* if (!page) {
|
|
75
|
+
* rc.status = 404;
|
|
76
|
+
* // 渲染 404 页面...
|
|
77
|
+
* return;
|
|
78
|
+
* }
|
|
79
|
+
* // 继续渲染页面...
|
|
80
|
+
* };
|
|
81
|
+
*
|
|
82
|
+
* // 2. 临时重定向示例
|
|
83
|
+
* export default async (rc: RenderContext) => {
|
|
84
|
+
* if (needMaintenance()) {
|
|
85
|
+
* rc.redirect = '/maintenance';
|
|
86
|
+
* rc.status = 307; // 临时重定向,保持请求方法不变
|
|
87
|
+
* return;
|
|
88
|
+
* }
|
|
89
|
+
* // 继续渲染页面...
|
|
90
|
+
* };
|
|
91
|
+
*
|
|
92
|
+
* // 3. 服务端处理示例
|
|
93
|
+
* app.use(async (req, res) => {
|
|
94
|
+
* const rc = await esmx.render({
|
|
95
|
+
* params: {
|
|
96
|
+
* url: req.url
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* // 处理重定向
|
|
101
|
+
* if (rc.redirect) {
|
|
102
|
+
* res.statusCode = rc.status || 302;
|
|
103
|
+
* res.setHeader('Location', rc.redirect);
|
|
104
|
+
* res.end();
|
|
105
|
+
* return;
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* // 设置状态码
|
|
109
|
+
* if (rc.status) {
|
|
110
|
+
* res.statusCode = rc.status;
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* // 响应 HTML 内容
|
|
114
|
+
* res.end(rc.html);
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
status = null;
|
|
119
|
+
_html = "";
|
|
120
|
+
/**
|
|
121
|
+
* 静态资源的基础路径
|
|
122
|
+
* @description
|
|
123
|
+
* base 属性用于控制静态资源的加载路径,是 Esmx 框架动态基础路径配置的核心:
|
|
124
|
+
*
|
|
125
|
+
* 1. **构建时处理**
|
|
126
|
+
* - 静态资源路径使用特殊占位符标记:`[[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/`
|
|
127
|
+
* - 占位符会被注入到所有静态资源的引用路径中
|
|
128
|
+
* - 支持 CSS、JavaScript、图片等各类静态资源
|
|
129
|
+
*
|
|
130
|
+
* 2. **运行时替换**
|
|
131
|
+
* - 通过 `esmx.render()` 的 `base` 参数设置实际基础路径
|
|
132
|
+
* - RenderContext 自动将 HTML 中的占位符替换为实际路径
|
|
133
|
+
*
|
|
134
|
+
* 3. **技术优势**
|
|
135
|
+
* - 部署灵活:同一套构建产物可部署到任意路径
|
|
136
|
+
* - 性能优化:保持静态资源的最佳缓存策略
|
|
137
|
+
* - 开发友好:简化多环境配置管理
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* // 1. 基础用法
|
|
142
|
+
* const rc = await esmx.render({
|
|
143
|
+
* base: '/esmx', // 设置基础路径
|
|
144
|
+
* params: { url: req.url }
|
|
145
|
+
* });
|
|
146
|
+
*
|
|
147
|
+
* // 2. 多语言站点示例
|
|
148
|
+
* const rc = await esmx.render({
|
|
149
|
+
* base: '/cn', // 中文站点
|
|
150
|
+
* params: { lang: 'zh-CN' }
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* // 3. 微前端应用示例
|
|
154
|
+
* const rc = await esmx.render({
|
|
155
|
+
* base: '/app1', // 子应用1
|
|
156
|
+
* params: { appId: 1 }
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
base;
|
|
161
|
+
/**
|
|
162
|
+
* 服务端渲染入口函数名称
|
|
163
|
+
* @description
|
|
164
|
+
* entryName 属性用于指定服务端渲染时使用的入口函数:
|
|
165
|
+
*
|
|
166
|
+
* 1. **基本用途**
|
|
167
|
+
* - 默认值为 'default'
|
|
168
|
+
* - 用于从 entry.server.ts 中选择要使用的渲染函数
|
|
169
|
+
* - 支持一个模块导出多个渲染函数的场景
|
|
170
|
+
*
|
|
171
|
+
* 2. **使用场景**
|
|
172
|
+
* - 多模板渲染:不同页面使用不同的渲染模板
|
|
173
|
+
* - A/B 测试:同一页面使用不同的渲染逻辑
|
|
174
|
+
* - 特殊渲染:某些页面需要自定义的渲染流程
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* // 1. 默认入口函数
|
|
179
|
+
* // entry.server.ts
|
|
180
|
+
* export default async (rc: RenderContext) => {
|
|
181
|
+
* // 默认渲染逻辑
|
|
182
|
+
* };
|
|
183
|
+
*
|
|
184
|
+
* // 2. 多个入口函数
|
|
185
|
+
* // entry.server.ts
|
|
186
|
+
* export const mobile = async (rc: RenderContext) => {
|
|
187
|
+
* // 移动端渲染逻辑
|
|
188
|
+
* };
|
|
189
|
+
*
|
|
190
|
+
* export const desktop = async (rc: RenderContext) => {
|
|
191
|
+
* // 桌面端渲染逻辑
|
|
192
|
+
* };
|
|
193
|
+
*
|
|
194
|
+
* // 3. 根据设备类型选择入口函数
|
|
195
|
+
* const rc = await esmx.render({
|
|
196
|
+
* entryName: isMobile ? 'mobile' : 'desktop',
|
|
197
|
+
* params: { url: req.url }
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
entryName;
|
|
202
|
+
/**
|
|
203
|
+
* 渲染参数
|
|
204
|
+
* @description
|
|
205
|
+
* params 属性用于在服务端渲染过程中传递和访问参数:
|
|
206
|
+
*
|
|
207
|
+
* 1. **参数类型**
|
|
208
|
+
* - 支持任意类型的键值对
|
|
209
|
+
* - 通过 Record<string, any> 类型定义
|
|
210
|
+
* - 在整个渲染生命周期中保持不变
|
|
211
|
+
*
|
|
212
|
+
* 2. **常见使用场景**
|
|
213
|
+
* - 传递请求信息(URL、query 参数等)
|
|
214
|
+
* - 设置页面配置(语言、主题等)
|
|
215
|
+
* - 注入环境变量(API 地址、版本号等)
|
|
216
|
+
* - 共享服务端状态(用户信息、权限等)
|
|
217
|
+
*
|
|
218
|
+
* 3. **访问方式**
|
|
219
|
+
* - 在服务端渲染函数中通过 rc.params 访问
|
|
220
|
+
* - 可以解构获取特定参数
|
|
221
|
+
* - 支持设置默认值
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* // 1. 基础用法 - 传递 URL 和语言设置
|
|
226
|
+
* const rc = await esmx.render({
|
|
227
|
+
* params: {
|
|
228
|
+
* url: req.url,
|
|
229
|
+
* lang: 'zh-CN'
|
|
230
|
+
* }
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* // 2. 页面配置 - 设置主题和布局
|
|
234
|
+
* const rc = await esmx.render({
|
|
235
|
+
* params: {
|
|
236
|
+
* theme: 'dark',
|
|
237
|
+
* layout: 'sidebar'
|
|
238
|
+
* }
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* // 3. 环境配置 - 注入 API 地址
|
|
242
|
+
* const rc = await esmx.render({
|
|
243
|
+
* params: {
|
|
244
|
+
* apiBaseUrl: process.env.API_BASE_URL,
|
|
245
|
+
* version: '1.0.0'
|
|
246
|
+
* }
|
|
247
|
+
* });
|
|
248
|
+
*
|
|
249
|
+
* // 4. 在渲染函数中使用
|
|
250
|
+
* export default async (rc: RenderContext) => {
|
|
251
|
+
* // 解构获取参数
|
|
252
|
+
* const { url, lang = 'en' } = rc.params;
|
|
253
|
+
*
|
|
254
|
+
* // 根据参数执行不同逻辑
|
|
255
|
+
* if (lang === 'zh-CN') {
|
|
256
|
+
* // 中文版本处理...
|
|
257
|
+
* }
|
|
258
|
+
*
|
|
259
|
+
* // 传递参数到组件
|
|
260
|
+
* const html = await renderToString(createApp({
|
|
261
|
+
* props: {
|
|
262
|
+
* currentUrl: url,
|
|
263
|
+
* language: lang
|
|
264
|
+
* }
|
|
265
|
+
* }));
|
|
266
|
+
*
|
|
267
|
+
* // 设置 HTML
|
|
268
|
+
* rc.html = `
|
|
269
|
+
* <!DOCTYPE html>
|
|
270
|
+
* <html lang="${lang}">
|
|
271
|
+
* <body>${html}</body>
|
|
272
|
+
* </html>
|
|
273
|
+
* `;
|
|
274
|
+
* };
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
params;
|
|
278
|
+
/**
|
|
279
|
+
* 模块依赖收集集合
|
|
280
|
+
* @description
|
|
281
|
+
* importMetaSet 是 Esmx 框架智能依赖收集机制的核心,用于在服务端渲染过程中追踪和记录模块依赖:
|
|
282
|
+
*
|
|
283
|
+
* 1. **按需收集**
|
|
284
|
+
* - 在组件实际渲染过程中自动追踪和记录模块依赖
|
|
285
|
+
* - 只收集当前页面渲染时真正使用到的资源
|
|
286
|
+
* - 精确记录每个组件的模块依赖关系
|
|
287
|
+
*
|
|
288
|
+
* 2. **性能优化**
|
|
289
|
+
* - 避免加载未使用的模块,显著减少首屏加载时间
|
|
290
|
+
* - 精确控制资源加载顺序,优化页面渲染性能
|
|
291
|
+
* - 自动生成最优的导入映射(Import Map)
|
|
292
|
+
*
|
|
293
|
+
* 3. **使用方式**
|
|
294
|
+
* - 在渲染函数中传递给 renderToString
|
|
295
|
+
* - 框架自动收集依赖,无需手动处理
|
|
296
|
+
* - 支持异步组件和动态导入的依赖收集
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* // 1. 基础用法
|
|
301
|
+
* const renderToString = (app: any, context: { importMetaSet: Set<ImportMeta> }) => {
|
|
302
|
+
* // 在渲染过程中自动收集模块依赖
|
|
303
|
+
* // 框架会在组件渲染时自动调用 context.importMetaSet.add(import.meta)
|
|
304
|
+
* // 开发者无需手动处理依赖收集
|
|
305
|
+
* return '<div id="app">Hello World</div>';
|
|
306
|
+
* };
|
|
307
|
+
*
|
|
308
|
+
* // 使用示例
|
|
309
|
+
* const app = createApp();
|
|
310
|
+
* const html = await renderToString(app, {
|
|
311
|
+
* importMetaSet: rc.importMetaSet
|
|
312
|
+
* });
|
|
313
|
+
*
|
|
314
|
+
* // 2. 提交依赖
|
|
315
|
+
* await rc.commit();
|
|
316
|
+
*
|
|
317
|
+
* // 3. 生成 HTML
|
|
318
|
+
* rc.html = `
|
|
319
|
+
* <!DOCTYPE html>
|
|
320
|
+
* <html>
|
|
321
|
+
* <head>
|
|
322
|
+
* <!-- 基于收集的依赖自动注入资源 -->
|
|
323
|
+
* ${rc.preload()}
|
|
324
|
+
* ${rc.css()}
|
|
325
|
+
* </head>
|
|
326
|
+
* <body>
|
|
327
|
+
* ${html}
|
|
328
|
+
* ${rc.importmap()}
|
|
329
|
+
* ${rc.moduleEntry()}
|
|
330
|
+
* ${rc.modulePreload()}
|
|
331
|
+
* </body>
|
|
332
|
+
* </html>
|
|
333
|
+
* `;
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
importMetaSet = /* @__PURE__ */ new Set();
|
|
337
|
+
/**
|
|
338
|
+
* 资源文件列表
|
|
339
|
+
* @description
|
|
340
|
+
* files 属性存储了在服务端渲染过程中收集到的所有静态资源文件路径:
|
|
341
|
+
*
|
|
342
|
+
* 1. **资源类型**
|
|
343
|
+
* - js: JavaScript 文件列表,包含所有脚本和模块
|
|
344
|
+
* - css: 样式表文件列表
|
|
345
|
+
* - modulepreload: 需要预加载的 ESM 模块列表
|
|
346
|
+
* - importmap: 导入映射文件列表
|
|
347
|
+
* - resources: 其他资源文件列表(图片、字体等)
|
|
348
|
+
*
|
|
349
|
+
* 2. **使用场景**
|
|
350
|
+
* - 在 commit() 方法中自动收集和分类资源
|
|
351
|
+
* - 通过 preload()、css() 等方法注入资源到 HTML
|
|
352
|
+
* - 支持基础路径配置,实现资源的动态加载
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* // 1. 资源收集
|
|
357
|
+
* await rc.commit();
|
|
358
|
+
*
|
|
359
|
+
* // 2. 资源注入
|
|
360
|
+
* rc.html = `
|
|
361
|
+
* <!DOCTYPE html>
|
|
362
|
+
* <html>
|
|
363
|
+
* <head>
|
|
364
|
+
* <!-- 预加载资源 -->
|
|
365
|
+
* ${rc.preload()}
|
|
366
|
+
* <!-- 注入样式表 -->
|
|
367
|
+
* ${rc.css()}
|
|
368
|
+
* </head>
|
|
369
|
+
* <body>
|
|
370
|
+
* ${html}
|
|
371
|
+
* ${rc.importmap()}
|
|
372
|
+
* ${rc.moduleEntry()}
|
|
373
|
+
* ${rc.modulePreload()}
|
|
374
|
+
* </body>
|
|
375
|
+
* </html>
|
|
376
|
+
* `;
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
files = {
|
|
380
|
+
js: [],
|
|
381
|
+
css: [],
|
|
382
|
+
modulepreload: [],
|
|
383
|
+
resources: []
|
|
384
|
+
};
|
|
385
|
+
_importMap = {
|
|
386
|
+
src: "",
|
|
387
|
+
code: ""
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* 定义 importmap 的生成模式
|
|
391
|
+
*
|
|
392
|
+
* @description
|
|
393
|
+
* ImportmapMode 用于控制 importmap 的生成方式,支持两种模式:
|
|
394
|
+
* - `inline`: 将 importmap 内容直接内联到 HTML 中(默认值),适用于以下场景:
|
|
395
|
+
* - 需要减少 HTTP 请求数量
|
|
396
|
+
* - importmap 内容较小
|
|
397
|
+
* - 对首屏加载性能要求较高
|
|
398
|
+
* - `js`: 将 importmap 内容生成为独立的 JS 文件,适用于以下场景:
|
|
399
|
+
* - importmap 内容较大
|
|
400
|
+
* - 需要利用浏览器缓存机制
|
|
401
|
+
* - 多个页面共享相同的 importmap
|
|
402
|
+
*
|
|
403
|
+
* 默认值选择 'inline' 的原因:
|
|
404
|
+
* 1. 简单直接
|
|
405
|
+
* - 减少额外的 HTTP 请求
|
|
406
|
+
* - 无需额外的资源管理
|
|
407
|
+
* - 适合大多数应用场景
|
|
408
|
+
* 2. 首屏性能
|
|
409
|
+
* - 避免额外的网络请求
|
|
410
|
+
* - 确保导入映射立即可用
|
|
411
|
+
* - 减少页面加载时间
|
|
412
|
+
* 3. 易于调试
|
|
413
|
+
* - 导入映射直接可见
|
|
414
|
+
* - 便于问题诊断
|
|
415
|
+
* - 简化开发流程
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```ts
|
|
419
|
+
* // 使用内联模式(默认)
|
|
420
|
+
* const rc = await esmx.render({
|
|
421
|
+
* params: { url: req.url }
|
|
422
|
+
* });
|
|
423
|
+
*
|
|
424
|
+
* // 显式指定内联模式
|
|
425
|
+
* const rc = await esmx.render({
|
|
426
|
+
* importmapMode: 'inline',
|
|
427
|
+
* params: { url: req.url }
|
|
428
|
+
* });
|
|
429
|
+
*
|
|
430
|
+
* // 使用 JS 文件模式
|
|
431
|
+
* const rc = await esmx.render({
|
|
432
|
+
* importmapMode: 'js',
|
|
433
|
+
* params: { url: req.url }
|
|
434
|
+
* });
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
importmapMode;
|
|
438
|
+
/**
|
|
439
|
+
* HTML 内容
|
|
440
|
+
* @description
|
|
441
|
+
* html 属性用于设置和获取最终生成的 HTML 内容:
|
|
442
|
+
*
|
|
443
|
+
* 1. **基础路径替换**
|
|
444
|
+
* - 在设置 HTML 时自动处理基础路径占位符
|
|
445
|
+
* - 将 `[[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/` 替换为实际的 base 路径
|
|
446
|
+
* - 确保所有静态资源的引用路径正确
|
|
447
|
+
*
|
|
448
|
+
* 2. **使用场景**
|
|
449
|
+
* - 设置服务端渲染生成的 HTML 内容
|
|
450
|
+
* - 支持动态基础路径配置
|
|
451
|
+
* - 自动处理静态资源的引用路径
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```ts
|
|
455
|
+
* // 1. 基础用法
|
|
456
|
+
* export default async (rc: RenderContext) => {
|
|
457
|
+
* // 设置 HTML 内容
|
|
458
|
+
* rc.html = `
|
|
459
|
+
* <!DOCTYPE html>
|
|
460
|
+
* <html>
|
|
461
|
+
* <head>
|
|
462
|
+
* ${rc.preload()}
|
|
463
|
+
* ${rc.css()}
|
|
464
|
+
* </head>
|
|
465
|
+
* <body>
|
|
466
|
+
* <div id="app">Hello World</div>
|
|
467
|
+
* ${rc.importmap()}
|
|
468
|
+
* ${rc.moduleEntry()}
|
|
469
|
+
* ${rc.modulePreload()}
|
|
470
|
+
* </body>
|
|
471
|
+
* </html>
|
|
472
|
+
* `;
|
|
473
|
+
* };
|
|
474
|
+
*
|
|
475
|
+
* // 2. 动态基础路径
|
|
476
|
+
* const rc = await esmx.render({
|
|
477
|
+
* base: '/app', // 设置基础路径
|
|
478
|
+
* params: { url: req.url }
|
|
479
|
+
* });
|
|
480
|
+
*
|
|
481
|
+
* // HTML 中的占位符会被自动替换:
|
|
482
|
+
* // [[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/css/style.css
|
|
483
|
+
* // 替换为:
|
|
484
|
+
* // /app/your-app-name/css/style.css
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
get html() {
|
|
488
|
+
return this._html;
|
|
489
|
+
}
|
|
490
|
+
set html(html) {
|
|
491
|
+
const varName = this.esmx.basePathPlaceholder;
|
|
492
|
+
this._html = varName ? html.replaceAll(this.esmx.basePathPlaceholder, this.base) : html;
|
|
493
|
+
}
|
|
494
|
+
constructor(esmx, options = {}) {
|
|
495
|
+
this.esmx = esmx;
|
|
496
|
+
this.base = options.base ?? "";
|
|
497
|
+
this.params = options.params ?? {};
|
|
498
|
+
this.entryName = options.entryName ?? "default";
|
|
499
|
+
this.importmapMode = options.importmapMode ?? "inline";
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* 将 JavaScript 对象序列化为字符串
|
|
503
|
+
* @description
|
|
504
|
+
* serialize 方法用于在服务端渲染过程中将状态数据序列化,以便传递到客户端:
|
|
505
|
+
*
|
|
506
|
+
* 1. **主要用途**
|
|
507
|
+
* - 序列化服务端状态数据
|
|
508
|
+
* - 确保数据可以安全地嵌入到 HTML 中
|
|
509
|
+
* - 支持复杂的数据结构(如 Date、RegExp 等)
|
|
510
|
+
*
|
|
511
|
+
* 2. **安全处理**
|
|
512
|
+
* - 自动转义特殊字符
|
|
513
|
+
* - 防止 XSS 攻击
|
|
514
|
+
* - 保持数据类型的完整性
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```ts
|
|
518
|
+
* // 1. 基础用法 - 序列化状态数据
|
|
519
|
+
* export default async (rc: RenderContext) => {
|
|
520
|
+
* const state = {
|
|
521
|
+
* user: { id: 1, name: 'Alice' },
|
|
522
|
+
* timestamp: new Date(),
|
|
523
|
+
* regex: /\d+/
|
|
524
|
+
* };
|
|
525
|
+
*
|
|
526
|
+
* rc.html = `
|
|
527
|
+
* <!DOCTYPE html>
|
|
528
|
+
* <html>
|
|
529
|
+
* <head>
|
|
530
|
+
* <script>
|
|
531
|
+
* // 将序列化后的状态注入到全局变量
|
|
532
|
+
* window.__INITIAL_STATE__ = ${rc.serialize(state)};
|
|
533
|
+
* <\/script>
|
|
534
|
+
* </head>
|
|
535
|
+
* <body>${html}</body>
|
|
536
|
+
* </html>
|
|
537
|
+
* `;
|
|
538
|
+
* };
|
|
539
|
+
*
|
|
540
|
+
* // 2. 自定义序列化选项
|
|
541
|
+
* const state = { sensitive: 'data' };
|
|
542
|
+
* const serialized = rc.serialize(state, {
|
|
543
|
+
* isJSON: true, // 使用 JSON 兼容模式
|
|
544
|
+
* unsafe: false // 禁用不安全的序列化
|
|
545
|
+
* });
|
|
546
|
+
* ```
|
|
547
|
+
*
|
|
548
|
+
* @param {any} input - 要序列化的输入数据
|
|
549
|
+
* @param {serialize.SerializeJSOptions} [options] - 序列化选项
|
|
550
|
+
* @returns {string} 序列化后的字符串
|
|
551
|
+
*/
|
|
552
|
+
serialize(input, options) {
|
|
553
|
+
return serialize(input, options);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 将状态数据序列化并注入到 HTML 中
|
|
557
|
+
* @description
|
|
558
|
+
* state 方法用于在服务端渲染时将状态数据序列化并注入到 HTML 中,以便客户端可以在激活时恢复这些状态:
|
|
559
|
+
*
|
|
560
|
+
* 1. **序列化机制**
|
|
561
|
+
* - 使用安全的序列化方法处理数据
|
|
562
|
+
* - 支持复杂的数据结构(对象、数组等)
|
|
563
|
+
* - 自动处理特殊字符和 XSS 防护
|
|
564
|
+
*
|
|
565
|
+
* 2. **使用场景**
|
|
566
|
+
* - 服务端状态同步到客户端
|
|
567
|
+
* - 初始化客户端应用状态
|
|
568
|
+
* - 实现无缝的服务端渲染到客户端激活
|
|
569
|
+
*
|
|
570
|
+
* @param varName 全局变量名,用于在客户端访问注入的数据
|
|
571
|
+
* @param data 需要序列化的数据对象
|
|
572
|
+
* @returns 包含序列化数据的 script 标签字符串
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```ts
|
|
576
|
+
* // 1. 基础用法 - 注入用户信息
|
|
577
|
+
* export default async (rc: RenderContext) => {
|
|
578
|
+
* const userInfo = {
|
|
579
|
+
* id: 1,
|
|
580
|
+
* name: 'John',
|
|
581
|
+
* roles: ['admin']
|
|
582
|
+
* };
|
|
583
|
+
*
|
|
584
|
+
* rc.html = `
|
|
585
|
+
* <!DOCTYPE html>
|
|
586
|
+
* <html>
|
|
587
|
+
* <head>
|
|
588
|
+
* ${rc.state('__USER__', userInfo)}
|
|
589
|
+
* </head>
|
|
590
|
+
* <body>
|
|
591
|
+
* <div id="app"></div>
|
|
592
|
+
* </body>
|
|
593
|
+
* </html>
|
|
594
|
+
* `;
|
|
595
|
+
* };
|
|
596
|
+
*
|
|
597
|
+
* // 2. 客户端使用
|
|
598
|
+
* // 在客户端可以直接访问注入的数据
|
|
599
|
+
* const userInfo = window.__USER__;
|
|
600
|
+
* console.log(userInfo.name); // 输出: 'John'
|
|
601
|
+
*
|
|
602
|
+
* // 3. 复杂数据结构
|
|
603
|
+
* export default async (rc: RenderContext) => {
|
|
604
|
+
* const appState = {
|
|
605
|
+
* user: {
|
|
606
|
+
* id: 1,
|
|
607
|
+
* preferences: {
|
|
608
|
+
* theme: 'dark',
|
|
609
|
+
* language: 'zh-CN'
|
|
610
|
+
* }
|
|
611
|
+
* },
|
|
612
|
+
* settings: {
|
|
613
|
+
* notifications: true,
|
|
614
|
+
* timezone: 'Asia/Shanghai'
|
|
615
|
+
* }
|
|
616
|
+
* };
|
|
617
|
+
*
|
|
618
|
+
* rc.html = `
|
|
619
|
+
* <!DOCTYPE html>
|
|
620
|
+
* <html>
|
|
621
|
+
* <head>
|
|
622
|
+
* ${rc.state('__APP_STATE__', appState)}
|
|
623
|
+
* </head>
|
|
624
|
+
* <body>
|
|
625
|
+
* <div id="app"></div>
|
|
626
|
+
* </body>
|
|
627
|
+
* </html>
|
|
628
|
+
* `;
|
|
629
|
+
* };
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
state(varName, data) {
|
|
633
|
+
return `<script>window[${serialize(varName)}] = ${serialize(data, { isJSON: true })};<\/script>`;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* 提交依赖收集并更新资源列表
|
|
637
|
+
* @description
|
|
638
|
+
* commit 方法是 RenderContext 依赖收集机制的核心,负责处理所有收集到的模块依赖并更新文件资源列表:
|
|
639
|
+
*
|
|
640
|
+
* 1. **依赖处理流程**
|
|
641
|
+
* - 从 importMetaSet 中收集所有使用到的模块
|
|
642
|
+
* - 基于 manifest 文件解析每个模块的具体资源
|
|
643
|
+
* - 处理 JS、CSS、资源文件等不同类型的依赖
|
|
644
|
+
* - 自动处理模块预加载和导入映射
|
|
645
|
+
*
|
|
646
|
+
* 2. **资源分类**
|
|
647
|
+
* - js: JavaScript 文件,包含所有脚本和模块
|
|
648
|
+
* - css: 样式表文件
|
|
649
|
+
* - modulepreload: 需要预加载的 ESM 模块
|
|
650
|
+
* - importmap: 导入映射文件
|
|
651
|
+
* - resources: 其他资源文件(图片、字体等)
|
|
652
|
+
*
|
|
653
|
+
* 3. **路径处理**
|
|
654
|
+
* - 自动添加基础路径前缀
|
|
655
|
+
* - 确保资源路径的正确性
|
|
656
|
+
* - 支持多应用场景的资源隔离
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```ts
|
|
660
|
+
* // 1. 基础用法
|
|
661
|
+
* export default async (rc: RenderContext) => {
|
|
662
|
+
* // 渲染页面并收集依赖
|
|
663
|
+
* const app = createApp();
|
|
664
|
+
* const html = await renderToString(app, {
|
|
665
|
+
* importMetaSet: rc.importMetaSet
|
|
666
|
+
* });
|
|
667
|
+
*
|
|
668
|
+
* // 提交依赖收集
|
|
669
|
+
* await rc.commit();
|
|
670
|
+
*
|
|
671
|
+
* // 生成 HTML
|
|
672
|
+
* rc.html = `
|
|
673
|
+
* <!DOCTYPE html>
|
|
674
|
+
* <html>
|
|
675
|
+
* <head>
|
|
676
|
+
* ${rc.preload()}
|
|
677
|
+
* ${rc.css()}
|
|
678
|
+
* </head>
|
|
679
|
+
* <body>
|
|
680
|
+
* ${html}
|
|
681
|
+
* ${rc.importmap()}
|
|
682
|
+
* ${rc.moduleEntry()}
|
|
683
|
+
* ${rc.modulePreload()}
|
|
684
|
+
* </body>
|
|
685
|
+
* </html>
|
|
686
|
+
* `;
|
|
687
|
+
* };
|
|
688
|
+
*
|
|
689
|
+
* // 2. 多应用场景
|
|
690
|
+
* const rc = await esmx.render({
|
|
691
|
+
* base: '/app1', // 设置基础路径
|
|
692
|
+
* params: { appId: 1 }
|
|
693
|
+
* });
|
|
694
|
+
*
|
|
695
|
+
* // 渲染并提交依赖
|
|
696
|
+
* const html = await renderApp(rc);
|
|
697
|
+
* await rc.commit();
|
|
698
|
+
*
|
|
699
|
+
* // 资源路径会自动添加基础路径前缀
|
|
700
|
+
* // 例如:/app1/your-app-name/js/main.js
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
async commit() {
|
|
704
|
+
const { esmx } = this;
|
|
705
|
+
const chunkSet = /* @__PURE__ */ new Set([`${esmx.name}@src/entry.client.ts`]);
|
|
706
|
+
for (const item of this.importMetaSet) {
|
|
707
|
+
if ("chunkName" in item && typeof item.chunkName === "string") {
|
|
708
|
+
chunkSet.add(item.chunkName);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const files = {
|
|
712
|
+
js: /* @__PURE__ */ new Set(),
|
|
713
|
+
modulepreload: /* @__PURE__ */ new Set(),
|
|
714
|
+
css: /* @__PURE__ */ new Set(),
|
|
715
|
+
resources: /* @__PURE__ */ new Set()
|
|
716
|
+
};
|
|
717
|
+
const getUrlPath = (...paths2) => path.posix.join("/", this.base, ...paths2);
|
|
718
|
+
const manifests = await this.esmx.getManifestList("client");
|
|
719
|
+
manifests.forEach((item) => {
|
|
720
|
+
const addPath = (setName, filepath) => files[setName].add(getUrlPath(item.name, filepath));
|
|
721
|
+
const addPaths = (setName, filepaths) => filepaths.forEach((filepath) => addPath(setName, filepath));
|
|
722
|
+
Object.entries(item.chunks).forEach(([filepath, info]) => {
|
|
723
|
+
if (chunkSet.has(filepath)) {
|
|
724
|
+
addPath("js", info.js);
|
|
725
|
+
addPaths("css", info.css);
|
|
726
|
+
addPaths("resources", info.resources);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
const paths = await esmx.getStaticImportPaths(
|
|
731
|
+
"client",
|
|
732
|
+
`${esmx.name}/src/entry.client`
|
|
733
|
+
);
|
|
734
|
+
paths?.forEach(
|
|
735
|
+
(filepath) => files.modulepreload.add(getUrlPath(filepath))
|
|
736
|
+
);
|
|
737
|
+
files.js = /* @__PURE__ */ new Set([...files.js, ...files.modulepreload]);
|
|
738
|
+
Object.keys(files).forEach(
|
|
739
|
+
(key) => this.files[key] = Array.from(files[key])
|
|
740
|
+
);
|
|
741
|
+
this._importMap = await esmx.getImportMapClientInfo(this.importmapMode);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 生成资源预加载标签
|
|
745
|
+
* @description
|
|
746
|
+
* preload() 方法用于生成资源预加载标签,通过提前加载关键资源来优化页面性能:
|
|
747
|
+
*
|
|
748
|
+
* 1. **资源类型**
|
|
749
|
+
* - CSS 文件:使用 `as="style"` 预加载样式表
|
|
750
|
+
* - JS 文件:使用 `as="script"` 预加载导入映射脚本
|
|
751
|
+
*
|
|
752
|
+
* 2. **性能优化**
|
|
753
|
+
* - 提前发现并加载关键资源
|
|
754
|
+
* - 与 HTML 解析并行加载
|
|
755
|
+
* - 优化资源加载顺序
|
|
756
|
+
* - 减少页面渲染阻塞
|
|
757
|
+
*
|
|
758
|
+
* 3. **最佳实践**
|
|
759
|
+
* - 在 head 中尽早使用
|
|
760
|
+
* - 只预加载当前页面必需的资源
|
|
761
|
+
* - 与其他资源加载方法配合使用
|
|
762
|
+
*
|
|
763
|
+
* @returns 返回包含所有预加载标签的 HTML 字符串
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* // 在 HTML head 中使用
|
|
768
|
+
* rc.html = `
|
|
769
|
+
* <!DOCTYPE html>
|
|
770
|
+
* <html>
|
|
771
|
+
* <head>
|
|
772
|
+
* <!-- 预加载关键资源 -->
|
|
773
|
+
* ${rc.preload()}
|
|
774
|
+
* <!-- 注入样式表 -->
|
|
775
|
+
* ${rc.css()}
|
|
776
|
+
* </head>
|
|
777
|
+
* <body>
|
|
778
|
+
* ${html}
|
|
779
|
+
* ${rc.importmap()}
|
|
780
|
+
* ${rc.moduleEntry()}
|
|
781
|
+
* ${rc.modulePreload()}
|
|
782
|
+
* </body>
|
|
783
|
+
* </html>
|
|
784
|
+
* `;
|
|
785
|
+
* ```
|
|
786
|
+
*/
|
|
787
|
+
preload() {
|
|
788
|
+
const { files, _importMap } = this;
|
|
789
|
+
const list = files.css.map((url) => {
|
|
790
|
+
return `<link rel="preload" href="${url}" as="style">`;
|
|
791
|
+
});
|
|
792
|
+
if (_importMap.src) {
|
|
793
|
+
list.push(
|
|
794
|
+
`<link rel="preload" href="${_importMap.src}" as="script">`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
return list.join("");
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* 注入首屏样式表
|
|
801
|
+
* @description
|
|
802
|
+
* css() 方法用于注入页面所需的样式表资源:
|
|
803
|
+
*
|
|
804
|
+
* 1. **注入位置**
|
|
805
|
+
* - 必须在 head 标签中注入
|
|
806
|
+
* - 避免页面闪烁(FOUC)和重排
|
|
807
|
+
* - 确保样式在内容渲染时就位
|
|
808
|
+
*
|
|
809
|
+
* 2. **性能优化**
|
|
810
|
+
* - 支持关键 CSS 提取
|
|
811
|
+
* - 自动处理样式依赖关系
|
|
812
|
+
* - 利用浏览器并行加载能力
|
|
813
|
+
*
|
|
814
|
+
* 3. **使用场景**
|
|
815
|
+
* - 注入首屏必需的样式
|
|
816
|
+
* - 处理组件级别的样式
|
|
817
|
+
* - 支持主题切换和动态样式
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* ```ts
|
|
821
|
+
* // 1. 基础用法
|
|
822
|
+
* rc.html = `
|
|
823
|
+
* <!DOCTYPE html>
|
|
824
|
+
* <html>
|
|
825
|
+
* <head>
|
|
826
|
+
* ${rc.preload()} <!-- 预加载资源 -->
|
|
827
|
+
* ${rc.css()} <!-- 注入样式表 -->
|
|
828
|
+
* </head>
|
|
829
|
+
* <body>
|
|
830
|
+
* <div id="app">Hello World</div>
|
|
831
|
+
* </body>
|
|
832
|
+
* </html>
|
|
833
|
+
* `;
|
|
834
|
+
*
|
|
835
|
+
* // 2. 与其他资源配合使用
|
|
836
|
+
* rc.html = `
|
|
837
|
+
* <!DOCTYPE html>
|
|
838
|
+
* <html>
|
|
839
|
+
* <head>
|
|
840
|
+
* ${rc.preload()} <!-- 预加载资源 -->
|
|
841
|
+
* ${rc.css()} <!-- 注入样式表 -->
|
|
842
|
+
* </head>
|
|
843
|
+
* <body>
|
|
844
|
+
* ${html}
|
|
845
|
+
* ${rc.importmap()}
|
|
846
|
+
* ${rc.moduleEntry()}
|
|
847
|
+
* ${rc.modulePreload()}
|
|
848
|
+
* </body>
|
|
849
|
+
* </html>
|
|
850
|
+
* `;
|
|
851
|
+
* ```
|
|
852
|
+
*/
|
|
853
|
+
css() {
|
|
854
|
+
return this.files.css.map((url) => `<link rel="stylesheet" href="${url}">`).join("");
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* 注入模块导入映射
|
|
858
|
+
* @description
|
|
859
|
+
* importmap() 方法用于注入 ESM 模块的路径解析规则:
|
|
860
|
+
*
|
|
861
|
+
* 1. **注入位置**
|
|
862
|
+
* - 必须在 body 中注入
|
|
863
|
+
* - 必须在 moduleEntry 之前执行
|
|
864
|
+
* - 避免阻塞页面首次渲染
|
|
865
|
+
*
|
|
866
|
+
* 2. **导入映射模式**
|
|
867
|
+
* - 内联模式(inline):
|
|
868
|
+
* - 将映射内容直接内联到 HTML 中
|
|
869
|
+
* - 适合映射内容较小的场景
|
|
870
|
+
* - 减少 HTTP 请求数量
|
|
871
|
+
* - JS 文件模式(js):
|
|
872
|
+
* - 生成独立的 JS 文件
|
|
873
|
+
* - 适合映射内容较大的场景
|
|
874
|
+
* - 可以利用浏览器缓存机制
|
|
875
|
+
*
|
|
876
|
+
* 3. **技术原因**
|
|
877
|
+
* - 定义了 ESM 模块的路径解析规则
|
|
878
|
+
* - 客户端入口模块和其依赖都需要使用这些映射
|
|
879
|
+
* - 确保在执行模块代码前已正确设置映射
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```ts
|
|
883
|
+
* // 1. 基础用法 - 内联模式
|
|
884
|
+
* const rc = await esmx.render({
|
|
885
|
+
* importmapMode: 'inline' // 默认模式
|
|
886
|
+
* });
|
|
887
|
+
*
|
|
888
|
+
* rc.html = `
|
|
889
|
+
* <!DOCTYPE html>
|
|
890
|
+
* <html>
|
|
891
|
+
* <head>
|
|
892
|
+
* ${rc.preload()}
|
|
893
|
+
* ${rc.css()}
|
|
894
|
+
* </head>
|
|
895
|
+
* <body>
|
|
896
|
+
* ${html}
|
|
897
|
+
* ${rc.importmap()} <!-- 注入导入映射 -->
|
|
898
|
+
* ${rc.moduleEntry()} <!-- 在导入映射之后执行 -->
|
|
899
|
+
* ${rc.modulePreload()}
|
|
900
|
+
* </body>
|
|
901
|
+
* </html>
|
|
902
|
+
* `;
|
|
903
|
+
*
|
|
904
|
+
* // 2. JS 文件模式 - 适合大型应用
|
|
905
|
+
* const rc = await esmx.render({
|
|
906
|
+
* importmapMode: 'js' // 使用 JS 文件模式
|
|
907
|
+
* });
|
|
908
|
+
* ```
|
|
909
|
+
*/
|
|
910
|
+
importmap() {
|
|
911
|
+
return this._importMap.code;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* 注入客户端入口模块
|
|
915
|
+
* @description
|
|
916
|
+
* moduleEntry() 方法用于注入客户端的入口模块:
|
|
917
|
+
* 1. **注入位置**
|
|
918
|
+
* - 必须在 importmap 之后执行
|
|
919
|
+
* - 确保在执行模块代码前已正确设置导入映射
|
|
920
|
+
* - 控制客户端激活(Hydration)的开始时机
|
|
921
|
+
*
|
|
922
|
+
* 2. **技术原因**
|
|
923
|
+
* - 作为客户端代码的入口点
|
|
924
|
+
* - 需要等待基础设施(如导入映射)就绪
|
|
925
|
+
* - 确保正确的模块路径解析
|
|
926
|
+
*
|
|
927
|
+
* 3. **使用场景**
|
|
928
|
+
* - 启动客户端应用
|
|
929
|
+
* - 执行客户端激活
|
|
930
|
+
* - 初始化客户端状态
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```ts
|
|
934
|
+
* // 1. 基础用法
|
|
935
|
+
* rc.html = `
|
|
936
|
+
* <!DOCTYPE html>
|
|
937
|
+
* <html>
|
|
938
|
+
* <head>
|
|
939
|
+
* ${rc.preload()}
|
|
940
|
+
* ${rc.css()}
|
|
941
|
+
* </head>
|
|
942
|
+
* <body>
|
|
943
|
+
* ${html}
|
|
944
|
+
* ${rc.importmap()} <!-- 先注入导入映射 -->
|
|
945
|
+
* ${rc.moduleEntry()} <!-- 再注入入口模块 -->
|
|
946
|
+
* ${rc.modulePreload()}
|
|
947
|
+
* </body>
|
|
948
|
+
* </html>
|
|
949
|
+
* `;
|
|
950
|
+
*
|
|
951
|
+
* // 2. 多入口配置
|
|
952
|
+
* const rc = await esmx.render({
|
|
953
|
+
* entryName: 'mobile', // 指定入口名称
|
|
954
|
+
* params: { device: 'mobile' }
|
|
955
|
+
* });
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
moduleEntry() {
|
|
959
|
+
return `<script type="module">import "${this.esmx.name}/src/entry.client";<\/script>`;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* 预加载模块依赖
|
|
963
|
+
* @description
|
|
964
|
+
* modulePreload() 方法用于预加载后续可能用到的模块:
|
|
965
|
+
*
|
|
966
|
+
* 1. **注入位置**
|
|
967
|
+
* - 必须在 importmap 和 moduleEntry 之后
|
|
968
|
+
* - 确保使用正确的模块路径映射
|
|
969
|
+
* - 避免与首屏渲染竞争资源
|
|
970
|
+
*
|
|
971
|
+
* 2. **性能优化**
|
|
972
|
+
* - 预加载后续可能用到的模块
|
|
973
|
+
* - 提升运行时性能
|
|
974
|
+
* - 优化按需加载体验
|
|
975
|
+
*
|
|
976
|
+
* 3. **技术原因**
|
|
977
|
+
* - 需要正确的路径解析规则
|
|
978
|
+
* - 避免重复加载
|
|
979
|
+
* - 控制加载优先级
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* ```ts
|
|
983
|
+
* // 1. 基础用法
|
|
984
|
+
* rc.html = `
|
|
985
|
+
* <!DOCTYPE html>
|
|
986
|
+
* <html>
|
|
987
|
+
* <head>
|
|
988
|
+
* ${rc.preload()}
|
|
989
|
+
* ${rc.css()}
|
|
990
|
+
* </head>
|
|
991
|
+
* <body>
|
|
992
|
+
* ${html}
|
|
993
|
+
* ${rc.importmap()}
|
|
994
|
+
* ${rc.moduleEntry()}
|
|
995
|
+
* ${rc.modulePreload()} <!-- 预加载模块依赖 -->
|
|
996
|
+
* </body>
|
|
997
|
+
* </html>
|
|
998
|
+
* `;
|
|
999
|
+
*
|
|
1000
|
+
* // 2. 与异步组件配合使用
|
|
1001
|
+
* const AsyncComponent = defineAsyncComponent(() =>
|
|
1002
|
+
* import('./components/AsyncComponent.vue')
|
|
1003
|
+
* );
|
|
1004
|
+
* // modulePreload 会自动收集并预加载异步组件的依赖
|
|
1005
|
+
* ```
|
|
1006
|
+
*/
|
|
1007
|
+
modulePreload() {
|
|
1008
|
+
return this.files.modulepreload.map((url) => `<link rel="modulepreload" href="${url}">`).join("");
|
|
1009
|
+
}
|
|
1010
|
+
}
|