Add MDX Embed support
• • ☕️ 3 min read你可以从本文了解到
如何让 mdx 支持各种嵌入式内容,使得可展示的内容大大扩展。包括了几十种内容。
最终效果 demo
如何安装和使用
1、安装依赖
yarn add mdx-embed
2、将页面用MDXEmbedProvider
包裹起来
import {MDXEmbedProvider} from 'mdx-embed';
// ...
export default function ({children}) {
return (
<MDXProvider
components={{
a: Link,
wrapper: ({children}) => (
<MDXEmbedProvider> <div className={styles.content}>{children}</div>
</MDXEmbedProvider> )
}}
>
<MDXComponent />
</MDXProvider>
);
}
原理小探
首先看MDXEmbedProvider
的代码,如下,基本上就是在原来的MDXProvider
上又增加一层封装,使得支持Embed组件
import React, { FunctionComponent } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { components } from './components';
interface IMdxProviderProps {
/** React Children */
children: React.ReactNode;
}
export const MDXEmbedProvider: FunctionComponent<IMdxProviderProps> = ({ children }) => ( <MDXProvider components={components}>{children}</MDXProvider>
);
那我们去看一下@mdx-js/react
中的MDXProvider
做了什么
export const MDXContext = React.createContext({})
const emptyObject = {}
export function useMDXComponents(components) {
const contextComponents = React.useContext(MDXContext)
// Memoize to avoid unnecessary top-level context changes
return React.useMemo(() => {
// Custom merge via a function prop
if (typeof components === 'function') {
return components(contextComponents)
}
return {...contextComponents, ...components} // 合并 components和contextComponents
}, [contextComponents, components])
}
export function MDXProvider({components, children, disableParentContext}) {
let allComponents = useMDXComponents(components)
if (disableParentContext) {
allComponents = components || emptyObject
}
return React.createElement(
MDXContext.Provider,
{value: allComponents}, // 将这些components提供给children
children
)
}
下面看一个test case,这说明内层定义的组件优先先进行转换,未命中的才会又上层组件来处理。
test('should support components as a function', async () => {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
renderToString(
<MDXProvider
components={{
h1: (props) => <h1 style={{color: 'tomato'}} {...props} />,
h2: (props) => <h2 style={{color: 'rebeccapurple'}} {...props} />
}}
>
<MDXProvider
components={() => ({
h2: (props) => <h2 style={{color: 'papayawhip'}} {...props} />
})}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>\n<h2 style="color:papayawhip">hello</h2>'
)
})
上面代码的运行顺序是
1、'# hi\n## hello'
先被内层组件转换成'# hi\n<h2 style="color:papayawhip">hello</h2>'
2、'# hi\n<h2 style="color:papayawhip">hello</h2>'
被外层组件转换成'<h1>hi</h1>\n<h2 style="color:papayawhip">hello</h2>'
那有人就要问了,为什么evaluate会执行这个转换,他怎么知道# hi
要命中h1
组件呢?
那么我们就需要来看evaluate
这个函数,他调用了compile进行编译,
// packages/mdx/lib/evaluate\.js
export async function evaluate(vfileCompatible, evaluateOptions) {
const {compiletime, runtime} = resolveEvaluateOptions(evaluateOptions) // 分离编译时参数和运行时参数
// V8 on Erbium.
/* c8 ignore next 2 */
return run(await compile(vfileCompatible, compiletime), runtime)
}
// packages/mdx/lib/compile\.js
/**
* Compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {CompileOptions} [compileOptions]
* @return {Promise<VFile>}
*/
export function compile(vfileCompatible, compileOptions) {
const {file, options} = resolveFileAndOptions(vfileCompatible, compileOptions) // 生成一个VFile对象
return createProcessor(options).process(file)
}
VFile是unified中用到的一个数据结构,用于在内存中进行文件操作。
调用了createProcessor
方法,代码如下。其主要工作就是将mdx解析成AST,最终经过一系列插件运算,转换成JSON对象
其中经过了3个AST转换,分别是mdast,hast,esast
。
MDAST(Markdown Abstract Syntax Tree)
是一种用于表示 Markdown 文本的抽象语法树(AST)格式。它是使用 Remark(一个用于处理 Markdown 文本的 JavaScript 库)解析 Markdown 文本后得到的结果。
HAST(HTML Abstract Syntax Tree)
是一种用于表示 HTML 文本的抽象语法树(AST)格式。它是使用 Rehype(一个用于处理 HTML 文本的 JavaScript 库)解析 HTML 文本后得到的结果。
ESAST(JavaScript Abstract Syntax Tree)
是一种用于表示 JavaScript 代码的抽象语法树(AST)格式。它是使用 Recast(一个用于处理 JavaScript 代码的 JavaScript 库)解析 JavaScript 代码后得到的结果。
因此,MDAST 用于表示 Markdown 文本,HAST 用于表示 HTML 文本,ESAST 用于表示 JavaScript 代码。
经过mdx-loader
的处理后,会输出JS文件
const mdx = require('@mdx-js/mdx')
// ...
module.exports = async function(source) {
let result
// ...
try {
result = await mdx(mdxContent, options)
} catch(err) {
return callback(err)
}
// ...
let code = `
import React from 'react'
import { mdx } from '@mdx-js/react' // 注入react binding
export const readingTime = ${JSON.stringify(estimatedReadingTime)}
${result}
`
return callback(null, code)
}
那么mdx binding是个啥呢?如下
在compile
的过程中,会在处理这个mdx文件的时候会给上面这段代码增加一个注释/* @jsx mdx */
,这就使得替换了React.createElement方法,也就起到了从components中映射自定义组件的能力。
// node_modules/@mdx-js/mdx/index.js
async function compile(mdx, options = {}) {
const opts = Object.assign({}, DEFAULT_OPTIONS, options)
const compiler = createCompiler(opts)
const fileOpts = {contents: mdx}
if (opts.filepath) {
fileOpts.path = opts.filepath
}
const {contents} = await compiler.process(fileOpts)
return `/* @jsx mdx */
${contents}`
}
总结一下就是
1、编译时,md通过mdx-loader转换成js文件
2、运行时,createElement被劫持,注入了自定义组件