创建和发布开源包是为生态系统和社区做贡献的好方法。你做了一个很酷的东西,并希望人们使用它。但仅仅将模块发布到注册表并祈祷用户会使用是不够的。帮助用户成功使用您的包不仅需要编写简明、描述性的文档,还需要确保用户能够在他们的工作流中访问文档(例如在 VSCode 中),以节省他们的时间。
感谢 JSDoc,编写与代码相关的文档并以多种格式供用户使用变得很容易。当与现代发布流程如 JSR 结合使用时,您可以轻松创建全面的包文档,不仅适合您的工作流,还能直接集成到用户使用您包的工具中。本博文旨在介绍编写 JSDoc 样式注释的最佳实践,以帮助您的用户尽快上手:
为什么选择 JSDoc?
好的 README 能回答“为什么要使用您的包?”,而好的文档应该回答“如何使用您的包?”。浏览您的文档的用户有一个需要解决的问题,而您的文档应该以最少的点击和键盘操作提供答案。
JSDoc 是一种很好的方式来编写与代码本身相关的参考文档,用户可以以多种格式(如 HTML、markdown、JSON 或在他们的 IDE 或文本编辑器中)使用这些文档。以下是一个 JSDoc 样式注释示例及其在各种媒介中作为文档出现的快速示意图:
当您在代码中编写 JSDoc 样式注释并发布到 JSR 时,它将格式化后显示在您的包的 JSR 文档页面上、VSCode 工具提示和自动完成中,以及 deno doc
输出中。
编写好的 JSDoc 可以提高您包的成功率。在我们深入探讨一些最佳实践之前,先简要介绍一下 JSDoc。
JSDoc 简介
JSDoc 将您的代码注释转换为文档对象,可以以多种格式呈现和显示。
JSDoc 注释是任何以 /**
开始并以 */
结束的块注释,这些注释位于代码块之前。以下是一个示例:
/** 添加两个值并返回它们的和。 */
function sum(value1, value2) {
return value1 + value2;
}
这个 JSDoc 然后会在您的 IDE 中作为工具提示出现:
JSDoc 注释可以跨多行。每行应以 *
开头并缩进一个空格。
/**
* 添加两个值并返回它们的和。
*
* 注意:JavaScript 数学使用 IEEE 754 浮点算术,因此在添加两个数字时可能会有一些舍入误差。
*/
function sum(value1, value2) {
return value1 + value2;
}
JSDoc 注释的第一段最重要。 它是符号的摘要,会显示在工具提示、编辑器中的自动完成以及搜索索引中。第一段应该是对符号的简明描述,应该以帮助用户快速理解此函数的作用的方式编写。
例如,不要写:
/**
* 此函数接受一个字符串作为第一个参数,并返回一个字符串。它使用正则表达式查找输入字符串中的所有空格,然后一个一个地将它们替换为下划线。然后函数返回修改后的字符串。
*/
function replaceSpacesWithUnderscores(value) {
return value.replace(/ /g, "_");
}
而是简洁地描述函数的作用:
/**
* 将字符串中的所有空格替换为下划线。
*/
function replaceSpacesWithUnderscores(value) {
return value.replace(/ /g, "_");
}
实现细节、注意事项或示例等附加信息应在后续段落中添加。由于 JSDoc 支持 markdown,您甚至可以使用标题来分隔不同的部分。
简单明了的摘要有助于用户在自动完成期间快速筛选符号列表并找到所需的符号。一旦他们找到了符号,他们可以阅读其余部分以了解详细信息。
提供好的类型信息
在简洁的描述性摘要之后,提供好的类型信息对您包中暴露的符号非常重要。这有两个主要目的:
- 它允许编辑器在参数和返回值上进行自动完成,因为编辑器知道参数和返回值的类型。
- 它帮助用户快速筛选函数列表以找到所需的函数。例如,如果他们在寻找一个组合两个字符串的函数,他们可以过滤掉不接受两个字符串作为参数的函数。
在这里,我们将使用 TypeScript 来添加类型信息。TypeScript 是一种基于 JavaScript 的强类型语言,通过提高代码质量和可维护性来提高开发者的生产力。
/**
* 添加两个值并返回它们的和。
*/
export function sum(value1: number, value2: number): number {
return value1 + value2;
}
在您的编辑器中,当您将鼠标悬停在函数上时,您将看到参数和返回值的类型信息:
当用户在编辑器中键入 sum(
时,他们将看到参数的类型信息:
在返回值上,您可以立即获得返回的 number
类型的方法的完成选项:
标签,标签,标签
JSDoc 支持多种标签,可用于提供有关您的符号的附加信息,如 @param
表示参数,@returns
表示返回值,或 @typeParam
表示类型参数。以下是一个带有类型信息和标签的函数示例:
/**
* 在字符串中查找子字符串并返回首次出现的索引。
*
* @param value 将搜索 needle 的字符串。
* @param needle 要在字符串中搜索的子字符串。
* @returns needle 在 value 中首次出现的索引,如果未找到 needle 则返回 -1。
*/
declare function find(value: string, needle: string): number;
在您的编辑器中,您将看到参数和返回值的类型信息,以及标签提供的附加信息:
在 JSR 上,标签以 HTML 格式呈现。以下是 deno_std/fs
中 move
函数的 JSDoc 中 @param
和 @return
标签的示例:
向 JSDoc 添加示例
示例是帮助用户快速理解如何使用您的库的另一种好方法。这对于具有复杂行为或多参数的函数尤为有用。可以使用 @example
标签将示例添加到您的 JSDoc 注释中:
/**
* 在字符串中查找子字符串并返回首次出现的索引。
*
* @example 在字符串中查找子字符串
* ```ts
* const value = "hello world";
* const needle = "world";
* const index = find(value, needle); // 6
* ```
*
* @example 查找不存在的子字符串
* ```ts
* const value = "hello world";
* const needle = "foo";
* const index = find(value, needle); // -1
* ```
*/
declare function find(value: string, needle: string): number;
最好的示例是简洁且展示函数最常见的用例。它们应易于理解,并可以复制粘贴到项目中。
如果有多个值得一提的用例,您甚至可以提供多个示例。以下是 JSR 中 deno_std/fs
的 move
函数的多个示例的呈现方式:
/**
* (为简洁起见已截断)
* @example 基本用法
* ```ts
* import { move } from "@std/fs/move";
*
* await move("./foo", "./bar");
* ```
*
* 这将把 `./foo` 处的文件或目录移动到 `./bar`,不覆盖。
*
* @example 覆盖
* ```ts
* import { move } from "@std/fs/move";
*
* await move("./foo", "./bar", { overwrite: true });
* ```
*
* 这将把 `./foo` 处的文件或目录移动到 `./bar`,如果存在则覆盖。
*/
注意,紧跟在 @example
后面的文本用作标题,而示例下方的文本在 JSR 上成为其描述:
但我应该记录什么?
你应该记录包中导出的每一个符号,包括函数、类、接口和类型别名。
这不仅仅是为每个符号添加一个 JSDoc 注释。例如,对于类和接口,你应该记录符号本身以及它的每个方法或属性,包括构造函数。以下是一个 带有 JSDoc 注释的 Oak 接口示例:
/** 基本的应用监听选项接口。 */
export interface ListenOptionsBase {
/** 监听的端口。如果未指定,默认为 `0`,由操作系统确定值。 */
port?: number;
/** 可以解析为 IP 地址的字面 IP 地址或主机名。
* 如果未指定,默认为 `0.0.0.0`。
*
* **关于 `0.0.0.0` 的注意事项** 虽然在所有平台上监听 `0.0.0.0` 都有效,
* 但 Windows 上的浏览器不支持 `0.0.0.0` 地址。
* 如果你的程序支持 Windows,你应该显示 `server running on localhost:8080`
* 而不是 `server running on 0.0.0.0:8080`。 */
hostname?: string;
secure?: false;
/** 可选的中止信号,可用于关闭监听器。 */
signal?: AbortSignal;
}
如果你的包包含 多个模块,在每个模块文件顶部添加 @module
标签的 JSDoc 注释会很有帮助。此模块注释应包含描述及其导出符号的使用示例。
以下是 Oak 的 application.ts
文件中 @module
的示例:
/**
* 包含 oak 的核心概念,即中间件应用程序。典型用法是创建应用实例,注册中间件,
* 然后开始监听请求。
*
* # 示例
*
* ```ts
* import { Application } from "jsr:@oak/oak@14/application";
*
* const app = new Application();
* app.use((ctx) => {
* ctx.response.body = "hello world!";
* });
*
* app.listen({ port: 8080 });
* ```
*
* @module
*/
在 JSR 上,第一段成为你包的 主文档页面 下模块的描述:
注意主文档页面只包含第一段。后续的 JSDoc 注释在点击后会出现在 模块页面:
使用 markdown 提供更好的文档体验
在 JSDoc 中使用 markdown 可以以更易读和引人入胜的方式组织文档。这有助于创建更易于理解的文档,并允许你使用链接引用外部资源或文档的其他部分。
你可以在 JSDoc 注释中使用一些 有用的 markdown 特性:
# my heading
用于章节标题- hello world
用于项目符号列表**important**
用于强调_noteworthy_
用于斜体> quote
用于引用块[foo](https://example.com)
用于链接`console.log("foo")`
用于内联代码片段
在 JSR 上,你还可以使用 [!IMPORTANT]
突出显示你想要引起注意的重要信息。
// 版权所有 2018-2024 oak 作者。保留所有权利。MIT 许可。
/** 中间件将 oak 特定的上下文转换为 Fetch API 标准的
* {@linkcode Request} 和 {@linkcode Response} 以及提供一些 oak 功能的修改上下文。
* 这旨在使代码更容易适应 oak。
*
* 有两个函数可以“包装”一个操作 Fetch API 请求和响应的处理程序并返回一个 oak 中间件。
* {@linkcode serve} 设计用于与 {@linkcode Application} 的 `.use()` 方法一起使用,
* 而 {@linkcode route} 设计用于与 {@linkcode Router} 一起使用。
*
* > \[!IMPORTANT\]
* > 这不适用于 oak 支持的高级用例,如集成的 cookie 管理、web sockets 和服务器发送事件。
* >
* > 此外,这些设计为非常确定性的请求/响应处理程序,而不是允许高级控制的更细致的中间件堆栈。
* > 因此,没有 `next()`。
* >
* > 对于这些高级用例,请创建没有包装器的中间件。
*
* @module
*/
此模块级 JSDoc 注释将作为顶级文档在 JSR 上显示:
在文档的其他部分进行内部链接
有时,你的文档会引用包内的另一个符号。为了便于用户在文档中导航,你可以使用 @link
、@linkcode
和 @linkplain
标签在文档内进行链接。这些标签接受名称路径或 URL,并生成一个 HTML 锚元素。以下是一个示例:
/** 样式文本时使用的选项,适用于 {@linkcode print} 函数。 */
export interface StyleOptions {
/** 打印消息的颜色。 */
color: "black" | "red" | "green";
/** 是否以粗体打印消息。 */
bold: boolean;
/** 是否以斜体打印消息。 */
italic: boolean;
}
/**
* 使用给定的选项将消息打印到终端的函数。
*
* 请注意,在某些版本的 Windows 上,{@linkcode StyleOptions.color} 可能不支持
* 与 {@linkcode StyleOptions.bold} 一起使用。
*/
declare function print(message: string, options: StyleOptions): void;
在 VSCode 中,悬停工具提示现在包含可点击的链接,可直接带你到定义该符号的代码:
这是 JSR 上 @linkcode
的显示方式。在 Oak 的 serve
函数的 JSDoc 中,它引用了 Application
,这在 JSR 上成为一个可点击的链接:
你还可以引用内置的 JavaScript 对象,例如 ArrayBuffer
,JSR 将自动链接到 相关的 MDN 文档。
随代码变更保持 JSDoc 更新
使用 JSDoc 的一个好处是,在编写代码的同时编写文档。这样,每当我们需要更改函数、接口或模块时,可以在最小上下文切换成本的情况下对 JSDoc 进行必要的更改。
但如何确保注释中的文档是更新的呢?从文档驱动开发开始,能帮助你在编写代码之前构思和推理需求。有时,这意味着可以更早地发现潜在问题,从而节省重新编写代码的时间。(关于这一点的更宏观的方法,请参阅 “Readme-driven development”)
如果你在文档中包含代码示例,可以使用 deno test --doc
从命令行进行类型检查。这是确保文档中的示例是最新且有效的有用工具。
例如,基于之前的 sum
函数,让我们添加一个包含代码片段的示例:
/**
* 添加两个值并返回总和。
*
* @example
* ```ts
* import { sum } from "jsr:@deno/sum";
* const finalValue = sum(1, "this is a string"); // 3
* ```
*/
export function sum(value1: number, value2: number):
number {
return value1 + value2;
}
然后,我们可以通过运行 deno test --doc
检查文档中的代码:
deno test --doc
Check file:///Users/sum.ts$8-13.ts
error: TS2345 [ERROR]: Argument of type 'string' is not assignable to parameter of type 'number'.
const finalValue = sum(1, "this is a string");
~~~~~~~
at file:///Users/main.ts$8-13.ts:2:27
哎呀!在我们修正文档中的类型错误后:
deno test --doc
Check file:///Users/sum.ts$8-13.ts
ok | 0 passed | 0 failed (0ms)
这为你提供了一种快速检查文档中代码示例类型的方式,在发布之前确保它们是最新的。
审核你的 JSDoc
如果你发布到 JSR,JSR 将处理所有基于 JSDoc 样式注释的格式化和文档生成。然而,如果你有兴趣使用工具来审核或测试 JSDoc 注释的输出,以下是一些建议:
deno doc <file>
:这个 Deno 命令将打印file
导出成员的 JSDoc 文档。此命令还接受--html
标志,生成带有文档的静态站点,以及--json
标志生成你可以自行显示的 JSON 输出。deno doc --lint
:这个命令将检查问题,例如缺少返回类型或公共类型缺少 JSDoc 注释。这些检查有助于你编写更好的文档,并在发布前捕获潜在问题。deno test --doc
:我们在本文前面提到过这个命令,它允许你轻松地检查文档示例的类型。jsdoc <directory>
:JSDoc 的 CLI 可以使用默认模板生成静态文档站点,并提供各种配置选项标志。如果默认模板有点单调,可以使用其他模板,例如 docdash,它提供了层次导航和语法高亮。
下一步是什么?
为你的 JavaScript 包编写好的 JSDoc 对其成功至关重要。让我们回顾一下最佳实践:
- 写一个简明摘要:JSDoc 注释的第一段应该是对符号的简明描述,帮助用户快速理解其功能。
- 提供良好的类型信息:类型信息帮助用户快速筛选函数列表,找到他们需要的函数。
- 使用标签:诸如
@param
、@returns
和@typeParam
等标签提供了有关函数或类特定部分的更多信息。 - 添加示例:示例帮助用户快速理解如何使用你的库。
- 记录一切:记录你包中公开的每个符号,包括多个模块。
- 内部链接:使用
@link
、@linkcode
和@linkplain
链接文档的其他部分,帮助用户导航文档。 - 测试你的文档:使用
deno test --doc
在发布前检查文档示例的类型,并使用deno doc --lint
检查 JSDoc 注释中的问题。
通过遵循这些最佳实践,你可以为你的包创建全面的文档,帮助用户尽快上手使用。