Node.js 的作者 Ryan Dahl 在 2018 年 JSConf EU 上发布了一篇演讲:10 Things I Regret About Node.js,讲述了他认为自己在设计 Node.js 时的十个(实际上演讲中只提到七个)失误,包括:没有坚持使用 Promise,使用了 GYP 构建系统,package.json 和 node_modules 的设计失误等。
在演讲中 Ryan Dahl 宣布了一个新项目:Deno,一个新的服务端 Javascript 运行时。经过两年多的发展,Deno 已经发布了 1.8 版本,也有了一个活跃的开发者社区。我们今天就来简单的了解一下 Deno。
Deno 简介
Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
Deno 使用 V8 引擎,由 Rust 构建,是一个简单、现代、安全的 Javascript 和 Typescript 运行时。
Deno 的主要特性有:
- 默认安全。外部代码没有文件系统、网络、环境的访问权限,除非显式开启。
- 支持开箱即用的 TypeScript 的环境。
- 只分发一个独立的可执行文件 (deno)。
- 有着内建的工具箱,比如一个依赖信息查看器 (deno info) 和一个代码格式化工具 (deno fmt)。
- 有一组经过审计的标准模块,保证能在 Deno 上工作。
- 脚本代码能被打包为一个单独的 JavaScript 文件。
安装 Deno
可以执行快速安装脚本方便地将 Deno 安装到不同操作系统上。
Mac 和 Linux 平台
代码语言:javascript复制curl -fsSL https://deno.land/x/install/install.sh | sh
Windows 平台(使用 Powershell)
代码语言:javascript复制iwr https://deno.land/x/install/install.ps1 -useb | iex
第一个 Deno 脚本
安装好 Deno 后,我们就可以直接在终端里使用 Deno 运行 Javascript/Typescript 脚本。
代码语言:javascript复制$ deno run https://deno.land/std/examples/welcome.ts
Download https://deno.land/std/examples/welcome.ts
Warning Implicitly using latest version (0.91.0) for http://deno.land/std/examples/welcome.ts
Download https://deno.land/std@0.91.0/examples/welcome.ts
Check https://deno.land/std/examples/welcome.ts
Welcome to Deno!
有例子中我们可以知道deno run
可以直接运行远程脚本,当然运行本地脚本也不是问题。
$ echo 'console.log("Hello, World!")' > hello.ts
$ deno run .hello.ts
Check file:///C:/Users/duyix/lab/deno-code/hello.ts
Hello, World!
使用 Deno 运行 HTTP Server
我们再运行一下 Deno 官网示例中的启动 HTTP Server 的程序。
代码语言:javascript复制import { serve } from "https://deno.land/std@0.90.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
req.respond({ body: "Hello Worldn" });
}
代码语言:javascript复制$ deno run .server.ts
Download https://deno.land/std@0.90.0/http/server.ts
# 省略了一些依赖下载的日志
Check file:///C:/Users/duyix/lab/deno-code/server.ts
error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag
at processResponse (core.js:223:11)
at Object.jsonOpSync (core.js:246:12)
at opListen (deno:cli/rt/30_net.js:32:17)
at Object.listen (deno:cli/rt/30_net.js:207:17)
at serve (server.ts:304:25)
at server.ts:2:11
可以看到直接运行server.ts
会失败,Deno 报错PermissionDenied
。这是 Deno 默认的安全策略的行为,使用 Deno 运行脚本时默认不提供网络连接、文件 IO 等权限,需要显式的在deno run
命令中启用对应的权限,在这个例子中我们需要加上--allow-net
标志来启用网络权限。
$ deno run --allow-net .server.ts
http://localhost:8000/
然后我们就可以访问本地的 8000 端口了。
代码语言:javascript复制$ http :8000
HTTP/1.1 200 OK
content-length: 12
Hello World
Deno REPL
和 Node 类似,Deno 也有 REPL 模式,直接运行deno rep
或deno
就可以交互式地运行 JavaScript 脚本了,Deno 的 REPL 模式暂时还不提供 Typescript 支持。
$ deno
Deno 1.5.4
exit using ctrl d or close()
> let resp = await fetch('https://httpbin.org/headers')
undefined
> await resp.json()
{
headers: {
Accept: "*/*",
"Accept-Encoding": "gzip, br",
Host: "httpbin.org",
"User-Agent": "Deno/1.5.4",
"X-Amzn-Trace-Id": "Root=1-6058b379-7fd616e829aa462029478558"
}
}
> close()
我们在 Deno REPL 中使用了 fetch 进行网络请求,Deno 实现了一些常见的Web API。并且可以看到在 REPL 模式中,deno 默认开启了网络权限。
使用 Deno 编写命令行程序
Deno 是编写命令行工具的一个新的选择。
代码语言:javascript复制// dn-echo.ts
console.log(Deno.args.join(" "));
代码语言:javascript复制$ deno run ./dn-echo.ts 1 2 3
1 2 3
Deno
是 Deno 的一个全局对象,提供了一些系统 API(命令行参数获取、文件读写等)。可以通过Deno.arg
获取命令行参数。
使用 Deno 编写命令行程序另一个便捷之处是我们可以很方便地通过deno install
命令将本地脚本或网络脚本安装成全局工具。
$ deno install .dn-echo.ts
✅ Successfully installed dn-echo
C:Usersduyix.denobindn-echo.cmd
值得注意的是deno install
命令也要加上对应的权限选项。
$ deno install -n dn-cat --allow-read https://deno.land/std@0.90.0/examples/cat.ts
Download https://deno.land/std@0.90.0/examples/cat.ts
Check https://deno.land/std@0.90.0/examples/cat.ts
✅ Successfully installed dn-cat
C:Usersduyix.denobindn-cat.cmd
C:Usersduyix.denobindn-cat (shell)
$ dn-cat .hello.ts
console.log("Hello, World!")
我们从 url 安装了一个命令行脚本 cat(使用-n dn-cat
选项指定了全局工具的名字),并且加上了--allow-read
选项来开启读文件权限。
使用 Deno 开发 Web 应用
作为 Node.js 的挑战者,开发 Web 应用自然是 Deno 的看家本领。Deno 标准库http
模块的功能已经很齐全了。不过为了更佳的开发体验,我们在这里使用oak
框架(借鉴了 Node.js 中的 koa 框架)来开发 web 应用。
//app.js
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
const port = 8000;
// Logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.headers.get("X-Response-Time");
console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);
console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});
// Timing
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});
const router = new Router();
router
.get("/", (ctx) => {
ctx.response.body = "Hello Deno";
})
.get("/:name", (ctx) => {
ctx.response.body = `Hello ${ctx.params.name}`;
});
app.use(router.routes());
app.use(router.allowedMethods());
app.addEventListener("listen", () => {
console.log(`Listening on localhost:${port}`);
});
await app.listen({ port });
在app.js
中,我们初始化了一个Application
对象app
,注册了两个中间件(分别用来打日志和记录请求时间)。
然后使用oak
框架提供的Router
组件实例化了一个router
对象,定义了两个接口并将router
注册到了app
上。
随后在app
上注册了一个事件监听,在listen
事件发生(也就是应用开始运行)时,输出了相关日志。
我们可以直接运行app.js
。
$ deno run --allow-net ./app.js
Listening on localhost:8000
随后就可以访问接口了。
代码语言:javascript复制$ http :8000/
HTTP/1.1 200 OK
content-length: 10
content-type: text/plain; charset=utf-8
x-response-time: 1ms
Hello Deno
$ http :8000/mike
HTTP/1.1 200 OK
content-length: 10
content-type: text/plain; charset=utf-8
x-response-time: 0ms
Hello mike
部署 Deno Web 应用
第一次运行app.js
的时候 Deno 需要从互联网上下载其相关依赖, 我们可以使用deno cache
命令手动将指定脚本的依赖缓存到本地(使用-r
选项可以强制重新加载)。
$ deno cache -r ./app.js
deno cache
命令在构建镜像时还是很必要的,因为无法保证容器在每次运行的时候都可以顺利下载好依赖。直接在构建阶段将依赖准备好是一个更稳妥的选择。
一个常见的 Deno Web 应用的 Dockerfile 如下:
代码语言:javascript复制FROM hayd/alpine-deno:1.8.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"]
打包脚本
Deno CLI 提供deno bundle
命令可以将指定脚本及其依赖打包成一个 javascript 文件(类似 webpack),提供了另外一种分发应用的方式。
$ deno bundle ./app.js app.bundle.js
Bundle file:///C:/Users/duyix/lab/deno-code/app.js
Check file:///C:/Users/duyix/lab/deno-code/app.js
Emit "app.bundle.js" (402.67KB)
可以看到打包生成的脚本还是很大的,不过根据我的观察app.bundle.js
其实是格式化良好的,并且里面的函数名和参数名等都没有缩减过。deno bundle
的输出文件大小精剪还是有很大优化空间的。
我个人还是更推荐使用容器镜像运行 Deno 应用,更规范,也更容易维护应用源代码和依赖。
总结
Deno 毕竟还是一个很年轻的项目,稳定性和配套的资源和支持与 Node.js 相比还是有很大差距的。但正如 Ryan Dahl 所说,Node.js 有其固有的一些历史遗留的设计问题。Deno 的优秀理念正在吸引越来越多的开发者使用并加入社区,相信 Deno 会成为服务端 Web 开发领域的新秀。