本指南介绍如何创建一个remark插件,在将MDX文件作为ES模块导入时,使阅读时间数据可用。
Remark是一个强大的Markdown处理器,可以用来创建自定义插件以转换Markdown内容。当使用remark解析Markdown文件时,内容会被转换成抽象语法树(AST),可以通过插件进行操作。
为了提供更好的用户体验,通常会显示文章的估计阅读时间。在本指南中,我们将创建一个remark插件,从MDX文件中提取阅读时间数据,并在将MDX文件作为ES模块导入时使其可用。
开始
首先创建一个MDX文件:
# Hello, world!
This is an example MDX file.
假设我们使用Vite作为打包工具,并且使用官方的@mdx-js/rollup插件来转换MDX文件,因此我们可以将MDX文件作为ES模块导入。Vite的配置应该如下所示:
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
{
// `enforce: 'pre'` 是使MDX插件生效所必需的
enforce: 'pre',
...mdx({
// ...配置
}),
},
],
});
如果我们将MDX文件作为ES模块导入,内容将是一个对象,其中default属性包含编译后的JSX。例如:
const mdx = await import('./example.mdx');
console.log(mdx);
将会得到:
{
// ...其他属性,如果你有插件转换MDX内容
default: [Function: MDXContent],
}
一旦我们有了这样的输出,我们就准备好创建remark插件了。
创建remark插件
让我们看看实现目标需要做些什么:
- 将MDX内容提取为文本以计算阅读时间。
- 计算阅读时间。
- 将阅读时间数据附加到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内容
};
};
如我们所见,插件简单地将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 }),
},
],
},
},
],
},
},
});
};
};
注意代码中的type: 'mdxjsEsm'。这是一个用于序列化MDX ESM的节点类型。上面的代码使用名称readingTime将reading time数据附加到MDX内容中,当将MDX文件作为ES模块导入时将得到如下输出:
{
default: [Function: MDXContent],
readingTime: { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }, // 阅读时间数据
}
如果你需要更改阅读时间数据的名称,可以更新Identifier节点的name属性。
TypeScript支持
为了使插件对开发者更加友好,我们可以通过增强MDX类型定义进行最后的调整:
declare module '*.mdx' {
import { type ReadTimeResults } from 'reading-time';
export const readingTime: ReadTimeResults;
// ...其他增强
}
现在,当导入MDX文件时,TypeScript将识别readingTime属性:
import { readingTime } from './example.mdx';
console.log(readingTime); // { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }
结论
希望本指南能帮助你在处理MDX文件时获得更好的体验。通过这个remark插件,你可以直接使用阅读时间数据,甚至利用ESM树摇优化性能。