@esmx/core
Version:
A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Federation capabilities.
1,327 lines (1,310 loc) • 39.5 kB
text/typescript
import path from 'node:path';
import serialize from 'serialize-javascript';
import type { Esmx } from './core';
/**
* RenderContext 的配置选项接口
*
* @description
* RenderContextOptions 用于配置 RenderContext 实例的行为,包括基础路径、入口名称、参数和导入映射模式等。
*
* @example
* ```ts
* // 1. 基础路径配置示例
* // 支持将静态资源部署到不同的路径下
* const rc = await esmx.render({
* // 设置基础路径为 /esmx,所有静态资源都会基于此路径加载
* base: '/esmx',
* // 其他配置...
* });
*
* // 2. 多语言站点部署示例
* // 通过不同的基础路径支持多语言站点
* const rc = await esmx.render({
* base: '/cn', // 中文站点
* params: { lang: 'zh-CN' }
* });
*
* // 3. 导入映射模式配置示例
* const rc = await esmx.render({
* // 使用内联模式,适合小型应用
* importmapMode: 'inline',
* // 其他配置...
* });
* ```
*/
export interface RenderContextOptions {
/**
* 静态资源的基础路径
* @description
* - 默认为空字符串
* - 所有静态资源(JS、CSS、图片等)都会基于此路径加载
* - 支持运行时动态配置,无需重新构建
* - 常用于多语言站点、微前端应用等场景
*/
base?: string;
/**
* 服务端渲染入口名称
* @description
* - 默认为 'default'
* - 用于指定服务端渲染时使用的入口函数
* - 当一个模块导出多个渲染函数时使用
*/
entryName?: string;
/**
* 渲染参数
* @description
* - 可以传递任意类型的参数给渲染函数
* - 常用于传递请求信息(URL、query 参数等)
* - 在服务端渲染过程中可以通过 rc.params 访问
*/
params?: Record<string, any>;
/**
* 定义 importmap 的生成模式
*
* @description
* ImportmapMode 用于控制 importmap 的生成方式,支持两种模式:
* - `inline`: 将 importmap 内容直接内联到 HTML 中(默认值),适用于以下场景:
* - 需要减少 HTTP 请求数量
* - importmap 内容较小
* - 对首屏加载性能要求较高
* - `js`: 将 importmap 内容生成为独立的 JS 文件,适用于以下场景:
* - importmap 内容较大
* - 需要利用浏览器缓存机制
* - 多个页面共享相同的 importmap
*
* 默认值选择 'inline' 的原因:
* 1. 简单直接
* - 减少额外的 HTTP 请求
* - 无需额外的资源管理
* - 适合大多数应用场景
* 2. 首屏性能
* - 避免额外的网络请求
* - 确保导入映射立即可用
* - 减少页面加载时间
* 3. 易于调试
* - 导入映射直接可见
* - 便于问题诊断
* - 简化开发流程
*
* @example
* ```ts
* // 使用内联模式(默认)
* const rc = await esmx.render({
* params: { url: req.url }
* });
*
* // 显式指定内联模式
* const rc = await esmx.render({
* importmapMode: 'inline',
* params: { url: req.url }
* });
*
* // 使用 JS 文件模式
* const rc = await esmx.render({
* importmapMode: 'js',
* params: { url: req.url }
* });
* ```
*/
importmapMode?: ImportmapMode;
}
/**
* 服务端渲染函数
*/
export type ServerRenderHandle = (rc: RenderContext) => Promise<void>;
/**
* 渲染资源文件列表接口
* @description
* RenderFiles 接口定义了服务端渲染过程中收集的各类静态资源:
*
* 1. **资源类型**
* - css: 样式表文件列表
* - modulepreload: 需要预加载的 ESM 模块列表
* - js: JavaScript 文件列表
* - resources: 其他资源文件列表
*
* 2. **使用场景**
* - 在 commit() 方法中自动收集
* - 通过 preload()、css() 等方法注入
* - 支持基础路径配置
*
* @example
* ```ts
* // 1. 资源收集
* await rc.commit();
*
* // 2. 资源注入
* rc.html = `
*
* <html>
* <head>
* <!-- 预加载资源 -->
* ${rc.preload()}
* <!-- 注入样式表 -->
* ${rc.css()}
* </head>
* <body>
* ${html}
* <!-- 注入导入映射 -->
* ${rc.importmap()}
* <!-- 注入客户端入口 -->
* ${rc.moduleEntry()}
* <!-- 预加载模块 -->
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* ```
*/
export interface RenderFiles {
/**
* JavaScript 文件列表
*/
js: string[];
/**
* CSS 文件列表
*/
css: string[];
/**
* 需要预加载的 ESM 模块列表
*/
modulepreload: string[];
/**
* 其他资源文件列表(图片、字体等)
*/
resources: string[];
}
/**
* 定义 importmap 的生成模式
*
* @description
* ImportmapMode 用于控制 importmap 的生成方式,支持两种模式:
* - `inline`: 将 importmap 内容直接内联到 HTML 中(默认值),适用于以下场景:
* - 需要减少 HTTP 请求数量
* - importmap 内容较小
* - 对首屏加载性能要求较高
* - `js`: 将 importmap 内容生成为独立的 JS 文件,适用于以下场景:
* - importmap 内容较大
* - 需要利用浏览器缓存机制
* - 多个页面共享相同的 importmap
*
* 默认值选择 'inline' 的原因:
* 1. 简单直接
* - 减少额外的 HTTP 请求
* - 无需额外的资源管理
* - 适合大多数应用场景
* 2. 首屏性能
* - 避免额外的网络请求
* - 确保导入映射立即可用
* - 减少页面加载时间
* 3. 易于调试
* - 导入映射直接可见
* - 便于问题诊断
* - 简化开发流程
*
* @example
* ```ts
* // 使用内联模式(默认)
* const rc = await esmx.render({
* params: { url: req.url }
* });
*
* // 显式指定内联模式
* const rc = await esmx.render({
* importmapMode: 'inline',
* params: { url: req.url }
* });
*
* // 使用 JS 文件模式
* const rc = await esmx.render({
* importmapMode: 'js',
* params: { url: req.url }
* });
* ```
*/
export type ImportmapMode = 'inline' | 'js';
/**
* RenderContext 是 Esmx 框架中的核心类,负责服务端渲染(SSR)过程中的资源管理和 HTML 生成
*
* @description
* RenderContext 具有以下核心特点:
* 1. **基于 ESM 的模块系统**
* - 采用现代的 ECMAScript Modules 标准
* - 支持原生的模块导入导出
* - 实现了更好的代码分割和按需加载
*
* 2. **智能依赖收集**
* - 基于实际渲染路径动态收集依赖
* - 避免不必要的资源加载
* - 支持异步组件和动态导入
*
* 3. **精确的资源注入**
* - 严格控制资源加载顺序
* - 优化首屏加载性能
* - 确保客户端激活(Hydration)的可靠性
*
* 4. **灵活的配置机制**
* - 支持动态基础路径配置
* - 提供多种导入映射模式
* - 适应不同的部署场景
*
* @example
* ```ts
* export default async (rc: RenderContext) => {
* // 1. 渲染页面内容并收集依赖
* const app = createApp();
* const html = await renderToString(app, {
* importMetaSet: rc.importMetaSet
* });
*
* // 2. 提交依赖收集
* await rc.commit();
*
* // 3. 生成完整 HTML
* rc.html = `
*
* <html>
* <head>
* <!-- 预加载 CSS 和 JS 资源,提前开始加载以优化性能 -->
* ${rc.preload()}
* <!-- 注入首屏样式表,避免页面闪烁 -->
* ${rc.css()}
* </head>
* <body>
* ${html}
* <!-- 注入模块导入映射,定义 ESM 模块的路径解析规则 -->
* ${rc.importmap()}
* <!-- 注入客户端入口模块,必须在 importmap 之后执行 -->
* ${rc.moduleEntry()}
* <!-- 预加载模块依赖,基于实际渲染收集的依赖进行优化加载 -->
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* };
* ```
*/
export class RenderContext {
public esmx: Esmx;
/**
* 重定向地址
* @description
* - 默认为 null,表示不进行重定向
* - 设置后,服务端可以根据此值进行 HTTP 重定向
* - 常用于登录验证、权限控制等场景
*
* @example
* ```ts
* // 1. 登录验证示例
* export default async (rc: RenderContext) => {
* if (!isLoggedIn()) {
* rc.redirect = '/login';
* rc.status = 302;
* return;
* }
* // 继续渲染页面...
* };
*
* // 2. 权限控制示例
* export default async (rc: RenderContext) => {
* if (!hasPermission()) {
* rc.redirect = '/403';
* rc.status = 403;
* return;
* }
* // 继续渲染页面...
* };
*
* // 3. 服务端处理示例
* app.use(async (req, res) => {
* const rc = await esmx.render({
* params: {
* url: req.url
* }
* });
*
* // 处理重定向
* if (rc.redirect) {
* res.statusCode = rc.status || 302;
* res.setHeader('Location', rc.redirect);
* res.end();
* return;
* }
*
* // 设置状态码
* if (rc.status) {
* res.statusCode = rc.status;
* }
*
* // 响应 HTML 内容
* res.end(rc.html);
* });
* ```
*/
public redirect: string | null = null;
/**
* HTTP 响应状态码
* @description
* - 默认为 null,表示使用 200 状态码
* - 可以设置任意有效的 HTTP 状态码
* - 常用于错误处理、重定向等场景
* - 通常与 redirect 属性配合使用
*
* @example
* ```ts
* // 1. 404 错误处理示例
* export default async (rc: RenderContext) => {
* const page = await findPage(rc.params.url);
* if (!page) {
* rc.status = 404;
* // 渲染 404 页面...
* return;
* }
* // 继续渲染页面...
* };
*
* // 2. 临时重定向示例
* export default async (rc: RenderContext) => {
* if (needMaintenance()) {
* rc.redirect = '/maintenance';
* rc.status = 307; // 临时重定向,保持请求方法不变
* return;
* }
* // 继续渲染页面...
* };
*
* // 3. 服务端处理示例
* app.use(async (req, res) => {
* const rc = await esmx.render({
* params: {
* url: req.url
* }
* });
*
* // 处理重定向
* if (rc.redirect) {
* res.statusCode = rc.status || 302;
* res.setHeader('Location', rc.redirect);
* res.end();
* return;
* }
*
* // 设置状态码
* if (rc.status) {
* res.statusCode = rc.status;
* }
*
* // 响应 HTML 内容
* res.end(rc.html);
* });
* ```
*/
public status: number | null = null;
private _html = '';
/**
* 静态资源的基础路径
* @description
* base 属性用于控制静态资源的加载路径,是 Esmx 框架动态基础路径配置的核心:
*
* 1. **构建时处理**
* - 静态资源路径使用特殊占位符标记:`[[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/`
* - 占位符会被注入到所有静态资源的引用路径中
* - 支持 CSS、JavaScript、图片等各类静态资源
*
* 2. **运行时替换**
* - 通过 `esmx.render()` 的 `base` 参数设置实际基础路径
* - RenderContext 自动将 HTML 中的占位符替换为实际路径
*
* 3. **技术优势**
* - 部署灵活:同一套构建产物可部署到任意路径
* - 性能优化:保持静态资源的最佳缓存策略
* - 开发友好:简化多环境配置管理
*
* @example
* ```ts
* // 1. 基础用法
* const rc = await esmx.render({
* base: '/esmx', // 设置基础路径
* params: { url: req.url }
* });
*
* // 2. 多语言站点示例
* const rc = await esmx.render({
* base: '/cn', // 中文站点
* params: { lang: 'zh-CN' }
* });
*
* // 3. 微前端应用示例
* const rc = await esmx.render({
* base: '/app1', // 子应用1
* params: { appId: 1 }
* });
* ```
*/
public readonly base: string;
/**
* 服务端渲染入口函数名称
* @description
* entryName 属性用于指定服务端渲染时使用的入口函数:
*
* 1. **基本用途**
* - 默认值为 'default'
* - 用于从 entry.server.ts 中选择要使用的渲染函数
* - 支持一个模块导出多个渲染函数的场景
*
* 2. **使用场景**
* - 多模板渲染:不同页面使用不同的渲染模板
* - A/B 测试:同一页面使用不同的渲染逻辑
* - 特殊渲染:某些页面需要自定义的渲染流程
*
* @example
* ```ts
* // 1. 默认入口函数
* // entry.server.ts
* export default async (rc: RenderContext) => {
* // 默认渲染逻辑
* };
*
* // 2. 多个入口函数
* // entry.server.ts
* export const mobile = async (rc: RenderContext) => {
* // 移动端渲染逻辑
* };
*
* export const desktop = async (rc: RenderContext) => {
* // 桌面端渲染逻辑
* };
*
* // 3. 根据设备类型选择入口函数
* const rc = await esmx.render({
* entryName: isMobile ? 'mobile' : 'desktop',
* params: { url: req.url }
* });
* ```
*/
public readonly entryName: string;
/**
* 渲染参数
* @description
* params 属性用于在服务端渲染过程中传递和访问参数:
*
* 1. **参数类型**
* - 支持任意类型的键值对
* - 通过 Record<string, any> 类型定义
* - 在整个渲染生命周期中保持不变
*
* 2. **常见使用场景**
* - 传递请求信息(URL、query 参数等)
* - 设置页面配置(语言、主题等)
* - 注入环境变量(API 地址、版本号等)
* - 共享服务端状态(用户信息、权限等)
*
* 3. **访问方式**
* - 在服务端渲染函数中通过 rc.params 访问
* - 可以解构获取特定参数
* - 支持设置默认值
*
* @example
* ```ts
* // 1. 基础用法 - 传递 URL 和语言设置
* const rc = await esmx.render({
* params: {
* url: req.url,
* lang: 'zh-CN'
* }
* });
*
* // 2. 页面配置 - 设置主题和布局
* const rc = await esmx.render({
* params: {
* theme: 'dark',
* layout: 'sidebar'
* }
* });
*
* // 3. 环境配置 - 注入 API 地址
* const rc = await esmx.render({
* params: {
* apiBaseUrl: process.env.API_BASE_URL,
* version: '1.0.0'
* }
* });
*
* // 4. 在渲染函数中使用
* export default async (rc: RenderContext) => {
* // 解构获取参数
* const { url, lang = 'en' } = rc.params;
*
* // 根据参数执行不同逻辑
* if (lang === 'zh-CN') {
* // 中文版本处理...
* }
*
* // 传递参数到组件
* const html = await renderToString(createApp({
* props: {
* currentUrl: url,
* language: lang
* }
* }));
*
* // 设置 HTML
* rc.html = `
*
* <html lang="${lang}">
* <body>${html}</body>
* </html>
* `;
* };
* ```
*/
public readonly params: Record<string, any>;
/**
* 模块依赖收集集合
* @description
* importMetaSet 是 Esmx 框架智能依赖收集机制的核心,用于在服务端渲染过程中追踪和记录模块依赖:
*
* 1. **按需收集**
* - 在组件实际渲染过程中自动追踪和记录模块依赖
* - 只收集当前页面渲染时真正使用到的资源
* - 精确记录每个组件的模块依赖关系
*
* 2. **性能优化**
* - 避免加载未使用的模块,显著减少首屏加载时间
* - 精确控制资源加载顺序,优化页面渲染性能
* - 自动生成最优的导入映射(Import Map)
*
* 3. **使用方式**
* - 在渲染函数中传递给 renderToString
* - 框架自动收集依赖,无需手动处理
* - 支持异步组件和动态导入的依赖收集
*
* @example
* ```ts
* // 1. 基础用法
* const renderToString = (app: any, context: { importMetaSet: Set<ImportMeta> }) => {
* // 在渲染过程中自动收集模块依赖
* // 框架会在组件渲染时自动调用 context.importMetaSet.add(import.meta)
* // 开发者无需手动处理依赖收集
* return '<div id="app">Hello World</div>';
* };
*
* // 使用示例
* const app = createApp();
* const html = await renderToString(app, {
* importMetaSet: rc.importMetaSet
* });
*
* // 2. 提交依赖
* await rc.commit();
*
* // 3. 生成 HTML
* rc.html = `
*
* <html>
* <head>
* <!-- 基于收集的依赖自动注入资源 -->
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* ```
*/
public importMetaSet = new Set<ImportMeta>();
/**
* 资源文件列表
* @description
* files 属性存储了在服务端渲染过程中收集到的所有静态资源文件路径:
*
* 1. **资源类型**
* - js: JavaScript 文件列表,包含所有脚本和模块
* - css: 样式表文件列表
* - modulepreload: 需要预加载的 ESM 模块列表
* - importmap: 导入映射文件列表
* - resources: 其他资源文件列表(图片、字体等)
*
* 2. **使用场景**
* - 在 commit() 方法中自动收集和分类资源
* - 通过 preload()、css() 等方法注入资源到 HTML
* - 支持基础路径配置,实现资源的动态加载
*
* @example
* ```ts
* // 1. 资源收集
* await rc.commit();
*
* // 2. 资源注入
* rc.html = `
*
* <html>
* <head>
* <!-- 预加载资源 -->
* ${rc.preload()}
* <!-- 注入样式表 -->
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* ```
*/
public files: RenderFiles = {
js: [],
css: [],
modulepreload: [],
resources: []
};
private _importMap: { src: string | null; code: string } = {
src: '',
code: ''
};
/**
* 定义 importmap 的生成模式
*
* @description
* ImportmapMode 用于控制 importmap 的生成方式,支持两种模式:
* - `inline`: 将 importmap 内容直接内联到 HTML 中(默认值),适用于以下场景:
* - 需要减少 HTTP 请求数量
* - importmap 内容较小
* - 对首屏加载性能要求较高
* - `js`: 将 importmap 内容生成为独立的 JS 文件,适用于以下场景:
* - importmap 内容较大
* - 需要利用浏览器缓存机制
* - 多个页面共享相同的 importmap
*
* 默认值选择 'inline' 的原因:
* 1. 简单直接
* - 减少额外的 HTTP 请求
* - 无需额外的资源管理
* - 适合大多数应用场景
* 2. 首屏性能
* - 避免额外的网络请求
* - 确保导入映射立即可用
* - 减少页面加载时间
* 3. 易于调试
* - 导入映射直接可见
* - 便于问题诊断
* - 简化开发流程
*
* @example
* ```ts
* // 使用内联模式(默认)
* const rc = await esmx.render({
* params: { url: req.url }
* });
*
* // 显式指定内联模式
* const rc = await esmx.render({
* importmapMode: 'inline',
* params: { url: req.url }
* });
*
* // 使用 JS 文件模式
* const rc = await esmx.render({
* importmapMode: 'js',
* params: { url: req.url }
* });
* ```
*/
public importmapMode: ImportmapMode;
/**
* HTML 内容
* @description
* html 属性用于设置和获取最终生成的 HTML 内容:
*
* 1. **基础路径替换**
* - 在设置 HTML 时自动处理基础路径占位符
* - 将 `[[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/` 替换为实际的 base 路径
* - 确保所有静态资源的引用路径正确
*
* 2. **使用场景**
* - 设置服务端渲染生成的 HTML 内容
* - 支持动态基础路径配置
* - 自动处理静态资源的引用路径
*
* @example
* ```ts
* // 1. 基础用法
* export default async (rc: RenderContext) => {
* // 设置 HTML 内容
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* <div id="app">Hello World</div>
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* };
*
* // 2. 动态基础路径
* const rc = await esmx.render({
* base: '/app', // 设置基础路径
* params: { url: req.url }
* });
*
* // HTML 中的占位符会被自动替换:
* // [[[___GEZ_DYNAMIC_BASE___]]]/your-app-name/css/style.css
* // 替换为:
* // /app/your-app-name/css/style.css
* ```
*/
public get html() {
return this._html;
}
public set html(html) {
const varName = this.esmx.basePathPlaceholder;
this._html = varName
? html.replaceAll(this.esmx.basePathPlaceholder, this.base)
: html;
}
public constructor(esmx: Esmx, options: RenderContextOptions = {}) {
this.esmx = esmx;
this.base = options.base ?? '';
this.params = options.params ?? {};
this.entryName = options.entryName ?? 'default';
this.importmapMode = options.importmapMode ?? 'inline';
}
/**
* 将 JavaScript 对象序列化为字符串
* @description
* serialize 方法用于在服务端渲染过程中将状态数据序列化,以便传递到客户端:
*
* 1. **主要用途**
* - 序列化服务端状态数据
* - 确保数据可以安全地嵌入到 HTML 中
* - 支持复杂的数据结构(如 Date、RegExp 等)
*
* 2. **安全处理**
* - 自动转义特殊字符
* - 防止 XSS 攻击
* - 保持数据类型的完整性
*
* @example
* ```ts
* // 1. 基础用法 - 序列化状态数据
* export default async (rc: RenderContext) => {
* const state = {
* user: { id: 1, name: 'Alice' },
* timestamp: new Date(),
* regex: /\d+/
* };
*
* rc.html = `
*
* <html>
* <head>
* <script>
* // 将序列化后的状态注入到全局变量
* window.__INITIAL_STATE__ = ${rc.serialize(state)};
* </script>
* </head>
* <body>${html}</body>
* </html>
* `;
* };
*
* // 2. 自定义序列化选项
* const state = { sensitive: 'data' };
* const serialized = rc.serialize(state, {
* isJSON: true, // 使用 JSON 兼容模式
* unsafe: false // 禁用不安全的序列化
* });
* ```
*
* @param {any} input - 要序列化的输入数据
* @param {serialize.SerializeJSOptions} [options] - 序列化选项
* @returns {string} 序列化后的字符串
*/
public serialize(
input: any,
options?: serialize.SerializeJSOptions
): string {
return serialize(input, options);
}
/**
* 将状态数据序列化并注入到 HTML 中
* @description
* state 方法用于在服务端渲染时将状态数据序列化并注入到 HTML 中,以便客户端可以在激活时恢复这些状态:
*
* 1. **序列化机制**
* - 使用安全的序列化方法处理数据
* - 支持复杂的数据结构(对象、数组等)
* - 自动处理特殊字符和 XSS 防护
*
* 2. **使用场景**
* - 服务端状态同步到客户端
* - 初始化客户端应用状态
* - 实现无缝的服务端渲染到客户端激活
*
* @param varName 全局变量名,用于在客户端访问注入的数据
* @param data 需要序列化的数据对象
* @returns 包含序列化数据的 script 标签字符串
*
* @example
* ```ts
* // 1. 基础用法 - 注入用户信息
* export default async (rc: RenderContext) => {
* const userInfo = {
* id: 1,
* name: 'John',
* roles: ['admin']
* };
*
* rc.html = `
*
* <html>
* <head>
* ${rc.state('__USER__', userInfo)}
* </head>
* <body>
* <div id="app"></div>
* </body>
* </html>
* `;
* };
*
* // 2. 客户端使用
* // 在客户端可以直接访问注入的数据
* const userInfo = window.__USER__;
* console.log(userInfo.name); // 输出: 'John'
*
* // 3. 复杂数据结构
* export default async (rc: RenderContext) => {
* const appState = {
* user: {
* id: 1,
* preferences: {
* theme: 'dark',
* language: 'zh-CN'
* }
* },
* settings: {
* notifications: true,
* timezone: 'Asia/Shanghai'
* }
* };
*
* rc.html = `
*
* <html>
* <head>
* ${rc.state('__APP_STATE__', appState)}
* </head>
* <body>
* <div id="app"></div>
* </body>
* </html>
* `;
* };
* ```
*/
public state(varName: string, data: Record<string, any>): string {
return `<script>window[${serialize(varName)}] = ${serialize(data, { isJSON: true })};</script>`;
}
/**
* 提交依赖收集并更新资源列表
* @description
* commit 方法是 RenderContext 依赖收集机制的核心,负责处理所有收集到的模块依赖并更新文件资源列表:
*
* 1. **依赖处理流程**
* - 从 importMetaSet 中收集所有使用到的模块
* - 基于 manifest 文件解析每个模块的具体资源
* - 处理 JS、CSS、资源文件等不同类型的依赖
* - 自动处理模块预加载和导入映射
*
* 2. **资源分类**
* - js: JavaScript 文件,包含所有脚本和模块
* - css: 样式表文件
* - modulepreload: 需要预加载的 ESM 模块
* - importmap: 导入映射文件
* - resources: 其他资源文件(图片、字体等)
*
* 3. **路径处理**
* - 自动添加基础路径前缀
* - 确保资源路径的正确性
* - 支持多应用场景的资源隔离
*
* @example
* ```ts
* // 1. 基础用法
* export default async (rc: RenderContext) => {
* // 渲染页面并收集依赖
* const app = createApp();
* const html = await renderToString(app, {
* importMetaSet: rc.importMetaSet
* });
*
* // 提交依赖收集
* await rc.commit();
*
* // 生成 HTML
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* };
*
* // 2. 多应用场景
* const rc = await esmx.render({
* base: '/app1', // 设置基础路径
* params: { appId: 1 }
* });
*
* // 渲染并提交依赖
* const html = await renderApp(rc);
* await rc.commit();
*
* // 资源路径会自动添加基础路径前缀
* // 例如:/app1/your-app-name/js/main.js
* ```
*/
public async commit() {
const { esmx } = this;
const chunkSet = new Set([`${esmx.name}@src/entry.client.ts`]);
for (const item of this.importMetaSet) {
if ('chunkName' in item && typeof item.chunkName === 'string') {
chunkSet.add(item.chunkName);
}
}
const files: {
[K in keyof RenderFiles]: Set<string>;
} = {
js: new Set(),
modulepreload: new Set(),
css: new Set(),
resources: new Set()
};
const getUrlPath = (...paths: string[]) =>
path.posix.join('/', this.base, ...paths);
const manifests = await this.esmx.getManifestList('client');
manifests.forEach((item) => {
const addPath = (setName: keyof RenderFiles, filepath: string) =>
files[setName].add(getUrlPath(item.name, filepath));
const addPaths = (
setName: keyof RenderFiles,
filepaths: string[]
) => filepaths.forEach((filepath) => addPath(setName, filepath));
Object.entries(item.chunks).forEach(([filepath, info]) => {
if (chunkSet.has(filepath)) {
addPath('js', info.js);
addPaths('css', info.css);
addPaths('resources', info.resources);
}
});
});
const paths = await esmx.getStaticImportPaths(
'client',
`${esmx.name}/src/entry.client`
);
paths?.forEach((filepath) =>
files.modulepreload.add(getUrlPath(filepath))
);
files.js = new Set([...files.js, ...files.modulepreload]);
Object.keys(files).forEach(
(key) => (this.files[key] = Array.from(files[key]))
);
this._importMap = await esmx.getImportMapClientInfo(this.importmapMode);
}
/**
* 生成资源预加载标签
* @description
* preload() 方法用于生成资源预加载标签,通过提前加载关键资源来优化页面性能:
*
* 1. **资源类型**
* - CSS 文件:使用 `as="style"` 预加载样式表
* - JS 文件:使用 `as="script"` 预加载导入映射脚本
*
* 2. **性能优化**
* - 提前发现并加载关键资源
* - 与 HTML 解析并行加载
* - 优化资源加载顺序
* - 减少页面渲染阻塞
*
* 3. **最佳实践**
* - 在 head 中尽早使用
* - 只预加载当前页面必需的资源
* - 与其他资源加载方法配合使用
*
* @returns 返回包含所有预加载标签的 HTML 字符串
*
* @example
* ```ts
* // 在 HTML head 中使用
* rc.html = `
*
* <html>
* <head>
* <!-- 预加载关键资源 -->
* ${rc.preload()}
* <!-- 注入样式表 -->
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* ```
*/
public preload() {
const { files, _importMap } = this;
const list = files.css.map((url) => {
return `<link rel="preload" href="${url}" as="style">`;
});
if (_importMap.src) {
list.push(
`<link rel="preload" href="${_importMap.src}" as="script">`
);
}
return list.join('');
}
/**
* 注入首屏样式表
* @description
* css() 方法用于注入页面所需的样式表资源:
*
* 1. **注入位置**
* - 必须在 head 标签中注入
* - 避免页面闪烁(FOUC)和重排
* - 确保样式在内容渲染时就位
*
* 2. **性能优化**
* - 支持关键 CSS 提取
* - 自动处理样式依赖关系
* - 利用浏览器并行加载能力
*
* 3. **使用场景**
* - 注入首屏必需的样式
* - 处理组件级别的样式
* - 支持主题切换和动态样式
*
* @example
* ```ts
* // 1. 基础用法
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()} <!-- 预加载资源 -->
* ${rc.css()} <!-- 注入样式表 -->
* </head>
* <body>
* <div id="app">Hello World</div>
* </body>
* </html>
* `;
*
* // 2. 与其他资源配合使用
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()} <!-- 预加载资源 -->
* ${rc.css()} <!-- 注入样式表 -->
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()}
* </body>
* </html>
* `;
* ```
*/
public css() {
return this.files.css
.map((url) => `<link rel="stylesheet" href="${url}">`)
.join('');
}
/**
* 注入模块导入映射
* @description
* importmap() 方法用于注入 ESM 模块的路径解析规则:
*
* 1. **注入位置**
* - 必须在 body 中注入
* - 必须在 moduleEntry 之前执行
* - 避免阻塞页面首次渲染
*
* 2. **导入映射模式**
* - 内联模式(inline):
* - 将映射内容直接内联到 HTML 中
* - 适合映射内容较小的场景
* - 减少 HTTP 请求数量
* - JS 文件模式(js):
* - 生成独立的 JS 文件
* - 适合映射内容较大的场景
* - 可以利用浏览器缓存机制
*
* 3. **技术原因**
* - 定义了 ESM 模块的路径解析规则
* - 客户端入口模块和其依赖都需要使用这些映射
* - 确保在执行模块代码前已正确设置映射
*
* @example
* ```ts
* // 1. 基础用法 - 内联模式
* const rc = await esmx.render({
* importmapMode: 'inline' // 默认模式
* });
*
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()} <!-- 注入导入映射 -->
* ${rc.moduleEntry()} <!-- 在导入映射之后执行 -->
* ${rc.modulePreload()}
* </body>
* </html>
* `;
*
* // 2. JS 文件模式 - 适合大型应用
* const rc = await esmx.render({
* importmapMode: 'js' // 使用 JS 文件模式
* });
* ```
*/
public importmap() {
return this._importMap.code;
}
/**
* 注入客户端入口模块
* @description
* moduleEntry() 方法用于注入客户端的入口模块:
* 1. **注入位置**
* - 必须在 importmap 之后执行
* - 确保在执行模块代码前已正确设置导入映射
* - 控制客户端激活(Hydration)的开始时机
*
* 2. **技术原因**
* - 作为客户端代码的入口点
* - 需要等待基础设施(如导入映射)就绪
* - 确保正确的模块路径解析
*
* 3. **使用场景**
* - 启动客户端应用
* - 执行客户端激活
* - 初始化客户端状态
*
* @example
* ```ts
* // 1. 基础用法
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()} <!-- 先注入导入映射 -->
* ${rc.moduleEntry()} <!-- 再注入入口模块 -->
* ${rc.modulePreload()}
* </body>
* </html>
* `;
*
* // 2. 多入口配置
* const rc = await esmx.render({
* entryName: 'mobile', // 指定入口名称
* params: { device: 'mobile' }
* });
* ```
*/
public moduleEntry() {
return `<script type="module">import "${this.esmx.name}/src/entry.client";</script>`;
}
/**
* 预加载模块依赖
* @description
* modulePreload() 方法用于预加载后续可能用到的模块:
*
* 1. **注入位置**
* - 必须在 importmap 和 moduleEntry 之后
* - 确保使用正确的模块路径映射
* - 避免与首屏渲染竞争资源
*
* 2. **性能优化**
* - 预加载后续可能用到的模块
* - 提升运行时性能
* - 优化按需加载体验
*
* 3. **技术原因**
* - 需要正确的路径解析规则
* - 避免重复加载
* - 控制加载优先级
*
* @example
* ```ts
* // 1. 基础用法
* rc.html = `
*
* <html>
* <head>
* ${rc.preload()}
* ${rc.css()}
* </head>
* <body>
* ${html}
* ${rc.importmap()}
* ${rc.moduleEntry()}
* ${rc.modulePreload()} <!-- 预加载模块依赖 -->
* </body>
* </html>
* `;
*
* // 2. 与异步组件配合使用
* const AsyncComponent = defineAsyncComponent(() =>
* import('./components/AsyncComponent.vue')
* );
* // modulePreload 会自动收集并预加载异步组件的依赖
* ```
*/
public modulePreload() {
return this.files.modulepreload
.map((url) => `<link rel="modulepreload" href="${url}">`)
.join('');
}
}