MDX 图片处理
2022-08-26 · 955 chars · 5 min read
在 markdown 中使用图片非常简单:
![alt 属性文本](图片地址 '可选标题') ![插图1](https://example.com/image1.jpg)
最终转为 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 文件:
![插图1](./image1.png '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