Deno Style Guide[Node.js、Deno 之父教你写 TypeScript][官文全文翻译]

首发知乎:https://zhuanlan.zhihu.com/p/437877982
链接:Deno - A modern runtime for JavaScript and TypeScript

作者:Deno 团队

标题少说了其他人,其实这篇文章是 Deno 团队的脑力精华。

导读:全网有很多 TypeScript 的风格教程,但是都很冗长,涉及面广,也不好应用。这篇文章是我在学习 Deno 的时候看到了,这篇文章很短,英文只有 360 行,但是基本是最重要的一些问题。因为 Deno Style Guide 实际不是给初学者,而是为了给 Deno 标准库的作者准备的,与其说是面面俱到的风格约束,不如说是,往 Deno 标准库里贡献的标准。

版权页眉(Copyright Headers)

在标准仓库中的模块应该有以下的版权页眉:

// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

如果代码起源于别的地方,确保文件有合适的版权页眉。我们只允许 MIT,BSD 和 Apache 协议的代码。

使用下划线作为文件名,而不使用中划线(Use underscores, not dashes in filenames.)

例子: 使用file_server.ts而不是file-server.ts.

给新功能加测试(Add tests for new features.)

每一个模块的对外功能都应该包含测试。

TODO 注释(TODO Comments)

TODO 注释应该用括号标注 issue 编号或者作者的 github 用户名。

// TODO(ry): Add tests.
// TODO(#123): Support Windows.
// FIXME(#349): Sometimes panics.

不鼓励元编程,包括 Proxy(Meta-programming is discouraged. Including the use of Proxy.)

清晰且易于理解是最重要的,哪怕会需要更多的代码。

有一些场景,可能必须要用元编程,但是绝大部分情况,是可以避免的。

Rust

遵守 Rust 的惯例,与现存代码风格一致。

TypeScript

TypeScript 主要是标准库 std 。

使用 TypeScript 代替 JavaScript(Use TypeScript instead of JavaScript)

使用术语 “module” 而不是 “library” 或者 “package”(Use the term "module" instead of "library" or "package".)

为了清晰和一致,避免词汇 “library” 和 “package”。使用 “module” 来描述一个 JS 或者 TS 文件,也只带一个 TS/JS 的代码目录。

不要使用文件名 index.ts/index.js(Do not use the filename index.ts/index.js.)

不要把 “index.js” 和 “index.ts” 弄得太特殊了。使用这些名字,似乎暗示了这些文件可以移出模块,但是实际不能。这会造成误解。

如果一个文件夹的代码需要一个默认的进入点(entry point),使用文件名mod.ts。文件名mod.ts遵守了 Rust 的习惯,比 index.ts 要短。并且这个名字没有暗示这个文件的作用。

导出函数:最多两个参数,把其他参数放入 options object(最多 3 个参数)(Exported functions: max 2 args, put the rest into an options object.)

当设计函数接口时,遵守以下原则

  1. 公开的函数 API 的参数,使用 0-2 个可选或者必选的参数,以及一个 options object。(所以,最多 3个参数)
  2. 所有的可选参数都应该放入 options object。
  3. 如果只有一个可选参数,也可以接受不放入 options object,单独作为一个参数。但是这么做,对于以后的扩展是不方便的。(言外之意,还是建议不这么做,因为潜在的,改这些公共库的接口是影响很大的事情。)
  4. option 参数是一个普通的 Object
  5. 其他参数也可以是 objects,但是他们应该和 ‘plain’ Object 区分开
  6. 一个可区分的原型(例如 Array,Map,Date,class MyThing)
  7. 一个大家都知道的 symbol(例如 Symbol.iterator)
  8. 这样可以让 API 在后续进化中保持前向兼容,即便 options object 的位置发生变化。
// BAD: 参数不是 options object 的一部分. (#2)
export function resolve(
  hostname: string,
  family?: "ipv4" | "ipv6",
  timeout?: number,
): IPAddress[] {}

// GOOD.
export interface ResolveOptions {
  family?: "ipv4" | "ipv6";
  timeout?: number;
}
export function resolve(
  hostname: string,
  options: ResolveOptions = {},
): IPAddress[] {}
export interface Environment {
  [key: string]: string;
}

// BAD: `env` 可能是一个普通的 Object 这样就
// 不能和 option object 区分开了。 (#3)
export function runShellWithEnv(cmdline: string, env: Environment): string {}

// GOOD.
export interface RunShellOptions {
  env: Environment;
}
export function runShellWithEnv(
  cmdline: string,
  options: RunShellOptions,
): string {}
// BAD: 超过了 3 个参数 (#1),多个可选参数 (#2).
export function renameSync(
  oldname: string,
  newname: string,
  replaceExisting?: boolean,
  followLinks?: boolean,
) {}

// GOOD.
interface RenameOptions {
  replaceExisting?: boolean;
  followLinks?: boolean;
}
export function renameSync(
  oldname: string,
  newname: string,
  options: RenameOptions = {},
) {}
// BAD: 太多参数了 (#1)
export function pwrite(
  fd: number,
  buffer: TypedArray,
  offset: number,
  length: number,
  position: number,
) {}

// BETTER.
export interface PWrite {
  fd: number;
  buffer: TypedArray;
  offset: number;
  length: number;
  position: number;
}
export function pwrite(options: PWrite) {}

所有的导出函数使用的 interface 也都应该导出(Export all interfaces that are used as parameters to an exported member)

当你导出一个函数的时候,这个函数使用了一些 interface,这些 interface 也应该导出。(使用者需要这些 interface!)

// my_file.ts
export interface Person {
  name: string;
  age: number;
}

export function createPerson(name: string, age: number): Person {
  return { name, age };
}

// mod.ts
export { createPerson } from "./my_file.ts";
export type { Person } from "./my_file.ts";

最小化依赖;不要循环引用(Minimize dependencies; do not make circular imports.)

std 没有外部依赖,我们必须要小心处理内部依赖,保持其简单和可管理。注意不要引入循环依赖。

如果一个文件以下划线开头: _foo.ts,不要链接它们 (If a filename starts with an underscore: _foo.ts, do not link to it.)

一些时候,一些导出的 API 需要一些功能,但是这些功能还不足够稳定对外提供。这些时候把这些模块加一个下划线前缀,作为内部文件。按照惯例,只有这个文件夹内部的文件可以导入这些文件。

(如果你在另一个文件想使用这个文件,请复制一份。)

使用 JSDoc 描述导出的符号(Use JSDoc for exported symbols.)

我们渴求完整的文档。每一个导出的符号都应该有一个文档内容。

如果可以,使用一行 JSDoc

/** foo does bar. */
export function foo() {
  // ...
}

文档要易于人类阅读,也要加一些必要的风格信息来保证自动生成的文档是富文本。所以书写 JSDoc 的时候,要符合 markdown 的规范。

虽然 markdown 支持 HTML 标签,但是我们禁止在 JSDoc 内部写这些标签。

代码提及的内容要加反引号(`)而不是引号。

/** Import something from the `deno` module. */

不要标记函数的参数,除非这个参数的意图还不足够明显(尽管这些参数意图不明显,API 还得考虑这些参数。)所以 @param 标签一般情况是禁止使用的。但是如果你必须要使用 @para ,不要加任何 Type,因为 TypeScript 已经做了更强的 type 信息。

/**
 * Function with non obvious param.
 * @param foo Description of non obvious parameter.
 */ 

能压缩垂直注释时,要压缩。一行注释应该写为:

/** This is a good single line JSDoc. */

而不是

/**
 * This is a bad single line JSDoc.
 */

代码示例应该加上 markdown 的格式:

/** A straight forward comment and an example:
 * ```ts
 * import { foo } from "deno";
 * foo("bar");
 * ```
 */

代码示例不要加额外的注释,前面已经加了注释了。

使用指令处理 linting 问题(Resolve linting problems using directives)

在 building 阶段处理 linting 问题使用 dlint。如果代码需要让 linter 忽略一些东西,使用 deno-lint-ignore 来提示 linter。

// deno-lint-ignore no-explicit-any
let x: any; 

这样确保持续集成不会因为 linting 问题导致中断。但是使用时应该格外小心。

每一个模块,都应该有一个测试模块对应(Each module should come with a test module.)

每一个模块 foo.ts 都应该有一个测试模块 foo_test.ts。std 模块的测试模块应该在 std/tests 里,否则就应该在自己的测试模块里。

单元测试应该清晰(Unit Tests should be explicit.)

为了更好的让别人理解测试,函数名的命名要清晰。这些名称会弹出在命令行中。

test myTestFunction ... ok

测试的例子

import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { foo } from "./mod.ts";

Deno.test("myTestFunction", function () {
  assertEquals(foo(), { bar: "bar" });
});

顶级导出函数不应该用箭头函数(Top level functions should not use arrow syntax.)

顶级导出函数应该用 function 关键字。箭头函数应该限制在闭包中。

不好的例子:

export const foo = (): string => {
  return "bar";
};

好的例子:

export function foo(): string {
  return "bar";
}

std

不要依赖外部代码(Do not depend on external code.)

https://deno.land/std/ 的目标是所有 Deno 程序的基础函数依赖。我们要保证用户用的这些代码不会包含没有审查的第三方代码。

文档和保持浏览器兼容性(Document and maintain browser compatibility.)

如果一个模块是浏览器兼容的,在模块的最上面应该添加以下注释:

// This module is browser compatible. 

保持浏览器兼容要保证不使用 Deno 名字空间下的东西和特性。应该保证所有的依赖都是浏览器兼容的。

全文完

最后

这篇文章非常短,但是基本指出了一个希望长久给别人使用,且持续进化,且多人参与的项目应该注意的点。这些点的实践是不难的,收益是很大的。

长远看,有价值的东西都会变成网上的一个库,所以建议新手开始写第一个库就注意这些事情。

转载请注明本文知乎链接和译者 Hugo。

你可能感兴趣的:(Deno Style Guide[Node.js、Deno 之父教你写 TypeScript][官文全文翻译])