本指南介绍如何创建一个remark插件,在将MDX文件作为ES模块导入时,使阅读时间数据可用。


Remark是一个强大的Markdown处理器,可以用来创建自定义插件以转换Markdown内容。当使用remark解析Markdown文件时,内容会被转换成抽象语法树(AST),可以通过插件进行操作。

为了提供更好的用户体验,通常会显示文章的估计阅读时间。在本指南中,我们将创建一个remark插件,从MDX文件中提取阅读时间数据,并在将MDX文件作为ES模块导入时使其可用。

开始

首先创建一个MDX文件:

# Hello, world!

This is an example MDX file.
  • 1.
  • 2.
  • 3.

假设我们使用Vite作为打包工具,并且使用官方的@mdx-js/rollup插件来转换MDX文件,因此我们可以将MDX文件作为ES模块导入。Vite的配置应该如下所示:

import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    {
      // `enforce: 'pre'` 是使MDX插件生效所必需的
      enforce: 'pre',
      ...mdx({
        // ...配置
      }),
    },
  ],
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

如果我们将MDX文件作为ES模块导入,内容将是一个对象,其中default属性包含编译后的JSX。例如:

const mdx = await import('./example.mdx');
console.log(mdx);
  • 1.
  • 2.

将会得到:

{
  // ...其他属性,如果你有插件转换MDX内容
  default: [Function: MDXContent],
}
  • 1.
  • 2.
  • 3.
  • 4.

一旦我们有了这样的输出,我们就准备好创建remark插件了。

创建remark插件

让我们看看实现目标需要做些什么:

  1. 将MDX内容提取为文本以计算阅读时间。
  2. 计算阅读时间。
  3. 将阅读时间数据附加到MDX内容中,使其在将MDX文件作为ES模块导入时可用。

幸运的是,已经有库可以帮助我们计算阅读时间和进行基本的AST操作:

  • reading-time 用于计算阅读时间。
  • mdast-util-to-string 用于将MDX AST转换为文本。
  • estree-util-value-to-estree 用于将阅读时间数据转换为ESTree节点。

如果你使用TypeScript,你可能还需要安装这些包以获得类型定义:

  • @types/mdast 用于MDX根节点类型定义。
  • unified 用于插件类型定义。

只要我们安装了这些包,就可以开始创建插件:

import { type Root } from 'mdast';
import { toString } from 'mdast-util-to-string';
import getReadingTime from 'reading-time';
import { type Plugin } from 'unified';

// 第一个参数是配置,在这种情况下不需要。你可以更新类型,如果你需要配置的话。
export const remarkMdxReadingTime: Plugin<void[], Root> = function () {
  return (tree) => {
    const text = toString(tree);
    const readingTime = getReadingTime(text);

    // TODO: 将阅读时间数据附加到MDX内容
  };
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

如我们所见,插件简单地将MDX内容提取为文本并计算阅读时间。现在我们需要将阅读时间数据附加到MDX内容中,这看起来不是很简单。但如果我们查看其他很棒的库,比如 remark-mdx-frontmatter,我们可以找到一种方法来实现:

import { valueToEstree } from 'estree-util-value-to-estree';
import { type Root } from 'mdast';
import { toString } from 'mdast-util-to-string';
import getReadingTime from 'reading-time';
import { type Plugin } from 'unified';

export const remarkMdxReadingTime: Plugin<void[], Root> = function () {
  return (tree) => {
    const text = toString(tree);
    const readingTime = getReadingTime(text);

    tree.children.unshift({
      type: 'mdxjsEsm',
      value: '',
      data: {
        estree: {
          type: 'Program',
          sourceType: 'module',
          body: [
            {
              type: 'ExportNamedDeclaration',
              specifiers: [],
              declaration: {
                type: 'VariableDeclaration',
                kind: 'const',
                declarations: [
                  {
                    type: 'VariableDeclarator',
                    id: { type: 'Identifier', name: 'readingTime' },
                    init: valueToEstree(readingTime, { preserveReferences: true }),
                  },
                ],
              },
            },
          ],
        },
      },
    });
  };
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

注意代码中的type: 'mdxjsEsm'。这是一个用于 序列化MDX ESM的节点类型。上面的代码使用名称readingTimereading time数据附加到MDX内容中,当将MDX文件作为ES模块导入时将得到如下输出:

{
  default: [Function: MDXContent],
  readingTime: { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }, // 阅读时间数据
}
  • 1.
  • 2.
  • 3.
  • 4.

如果你需要更改阅读时间数据的名称,可以更新Identifier节点的name属性。

TypeScript支持

为了使插件对开发者更加友好,我们可以通过增强MDX类型定义进行最后的调整:

declare module '*.mdx' {
  import { type ReadTimeResults } from 'reading-time';

  export const readingTime: ReadTimeResults;
  // ...其他增强
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

现在,当导入MDX文件时,TypeScript将识别readingTime属性:

import { readingTime } from './example.mdx';

console.log(readingTime); // { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }
  • 1.
  • 2.
  • 3.

结论

希望本指南能帮助你在处理MDX文件时获得更好的体验。通过这个remark插件,你可以直接使用阅读时间数据,甚至利用ESM树摇优化性能。