docusaurus插件开发
Some checks failed
Release / deploy (push) Failing after 1m42s

This commit is contained in:
王一之 2024-03-24 18:34:18 +08:00
parent ac55a0c6a2
commit e9c32c4118
9 changed files with 340 additions and 47 deletions

View File

@ -0,0 +1,229 @@
# docusaurus 的插件开发与自定义主题
我为了本博客的一些需求,开发了一些 docusaurus 的插件,这里记录一下开发过程。其中踩到了一些坑,折腾了我好几天,结果回过头来一看是那么简单,希望对你能有所帮助。
## 创建插件
你需要创建一个类似下面的 npm 包:
```json
{
"name": "docusaurus-plugin-docs-info",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@docusaurus/core": "^3.1.1",
"dayjs": "^1.11.10",
"reading-time": "^1.5.0"
}
}
```
目录结构就像这样:
![image-20240324133547293](img/docusaurus%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91.assets/image-20240324133547293.png)
其中源代码放在 src 目录下theme 是相关主题components 是组件
## 引用插件
创建完成插件后可以使用下面的方式进行引用
### 本地路径
可以通过本地相对路径来进行引用
```js
plugins: [
["./packages/docusaurus-plugin-docs-info", {}]
],
```
### npm 包
也可以使用安装 npm 包的形式来引用
首先需要执行:`npm i ./packages/docusaurus-plugin-content-docs-ex`来安装包,然后再使用下方的配置进行配置
```json
plugins: [
["docusaurus-plugin-docs-info", {}]
],
```
## API
你可以看官方文档了解更多:[Plugin Method References](https://www.docusaurus.cn/docs/api/plugin-methods),官方主要分为以下四个部分:
- Lifecycle APIs生命周期 API当到达构建的某些阶段docusaurus 会调用插件的生命周期 API你可以在这些 API 中做一些事情,比如修改配置,添加一些内容等。
- Extending infrastructure扩展基础设施
- I18n lifecyclesi18n 生命周期,一些 i18n 翻译相关的内容
- Static methods静态方法主要是校验选项和校验主题两个方法
这里我简单的介绍一些常用的:
### loadContent
这个 API 可以用来预加载一些内容,比如读取文件,将某些数据预加载好,这样在后续的 API 中就可以使用这些内容了。
例如官方的`docusaurus-plugin-content-docs`插件就是在此预加载好所有的文档数据,然后在`contentLoaded`中再添加路由的。
### contentLoaded
这个 API 有两个参数,一个是 content一个是 actionscontent 就是 loadContent 返回的内容,
actions 里面包含了`addRoute``createData``setGlobalData`3 个方法,
可以通过这三个方法添加数据和路由,这样就可以自定义一些页面了。
### getThemePath
获取主题路径,在`contentLoaded`添加路由时,会要求填写一个组件:这里的组件填写的是一个字符串,你可以用`@site/`前缀表示主路径下的组件,也可以使用`@theme/`前缀表示`getThemePath`返回的路径下的组件。
```ts
export default function (context: LoadContext, options: any): Plugin {
const themePath = path.resolve(__dirname, "./theme");
return {
name: "docusaurus-plugin-docs-info",
getThemePath() {
return themePath;
},
};
}
// 添加路由
addRoute({
path: "/timeline",
component: "@theme/Timeline",
modules: {
articles: pageData,
},
exact: true,
});
```
### injectHtmlTags
在 body/head 中注入一些标签,比如添加一些统计代码,广告代码等。
### postBuild
构建完成时调用,会有生成路由的信息,可以在这里做一些事情,比如生成 sitemap rss 等。
### validateOptions
校验选项,可以在这里校验一些配置是否正确。
## Hooks
除了上面的 API 之外,还有一些 hooks 可以使用,很多都是官方内部的,文档中并没有说明,也是我翻阅源码看到的,这里我列举一些我用到的:
### useGlobalData/PluginData
用于获取在`contentLoaded`中设置的全局数据与插件数据,可以在组件中使用。
```ts
import useGlobalData, {
usePluginData,
useAllPluginInstancesData,
} from "@docusaurus/useGlobalData";
```
### useDoc
获取文档数据,可以获取到文档的标题、内容、路径等。
```ts
import { useDoc } from "@docusaurus/theme-common/internal";
```
### useColorMode
获取颜色模式,例如黑夜/白天模式。
```ts
import { useColorMode } from "@docusaurus/theme-common";
```
## 自定义主题
如果你想要自定义你自己的主题,你可以使用`npm run swizzle --typescript`命令,这样可以让你替换插件的默认组件,来自定义你自己的样式。
官方文档:[Swizzling | Docusaurus](https://docusaurus.io/docs/swizzling)
例如我就包装了原版的官方 docs 插件,然后自定义了主题,使我的博客 docs 稳定支持了创建时间与阅读时间。
## 使用包装器模式重制官方 docs 插件
虽然使用其它模式,例如再新写一个插件,也可以实现,但是使用包装器模式,可以更好的保持原有的插件的功能,只是在原有的基础上添加一些新的功能。
此处需要注意一个坑,如果使用包装器模式需要在`docusaurus.config.ts`中将原本的 docs 插件关闭掉,否则会产生冲突。
```ts
presets: [
[
"classic",
{
docs: false,
// docs: {
// routeBasePath: "/",
// sidebarPath: "./sidebars.ts",
// // Please change this to your repo.
// // Remove this to remove the "edit this page" links.
// editUrl: "https://github.com/codfrm/blog",
// showLastUpdateTime: true,
// },
} satisfies Preset.Options,
],
],
```
引用时在plugin单独引用
```ts
plugins: [
[
"docusaurus-plugin-content-docs-ex",
{
routeBasePath: "/",
sidebarPath: "./sidebars.ts",
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/CodFrm/blog/edit/main",
showLastUpdateTime: true,
},
]
]
```
然后就可以在插件中使用原有的 docs 插件,然后在原有的基础上添加一些新的功能。
```ts
export default async function pluginContentDocs(
context: LoadContext,
options: PluginOptions & { debug?: boolean }
): Promise<Plugin<LoadedContent>> {
const ret = (await docsPlugin.call(
this,
context,
options
)) as Plugin<LoadedContent>;
const themePath = path.resolve(__dirname, "./theme");
ret.getThemePath = () => {
return themePath;
};
const warpLoadContent = ret.loadContent;
ret.loadContent = async () => {
const ret = await warpLoadContent();
// ....一些操作
return ret;
}
return ret;
}
```
更多详细的内容你可以看我的插件:[docusaurus-plugin-content-docs-ex](https://github.com/CodFrm/blog/tree/main/packages/docusaurus-plugin-content-docs-ex)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -3,12 +3,41 @@ package code
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/md5"
"crypto/sha1"
"io"
"testing"
)
type WrapReader struct {
len int
r io.Reader
}
func (w *WrapReader) Read(p []byte) (int, error) {
n, err := w.r.Read(p)
w.len += n
return n, err
}
func TestReader(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
wr := WrapReader{len: 0, r: file}
_, _ = io.Copy(io.Discard, &wr)
t.Logf("文件长度: %d\n", wr.len)
}
func TestGzip(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
buf := bytes.NewBuffer(nil)
w := gzip.NewWriter(buf)
_, _ = io.Copy(w, file)
t.Logf("不Close文件长度: %d\n", buf.Len())
w.Close()
t.Logf("压缩后文件长度: %d\n", buf.Len())
}
func TestIOHash(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
md5 := md5.New()

View File

@ -1,31 +1,71 @@
# Golang IO相关操作
# Golang IO 相关操作
- io包提供了基础的io操作几乎所有的io操作都是基于io.Reader和io.Writer接口的。
- ioutil 包提供了一些方便的io操作函数但是在go 1.16中已经被废弃放进了io包。
- bufio实现了缓冲io可以提高io效率。
- bytes 包中提供了Buffer类型可以用来做io操作。
- io 包提供了基础的 io 操作,几乎所有的 io 操作都是基于 io.Reader io.Writer 接口的。
- ~~ioutil 包提供了一些方便的 io 操作函数,但是在 go 1.16 中已经被废弃,放进了 io 包。~~
- bufio 实现了缓冲 io可以提高 io 效率。
- bytes 包中提供了 Buffer 类型,可以用来做 io 操作。
## 核心接口
### Reader/Writer/Seeker
Go 中几乎所有的 io 操作都是围绕着 Reader/Writer/Seeker 这三个接口进行,这三个接口都是独立开来的,可以随机组合。
Reader和Writer是io包中最重要的接口它们定义了io操作的基本行为。像一些文件操作网络操作等等都是基于这两个接口的。
Reader Writer io 包中最重要的接口,它们定义了 io 操作的基本行为。像一些文件操作,网络操作等等,都是基于这两个接口的。
Seeker接口定义了Seek方法可以在Reader/Writer中定位到指定的位置文件操作中常用。
Seeker 接口定义了 Seek 方法,可以在 Reader/Writer 中定位到指定的位置,文件操作中常用。
还有一个 Close 接口,用来关闭 io也是经常使用有这个接口的时候我们可以使用 defer 来关闭,避免忘记,而且一般情况下都需要注意关闭,否则会产生内存泄漏之类的问题。
有很多操作也是需要执行完 Close 之后才能生效,例如 gzip 压缩,如果不 Close可能会导致压缩文件不完整。
```go
func TestGzip(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
buf := bytes.NewBuffer(nil)
w := gzip.NewWriter(buf)
_, _ = io.Copy(w, file)
t.Logf("不Close文件长度: %d\n", buf.Len())
w.Close()
t.Logf("压缩后文件长度: %d\n", buf.Len())
}
```
### 基础使用
别看这三个接口简单,他们可以玩出很多花出来。例如对 Reader/Writer 进行包装,实现读取写入大小统计,来做流量统计时这很有用;标准库中也有很多方法,值得可以学习。
```go
type WrapReader struct {
len int
r io.Reader
}
func (w *WrapReader) Read(p []byte) (int, error) {
n, err := w.r.Read(p)
w.len += n
return n, err
}
func TestReader(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
wr := WrapReader{len: 0, r: file}
_, _ = io.Copy(io.Discard, &wr)
t.Logf("文件长度: %d\n", wr.len)
}
```
## 常用函数
下面我们来看一些常用的io操作函数更多的函数可以查看官方文档我只是列出一些我常用到的。
下面我们来看一些常用的 io 操作函数,更多的函数可以查看官方文档,我只是列出一些我常用到的。
- io.ReadeAll 读取所有数据返回一个bytes
- io.MultiWriter/Reader 可以将多个Reader/Writer合并成一个
- io.LimitReader 限制Reader的读取长度读取n个长度后返回io.EOF
- io.TeeReader 读取数据的同时写入到另一个Writer中
- io.ReadAll 读取所有数据,返回一个 bytes
- io.MultiWriter/Reader 可以将多个 Reader/Writer 合并成一个
- io.LimitReader 限制 Reader 的读取长度,读取 n 个长度后返回 io.EOF
- io.TeeReader 读取数据的同时写入到另一个 Writer
- io.Pipe 创建同步内存管道
- io.Copy/CopyN 复制Reader到Writer
- io.NopCloser 给一个io.Reader加上Close方法
- bytes.NewBuffer 创建一个Buffer
- bufio.NewReader/Writer 创建一个缓冲Reader/Writer
- io.Copy/CopyN 复制 Reader Writer
- io.NopCloser 给一个 io.Reader 加上 Close 方法
- bytes.NewBuffer 创建一个 Buffer
- bufio.NewReader/Writer 创建一个缓冲 Reader/Writer
## 常见小坑
@ -33,32 +73,32 @@ Seeker接口定义了Seek方法可以在Reader/Writer中定位到指定的位
### io.EOF
io.EOF是io包中定义的一个错误表示读取到文件末尾。在读取文件时当读取到文件末尾时会返回io.EOF错误。
io.EOF io 包中定义的一个错误,表示读取到文件末尾。在读取文件时,当读取到文件末尾时,会返回 io.EOF 错误。
请注意当读取到文件末尾时返回的数据可能不是nil而是文件的最后一部分数据需要注意处理。
请注意,当读取到文件末尾时,返回的数据可能不是 nil而是文件的最后一部分数据需要注意处理。
### 大文件的操作
在处理大文件时需要注意内存的使用尽量使用io.Reader和io.Writer来处理文件避免一次性读取整个文件到内存中。
在处理大文件时,需要注意内存的使用,尽量使用 io.Reader io.Writer 来处理文件,避免一次性读取整个文件到内存中。
### bufio.Writer.Flush
在使用bufio.Writer时需要注意Flush方法因为bufio.Writer是带缓冲的数据并不是实时写入到Writer中的需要调用Flush方法来刷新缓冲区。
在使用 bufio.Writer 时,需要注意 Flush 方法,因为 bufio.Writer 是带缓冲的,数据并不是实时写入到 Writer 中的,需要调用 Flush 方法来刷新缓冲区。
### io.Pipe
io.Pipe是一个同步的内存管道请注意在使用时Reader与Writer必须在不同的goroutine中且需要注意Reader与Writer的速度需要一致否则会造成死锁或线程阻塞。
io.Pipe 是一个同步的内存管道请注意在使用时Reader Writer 必须在不同的 goroutine 中,且需要注意 Reader Writer 的速度需要一致,否则会造成死锁或线程阻塞。
并且需要使用Close方法来关闭Reader或Writer否则会造成内存泄漏。
并且需要使用 Close 方法来关闭 Reader Writer否则会造成内存泄漏。
## 高级用例
下面介绍一些高级用例你也可以从中学习到一些高级的io操作技巧。
下面介绍一些高级用例,你也可以从中学习到一些高级的 io 操作技巧。
### 计算上传文件的hash
### 计算上传文件的 hash
下面的例子是计算上传文件的md5和sha1的hash值我们使用io.TeeReader将文件内容同时写入到md5和sha1的hash计算器中。
像我们在http上传文件时可以在上传的同时计算文件的hash值这样可以保证文件的完整性。同时也可以打开文件再将文件的Writer放到io.Copy中这样可以同时写入文件和计算hash值。
下面的例子是计算上传文件的 md5 sha1 hash 值,我们使用 io.TeeReader 将文件内容同时写入到 md5 sha1 hash 计算器中。
像我们在 http 上传文件时,可以在上传的同时计算文件的 hash 值,这样可以保证文件的完整性。同时也可以打开文件,再将文件的 Writer 放到 io.Copy 中,这样可以同时写入文件和计算 hash 值。
```go
func TestIOHash(t *testing.T) {
@ -74,7 +114,7 @@ func TestIOHash(t *testing.T) {
### 读取文件的部分内容
下面的例子是读取文件的部分内容我们使用io.LimitReader限制读取的长度读取n个长度后返回io.EOF这在解析文件头部信息时非常有用。
下面的例子是读取文件的部分内容,我们使用 io.LimitReader 限制读取的长度,读取 n 个长度后返回 io.EOF这在解析文件头部信息时非常有用。
```go
func TestLimitReader(t *testing.T) {
@ -87,9 +127,9 @@ func TestLimitReader(t *testing.T) {
### 大文件按行读取
下面的例子是读取大文件的每一行我们使用bufio.Reader来读取文件的每一行这样可以避免一次性读取整个文件到内存中。
下面的例子是读取大文件的每一行,我们使用 bufio.Reader 来读取文件的每一行,这样可以避免一次性读取整个文件到内存中。
在读一些io比较慢的文件时网络文件也可以使用这种方式来读取文件。写文件也是类似的可以使用bufio.Writer来写文件提高磁盘io效率避免频繁的io操作。
在读一些 io 比较慢的文件时(网络文件),也可以使用这种方式来读取文件。写文件也是类似的,可以使用 bufio.Writer 来写文件,提高磁盘 io 效率,避免频繁的 io 操作。
```go
func TestBufIo(t *testing.T) {
@ -106,9 +146,9 @@ func TestBufIo(t *testing.T) {
}
```
### 使用Pipe将上传文件解密写入文件
### 使用 Pipe 将上传文件解密写入文件
有时候上传的文件是一个加密的我们需要在上传的同时解密文件然后同时写入到磁盘中这时候可以使用io.Pipe来实现。
有时候上传的文件是一个加密的,我们需要在上传的同时解密文件,然后同时写入到磁盘中,这时候可以使用 io.Pipe 来实现。
```go

View File

@ -172,7 +172,7 @@ func ModifySlice(slice []int) {
在1.18之前切片的扩容是原来的2倍但是当容量超过1024时每次容量变成原来的1.25倍直到大于期望容量。在1.18后更换了新的机制:
<https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L267>
[src/runtime/slice.go#L267](https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L267)
- 当新切片>旧切片\*2时直接安装新切片容量计算
- 如果旧切片\<256新切片容量为旧切片\*2
@ -195,7 +195,7 @@ func TestCap(t *testing.T) {
至于实际的结果为什么没有和上述说的一样,可以看到,在`nextslicecap`计算出容量后续,还有对`newcap`的一系列操作,这是内存对齐的一系列计算。
<https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L188>
[src/runtime/slice.go#L188](https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L188)
逻辑比较复杂,可以进入调试模式跟踪逻辑,这里就不多展开了

View File

@ -4,8 +4,8 @@ import type * as Preset from "@docusaurus/preset-classic";
import docsToBlog from "./packages/docusaurus-plugin-docs-info/src";
const config: Config = {
title: "王一之",
tagline: "王一之的个人博客:分享知识,记录生活,认识朋友",
title: "一知",
tagline: "王一之的个人博客:分享知识,认识朋友",
favicon: "img/favicon.ico",
// Set the production url of your site here
@ -77,7 +77,7 @@ const config: Config = {
// Replace with your project's social card
image: "img/docusaurus-social-card.jpg",
navbar: {
title: "王一之的博客",
title: "一知",
logo: {
alt: "Logo",
src: "img/avatar.png",

View File

@ -36,11 +36,6 @@ export default async function pluginContentDocs(
)) as Plugin<LoadedContent>;
const isProd = process.env.NODE_ENV === "production";
const themePath = path.resolve(__dirname, "./theme");
ret.getThemePath = () => {
return themePath;
};
const warpLoadContent = ret.loadContent;
ret.loadContent = async () => {
const ret = await warpLoadContent();

View File

@ -9,12 +9,11 @@ export default function HeadingWrapper(props) {
const syntheticTitle = useSyntheticTitle();
const doc = useDoc();
const detail = (doc.metadata as any).detail as Detail;
return (
<>
<Heading {...props}>
{props.children}
{detail && !syntheticTitle && (
{detail && !syntheticTitle && props.as.toString() === "h1" && doc.contentTitle==props.children && (
<span
style={{
display: "block",

View File

@ -6,9 +6,10 @@ export default function AntdProvider({ children }) {
return (
<ConfigProvider
theme={{
algorithm: colorMode.isDarkTheme
? theme.darkAlgorithm
: theme.defaultAlgorithm,
algorithm:
colorMode.colorMode == "dark"
? theme.darkAlgorithm
: theme.defaultAlgorithm,
}}
>
{children}