大纲
- 什么是Deno?
- 为什么不继续使用Node?
- Node和Deno的区别在哪里?
- Deno是如何设计的?
什么是Deno?
Deno 介绍
Deno是基于Google V8引擎构建的安全的JavaScript, TypeScript(以及WebAssembly)运行环境。
等等, 这话听着耳熟, 这不是和node一个玩意吗?
是的, 这确实和node是类似的玩意.
Deno和Node实际上是同一个作者Ryan Dahl(以下简称Ry)的作品. Deno的名称由来也就是node进行拆分, 然后倒过来, 取Destroy Node.js的意思, 同时取dinosaur的缩写dino, 因此把一直可爱的小恐龙作为标志.
说了这么多, 我们已经如此广泛的应用了Node, 有什么理由来让我们放弃Node, 改而使用Deno呢?
Ry为什么不继续使用Node?
Node的发展历程
基于现在的情况, 我们不会也不可能放弃掉Node, 但是, Deno给我们解释了一些Node的不足以及Deno会带来的辩护.
实际上, Deno的目标可能不是替代Node. 为什么这么说, 我们来看看Deno是如何出现的.

我们先来看看这些年前端开发中的一些重要历史节点. 你会发现, 实际上Node已经是一个历经了11年的框架了.
尤其是在过去的5,6年间, javascript这门语言发生了脱胎换骨的变化, ES6引入了大量的新的语法特性, 比如说其中影响最大两个新特性: Promise 和 ES模块方案.
Node存在的缺陷
说回到Node, Ry在Node设计之初, 并没有想到Node会发展到如今的地步. 站在今天的角度来看, node的最初设计无意是犯了很多错误的.
Node项目最早创立于2007年, 在2012年之后由社区接管其发展. Ry在2017年回过头来捡起这个项目, 却忽然发现"这个项目已经背离了他的初衷, 有一些无法忽略的问题".
上面我们说到了后来出现了Promise和ES modules. 不幸的是, node对这两种特性的支持都不尽人意. 考虑到兼容性, node一直保持了callback的异步回调方式, 因此代码中就会存在两种异步回调方式, 此外, es模块方案和node的模块管理机制总是存在冲突. 导致迟迟无法完全支持es模块
再者, node的包管理依赖于npm, 所有的包都存在node_modules中, 目录越来越复杂, 管理也越来越难. 还有一些其他的, 难以挽回的糟糕设计.
如此种种, Ry决定放弃Node, 然后从头开始新造一个轮子. 这个轮子的目标, 就是提供一个简洁安全的new runtime, 来运行javascript/typescript/webassemblt
Node 的 设计错误
没有提供promise, 而是采用了callback. 导致了:
- promise是async/await的必要抽象
- node如果提供了promise可以帮助加快最终标准化和
async/await交付 - 导致node中的很多异步API老化
默认有权限访问文件,网络和环境, 放弃了v8提供的很好的沙盒机制.
使用GYP构建系统(目前Node是GYP的唯一用户), Google自己都放弃了GYP而采用了GN.
使用package.json来管理模块版本, 在node的发布版本中包含了npm, 这个复杂的模块控制方案和一个模块化(私有控制)的存储库. 提出了一个
module作为文件目录的概念, 这不是一个严格必要的抽象, 也不是网络中存在的抽象, 现在的package.json包含了各种不必要的信息: 许可证/仓库/描述等.
node_modules: 极大的复杂了模块分辨算法, 偏离了浏览器语义.

require('module')没有扩展名'.js'. 增加了不必要的模块加载器的分析复杂度, 和浏览器的行为不同.index.js: 不必要的复杂度.
Node 和 Deno 的区别在哪里?
如果要了解Deno, 我们首先需要了解Node.
Node 架构简析
那么, Node是什么? 简单的说, Node是一个js的不依赖于browser的运行环境.

node是基于C++写的, 本质上就是一个C++应用, 底层主要由几个核心模块构成:
- v8:
- 虚拟机, 执行js代码(包含业务代码, 第三方代码和
native modules代码) - 提供
C++接口, 为nodejs提供了v8初始化, 创建context,scope等.
- 虚拟机, 执行js代码(包含业务代码, 第三方代码和
- libuv: 基于事件驱动的IO模型库, js发出请求, 有libuv完成, 然后触发回调函数
- builtin modules: 由C++代码写成的各类模块, 包含了
crypto,zlib,file stream etc基础功能. (v8 提供了函数接口, libuv提供IO模型, 以及一些nodejs函数, 为`builtin modules提供服务) - native modules: 由js写成, 提供我们应用程序调用的库, 同时这些模块依赖
builtin modules
也就是说, node的底层基于v8和libuv, 一手调用C++模块, 一手支撑nodejs方法, 从而为js代码提供了运行能力. 关于其中的一些技术细节, 我们不在这里进行展开的描述.
那么回过头来看看Deno, 它又是什么样的东西呢?
Deno 架构简析

- 底层以Rust实现
最开始的时候, Deno的原型并不是用Rest实现的, 而是Golang. 由于Go本身提供了GC, 而v8也内置了GC, 导致底层有两条GC在同时运行, 并且需要设计互动. 导致程序的复杂度增加太多. 对于这种高性能要求的库来说, Rust合适很多. 至于为什么不适用C++/C这样的语言: 一是Rust性能也很不错, 并且内存安全, 没有GC, 另外一点是Rust提供了很多现成的模块, 对于Deno来说, 可以节约很多开发时间.
顺理成章的, Deno使用Rust提供Tokio作为异步IO的库, 并且这也是一个基于轮询模型的I/O库.
关于Rust的一些讨论可以参考https://github.com/denoland/deno/issues/205这个issues下面的讨论.
Deno通过v8支持运行js, 然后通过快照缓存了一个tsc的引擎, 原生支持了ts(会自动的进行编译和缓存), 然后又因为rust原生支持webassembly, 所以也可以直接运行webassembly.
结构划分

Deno的结构可以分成三层: 前端V8, 中端libdeno, 后端rust.
- 前端v8: 这层包含了对外开放的接口中, 没有直接系统调用的和大多数的工具函数的接口. 在系统中处于"没有特权方", 在默认情况下没有文件系统和网络的访问权限(运行在v8沙箱环境中). 如果想要进行文件和网络访问, 就需要与后端的
Rust进行通信. 所以很多的系统调用API都是前端通过创建buffer数据, 发送给中端, 然后等待rust返回结果. - 中端
libdeno: 本层是介于前端和后端中间比较薄的一层, 用来和v8进行通信并且暴露一些必要的插件(由C/C++实现). 主要暴露了在前端和rust之间发送和接收消息的传递接口, 也用来初始化v8platforms/isolates和创建或者加载V8 snapshot--用来保存序列化得到的堆信息的数据块(文章:https://v8.dev/blog/custom-startup-snapshots). 注意在deno的snapshot中缓存了一个完整的ts编译器, 用来减少编译器的启动时间 - 后端
Rust: 拥有文件系统, 网络和环境访问权限的后端特权方, 由Rust实现.
核心依赖
V8: 由c++编写, deni保存了一个snapshot形式的编译器, 可以直接支持ts的代码编译到js, 缓存会生成在.deno目录下.
FlatBuffers: Google的序列化库, 允许在不通过的语言之间传递和接受消息, 没有打包和解包的性能损耗. 在deno中, 这个库用来在特权方和无特权方进行消息通信. 前端创建包含序列化数据的buffer, 然后传递到rust, rust处理这些数据后再创建序列化的buffer, 然后回传. 反序列化这些buffer使用的是flatbuffer编译器产生的文件.
相比起node, node的做法是对于每一个特权创建v8的扩展, deni则只需要暴露出前端和rust之间进行消息通信的接口就可以了.
这个讨论下面讲述了为什么用FlatBuffers来替换原来的Protobuf: https://github.com/denoland/deno/issues/269
Tokio: 一个使用Rust实现的异步runtime, 用来创建个处理事件. 允许Deno在一个内部的线程池创建任务, 然后当任务完成, 接受到输出的消息通知.
模块管理
- 加载模块是通过路径进行加载的, 因此不能忽略文件后缀
- 支持通过URL进行加载
- 支持ES模块
- 会进行本地缓存
内置工具
内置了各种工程化脚本:
- deno bundle:将脚本和依赖打包
- deno eval:执行代码
- deno fetch:将依赖抓取到本地
- deno fmt:代码的格式美化
- deno help:等同于-h参数
- deno info:显示本地的依赖缓存
- deno install:将脚本安装为可执行文件
- deno repl:进入 REPL 环境
- deno run:运行脚本
- deno test:运行测试
DENO_DIR
deno在会在$DENO_DIR(默认在$HOME/.deno)下面创建一个deno目录:
# DIRECTORIES
gen/: 缓存编译为JavaScript的文件
deps/: 缓存导入的远程url的文件
|__ http/: http方式导入的文件
|__ https/: https方式导入的文件
# FILES
deno_history.txt: Deno REPL历史记录缓存
单执行文件
Deno只有一个可执行文件, 所有的操作都可以通过这个文件完成. 可以被嵌入到一些其他的地方, 比如你想要执行一点点的js的时候. 直接使用原始的v8非常困难, 使用node又不安全, 并且过于复杂.
Deno可以看成一个v8的包装层, 可以很方面的调用它运行你的js代码. 并且Deno本身也是Rust的一个模块, 可以让我们在rust里面使用v8.
默认无权限
deno默认不具有网络, 读写等权限, 必须使用显式的命令才能打开权限:
-allow-read:打开读权限,可以指定可读的目录,比如--allow-read=/temp。
--allow-write:打开写权限。
--allow-net=google.com:允许网络通信,可以指定可请求的域,比如--allow-net=google.com。
--allow-env:允许读取环境变量。
参考链接:
- 阮一峰: http://www.ruanyifeng.com/blog/2020/01/deno-intro.html
- 官网: https://deno.land/
- 知乎: https://www.zhihu.com/question/359684696/answer/926276352
- github: https://github.com/denoland/deno
- deno核心指南: https://www.yuque.com/denoland/guide
- make mistake in node: https://zhuanlan.zhihu.com/p/37637923?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com
- https://www.cnblogs.com/peiyu1988/p/8192066.html
- https://www.jianshu.com/p/0d6df7b5ea85
- https://segmentfault.com/a/1190000005892501
- https://zhuanlan.zhihu.com/p/55311049
