从node到deno

简介

Deno简单说是Node.js的替代品,是Node.js之父Ryan Dahl 为挽回Node.js的错误而开发的。

Node.js存在的问题有:

  1. npm包管理(node_modules)复杂。
  2. 历史原因导致的api维护,比如早期变态的callback设置。
  3. 没有安全措施,用户只要下载了外部模块,就只好听任别人的代码在本地运行,进行各种读写操作。
  4. 功能不完善,导致各种工具层出不穷,比如webpack、babel等。

由于上面这些原因,Ryan Dahl 决定放弃 Node.js,从头写一个替代品,彻底解决这些问题。
deno 这个名字就是来自 Node 的字母重新组合(Node = no + de),表示"拆除 Node.js"(de = destroy, no = Node.js)。

跟 Node.js一样,Deno 也是一个服务器运行时,但是支持多种语言,可以直接运行 JavaScript、TypeScript 和 WebAssembly 程序。

  • 它内置了 V8 引擎,用来解释 JavaScript。同时,也内置了 tsc 引擎,解释 TypeScript。
  • 它使用 Rust 语言开发,由于 Rust 原生支持 WebAssembly,所以它也能直接运行 WebAssembly。
  • 它的异步操作不使用 libuv 这个库,而是使用 Rust 语言的 Tokio 库,来实现事件循环(event loop)。

它的架构如下图所示:

从node到deno_第1张图片

说明:
1、Rust 是由 Mozilla 主导开发的通用、编译型编程语言。设计准则为 “安全、并发、实用”,支持函数式、并发式、过程式以及面向对象的编程风格。Deno 使用 Rust 语言来封装 V8 引擎,通过 libdeno 绑定,我们就可以在 JavaScript 中调用隔离的功能。
2、Tokio 是 Rust 编程语言的异步运行时,提供异步事件驱动平台,构建快速,可靠和轻量级网络应用。利用 Rust 的所有权和并发模型确保线程安全。Tokio 构建于 Rust 之上,提供极快的性能,使其成为高性能服务器应用程序的理想选择。在 Deno 中 Tokio 用于并行执行所有的异步 IO 任务。
3、V8 是一个由 Google 开发的开源 JavaScript 引擎,用于 Google Chrome 及 Chromium 中。V8 在运行之前将JavaScript 编译成了机器代码,而非字节码或是解释执行它,以此提升性能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript 程序与 V8 引擎的速度媲美二进制编译。在 Deno 中,V8 引擎用于执行 JavaScript 代码。

优势

Deno凭什么来吸引开发者改换门庭,转投它的怀抱呢?于我而言,主要有以下几点:

  • 天然支持ts。如果用Node.js,需要我们手动将ts编译成js,或者使用ts-node这种第三方工具。Deno不需要配置,开箱即用。
  • 内存安全。相较于Node.js,它的性能优势并不明显,因为就是js换了个运行环境而已,Rust并不见得比C++更高效,不过Rust会天然让它的内存安全和核心代码的健壮性更有保障些。
  • 安全机制。除非明确启用,否则没有文件,网络或环境访问权限。对于运行权限的划分,能清晰知道你的程序需要拥有哪些权限,对网络资源没有信任的同学可以放心了。
  • 没有历史包袱。不像Node.js一开始自定义了commonjs规范,Deno直接支持ES Modules。没有回调地狱,代码结构更清晰,利于tree shaking;也可以直接使用顶级await。
  • 去中心化。不用每个工程都安装一大堆node_modules,有个线上的url就能获取代码。
  • 内置浏览器API。Deno实现了fetch、FormData、WebSocket等浏览器的API,虽然被某些开发者诟病,但对前端开发者而言是真的友好,比如进行接口调用,与在浏览器里使用几乎没有区别。
  • 全家桶似的服务,比如打包、格式化、代码校验,可以让你更专注业务,而不是成为某工具的配置工程师。

缺点

Deno的缺点也很明显:

  • 没有针对Node.js压倒性的性能优势,所以对以上优势不感兴趣的开发者而言,就没有吸引力了。

    Deno 是一个合适的异步服务器,每秒 25k 请求足以满足大多数目的,此外,由于普遍使用 Promise,Deno 有更好的尾部延迟。目前 Deno HTTP 服务器每秒处理约 25 000 个请求,最大延迟为 1.3 毫秒,与之相比,Node 程序每秒处理 34 000 个请求,最大延迟介于 2 到 300 毫秒之间。
    可以看出,Deno的优势在于延迟低,但并没有压倒性优势。
  • 无法完全继承Node.js的生态。虽然Deno提供了几款CDN服务,可以将旧的npm包转换为Deno可使用的格式,但并非所有npm包都能转换成功。因为Node.js仍有部分API官方没有实现,如果某个偏底层的包转换失败,所有依赖它的包都无法成功。所以,你很有可能在开发的某个环节需要造轮子。当然,这对于喜欢钻研技术的同学而言反而是个利好。
  • 去中心化后,国内就没有办法像npm做镜像,官方搭建deno.land来存储github上各种资源,但目前只支持github上私有资源。想要引用私有文件,就必须自己搭建服务器,在CICD中构建的话,又有额外的技术难点需要解决。

    安装

1、 Mac/Linux

鉴于国内的网速原因,@justjavac 大佬搞了国内的镜像加速:

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

我用Linux部署后,报没有glibc-2.18这个版本依赖。

按网上教程,又安装了一下:

curl -O http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gz
tar zxf glibc-2.18.tar.gz
cd glibc-2.18/
mkdir build
cd build/
../configure --prefix=/usr
make -j2
make install

后两步比较慢,不要退出。

2、Windows

https://github.com/denoland/deno/releases

直接在这里下载exe文件吧。

当然,Mac与Linux也可以这样安装,但最新的Mac系统可能会因为安全权限而禁止程序使用,需要参考这里配置下。

设置环境变量

你可能还需要设置环境变量。以下是Mac/Linux命令行操作:

echo 'export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
export DENO_DIR=$HOME/.deno
' >> ~/.bash_profile

source ~/.bash_profile

测试

deno --version

升级

deno upgrade

或升级到特定版本,例如v1.3.0

deno upgrade --version 1.3.0

安全机制

Deno 具有安全控制,默认情况下脚本不具有读写权限。如果脚本未授权,就读写文件系统或网络,会报错。

必须使用参数,显式打开权限才可以。

--allow-read:打开读权限,可以指定可读的目录,比如--allow-read=/temp。
--allow-write:打开写权限。
--allow-net=google.com:允许网络通信,可以指定可请求的域,比如--allow-net=google.com。
--allow-env:允许读取环境变量。
--allow-run:允许运行子进程。

要使用不稳定的新特性,需要这样:

--unstable

还有新加的--allow-ffi--allow-hrtime,一般用不到,这里不再赘述,请情参考官方文档

运行

使用deno run来运行程序,当然,一般需要加上上面的安全机制。
如果你的程序什么权限都没使用,只是下面这种:

console.log('app start');

那么直接用deno run main.ts就可以了。
注意,我们推荐使用ts,但也可以运行js。

如果需要读取文件,比如

const decoder = new TextDecoder("utf-8");
const data = await Deno.readFile("hello.txt");
console.log(decoder.decode(data));

自然得这样:deno run --allow-read main.ts

如果需要网络,比如开启一个tcp:

/**
 * echo_server.ts
 */
import { copy } from "https://deno.land/std@0.106.0/io/util.ts";
const listener = Deno.listen({ port: 8080 });
console.log("listening on 0.0.0.0:8080");
for await (const conn of listener) {
  copy(conn, conn).finally(() => conn.close());
}

你必须:deno run --allow-net main.ts

如果懒得管理权限,则使用-Adeno run -A main.ts),那就后果自负了。

当然,也可以运行远程服务器上的文件,比如官方的hello world

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

更新模块

默认代码中引用中的ts或js文件,会缓存到本地。如果你引用的文件url中没有带版本号,那么就是最新版的代码。这时,你想要更新代码,那么你需要这样操作:

deno cache --reload my_module.ts

更新线上与本地文件:

deno cache --reload=https://deno.land/std@0.106.0 my_module.ts

更新线上某个文件与某文件夹:

deno cache --reload=https://deno.land/std@0.106.0/fs/copy.ts,https://deno.land/std@0.106

锁定文件校验

推荐建一个deps.ts文件,来管理外部引用的文件。其实就是起到类似package-lock.json的作用,管理引用的文件与版本。

// deps.ts
export { xyz } from "https://unpkg.com/xyz-lib@v0.9.0/lib.ts";

然后用deno cache 生成一个lock.json文件:

# Create/update the lock file "lock.json".
deno cache --lock=lock.json --lock-write deps.ts

另一台机器克隆下代码,运行后:

# Download the project's dependencies into the machine's cache, integrity
# checking each resource.
deno cache --reload --lock=lock.json deps.ts

# Done! You can proceed safely.
deno test --allow-read src

导入地图

可以使用带有--import-map=CLI 标志的导入映射,这样开发时使用体验类似于Node.js了。

不过如果是发布给别人使用的模块,不建议用它,因为这需要别人也把你的map文件复制过来。

例:import_map.json

{   
  "imports": {      
    "fmt/": "https://deno.land/std@0.106.0/fmt/"   
  }
}

main.ts:

import { red } from "fmt/colors.ts"; 
console.log(red("hello world"));

然后:

deno run --import-map=import_map.json main.ts

来自CDN的包

目前,推荐将共享的代码发布在官方搭建的平台https://deno.land/x/上,它的发布也很简单,在你的github仓库里配置一个web hook,就可以了。
从node到deno_第2张图片

具体详见引导,非常简单,这里不再赘述。

esm.sh

https://esm.sh/是个转换npm包为Deno使用的线上地址,它使用esbuild进行转换,速度很快。比如我们要使用React,可以这样直接使用:

import React from "https://esm.sh/react";

export default class A extends React.Component {
  render() {
    return 
; } }

当然也支持版本号:

import React from "https://esm.sh/react@17.0.2";

Skypack

https://www.skypack.dev/也是类似:

import React from "https://cdn.skypack.dev/react";

export default class A extends React.Component {
  render() {
    return 
; } }

安装脚本

使用deno install可以方便地安装一个脚本,类似于npm i -g xxx模块。

$ deno install --allow-net --allow-read https://deno.land/std@0.106.0/http/file_server.ts
[1/1] Compiling https://deno.land/std@0.106.0/http/file_server.ts

✅ Successfully installed file_server.
/Users/deno/.deno/bin/file_server

打包

打包为js

可以打包线上或本地文件为一个js文件:

deno bundle https://deno.land/std@0.106.0/examples/colors.ts colors.bundle.js
deno bundle main.ts main.js

打包后的文件是可以运行的:

deno run -A bundle.js

因为导出的是个ES模块化的代码,甚至可以放到浏览器里使用:

或者在另一个js里引用:

编译为可执行文件

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

也可以通过添加--targetCLI 标志为其他平台编译二进制文件,比如在linux服务器上打包一个exe文件供windows使用。详见此处

生成文档

deno doc后跟一个或多个源文件的列表将打印每个模块导出成员的 JSDoc 文档。
例如,给定一个add.ts包含以下内容的文件:

/**
 * Adds x and y.
 * @param {number} x
 * @param {number} y
 * @returns {number} Sum of x and y
 */
export function add(x: number, y: number): number {
  return x + y;
}

运行 deno doc命令,将函数的 JSDoc 注释打印到stdout。

使用--json标志以 JSON 格式输出文档。这种 JSON 格式由 deno doc 网站使用,用于生成模块文档。

代码风格

格式化

使用deno fmt。它会将代码格式化为官方推荐的样子,其实起到的类似eslint --fix的效果。

# format all JS/TS files in the current directory and subdirectories
deno fmt
# format specific files
deno fmt myfile1.ts myfile2.ts
# check if all the JS/TS files in the current directory and subdirectories are formatted
deno fmt --check
# format stdin and write to stdout
cat file.ts | deno fmt -

代码校验

使用deno lint。有了它,就不必再使用eslint了。

# lint all JS/TS files in the current directory and subdirectories
deno lint
# lint specific files
deno lint myfile1.ts myfile2.ts
# print result as JSON
deno lint --json
# read from stdin
cat file.ts | deno lint -

编码风格指南

参见官方指南,举几条对我们有用的:

  • 使用 TypeScript 而不是 JavaScript
  • 在文件名中使用下划线,而不是破折号
  • 不要使用文件名index.ts/ index.js。入口文件使用mod.ts/mod.js
  • 导出的函数:最多 2 个参数,将其余的放入选项对象中
  • 顶级函数不应使用箭头语法。顶级函数应该使用function关键字。箭头语法应该仅限于闭包

调试

像Node.js一样,Deno程序也可以在chrome浏览器中调试,具体就不介绍了。
主要说下vscode中调试。

需要在项目根目录下配置.vscode/launch.json文件,内容大概如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Deno: Run",
            "request": "launch",
            "type": "pwa-node",
            "program": "src/main.ts", // 具体文件路径
            "cwd": "${workspaceFolder}",
            "runtimeExecutable": "deno",
            "runtimeArgs": [
                "run",
                "--unstable",
                "--inspect",
                "--allow-all"
            ],
            "attachSimplePort": 9229
        }
    ]
}

如果你的入口文件有多个,写起来比较麻烦,可以这样:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Deno",
      "type": "pwa-node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "deno",
      "runtimeArgs": [
        "run",
        "--inspect-brk",
        "-A",
        "--config",
        "tsconfig.json",
        "--unstable",
        "${file}"
      ],
      "attachSimplePort": 9229
    }
  ]
}

详情可以参考这里

各大主流库替代方案

Electron

我们知道,现在Electron是个优秀的桌面端解决方案,我们常用的vscode就是用它写的,基于Node.js + Chromium 的 Electron 来依托 Web 技术栈创建桌面应用程序。那么我们可以在 Deno 下使用 Electron 吗?或者还有其它更多选择吗?
答案是如今的 Electron 还远远不能运行在 Deno 上,我们必须寻找其它的解决方案。自从 Deno 选择用 Rust 语言构建其内核后,我们可以使用 Rust 生态上的 Web View @Bosop/web-view 来在 Deno 上运行桌面应用。
于是 @eliassjogreen/deno_webview 应运而生。

import { Webview } from "https://deno.land/x/webview/mod.ts";

const html = `
  
  
    

Hello from deno v${Deno.version.deno}

`; const webview = new Webview( { url: `data:text/html,${encodeURIComponent(html)}` }, ); await webview.run();

pm2

我们使用pm2来守护Node.js进程,可以让代码运行在后台。事实上,它也可以运行 Node.js 之外的的脚本语言。
所以,如果要运行deno,需要创建一个app.sh文件:

#!/bin/bash
deno run -A main.ts

再使用pm2启动:

pm2 start ./app.sh

Express / koa

这俩Node.js祖师爷级别的web框架,在deno里当然少不了,Express不建议使用,推荐用koa的高仿oak吧。

import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});
await app.listen({ port: 8000 });

nestjs

nestjs 是一个用于构建高效、可扩展的Node.js服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript(仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。从它身上,我们能看到Spring和Angular的影子。

我们可以利用nestjs轻松构建一个REST服务,以Controlller的用法为例:

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

官方目前并没有打算做deno的版本,所以我基于oak实现了一个简版的oak_nest,包括Controller、安全守卫和各类参数装饰器。感兴趣的同学可以试用,有问题随时可以找我。

常用数据库

这里列举俩,其它在这里自己搜索。

MongoDB

deno_mongo,有常用的API。当然,它没有mongoose那么丰富,更复杂的功能可能就需要你来自己造轮子了。
如果想使用Schema,可以试用deno_mongo_schema,它是基于deno_mongo的扩展。

Redis

deno-redis

Nodemon

我们使用nodemon来监控Node.js程序,可以自动重启服务,在开发阶段非常方便。
deno中替代品为denon,我们需要维护一个scripts.json,起到Node.js中package.json里script命令的作用。

一个简单的样例体会下:

# 使用denon dev启动服务
scripts:
  dev:
    cmd: "deno run example/main.ts"
    desc: "run example main.ts file"
    allow:
      - net
      - env
      - write
    unstable: false
    lock: lock.json
    log: info
    env:
      PORT: "1000"
  build:
    cmd: "deno bundle example/main.ts example/main.js"
    watch: false

测试工具

在Node.js里,我们有jest、Jasmine等库,而deno可以使用官方测试std库测试:https://deno.land/std/testing

import { assertStrictEq } from 'https://deno.land/std/testing/asserts.ts'
Deno.test('My first test', async () => {
  assertStrictEq(true, false)
})

运行测试:

➜  deno test

nvm

我们使用nvm来管理Node.js版本,在Deno中有dvm

在docker中运行

Dockerhub 现已提供 Deno 的官方 Docker 镜像。

  • Alpine Linux: denoland/deno:alpine
  • Centos: denoland/deno:centos
  • Debian: denoland/deno:debian(默认)
  • Distroless: denoland/deno:distroless
  • Ubuntu: denoland/deno:ubuntu

要在 Docker 内部运行 Deno,我们可以创建以下 Dockerfile

FROM denoland/deno:1.16.2

# The port that your application listens to.
EXPOSE 1993

WORKDIR /app

# Prefer not to run as root.
USER deno

# Cache the dependencies as a layer (the following two steps are re-run only when deps.ts is modified).
# Ideally cache deps.ts will download and compile _all_ external files used in main.ts.
COPY deps.ts .
RUN deno cache deps.ts

# These steps will be re-run upon each file change in your working directory:
ADD . .
# Compile the main app so that it doesn't need to be compiled each startup/entry.
RUN deno cache main.ts

CMD ["run", "--allow-net", "main.ts"]

然后这样使用:

docker build -t app . && docker run -it -p 1993:1993 app

详见这里

以下是一个我们项目中的样例:

FROM denoland/deno:alpine-1.16.2

EXPOSE 3000

WORKDIR /app

# Prefer not to run as root.
RUN chown -R deno /app
RUN chmod 755 /app

ADD . .

ENV DENO_DIR=deno-dir

CMD deno run --allow-net --allow-env --allow-write --allow-read --config tsconfig.json --unstable mod.ts

这里设置了DENO_DIR,原因是在前面CICD的流水线中,已经缓存了工程的依赖包在deno-dir目录下,在这里就不必再下载了。
最佳方案其实是运行打包后的js文件,这样服务的启动速度能快许多,遗憾的是官方bundle有个全局命名冲突的bug仍未修复。

据说1.17.0已经修复了,但因为其它工具包的兼容问题,我还没有验证。

总结

本文主要是为了科普Deno,介绍了它的优势与缺点,安装使用与常用Node.js库的替代方案。它相较于Node.js,没有历史包袱,所以可以轻装上阵。

但想要替代Node.js成为前端人员的标配,就不是一年两年可以做到的事情了。除非Deno 真的推出了 Node.js 无法复制的强大功能,那才有可能会改变游戏规则。

Deno现在正在努力做的是兼容Node.js的API,降低后者开发者的迁移难度,这样才有可能将Node.js长达十余年的生态继承过来。

虽然Deno没有达到石破天惊的效果,但仍是个优秀的工具,我看好它。希望大家都有兴趣玩一玩。

参考:

你可能感兴趣的