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" />

多了文件原始的 widthheight,配合 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
赞赏

微信