跳至主要内容
Deno 2 终于来了 🎉️
了解更多

Deno 1.21 已经发布,并包含以下新功能和更改

如果您已经安装了 Deno,可以通过运行以下命令升级到 1.21

deno upgrade

如果您是第一次安装 Deno,可以使用以下方法之一

# Using Shell (macOS and Linux):
curl -fsSL https://deno.land/x/install/install.sh | sh

# Using PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex

# Using Homebrew (macOS):
brew install deno

# Using Scoop (Windows):
scoop install deno

# Using Chocolatey (Windows):
choco install deno

deno check 和默认不进行类型检查的路径

过去几年表明,能够使用类型信息(通常以 TypeScript 的形式)来注释 JavaScript 代码是多么有用。 这具有两个主要好处

  • 类型信息有助于自动生成文档,既可以是 IDE 提示/补全的形式,也可以是像 https://doc.deno.land 上的静态文档页面的形式。
  • 类型信息可用于验证给定代码段在语义上是否正确(类型检查)。

这两个好处都非常有用,但在不同的时间有用。 前者是一种始终在开发过程中有用的环境效益。 后者是一种更复杂的好处,只有在您要发布某些代码之前才有用。 类型检查器实际上只是一个非常非常强大的 linter,可以帮助您在运行代码之前发现代码中的问题。

到目前为止,deno run 始终在运行代码之前自动对要运行的代码进行类型检查。 这有时可能是一种不错的用户体验,但更多时候,它并不是您想要的。 这样做的原因通常是类型检查非常慢:它通常是影响应用程序启动性能的最大因素。

问题是,大多数开发人员使用 IDE 在开发时已经显示了类型检查的结果。 所有使用 deno lsp 的用户都可以在开箱即用状态下获得这种体验。 这意味着,当他们运行新开发的代码时,他们必须等待很长时间才能执行类型检查,即使它实际上没有用,因为他们已经在 IDE 中看到了所有诊断信息。

此外,随着 JavaScript 正在走向原生获取类型注释的道路,浏览器在遇到类型注释时使用的语义将与 Deno 不同。 它们不会在运行某些代码之前进行类型检查,而是将此步骤留给开发者在发布代码之前执行的单独的类型检查步骤。

为此,我们已经做出决定,让 Deno 开始在 deno run 中默认禁用类型检查的路径。 类型检查需要使用新的 deno check 子命令显式执行。 由于我们知道,这种更改在目前来说是相当侵入性的,因此我们将慢慢进行。 我们对更改时间线的初步计划如下

  • 此版本添加了新的 deno check 子命令,以及一个可以设置为将 Deno 切换到“新的”默认不进行类型检查模式的 DENO_FUTURE_CHECK=1 环境变量,该模式将来将成为默认模式。
  • 我们将花费几个版本进行宣传,并告知用户有关更改的信息。
  • 在未来几个月内,我们将在即将发布的版本中将 deno run 上的默认类型检查禁用。

deno run默认禁用类型检查并不意味着我们从 Deno 中删除了 TypeScript 支持。 TypeScript 仍然是 Deno 中的一流语言,我们将继续鼓励用户在他们的项目中使用 TypeScript。 我们正在进行的唯一更改是,用户现在必须明确指定他们何时想要执行类型检查:要么通过运行 deno check,要么通过在 deno run 上指定 --check 选项。

新的 deno check 子命令与 deno run 中现有的类型检查相比,默认的类型检查语义略有不同:它只报告当前项目的诊断信息(如 deno lintdeno fmt),而不是任何远程依赖项的诊断信息。 此行为已经可以通过 deno run 上的 --no-check=remote 标志获得。 通过运行 deno check --remote 仍然可以对整个模块图进行类型检查。

我们知道,这种更改可能让某些用户感到意外,但实际上,我们一直希望进行这种更改,已经将近一年了。 我们认为,这种更改显着改善了 Deno 用户的开发体验,并且我们认为,我们可以更好地与整个 JavaScript 社区在过去所做的事情以及将来会做的事情保持一致。

如果您对这种更改或计划的时间线有任何反馈,请加入 我们的 Discord 服务器 进行讨论。

globalThis.reportError"error" 事件

此版本使 Deno 的错误处理行为与浏览器的错误处理行为保持一致,对于异步事件循环任务(如 setTimeoutsetInterval 或事件处理程序)中的未捕获异常,Deno 现在有一个全局的 "error" 事件,该事件将为上述 API 中的任何未捕获异常进行分派。 用户可以 event.preventDefault() 此事件以阻止运行时退出并返回非 0 状态代码,就像它通常在未捕获异常时那样。

此外,还添加了 Web 标准 globalThis.reportError,以使用户能够像异步事件循环任务中的未捕获异常那样报告错误。 globalThis.reportError(error)setTimeout(() => { throw error }, 0) 不同,前者同步执行异常报告步骤,而后者在将来的事件循环刻度(异步)中执行。

以下是如何使用新功能的示例

// This code will cause Deno to print
// the exception to stderr and will exit with a non 0 status code.
reportError(new Error("something went wrong!"));
// But if a user handles the "error" event, they can prevent termination and log
// the error themselves:
window.onerror = (e) => {
  e.preventDefault();
  console.error("We have trapped an uncaught exception:", e);
};

reportError(new Error("something went wrong!"));

感谢 Nayeem Rahman 为此功能做出贡献!

Deno 语言服务器和 VSCode 扩展的改进

配置文件自动发现和 deno task 集成

Deno 的 VSCode 扩展现在将在发现 deno.jsondeno.jsonc 文件时提示您在工作区中启用它。

此外,在配置文件 tasks 部分中定义的任务 将在命令面板中可用

在工作区子路径中启用扩展

此功能添加了长期期待的功能,可以通过使用Deno: 启用路径"deno.enablePaths" 设置来仅在配置的工作区的一部分中启用 Deno 扩展。

例如,如果您有这样的项目

project
├── worker
└── front_end

您只想启用 worker 路径(及其子路径)作为 Deno 启用的路径,您需要将 ./worker 添加到配置中的Deno: 启用路径列表中。

测试 API 集成

vscode_deno 现在与 VSCode 的 Testing API 集成,使您能够从 UI 运行 Deno 测试。

请确保更新到最新版本的 Deno VSCode 扩展,以便使用这些功能。

REPL 的改进

REPL 是用于快速原型设计和尝试新事物的工具,它几乎没有用于执行类型检查,尤其是对于导入的第三方代码。为此,我们决定在 REPL 中禁用导入模块的类型检查,从而导致更快的导入。

此版本中的一个新功能是 --eval-file 标志,它可以与 deno repl 子命令一起使用。此标志允许您传递要执行的文件的路径或 URL 列表,这些文件将在 REPL 启动之前执行。此功能对于创建自定义的专门 REPL 很有用。

$ deno repl --eval-file=https://deno.land/[email protected]/encoding/ascii85.ts
Download https://deno.land/[email protected]/encoding/ascii85.ts
Deno 1.21.0
exit using ctrl+d or close()
> rfc1924 // local (not exported) variable defined in ascii85.ts
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"

请记住,通过 --eval-file 标志提供​​的文件在与 REPL 本身相同的范围内执行。这意味着所有文件都作为“普通脚本”(而不是 ES 模块)执行,并且它们都共享相同的全局范围。这样做的一个结果是,评估的文件可能需要在导入语句中使用绝对说明符,因为相对说明符将相对于 REPL 的当前工作目录进行解析。

有关更多详细信息,请参阅 手册条目

感谢 Naju Mancheril 为此功能做出贡献!

此外,REPL 现在提供了一个全局 clear() 函数,它充当 console.clear() 的别名。这与许多浏览器中 REPL 中找到的内容一致。

DENO_NO_PROMPT 环境变量

此版本添加了一个新的 DENO_NO_PROMPT 环境变量。当它被设置时,deno 将禁用所有交互式提示,即使输出是交互式终端。它与在所有对 deno 二进制文件的调用中指定 --no-prompt 具有相同的效果。

不稳定 API 的改进

Deno.upgradeHttp 的 Unix 套接字支持

可以用于执行 HTTP 协议切换的不稳定 Deno.upgradeHttp API 现在支持在运行在 Unix 连接之上的 HTTP 服务器上执行协议切换。

Deno.Listener.unref()

Deno.Listener API 现在具有新的 .ref().unref() 方法,可以调用它们来启用或禁用此侦听器上的操作,以阻止事件循环。例如,如果用户通过调用 Deno.listen() 创建一个新的侦听器,然后在侦听器上调用 .accept(),则该进程将不会退出,直到接受连接。

如果用户在创建后在侦听器上调用 .unref(),则 .accept() 任务将不再阻止进程退出。事件循环将在所有其他异步任务完成后完成,忽略侦听器 .accept() 任务的完成状态。

增量格式化和代码整理

deno fmtdeno lint 现在使用后台缓存,以便跳过它已经知道从先前运行中已格式化或代码整理过的文件。这些子命令本身已经非常快,但通过这种更改,您应该在至少对某些文件运行它们一次后看到明显的性能改进,尤其是在使用并行性较低的慢速计算机上工作时。

例如,以下是 deno fmtdeno lint 在一台拥有 4 个核心(2.60GHz)的计算机上运行 deno_std 的存储库之前的时间长度。

$ time deno fmt
Checked 927 files
1.796s
$ time deno fmt
Checked 927 files
1.799s
$ time deno lint
Checked 872 files
2.201s
$ time deno lint
Checked 872 files
2.209

现在,在之后。

$ time deno fmt
Checked 927 files
1.764s
$ time deno fmt
Checked 927 files
0.114s
$ time deno lint
Checked 872 files
2.292s
$ time deno lint
Checked 872 files
0.106s

请注意,在每个命令的第一次运行后,时间会显著减少。

此外,从本版本开始,deno fmt 将自动跳过格式化 .git 目录内的文件。

deno bench 的改进

在 Deno v1.20 中,我们引入了一个新的 deno bench 子命令,它允许快速注册和运行代码片段以评估其性能。此子命令是根据 deno test 建模的,并且具有类似的输出格式。

我们从社区收到了关于此功能的良好反馈。两个抱怨尤其突出

  • 在大多数情况下,默认迭代次数太低
  • 报告格式的信息太少

鉴于我们标记了 Deno.bench() 作为不稳定 API,我们借此机会解决这两个抱怨,并为 Deno.bench() API 添加更多功能。

已删除指定每个案例应运行多少次的 Deno.BenchDefinition.nDeno.BenchDefinition.warmup - 相反,基准测试工具将反复运行基准案例,直到后续运行之间的时间差在统计上不显著(这类似于 Golangs 的方法)。

添加了 Deno.BenchDefinition.groupDeno.BenchDefinition.baseline,这些字段允许您整齐地对相关的基准案例进行分组,并将其中一个标记为其他案例的比较基础。

// This is the baseline case. All other cases belonging to the same "url"
// group will be compared against it.
Deno.bench({ name: "Parse URL", group: "url", baseline: true }, () => {
  new URL(import.meta.url);
});

Deno.bench({ name: "Resolve URL", group: "url" }, () => {
  new URL("./foo.js", import.meta.url);
});

最后,对基准报告进行了重新设计,以包含更多有用的信息,包括来自同一组的案例之间的比较。

`deno bench` report

感谢 @evanwashere 为此功能做出贡献。

子进程的新不稳定 API

Deno 1.21 为 Deno 命名空间添加了新的不稳定子进程 API,它包含了我们从社区收到的关于(即将弃用的)Deno.run 子进程 API 的大部分反馈。

高级 API

已添加了一个新的易于使用的 API 来生成子进程并在单个调用中收集其输出。Deno.spawn API 接受类似于 Deno.run 接受的选项包,但它不会返回 Deno.Process 对象,而是返回一个解析为 Deno.SpawnOutputPromise。它包含进程的退出状态,以及包含进程输出到 stdoutstderr 的字节的 Uint8Array

const { status, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

低级 API

还添加了一个新的低级 API(Deno.spawnChild)。它的工作原理与 Deno.run 非常相似,但有一些显着差异

  • 所有 stdio 流现在都是 ReadableStream / WritableStream,而不是 Deno.Reader / Deno.Writer

  • 现在,您不再需要调用 proc.status() 来获得解析为进程退出状态的承诺,而是可以从 child.status getter 获取状态承诺。

  • 子进程不再需要使用.close()手动关闭,就像在Deno.run中一样。

  • 添加了一个新的child.output() API,它具有与Deno.spawn()相同的返回值。

const child = Deno.spawnChild(Deno.execPath(), {
  args: [
    "eval",
    "console.log('Hello World')",
  ],
  stdin: "piped",
});

// open a file and pipe the subprocess output to it.
child.stdout.pipeTo(Deno.openSync("output").writable);

// manually close stdin
child.stdin.close();
const status = await child.status;

同步子进程执行

同步子进程执行是 Deno 之前无法实现的新功能。这个新的Deno.spawnSync API 的签名与Deno.spawn几乎相同。唯一的区别是返回值类型是SpawnOutput而不是Promise<SpawnOutput>

const { status, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

这个新 API 还不稳定。我们鼓励您尝试一下,并报告您遇到的任何问题。

deno test的改进

此版本为 Deno 的内置测试功能带来了许多改进和新功能。

用户代码输出格式

之前,deno test 并没有真正意识到用户代码可能产生的任何输出。这种情况经常导致测试运行器的报告与来自您代码的控制台日志交织在一起。在这个版本中,我们重构了测试运行器中很多底层代码,这使我们能够注意来自您代码的任何输出,无论是来自console API 方法,还是直接写入Deno.stdoutDeno.stderr

从这个版本开始,如果您的代码中有输出,它将被整齐地包含在标记之间,以使其与测试运行器的报告区分开来。

`deno test` output

我们还考虑默认情况下捕获此输出,并且仅在测试失败时才显示它。我们很乐意听取您的反馈,请告知我们在问题中

更具信息量的错误和堆栈跟踪

deno test 始终为抛出的错误提供完整的堆栈跟踪,但是如果您的测试文件没有很深的调用堆栈,则这样做不可取。结果是您会看到许多来自 Deno 内部代码的堆栈帧,这些堆栈帧无助于您查明问题所在。

// test.ts
Deno.test("error in a test", () => {
  throw new Error("boom!");
});
$ deno test test.ts
Check file:///dev/deno/test.ts
running 1 test from file:///dev/deno/test.ts
test error in a test ... FAILED (3ms)

failures:

error in a test
Error: boom!
    at file:///dev/deno/test.ts:2:9
    at testStepSanitizer (deno:runtime/js/40_testing.js:444:13)
    at asyncOpSanitizer (deno:runtime/js/40_testing.js:145:15)
    at resourceSanitizer (deno:runtime/js/40_testing.js:370:13)
    at Object.exitSanitizer [as fn] (deno:runtime/js/40_testing.js:427:15)
    at runTest (deno:runtime/js/40_testing.js:788:18)
    at Object.runTests (deno:runtime/js/40_testing.js:986:28)
    at [deno:cli/tools/test.rs:512:6]:1:21

failures:

    error in a test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (9ms)

error: Test failed

如上所示,错误包含 8 个堆栈跟踪,但只有顶部帧包含调试问题的有用信息。

从这个版本开始,deno test 将过滤掉来自 Deno 内部代码的堆栈帧,并显示错误源代码行。

$ deno test test.ts
Check file:///dev/deno/test.ts
running 1 test from ./test.ts
error in a test ... FAILED (5ms)

failures:

./test.ts > error in a test
Error: boom!
  throw new Error("boom!");
        ^
    at file:///dev/deno/test.ts:2:9

failures:

    ./test.ts
    error in a test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (15ms)

error: Test failed

感谢 Nayeem Rahman 为此功能做出贡献!

我们打算在接下来的版本中进一步改进测试报告器,以提供最佳的测试体验。

BDD 风格的测试

在这个版本中,我们为Deno 标准模块添加了一个 BDD(或行为驱动开发)风格的测试运行器。

在 JavaScript 测试生态系统中,组织测试用例主要有两种风格。一种是test 函数风格(taptapeava 属于此类),另一种是 BDD 风格(jasminemochajestvitest 属于此类)风格(或describeit 函数风格)。

虽然我们为默认的测试 API(Deno.test())选择了test 函数风格,因为它很简单,但社区一直需要 BDD 风格的语法。由于这个需求,我们决定在这个版本中添加对这种风格的支持,并且该功能现在可以在std/testing/bdd.ts中使用。

describeit 函数的基本用法如下所示。

import {
  assertEquals,
  assertStrictEquals,
  assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";
import {
  afterEach,
  beforeEach,
  describe,
  it,
} from "https://deno.land/[email protected]/testing/bdd.ts";
import { User } from "https://deno.land/[email protected]/testing/bdd_examples/user.ts";

describe("User", () => {
  it("constructor", () => {
    const user = new User("John");
    assertEquals(user.name, "John");
    assertStrictEquals(User.users.get("John"), user);
    User.users.clear();
  });

  describe("age", () => {
    let user: User;

    beforeEach(() => {
      user = new User("John");
    });

    afterEach(() => {
      User.users.clear();
    });

    it("getAge", function () {
      assertThrows(() => user.getAge(), Error, "Age unknown");
      user.age = 18;
      assertEquals(user.getAge(), 18);
    });

    it("setAge", function () {
      user.setAge(18);
      assertEquals(user.getAge(), 18);
    });
  });
});

describeit 函数在内部包装了Deno.test API,因此您可以像往常一样使用deno test 命令执行上述测试。

$ deno test bdd_example.ts
running 1 test from ./bdd.ts
User ...
  constructor ... ok (4ms)
  age ...
    getAge ... ok (3ms)
    setAge ... ok (3ms)
  ok (10ms)
ok (18ms)

test result: ok. 1 passed (4 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (40ms)

除了describeit 之外,我们目前支持 4 个钩子beforeAllafterAllbeforeEachafterEach,以及describe.onlyit.onlydescribe.ignoreit.ignore 简写。有关更多详细信息,请参见此文档

感谢Kyle June 为此功能做出的贡献!

模拟工具

在这个版本中,我们为Deno 标准模块添加了模拟工具。

当您使用外部依赖项(例如 Twitter 的 API、某些银行 API 等)构建软件,并且您无法控制该外部系统时,测试此类软件通常非常困难。解决此情况的一种方法是使用模拟对象,它模拟外部系统的行为。我们现在使用模拟工具支持这种测试方式。

我们添加了两种基本模拟类型:spystub,以及专用于这两种类型的断言函数。

spy 是一种记录与它交互的每项操作的对象,您可以稍后断言交互已发生。以下示例说明了基本知识。

import {
  assertSpyCall,
  assertSpyCalls,
  spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";

Deno.test("how spy works", () => {
  const func = spy();
  // The spy isn't called yet
  assertSpyCalls(func, 0);

  assertEquals(func(), undefined);
  // The spy was called with empty args
  assertSpyCall(func, 0, { args: [] });
  assertSpyCalls(func, 1);

  assertEquals(func("x"), undefined);
  // The spy was called with "x"
  assertSpyCall(func, 1, { args: ["x"] });
  assertSpyCalls(func, 2);
});

stub 是模拟某些预定义行为的对象类型。以下示例说明了基本知识。

import {
  returnsNext,
  stub,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";

Deno.test("how stub works", () => {
  // Replace Math.random with stub
  // now it returns 0.1 for the 1st call, 0.2 for the 2nd, 0.3 for the 3rd.
  const mathStub = stub(Math, "random", returnsNext([0.1, 0.2, 0.3]));
  try {
    assertEquals(Math.random(), 0.1);
    assertEquals(Math.random(), 0.2);
    assertEquals(Math.random(), 0.3);
  } finally {
    // You need to call .restore() for restoring the original
    // behavior of `Math.random`
    mathStub.restore();
  }
});

我们在这里不列出任何真实的示例,因为它们需要很多空间,但请参见文档 以获取更多详细信息,以及示例

感谢Kyle June 为此功能做出的贡献!

快照测试

在这个版本中,我们在Deno 标准模块中添加了快照测试工具。

快照测试是用于测试产生复杂输出的软件的强大工具,例如生成的语言、AST、命令行输出等。

您可以从testing/snapshot.ts 中导入assertSnapshot 工具,它负责创建、更新和验证快照。

import { assertSnapshot } from "https://deno.land/[email protected]/testing/snapshot.ts";

Deno.test("The generated output matches the snapshot", async (t) => {
  const output = generateComplexStuff();
  await assertSnapshot(t, output);
});

上面的调用断言输出与保存的快照匹配。当您还没有快照或想要更新快照时,您可以通过使用--update 选项调用deno test 命令来更新它。

$ deno test --allow-read --allow-write -- --update
running 1 test from ./foo/test.ts
The generated output matches the snapshot ... ok (11ms)

 > 1 snapshots updated.

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (46ms)

请注意,您需要为测试进程提供--allow-read--allow-write 权限才能读取和写入快照文件。

感谢Yongwook ChoiBen Heidemann 为此功能做出的贡献!

FakeTime 测试工具

在这个版本中,我们在Deno 标准模块中添加了FakeTime 测试工具。

测试日期时间功能有时很困难,因为这些功能通常依赖于当前系统日期时间。在这个版本中,我们在std/testing/time.ts 中添加了FakeTime 工具,它允许您使用它模拟系统日期时间和计时器行为。

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

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    // now points to the exact same point of time
    assertEquals(Date.now(), 1640908800000);
  } finally {
    time.restore();
  }
});

FakeTime 工具还模拟计时器函数(如setTimeoutsetInterval 等)的行为。您可以使用.tick() 调用控制精确的经过时间。

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

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    let cnt = 0;
    // Starts the interval
    setInterval(() => cnt++, 1000);

    time.tick(500);
    assertEquals(cnt, 0);

    // Now 999ms after the start
    // the interval callback is still not called
    time.tick(499);
    assertEquals(cnt, 0);

    // Now 1000ms elapsed after the start
    time.tick(1);
    assertEquals(cnt, 1);

    // 3 sec later
    time.tick(3000);
    assertEquals(cnt, 4);

    // You can jump far into the future
    time.tick(997000);
    assertEquals(cnt, 1001);
  } finally {
    time.restore();
  }
});

请注意,您始终必须在测试用例结束时调用time.restore() 来停止模拟系统日期时间。

感谢Kyle June 为此功能做出的贡献!