忘掉 NodeJS!用 Deno 构建原生 TypeScript 应用
安装 Deno
特征
Deno 应用程序的实际运行
结论
最初发表于deepu.tech。
你听说过Deno吗?如果没有,你应该了解一下。Deno 是一个现代的 JavaScript/TypeScript 运行时和脚本环境。根据 NodeJS 创始人 Ryan Dahl 的说法,Deno 正是 NodeJS 应有的样子。Deno 也是由 Ryan Dahl 于 2018 年创建的,它基于V8、Rust和Tokio构建,专注于安全性、性能和易用性。Deno 从 Go 和 Rust 中汲取了许多灵感。
在本文中,我们将了解 Deno 的功能以及它与 NodeJS 的比较。您也可以观看我为乌克兰 Devoxx 举办的讲座。
在我们继续之前,让我们安装 Deno。
安装 Deno
有多种方法可以安装 Deno。如果您使用的是 Mac 或 Linux,则可以通过Homebrew安装。在 Windows 上,您可以使用Chocolatey。
# Mac/Linux
brew install deno
# windows
choco install deno
其他安装方法请参考官方文档
请注意,Deno 仍处于积极开发阶段,因此可能尚未准备好投入生产使用
现在我们已经安装了 Deno,让我们看看它的功能。
特征
- 开箱即用,无需任何转译设置
- 可以执行远程脚本
- 默认安全。除非明确启用,否则默认不访问文件、网络或环境
- 提供精选的标准模块
- 仅支持 ES 模块。模块全局缓存,且不可变
- 内置工具(格式、lint、测试、捆绑等)
- Deno 应用程序可以兼容浏览器
- 基于 Promise 的 API(
async/await
支持)且无回调地狱 - 顶级
await
支持 - 使用 Web Worker 的子流程
- WebAssembly 支持
- 轻量级多平台可执行文件(~10MB)
Deno 不使用 NPM 进行依赖管理,因此不存在
node_modules
麻烦,在我看来这是一个巨大的卖点
TypeScript 支持
Deno 原生支持 TypeScript 和 JavaScript。您可以直接用 TypeScript 编写 Deno 应用程序,Deno 无需您进行任何转译步骤即可执行它们。让我们来试试吧!
function hello(person: string) {
return "Hello, " + person;
}
console.log(hello("John"));
将其保存到hello.ts
文件并执行deno hello.ts
。你将看到 Deno 编译并执行该文件。
Deno 支持最新版本的 TypeScript,并保持支持最新。
远程脚本执行
使用 Deno,你可以轻松地运行本地或远程脚本。只需指向脚本的文件或 HTTP URL,Deno 就会下载并执行它。
deno https://deno.land/std/examples/welcome.ts
这意味着您只需指向原始的 GitHub URL 即可执行脚本,无需进行任何安装。Deno 的默认安全模型也适用于远程脚本。
默认安全
默认情况下,使用 Deno 运行的脚本无法访问文件系统、网络、子进程或环境。这会为脚本创建一个沙盒,用户必须明确提供权限。这将控制权交到了最终用户手中。
- 细粒度权限
- 权限可以被撤销
- 权限白名单支持
可以在执行期间通过命令行标志提供权限,或者在使用子进程时以编程方式提供权限。
可用的标志有:
--allow-all | -A
--allow-env
--allow-hrtime
--allow-read=<whitelist>
--allow-write=<whitelist>
--allow-net=<whitelist>
--allow-plugin
--allow-run
请注意,标志必须在文件名之前传递,例如
deno -A file.ts
或deno run -A file.ts
。文件名之后传递的任何内容都将被视为程序参数。
让我们看一个创建本地 HTTP 服务器的示例:
console.info("Hello there!");
import { serve } from "https://deno.land/std/http/server.ts";
const server = serve(":8000");
console.info("Server created!");
该代码片段尝试使用网络,因此当您使用 Deno 运行该程序时,它将失败并出现错误
为了避免错误,我们需要在运行程序时传递--allow-net
或--allow-all
标志。您也可以使用白名单授予对特定端口和域的访问权限。例如deno --allow-net=:8000 security.ts
标准模块
Deno 提供NodeJS、Go 或 Rust 等标准模块。随着新版本的发布,该列表也在不断扩展。目前可用的模块包括:
archive
- TAR 档案处理colors
- 控制台上的 ANSI 颜色datetime
- 日期时间解析实用程序encoding
- 编码/解码 CSV、YAML、HEX、Base32 和 TOMLflags
- CLI 参数解析器fs
- 文件系统 APIhttp
- HTTP 服务器框架log
- 日志框架media_types
- 解析媒体类型prettier
- 更漂亮的格式化 APIstrings
- 字符串实用程序testing
- 测试实用程序uuid
- UUID支持ws
- Websocket 客户端/服务器
标准模块在命名空间下可用https://deno.land/std
,并根据 Deno 版本进行标记。
import { green } from "https://deno.land/std/fmt/colors.ts";
ES 模块
Deno 仅支持使用远程或本地 URL 的ES 模块。这使得依赖管理变得简单且轻量。与 NodeJS 不同,Deno 在这方面并不会显得过于智能,这意味着:
require()
不受支持,因此不会与导入语法混淆- 没有“神奇”的模块解析
- 通过URL导入第三方模块(本地和远程)
- 远程代码仅获取一次并全局缓存以供以后使用
- 远程代码被视为不可变的,除非
--reload
使用标志,否则永远不会更新 - 支持动态导入
- 支持导入地图
- 第三方模块可在https://deno.land/x/获取
- 如果需要,可以使用 NPM 模块作为简单的本地文件 URL,或者从jspm.io或pika.dev使用
因此,我们可以导入任何通过 URL 访问的库。让我们基于 HTTP 服务器示例进行构建
import { serve } from "https://deno.land/std/http/server.ts";
import { green } from "https://raw.githubusercontent.com/denoland/deno/master/std/fmt/colors.ts";
import capitalize from "https://unpkg.com/lodash-es@4.17.15/capitalize.js";
const server = serve(":8000");
console.info(green(capitalize("server created!")));
const body = new TextEncoder().encode("Hello there\n");
(async () => {
console.log(green("Listening on http://localhost:8000/"));
for await (const req of server) {
req.respond({ body });
}
})();
通过使用下面的导入映射可以使导入路径变得更好
{
"imports": {
"http/": "https://deno.land/std/http/",
"fmt/": "https://raw.githubusercontent.com/denoland/deno/master/std/fmt/",
"lodash/": "https://unpkg.com/lodash-es@4.17.15/"
}
}
现在我们可以简化路径如下
import { serve } from "http/server.ts";
import { green } from "fmt/colors.ts";
import capitalize from "lodash/capitalize.js";
const server = serve(":8000");
console.info(green(capitalize("server created!")));
const body = new TextEncoder().encode("Hello there\n");
(async () => {
console.log(green("Listening on http://localhost:8000/"));
for await (const req of server) {
req.respond({ body });
}
})();
--importmap
使用标志运行此程序deno --allow-net=:8000 --importmap import-map.json server.ts
。请注意,标志应位于文件名之前。现在您可以访问http://localhost:8000
并验证这一点。
内置工具
Deno 借鉴了 Rust 和 Golang 的灵感,提供了内置工具。在我看来,这非常棒,因为它可以帮助你轻松上手,无需费心设置测试、linting 和打包框架。以下是目前可用/计划中的工具
- 依赖项检查器(
deno info
):提供有关缓存和源文件的信息 - Bundler(
deno bundle
):将模块和依赖项捆绑到单个 JavaScript 文件中 - 安装程序(
deno install
):全局安装一个 Deno 模块,相当于npm install
- 测试运行器(
deno test
):使用 Deno 内置测试框架运行测试 - 类型信息(
deno types
):获取 Deno TypeScript API 参考 - 代码格式化程序(
deno fmt
):使用 Prettier 格式化源代码 - Linter(计划中)(
deno lint
):源代码的 Linting 支持 - 调试器(计划中)(
--debug
):Chrome Dev 工具的调试支持
例如,使用 Deno,您可以使用提供的实用程序轻松编写测试用例
假设我们有factorial.ts
export function factorial(n: number): number {
return n == 0 ? 1 : n * factorial(n - 1);
}
我们可以为此编写一个测试,如下所示
import { test } from "https://deno.land/std/testing/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { factorial } from "./factorial.ts";
test(function testFactorial(): void {
assertEquals(factorial(5), 120);
});
test(function t2(): void {
assertEquals("world", "worlds");
});
浏览器兼容性
如果满足以下条件,Deno 程序或模块也可以在浏览器上运行
- 程序必须完全用 JavaScript 编写,并且不应使用全局 Deno API
- 如果程序是用 Typescript 编写的,则必须将其捆绑为 JavaScript,
deno bundle
并且不应使用全局 Deno API
为了兼容浏览器,Deno 还支持window.load
和window.unload
事件。load
并且unload
事件也可以一起使用window.addEventListener
。
让我们看下面的示例,可以使用它运行,deno run
也可以将其打包并在浏览器中执行
import capitalize from "https://unpkg.com/lodash-es@4.17.15/capitalize.js";
export function main() {
console.log(capitalize("hello from the web browser"));
}
window.onload = () => {
console.info(capitalize("module loaded!"));
};
我们可以打包它deno bundle example.ts browser_compatibility.js
,并在 HTML 文件中使用它browser_compatibility.js
,然后在浏览器中加载它。尝试一下,看看浏览器控制台。
承诺 API
Deno 的另一个优点是它的所有 API 都基于Promise,这意味着与 NodeJS 不同,我们无需处理回调地狱。此外,该 API 在各个标准模块之间保持高度一致。让我们来看一个例子:
const filePromise: Promise<Deno.File> = Deno.open("dummyFile.txt");
filePromise.then((file: Deno.File) => {
Deno.copy(Deno.stdout, file).then(() => {
file.close();
});
});
但是我们说没有回调,Promise API 的好处是我们可以使用async/await语法,因此,我们可以重写上面的代码
const filePromise: Promise<Deno.File> = Deno.open("dummyFile.txt");
filePromise.then(async (file: Deno.File) => {
await Deno.copy(Deno.stdout, file);
file.close();
});
运行deno -A example.ts
看看它的实际效果,别忘了创建dummyFile.txt
一些内容
顶级await
上面的代码仍然使用了回调函数,如果我们await
也可以使用它呢?幸运的是,Deno 支持顶级await
提案(TypeScript 尚不支持)。有了它,我们可以重写上面的代码
const fileName = Deno.args[0];
const file: Deno.File = await Deno.open(fileName);
await Deno.copy(Deno.stdout, file);
file.close();
是不是很棒?运行如下命令deno -A example.ts dummyFile.txt
使用 Web Worker 的子进程
由于 Deno 使用的是单线程的 V8 引擎,因此我们必须使用类似 NodeJS 的子进程来创建新线程(V8 实例)。这可以通过 Deno 中的服务工作线程 (Service Worker) 来实现。以下是示例,我们将顶层await
示例中使用的代码导入到此处的子进程中。
const p = Deno.run({
args: ["deno", "run", "--allow-read", "top_level_await.ts", "dummyFile.txt"],
stdout: "piped",
stderr: "piped",
});
const { code } = await p.status();
if (code === 0) {
const rawOutput = await p.output();
await Deno.stdout.write(rawOutput);
} else {
const rawError = await p.stderrOutput();
const errorString = new TextDecoder().decode(rawError);
console.log(errorString);
}
Deno.exit(code);
您可以像在 NodeJS 中一样以子进程的形式运行任何 CMD/Unix 命令
WebAssembly 支持
WebAssembly是 JavaScript 领域最具创新性的功能之一。它允许我们使用任何兼容语言编写的程序在 JS 引擎中执行。Deno 原生支持 WebAssembly。让我们来看一个例子。
首先,我们需要一个 WebAssembly (WASM) 二进制文件。由于我们这里主要讨论 Deno,所以我们使用一个简单的 C 程序。你也可以使用 Rust、Go 或任何其他支持的语言。最后,你只需要提供一个编译好的.wasm
二进制文件。
int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
我们可以使用此处的在线转换器将其转换为 WASM 二进制文件,并将其导入到下面的 TypeScript 程序中
const mod = new WebAssembly.Module(await Deno.readFile("fact_c.wasm"));
const {
exports: { factorial },
} = new WebAssembly.Instance(mod);
console.log(factorial(10));
运行deno -A example.ts
并查看 C 程序的输出。
Deno 应用程序的实际运行
现在我们已经对 Deno 的功能有了大致的了解,接下来让我们构建一个 Deno CLI 应用程序
运行
deno --help
并deno run --help
查看运行程序时可以传递的所有选项。您可以在 Deno网站和手册中了解有关 Deno 功能和 API 的更多信息。
让我们构建一个简单的代理服务器,它可以作为 CLI 工具安装。这是一个非常简单的代理,但如果你愿意,可以添加更多功能让它更智能。
console.info("Proxy server starting!");
import { serve } from "https://deno.land/std/http/server.ts";
import { green, yellow } from "https://deno.land/std/fmt/colors.ts";
const server = serve(":8000");
const url = Deno.args[0] || "https://deepu.tech";
console.info(green("proxy server created!"));
(async () => {
console.log(green(`Proxy listening on http://localhost:8000/ for ${url}`));
for await (const req of server) {
let reqUrl = req.url.startsWith("http") ? req.url : `${url}${req.url}`;
console.log(yellow(`URL requested: ${reqUrl}`));
const res = await fetch(reqUrl);
req.respond(res);
}
})();
运行deno --allow-net deno_app.ts https://google.com
并访问http://localhost:8000/。现在你可以在控制台上看到所有流量了。你可以使用任何你喜欢的 URL 来代替 Google。
让我们打包并安装该应用程序。
deno install --allow-net my-proxy deno_app.ts
如果要覆盖文件,请使用deno install -f --allow-net my-proxy deno_app.ts
。您还可以将脚本发布到 HTTP URL 并从那里安装。
现在只需运行my-proxy https://google.com
,我们就有了自己的代理应用。是不是简洁又好用?
结论
让我们看看 Deno 与 NodeJS 的比较,以及为什么我相信它具有巨大的潜力
为什么 Deno 比 NodeJS 更好
我认为 Deno 比 NodeJS 更好,原因如下。我猜 NodeJS 的创建者也这么认为
- 易于安装 - 单一轻量级二进制文件,内置依赖管理
- 默认安全 - 沙盒、细粒度权限和用户控制
- 简单的 ES 模块解析 - 没有像 NodeJS 那样的智能(混乱)模块系统
- 去中心化且全局缓存的第三方模块——
node_modules
高效 - 不依赖包管理器或包注册表(无 NPM、无 Yarn、无
node_modules
) - 原生 TypeScript 支持
- 遵循网络标准和现代语言特性
- 浏览器兼容性——能够在浏览器和 Deno 应用程序中重用模块
- 远程脚本运行器 - 脚本和工具的整洁安装
- 内置工具——无需设置工具、捆绑器等
为什么这很重要
这有什么关系?为什么我们需要另一个脚本环境?JavaScript 生态系统难道还不够臃肿吗?
- NodeJS 生态系统已经变得过于沉重和臃肿,我们需要一些东西来打破垄断并推动建设性的改进
- 动态语言仍然很重要,尤其是在以下领域
- 数据科学
- 脚本
- 工具
- 命令行界面
- 许多 Python/NodeJS/Bash 用例可以使用 Deno 替换为 TypeScript
- TypeScript 提供更好的开发人员体验
- 一致且可记录的 API
- 更易于构建和分发
- 不会一直下载互联网
- 更安全
挑战
这并非没有挑战,Deno 要想成功,仍然必须克服这些问题
- 库和模块的碎片化
- 与现有的许多 NPM 模块不兼容
- 库作者必须发布与 Deno 兼容的版本(这并不难,但需要额外的步骤)
- 由于 API 不兼容,迁移现有的 NodeJS 应用程序并不容易
- 捆绑包尚未优化,因此可能需要工具或改进
- 稳定性,因为 Deno 还比较新(NodeJS 已经经过实战检验)
- 尚未投入生产
如果您喜欢这篇文章,请点赞或留言。
封面图片来源:来自互联网的随机图片
文章来源:https://dev.to/deepu105/forget-nodejs-build-native-typescript-applications-with-deno-kkb