跳到主要内容
How to document your JavaScript package

如何编写 JavaScript 包的文档

创建和发布开源包是为生态系统和社区做出贡献的好方法。您制作了一些很酷的东西,并希望人们使用它。但是,仅仅将您的模块发布到注册表并祈祷是无法获得用户的。帮助您的用户成功使用您的包不仅意味着编写简洁、描述性的文档,还意味着确保您的用户可以在其工作流程(例如,在 VSCode 中)中访问文档,以节省他们的时间。

感谢 JSDoc,可以轻松编写与您的代码耦合的文档,并且可以以各种格式供用户使用。当与像 JSR 这样的现代发布流程结合使用时,您可以轻松地为您的包创建全面的文档,这些文档不仅适合您的工作流程,还可以直接集成到您的用户使用您的包的工具中。这篇博文旨在介绍编写 JSDoc 风格注释的最佳实践,以帮助您的用户尽快上手。

为什么要使用 JSDoc?

虽然好的 README 回答了“为什么我应该使用你的包?”,但好的文档应该回答“如何我可以使用你的包?”。浏览您的文档的用户有一个他们需要解决的问题,您的文档应该以最少的点击次数和键盘敲击次数为他们提供答案。

JSDoc 是编写参考文档的好方法,该文档与代码本身耦合,并且可以以各种格式供用户使用,例如 HTML、markdown、JSON 或在他们的 IDE 或文本编辑器中。这是一个示例 JSDoc 风格注释及其在各种媒介中作为文档的显示方式的快速图表

JSDoc diagram on JSR

当您在代码中编写 JSDoc 风格的注释并发布到 JSR 时,它将以格式化的形式出现在您包在 JSR 上的文档页面、VSCode 工具提示和自动完成以及 `deno doc` 输出中。

编写好的 JSDoc 可以提高您的包的成功率。在我们深入研究一些最佳实践之前,这里对 JSDoc 进行一个简要的高级介绍。

JSDoc 简介

JSDoc 将您代码中的注释转换为文档对象,该对象可以呈现并以各种格式显示。

JSDoc 注释是以 /** 开头并以 */ 结尾的块注释,它们位于代码块之前。这是一个例子

/** Adds two values and returns the sum. */
function sum(value1, value2) {
  return value1 + value2;
}

然后,此 JSDoc 将作为工具提示出现在您的 IDE 中

Example of JSDoc with sum function

JSDoc 注释可以跨越多行。每行应以 * 开头,并应缩进一个空格。

/**
 * Adds two values and returns the sum.
 *
 * NOTE: JavaScript math uses IEEE 754 floating point arithmetic, so there may
 * be some rounding errors when adding two numbers.
 */
function sum(value1, value2) {
  return value1 + value2;
}

JSDoc 注释的第一段是最重要的。 它是符号的摘要,显示在工具提示、编辑器中的自动完成中,并由搜索索引。第一段应该是符号的简洁描述,并且应该以帮助用户快速了解此函数的作用的方式编写。

例如,不要写

/**
 * This function takes a string in the first and returns a string. It looks for
 * all the spaces in the input string using a regexp, and then replaces them one
 * by one with an underscore. The function then returns the modified string.
 */
function replaceSpacesWithUnderscores(value) {
  return value.replace(/ /g, "_");
}

相反,简洁地描述函数的作用

/**
 * Replaces all spaces in a string with underscores.
 */
function replaceSpacesWithUnderscores(value) {
  return value.replace(/ /g, "_");
}

其他信息,如实现细节、注意事项或示例,应添加到后续段落中。由于 JSDoc 支持 markdown,您甚至可以使用标题来分隔不同的部分。

简单而简洁的摘要有助于用户在自动完成期间快速过滤符号列表并找到他们需要的符号。一旦他们找到符号,他们就可以阅读其余部分以了解详细信息。

提供良好的类型信息

在简洁的描述性摘要之后,为您的包中公开的符号提供良好的类型信息非常重要。这有两个主要目的

  1. 它允许在编辑器中自动完成参数和返回值,因为编辑器知道参数和返回值的类型。
  2. 它可以帮助用户快速过滤函数列表以找到他们需要的函数。例如,如果他们正在寻找一个组合两个字符串的函数,他们可以过滤掉不接受两个字符串作为参数的函数。

在这里,我们将使用 TypeScript 添加类型信息。TypeScript,增长最快的编程语言之一,是一种构建在 JavaScript 之上的强类型语言,可增强代码质量和可维护性,同时提高开发人员的生产力。

/**
 * Adds two values and returns the sum.
 */
export function sum(value1: number, value2: number): number {
  return value1 + value2;
}

在您的编辑器中,当您将鼠标悬停在函数上时,您将看到参数和返回值的类型信息

Getting type information for parameters and return value in VSCode on hover

当用户在他们的编辑器中键入 sum( 时,他们将看到参数的类型信息

Getting type information in the parameters on VSCode

在返回值上,您可以立即获得返回的 number 上的方法的完成

Getting completion options for number type in VSCode

标签、标签、标签

JSDoc 支持各种标签,可用于提供有关您的符号的其他信息,例如 @param 用于参数,@returns 用于返回值,或 @typeParam 用于类型参数。这是一个带有类型信息和标签的函数示例

/**
 * Find a substring in a string and return the index of the first occurrence.
 *
 * @param value The string that will be searched for the needle.
 * @param needle The substring to search for in the string.
 * @returns The index of the first occurrence of the needle in the value, or -1 if the needle is not found.
 */
declare function find(value: string, needle: string): number;

在您的编辑器中,您将看到参数和返回值的类型信息,以及标签提供的其他信息

Seeing param info in the hover on VSCode

在 JSR 上,标签以 HTML 呈现。这是 deno_std/fs 中的 move 函数的 JSDoc 中 @param@return 标签的示例

Seeing param and return value information on JSR

在 JSDoc 中添加示例

示例是帮助用户快速了解如何使用您的库的另一种好方法。这对于具有复杂行为或许多参数的函数尤其有用。可以使用 @example 标签将示例添加到您的 JSDoc 注释中

/**
 * Find a substring in a string and return the index of the first occurrence.
 *
 * @example Find a substring in a string
 * ```ts
 * const value = "hello world";
 * const needle = "world";
 * const index = find(value, needle); // 6
 * ```
 *
 * @example Find a substring in a string that doesn't exist
 * ```ts
 * const value = "hello world";
 * const needle = "foo";
 * const index = find(value, needle); // -1
 * ```
 */
declare function find(value: string, needle: string): number;

最好的示例是简洁的,并演示了您函数最常见的用例。它们应该易于理解,并且可以复制并粘贴到项目中。

如果存在多个值得提及的用例,您甚至可以提供多个示例。这是一个来自 deno_std/fsmove 函数的多个示例如何在 JSR 上显示的示例

/**
 * (truncated for brevity)
 * @example Basic usage
 * ```ts
 * import { move } from "@std/fs/move";
 *
 * await move("./foo", "./bar");
 * ```
 *
 * This will move the file or directory at `./foo` to `./bar` without
 * overwriting.
 *
 * @example Overwriting
 * ```ts
 * import { move } from "@std/fs/move";
 *
 * await move("./foo", "./bar", { overwrite: true });
 * ```
 *
 * This will move the file or directory at `./foo` to `./bar`, overwriting
 * `./bar` if it already exists.
 */

请注意,紧跟 @example 的文本用作标题,示例下方的文本成为其在 JSR 上的描述

How examples appear on JSR

但我应该记录什么?

您应该记录您的包导出的每个符号,包括函数、类、接口和类型别名。

这不仅仅是一个符号一个 JSDoc 注释。例如,对于类和接口,您应该记录符号本身、其上的每个方法或属性,包括构造函数。这是一个 带有其属性上的 JSDoc 注释的 Oak 接口的示例

/** Base interface for application listening options. */
export interface ListenOptionsBase {
  /** The port to listen on. If not specified, defaults to `0`, which allows the
   * operating system to determine the value. */
  port?: number;
  /** A literal IP address or host name that can be resolved to an IP address.
   * If not specified, defaults to `0.0.0.0`.
   *
   * __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms,
   * the browsers on Windows don't work with the address `0.0.0.0`.
   * You should show the message like `server running on localhost:8080` instead of
   * `server running on 0.0.0.0:8080` if your program supports Windows. */
  hostname?: string;
  secure?: false;
  /** An optional abort signal which can be used to close the listener. */
  signal?: AbortSignal;
}

这是 属性上的 JSDoc 注释在 JSR 上的显示方式

How property documentation appears on JSR

如果您的包由多个模块组成,则在每个模块文件的顶部添加带有 @module 标签的 JSDoc 注释将很有帮助。此模块注释应包括描述和如何使用其导出符号的示例。

这是 Oakapplication.ts 文件中 @module 的示例

/**
 * Contains the core concept of oak, the middleware application. Typical usage
 * is the creation of an application instance, registration of middleware, and
 * then starting to listen for requests.
 *
 * # Example
 *
 * ```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 中,第一段成为您包的 主文档页面上模块下方的描述

Oak module description

请注意,主文档页面仅包含第一段。当您点击进入 模块页面时,会出现后续的 JSDoc 注释

Oak's application module description

使用 markdown 以获得更好的文档体验

在 JSDoc 中使用 markdown 可以让您以更易读和更具吸引力的方式组织文档。这可以帮助您创建更易于理解的文档,并允许您使用链接链接到外部资源或文档的其他部分。

您可以在 JSDoc 注释中使用的一些 有用的 markdown 功能包括

  • # 我的标题 用于节标题
  • - 你好世界 用于项目符号
  • **重要** 用于 粗体
  • _值得注意_ 用于 斜体
  • > 引用 用于块引用
  • [foo](https://example.com) 用于链接
  • `console.log("foo")` 用于内联代码片段

在 JSR 上,您还可以使用 [!IMPORTANT] 来突出显示文档中您想要引起注意的重要信息。

// Copyright 2018-2024 the oak authors. All rights reserved. MIT license.

/** Middleware that converts the oak specific context to a Fetch API standard
 * {@linkcode Request} and {@linkcode Response} along with a modified context
 * providing some of the oak functionality. This is intended to make it easier
 * to adapt code to work with oak.
 *
 * There are two functions which will "wrap" a handler that operates off a
 * Fetch API request and response and return an oak middleware. The
 * {@linkcode serve} is designed for using with the {@linkcode Application}
 * `.use()` method, while {@linkcode route} is designed for using with the
 * {@linkcode Router}.
 *
 * > [!IMPORTANT]
 * > This is not intended for advanced use cases that are supported by oak,
 * > like integrated cookie management, web sockets and server sent events.
 * >
 * > Also, these are designed to be very deterministic request/response handlers
 * > versus a more nuanced middleware stack which allows advanced control.
 * > Therefore there is no `next()`.
 * >
 * > For these advanced use cases, create middleware without the wrapper.
 *
 * @module
 */

此模块级 JSDoc 注释将如此出现在 JSR 的顶层

Markdown in JSDoc example

有时,您的文档会引用包中的另一个符号。为了方便用户在整个文档中导航,您可以使用 @link@linkcode@linkplain 标签在文档内部链接。这些标签接受名称路径或 URL,从中生成 HTML 锚元素。这是一个例子

/** Options to use when styling text with the {@linkcode print} function. */
export interface StyleOptions {
  /** The color to print the message in. */
  color: "black" | "red" | "green";
  /** Whether to print the message in bold. */
  bold: boolean;
  /** Whether to print the message in italic. */
  italic: boolean;
}

/**
 * A function that prints a message to the terminal with the given options.
 *
 * Note that on some versions of Windows, {@linkcode StyleOptions.color} may not
 * be supported in combination with {@linkcode StyleOptions.bold}.
 */
declare function print(message: string, options: StyleOptions): void;

在 VSCode 中,悬停工具提示现在包含可点击的链接,这些链接将直接带您到定义该符号的代码位置

Example of linkcode in VSCode hover tooltip

这是一个 @linkcode 在 JSR 中如何显示的示例。在 Oak 的 serve 函数的 JSDoc 中,它引用了 Application,这在 JSR 上变成了一个可点击的链接

Example of linkcode in JSR

您还可以引用内置的 JavaScript 对象,例如 ArrayBuffer,JSR 将自动链接到 相关的 MDN 文档

保持 JSDoc 与代码更改同步更新

使用 JSDoc 的一个好处是在注释中编写文档与编写代码同时进行。这意味着每当我们需要更改函数、接口或模块时,我们都可以以最小的上下文切换成本对 JSDoc 进行必要的更改。

但是,如何确保注释中的文档已更新?从文档开始,类似于文档驱动开发,可以帮助您在编写单行代码之前详细说明和推断需求。有时,这意味着更早地发现潜在问题并节省重新编写代码的时间。(有关更宏观的方法,请查看 “Readme 驱动开发”。)

如果您在文档中包含了代码示例,则可以使用 deno test --doc 从命令行对它们进行类型检查。这是一个有用的工具,可确保文档中的示例是最新的并且可以正常工作。

例如,在我们之前使用的 sum 函数的基础上,让我们添加一个带有代码片段的示例

/**
 * Adds two values and returns the 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,它将处理基于您的 JSDoc 风格注释的所有格式化和文档生成。但是,如果您有兴趣使用您的工具来审核或测试 JSDoc 注释输出的外观,这里有一些建议。

  • deno doc <file>:此 Deno 命令将打印每个file的导出成员的 JSDoc 文档。此命令还接受一个 --html 标志,该标志生成一个带有文档的静态站点,以及一个 --json 标志,用于生成您可以自己显示的 JSON 输出。
  • deno doc --lint`: 此命令将检查问题,例如缺少返回类型或公共类型缺少 JSDoc 注释。这些 lint 可以帮助您编写更好的文档,并在发布之前发现潜在问题。
  • deno test --doc`: 我们在本篇文章前面提到了此命令,但这使您可以轻松地类型检查文档示例。
  • jsdoc <directory>: JSDoc 自己的 CLI 可以使用其默认模板生成静态文档站点,并提供各种配置选项标志。如果默认模板有点无聊,还有其他模板,例如 docdash,它提供分层导航和语法突出显示。

下一步是什么?

为您的 JavaScript 包编写良好的 JSDoc 对其成功至关重要。让我们回顾一下最佳实践

  • 编写简洁的摘要:您的 JSDoc 注释的第一段应该是符号的简洁描述,以帮助用户快速了解其作用。
  • 提供良好的类型信息:类型信息可以帮助用户快速过滤函数列表并找到他们需要的函数。
  • 使用标签:诸如 @param@returns@typeParam 之类的标签提供了有关函数或类的特定部分的更多信息。
  • 添加示例:示例可以帮助用户快速了解如何使用您的库。
  • 记录所有内容:记录您在包中公开的每个符号,包括模块(如果您公开了多个模块)。
  • 内部链接:使用 @link@linkcode@linkplain 链接到文档的其他部分,以帮助用户导航您的文档。
  • 测试您的文档:在发布之前使用 deno test --doc 对文档示例进行类型检查,并使用 deno doc --lint 检查 JSDoc 注释中的问题。

通过遵循这些最佳实践,您可以为您的包创建全面的文档,帮助用户尽快上手使用您的包。

🚨️ 了解更多关于 JSR 的信息 🚨️