MDX 图片处理
2022-08-26 · 955 chars · 5 min read
在 markdown 中使用图片非常简单:
 
最终转为 HTML 就是:
<p> <img alt="插图1" src="https://example.com/image1.jpg" /> </p>
但是在 MDX 里就不太一样了,MDX 的使用场景比较复杂,涉及到 React 组件和编译等,通常图片也是存放在本地,和 .mdx 文件在一起的。这时候普通的纯文本 markdown 语法,就无法很好的在图片文件和 .mdx 文件间建立关联,参与到编译、构建等流程中去。
但是 markdown 的语法确实方便一些,如果用 mdx 语法写:
import image1 from './image1.jpg';
<p>
<img alt="插图1" src={image1} />
</p>
肉眼可见的麻烦,要兼顾两者,就要使用 remark-mdx-images 插件了。
使用 remark-mdx-images#
remark-mdx-images 的作用就是将 markdown 图片语法,转化为 JS 的 import 语法。
用法比较简单,参考 mdx 文档,按照 remark 插件的方式配置即可。例如基于 mdx-loader 的用法:
import remarkMdxImages from 'remark-mdx-images';
export default {
module: {
rules: [
{
test: /\.mdx/,
exclude: /node_modules/,
use: [
'babel-loader',
{
loader: '@mdx-js/loader',
options: {
remarkPlugins: [remarkMdxImages],
providerImportSource: '@mdx-js/react',
},
},
],
},
],
},
}
原始的 mdx 文件:

mdx-loader 处理后:
import image1 from './image1.png';
<p>
<img alt="插图1" src={image1} title="MDX 图片处理!" />
</p>
这样转为常规 jsx 语法后,babel 就能正常的进一步处理。但是...
图片 CLS 问题#
对于 mdx 的主要使用场景,文档和博客来说,插图是页面整体非常重要的一环,而且一般尺寸也比较大。按照上面的 jsx 结构渲染页面,图片是没有明确的宽度和高度的,必然产生 CLS 问题。
其实解决问题的思路是很清晰的:既然 remark-mdx-images 可以转换语法,同时图片文件是存放在本地的(网络也问题不大,慢点而已),那么为什么不能在转换的同时,读取图片的尺寸,并直接写在 <img /> 标签上呢?
我们改造一下插件,在构造 <img /> 最后一步,读取文件信息,把原始的宽度和高度写在标签上:
import { dirname, join } from 'path'
import sizeOf from 'image-size'
// ...
const imagePath = join(dirname(file.path), url)
const imageSize = sizeOf(imagePath)
textElement.attributes.push(
...[
{
type: 'mdxJsxAttribute',
name: 'width',
value: imageSize.width,
},
{
type: 'mdxJsxAttribute',
name: 'height',
value: imageSize.height,
},
],
)
// ...
加完后生成的 jsx 长这样:
<img alt="" src="/assets/image/49360e05.png" width="1200" height="769" />
多了文件原始的 width 和 height,配合 CSS,即可保证 <img /> 标签在图片加载前,也可保持原始的宽高比例。
最后附上修改后的完成代码
查看完整代码
/**
* 基于 remark-mdx-images v2 修改的:添加默认 alt 和 width、height
*
* https://github.com/remcohaszing/remark-mdx-images
*/
import { dirname, join } from 'path'
import { visit } from 'unist-util-visit'
import sizeOf from 'image-size'
const urlPattern = /^(https?:)?\//
const relativePathPattern = /\.\.?\//
const remarkMdxImages =
({ resolve = true } = {}) =>
(ast, file) => {
const imports = []
const imported = new Map()
visit(ast, 'image', (node, index, parent) => {
let { alt = null, title, url } = node
if (urlPattern.test(url)) {
return
}
if (!relativePathPattern.test(url) && resolve) {
url = `./${url}`
}
let name = imported.get(url)
if (!name) {
name = `__${imported.size}_${url.replace(/\W/g, '_')}__`
imports.push({
type: 'mdxjsEsm',
value: '',
data: {
estree: {
type: 'Program',
sourceType: 'module',
body: [
{
type: 'ImportDeclaration',
source: { type: 'Literal', value: url, raw: JSON.stringify(url) },
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name },
},
],
},
],
},
},
})
imported.set(url, name)
}
const textElement = {
type: 'mdxJsxTextElement',
name: 'img',
children: [],
attributes: [
{ type: 'mdxJsxAttribute', name: 'alt', value: alt },
{
type: 'mdxJsxAttribute',
name: 'src',
value: {
type: 'mdxJsxAttributeValueExpression',
value: name,
data: {
estree: {
type: 'Program',
sourceType: 'module',
comments: [],
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name },
},
],
},
},
},
},
],
}
if (title) {
textElement.attributes.push({
type: 'mdxJsxAttribute',
name: 'title',
value: title,
})
}
const imagePath = join(dirname(file.path), url)
const imageSize = sizeOf(imagePath)
textElement.attributes.push(
...[
{
type: 'mdxJsxAttribute',
name: 'width',
value: imageSize.width,
},
{
type: 'mdxJsxAttribute',
name: 'height',
value: imageSize.height,
},
],
)
parent.children.splice(index, 1, textElement)
})
ast.children.unshift(...imports)
}
export default remarkMdxImages


