Skip to content

Instantly share code, notes, and snippets.

@futurist
Last active October 31, 2023 14:11
Show Gist options
  • Save futurist/f5f45aeb28076248b425243822bc0b29 to your computer and use it in GitHub Desktop.
Save futurist/f5f45aeb28076248b425243822bc0b29 to your computer and use it in GitHub Desktop.
从Node到Deno

从Node到Deno,你需要知道的

* 第一部分:安装

以MacOS系统为例,说明安装的不同

A. Node安装

涉及的文件:node, npm, npx, node_modules 文件夹

安装方法一 原生Node包安装会需要 root权限,安装到 /usr/local/bin,然后每一个 npm i -g 全局安装都会需要 root 权限!

安装方法二 使用nvm,安装多个node版本到 ~/.nvm 目录,同时需要修改 .bashrc.bash_profile 来激活某个版本,严重拖慢启动bash的速度。

安装方法三 全手动安装,涉及到一堆环境变量的修改和位置依赖,弄不好容易变成坑。

涉及环境变量:

  • NODE_HOME (主目录)
  • NODE_PATH (全局模块目录)
  • NPM_CONFIG_PREFIX (NPM目录)
  • NPM_CONFIG_CACHE (NPM缓存)
  • NPM_CONFIG_REGISTRY (NPM中心仓库)
  • 还有很多...

B. Deno安装

由于Deno只有一个可执行文件,Deno官方安装其实就是下载它,1.0版本解压后为42M,下载即安装。官方脚本默认安装位置:~/.deno/bin/deno,用任何方法安装都可以,只要你能运行得到。

涉及环境变量:

  • DENO_DIR(Deno主目录, 不设置的话默认为 ~/.deno)
  • DENO_INSTALL_ROOT (deno install命令的输出目录,不设置的话默认为~/.deno/bin)
  • NO_COLOR (是否启用控制台颜色)
  • HTTP_PROXY & HTTPS_PROXY (代理)

* 第二部分:模块

A. Node

Node运行时按node_modules查找算法查找node_modules,模块安装下载与依赖管理主要依靠npm,另外还有:cnpmpnpm, tnpm, bnpm, yarn......

package.json是模块管理入口文件,文件内容为JSON格式,依赖包存放在dependencies内。

npm install是更新依赖包的命令。

B. Deno

没有独立的依赖管理工具,外部可以使用 HTTP 协议 来下载并缓存模块文件,社区中建议使用一个 deps.ts 文件来存放所有的外部文件,如下

// deps.ts
export * as server from "https://deno.land/[email protected]/http/server.ts";

以上用法并非官方!,官方建议使用 import_map.json,但 它是非稳定的!

// import_map.json

{
   "imports": {
      "http/": "https://deno.land/std/http/"
   }
}
// import { serve } from "http/server.ts";

然后:deno run --allow-net --unstable --importmap=import_map.json main.ts 来引入。

注意以上 --unsatble 参数是必须的,因为它模拟了浏览器标准的Import Map规范,但目前其状态是非官方草案,将来随时可能会发生变化。

关于缓存

建议设置 DENO_DIR 环境变量,若此变量不为空则缓存目录为$DENO_DIR,否则会用系统缓存目录:

macOS: ~/Library/Caches/deno Windows: ~/AppData/Local/deno

缓存通常包含目录:deps (下载的依赖文件缓存,以内容hash命令) 和 gen (编译后的js生成物)

删除上面两个文件即可达到清除缓存效果。

* 第三部分:运行时

- Linux操作系统类比表

Linux Node Deno
Processes require('process') Web Workers
Syscalls require('v8) / Addons Ops
File descriptors (fd) via process.stdxx, fs.open Resource ids (rid)
Scheduler libuv Tokio
Userland: libc++ / glib / boost https://www.npmjs.com https://deno.land/std/
/proc/$$/stat process.report Deno.metrics()
man pages node docs deno types

- API

以下以Node经典API为例说明deno对应的用法。

注意: Deno跟Node不是同一个东西,以下只是列出可以在功能上达到相似效果的一些用法,便于理解,可以参考,不可照搬,切记!

require('my-module')

Deno使用标准ES6 Modules,而Node使用了CommonJS2,虽然13.2.0兼容了ES6 Modules,但两种方式混用的情况在前端领域很常见,非常不利于维护。

常见require在Deno的对应

require.main -> import.meta.main
__filename -> import.meta.url
__dirname -> 核心无对应 (需借助外部模块实现)

一些Deno的node polyfill

import { createRequire } from "https://deno.land/std/node/module.ts";
const require = createRequire(import.meta.url);
// 加载原生模块polyfill
// 可加载列表:https://deno.land/std/node#supported-builtins
const path = require("path");
// 可加载不带后缀模块,自动解析后缀/目录.
const cjsModule = require("./my_mod");
// 可兼容Node经典的node_modules查找算法
const leftPad = require("left-pad");

console.log(leftPad, cjsModule);
console.log(path.join('root', 'xx.js'))

// 实现上面的__dirname
const __dirname = path.dirname(import.meta.url)

注意: https://deno.land/std/node/module.ts 模块使用了不稳定API,要用以下命令运行:

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

另外还有以下独立模块不需加 --unstable:

https://deno.land/std/path 对应于 path模块

https://deno.land/std/fs 对应于 fs模块

https://deno.land/std/bytes 可实现Buffer.concat()

https://deno.land/std/http 对应于 http模块

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
assertEquals("world", "world");
import EventEmitter, { on } from "https://deno.land/std/node/events.ts";

const testEmitter = new EventEmitter();
testEmitter.on("event", () => {
    console.log("event fired");
});
testEmitter.emit('event');

注意Node的Buffer实现是在ArrayBuffer标准提出之前的,所以并不标准,Deno.Buffer 尽量向标准靠拢,两者有本质区别!某些API有相似之处。

Deno.Buffer实现其实是基于Go Buffer(https://golang.org/pkg/bytes/#Buffer)的。

Deno.Buffer is NOT the same thing as Node's Buffer. Node's Buffer was created in 2009 before JavaScript had the concept of ArrayBuffers. It's simply a non-standard ArrayBuffer.

Node示例代码:打印 0x0102030405060708Uint64

// buf 长度固定
const buf = Buffer.from(new Uint8Array([1,2,3,4,5,6,7,8]));
const d = new DataView(buf.buffer).getBigUint64(0);
console.log(buf.length, ',', d);
//output: 8, 72623859790382856n
// buf 长度可变
const buf = new Deno.Buffer();
// buf.write 会改变 Buffer 的长度,向 buf 后追加数据
await buf.write(new Uint8Array([1,2,3,4,5,6,7,8]));
const d = new DataView(buf.bytes().buffer).getBigUint64(0);
console.log(buf.length, ',', d);
//output: 8, 72623859790382856n

以上只是做为Demo示例,说明Node中Buffer是定长内存块,而Deno.Buffer是可变长内存块,所以 Deno 有以下API而 Node 没有:

buf.grow(n) //变长n字节
buf.truncate(n) //保留n字节
buf.reset()  //相当于 buf.truncate(0)

Node要增加只能通过拼接一个新的Buffer:

let newBuf = Buffer.concat(buf, Buffer.alloc(10));

另外,由于历史包袱,Node现在的Buffer API看着非常乱!

对应的几个常用的代码:

process.argv -> Deno.execPath() + import.meta.url + Deno.args  //注意Deno拆成了三部分
process.abort() -> 
process.env -> Deno.env.toObject()  // 需要--allow-env

// 以下相同
process.exit(1) -> Deno.exit(1)
process.chdir(dir) -> Deno.chdir(dir)
process.execPath() -> Deno.execPath()
process.pwd() -> Deno.pwd()
process.pid -> Deno.pid
process.stderr -> Deno.stderr
process.stdout -> Deno.stdout
process.stdin -> Deno.stdin
process.version -> Deno.version

Node会有execspawn,Deno只有run:

//Node
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { stdout } = await exec('echo hello');
console.log(`stdout: ${stdout}`);
//Deno
const p = Deno.run({
    cmd: ["echo", "hello"],
    stdout: 'piped',
    stderr: 'piped',
});
const outBuf = new Deno.Buffer()
// p.stdout!是一个Deno.Reader
await outBuf.readFrom(p.stdout!)
const stdout = new TextDecoder().decode(outBuf.bytes().buffer)
console.log(`stdout: ${stdout}`);

可以看到Deno的代码量其实更大。

Stream的实现, Node与标准几乎完全不同,Deno完全按标准实现了一遍,两者对应如下:

Writable 对应于 标准的WritableStream

Readable 对应于 标准的ReadableStream

Duplex and Transform Streams 对应于 Deno的TransformStream

不见得哪个更优秀,目前Node已有的大量基于Stream的库才是王道。

- 扩展

基本上Node使用C++扩展,而Deno使用Rust

与V8通信,Node使用N-API,Deno内建了 插件 通信机制

向系统扩展,Node使用C++ Addons,Deno使用Rust插件

以下代码复制到命令行执行,可以感受一下Deno进行系统扩展的方便:

deno run -A -r --unstable https://deno.land/x/webview/examples/multiple.ts

- Deno有但Node无

因为Deno拥抱了浏览器生态与标准,所以浏览器中有但Node中无的东西基本都是Deno的优势,比如:Fetch API

具体可以参考Deno文档

另外Deno内置了 window 对象,这样做同构应用(Isomorphic Application)方便不少。

还有就是原生TypeScript支持,做大项目工程化,协作利器。

- 性能差异

主要分为运行时,网络I/O几个方面,详情可见官网Benchmarks,以及Deno 1.0发版时关于性能的说明,总体来说,如果特别关心性能,要考虑一下使用JS运行时是否真的合适。

启动执行

执行时间官网只有Deno自身的数据,冷启动Deno要编译TS到JS,热启动是从缓存中直接执行JS,关键指标解读:

执行时间:

冷启动本地导入模块并编译 0.5s,热启动 100ms, 起50个Worker 0.5s,给4个Worker发400条消息(round_robin) 200ms,下图:

Execute Time

官方没有提供Node对比数据,我们就以运行一个.js文件(Deno此时为热启动)10次,做个简单的对比:(macOS 10.14, Node版本v12.16.3)

$ echo 'console.log("Hello World!");' > hello.js

$ time for i in {1..10}; do deno run hello.js; done
real    0m0.362s
user    0m0.171s
sys     0m0.088s

$ time for i in {1..10}; do node hello.js; done
real    0m0.475s
user    0m0.358s
sys     0m0.108s

系统+用户时间: Node 466ms, Deno 259ms,热启动Deno更快些。

内存占用:

冷启动本地导入模块并编译 68M,热启动 20M,起50个Worker 157M,给4个Worker发400条消息(round_robin) 43M,下图:

Max Memory

官方没有提供Node对比数据,我们就以运行一个.js文件(Deno此时为热启动),统计一下RSS:(macOS 10.14, Node版本v12.16.3)

$ echo 'console.log("Hello World!");' > hello.js

$ /usr/bin/time -lp deno run hello.js
... ...
16207872  maximum resident set size

$ /usr/bin/time -lp node hello.js
19189760  maximum resident set size

最大RSS: Node 18.3M,Deno 15.5M,两者相差不大,Deno略小。

HTTP吞吐:

  • deno_core_http_bench是指在rust中使用deno_core模块,处理JS发出的请求
  • hyper是rust原生开发的http server

QPS: deno_core_http_bench最高 91K,hyper为 76Knode_http37Kdeno_http最低为 27K,其中Deno为Node的 72%,下图:

HTTP Throughput

HTTP延迟:

node_http延迟最高2ms - 65ms,波动非常大,deno_http一直稳定在1ms - 3ms之间,波动较小,hyper延时最小,下图:

HTTP Latency

* 第四部分:常用库

Deno现有的库比Node少很多,常用的对应如下:

另外纯JS库像 lodash, moment等库,理论上基本直接能用或做很少的工作就能用。

更多的,可以在官网搜索

@futurist
Copy link
Author

The images:

exec-time
HTTP-Latency
http-throughput
max-mem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment