跳到主要内容

创建 FileBlock 自定义组件

背景

有些时候需要引用一个代码文件,而不是直接在 markdown 中写代码。比如内容很多,如果都直接写到 markdown 文件中的话,维护起来很麻烦。

Docusaurus 官方方案

Docusaurus 官方提供了 Importing code snippets 的方法,就是 import CodeBlock 标签,再通过 raw-loader 将源文件内容读取成字符串传给 CodeBlock 标签。虽然能实现将代码文件导入成代码块,但对于我来说并不完美,因为我的很多文档中有大量的示例代码文件,而且很多内容都很多,用官方的这种方式会搞的满屏的 import 和变量传递,写起来麻烦不说,还巨丑。

基于 CodeBlock 实现 FileBlock

由于官方方案存在的缺陷,我决定扩展出一个更好的方案:

  1. 使用 FileBlock 标签导入代码文件,可指定代码文件路径,无需显式读取和传递文件内容。
  2. 可根据文件后缀自动识别语言类型进行语法高亮渲染。
  3. 可选显示文件名,也可以手动设置文件名。

安装依赖

npm install --save raw-loader path-browserify

创建 FileBlock 组件

新建文件 src/components/FileBlock.tsx

src/components/FileBlock.tsx
import React from 'react';
import CodeBlock from '@theme/CodeBlock';
import { useLocation } from '@docusaurus/router';
import * as path from 'path-browserify';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

let extToLang = new Map([
["sh", "bash"],
["yml", "yaml"]
]);

export default function FileBlock({ file, showFileName, ...prop }: { file: string, showFileName?: boolean }) {
// get url path without "/" prefix and suffix
var urlPath = useLocation().pathname.replace(/^\/|\/$/g, '');

// remove locale prefix in urlPath
const { i18n } = useDocusaurusContext()
if (i18n.currentLocale != i18n.defaultLocale) {
urlPath = urlPath.replace(/^[^\/]*\/?/g, '')
}

// find file content according to topPath and file path param
var filepath = ""
if (file.startsWith("@site/")) {
filepath = file.replace(/^@site\//g, '')
} else {
filepath = "codeblock/" + file
}

// load file raw content according to filepath
var content = require('!!raw-loader!@site/' + filepath)?.default
content = content.replace(/\t/g, " "); // replace tab to 2 spaces

// infer language of code block based on filename extension if language is not set
const filename = path.basename(file);
if (!prop.language) {
var language = path.extname(filename).replace(/^\./, '')
const langMappingName = extToLang.get(language)
if (langMappingName) {
language = langMappingName
}
prop.language = language
}

// set title to filename if showFileName is set and title is not set
if (!prop.title && showFileName) {
prop.title = filename
}

return (
<CodeBlock {...prop}>
{content}
</CodeBlock>
);
}

扩展 MDXComponents

新建文件 src/theme/MDXComponents.tsx 来扩展默认的 MDXComponents

src/theme/MDXComponents.tsx
import React from 'react';
// Import the original mapper
import MDXComponents from '@theme-original/MDXComponents';

import FileBlock from '@site/src/components/FileBlock';
import CodeBlock from '@theme-original/CodeBlock';
import Tabs from '@theme-original/Tabs';
import TabItem from '@theme-original/TabItem';

export default {
// Re-use the default mapping
...MDXComponents,
// Add more components to be imported by default
FileBlock,
CodeBlock,
Tabs,
TabItem,
};

配置插件

存放到 codeblock 目录下的所有文件用于代码文件的导入,不单独渲染页面,配置 plugin-content-docs 插件在生成页面时忽略该目录下的文件:

docusaurus.config.js
  plugins: [
[
/** @type {import('@docusaurus/plugin-content-docs').PluginOptions} */
'@docusaurus/plugin-content-docs',
({
id: 'kubernetes',
path: 'kubernetes',
routeBasePath: '/kubernetes',
sidebarPath: require.resolve('./kubernetes/sidebars.js'),
remarkPlugins: [
[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }],
],
editUrl: ({ docPath }) =>
`https://github.com/imroc/kubernetes-guide/edit/master/${docPath}`,
}),
],
[
'@docusaurus/plugin-content-docs',
/** @type {import('@docusaurus/plugin-content-docs').PluginOptions} */
({
id: 'istio',
path: 'istio',
routeBasePath: '/istio',
sidebarPath: require.resolve('./istio/sidebars.js'),
remarkPlugins: [
[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }],
],
editUrl: ({ docPath }) =>
`https://github.com/imroc/istio-guide/edit/master/${docPath}`,
}),
],
[
'@docusaurus/plugin-content-docs',
/** @type {import('@docusaurus/plugin-content-docs').PluginOptions} */
({
id: 'note',
path: 'note',
exclude: ['codeblock/**'],
routeBasePath: '/note',
sidebarPath: require.resolve('./note/sidebars.js'),
remarkPlugins: [
[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }],
],
editUrl: ({ docPath }) =>
`https://github.com/imroc/imroc.cc/edit/master/note/${docPath}`,
}),
],
]

在 markdown 中使用 FileBlock 引用代码文件

<FileBlock file="demo/hello.go" />

结合 Tab 与 TabItem 实现多标签代码块

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import FileBlock from '@site/src/components/FileBlock';

<Tabs>
<TabItem value="js" label="JavaScript">
<FileBlock file="demo/hello.js" />
</TabItem>

<TabItem value="py" label="Python">
<FileBlock file="demo/hello.py" />
</TabItem>

<TabItem value="java" label="Java">
<FileBlock file="demo/hello.java" />
</TabItem>
</Tabs>