介绍

deno-manual-cnGitHub stars

此版本更新于 2020 年 6 月 1 日 deno @ edeeedf

Deno 是一个 JavaScript/TypeScript 的运行时,默认使用安全环境执行代码,有着卓越的开发体验。

Deno 建立在 V8、Rust 和 Tokio 的基础上。

功能亮点

  • 默认安全。外部代码没有文件系统、网络、环境的访问权限,除非显式开启。

  • 支持开箱即用的 TypeScript 的环境。

  • 只分发一个独立的可执行文件 (deno)。

  • 有着内建的工具箱,比如一个依赖信息查看器 (deno info) 和一个代码格式化工具 (deno fmt)。

  • 有一组经过审计的 标准模块,保证能在 Deno 上工作。

  • 脚本代码能被打包为一个单独的 JavaScript 文件。

哲学

Deno 旨在为现代程序员提供高效、安全的脚本环境。

它将始终作为单个可执行文件分发,并且该可执行文件将能运行任何 Deno 程序。给定一个 Deno 程序的 URL,您应该能够用压缩后不超过 15 MB 的 Deno 可执行文件运行它。

Deno 明确地承担了运行时和包管理器的角色。它使用标准的浏览器兼容协议(URL)来加载模块。

对于过去用 bash 或 python 编写的工具脚本来说,Deno 是一个优秀的替代品。

目标

  • 只分发一个独立的可执行文件 (deno)。

  • 默认安全。外部代码没有文件系统、网络、环境的访问权限,除非显式开启。

  • 浏览器兼容:完全用 JavaScript 编写且不使用全局Deno命名空间(或功能测试)的程序是 Deno 程序的子集,应该能够直接在现代浏览器中运行而无需更改。

  • 提供内置工具来提升开发体验,比如单元测试、代码格式化、代码检查。

  • 不把 V8 的概念泄露到用户空间。

  • 能够高效地提供 HTTP 服务

与 Node.js 的比较

  • Deno 不使用 npm,而是使用 URL 或文件路径引用模块。

  • Deno 在模块解析算法中不使用 package.json

  • Deno 中的所有异步操作返回 promise,因此 Deno 提供与 Node 不同的 API。

  • Deno 需要显式指定文件、网络和环境权限。

  • 当未捕获的错误发生时,Deno 总是会异常退出。

  • 使用 ES 模块,不支持 require()。第三方模块通过 URL 导入。

    import * as log from "https://deno.land/std/log/mod.ts";
    

其他关键行为

  • 远程代码在第一次运行时获取并缓存,直到代码通过 --reload 选项运行。(所以它在飞机上也能工作)

  • 从远程 URL 加载的模块或文件应当是不可变且可缓存的。

图标

这些 Deno 图标在 MIT 协议下分发(公共领域,免费使用),例如 Deno 软件。

入门

在本章节,我们将讨论;

  • 安装 Deno
  • 设置您的环境
  • 运行一个 Hello World 脚本
  • 编写您自己的脚本
  • 理解权限控制
  • 在 Deno 中使用 TypeScript
  • 在 Deno 中使用 WebAssembly

安装

Deno 能够在 macOS、Linux 和 Windows 上运行。Deno 是一个单独的可执行文件,它没有额外的依赖。

下载安装

deno_install 提供了方便的脚本,用以下载安装 Deno.

使用 Shell (macOS 和 Linux):

curl -fsSL https://deno.land/x/install/install.sh | sh

使用 PowerShell (Windows):

iwr https://deno.land/x/install/install.ps1 -useb | iex

使用 Scoop (Windows):

scoop install deno

使用 Chocolatey (Windows):

choco install deno

使用 Homebrew (macOS):

brew install deno

使用 Cargo (Windows, macOS, Linux):

cargo install deno

Deno 也可以手动安装,只需从 github.com/denoland/deno/releases 下载一个 zip 文件。它仅包含一个单独的可执行文件。在 macOS 和 Linux 上,您需要为它设置执行权限。

测试安装

运行 deno --version,如果它打印出 Deno 版本,说明安装成功。

运行 deno help 以查看帮助文档。

运行 deno help <subcommand> 以查看子命令的选项。

升级

要升级已安装的版本,运行:

deno upgrade

这会从 github.com/denoland/deno/releases 获取最新的发布版本,然后解压并替换现有的版本。

您也可以用此来安装一个特定的版本:

deno upgrade --version 1.0.1

从源码构建

关于构建步骤的信息请查阅 贡献 章节。

设置您的环境

要高效地使用 Deno,您需要设置环境,比如命令行自动补全、环境变量、编辑器或 IDE。

环境变量

这是一些控制 Deno 行为的环境变量:

DENO_DIR 默认为 $HOME/.cache/deno,但可以设置为任何路径。这是 Deno 存放生成的代码和缓存的源码的路径。

如果 NO_COLOR 被设置,Deno 将会关闭彩色输出 (https://no-color.org/)。用户代码可以通过布尔常量 Deno.noColor 测试 NO_COLOR 是否被设置,这不需要环境权限 (--allow-env)。

命令行自动补全

通过 deno completions <shell> 命令可以生成补全脚本。它会输出到 stdout,您应该将它重定向到适当的文件。

Deno 支持的 shell 如下:

  • zsh
  • bash
  • fish
  • powershell
  • elvish

示例:

deno completions bash > /usr/local/etc/bash_completion.d/deno.bash
source /usr/local/etc/bash_completion.d/deno.bash

编辑器和 IDE

Deno 需要用文件后缀名来支持模块导入和 HTTP 导入。目前,大多数编辑器和语言服务器没有原生支持这点,一些编辑器可能会抛出“无法找到文件”的错误,或是“不必要的文件后缀名”错误。

社区已经开发了一些插件用来解决这些问题。

VS Code

目前内测版的 vscode_deno 扩展已经发布到了 Visual Studio Marketplace。如果遇到 bug 欢迎提 issues。

JetBrains IDE

JetBrains IDE 通过插件来提供 Deno 支持:Deno 插件

要了解有关设置步骤的更多信息,请在 YouTrack 上阅读 这个评论

Vim 和 NeoVim

如果您安装 CoC(intellisense engine and language server protocol),Vim 对于 Deno/TypeScript 来说非常友好 。当安装完 CoC 后,可以在 Vim 内部运行 :CocInstall coc-deno。你会发现,诸如 gd(转到定义)和 gr(转到/查找引用)之类的东西可以正常工作了。

Emacs

对于目标为 Deno 的 TypeScript 项目,Emacs 工作得很好,只需使用两个插件:

首先确保您已经安装了 tide,下一步,按照 typescript-deno-plugin 页面的指示,在项目中运行 npm install --save-dev typescript-deno-plugin typescript (npm init -y 是必要的),并在 tsconfig.json 中添加以下设置,然后准备开发吧!

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-deno-plugin",
        "enable": true, // default is `true`
        "importmap": "import_map.json"
      }
    ]
  }
}

如果您没有在列表中看到您最喜欢的 IDE,或许可以开发一个插件,我们的社区能够帮助您起步:Discord

第一步

这个页面包含一些示例,您可以从中学到 Deno 的基本概念。

我们假设您已经对 JavaScript 有过预先的了解,特别是 async/await。如果您没有了解过 JavaScript,您可能需要先阅读这个指南:JavaScript.

Hello World

Deno 是一个 JavaScript 和 TypeScript 的运行时,并尝试与浏览器兼容并使用现代的功能 (features)。

由于 Deno 具有浏览器兼容性,Hello World 程序与浏览器里的没有区别。

console.log("Welcome to Deno 🦕");

尝试一下:

deno run https://deno.land/std/examples/welcome.ts

发出一个 HTTP 请求

通过 HTTP 请求从服务器获取数据是一件很常见的事。让我们编写一个简单的程序来获取文件并打印到终端。

就像浏览器一样,您可以使用 web 标准的 fetch API 来发出请求。

const url = Deno.args[0];
const res = await fetch(url);

const body = new Uint8Array(await res.arrayBuffer());
await Deno.stdout.write(body);

让我们看看它做了什么:

  1. 我们取得了第一个命令行参数,存储到变量 url

  2. 我们向指定的地址发出请求,等待响应,然后存储到变量 res

  3. 我们把响应体解析为一个 ArrayBuffer,等待接收完毕,将其转换为 Uint8Array,最后存储到变量 body

  4. 我们把 body 的内容写入标准输出流 stdout

尝试一下:

deno run https://deno.land/std/examples/curl.ts https://example.com

这个程序将会返回一个关于网络权限的错误,我们做错了什么?您可能会想起来,Deno 默认用安全环境执行代码。这意味着您需要显式赋予程序权限,允许它进行一些特权操作,比如网络访问。

用正确的权限选项再试一次:

deno run --allow-net=example.com https://deno.land/std/examples/curl.ts https://example.com

读取一个文件

Deno 也提供内置的 API,它们都位于全局变量 Deno 中。您可以在此找到相关文档:doc.deno.land

文件系统 API 没有 web 标准形式,所以 Deno 提供了内置的 API。

示例:Unix cat

在这个程序中,每个命令行参数都是一个文件名,参数对应的文件将被依次打开,打印到标准输出流。

const filenames = Deno.args;
for (const filename of filenames) {
  const file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}

除了内核到用户空间再到内核的必要拷贝,这里的 copy() 函数不会产生额外的昂贵操作,从文件中读到的数据会原样写入标准输出流。这反映了 Deno I/O 流的通用设计目标。

尝试一下:

deno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd

TCP 服务

示例:TCP echo

这个示例是一个 TCP echo 服务,接收 8080 端口的连接,把接收到的任何数据返回给客户端。

const hostname = "0.0.0.0";
const port = 8080;
const listener = Deno.listen({ hostname, port });
console.log(`Listening on ${hostname}:${port}`);
for await (const conn of listener) {
  Deno.copy(conn, conn);
}

当这个程序启动时,它会抛出一个没有网络权限的错误。

$ deno run https://deno.land/std/examples/echo_server.ts
error: Uncaught PermissionDenied: network access to "0.0.0.0:8080", run again with the --allow-net flag
► $deno$/dispatch_json.ts:40:11
    at DenoError ($deno$/errors.ts:20:5)
    ...

为了安全,Deno 不允许程序访问网络,除非显式赋予权限。使用一个命令行选项来允许程序访问网络:

deno run --allow-net https://deno.land/std/examples/echo_server.ts

尝试用 netcat 向它发送数据。

$ nc localhost 8080
hello world
hello world

像示例 cat.ts 一样,copy() 函数不会产生不必要的内存拷贝。它从内核接收数据包,然后发送回去,就这么简单。

更多示例

您可以在 示例 一章找到更多示例。

权限

默认情况下,Deno是安全的。因此 Deno 模块没有文件、网络或环境的访问权限,除非您为它授权。在命令行参数中为 deno 进程授权后才能访问安全敏感的功能。

在以下示例中,mod.ts 只被授予文件系统的只读权限。它无法对其进行写入,或执行任何其他对安全性敏感的操作。

deno run --allow-read mod.ts

权限列表

以下权限是可用的:

  • -A, --allow-all 允许所有权限,这将禁用所有安全限制。
  • --allow-env 允许环境访问,例如读取和设置环境变量。
  • --allow-hrtime 允许高精度时间测量,高精度时间能够在计时攻击和特征识别中使用。
  • --allow-net=<allow-net> 允许网络访问。您可以指定一系列用逗号分隔的域名,来提供域名白名单。
  • --allow-plugin 允许加载插件。请注意:这是一个不稳定功能。
  • --allow-read=<allow-read> 允许读取文件系统。您可以指定一系列用逗号分隔的目录或文件,来提供文件系统白名单。
  • --allow-run 允许运行子进程。请注意,子进程不在沙箱中运行,因此没有与 deno 进程相同的安全限制,请谨慎使用。
  • --allow-write=<allow-write> 允许写入文件系统。您可以指定一系列用逗号分隔的目录或文件,来提供文件系统白名单。

权限白名单

Deno 还允许您使用白名单控制权限的粒度。

这是一个用白名单限制文件系统访问权限的示例,仅允许访问 /usr 目录,但它会在尝试访问 /etc 目录时失败。

$ deno run --allow-read=/usr https://deno.land/std/examples/cat.ts /etc/passwd
error: Uncaught PermissionDenied: read access to "/etc/passwd", run again with the --allow-read flag
► $deno$/dispatch_json.ts:40:11
    at DenoError ($deno$/errors.ts:20:5)
    ...

改为 /etc 目录,赋予正确的权限,再试一次:

$ deno run --allow-read=/etc https://deno.land/std/examples/cat.ts /etc/passwd

--allow-write 也一样,代表写入权限。

网络访问

fetch.ts:

const result = await fetch("https://deno.land/");

这是一个设置 host 或 url 白名单的示例:

$ deno run --allow-net=github.com,deno.land fetch.ts

如果 fetch.ts 尝试与其他域名建立网络连接,那么这个进程将会失败。

允许访问任意地址:

$ deno run --allow-net fetch.ts

使用 TypeScript

Deno 同时支持 JavaScript 和 TypeScript,它们是 Deno 的第一等语言。 这意味着它需要标准的模块名称,包括 扩展名(或提供正确媒体类型的服务器)。此外,Deno 还拥有“平凡”的模块解析算法。导入模块指定为文件(包括扩展名)或全限定 URL (fully qualified URL)。TypeScript 模块可以被直接导入,例如:

import { Response } from "https://deno.land/std@0.53.0/http/server.ts";
import { queue } from "./collections.ts";

使用外部类型定义

开箱即用的 TypeScript 编译器依赖于两种无扩展名 模块和 Node.js 模块解析逻辑,以将类型应用于 JavaScript 模块。

为了弥合这种差距,Deno 支持三种引用类型定义文件的方法,而不必求助于“魔法”的模块解析。

编译提示

如果您要导入 JavaScript 模块,并且知道该模块的类型定义在哪里,您可以在导入时指定类型定义。这采用编译器提示的形式。编译提示告诉 Deno .d.ts 文件的位置和与之相关的导入的 JavaScript 代码。编译提示指令是 @deno-types,当指定时,该值将在编译器中使用,而不是 JavaScript 模块。

例如,如果您有 foo.js,但您知道旁边的 foo.d.ts 是该模块的类型定义,代码将像这样:

// @deno-types="./foo.d.ts"
import * as foo from "./foo.js";

该值遵循与导入模块相同的解析逻辑,这意味着它需要具有扩展名,并且是相对于当前模块的。远程模块也可以使用该说明符。

此编译提示影响下一个 import 语句,或是 export ... from 语句,在编译时,该值将替换模块。像上面的示例一样,Deno 编译器将加载 ./foo.d.ts,而不是 ./foo.js。Deno 在运行时仍然会加载 ./foo.js

JavaScript 文件中的三斜杠引用指令

如果您要发布由 Deno 使用的模块,并且想要告诉 Deno 类型定义的位置,您可以使用实际代码中的三斜杠指令。

例如,如果您有一个 JavaScript 模块,想为 Deno 提供类型定义的位置,您的 foo.js 可能看起来像这样:

/// <reference types="./foo.d.ts" />
export const foo = "foo";

Deno 会看到这一点,并且在检查类型时,编译器将使用 foo.d.ts 文件,尽管 foo.js 将在运行时加载。

该值遵循与导入模块相同的解析逻辑,这意味着它需要具有扩展名,并且是相对于当前模块的。远程模块也可以使用该说明符。

X-TypeScript-Types 自定义 Header

如果您要发布由 Deno 使用的模块,并且想要告诉 Deno 类型定义的位置,您可以使用自定义 HTTP 头 X-TypeScript-Types,来告诉 Deno 文件位置。

标头的工作方式与上述三斜杠参考相同,这只是意味着 JavaScript 文件本身的内容不需要修改后,并且类型定义的位置可以通过服务器本身确定。

不是所有类型定义都受支持

Deno 将使用编译提示来加载指示的 .d.ts 文件,但有些 .d.ts 文件包含不受支持的功能。具体来说,有些 .d.ts 文件期望能够从其他包中加载或引用类型定义,它们使用模块解析逻辑。

例如,一个包含 node 的类型引用指令,希望解析为像 ./node_modules/@types/node/index.d.ts 的某些路径。由于这取决于非相对性的模块解析算法,Deno 无法处理这种情况。

为什么不在 TypeScript 文件中使用三斜杠类型引用?

TypeScript 编译器支持三斜杠指令,包括类型 参考指令。如果 Deno 使用此功能,则会干扰 TypeScript 编译器。Deno 仅在 JavaScript (包括 JSX)文件中查找指令。

自定义 TypeScript 编译器选项

在 Deno 生态系统中,所有严格标志都被默认启用,以符合 TypeScript 的理想状态。Deno 也支持自定义配置文件,例如 tsconfig.json

您需要通过显式设置 -c--config 选项,来明确告诉 Deno 在哪里寻找此配置。

deno run -c tsconfig.json mod.ts

以下是 Deno 当前允许的设置及其默认值:

{
  "compilerOptions": {
    "allowJs": false,
    "allowUmdGlobalAccess": false,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "alwaysStrict": true,
    "assumeChangesOnlyAffectDirectDependencies": false,
    "checkJs": false,
    "disableSizeLimit": false,
    "generateCpuProfile": "profile.cpuprofile",
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "lib": [],
    "noFallthroughCasesInSwitch": false,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitUseStrict": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveConstEnums": false,
    "removeComments": false,
    "resolveJsonModule": true,
    "strict": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": false,
    "useDefineForClassFields": false
  }
}

有关上述选项和用例的文档,请访问 typescript docs.

注意:以上列表中不包含 Deno 不支持的选项,或者 TypeScript 文档中已经标记为废弃的/实验性的选项。

WASM 支持

Deno 能够运行 wasm 二进制文件。

const wasmCode = new Uint8Array([
  0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127,
  3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0,
  5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145,
  128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97,
  105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0,
  65, 42, 11
]);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
console.log(wasmInstance.exports.main().toString());

运行时

包含所有运行时函数(Web API 与全局空间 Deno)的文档可以在 doc.deno.land 找到。

Web API

对于 web 标准中存在的 API,比如 fetch,Deno 使用它们,而不是自己发明新的。

已实现的 Web API 文档:doc.deno.land

已实现的 Web API 的列表在 这里

已实现的 web API 的 TypeScript 定义:

worker 特定的 API 定义:lib.deno.worker.d.ts

全局空间 Deno

所有非 web 标准的 API 位于全局命名空间 Deno

这其中包含文件读取、打开 TCP sockets、运行子进程等。

Deno 命名空间 的 TypeScript 定义:lib.deno.ns.d.ts

Deno 特定的 API 定义:doc.deno.land

稳定性

从 Deno 1.0.0 开始,Deno 命名空间内的 API 是稳定的。这意味着我们将尽力使 1.0.0 下的代码在未来的 Deno 版本上继续运行。

但是在现阶段,并非所有的 Deno 功能都可以应用于生产环境。仍处于起草阶段的、还未准备完善的功能被锁定在 --unstable 命令行选项后。

deno run --unstable mod_which_uses_unstable_stuff.ts

传递这个选项可以有如下效果:

  • 它将允许在运行时使用不稳定的 API。
  • 它将 lib.deno.unstable.d.ts 文件添加到用于类型检查的类型脚本定义列表中。这包括 deno types 的输出。

请注意,不稳定的 API 可能没有经过安全检查,将来可能有破坏性改动,并且还没有准备投入生产

标准模块

Deno 的 标准模块 尚不稳定。为了体现这点,我们用与 CLI 不同的版本号标记标准模块。和 Deno 命名空间不同,使用标准模块不需要 --unstable 选项(除非该模块使用了不稳定的 Deno 功能)。

程序生命周期

Deno 支持浏览器兼容的生命周期事件 loadunload。您可以使用这些事件在程序中提供用于安装 (setup) 和清理 (cleanup) 的代码。

load 事件的侦听器 (listener) 可以是异步 (async) 的,将被等待 (await)。unload 事件的监听器需要是同步的。这两项事件都不能被取消。

示例:

main.ts

import "./imported.ts";

const handler = (e: Event): void => {
  console.log(`got ${e.type} event in event handler (main)`);
};

window.addEventListener("load", handler);

window.addEventListener("unload", handler);

window.onload = (e: Event): void => {
  console.log(`got ${e.type} event in onload function (main)`);
};

window.onunload = (e: Event): void => {
  console.log(`got ${e.type} event in onunload function (main)`);
};

console.log("log from main script");

imported.ts

const handler = (e: Event): void => {
  console.log(`got ${e.type} event in event handler (imported)`);
};

window.addEventListener("load", handler);
window.addEventListener("unload", handler);

window.onload = (e: Event): void => {
  console.log(`got ${e.type} event in onload function (imported)`);
};

window.onunload = (e: Event): void => {
  console.log(`got ${e.type} event in onunload function (imported)`);
};

console.log("log from imported script");

注意,您可以同时使用 window.addEventListenerwindow.onload / window.onunload 来定义事件的处理程序。它们之间有一个主要的区别,让我们运行示例:

$ deno run main.ts
log from imported script
log from main script
got load event in onload function (main)
got load event in event handler (imported)
got load event in event handler (main)
got unload event in onunload function (main)
got unload event in event handler (imported)
got unload event in event handler (main)

所有通过 window.addEventListener 添加的侦听器都被运行,但是在 main.ts 中定义的 window.onloadwindow.onunload 覆盖了 imported.ts 中定义的处理程序。

换句话说,您可以注册多个 window.addEventListener "load""unload" 事件,但只有最后加载的 window.onloadwindow.onunload 事件将被执行。

编译器 API

这是一个不稳定的 Deno 特性。 更多信息请查阅 稳定性

Deno 支持对内置 TypeScript 编译器的运行时访问。Deno 命名空间中有三种方法提供此访问。

Deno.compile()

这类似于 deno cache,因为它可以获取代码、缓存代码、编译代码,但不运行代码。它最多接受三个参数,rootName、可选的 sources 和可选的 options

rootName 是用于生成目标程序的根模块。这类似于在 deno run --reload example.ts 中在命令行上传递的模块名。

sources 是一个哈希表,其中键是完全限定的模块名称,值是模块的文本源。如果传递了 sources,Deno 将从该哈希表中解析所有模块,而不会尝试在 Deno 之外解析它们。如果没有提供 sources,Deno 将解析模块,就像根模块已经在命令行上传递了一样。Deno 还将缓存所有的这些资源。所有已解析的资源都会被当成动态导入对待,导入行为是否需要读取和网络权限取决于目标在本地还是远程。

options 参数是一组 Deno.CompilerOptions 类型的选项,它是包含 Deno 支持选项的 TypeScript 编译器选项的子集。

该方法返回元组。第一个参数包含与代码相关的任何诊断信息(语法或类型错误)。第二个参数是一个映射,其中键是输出文件名,值是内容。

提供 sources 的一个例子:

const [diagnostics, emitMap] = await Deno.compile("/foo.ts", {
  "/foo.ts": `import * as bar from "./bar.ts";\nconsole.log(bar);\n`,
  "/bar.ts": `export const bar = "bar";\n`,
});

assert(diagnostics == null); // 确保没有返回诊断信息
console.log(emitMap);

我们希望 map 包含 4 个 “文件(files)” ,分别命名为 /foo.js.map/foo.js/bar.js.map,和 /bar.js

当不提供资源时,您可以使用本地或远程模块,就像在命令行上做的那样。所以您可以这样做:

const [diagnostics, emitMap] = await Deno.compile(
  "https://deno.land/std/examples/welcome.ts"
);

在这种情况下,emitMap 将包含一个 console.log() 语句。

Deno.bundle()

这与 deno bundle 在命令行上的工作非常相似。 它也与 Deno.compile() 类似,只是它不返回文件映射,而是只返回一个字符串,这是一个自包含的 JavaScript ES 模块,它将包含提供或解析的所有代码,以及提供的根模块的所有导出。它最多接受三个参数,rootName、可选的 sources 和可选的 options

rootName 是用于生成目标程序的根模块。这类似于在 deno bundle example.ts 中在命令行上传递的模块名。

sources 是一个哈希表,其中键是完全限定的模块名称,值是模块的文本源。如果传递了 sources,Deno 将从该哈希表中解析所有模块,而不会尝试在 Deno 之外解析它们。如果没有提供 sources,Deno 将解析模块,就像根模块已经在命令行上传递了一样。Deno 还将缓存所有的这些资源。所有已解析的资源都会被当成动态导入对待,导入行为是否需要读取和网络权限取决于目标在本地还是远程。

options 参数是一组 Deno.CompilerOptions 类型的选项,它是包含 Deno 支持选项的 TypeScript 编译器选项的子集。

提供 sources 的一个例子:

const [diagnostics, emit] = await Deno.bundle("/foo.ts", {
  "/foo.ts": `import * as bar from "./bar.ts";\nconsole.log(bar);\n`,
  "/bar.ts": `export const bar = "bar";\n`,
});

assert(diagnostics == null); // 确保没有返回诊断信息
console.log(emit);

我们希望 emit 是一个 ES 模块的文本,它将包含两个模块的输出源。

当不提供资源时,您可以使用本地或远程模块,就像在命令行上做的那样。所以您可以这样做:

const [diagnostics, emit] = await Deno.bundle(
  "https://deno.land/std/http/server.ts"
);

在这种情况下,emit 将是一个自包含的 JavaScript ES 模块,并解析了所有依赖项,导出与源模块相同的导出。

Deno.transpileOnly()

这是基于 TypeScript 函数 transpileModule() 的。所有这些操作都会“擦除”模块中的任何类型并释放 JavaScript。没有类型检查和依赖关系的解析。它最多接受两个参数,第一个参数是哈希表,其中键是模块名称,值是内容。模块名称的唯一用途是在将信息放入源映射时,显示源文件名称是什么。第二个参数包含 Deno.CompilerOptions 类型的可选 options。函数通过映射解析,其中键是提供的源模块名称,值是具有 source 属性和可选 map 属性的对象。第一个是模块的输出内容。 map 属性是源映射。源映射是默认提供的,但可以通过 options 参数关闭。

举个例子:

const result = await Deno.transpileOnly({
  "/foo.ts": `enum Foo { Foo, Bar, Baz };\n`,
});

console.log(result["/foo.ts"].source);
console.log(result["/foo.ts"].map);

我们期望 enum 被重写成一个构造可枚举的 IIFE,并且映射被定义。

引用 TypeScript 库文件

当您使用 deno run 或其他 TypeScript 类型的 Deno 命令时,该代码将根据描述 Deno 支持的环境的自定义库进行评估。默认情况下,TypeScript 类型的编译器运行时 API 也使用这些库(Deno.compile()Deno.bundle())。

但是,如果您希望为其他运行时编译或捆绑 TypeScript,则您可能希望重载默认库。为此,运行时 API 支持编译器选项中的 lib 属性。例如,如果你的 TypeScript 代码是为浏览器准备的,您可以使用 TypeScript 的 "dom" 库:

const [errors, emitted] = await Deno.compile(
  "main.ts",
  {
    "main.ts": `document.getElementById("foo");\n`,
  },
  {
    lib: ["dom", "esnext"],
  }
);

有关 TypeScript 支持的所有库的列表,请参见 lib 编译器选项不要忘记包含 JavaScript 库

就像 tsc 一样,当您提供一个 lib 编译器选项时,它会覆盖默认的选项,这意味着基本的 JavaScript 库不会被包含,而您应该包含最能代表您的目标运行时的选项(例如 es5es2015es2016es2017es2018es2019es2020esnext)。

包含 Deno 命名空间

除了 TypeScript 提供的库之外,还有四个内置在 Deno 中的库可以引用:

  • deno.ns - 提供 Deno 命名空间
  • deno.shared_globals - 提供 Deno 运行时支持的全局接口和变量,然后由最终运行时库公开
  • deno.window - 公开全局变量和 Deno 主工作进程中可用的 Deno 命名空间,是运行时编译器的默认 API
  • deno.worker - 公开在 Deno 下的工作进程中可用的全局变量。

因此,要将 Deno 命名空间添加到编译中,需要在数组中包含 deno.ns 库,例如:

const [errors, emitted] = await Deno.compile(
  "main.ts",
  {
    "main.ts": `document.getElementById("foo");\n`,
  },
  {
    lib: ["dom", "esnext", "deno.ns"],
  }
);

注意,Deno 命名空间需要一个 ES2018 或更新的运行时环境。这意味着,如果您使用的库“低于” ES2018,那么您将得到编译过程中输出的错误。

使用三斜杠引用(triple-slash reference)

您不必在编译器选项中指定 lib。Deno 支持对库的三斜杠引用,并可以嵌入到文件的内容中。举个例子,如果你有一个 main.ts

/// <reference lib="dom" />

document.getElementById("foo");

它可以编译,且不会出现下面这样的错误:

const [errors, emitted] = await Deno.compile("./main.ts", undefined, {
  lib: ["esnext"],
});

注意dom 库与 Deno 的默认类型库中定义的一些默认全局变量有冲突。为了避免这种情况,需要在编译器选项中为运行时编译器 API 指定一个 lib 选项。

Worker

Deno 支持 Web Worker API.

Worker 能够用来在多个线程中运行代码,Worker 的每个实例都会在一个单独的线程中运行,这个线程专属于它。

目前,Deno 只支持 module 类型的 worker,因此在创建新的 worker 时必须传递 type: "module" 选项。

// Good
new Worker("./worker.js", { type: "module" });

// Bad
new Worker("./worker.js");
new Worker("./worker.js", { type: "classic" });

权限

创建一个新的 Worker 实例的行为与动态导入类似,因此 Deno 需要适当的权限来做这个操作。

对于使用本地模块的 worker,Deno 需要读取 (--allow-read) 权限:

main.ts

new Worker("./worker.ts", { type: "module" });

worker.ts

console.log("hello world");
self.close();
$ deno run main.ts
error: Uncaught PermissionDenied: read access to "./worker.ts", run again with the --allow-read flag

$ deno run --allow-read main.ts
hello world

对于使用远程模块的 worker,Deno 需要网络 (--allow-net) 权限:

main.ts

new Worker("https://example.com/worker.ts", { type: "module" });

worker.ts

console.log("hello world");
self.close();
$ deno run main.ts
error: Uncaught PermissionDenied: net access to "https://example.com/worker.ts", run again with the --allow-net flag

$ deno run --allow-net main.ts
hello world

在 Worker 中使用 Deno

这是一个不稳定的 Deno 特性。 更多信息请查阅 稳定性

默认情况下,Deno 命名空间在 worker 作用域中不可用。

要想启用 Deno 命名空间,在创建新的 worker 时传递 deno: true 选项:

main.js

const worker = new Worker("./worker.js", { type: "module", deno: true });
worker.postMessage({ filename: "./log.txt" });

worker.js

self.onmessage = async (e) => {
  const { filename } = e.data;
  const text = await Deno.readTextFile(filename);
  console.log(text);
  self.close();
};

log.txt

hello world
$ deno run --allow-read --unstable main.js
hello world

Deno 命名空间在 worker 作用域中启用时,此 worker 继承创建者的权限(使用类似 --allow-* 的选项指定的权限)。

我们计划提供 worker 权限的配置方法。

与外部代码连接

入门 章节中,我们看到 Deno 能够从 URL 执行脚本。像浏览器中的 JavaScript 一样,Deno 可以从 URL 直接导入代码库。

这个示例使用 URL 来导入一个断言库:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

assertEquals("hello", "hello");
assertEquals("world", "world");

console.log("Asserted! 🎉");

尝试运行一下:

$ deno run test.ts
Compile file:///mnt/f9/Projects/github.com/denoland/deno/docs/test.ts
Download https://deno.land/std/testing/asserts.ts
Download https://deno.land/std/fmt/colors.ts
Download https://deno.land/std/testing/diff.ts
Asserted! 🎉

对于这个程序,我们不需要提供 --allow-net 选项。当它访问网络时,Deno 运行时有着特殊权限来下载模块并缓存到磁盘。

Deno 在一个特殊目录缓存了远程模块,该路径可以被 $DENO_DIR 指定,如果没有指定,默认为系统缓存目录。下一次运行这个程序时无需下载。如果这个程序没有改动,它不会被再次编译。

系统缓存目录默认为:

  • Linux/Redox: $XDG_CACHE_HOME/deno or $HOME/.cache/deno
  • Windows: %LOCALAPPDATA%/deno (%LOCALAPPDATA% = FOLDERID_LocalAppData)
  • macOS: $HOME/Library/Caches/deno

如果失败,该路径设置为 $HOME/.deno

FAQ

如何导入特定版本?

只需在 URL 中指定版本。举个例子,这个 URL 指定了要运行的版本 https://unpkg.com/liltest@0.0.5/dist/liltest.js

到处导入 URL 似乎很麻烦

如果其中一个 URL 链接到一个完全不同的库版本,该怎么办?

在大型项目中到处维护 URL 是否容易出错?

解决办法是在一个中心化的 deps.ts 中重新导出所依赖的外部库,它和 Node 的 package.json 具有相同的作用。

举个例子,您正在一个大型项目中使用一个断言库,您可以创建一个 deps.ts 文件来导出第三方代码,而不是到处导入 "https://deno.land/std/testing/asserts.ts"

export {
  assert,
  assertEquals,
  assertStrContains,
} from "https://deno.land/std/testing/asserts.ts";

在这个项目中,您可以从 deps.ts 导入,避免对相同的 URL 产生过多引用。

import { assertEquals, runTests, test } from "./deps.ts";

这种设计避免了由包管理软件、集中的代码存储库和多余的文件格式所产生的大量复杂性。

如何信任可能更改的 URL?

使用 --lock 命令行选项,通过一个锁文件 (lock file),您可以确保从一个 URL 下载的代码和初始开发时一样。更多信息请看 这里

如果依赖宕机怎么办?源代码将不再可用。

像上面一样,这是 任何 远程依赖系统都要面对的问题。

依赖外部服务在开发时很方便,但在生产环境很脆弱。生产级软件总是应该打包 (vendor) 所有依赖。

在 Node 中,这需要将 node_modules 检入版本控制系统。

在 Deno 中,这需要在运行时将 $DENO_DIR 指向项目内的目录,同样把依赖检入版本控制系统。

# 下载依赖
DENO_DIR=./deno_dir deno cache src/deps.ts

# 确保需要缓存的任何命令都设置了 `DENO_DIR` 变量
DENO_DIR=./deno_dir deno test src

# 将缓存目录检入版本控制
git add -u deno_dir
git commit

重新加载特定的模块

您可以使用 --reload 选项使本地 DENO_DIR 缓存失效。其用途如下:

重新加载所有内容:--reload

有时我们只想升级某些模块,可以通过将参数传递给 --reload 选项来控制它。

重新加载所有标准模块:--reload=https://deno.land/std

为了重新加载特定的模块(在这个例子中是 colors 和 file system copy),需要使用逗号来分隔 URL:

--reload=https://deno.land/std/fs/copy.ts,https://deno.land/std/fmt/colors.ts

完整性检查与锁定文件

Deno 可以使用一个较小的 JSON 文件存储和检查模块的子资源完整性。

使用 --lock=lock.json 启用和指定锁文件检查。

要更新或创建锁,可以使用 --lock=lock.json --lock-write

一个典型的工作流看起来像这样:

// 向 "src/deps.ts" 添加一个新的依赖,在别处使用。
export { xyz } from "https://unpkg.com/xyz-lib@v0.9.0/lib.ts";
# 创建或更新锁文件 "lock.json"
deno cache --lock=lock.json --lock-write src/deps.ts

# 在提交时包含这一变化
git add -u lock.json
git commit -m "feat: Add support for xyz using xyz-lib"
git push

另一台机器上的合作者刚刚把项目克隆下来:

# 下载、缓存并检查项目的依赖
deno cache -r --lock=lock.json src/deps.ts

# 在这完成之后,您可以安心开发了
deno test --allow-read src

代理(Proxies)

Deno 支持模块下载和 Web 标准 fetch API 的代理。

代理配置从环境变量中读取: HTTP_PROXYHTTPS_PROXY

在 Windows 的环境下,如果没有发现环境变量,Deno 会从注册表中读取代理。

导入映射(Import maps)

这是一个不稳定的特性。 更多信息请查阅 稳定性

Deno 支持 导入映射

您可以通过 --importmap=<FILE> 的命令行选项使用导入映射。

目前的限制:

  • 只支持单个导入映射
  • 没有 fallback URL
  • Deno 不支持 std: 命名空间
  • 仅支持 file:http:https: 协议

示例:

// import_map.json

{
   "imports": {
      "http/": "https://deno.land/std/http/"
   }
}
// hello_server.ts

import { serve } from "http/server.ts";

const body = new TextEncoder().encode("Hello World\n");
for await (const req of serve(":8000")) {
  req.respond({ body });
}
$ deno run --allow-net --importmap=import_map.json --unstable hello_server.ts

标准库

Deno 提供一组标准模块,它们经过核心团队审计,保证能在 Deno 上工作。

标准库地址:https://deno.land/std/

版本和稳定性

标准库尚不稳定,因此采用与 Deno 不同的版本号。

最新的发布请查阅 https://deno.land/std/https://deno.land/std/version.ts

我们强烈建议:始终使用确定版本的标准库,以避免意外的改动。

排错 (Troubleshooting)

标准库中的一些模块使用了不稳定的 Deno API。

不用 --unstable 命令行选项运行这些模块会产生一些 TypeScript 错误,表示 Deno 命名空间中不存在一些 API:

// main.ts
import { copy } from "https://deno.land/std@0.50.0/fs/copy.ts";

copy("log.txt", "log-old.txt");
$ deno run --allow-read --allow-write main.ts
Compile file:///dev/deno/main.ts
Download https://deno.land/std@0.50.0/fs/copy.ts
Download https://deno.land/std@0.50.0/fs/ensure_dir.ts
Download https://deno.land/std@0.50.0/fs/_util.ts
error: TS2339 [ERROR]: Property 'utime' does not exist on type 'typeof Deno'.
    await Deno.utime(dest, statInfo.atime, statInfo.mtime);
               ~~~~~
    at https://deno.land/std@0.50.0/fs/copy.ts:90:16

TS2339 [ERROR]: Property 'utimeSync' does not exist on type 'typeof Deno'.
    Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
         ~~~~~~~~~
    at https://deno.land/std@0.50.0/fs/copy.ts:101:10

解决方法是加上 --unstable 选项:

$ deno run --allow-read --allow-write --unstable main.ts

要确定哪些 API 是不稳定的,请查阅类型声明 lib.deno.unstable.d.ts

这个问题会在不远的将来解决。

测试

Deno 有一个内置的测试器,可以用来测试 JavaScript 或 TypeScript 代码。

编写测试

要定义测试,需要使用要测试的名称和函数调用 Deno.test

Deno.test("hello world", () => {
  const x = 1 + 2;
  if (x !== 3) {
    throw Error("x should be equal to 3");
  }
});

https://deno.land/std/testing 上有一些有用的断言实用程序,可以简化测试:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

Deno.test("hello world", () => {
  const x = 1 + 2;
  assertEquals(x, 3);
});

异步函数

您还可以通过传递一个测试函数来测试异步代码,该函数返回一个 promise。 为此,您可以在定义函数时使用 async 关键字:

import { delay } from "https://deno.land/std/async/delay.ts";

Deno.test("async hello world", async () => {
  const x = 1 + 2;

  // await some async task
  await delay(100);

  if (x !== 3) {
    throw Error("x should be equal to 3");
  }
});

资源和异步操作清理器

Deno 中的某些操作在资源表(在此处了解更多)中创建资源。 这些资源应该在使用完后关闭。

对于每个测试定义,测试器会检查此测试中创建的所有资源是否已关闭,以防止资源“泄漏”。 默认情况下,这对所有测试都是启用的,但可以通过在测试定义中将 sanitizeResources 布尔值设置为 false 来禁用。

对于异步操作(如与文件系统交互)也是如此。测试器检查您在测试中启动的每个操作是否在测试结束之前完成。默认情况下,这对所有测试都是启用的,但可以通过在测试定义中将 sanitizeps 布尔值设置为 false 来禁用。

Deno.test({
  name: "leaky test",
  fn() {
    Deno.open("hello.txt");
  },
  sanitizeResources: false,
  sanitizeOps: false,
});

忽略测试

有时您希望忽略基于某种条件的测试(例如您只希望在 Windows 上运行测试)。 为此,您可以使用 ignore 测试定义中的布尔值。 如果它被设置为 true,则测试将被跳过。

Deno.test({
  name: "do macOS feature",
  ignore: Deno.build.os !== "darwin",
  fn() {
    doMacOSFeature();
  },
});

运行测试

要运行测试,请使用包含您测试函数的文件调用 deno test

deno test my_test.ts

您还可以省略文件名,在这种情况下,当前目录(递归)下所有与通配符 {*_,}test.{js,ts,jsx,tsx} 匹配的测试将会被运行。 如果传递一个目录,则该目录中与此 glob 匹配的所有文件将运行。

内置工具

Deno 提供了一些内置的工具,在处理 JavaScript 和 TypeScript 时很有用。

调试器 (debugger)

Deno 支持 V8 Inspector Protocol.

使用 Chrome Devtools 或其他支持该协议的客户端(比如 VSCode)能够调试 Deno 程序。

要启用调试功能,用 --inspect--inspect-brk 选项运行 Deno。

--inspect 选项允许在任何时间点连接调试器,而 --inspect-brk 选项会等待调试器连接,在第一行代码处暂停执行。

Chrome Devtools

让我们用 Chrome 开发者工具来调试一个程序,我们将使用来自 stdfile_server.ts,这是一个静态文件服务。

使用 --inspect-brk 选项,在第一行代码处暂停执行。

$ deno run --inspect-brk --allow-read --allow-net https://deno.land/std@v0.50.0/http/file_server.ts
Debugger listening on ws://127.0.0.1:9229/ws/1e82c406-85a9-44ab-86b6-7341583480b1
Download https://deno.land/std@v0.50.0/http/file_server.ts
Compile https://deno.land/std@v0.50.0/http/file_server.ts
...

打开 chrome://inspect,点击 target 旁边的 Inspect

chrome://inspect

开发者工具加载所有模块时可能会等待几秒。

Devtools opened

您可能注意到开发者工具暂停执行的地方不是 file_server.ts,而是 _constants.ts 的第一行。这是符合预期的行为,ES 模块在 V8 中执行的顺序如此。_constants.tsfile_server.ts 最深、最先的依赖,因此它会最先执行。

在这时,所有源码在开发者工具中都可用。打开 file_server.ts,加一处断点,然后打开 "Sources" 面板,展开树:

Open file_server.ts

仔细观察,您会发现每个文件都有重复的条目,一个是正常字体,另一个是斜体。前者是编译后的源文件(所以 .ts 文件会生成 JavaScript 源代码),后者是该文件的源映射 (source map)。

listenAndServe 方法处加一个断点。

Break in file_server.ts

添加断点后,开发者工具会自动打开源映射文件,让我们能在包含类型的实际源码中步进。

现在我们已经设置了断点,在触发断点时,我们可以检查传入的请求,也可以继续执行脚本。点击恢复脚本执行的按钮即可,您可能需要点两次。

当脚本继续运行时,让我们发送一个请求,看看开发者工具中发生了什么。

$ curl http://0.0.0.0:4500/

Break in request handling

在这时,我们可以检查请求的内容,逐步调试代码。

VSCode

Deno 可以在 VSCode 中调试。

插件的官方支持正在开发中 https://github.com/denoland/vscode_deno/issues/12

我们也可以通过手动提供 launch.json 配置,来连接调试器:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Deno",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "deno",
      "runtimeArgs": ["run", "--inspect-brk", "-A", "<entry_point>"],
      "port": 9229
    }
  ]
}

注意:将 <entry_point> 替换为实际的脚本名称。

让我们尝试一下本地源文件,创建 server.ts

import { serve } from "https://deno.land/std@v0.50.0/http/server.ts";
const server = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of server) {
  req.respond({ body: "Hello World\n" });
}

<entry_point> 改为 server.ts,然后运行。

VSCode debugger

VSCode debugger

JetBrains IDE

您可以使用 JetBrains IDE 来调试 Deno,右击您想要调试的文件,选择 Debug 'Deno: <file name>'。这会创建一个没有权限设置的 运行/调试 配置,您可能需要更改 Arguments 字段来提供所需权限。

其他

实现 Devtools 协议的任何客户端都能连接 Deno 进程。

限制

开发者工具的支持仍不成熟,有一些功能是缺失的,或是有 bug 的:

  • 开发者工具控制台中的自动补全会让 Deno 进程退出。
  • 性能分析 (profiling) 和内存转储 (memory dump) 可能不正确。

脚本安装器

Deno 提供 deno install 来安装和分发可执行代码。

deno install [OPTIONS...] [URL] [SCRIPT_ARGS...] 将把位于 URL 的脚本安装到名称 EXE_NAME 下。

这个命令将会创建一个轻薄的 shell 脚本来调用 deno,其中写入了特定的命令行参数。它位于 deno 安装目录的 bin 子目录下。

示例:

$ deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts
[1/1] Compiling https://deno.land/std/http/file_server.ts

✅ Successfully installed file_server.
/Users/deno/.deno/bin/file_server

要改变命令名称,使用 -n/--name 参数:

deno install --allow-net --allow-read -n serve https://deno.land/std/http/file_server.ts

默认情况下,Deno 会自动推导命令名称。

  • 尝试获取文件名称 (file stem),以上示例将推导为 "file_server"

  • 如果文件名称是通用的,比如 "main"、"mod"、"index" 或 "cli",并且它的路径没有父级,那么取父级路径的文件名,否则设置为原通用名称。

要改变安装路径,使用 --root 选项:

$ deno install --allow-net --allow-read --root /usr/local https://deno.land/std/http/file_server.ts

按照优先顺序确定安装根目录:

  • --root 选项
  • DENO_INSTALL_ROOT 环境变量
  • $HOME/.deno

如果需要,它们必须被添加进 PATH 环境变量。

$ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc

在安装时,您必须指定脚本会用到的权限。

$ deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts 8080

以上命令会创建一个名叫 file_server 的命令,运行时需要读取权限和网络权限,绑定到 8080 端口。

我们建议使用 import.meta.main 来指定作为可执行脚本时的入口点。

示例:

// https://example.com/awesome/cli.ts
async function myAwesomeCli(): Promise<void> {
  -- snip --
}

if (import.meta.main) {
  myAwesomeCli();
}

当您创建一个可执行脚本时,最好在仓库中告诉用户如何安装,让用户看到一个示例安装命令。

# 使用 deno install 安装

$ deno install -n awesome_cli https://example.com/awesome/cli.ts

代码格式化

Deno 有着内置的格式化工具,能够格式化 TypeScript 和 JavaScript 代码。

# 格式化当前目录和子目录下的所有 JS/TS 文件
deno fmt
# 格式化特定的文件
deno fmt myfile1.ts myfile2.ts
# 检查当前目录和子目录下的所有 JS/TS 文件是否都已被格式化
deno fmt --check
# 将标准输入流格式化并写入标准输出流
cat file.ts | deno fmt -

通过加上一句 // deno-fmt-ignore 注释来忽略格式化。

// deno-fmt-ignore
export const identity = [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1,
];

在文件头部加上一句 // deno-fmt-ignore-file 注释可以忽略整个文件。

打包

deno bundle [URL] 将输出一个单独的 JavaScript 文件,其中包含了它的所有依赖。

示例:

> deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js
Bundling "colors.bundle.js"
Emitting bundle to "colors.bundle.js"
9.2 kB emitted.

如果您忽略了输出文件参数,打包文件将输出到 stdout。

这个打包文件能够像其他任何模块一样在 Deno 中运行。

deno run colors.bundle.js

打包文件是一个自包含 (self contained) 的 ES 模块,其中的任何导出仍然可用。

举个例子,如果主模块是这样的:

export { foo } from "./foo.js";

export const bar = "bar";

它可以像这样被导入:

import { foo, bar } from "./lib.bundle.js";

打包文件也可以在浏览器中被加载,它是一个自包含的 ES 模块,因此 type 属性 (attribute) 必须设置为 "module"

示例:

<script type="module" src="website.bundle.js"></script>

除了直接加载,它也可以从其他模块导入。

<script type="module">
  import * as website from "website.bundle.js";
</script>

文档生成器

依赖检查器

deno info [URL] 会列出 ES 模块和它的所有依赖。

deno info https://deno.land/std@0.52.0/http/file_server.ts
Download https://deno.land/std@0.52.0/http/file_server.ts
...
local: /Users/deno/Library/Caches/deno/deps/https/deno.land/5bd138988e9d20db1a436666628ffb3f7586934e0a2a9fe2a7b7bf4fb7f70b98
type: TypeScript
compiled: /Users/deno/Library/Caches/deno/gen/https/deno.land/std@0.52.0/http/file_server.ts.js
map: /Users/deno/Library/Caches/deno/gen/https/deno.land/std@0.52.0/http/file_server.ts.js.map
deps:
https://deno.land/std@0.52.0/http/file_server.ts
  ├─┬ https://deno.land/std@0.52.0/path/mod.ts
  │ ├─┬ https://deno.land/std@0.52.0/path/win32.ts
  │ │ ├── https://deno.land/std@0.52.0/path/_constants.ts
  │ │ ├─┬ https://deno.land/std@0.52.0/path/_util.ts
  │ │ │ └── https://deno.land/std@0.52.0/path/_constants.ts
  │ │ └─┬ https://deno.land/std@0.52.0/testing/asserts.ts
  │ │   ├── https://deno.land/std@0.52.0/fmt/colors.ts
  │ │   └── https://deno.land/std@0.52.0/testing/diff.ts
  │ ├─┬ https://deno.land/std@0.52.0/path/posix.ts
  │ │ ├── https://deno.land/std@0.52.0/path/_constants.ts
  │ │ └── https://deno.land/std@0.52.0/path/_util.ts
  │ ├─┬ https://deno.land/std@0.52.0/path/common.ts
  │ │ └── https://deno.land/std@0.52.0/path/separator.ts
  │ ├── https://deno.land/std@0.52.0/path/separator.ts
  │ ├── https://deno.land/std@0.52.0/path/interface.ts
  │ └─┬ https://deno.land/std@0.52.0/path/glob.ts
  │   ├── https://deno.land/std@0.52.0/path/separator.ts
  │   ├── https://deno.land/std@0.52.0/path/_globrex.ts
  │   ├── https://deno.land/std@0.52.0/path/mod.ts
  │   └── https://deno.land/std@0.52.0/testing/asserts.ts
  ├─┬ https://deno.land/std@0.52.0/http/server.ts
  │ ├── https://deno.land/std@0.52.0/encoding/utf8.ts
  │ ├─┬ https://deno.land/std@0.52.0/io/bufio.ts
  │ │ ├─┬ https://deno.land/std@0.52.0/io/util.ts
  │ │ │ ├── https://deno.land/std@0.52.0/path/mod.ts
  │ │ │ └── https://deno.land/std@0.52.0/encoding/utf8.ts
  │ │ └── https://deno.land/std@0.52.0/testing/asserts.ts
  │ ├── https://deno.land/std@0.52.0/testing/asserts.ts
  │ ├─┬ https://deno.land/std@0.52.0/async/mod.ts
  │ │ ├── https://deno.land/std@0.52.0/async/deferred.ts
  │ │ ├── https://deno.land/std@0.52.0/async/delay.ts
  │ │ └─┬ https://deno.land/std@0.52.0/async/mux_async_iterator.ts
  │ │   └── https://deno.land/std@0.52.0/async/deferred.ts
  │ └─┬ https://deno.land/std@0.52.0/http/_io.ts
  │   ├── https://deno.land/std@0.52.0/io/bufio.ts
  │   ├─┬ https://deno.land/std@0.52.0/textproto/mod.ts
  │   │ ├── https://deno.land/std@0.52.0/io/util.ts
  │   │ ├─┬ https://deno.land/std@0.52.0/bytes/mod.ts
  │   │ │ └── https://deno.land/std@0.52.0/io/util.ts
  │   │ └── https://deno.land/std@0.52.0/encoding/utf8.ts
  │   ├── https://deno.land/std@0.52.0/testing/asserts.ts
  │   ├── https://deno.land/std@0.52.0/encoding/utf8.ts
  │   ├── https://deno.land/std@0.52.0/http/server.ts
  │   └── https://deno.land/std@0.52.0/http/http_status.ts
  ├─┬ https://deno.land/std@0.52.0/flags/mod.ts
  │ └── https://deno.land/std@0.52.0/testing/asserts.ts
  └── https://deno.land/std@0.52.0/testing/asserts.ts

依赖检查器对本地或远程的任意 ES 模块都有效。

缓存位置

deno info 可以用来显示与缓存位置有关的信息:

deno info
DENO_DIR location: "/Users/deno/Library/Caches/deno"
Remote modules cache: "/Users/deno/Library/Caches/deno/deps"
TypeScript compiler cache: "/Users/deno/Library/Caches/deno/gen"

嵌入式 Deno

Deno 由多个部分组成,其中之一是 deno_core。这是一个 rust crate,可以用作 Rust 应用中的嵌入式 JavaScript 运行时。

Deno 建立在 deno_core 的基础上。

Deno crate 发布于 crates.io

您可以通过 docs.rs 查阅其 API。

贡献

开发

从源码构建的步骤在 这里

发起一个 Pull Request

在提交之前,请确认以下步骤:

  1. 存在一个相关 issue,并且 PR 文本中引用了它。
  2. 有覆盖这些变化的测试。
  3. 确保 cargo test 通过。
  4. 使用 tools/format.py 格式化代码。
  5. 确保 ./tools/lint.py 通过。

third_party 的改动

deno_third_party 包含了大部分 Deno 依赖的外部代码,所以我们在任何时候都知道我们在运行什么。我们用一些手动操作和私有脚本来维护它,要做出改动,您可能需要联系 @ry 或 @piscisaureus。

增加 Ops

Ops 又称“绑定” (bindings)。

我们非常担心在添加新 API 时会出错。在向 Deno 添加 Op 时,应该研究其他平台上的对应接口。请列出如何在 Go、Node、Rust 和 Python 中完成此功能。

例如,参考 deno.rename() 是如何在 PR #671 中提出并添加的。

发布

以往发布的所做更改的总结在 这里

API 文档

公开 API 的文档很重要,我们希望它与代码内联。这有助于确保代码和文档紧密结合在一起。

利用 JSDoc

所有通过 deno 模块以及 global/window 命名空间公开的 API 和类型都应该有 JSDoc 文档。该文档经过解析并可供 TypeScript 编译器使用,因此易于在下游提供。JSDoc 块就在它们应用的语句之前,并以 /** doc */ 表示。例如:

/** A simple JSDoc comment */
export const FOO = "foo";

更多信息位于 https://jsdoc.app/

从源码构建

以下是从源码构建 Deno 的操作步骤。如果您只是想使用 Deno,您可以下载一个预构建的可执行文件,参考 入门 章节。

克隆 Deno 仓库

Linux 或 Mac 系统:

Clone on Linux or Mac:

git clone --recurse-submodules https://github.com/denoland/deno.git

在 Windows 系统上有一些额外步骤:

  1. 打开“开发者模式”,否则符号链接将需要管理员权限。

  2. 确认您正在使用 git 2.19.2.windows.1 或更高版本。

  3. 在检出 (checkout) 前,设置 core.symlinks=true

    git config --global core.symlinks true
    git clone --recurse-submodules https://github.com/denoland/deno.git
    

前置条件

最简单的方式是使用预编译的 V8 :

cargo build -vv

如果您想要从源码构建 Deno 和 V8 :

V8_FROM_SOURCE=1 cargo build -vv

从源码构建 V8 时会有更多依赖:

Python 2. 确认您的 PATH 环境变量中有一个无后缀 (suffix-less) 的 python/python.exe,并且它指向 Python 2,而不是 Python3 (issue 464)。

对于 Linux 用户,必须已经安装 glib-2.0 开发文件。(在 Ubuntu 上,运行 apt install libglib2.0-dev

对于 Mac 用户,必须已经安装 XCode

对于 Windows 用户:

  1. 安装 VS Community 2019,安装 "Desktop development with C++" 工具包,确认以下工具都已被选中和安装。

    • Visual C++ tools for CMake
    • Windows 10 SDK (10.0.17763.0)
    • Testing tools core features - Build Tools
    • Visual C++ ATL for x86 and x64
    • Visual C++ MFC for x86 and x64
    • C++/CLI support
    • VC++ 2015.3 v14.00 (v140) toolset for desktop
  1. 启用 "Debugging Tools for Windows": "Control Panel" → "Programs" → "Programs and Features" → 选择 "Windows Software Development Kit - Windows 10" → "Change" → "Change" → 检查 "Debugging Tools For Windows" → "Change" → "Finish" 或者使用 Debugging Tools for Windows,它会下载文件,您应该手动安装 X64 Debuggers And Tools-x64_en-us.msi

有关构建 V8 的更多细节请查阅 rusty_v8's README

构建

使用 Cargo:

# 构建:
cargo build -vv

# 构建失败?确保您位于最新的 master 分支,然后再试一次。如果还不行,尝试清除上一次的结果:
cargo clean && cargo build -vv

# 运行:
./target/debug/deno run cli/tests/002_hello.ts

测试和工具

测试

测试 deno:

# 运行所有测试套件:
cargo test

# 只测试 cli/js/:
cargo test js_unit_tests

测试 std/:

cargo test std_tests

代码检查与格式化

检查

./tools/lint.py

格式化

./tools/format.py

性能分析

# 确认我们正在构建发布版 (release)。
# 构建 deno 和 V8 的 d8。
ninja -C target/release d8

# 使用 --prof 选项运行想要分析的程序。
./target/release/deno run tests/http_bench.ts --allow-net --v8-flags=--prof &

# 施加压力。
third_party/wrk/linux/wrk http://localhost:4500/
kill `pgrep deno`

V8 将在当前目录写入一个文件,像这样 isolate-0x7fad98242400-v8.log。查看这个文件:

D8_PATH=target/release/ ./third_party/v8/tools/linux-tick-processor
isolate-0x7fad98242400-v8.log > prof.log
# 在 macOS 上, 使用 ./third_party/v8/tools/mac-tick-processor

prof.log 将包含不用调用的 tick 分布。

用 Web UI 查看这个日志,先生成 JSON 文件:

D8_PATH=target/release/ ./third_party/v8/tools/linux-tick-processor
isolate-0x7fad98242400-v8.log --preprocess > prof.json

在您的浏览器中打开 third_party/v8/tools/profview/index.html,选择 prof.json 以查看分布图。

在性能分析时有用的 V8 选项:

  • --prof
  • --log-internal-timer-events
  • --log-timer-events
  • --track-gc
  • --log-source-code
  • --track-gc-object-stats

有关 d8 和性能分析的更多信息,请查阅以下链接:

使用 LLDB 调试

Debugging with LLDB

$ lldb -- target/debug/deno run tests/worker.js
> run
> bt
> up
> up
> l

调试 Rust 代码,可以用 rust-lldb

$ rust-lldb -- ./target/debug/deno run --allow-net tests/http_bench.ts
# 在 macOS 上,您可能看到像这样的警告:
# `ImportError: cannot import name _remove_dead_weakref`
# 在这种情况下,设置 PATH 以使用系统 python,例如
# PATH=/System/Library/Frameworks/Python.framework/Versions/2.7/bin:$PATH
(lldb) command script import "/Users/kevinqian/.rustup/toolchains/1.36.0-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"
(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust
(lldb) type category enable Rust
(lldb) target create "../deno/target/debug/deno"
Current executable set to '../deno/target/debug/deno' (x86_64).
(lldb) settings set -- target.run-args  "tests/http_bench.ts" "--allow-net"
(lldb) b op_start
(lldb) r

V8 选项

V8 有很多内部的命令行选项。

# 列出可用的 V8 选项
$ deno --v8-flags=--help

# 使用多个选项的示例
$ deno --v8-flags=--expose-gc,--use-strict

特别有用的:

--async-stack-trace

持续的性能测试

参考我们的测试 https://deno.land/benchmarks

测试图表假设 https://github.com/denoland/benchmark_data/blob/gh-pages/data.json 有着 BenchmarkData[] 类型。以下是 BenchmarkData 的定义:

interface ExecTimeData {
  mean: number;
  stddev: number;
  user: number;
  system: number;
  min: number;
  max: number;
}

interface BenchmarkData {
  created_at: string;
  sha1: string;
  benchmark: {
    [key: string]: ExecTimeData;
  };
  binarySizeData: {
    [key: string]: number;
  };
  threadCountData: {
    [key: string]: number;
  };
  syscallCountData: {
    [key: string]: number;
  };
}

Deno 风格指南

目录

版权标题

存储库中的大多数模块都应具有以下版权标题:

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

如果代码来源于其他地方,请确保文件拥有适当的版权拥有者。 我们只允许 MIT、BSD 和 Apache 许可代码。

在文件名中使用下划线,而不是破折号

例如: 将文件命名为 file_server.ts 而不是 file-server.ts

为新特性添加测试

每个模块都应该包含或伴随着对其公共功能的测试。

TODO 注释

TODO 注释通常应该将一个 issue 或者作者的 github 用户名放在括号中。例如:

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

不建议使用元编程(Meta-programming),包括代理(Proxy)的使用

即使要写更多的代码,也要力求明确。

在某些情况下,使用这些技术可能是有意义的,但是在绝大多数情况下,它们是没有意义的。

Rust

遵循 Rust 约定,并与现有代码保持一致。

Typescript

代码库的 TypeScript 部分包括内置的 cli/js 和标准库 std

使用 TypeScript 而不是 JavaScript

使用术语“模块(module)”,而不是“库(library)”或“包(package)”

为了保证明确性和一致性,避免使用术语 “library” 和 “package” ,而是使用 “module” 来引用一个 JS 或 TS 文件,或者一个 TS/JS 代码目录。

不要使用 index.tsindex.js 作为文件名

Deno 不会以特殊的方式处理 “index.js” 或 “index.ts” 文件。如果使用了这些名称,就意味着当它们需要模块说明符时,可能被排除在外。这会造成误解。

如果一个代码目录需要一个默认的入口点,使用文件名 mod.ts。 文件名 mod.ts 遵循 Rust 的约定,比 index.ts 短,并且没有任何关于它如何工作的先入为主的概念。

导出函数(Exported functions): 最多 2 个参数,其余的放入一个选项对象(options object)

在设计函数接口时,请严格遵循以下规则:

  1. 若某函数是公共 API 的一部分,则其可以接受 0~2 个参数,如果必要的话,可以外加一个选项对象,因此最大总数为 3 个。

  2. 可选参数通常应放到选项对象中。

    如果只有一个可选参数,并且将来一般不会添加更多可选参数,那么该可选参数可以不放在选项对象中。

  3. 选项参数是唯一一个常规对象参数

    其他参数可以是对象,但它们在运行时必须能区别于其他一般的对象("plain" Object)。有以下两种方法进行区别:

    • 一个独特的原型(例如:ArrayMapDateclass MyThing
    • 一个众所周知的符号属性(例如 Symbol.iterator

    这允许 API 以向后兼容的方式发展,即使选项对象的位置发生了变化。

// 错误示例:可选参数不是选项对象的一部分 (#2)
export function resolve(
  hostname: string,
  family?: "ipv4" | "ipv6",
  timeout?: number
): IPAddress[] {}

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

// 错误示例:`env`可以是一个常规对象,因此无法与选项对象区分 (#3)
export function runShellWithEnv(cmdline: string, env: Environment): string {}

// 正确示例
export interface RunShellOptions {
  env: Environment;
}
export function runShellWithEnv(
  cmdline: string,
  options: RunShellOptions
): string {}
// 错误示例:多于3个参数 (#1),多个可选参数 (#2)。
export function renameSync(
  oldname: string,
  newname: string,
  replaceExisting?: boolean,
  followLinks?: boolean
) {}

// 正确示例
interface RenameOptions {
  replaceExisting?: boolean;
  followLinks?: boolean;
}
export function renameSync(
  oldname: string,
  newname: string,
  options: RenameOptions = {}
) {}
// 错误示例:参数过多 (#1)
export function pwrite(
  fd: number,
  buffer: TypedArray,
  offset: number,
  length: number,
  position: number
) {}

// 正确示例:
export interface PWrite {
  fd: number;
  buffer: TypedArray;
  offset: number;
  length: number;
  position: number;
}
export function pwrite(options: PWrite) {}

尽量降低依赖性;不要进行循环导入

尽管 cli/jsstd 没有外部依赖关系,但仍然必须注意保持内部依赖关系的简单性和可管理性。请尤为注意,不要引入循环导入。

不要连接到文件名以下划线开头的文件,如:_foo.ts

有时候可能需要一个内部模块,但是它的 API 并不稳定或者不被连接。这种情况下,在文件名前面加一个下划线。按照惯例,只有它自己目录中的文件才能导入它。

对导出的符号使用 JSDoc

我们力求文档的完整性。理想情况下,每个导出的文档符号都应该有一个文档行。

如果可能的话,最好写单行 JSDoc。例如:

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

文档易于阅读是很重要的,但是还需要提供额外的样式信息,以确保生成的文档有更丰富的含义。因此,JSDoc 通常应该遵循 markdown 标记来丰富文本。

虽然 markdown 支持 HTML 标记,但是在 JSDoc 块中是禁止的。

代码字符串文字应使用反引号(`)括起来,而不是用引号。例如:

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

不要记录函数参数,除非它们的意图不明显(当然,如果它们没有明显的意图,应该重新考虑 API 的设计)。因此,通常不应使用 @param。如果使用了 @param,则不应该包含 type ,因为 TypeScript 已经是强类型化的了。

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

应尽可能减小垂直间距。因此单行注释应写为:

/** 这样写单行 JSDoc 注释。 */

不要写为:

/**
 * 不要这样写单行 JSDoc 注释。
 */

代码示例不应使用三个反引号(```)标记。它们应该用缩进标记,要求在示例代码块前加入一个空行,并且示例代码的每一行需要有 6 个额外空格。比注释的第一列多 4 个空格。例如:

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

既然代码示例已经是一个注释了,它就不应再包含其他注释。如果它需要进一步的注释,那意味着它不是一个很好的示例。

每个模块都应该附带一个测试模块

每个带有公共功能 foo.ts 的模块都应该附带一个测试模块 foo_test.ts。由于 cli/js 模块的上下文不同,它的测试应该放在 cli/js/tests 中,或者它应只是测试模块的同级模块。

单元测试应是明确的

为了更好地理解测试,函数应该在测试命令中按照提示正确命名,如:

test myTestFunction ... ok

测试示例:

import { assertEquals } from "https://deno.land/std@v0.11/testing/asserts.ts";
import { foo } from "./mod.ts";

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

顶级函数不应使用箭头语法

顶级函数应使用 function 关键字。箭头语法应限于闭包。

错误示例:

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

正确示例:

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

std

不要依赖外部代码

https://deno.land/std/ 旨在成为所有 Deno 程序可以依赖的基础功能。我们希望向用户保证,此代码不包含任何可能未经审核的第三方代码。

文档以及维护浏览器兼容性

如果一个模块兼容浏览器,在模块顶部的 JSDoc 中包含以下声明:

/** This module is browser compatible. */

为该模块维护浏览器兼容性,在代码和测试中都不要使用 Deno 命名空间,确保任何新的依赖都兼容浏览器。

内部细节

Deno 和 Linux 类比

LinuxDeno
进程 (Processes)Web Workers
系统调用 (Syscalls)Ops
文件描述符 (fd)Resource ids (rid)
调度器 (Scheduler)Tokio
用户空间: libc++ / glib / boosthttps://deno.land/std/
/proc/$$/statDeno.metrics()
手册页 (man pages)deno types

资源 (Resources)

资源(Resources),又称 rid,是 Deno 版本的文件描述符。它们是一些整数数值,用来指代打开的文件、套接字 (sockets) 和其他概念。基于 rid,Deno 能够查询系统中有多少个打开的资源,这在测试时很有用。

const { resources, close } = Deno;
console.log(resources());
// { 0: "stdin", 1: "stdout", 2: "stderr" }
close(0);
console.log(resources());
// { 1: "stdout", 2: "stderr" }

指标 (Metrics)

指标 (Metrics) 是 Deno 用于各种统计数据的内部计数器。

> console.table(Deno.metrics())
┌──────────────────┬────────┐
│     (index)      │ Values │
├──────────────────┼────────┤
│  opsDispatched   │   9    │
│   opsCompleted   │   9    │
│ bytesSentControl │  504   │
│  bytesSentData   │   0    │
│  bytesReceived   │  856   │
└──────────────────┴────────┘

架构示意图

架构示意图

示例

在本章节,您可以找到一些示例程序,用来学习 Deno。

Unix cat

在这个程序中,每个命令行参数都是一个文件名,参数对应的文件将被依次打开,打印到标准输出流。

const filenames = Deno.args;
for (const filename of filenames) {
  const file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}

除了内核到用户空间再到内核的必要拷贝,这里的 copy() 函数不会产生额外的昂贵操作,从文件中读到的数据会原样写入标准输出流。这反映了 Deno I/O 流的通用设计目标。

尝试一下:

deno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd

文件服务器

这个示例将会启动一个本地目录的 HTTP 服务器。

安装

deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts

运行

$ file_server .
Downloading https://deno.land/std/http/file_server.ts...
[...]
HTTP server listening on http://0.0.0.0:4500/

如果想要升级到最新版本:

$ file_server --reload

TCP echo

这个示例是一个 TCP echo 服务,接收 8080 端口的连接,把接收到的任何数据返回给客户端。

const hostname = "0.0.0.0";
const port = 8080;
const listener = Deno.listen({ hostname, port });
console.log(`Listening on ${hostname}:${port}`);
for await (const conn of listener) {
  Deno.copy(conn, conn);
}

当这个程序启动时,它会抛出一个没有网络权限的错误。

$ deno run https://deno.land/std/examples/echo_server.ts
error: Uncaught PermissionDenied: network access to "0.0.0.0:8080", run again with the --allow-net flag
► $deno$/dispatch_json.ts:40:11
    at DenoError ($deno$/errors.ts:20:5)
    ...

为了安全,Deno 不允许程序访问网络,除非显式赋予权限。使用一个命令行选项来允许程序访问网络:

deno run --allow-net https://deno.land/std/examples/echo_server.ts

尝试用 netcat 向它发送数据。

$ nc localhost 8080
hello world
hello world

像示例 cat.ts 一样,copy() 函数不会产生不必要的内存拷贝。它从内核接收数据包,然后发送回去,就这么简单。

运行子进程

API 参考手册

示例:

// 创建子进程
const p = Deno.run({
  cmd: ["echo", "hello"],
});

// 等待完成
await p.status();

运行

$ deno run --allow-run ./subprocess_simple.ts
hello

window.onload 被赋值为一个函数,它将会在主脚本加载后被调用,和浏览器的 onload 一样,可以用于主入口点。

默认情况下,当您调用 Deno.run() 时,子进程将继承父进程的标准流。如果您想要和子进程通信,可以使用 "piped" 选项。

const fileNames = Deno.args;

const p = Deno.run({
  cmd: [
    "deno",
    "run",
    "--allow-read",
    "https://deno.land/std/examples/cat.ts",
    ...fileNames,
  ],
  stdout: "piped",
  stderr: "piped",
});

const { code } = await p.status();

if (code === 0) {
  const rawOutput = await p.output();
  await Deno.stdout.write(rawOutput);
} else {
  const rawError = await p.stderrOutput();
  const errorString = new TextDecoder().decode(rawError);
  console.log(errorString);
}

Deno.exit(code);

运行

$ deno run --allow-run ./subprocess.ts <somefile>
[file content]

$ deno run --allow-run ./subprocess.ts non_existent_file.md

Uncaught NotFound: No such file or directory (os error 2)
    at DenoError (deno/js/errors.ts:22:5)
    at maybeError (deno/js/errors.ts:41:12)
    at handleAsyncMsgFromRust (deno/js/dispatch.ts:27:17)

检查与放弃权限

这个程序使用了不稳定的 Deno 特性。更多信息请查阅 稳定性

有时一个程序会放弃之前获得的权限,在此之后,需要该权限的操作将失败。

// 查找一个权限
const status = await Deno.permissions.query({ name: "write" });
if (status.state !== "granted") {
  throw new Error("need write permission");
}

const log = await Deno.open("request.log", { write: true, append: true });

// 放弃一些权限
await Deno.permissions.revoke({ name: "read" });
await Deno.permissions.revoke({ name: "write" });

// 使用日志文件
const encoder = new TextEncoder();
await log.write(encoder.encode("hello\n"));

// 这将会失败
await Deno.remove("request.log");

处理系统信号

这个程序使用了不稳定的 Deno 特性。更多信息请查阅 稳定性

API 参考手册

您可以使用 Deno.signal() 函数来处理系统信号。

for await (const _ of Deno.signal(Deno.Signal.SIGINT)) {
  console.log("interrupted!");
}

Deno.signal() 也是一个 promise。

await Deno.signal(Deno.Signal.SIGINT);
console.log("interrupted!");

如果您想要停止监控信号,可以使用信号对象的 dispose() 方法。

const sig = Deno.signal(Deno.Signal.SIGINT);
setTimeout(() => {
  sig.dispose();
}, 5000);

for await (const _ of sig) {
  console.log("interrupted");
}

以上 for-await 循环将在 sig.dispose() 被调用时退出,运行时间为 5 秒。

文件系统事件

轮询文件系统事件:

const watcher = Deno.watchFs("/");
for await (const event of watcher) {
  console.log(">>>> event", event);
  // { kind: "create", paths: [ "/foo.txt" ] }
}

请注意,事件的确切顺序可能因操作系统而异。

此功能根据平台使用不同的系统调用:

  • Linux: inotify
  • macOS: FSEvents
  • Windows: ReadDirectoryChangesW

测试当前文件是否为主程序

当前脚本作为主程序的标志是 import.meta.main

if (import.meta.main) {
  console.log("main");
}