跳至主要内容

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,IDE 已经在开发时显示类型检查的结果。所有使用 deno lsp 的用户都可以开箱即用地获得这种体验。这意味着当他们运行新开发的代码时,即使类型检查实际上没有用,他们也必须等待很长时间才能执行类型检查,因为他们已经可以在 IDE 中看到所有诊断信息。

此外,随着 JavaScript 即将原生支持类型注解,浏览器在遇到类型注释时将使用的语义将与 Deno 不同。它们不会在运行代码之前进行类型检查,而是将该步骤留给开发人员在发布代码之前运行的单独的类型检查步骤。

根据这一点,我们已决定开始让 Deno 默认在 deno run 中禁用类型检查。类型检查需要使用新的 deno check 子命令显式执行。因为我们知道此时此更改具有相当大的侵入性,所以我们将缓慢进行。我们暂定的时间表计划如下:

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

deno run默认禁用类型检查并不意味着我们正在从 Deno 中删除 TypeScript 支持。TypeScript 仍然是 Deno 中的一等公民语言,我们将继续鼓励用户在其项目中使用 TypeScript。 我们所做的唯一更改是,用户现在必须显式指定何时要执行类型检查:通过运行 deno check,或者通过在 deno run 上指定 --check 选项。

新的 deno check 子命令也具有与 deno run 中现有类型检查略有不同的默认类型检查语义:它仅报告当前项目的诊断信息(如 deno lintdeno fmt),而不报告任何远程依赖项的诊断信息。此行为已通过 --no-check=remote 标志在 deno run 中可用。仍然可以通过运行 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.jsondeno.jsonc 文件,Deno 的 VSCode 扩展现在会提示您在工作区中启用它。

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

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

此功能添加了期待已久的功能,即通过使用Deno: 启用路径"deno.enablePaths" 设置,仅在配置的工作区的某些部分启用 Deno 扩展。

例如,如果您有如下项目:

project
├── worker
└── front_end

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

测试 API 集成

vscode_deno 现在提供与 VSCode 测试 API 的集成,从而可以在 UI 中运行 Deno 测试

请务必更新到最新版本的 Deno VSCode 扩展 以使用这些功能。

REPL 的改进

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

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

$ deno repl --eval-file=https://deno.land/std@0.136.0/encoding/ascii85.ts
Download https://deno.land/std@0.136.0/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 模块,并且它们都共享相同的全局作用域。这样做的后果之一是,评估的文件可能需要在 import 语句中使用绝对说明符,因为相对说明符将相对于 REPL 的当前工作目录解析。

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

感谢 Naju Mancheril 贡献此功能!

此外,REPL 现在具有可用的全局 clear() 函数,该函数充当 console.clear() 的别名。这与许多浏览器的 REPL 中发现的功能一致。

DENO_NO_PROMPT 环境变量

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

不稳定 API 的改进

Unix 套接字支持 Deno.upgradeHttp

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

.unref() 用于 Deno.Listener

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

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

增量格式化和 lint

deno fmtdeno lint 现在使用幕后缓存,以便跳过它已经知道在先前运行中已格式化或 lint 的文件。这些子命令已经非常快,但是通过此更改,在您至少对某些文件运行过一次之后,您应该会看到明显的性能改进,尤其是在使用并行性较差的慢速计算机时。

例如,以下是在此更改之前,deno fmtdeno lint 在具有 4 个内核 (2.60GHz) 的计算机上运行 deno_std 的 repo 所花费的时间

$ 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 现在已删除 - 相反,基准测试工具将重复运行基准测试用例,直到后续运行之间的时间差在统计上不显着为止(这类似于 Golang 的方法)。

添加了 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

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

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

  • 添加了一个新的 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

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

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

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 属于这一类)(或 describe-and-it 函数风格)。

虽然出于简洁性考虑,我们为默认测试 API (Deno.test()) 选择了 test 函数风格,但社区一直需要 BDD 风格的语法。由于这种需求,我们决定在此版本中添加对这种风格的支持,该功能现在已在 std/testing/bdd.ts 中可用。

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

import {
  assertEquals,
  assertStrictEquals,
  assertThrows,
} from "https://deno.land/std@0.136.0/testing/asserts.ts";
import {
  afterEach,
  beforeEach,
  describe,
  it,
} from "https://deno.land/std@0.136.0/testing/bdd.ts";
import { User } from "https://deno.land/std@0.136.0/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 等)并且你无法控制该外部系统时,测试此类软件通常会变得非常困难。解决这种情况的一种方法是使用模拟对象 (Mock object),它模拟外部系统的行为。我们现在通过我们的模拟工具支持这种测试方式。

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

spy 是一种对象类型,它记录与它的每次交互,你可以在稍后断言发生了交互。以下示例说明了基本用法。

import {
  assertSpyCall,
  assertSpyCalls,
  spy,
} from "https://deno.land/std@0.136.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/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/std@0.136.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/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/std@0.136.0/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/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/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/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/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 贡献了此功能!