Deno 1.0,我们需要了解的知识点

经过近两年的等待,官方正式宣布 Deno 1.0 将于 5 月 13 日发布。如今,API 已经冻结,倒计时开始。

借助创始人的大名,加之其前瞻性愿景,Deno 的发布无疑是近期最激动人心也最具争议性的 JavaScript 话题。

Deno 是通用 JavaScript/TypeScript 编程环境,集成了很多最好的开源技术,在一个小执行文件中提供了全面的解决方案。

作为 Node.js 的创始人,Ryan Dahl 又打造了 Deno。Deno 利用了 2009 年 Node.js 发布之后 JavaScript 的新增特性,同时也解决了 Ryan 在其“Node.js 十大遗憾”(演讲)中提到的设计缺陷。有人称其为 Node.js 的后续者,但作者本人并没有这么说过。

与 Node.js 使用 C++ 不同,Deno 是使用 Rust 开发的,构建在 Tokio (opens new window)(Rust 异步运行时)平台之上。但与 Node.js 类似,Deno 也使用 V8 引擎运行 JavaScript。内置 TypeScript 是 Deno 的是一个明显特征。尽管需要先编译成 JavaScript 再运行,但这个过程在内部完成,因此看起来就像 Deno 原生支持 TypeScript 一样。

上手

根据官网主页(https://deno.land/ (opens new window))的指导,可以下载 Deno。同时运行 deno upgrade 升级到新版本。

要了解 Deno 子命令,使用如下任意命令。

  • deno [subcommand] -h:显示摘要
  • deno [subcommand] --help:显示详细信息

本文将介绍 Deno 1.0 的重点特性,包括使用最新语法应用这些特性的示例。我们会尽可能使用 TypeScript,等价的 JavaScript 当然也没问题。

相信看完这篇文章你一定会喜欢上 Deno。本文将正式带领读者进入 Deno 开发的大门。

安全

Deno 默认安全。相比之下,Node.js 默认拥有访问文件系统和网络的权限。

要在没有授权的情况下运行一个需要启动子进程的程序,比如:

deno run file-needing-to-run-a-subprocess.ts

如果需要相关权限,你会看到一条警示消息:

error: Uncaught PermissionDenied: access to run a subprocess, run again with the --allow-run flag

Deno 使用命令行选项显式允许对系统不同部分的访问。最常用的包括:

  • 环境
  • 网络
  • 文件系统读/写
  • 运行子进程

要了解包含示例的全部权限,运行 deno run -h

最佳实践是对 readwritenet 使用权限白名单。这样可以更具体地指定允许 Deno 访问什么。例如,要授予 Deno 对 /etc 目录的只读权限,可以这样:

deno --allow-read=/etc

使用权限的快捷方式

每次运行应用都要授权很快就会觉得麻烦。为此,可以使用如下方法。

允许所有权限

可以使用 --allow-all 或快捷方式 -A。不推荐这么做,因为这么做就无法控制特定的权限。

写个 bash 脚本

写一个 bash 脚本来运行应用,授权该应用所需的最低权限。

#!/bin/bash
// 允许运行子进程及文件系统写权限
deno run --allow-run --allow-write mod.ts

这个方法的缺点是可能要针对运行、测试和打包都分别写一个脚本。

使用任务运行器

可以使用 GNU 工具 make 创建包含一组 Deno 命令及相关权限的文件。也可以使用特定于 Deno 的版本 Drake (opens new window)

安全可执行的 Deno 程序

使用 deno install 安装一个包含其执行所需权限的 Deno 程序 (opens new window)。安装之后,这个程序的路径就在$PATH里。

标准库

Deno 标准库 (opens new window)包含常用的模块,由 Deno 项目维护,保证可以在 Deno 中使用。标准库涵盖最常用的工具,API 风格及特性镜像了 Go 语言的标准库。

JavaScript 一直因缺少标准库而饱受诟病。用户不得不为此重复“发明轮子”,开发者经常要搜索 npm 仓库来寻找解决常见问题的模块,而这些模块本来就是应该由平台提供的。

像 React 这样解决复杂问题的第三方包另当别论,但像生成 UUID (opens new window) 这样简单的任务最好还是使用标准库来完成。这些小库可以作为更大库的组件,让开发更快、惊吓更少。不知道有多少次一个流行的库突然宣布废弃,用户只能自己维护或者再去寻找新的替代库。调查显示,常用的开源软件包中有 10-20% 已经不再积极维护 (opens new window)

内置模块及对应的 npm 包

Deno 模块 说明 npm 包
colors (opens new window) 给终端输出设置颜色 chalk, kleur, colors
datetime (opens new window) 帮助处理 JavaScript 的 Date 对象
encoding (opens new window) 增加对 base32, 二进制, csv, toml 和 yaml 等外部数据结构的支持
flags (opens new window) 帮助处理命令行参数 minimist
fs (opens new window) 帮助实现文件系统操作
http (opens new window) 支持通过 HTTP 访问本地文件 http-server
log (opens new window) 用于创建日志 winston
testing (opens new window) 用于单元测试和基准测试 chai
uuid (opens new window) 生成 UUID uuid
ws (opens new window) 帮助创建 WebSocket 客户端/服务器 Ws

内置 TypeScript

TypeScript 是 JavaScript 的超集,增加了显式类型声明。任何有效的 JavaScript 也是有效的 TypeScript,因此把你的代码转换为 TypeScript 不需要什么代价。只要把扩展名改为 .ts,然后再加上类型就可以了。

在 Deno 中使用 TypeScript,你什么也不用做。如果没有 Deno,那你必须先把 TypeScript 编译为 JavaScript,然后才能运行。Deno 内部帮你进行编译,因此让你使用 TypeScript 更容易。

使用自己的 tsconfig.json

熟悉 TypeScript 的人可能知道要使用 tsconfig.json 文件指定编译选项。但在使用 Deno 时这个文件不是必需的。因为 Deno 有自己默认的配置。如果你要使用自己的 tsconfig.json,而其中的选项与 Deno 有冲突,你会看到警示消息。

这个特性要求使用 -c 选项并指定你自己的 tsconfig.json。

deno run -c tsconfig.json [file-to-run.ts]

如果你跟多数开发者一样,那听说 Deno 默认使用 strict 模式一定会高兴。除非有人故意重写这个设置,否则 Deno 会尽其所能将代码中的草率之处报告给用户。

Deno 极力贴近 Web 标准

Web 标准的制定时间很长,一旦发布,谁也不能视而不见。虽然各种框架你方唱罢我登场,但 Web 标准则始终如一。在学习 Web 标准上花费的时间永远不会浪费,因为没有人胆敢推翻 Web。在可以预见的未来几十年,甚至到你职业生涯的终点,Web 仍将继续存在和发展。

fetch 是用于获取资源的 Web API。浏览器中有一个 JavaScript 方法叫 fetch()。如果你想在 Node.js 中使用这个标准 API,需要依赖第三方的 Node Fetch (opens new window)。而在 Deno 中,这个 API 是内置的,就像浏览器中的版本一样,开箱即用。

Deno 1.0 提供以下兼容 Web 的 API。

  • addEventListener
  • atob
  • btoa
  • clearInterval
  • clearTimeout
  • dispatchEvent
  • fetch
  • queueMicrotask
  • removeEventListener
  • setInterval
  • setTimeout
  • AbortSignal
  • Blob
  • File
  • FormData
  • Headers
  • ReadableStream
  • Request
  • Response
  • URL
  • URLSearchParams
  • console
  • isConsoleInstance
  • location
  • onload
  • onunload
  • self
  • window
  • AbortController
  • CustomEvent
  • DOMException
  • ErrorEvent
  • Event
  • EventTarget
  • MessageEvent
  • TextDecoder
  • TextEncoder
  • Worker
  • ImportMeta
  • Location

如上 API 都在程序的顶级作用域。这意味着如果你不使用 Deno() 命名空间中的任何方法,你的代码应该同时可以在 Deno 和浏览器中运行。虽然 Deno 的这些 API 并不是 100% 符合 Web 标准,但这对前端开发者依然重大利好。

ECMAScript 模块

Deno 相比于 Node.js 的一个主要变化是使用了正式的 ECMAScript 模块标准,而不是老旧的 CommonJS。Node.js 直到 2019 年底才在 13.2.0 中支持 ECMAScript 模块,即便如此支持仍不完善,并且还需要包含有争议的.mjs 扩展名。

Deno 通过在其模块系统中拥抱现代 Web 标准与过去挥手作别。模块可以使用 URL 或者包含强制扩展名的文件路径来引用。例如:

import * as log from 'https://deno.land/std/log/mod.ts';
import { outputToConsole } from './view.ts';

使用扩展名的问题

Deno 希望模块包含文件扩展名,但 TypeScript 不希望如此:

使用扩展名符合逻辑,也是一种显而易见的方式。可惜现实总比理想要复杂。目前为止,可以使用 Visual Studio Code Deno 扩展 (opens new window)在 Deno 项目中解决这个问题。

TypeScript 创始人似乎对这个问题有自己的看法。在最终抛弃 CommonJS 之前,我认为这个问题不会有简单的解决方案。

对于睿智但上了点年纪的编程大神们,我们还需要多一点耐心。但愿他们早日摒弃这些过气的格式,对那些死抓住它们不放而伤害我们的家伙降下惩罚。

包管理

Deno 的包管理方式已经发生了天翻地覆的变化。不再依赖中心化的仓库,Deno 的包管理以去中心化为特色。任何人可以像在 Web 上托管任何类型的文件一样托管一个包。

像 npm 这样的中心化仓库有好处也有不足,这方面也是 Deno 饱受争议之处。

Deno 新的包管理机制

导入一个包变得如此简单可能会吓到你。

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

下面我们来分析一下变化。

  • 不再有中心化的包管理器,而是直接从 Web 上导入 ECMAScript 模块。
  • 不再有“魔法般”的 Node.js 模块解析。现在,直观的语法更容易定位来源。
  • 不再有 node_modules 目录。相反,依赖下载后会藏身于你的硬盘,你看不到。如果想刷新缓存再次下载,只要在命令后面加上 --reload

如果想把依赖下载到项目代码附近而不是使用全局缓存,可以使用 $DENO_DIR 环境变量。

查找兼容的第三方库

目前有一个存放兼容 Deno 的第三方库的用户区域 (opens new window),但导航设计很简陋。例如,不能按照流行度或下载量搜索。预计这个用户区域要么会被扩展,要么会出现替代性的网站,用于托管社区贡献的第三方模块。

虽然官方没有支持向后兼容 Node.js,但仍然有很多库和应用可以在 Deno 下使用。有些可以开箱即用,有些则需要一些调整才能用。

库类型 兼容性
在浏览器中运行
使用 ESM 语法
应该可以开箱即用,试试 Pika CDN (opens new window)
在浏览器中运行
使用 CommonJS 语法
使用 jspm.io (opens new window) 用 ESM 语法来封装
不在浏览器中运行
不使用 Node.js API
使用 jspm.io 用 ESM 语法来封装
使用 Node.js API 可能无法使用,不过可以试试这个官方针对 Node.js 的兼容层 (opens new window)

安装第三方模块

Deno 仍然非常新,周围生态还有待完善。在本文写作时,我推荐在标准库和用户库之后,把 Pika (opens new window) 作为搜索兼容模块的首选。

Pika 的开发者已经针对 Deno 通过 ECMAScript 提供了 TypeScript 类型,叫 X-TypeScript-Types (opens new window)

超越 package.json

JavaScript 生态的主要依赖关系还是依托 package.json。这个文件已经膨胀到身兼数职,比如:

  • 保存项目元数据
  • 列出项目带版本的依赖
  • 将依赖区分为 dependencies 和 devDependencies
  • 定义程序的入口
  • 存储与项目相关的终端脚本
  • 定义 type,是最近为改进对 ECMAScript 模块的支持而新增的
{
  name: 'Project Name', // 元数据
  version: '1.0.0', //元数据
  description: 'My application', // 元数据
  type: 'module', // 模块功能
  main: 'src/mod.ts', // 模块功能
  scripts: {
    build: 'npm run _copy-build-files && rollup -c',
    'build-watch': 'npm run _copy-build-files && rollup -cw',
  }, // 脚本功能
  license: 'gpl-3.0', // 元数据
  devDependencies: {
    '@rollup/plugin-typescript': '^3.1.1',
    rollup: '^1.32.1',
    typescript: '^3.8.3',
  }, // 版本及分类功能
  dependencies: {
    tplant: '^2.3.3',
  }, // 版本及分类功能
}

所有这些功能都是随时间推移而增加的,现在已经成为 JavaScript 生态运行的标准方式。很多人都会忘记这并不是一个正式标准,而只会在这些功能必要时才会想起来。既然 JavaScript 这么流行,这件事就该好好从头想一想。

Deno 还不能取代 package.json 的全部功能,但眼下也有一些解决方案。

使用 deps.ts 和 URL 管理版本

Deno 有一个管理包版本的惯例,即使用一个特殊文件 deps.ts。在这个文件里,依赖会被再次导出。这样应用中的不同模块可以引用相同的出处。

与告诉 npm 要下载模块的哪个版本不同,deps.ts 将版本放到 URL 中:

export { assert } from 'https://deno.land/std@v0.39.0/testing/asserts.ts';
export { green, bold } from 'https://deno.land/std@v0.39.0/fmt/colors.ts';

如果想更新某个模块,可以修改 deps.ts 中的 URL。例如,把 @v0.39.0 替换成 @v0.41.0,这样其他地方就都会使用新版本了。如果你直接在每个模块中导入 https://deno.land/std@v0.39.0/fmt/colors.ts ,那么就得搜索整个应用,逐一替换。

假设以前下载的模块不会被以后下载的模影响也存在安全风险。这也是为什么有一个选项用于创建锁文件 (opens new window)的原因。这样可以保证新下载的模块与最初下载的模块相同。

deno doc与对元数据使用 JSDoc

JSDoc 发布于 1999 年,21 年前。它是目前使用和支持最多的 JavaScript 和 TypeScript 文档方式。虽然不是正式 Web 标准,但 JSDoc 是 package.json 中所有元数据的完美替代方案。

/**
 * @file Manages the configuration settings for the widget
 * @author Lucio Fulci
 * @copyright 2020 Intervision
 * @license gpl-3.0
 * @version 1.0
 */

Deno 内置支持 JSDoc 并使用它构建文档系统。虽然目前尚未使用类似上面的元数据,但 deno doc 会读取函数及其参数的描述。

/**
 * Returns a value of (true?) if the rule is to be included
 *
 * @param key Current key name of rule being checked
 * @param val Current value of rule being checked
 **/

可以使用 deno doc <filename> 来查询你的程序文档。

deno doc mod.ts
function rulesToRemove(key: string, val: any[]): boolean
  Returns a value of if the rule is to be included

如果你的程序是在线托管的,可以使用在线文档查看器 (opens new window)

Deno 的内置工具

这是对前端开发者影响最大的一个领域。JavaScript 工具目前的情形可以说是相当地乱。再加上 TypeScript 的工具,复杂性会进一步增加。

JavaScript 本身是不是需要编译的,因此可以直接在浏览器中运行。这样可以很快知道自己的代码是否存在问题。总之门槛非常低,只需要一个文本编辑器和一个浏览器。

不幸的是,这种简单性和低门槛已经被一种叫做极度工具崇拜的东西在不知不觉间破坏了。结果 JavaScript 开发变成一个复杂的噩梦。我曾经完整地学习过一个讲解如何配置 Webpack 的课程。人生苦短,这种没意义的生活该结束了。

工具之乱已经让很多人急切想回归真正写代码的状态,而不是摆弄配置文件或者因为要在不同的竞争性标准中做出选择而苦恼。Facebook 的 Rome (opens new window) 是一个为解决这个问题而出现的项目。在本文写作时,这个项目还处于幼年期。虽然这个项目是有益的,但 Deno 应该是一个更本质的解决方案。

Deno 本身是一个完整的生态,包含运行时及自己的模块/包管理系统。这就决定了它自己内置的工具会有更广泛的应用范围。下面我们就来介绍 Deno 中内置的工具,以及如何利用它们减少对第三方库的依赖和简化开发。

当然,目前 Deno 还不可能取代整个前端构建工具链,但我们离这一天应该不远了。

测试

测试运行器以 Deno.test() 函数的形式内置于 Deno 的核心,而断言库 (opens new window)也包含在标准库中。你喜欢的 assertEquals()assertStrictEq() 一个也不少,此外还包含一些不太常见的断言,如 assertThrowsAsync()

在本文写作时,没有测试覆盖功能。另外,监控模式也需要使用 Denon (opens new window) 等第三方工具来设置。

要了解测试运行器的全部选项,使用 deno test --help。虽然还很有限,但或许包含很多某些你熟悉的程序如 Mocha 中的特性。例如,--failfast 会在遇到第一个错误时停止,而 --filter 可用于过滤要运行的测试。

使用测试运行器

最基本的语法是 deno test。这个命令会运行工作目录中所有以 _test.test 结尾且扩展名为 .js.ts.jsx.tsx 的文件(如 example_test.ts)。

import { assertEquals } from 'https://deno.land/std/testing/asserts.ts';
Deno.test({
  name: 'testing example',
  fn(): void {
    assertEquals('world', 'world');
    assertEquals({ hello: 'world' }, { hello: 'world' });
  },
});

如果你的代码使用了 DOM,那么需要提供自己的 tsconfig.json 文件,包含 lib: ["dom", "esnext"]。下面会介绍细节。

格式化

格式化基于 dprint (opens new window),是一个 Prettier 替代库,照搬了 Prettier 2.0 所有得到认可的规则。

要格式化文件,可以使用 deno fmt <files> 或者 Visual Studio Code 扩展 (opens new window),后面会介绍。

编译与打包

Deno 可以通过命令行 deno bundle 创建简单的包,但它也暴露了内部编译器 API (opens new window),因此用户可以控制自己的输出,有时候可以为在前端使用而自定义。这个 API 当前被标记为不稳定,所以需要使用 --unstable 标签。

Deno 虽然有一些兼容 Web 的 API,但并不完整。如果想编译引用 DOM 的前端 TypeScript,需要在编译或打包时告诉 Deno 相关的类型。可以使用编译器 API 选项 lib

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1 id="greeter">Replace me</h1>
  </body>
</html>

test-dom.ts

let greeter: HTMLElement | null = document.getElementById('greeter')!; // Please forgive the Non-Null Assertion Operator
greeter.innerText = 'Hello world!';

compile.ts

const [errors, emitted] = await Deno.compile('test-dom.ts', undefined, {
  lib: ['dom', 'esnext'], // include "deno.ns" for deno namespace
  outDir: 'dist',
});
if (errors) {
  console.log('There was an error:');
  console.error(errors);
} else {
  console.log(emitted); // normally we would write the file
}

下面是终端中打印出来的结果。

{
 dist/test-dom.js.map: "{\"version\":3,\"file\":\"test-dom.js\",\"sourceRoot\":\"\",\"sources\":[\"file:///home/david/Downloads/deno-arti...",
 dist/test-dom.js: "\"use strict\";\nlet greeter = document.getElementById(\"greeter\");\ngreeter.innerText = \"Hello world!\";\n..."
}

在上面的例子中,我们编译了引用 DOM 的 test-dom.ts 文件。在 Deno.compile()lib 选项中覆盖了 Deno 默认的 lib 值,因此需要也加上 esnext。此外要使用 Deno 命名空间,也可以选择加上 deno.ns

这还是有点实验性的,但我希望 bundle 命令能够发展到可以实现类似摇树清冗(tree shaking)的功能,类似 Rollup.js 那样。

调试

Dene 内置了调试功能,但在本文写作时,Visual Studio Code 扩展还不支持它。要调试,需要手工执行如下操作。

  • deno run -A --inspect-brk fileToDebug.ts(注意:对你的模块只授权最低权限)
  • 在 Chrome 或 Chromium 中打开chrome://inspect,之后会看到类似下面的视图:
  • 单击”inspect“连接并开始调试代码。

文件监控

Deno 内置了基于 Rust notify (opens new window) 的文件监控功能,通过 Deno.watchFs() 来使用。Deno 喜欢在后台暴露强大的 API,让用户自己按喜好实现自己的代码。因此没有 --watch 标记,而是需要创建自己的实现或使用一个第三方模块。

编写自己的文件监控器,唯一有点难度的是消除抖动。这个 API 可能连续触发很多事件,而我们可能并不希望多次执行某个操作。Github 用户 Caesar2011 使用 Date.now()23 行 TypeScript 代码 (opens new window)就解决了这个问题。

还有一个更高级的 Deno 文件监控工具叫 Denon (opens new window),相当于 nodemon。如果你想监控工作空间的变化并重新运行测试,只要执行下面的命令:

denon test

Visual Studio Code 插件

axetroy 在 Visual Studio Market Place (opens new window) 发布的插件是目前最好的扩展。安装以后,在项目目录下创建一个 .vscode/settings.json 文件,然后就可以在每个项目中独立启用这个扩展。

// .vscode/settings.json
{
  'deno.enable': true,
}

之后就可以使用包括智能感知在内的所有编码辅助功能了。

小结

JavaScript 生态的快速发展本身有好有坏。从积极方面看,从来没有出现过那么多高质量的工具。从消极方面说,各种框架和库无休止地密集出现很容易让开发者怨声载道,甚至怀疑人生。

Deno 成功避免了很多 JavaScript 开发的缺点。下面只列举几点:

  • 通过使用 Web 标准,Deno 让自己的 API 更加面向未来。同时,这也让开发者信心大增,不必再浪费时间去学习那些很快过时的东西。
  • 以 TypeScript 加强 JavaScript 同时去掉编译负担,实现了更紧密的集成。
  • 内置工具意味着常见功能开箱即用,用着不再浪费时间去搜索。
  • 去中心化的包管理将用户从 npm 中解放出来,而 ECMAScript 模块系统相比于老旧的 CommonJS 也让人眼前一亮。

虽然还不能完全取代 Node.js,但 Deno 已经成为可以日常使用的一个出色的编程环境。

相关文章