跳到主要内容
Deno 2 终于来了 🎉️
了解更多
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;

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

如果有多个值得一提的用例,您甚至可以提供多个示例。这是一个示例,说明了多个示例在 JSR 上如何显示,来自 `deno_std/fs` 中的 `move` 函数

/**
 * (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

如果您的包包含多个模块,则在每个模块文件的顶部添加一个 JSDoc 注释,并使用 `@module` 标签会很有用。此模块注释应包含描述和使用其导出符号的示例。

这是一个在 Oak 的 `application.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 功能包括

  • # my heading 用于章节标题
  • - hello world 用于项目符号
  • **important** 用于粗体
  • _noteworthy_ 用于斜体
  • > quote 用于块引用
  • [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

以下是如何在 JSR 中显示 @linkcode 的示例。在 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 的更多信息 🚨️