diff --git a/docs/dev/frontend/docusaurus插件开发.md b/docs/dev/frontend/docusaurus插件开发.md new file mode 100644 index 0000000..f631f15 --- /dev/null +++ b/docs/dev/frontend/docusaurus插件开发.md @@ -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 lifecycles:i18n 生命周期,一些 i18n 翻译相关的内容 +- Static methods:静态方法,主要是校验选项和校验主题两个方法 + +这里我简单的介绍一些常用的: + +### loadContent + +这个 API 可以用来预加载一些内容,比如读取文件,将某些数据预加载好,这样在后续的 API 中就可以使用这些内容了。 +例如官方的`docusaurus-plugin-content-docs`插件就是在此预加载好所有的文档数据,然后在`contentLoaded`中再添加路由的。 + +### contentLoaded + +这个 API 有两个参数,一个是 content,一个是 actions,content 就是 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> { + const ret = (await docsPlugin.call( + this, + context, + options + )) as Plugin; + 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) \ No newline at end of file diff --git a/docs/dev/frontend/img/docusaurus插件开发.assets/image-20240324133547293.png b/docs/dev/frontend/img/docusaurus插件开发.assets/image-20240324133547293.png new file mode 100644 index 0000000..29f0eda Binary files /dev/null and b/docs/dev/frontend/img/docusaurus插件开发.assets/image-20240324133547293.png differ diff --git a/docs/dev/language/golang/code/io_test.go b/docs/dev/language/golang/code/io_test.go index 8ed3b2d..095c613 100644 --- a/docs/dev/language/golang/code/io_test.go +++ b/docs/dev/language/golang/code/io_test.go @@ -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() diff --git a/docs/dev/language/golang/go-io.md b/docs/dev/language/golang/go-io.md index 992510c..8f9508c 100644 --- a/docs/dev/language/golang/go-io.md +++ b/docs/dev/language/golang/go-io.md @@ -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 diff --git a/docs/dev/language/golang/go-slice.md b/docs/dev/language/golang/go-slice.md index a9ef33b..7f01741 100644 --- a/docs/dev/language/golang/go-slice.md +++ b/docs/dev/language/golang/go-slice.md @@ -172,7 +172,7 @@ func ModifySlice(slice []int) { 在1.18之前,切片的扩容是原来的2倍,但是当容量超过1024时,每次容量变成原来的1.25倍,直到大于期望容量。在1.18后更换了新的机制: - +[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`的一系列操作,这是内存对齐的一系列计算。 - +[src/runtime/slice.go#L188](https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L188) 逻辑比较复杂,可以进入调试模式跟踪逻辑,这里就不多展开了 diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 2ae6b76..d67affc 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -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", diff --git a/packages/docusaurus-plugin-content-docs-ex/src/index.ts b/packages/docusaurus-plugin-content-docs-ex/src/index.ts index 8fbc239..510ace7 100644 --- a/packages/docusaurus-plugin-content-docs-ex/src/index.ts +++ b/packages/docusaurus-plugin-content-docs-ex/src/index.ts @@ -36,11 +36,6 @@ export default async function pluginContentDocs( )) as Plugin; 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(); diff --git a/src/theme/Heading/index.tsx b/src/theme/Heading/index.tsx index 8f43205..983be35 100644 --- a/src/theme/Heading/index.tsx +++ b/src/theme/Heading/index.tsx @@ -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 ( <> {props.children} - {detail && !syntheticTitle && ( + {detail && !syntheticTitle && props.as.toString() === "h1" && doc.contentTitle==props.children && ( {children}