精读《Deno 1.0 你需要了解的》

1 引言

Deno 是什么?Deno 和 Node 有什么关系?Deno 和我有什么关系?

Deno 将于 2020-05-13 发布 1.0,如果你还有上面的疑惑,可以和我一起通过 Deno 1.0: What you need to know 这篇文章一起了解 Deno 基础知识。

希望你带着疑问思考,未来 10 年看今天,会不会出现 Deno 官方生态壮大,完全替代 Node 进而影响到 Web 生态的局面呢?这个思考结果会影响到你未来职业发展,你需要学会自己思考,并对这个思考结果负责。

2 介绍 & 精读

Deno 的作者是 Ryan Dahl,他是 Nodejs 背后的策划者,曾经说过 我对 Nodejs 感到遗憾的 10 件事 。这也是为什么新开一个坑的原因,但 Deno 并不定位为 Nodejs 的替代品,从整体功能来看,Deno 有更大的野心,据我的推测是想要取代现在陈旧的前后端开发模式,让 Deno 一统前后端开发全流程。

Nodejs 是由 C++ 写的,而 Deno 则是由 Rust 写的,并选择了 Tokio 这个异步编程框架,并使用 V8 引擎解析 Javascript,并内置了对 Ts 的解析。

安装

Deno 支持如下安装方式:

Shell:

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

PowerShell:

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

Homebrew:

brew install deno

Chocolatey:

choco install deno

脚本执行方式为 deno run ,可以类比为 node ,但功能不同且支持远程文件,实际上远程依赖是 Deno 的一大特色,也是有争议的地方:

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

在 ts 文件中允许用远程脚本加载资源,这个后面还会提到:

import { serve } from "https://deno.land/std@v0.42.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

安全性

Deno 是默认安全的,这体现在默认没有环境、网络访问权限、文件读写权限、运行子进程的能力。所以如果直接运行一个依赖权限的文件会报错:

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

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

可以通过参数方式允许权限的执行,有 --allow-read--allow-write--allow-net 等:

deno --allow-read=/etc

上面表示 /etc 文件夹下的文件拥有文件读权限。

除了直接加参数调用、Bash 脚本调用外,还可以用 Make 运行,或者使用类似的 drake 启动。

或者使用 deno install 命令,将脚本转化为一个快捷指令:

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

-n 表示 --name ,可以对这个脚本进行重命名,比如上面的例子中, serve 命令就等同于 deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts

标准库

Deno 在标准库上很有特点,对常用功能提供了官方版本,保证可用性与稳定性。原文中列出了一些与 Npm 三方库的对比:

从这个点上来看,Deno 既做运行环境又做基础生态,缓解了 Npm 生态下选择困难症,这件事需要辩证来看:集成了官方包对功能确定的模块来说是很有必要的,而且提高了底层库的稳定性;但 Deno 生态也有三方库,而且本质上三方库和官方库在功能上没有任何壁垒,因为实现代码都类似,唯一区别是谁能为其稳定性站台,假设微软和 Deno 同时出了基于 Npm 生态与 Deno 生态官方库,都保证会持续维护,你更相信谁呢?官方是否有优势要取决于官方自身的实力。

内置 Typescript

Deno 内置支持了 TS,因此不需要 ts-node 我们就可以用 deno run test.ts 运行 Typescript 文件。值得注意的是,Deno 内部也是利用 Typescript 引擎解析为 Js 后交由 V8 引擎解析,因此本质上没太大的变化,只是这样 Deno 的生态会更规范。

由于内置了 TS 支持,自然也不需要写 tsconfig.json 配置了,但你依然可以定制它:

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

Deno 默认还开启了 TS 严格模式,所以看到这里,可以认为 Deno 是为了构建高质量理想库而诞生的运行环境,基于已有的生态来做,但做了更多内置技术选型,这和 Facebook 的 rome 很像,但做的却更彻底。

其实从实现上来看,我们基于 Javascript 生态也能写出 deno run test.ts 这样类似的引擎,只不过是由 JS 驱动执行,可能编译还会选择 Webpack,但 Deno 本身基于 Rust 实现,并重新实现了一套模块加载标准,可以说从更底层的方式重新解读了 W3C 标准规范,以期望解决 Javascript 生态的各种痛点问题。

支持 Web 标准

Deno 还支持 W3C 标准规范,因此像 fetchsetTimeout 等 API 都可以被直接使用,如果你按照 Deno 支持的那几个函数写代码,可以保证在 Deno、Node、Web 三个平台实现跨平台运行。

虽然距离完全实现 W3C 所有标准规范还有一些路要走,但我们看到了 Deno 兼容规范的决心。

ESModule

模块化是 Deno 的亮点,Deno 使用官方 ESModule 规范,但引用路径必须加上后缀:

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

Deno 不需要申明依赖,代码的引用路径就是依赖申明,会包括完整的路径以及文件后缀,也支持网络资源,可以摆脱 NPM 中心化的包管理模式,因为这个路径可以是任何网络地址。

包管理

对于 import * as log from "https://deno.land/std/log/mod.ts"; 这行代码,Deno 会下载到一个缓存文件夹,用户不会感知到这个文件夹与这个过程的存在,也就是说,Deno 环境中是没有 node_modules 的。

也可以通过 deno --reload 的方式强制刷新缓存。

但这里也要辩证的看待 “Deno 去中心化” 这件事,虽然引用了网络源,但会引发下面几个问题:

deps.ts

即使被打上 “中心化恶人” 的 npm 也有去中心化的一面,因为 npm 支持私有化部署,无论是速度还是稳定性都可以由公司自己掌控,从稳定性来说还是 npm 拥有压倒性优势。

三方库

Deno 还有第三方库生态,截止目前共有 221 个三方库 “%5B%5D( https://deno.land/x/ )”)。

由于 Deno 走网络资源,我们可以借助 Pika 提供的 CDN 服务直接引用网络资源包:

import * as pkg from "https://cdn.pika.dev/preact@^10.3.0";

虽然这样看上去很轻量,但对公司来说还是需要自建一个 “Pika” 保障稳定性,以及做全球 CDN 缓存等的工作。

告别 package.json

npm 生态下包信息存放在 package.json ,包含但不限于下面的内容:

  • 项目元信息。
  • 项目依赖和版本号。
  • 依赖还进行分类,比如 dependenciesdevDependencies 甚至 peerDependencies
  • 标记入口, mainmodule ,还有 TS 用的 typestypings ,脚手架的 bin 等等。
  • npm scripts。

随着标准的不断更新, package.json 信息已经非常臃肿了。

对于 Deno 来说,则使用 deps.ts 集中管理依赖:

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 就是一个普通文件,只是将项目的依赖精确描述出来,这样其他地方引用 assert 时,就可以这么写了:

// import { assert } from "https://deno.land/std@v0.39.0/testing/asserts.ts";
import { assert } from "./deps.ts";

如果需要锁定依赖,可以通过 deno --lock=lock.json 方式申明。

deno doc

deno doc 命令可以根据文件按照 JS Doc 规则生成文档,同时也支持 TS 语法,比如下面这段代码:

/** Asynchronously fulfill a response with a file from the local file
 * system. */
export async function send(
  { request, response }: Context,
  path: string,
  options: SendOptions = { root: "" }
): Promise {
  // ...
}

生成文档如下:

function send(_: Context, path: string, options: SendOptions): Promise
Asynchronously fulfill a response with a file from the local file system.

deno 本身文档就是用这个命令生成的,可以 访问官方文档 查看使用效果。

内置工具链

前端 Javascript 工具链相当混乱,虽然业界已有 Umi 等框架做了开箱即用的封装,但回到 Javascript 设计的初衷就是可以在浏览器直接使用的,包括浏览器对不依赖构建工具的模块化支持,注定了未来 Webpack 一定会被消灭。

Deno 通过内置一套工具链的方式解决这个问题,包括:

  • 测试:提供 deno test 命令与 Deno.test() 测试函数。
  • 格式化:提供 vscode 插件
  • 编译:提供 deno bundle 命令。

不过值得注意的是,在最重要的编译环节, deno bundle 目前提供的能力是相对欠缺的,比如还不支持 Tree Shaking。

用 Rust 等语言提升构建效率是业界一直在尝试的事,比如 @陈成 就基于 esbuild 做了 @umijs/plugin-esbuild 插件用于提升 Umi 构建速度,但为了防止生产构建产物与 Webpack 默认规则不一致,仅使用了其压缩(minifier)功能。

对 deno 来说也一样,目前其实没有任何证据表明 deno 的构建结果可以完美适配 webpack 环境,所以请勿认为 deno 发布了 1.0 版本就等于可以在生产环境使用。

3 总结

正如原文结尾所说的,Deno 虽然将要发布 1.0 版本,但仍不能完全替代 Nodejs,这背后的原因主要是历史兼容成本,也就是完整支持整个 Node 生态不只是设计的问题,更是一个体力活,需要一个个高地去攻克。

同样 Deno 对 Web 的支持也让人耳目一新,但仍不能放到生产环境使用,除了官方和三方生态还在逐渐完善外, deno bundle 对 Tree Shaking 能力的缺失以及构建产物无法保证与现在的 Webpack 完全相同,这样会导致对稳定性要求极高的大型应用迁移成本非常高。

最亮眼的改动是模块化部分,依赖完全去中心化从长远来看是一个非常好的设计,只是基础设施和生态要达到一个较为理想的水平。

最后,让我们站在一个预言者角度思考一下 Deno 到底会不会火吧:

Deno 做的初心是做一个更好的 Node,但很不信,对于这种级别的生态底层工具来说,重新做一个并重新火起来的难度,不亚于重新做一个阿里巴巴并取代现在阿里的难度。也就是不同的时间点做同一件事,哪怕后者可以吸取教训,大概率也无法复制以前成功的路线。

从 Deno 的功能来看,解决了 Node 很多痛点,其中就包括去中心化管理,有点云开发的意思,但在 2020 年,基于 Nodejs 和 Webpack 的云开发都搞出来了,说实话是没有 Deno 什么空间的。从功能上来看,开篇就说了 Deno 基于 V8 解析 Javascript,对于性能和功能都没有革命性提升,从技术上作出突破也几乎不可能了。

Deno 的思想确实比 Node 先进,但不能说比 Node 好十倍,则无法撼动 Node 的生态,即便是 Node 作者自己可能也不行。

然而我上面说的可能都是错的。