Deno 是一个类似于 Node.js 的 JavaScript 和 TypeScript 运行时,基于 Rust 和 JavaScript V8 引擎构建。它由 Node.js 创始人 Ryan Dahl 创建,以弥补他在 2009 年最初设计和发布 Node.js 时所犯的错误。
Ryan's 有关 Node.js 的遗憾在 2018 年 JSConf EU 上著名的演讲 “我对 Node.js 遗憾的十件事” 有充分的记录。总而言之,他感叹缺乏对安全性的关注、通过 node_modules 解析模块、浏览器工作方式的各种偏差以及其他问题,他开始在 Deno 中修复这些错误。
在本文中,我们将讨论 Deno 的创建原因以及它与 Node.js 相比的优缺点。还将对 Deno 的怪癖(quirks)和功能做一个实用概述,以便您决定它是否适合于您的下一个项目。
本文主题
安装 Deno
Deno 做为一个独立的、自包含的二进制文件没有任何依赖。你可以通过多种方式安装 Deno,具体取决于你的操作系统。最简单的方法是下载并执行一个 shell 脚本,如下所示:
# Linux and macOS $ curl -fsSL https://deno.land/x/install/install.sh | sh # Windows PowerShell $ iwr https://deno.land/x/install/install.ps1 -useb | iex
一旦你为你的操作系统执行了适当的命令,Deno CLI 二进制文件将会下载到你的计算机。根据你选择的安装方法,你需要添加这个二进制文件位置到你的 PATH。
你可以在 Bash 中添加以下行到你的 $HOME/bash_profile 文件来执行此操作。你可能需要开启一个新的 shell 会话来使更改生效。
export DENO_INSTALL="$HOME/.deno" export PATH="$DENO_INSTALL/bin:$PATH"
运行以下命令,验证 Deno 的安装版本。如果这个 CLI 已经下载成功并且添加到您的 PATH,控制台应该会打印输出 Deno 版本信息。
$ deno --version deno 1.14.2 (release, x86_64-unknown-linux-gnu) v8 9.4.146.16 typescript 4.4.2
如果你的 Deno 版本已过时,可以通过 upgrade 子命令升级到最新的 release 版本。
$ deno upgrade Looking up latest version Found latest version 1.14.2 Checking https://github.com/denoland/deno/releases/download/v1.14.2/deno-x86_64-unknown-linux-gnu.zip 31.5 MiB / 31.5 MiB (100.0%) Deno is upgrading to version 1.14.2 Archive: /tmp/.tmpfdtMXE/deno.zip inflating: deno Upgraded successfully
接下来,编写一个常用的 Hello world 程序来验证是否可以正常工作。可以为你的 Deno 项目创建一个目录并在该目录下创建一个 index.ts 文件放入以下代码:
function hello(str: string) { return `Hello ${str}!`; } console.log(hello("Deno"));
在 deno run 子命令后将文件名做为参数执行,如果输出 “Hello Deno!”,意味着你已经成功的安装了 Deno。
$ deno run index.ts Check file:///home/ayo/dev/deno/index.ts Hello Deno!
要了解 Deno CLI 提供的其它功能及选项,使用 --help 标志:
$ deno --help
一流的 TypeScript 支持
Deno 相较于 Node.js 的一大卖点是它对 TypeScript 的一流支持。
正如你所看到的,除了安装 Deno CLI 之外,你不需要做任何的事情让它来工作。与其前身一样,使用 V8 引擎运行时来解析解释和执行 JavaScript 代码,但它还在其可执行文件中使用 TypeScript 编译器以实现对 TypeScript 的支持。
在后台,TypeScript 代码被检查和编译。生成的 JavaScript 代码会缓存在文件系统上的一个目录中,可以再次执行而无需从头编译。你可以使用 deno info 检查缓存目录位置和其它包含 Deno 管理文件的目录。
在使用 TypeScript 时 Deno 不需要任何配置,但如果你想调整 TypeScript 编译器解析代码的方式,你可以提供一个 JSON 文件指定 TypeScript 的编译选项。尽管 tsconfig.json 是用 TSC 编译时的一个常见选择方式,但是 Deno 团队建议使用 deno.json 因为其它特定于 Deno 的配置选项也可以放置在这里。
请注意 Deno 不支持所有的 TypeScript 编译选项。在 Deno 的文档中提供了一个完整的可用的的选项和它们的默认值。对于 Deno 的示例配置文件如下所示:
{ "compilerOptions": { "checkJs": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "noUncheckedIndexedAccess": true } }
在撰写本文时,Deno 不会自动检测 deno.json 文件,因此必须通过 --config 标志指定它。然后,这个功能计划在未来的一个版本发布。
$ deno run --config deno.json index.ts
当 Deno CLI 遇到类型错误时,它会停止脚本编译并以非 0 状态码退出终止。你可以通过以下方式绕错错误:
在错误发生的地方使用 //@ts-ignore 或 //@ts-expect-error // @ts-nocheck 在一个文件的开始忽略所有的错误。
Deno 还提供了一个 --no-check 标志来完全禁用类型检查。这有助于防止 TypeScript 编译器在快速迭代问题时减慢你的速度。
$ deno run --no-check index.ts
权限功能
Deno 以成为 JavaScript 和 TypeScript 的安全运行时而骄傲。它维护安全性部分的方式是通过权限功能完成的。为了演示权限功能在 Deno 中的工作方式,添加以下脚本到您的 index.ts 文件。这个脚本会从 disease.sh 获取最新的全球 Covid-19 统计数据。
async function getCovidStats() { try { const response = await fetch("https://disease.sh/v3/covid-19/all"); const data = await response.json(); console.table(data); } catch (err) { console.error(err); } } getCovidStats();
当你尝试执行脚本时,它应该显示 PermissionDenied 错误:
$ deno run index.ts PermissionDenied: Requires net access to "disease.sh", run again with the --allow-net flag # PermissionDenied:需要对 “disease.sh” 的网络访问,使用 --allow-net 标志再次运行。
上面的错误消息表明脚本没有给予网络访问权限,它建议在命令中包含 --allow-net 标志以授予访问权限。
$ deno run --allow-net index.ts ┌────────────────────────┬───────────────┐ │ (idx) │ Values │ ├────────────────────────┼───────────────┤ │ updated │ 1633335683059 │ │ cases │ 235736138 │ │ todayCases │ 32766 │ │ deaths │ 4816283 │ │ todayDeaths │ 670 │ │ recovered │ 212616434 │ │ todayRecovered │ 51546 │ │ active │ 18303421 │ │ critical │ 86856 │ │ casesPerOneMillion │ 30243 │ │ deathsPerOneMillion │ 617.9 │ │ tests │ 3716763329 │ │ testsPerOneMillion │ 473234.63 │ │ population │ 7853954694 │ │ oneCasePerPeople │ 0 │ │ oneDeathPerPeople │ 0 │ │ oneTestPerPeople │ 0 │ │ activePerOneMillion │ 2330.47 │ │ recoveredPerOneMillion │ 27071.26 │ │ criticalPerOneMillion │ 11.06 │ │ affectedCountries │ 223 │ └────────────────────────┴───────────────┘
你可以提供一个以逗号分隔的主机名或 IP 地址的允许列表做为 --allow-net的参数,以便脚本仅可以访问指定的网站,而不是授予脚本访问所有网站的权限(如上所示)。如果脚本尝试链接不在允许列表中的列,Deno 将会阻止它的链接,并且脚本将会执行失败。
$ deno run --allow-net='disease.sh' index.ts
这个功能是 Deno 对 Node.js 的改进之一,任何脚本都可以通过网络访问任何资源。读取和写入文件系统也存在类似的权限。如果一个脚本需要执行任一任务,你需要分别指定 --allow-read 和 --allow-write 权限。两个标志允许你为脚本设置特定可访问的目录,以便文件系统的其它部分免受篡改。Deno 还提供了一个 --allow-all 标志,如果需要,可以为一个脚本开启所有的安全敏感功能。
与浏览器 API 的兼容性
Deno 的主要目标之一是尽可能与 Web 浏览器兼容。这反映在它使用 Web 平台 API,而不是为某些操作创建特定于 Deno 的 API。例如,我们在上一节中看到了 Fetch API 的实际应用。这是一个在浏览器中使用的确切 Fetch API,在必要时有一些偏差以说明 Deno 中独特的安全模型(这些更改大多无关紧要)。
Deno 的在线文档中列出了所有已实现的浏览器 API。
依赖管理
Deno 管理依赖方式可能是它与 Node.js 的最大不同之处。
Node.js 使用 npm 或 yarn 之类的包管理器将第三方包从 npm 注册表下载到 node_modules 目录和 package.json 文件以跟踪项目的依赖关系。Deno 摒弃了这些机制,转而采用更加以浏览器为中心的方式来使用第三方包:URLs。
这里有一个使用 Deno web 应用程序框架 Oka 来创建基础 web server 的示例:
import { Application } from "https://deno.land/x/oak/mod.ts"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello Deno!"; }); app.addEventListener("listen", ({ hostname, port, secure }) => { console.log(`Listening on: http://localhost:${port}`); }); await app.listen({ port: 8000 });
Deno 使用 ES 模块,与 Web 浏览器中使用的模块系统相同。只要引用的脚本(模块)导出方法或其它值,就可以从绝对路径或相对路径导入模块。值得注意的是,文件扩展名必须始终存在,无论您是从绝对路径还是相对路径导入。
虽然您可以从任何 URL 导入模块,但许多专门为 Deno 构建的第三方模块都通过 **deno.land/x** 进行缓存。每次发布模块的新版本时,它都会自动缓存在该位置并使其不可变,以便模块的特定版本内容保持不变。
假设您运行上一个片段中的代码。在这种情况下,它将下载模块及其所有依赖项并将它们缓存在本地 DENO_DIR 环境变量指定的目录中(默认应为 $HOME/.cache/deno)。下次程序运行时,将不会再次下载,因为所有依赖项都已缓存在本地。这类似于 Go 模块系统的工作方式。
$ deno run --allow-net index.ts Download https://deno.land/x/oak/mod.ts Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts Download https://deno.land/x/oak@v9.0.1/mod.ts
对于生产应用程序,Deno 创建者建议检查您的依赖性已进入代码版本控制系统,以确保持续可用(即时模块的源代码不可用,无论处于何原因)。将 DENO_DIR 环境变量指向项目中的本地目录(例如 vendor),您可以将其提交给 Git。
例如,下面的命令会将脚本的所有依赖项下载到项目中的 vendor 目录中。随后,您可以提交该文件夹以在您的生产服务器中一次将其全部拉下。您还需要将 DENO_DIR 变量设置为从服务器上的 vendor 目录中读取,而不是重新下载它们。
$ DENO_DIR=$PWD/vendor deno cache index.ts # Linux and macOS $ $env:DENO_DIR="$(get-location)\vendor"; deno cache index.ts # Windows PowerShell
Deno 还支持对依赖项进行版本控制,以确保可重现的构建。目前,我们已经从 https://deno.land/x/oak/mod.ts 导入了 Oak。这始终会下载最新版本,将来可能与您的程序不兼容。当你第一次下载模块时,它还会导致 Deno 产生警告:
Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts
最佳实践是引用一个特定版本,如下所示:
import { Application } from 'https://deno.land/x/oak@v9.0.1/mod.ts';
如果您在代码库的许多文件中引用一个模块,升级它可能会变得乏味,因为您必须在许多地方更新 URL。为了规避这个问题,Deno 团队建议将您的外部依赖项导入到一个集中的 deps.ts 文件中,然后重新导出它们。这是一个 deps.ts 示例文件,它从 Oak 库中导出我们需要的内容。
export { Application, Router } from "https://deno.land/x/oak@v9.0.1/mod.ts";
然后在您的应用程序代码中,您可以按如下方式导入它们:
import { Application, Router } from "./deps.ts";
此时,更新一个模块变得很简单,只需将 deps.ts 文件中的 URL 更改为指向新版本即可。
标准库
Deno 提供了标准库(stdlib)旨在成为 Go 标准库的松散端口(译者注:这里原句为 “aims to be a loose port of Go's standard library”)。标准中包含的模块由 Deno 团队审核并随着每一次 Deno release 而更新。提供标准库的目的是让您立即创建有用的 Web 应用程序,而不需要任何第三方包(这是 Node.js 生态系统中的规范)。
你可能会发现一些有帮助的第三方模块,示例如下:
- HTTP:Deno 的 HTTP 客户端和服务端实现。
- Fmt:包括用于打印格式化输出的助手。
- Testing:提供了用于代码测试和基准测试的基本实用工具。
- FS:文件系统。
- Encoding:提供处理各种文件格式的助手,例如 XML、CSV、base64、YAML、二进制等。
- Node: Node.js 标准可兼容层。
这是一个示例(取自 Deno 官方文档)它取自 Deno 标准库中的 HTTP 模块用于创建一个基本的 Web Server。
import { listenAndServe } from "https://deno.land/std@0.109.0/http/server.ts"; const addr = ":8080"; const handler = (request: Request): Response => { let body = "Your user-agent is:\n\n"; body += request.headers.get("user-agent") || "Unknown"; return new Response(body, { status: 200 }); }; console.log(`HTTP webserver running. Access it at: http://localhost:8080/`); await listenAndServe(addr, handler);
通过以下命令开启这个服务:
$ deno run --allow-net index.ts HTTP webserver running. Access it at: http://localhost:8080/
在不同终端,通过以下命令访问这个正在运行的服务。
$ curl http://localhost:8080 Your user-agent is: curl/7.68.0
请注意,标准库中的模块当前标记为不稳定(如版本号所示)。这意味着您不应该仅仅依赖它们来进行严肃的生产应用程序。
在 Deno 中使用 NPM
不可否认 Node.js 成功的主要原因是可以在项目下载使用大量的 NPM 包。如果你正在考虑切换到 Deno,你也许想知道是否要放弃所有你知道且喜爱的 NPM 包。
最简单的答案是 “不”。如果某些 NPM 包依赖于 Node.js API,您可能无法在 Deno 中使用它们(特别是如果 Deno 的 Node.js 兼容层不支持特定 API),但许多 NPM 包可以通过 CDN 在 Deno 中使用,例如 esm.sh 和 skypack.dev。这两个 CDN 都将 NPM 包作为 ES 模块提供,即使包的作者没有专门针对 Deno 进行设计,也可以随后在 Deno 脚本中使用这些包。
这是一个在 Deno 脚本中从 Skypack 到入 NPM 包的示例:
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7"; console.log(`Today is: ${dayjs().format("MMMM DD, YYYY")}`);
$ deno run index.ts Today is: October 05, 2021
为确保 Deno 可以发现与包关联的类型,请确保在使用 Skypack CDN 时在包 URL 的末尾添加 ?dts后缀。这会使 Skypack 设置 X-TypeScript-Types 标头,以便 Deno 可以自动发现与包关联的类型。Esm.sh 默认包含此标头,但您可以通过在包 URL 末尾添加 ?no-check 后缀来选择退出。
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7?dts";
工具
Deno CLI 附带了几个有价值的工具,可以提高开发人员的使用体验。与 Node.js 一样,它带有一个 REPL(交互式解析器),您可以使用 deno repl 访问它。
$ deno repl Deno 1.14.2 exit using ctrl+d or close() > 2+2 4
它还有一个内置的文件观察器,可以与它的几个子命令一起使用。例如,您可以使用 --watch 标志将 deno run 配置为在文件更改后自动重建和重新启动程序。在 Node.js 中,这个功能一般是通过一些第三方包来实现的,比如 nodemon。
$ deno run --allow-net --watch index.ts HTTP webserver running. Access it at: http://localhost:8080/ Watcher File change detected! Restarting! HTTP webserver running. Access it at: http://localhost:8080/
使用 Deno 1.6,您可以通过 compile 子命令将脚本编译为不需要安装 Deno 的独立可执行文件(您可以在 Node.js 中使用 pkg 执行相同操作)。您还可以通过--target 标志为其他平台(交叉编译)生成可执行文件。编译脚本时,您必须指定其运行所需的权限。
$ deno compile --allow-net --output server index.ts $ ./server HTTP webserver running. Access it at: http://localhost:8080/
请注意,通过此过程生成的二进制文件非常庞大。在我的测试中,deno compile 为一个简单的 “Hello world” 程序生成了一个 83MB 的二进制文件。然而,Deno 团队目前正在努力减少文件大小以使其更易于管理。
发布 Deno 程序的另一种方法是通过 bundle 子命令将其打包到单个 JavaScript 文件中。该文件包含程序的源代码及其所有依赖项,可以通过 deno run 执行,如下所示:
$ deno bundle index.ts index.bundle.js Check file:///home/ayo/dev/demo/deno/index.js Bundle file:///home/ayo/dev/demo/deno/index.js Emit "index.bundle.js" (7.39KB) $ deno run --allow-net index.bundle.js HTTP webserver running. Access it at: http://localhost:8080/
Deno 附带的另外两个很棒的工具是内置的 linter (deno lint) 和 formatter (deno fmt)。在 Node.js 生态系统中,linting 和格式化代码通常分别由 ESLint 和 Prettier 处理。
使用 Deno 时,您不再需要安装任何东西或编写配置文件来获取 JavaScript、TypeScript 和其他支持的文件格式的 linting 和格式化。
单元测试
Deno 内置了对 JavaScript 和 TypeScript 代码的单元测试支持。当您运行 deno test 时,它会自动检测任何以 _test.ts 或 .test.ts 结尾的文件(也支持其他文件扩展名)并在其中执行任何定义的测试。
要编写您的第一个测试,请创建一个 index_test.ts 文件并使用以下代码填充它:
import { assertEquals } from "https://deno.land/std@0.109.0/testing/asserts.ts"; Deno.test("Multiply two numbers", () => { const ans = 2 * 2; assertEquals(ans, 4); });
Deno 提供了用于创建单元测试的 Deno.test 方法。它将测试的名称作为其第一个参数。它的第二个参数是测试运行时执行的函数。
还有第二种风格接受一个对象而不是两个参数。除了测试名称和功能之外,它还支持其他属性来配置测试是否应该运行或如何运行。
Deno.test({ name: "Multiply two numbers", fn() { const ans = 2 * 2; assertEquals(ans, 4); }, });
assertEquals() 方法来自标准库中的测试模块,它提供了一种轻松检查两个值是否相等的方法。
继续运行测试:
$ deno test test Multiply two numbers ... ok (8ms) test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (37ms)
Deno 语言服务协议
选择编程语言或环境的主要考虑因素之一是它与编辑器和 IDE 的集成。在 Deno 1.6 中,内置语言服务协议(deno lsp) 被添加到运行时以提供以下功能:
- 代码自动补全
- 跳转到定义处
- 代码 lint 和格式化集成
以及任何支持语言服务协议 (LSP) 的编辑器的其他语言智能。你可以在 Deno 的在线文档中了解有关在编辑器中设置 Deno 支持的更多信息。
总结:Deno 还是 Node.js ?
在本文中,我们考虑了 Deno 运行时的许多方面,以及它是对 Node.js 的升级的方式。
关于 Deno 及其生态系统还有很多话要说,但对于考虑将 Deno 用于新项目的 Node.js 开发人员来说,这应该是一个有用的介绍。Deno 第三方软件包的可用性较低是它明显不足的一个方面,因为它在现实世界中由于年龄太小而没有像 Node.js 那样经过实战测试(Deno 1.0 于 2020 年 5 月发布)。
比较 Node.js 和 Deno 之间的性能可以发现,在大多数情况下,它们在同一个范围内,尽管在少数情况下 Node.js 表现出更出色的性能。随着 Deno 变得更加成熟,所测得的差异势必会有所改善。
在选择 Node.js 和 Deno 时,同样重要的是要记住,Deno 提供的一些好处也可以在第三方包的帮助下在 Node.js 中使用。因此,如果您对 Deno 只有一两点欣赏,那么您很可能在 Node.js 中也可获得类似的结果,尽管不是那么无缝。
译者:五月君
作者:ayooluwa-isaiah
https://blog.appsignal.com/2022/02/09/an-introduction-to-deno-is-it-better-than-nodejs.html
原文地址:https://mp.weixin.qq.com/s/FUIaBBv5-lc6OZcFDbc9Cg