如何在无磁盘访问下将带组件的MDX2编译为静态HTML?
无磁盘MDX2静态HTML编译方案(支持自定义组件)
核心逻辑
放弃hack式字符串拼接,基于unified生态的AST(抽象语法树)流程实现:
- 将自定义组件字符串预编译为可直接生成Hast节点的渲染函数
- 处理主MDX字符串时,在Esast(ECMAScript AST)阶段拦截组件引用,替换为对应组件的Hast输出
- 最终将整合后的Hast转换为静态HTML
具体实现步骤
1. 依赖准备
使用以下核心包:
@mdx-js/mdx:MDX到Esast的编译unified:AST处理管道estree-util-is-identifier-name:识别Esast中的组件标识符hast-util-to-html:Hast转HTMLunist-util-visit:遍历AST节点react-to-hast:React元素转Hast节点jsdom:AWS Lambda环境模拟浏览器DOM
2. 预编译自定义组件
将组件字符串转换为可生成Hast的函数(全程内存操作,无磁盘读写):
import { compile } from '@mdx-js/mdx' import { evaluate } from '@mdx-js/mdx/lib/evaluate.js' import { JSDOM } from 'jsdom' import { reactToHast } from 'react-to-hast' // Lambda环境初始化DOM const dom = new JSDOM('') globalThis.document = dom.window.document // 预编译组件为Hast生成函数 async function precompileComponent(componentStr) { const compiled = await compile(componentStr, { outputFormat: 'function-body' }) const { default: Component } = await evaluate(compiled, { exports: {}, runtime: await import('@mdx-js/react/lib/runtime.js') }) return (props) => reactToHast(Component(props)) }
3. 拦截MDX组件引用并替换
在unified管道中添加自定义插件,遍历Esast节点替换组件调用:
import { visit } from 'unist-util-visit' import { isIdentifierName } from 'estree-util-is-identifier-name' // 自定义AST插件:替换组件调用为Hast节点 function mdxComponentReplacer(components) { return (tree) => { visit(tree, 'CallExpression', (node) => { if (node.callee.type === 'Identifier' && isIdentifierName(node.callee.name)) { const componentFn = components[node.callee.name] if (!componentFn) return // 解析组件props const props = node.arguments[0]?.properties?.reduce((acc, prop) => { acc[prop.key.name] = prop.value.value return acc }, {}) || {} // 替换当前节点为Hast结构 Object.assign(node, componentFn(props)) } }) } }
4. 完整编译流程
整合所有步骤,实现MDX字符串+组件字符串到HTML的转换:
import { unified } from 'unified' import remarkParse from 'remark-parse' import remarkMdx from 'remark-mdx' import mdx from '@mdx-js/mdx' import rehypeStringify from 'rehype-stringify' async function compileMdxWithComponents(mainMdxStr, componentMap) { // 预编译所有组件 const compiledComponents = {} for (const [name, str] of Object.entries(componentMap)) { compiledComponents[name] = await precompileComponent(str) } // 构建unified处理管道 const processor = unified() .use(remarkParse) .use(remarkMdx) .use(mdx, { outputFormat: 'estree' }) .use(mdxComponentReplacer, compiledComponents) .use(rehypeStringify) // 处理主MDX并输出HTML const file = await processor.process(mainMdxStr) return String(file) } // 示例调用 const mainMdx = ` # Hello MDX <ComponentA title="Test Component" /> <ComponentB content="Custom Content" /> ` const components = { ComponentA: `export default function ComponentA({ title }) { return <div class="component-a"><h2>{title}</h2></div> }`, ComponentB: `export default function ComponentB({ content }) { return <p class="component-b">{content}</p> }` } compileMdxWithComponents(mainMdx, components).then(html => console.log(html))
关键注意事项
- Lambda环境需安装
jsdom以模拟DOM,确保React组件渲染正常 - 若组件依赖外部库,需在预编译阶段注入对应全局变量或模块
- 复杂组件可调整
reactToHast配置,保留更多样式与结构细节
内容的提问来源于stack exchange,提问作者Sir Robert
相关产品推荐
相关产品推荐

