Deno 入门指南(二)
五、现有模块
到目前为止,我们已经介绍了 Deno 引入 JavaScript 生态系统的每一个重大变化,现在是时候回顾一下您已经可以用它做的事情了。
不要误会我;你可以用它做任何你想用 Node.js 做的事情。你可能知道,NPM 有几乎同样多的用户发布的数百万个模块,虽然这些代码是 JavaScript,但它并不是 100%与 Deno 兼容,所以我们不能像 11 年前一样重复使用这些工作。
也就是说,Deno 的标准库已经很强大了,从第一天开始,就已经有用户将模块从 Node 移植到 Deno,以使它们兼容,所以我们有很多工具可以使用。
在这一章中,我将介绍其中的一些,以便向您展示虽然这个新的运行时还不到一年,但是您可以用它来做一些非常有趣的项目。
Deno 标准:标准库
让我们从安装 Deno:标准库的第一天就提供给我们的模块开始。这实际上非常重要,因为对 Ryan 来说,Node.js 有一个非常糟糕的标准库,并且缺少任何人开始做相关事情所需的大多数基本工具。作为在 2012 年左右开始在 0.10 版本上使用 Node 的人,我可以确认,在存在 3 年之后,Node.js 没有真正的标准库。它的重点是为后端开发人员提供异步 I/O,但仅此而已;整个开发人员的体验并不好,尤其是与今天的标准相比。
不过这不是问题,因为 Node 越受欢迎,就有越多的用户将他们可用的基本构建模块编译成更有用的库,并开始通过 NPM 共享。尽管 Deno 已经有了相当多的社区,并且他们开始编写新的库或者将现有的库移植到这边,但是这些数字还不能比较,至少现在还不能。
现在回到性病,因为这是我们在这里要讨论的。正如我在第一章中提到的,这种最初的功能分组部分受到了 Go 及其标准库的启发,所以如果在 Deno 的未来更新中,他们继续从那里移植更多的想法,我不会感到惊讶。但是到目前为止,Deno 的标准库包含 21 个稳定的模块,一个实验性的模块,以及一些已经可供您查看的示例。
表 5-1
Deno 标准库包含的所有模块的列表
|组件
|
描述
|
| --- | --- |
| 档案馆 | 归档功能,在撰写本书时,它为您提供了归档和解压缩文件的能力。 |
| 异步ˌ非同步(asynchronous) | 处理异步行为的工具集。我不是在谈论承诺或异步/等待;这些是语言本身的一部分。这里有一些东西,比如选择代码执行延迟时间的延迟函数,或者将 resolve 和 reject 函数作为方法添加到 promise 中。 |
| 字节 | 操作字节的低级函数集。如果你不这样对待二进制对象,它们将需要更多的工作;这些功能将帮助您简化这项任务。 |
| 日期时间 | 一些辅助函数和一些字符串解析函数,帮助您在字符串和日期对象之间架起一座桥梁。 |
| 编码 | 非常有用的模块来处理外部数据结构。还记得 JSON 结构是如何获得 JSON 全局对象的吗?嗯,在这里您可以添加对 YAML,CSV 和其他几个的支持。 |
| 旗帜 | 命令行参数分析器。如果您正在构建一个 CLI 工具,就不再需要导入一个模块来完成这项工作;你已经有了。这显示了一个经过深思熟虑的标准库的威力。 |
| 滤波多音 | 文本格式化功能。如果console.log对你来说还不够,这个模块有你所需要的一切来增加你的控制台消息的活力。 |
| 满量程 | 额外文件系统功能。我们不是在谈论只是读写一个文件;这可以直接从 Deno 名称空间完成。我们正在讨论使用通配符作为路径的一部分,复制文件和文件夹,移动它们,等等。注意:在撰写本文时,这个模块被标记为不稳定,所以你需要--unstable标志来访问它。 |
| 混杂 | 该库增加了对创建和处理 10 多种用于创建散列的算法的支持。 |
| 超文本传送协议(Hyper Text Transport Protocol 的缩写) | HTTP 相关的函数。如果你试图创建一个 web 服务器(例如,在一个微服务上工作,一个单一的 web 应用,或者介于两者之间的东西),在这里你可以得到你所需要的一切。 |
| 木卫一 | 这是 Deno 处理流的模块,当然包括标准输入的模块。这意味着,如果您希望从用户那里请求输入(当然,除了其他事情之外),这就是您要使用的模块。 |
| 原木 | 这个模块是 Deno 拥有非常完整的标准库的证明。有多少次你不得不在 Node 中实现你自己的记录器?或者为你的项目寻找最好的伐木工?相反,Deno 已经有一个非常完整和灵活的供您使用,而不必出去寻找任何东西。 |
| 哑剧 | 一组专用于处理多部分表单数据的函数,包括读取和写入。 |
| 结节 | 这是一个与 Node.js 的标准库兼容的模块。它为一些最常见的节点功能(如 require 或 events)提供了聚合填充。如果你想把代码从节点移植到节点,这是一个你想回顾的模块;不然真的没什么用。 |
| 小路 | 用来处理路径的一组经典函数,例如从路径中获取文件夹名,或者提取一组不同路径的公共路径等等。 |
| 许可 | 一个小模块,用来授予你的脚本权限。它要求使用--unstable标志。它与上一章描述的Deno.permissions API 非常相似。 |
| 信号 | 提供一个 API 来处理进程信号。这是一个相当低级的 API,但它允许您处理 SIGINT 和 SIGTSTP 之类的信号。 |
| 测试 | 就像日志模块一样,这次 Deno 也为您提供了创建测试套件所需的一切。 |
| 全局唯一识别 | 以前需要创建一个唯一的 ID 吗?本模块将帮助您使用 UUID 标准支持的不同版本(1、3、4 和 5)之一创建一个。 |
| 艾德 | WebAssembly 系统接口(WASI)的实现,可用于编译 WASM 代码。 |
| 《华盛顿明星报》 | 我们在列表中遗漏了一样东西:WebSocket 支持。 |
表 5-1 对标准模块进行了快速概述;它们都在不断发展,但同时,由于它们作为 Deno 生态系统的一部分发挥着至关重要的作用,它们由核心团队直接审查。就像任何开源项目一样,您可以发送您的贡献;只要明白他们不能有任何外部依赖。
外部模块
正如我已经提到的,Deno 的用户定制模块生态系统还不能与 Node 的相比,因为它已经存在了很长时间。也就是说,机构群体正在做大量工作来弥合这一差距。
毕竟,现有的每个节点模块都是用 JavaScript 编写的,只是风格略有不同,所以翻译是可行的。这只是需要时间,尤其是当你要翻译的模块有依赖项时,因为你也必须翻译那些依赖项。
自 Deno 发布以来,已经部署了一些解决方案,以便通过某种形式的单一位置浏览和查找模块(阿拉 NPM 网站),或者通过跟踪 URL 或者直接存储所有内容。
我将快速介绍最近部署的两个主要存储库,您可以使用它们来了解已经有哪些可供您使用的存储库。
官方名单
Deno 的网站( http://deno.land )提供免费的 URL 重写服务,你可以贡献你的链接到列表中。基本上,他们会在他们的网站上列出你的模块(目前,已经有超过 700 个模块被显示)并重定向到它们。这个注册中心的数据库目前是一个 JSON 文件,您必须对其进行编辑并发送一个 Pull 请求。
就个人而言,我不认为这是非常可扩展的,所以我假设在不久的将来他们会提供另一种方式来更新列表并向其中添加您自己的模块。
但是现在,创建这个列表的方法是向这个存储库发送一个 Pull 请求: https://github.com/denoland/deno_website2 ,具体来说是对名为database.json的文件的修改,这个文件可以直接在那个 repo 的根文件夹中找到。
文件的格式见清单 5-1;如您所知,没有太多的字段可以提供,尽管没有关于它的官方文档,但您可以看到它足够简单。
{
//...
"a0": {
"type": "github",
"owner": "being-curious",
"repo": "a0",
"desc": "A command line utility to manage `text/number/email/password/address/note` with Alias to easy recall & copy to clipboard.",
"default_version": "master"
//...
}
Listing 5-1Sample section of the database.json file
在deno.land/x可以看到储存库,看起来像图 5-1;本质上,你得到一个基本的搜索框,可以过滤超过 700 个已发布的模块。
图 5-1
Deno 的官方模块库
如果您将分支机构名称作为 URL 的一部分添加,该重定向规则也会考虑到它的创建方式。如果这样做,它会向该分支发送流量;否则,它会认为你的目标是主分支。作为我在第四章中提到的使用标签的替代方法,你也可以使用分支名称作为你的版本号,由于这个有用的重定向,这也可能对你有利。有了它,你可以写一些类似 http://deno.land/x/your-module@1.4/ 的东西,这将把流量重定向到你在 GitHub 的账户(假设这是我们正在谈论的你的模块),在它里面,到那个模块的文件夹,在它里面,特定的分支称为 1.4。
最酷的是,您可以使用这个方法从代码中导入模块。记住,这只是一个重定向服务;实际的文件存储在你放的任何地方,在这种情况下,它将是 GitHub 自己的服务器。
同样,这不是集中式存储库的替代品,而仅仅是一个搜索将不断增长的分散模块海洋的伟大工具。
凭借区块链的力量
第二个,也是最有希望找到 Deno 模块的平台是 nest.land。虽然与之前的服务不同,这个平台也存储你的代码,但它没有使用常规平台,而是使用区块链网络。
这是正确的;通过使用区块链的力量,这个平台不仅为你的模块创建了一个分布式存储,而且是一个永久的存储。通过这个平台和发布你的模块,你将它们储存在 Arweave perma web1中,从技术上讲,它们将永远存在于此。所以移除模块是不可能的,这在发布模块时已经提供了比任何其他选项更大的优势,因为模块可能被意外移除的事实是依赖外部包的最大风险之一。
这个平台的缺点是它还没有前一个平台受欢迎,所以没有很多包在那里发布。图 5-2 展示了他们主页的样子。
图 5-2
图库,列出已发布的模块
为了导入存储在该平台中的模块,您将从网站获得一个 URL,您可以从您的代码中使用它,它们都遵循相同的模式: https://x.nest.land/ <module-name>@<module-version>/mod.ts
例如,模块 Drash, 2 是一个 HTTP 微框架,可以使用以下 URL 导入: https://x.nest.land/deno-drash@1.0.7/mod.ts 。
另一方面,如果您希望在这个平台上发布您的模块,那么您必须安装他们的 CLI 工具(称为 egg)。为了做到这一点,您至少需要 Deno 的 1.1.0 版本,使用下面的命令,您应该能够安装它:
deno install -A -f --unstable -n eggs https://x.nest.land/eggs@0.1.8/mod.ts
请注意,您提供了所有特权(使用-A 标志),并且还通过使用--unstable标志授予了使用不稳定特性的权限。
一旦安装完毕,您就必须使用eggs link --key [your key]链接您的 API 密钥(您应该在注册后获取、下载并存储在您的本地存储中)。
通用安装说明到此结束;之后,你必须进入你的模块的文件夹,并使用egg init初始化它(就像你用npm init一样)。
在初始化过程中,您会被问到几个关于项目的问题,比如名称,如果是模块的不稳定版本的描述,要发布的文件列表,以及配置文件的格式(JSON 或 YAML)。
配置文件的格式类似于清单 5-2 中的格式。
{
"name": "module-name",
"description": "Your brief module description",
"version": "0.0.1",
"entry": "./src/main.ts",
"stable": true,
"unlisted": false,
"fmt": true,
"repository": "https://github.com/your_name/your_project",
"files": [
"./mod.ts",
"./src/**/*",
"./README.md"
]
}
Listing 5-2Sample configuration file for nest
尽管这看起来像是 Node 中受人喜爱的package.json的一个副本,但事实上并非如此。这个文件是简化显示信息和管理包的任务所必需的,但是它不包括额外的信息,例如依赖项列表,也不包括项目范围的配置或命令。因此,尽管它仍然添加了一个配置文件,但您并没有将所有内容都集中到一个充满不相关内容的文件中。
有了这个文件,剩下要做的就是发布你的模块,你可以用命令egg publish. A来完成,之后,你就可以在库中看到你的模块,它将永远存在(或者至少直到 permaweb 被关闭)。
要查看的有趣模块
为了结束这一章,我想介绍一些你可能感兴趣的模块,这取决于你想用 Deno 实现什么。
当然,没有什么可以阻止你使用其他模块,但至少它会给你一个起点。
API 开发
可能与后端异步 I/O 运行时相关的最常见任务之一是开发 API 或任何基于 web 的项目。这就是 Node.js 在微服务项目上获得如此多关注的原因。
对于 Deno,已经有一些非常有趣的框架可用。
德雷什
使用这个模块,您可以创建一个直接的 API 或 web 应用;您可以根据自己选择的生成器脚本来决定使用哪一个。本质上,Drash 为您提供了一个生成器脚本,让您能够创建所需的所有基本样板代码。
最酷的是,由于像 Deno 提供的远程导入和执行远程文件的能力,您可以使用生成器,而不必在您的计算机上安装任何东西。下面一行显示了您需要执行的确切命令:
$ deno run --allow-run --allow-read --allow-write --allow-net https://deno.land/x/drash/create_app.ts --api
现在,您应该能够完全理解这个命令在做什么。基本上,您正在执行create_app.ts脚本,为了确保它能够工作,您允许它运行子进程,在您的硬盘上读写,并建立网络连接,可能是为了下载所需的文件。
图 5-3
执行 Drash 生成器后的项目结构
项目结构非常简单,如图 5-3 所示;注意deps.ts文件,它遵循我在第四章中介绍的相同模式。目前,它只导出两个依赖项,如您在清单 5-3 中看到的,但是您将使用这个文件导出您将来可能添加的任何其他内容。
export { Drash } from "https://deno.land/x/drash@v1.0.0/mod.ts";
export { assertEquals } from "https://deno.land/std@v0.52.0/testing/asserts.ts";
Listing 5-3Default exports added by Drash
在 resources 文件夹中,您可以看到这个框架试图使用面向对象的方法,通过扩展Drash.Http.Resource来声明资源。清单 5-4 展示了自动生成的资源,这反过来清楚地说明了用这种方法实现一个基于 REST 的 API 是多么容易。
export default class HomeResource extends Drash.Http.Resource {
static paths = ["/"];
public GET() {
this.response.body = JSON.stringify(
{ success: true, message: "GET request received." },
);
return this.response;
}
public POST() {
this.response.body = JSON.stringify({ message: "Not implemented" });
return this.response;
}
public DELETE() {
this.response.body = JSON.stringify({ message: "Not implemented" });
return this.response;
}
public PUT() {
this.response.body = JSON.stringify({ message: "Not implemented" });
return this.response;
}
}
Listing 5-4Autogenerated resource class by Drash
至于它的文档,他们的网站 3 包含一组非常详细的例子,带你从最基本的用例到最复杂的用例。作为一名来自 Express 4 或 Restify、 5 等框架的开发人员,Drash 采用的方法是新鲜而有趣的,考虑到它主要侧重于 TypeScript 和我们在第二章中介绍的几个特性。
如果您希望快速完成一些工作,并使用 Deno 设置一个 API,可以考虑看看这个新的尝试,而不是使用迁移的节点模块。
数据库访问
无论您正在开发哪种应用,您都很可能需要使用数据库。无论是基于 SQL 的还是 NoSQL 的,如果你需要,Deno 都能满足你。
结构化查询语言
如果你正在考虑使用 SQL(特别是 SQLite、MySQL 或 Postgre),那么 Cotton 6 是你的首选;类似于 sequelize 7 为 Node 所做的,这个模块试图为开发者提供一个数据库无关的方法。您担心使用正确的方法,它会为您编写查询。最好的部分是,如果你需要,你也可以写你自己的原始查询,当然,这将打破 ORM 模式,但它也给你最复杂的用例所需的灵活性。
你可以直接从 Deno 的模块库中导入这个模块,换句话说,从你的代码中使用 https://deno.land/x/cotton/mod.ts 。然后使用清单 5-5 中的代码连接到数据库。
import { connect } from "https://deno.land/x/cotton/mod.ts";
const db = await connect({
type: "sqlite", // available type: 'mysql', 'postgres', and 'sqlite'
database: "db.sqlite",
// other...
});
Listing 5-5Connecting to your favorite SQL database
然后,您可以通过直接编写 SQL 来查询您的表,如清单 5-6 所示,或者使用清单 5-7 所示的数据库模型(遵循 ORM 模式)。
const users = await db.query("SELECT * FROM users;");
for (const user of users) {
console.log(user.email);
}
Listing 5-6Raw query getting the list of users
请注意,结果是如何将用户直接转换为具有正确属性的对象,而不是必须使用自定义方法来获取正确的属性,或者甚至为每个记录创建一个值数组。
import { Model } from "https://deno.land/x/cotton/mod.ts";
class User extends Model {
static tableName = "users";
@Field()
email!: string;
@Field()
age!: number;
@Field()
created_at!: Date;
}
//and now query your data...
const user = await User.findOne(1); // find user by id
console.log(user instanceof User); // true
Listing 5-7Using the ORM pattern to query the database
当然,如果您想走这条路,您必须更改 TypeScript 编译器上的默认配置;不然就不行了。你可以在你的项目文件夹中有一个类似于清单 5-8 的tsconfig.json文件。
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Listing 5-8Required configuration to make decorators work
然后用下面一行执行您的代码:
$ deno run -c tsconfig.json main.ts
NoSQL
另一方面,如果您希望与 NoSQL 数据库进行交互,推荐一个模块的任务会变得有点复杂,因为由于 NoSQL 数据库的性质,您很难找到一个适用于所有数据库的模块。
相反,您必须寻找专门为您的数据库设计的东西。在这里,我将为 MongoDB 和 Redis 推荐一些东西,因为它们是两个主要的 NoSQL 数据库。
MongoDB
基于文档的数据库是 NoSQL 的经典选择,尤其是 MongoDB,考虑到它与 JavaScript 的集成,非常适合我们最喜欢的运行时。
DenoDB 8 是除 deno_mongo 9 之外为数不多的为 mongoDB 提供支持的模块,deno _ Mongo9 是用 Rust 编写的 Mongo 驱动程序之上的直接包装器。有趣的是,这个模块还支持一些主要的基于 SQL 的数据库,所以它涵盖了所有的基础知识。
连接 Mongo 很容易;您只需要确保您指定了清单 5-9 中所示的正确选项,定义您的模型就像扩展模块导出的模型类一样简单(参见清单 5-10 中的示例)。
class Flight extends Model {
static fields = {
_id: {
primaryKey: true,
},
};
}
const flight = new Flight();
flight.departure = 'Dublin';
flight.destination = 'Paris';
flight.flightDuration = 3;
await flight.save()
Listing 5-10Using models to interact with the collections
import { Database } from 'https://deno.land/x/denodb/mod.ts';
const db = new Database('mongo', {
uri: 'mongodb://127.0.0.1:27017',
database: 'test',
});
Listing 5-9Connecting to Mongo
这个模块唯一的缺点是明显缺乏对原始查询的支持。因此,如果你发现自己需要模块的 API 没有给你的操作,请记住,它只是使用 deno_mongo 来处理连接,所以你可以通过getConnector方法直接访问该对象。
使用心得
Redis 是一种完全不同类型的数据库,因为它处理的是键-值对,而不是实际的类似文档的记录,所以遵循相同的基于 ORM 的方法没有什么意义。
因此,我们将使用 Deno 的 Redis 驱动程序的直接端口,可以访问所有经典的 Redis 方法。如果您来自 Node 并且以前使用过 Redis 包,这应该感觉非常相似。
import { connect } from "https://denopkg.com/keroxp/deno-redis/mod.ts";
const redis = await connect({
hostname: "127.0.0.1",
port: 6379
});
const ok = await redis.set("hoge", "fuga");
const fuga = await redis.get("hoge");
Listing 5-11Connecting to Redis from Deno
清单 5-11 显示了取自文档的一个基本例子,但是你可以在那里看到正在使用的set和get方法。还支持 Pub/Sub API 和一个非常有趣的特性:原始请求(参见下面的示例片段)。
await redis.executor.exec("SET", "redis", "nice"); // => ["status", "OK"]
await redis.executor.exec("GET", "redis"); // => ["bulk", "nice"]
当然,您通常希望使用 API 提供的方法,但是这允许您访问尚不属于稳定 API 的特性。仅在极端情况下使用此选项;否则,坚持使用标准方法。
Tip
为了让您的代码与这个模块一起工作,您需要使用--allow-net标志提供网络特权。
命令行界面
作为 Deno 等运行时的另一个经典用例,考虑到 JavaScript 的动态性,很容易将其用于开发工具,这就是 CLI 工具的用武之地。
尽管 Deno 作为其标准库的一部分已经提供了一个非常全面的参数解析模块,但是在创建命令行工具时还需要注意其他事情。
为此,模块 Cliffy 10 提供了一套完整的软件包,处理创建这些工具所涉及的所有方面。
作为该模块的一部分,有六个集中了不同功能的包,允许您只导入您需要的部分,而没有单一的巨大依赖。
-
ansi-escape:11允许您在需要时通过四处移动或隐藏光标来与 CLI 光标进行交互。
-
命令:12你可以使用这个模块为你的 CLI 工具创建命令。它提供了一个非常易于使用的 API,可以自动生成帮助消息并帮助您解析 CLI 参数。
-
flags : 13 把这个模块想象成 Deno 的 flag 解析包上了类固醇。它允许您为您的标志提供一个非常详细的模式,指定诸如别名、它们是否是强制的、与其他标志的依赖性等等。它帮助您将 CLI 工具从基础版本升级为经过充分思考和专业设计的工具。
-
keycode:14如果你试图请求用户输入普通文本以外的内容(例如,按下 CTRL 键),这个模块将帮助你解析那些信号。
-
提示 : 15 请求用户输入可以简单到使用一个带有消息的 console.log,然后依赖 Deno 的 Stdin 阅读器,或者您可以使用这个包给用户一个很好的体验。除了请求自由文本输入,您还可以提供下拉框、复选框、数字输入等等。
-
表格 : 16 如果您需要在终端上显示表格数据,这个模块就是您的首选。它允许您设置格式选项,如填充、边框宽度、最大单元格宽度等。
作为这个库可以做什么的一个例子,我将向您展示如何使用我刚才提到的最后一个模块在一个格式良好的表格上显示 CSV 文件的内容。
文件内容见图 5-4 。你会发现它没有什么特别之处,只是你的普通电子表格,我会把它保存为普通的 CSV 文件,并使用清单 5-12 中的代码,我会加载并显示它。在图 5-5 中,你会看到最终结果显示在我的终端上。
图 5-4
基本 CSV 文件
import { parse } from "https://deno.land/std/encoding/csv.ts";
import { BufReader } from "https://deno.land/std/io/bufio.ts";
import { Table } from 'https://deno.land/x/cliffy/table.ts';
const f = await Deno.open("./data.csv");
const reader = new BufReader(f)
const records:[string[]] = <[string[]]>(await parse(reader))
f.close();
Table.from( records )
.maxCellWidth( 20 )
.padding( 1 )
.indent( 2 )
.border( true )
.render();
Listing 5-12Deno code to display the content of our CSV in table format on the terminal
注意如何解析 CSV。我实际上使用的是 Deno 的标准库,table 模块期望得到与parse方法返回的格式相同的格式,所以我们在这里实际上不必做太多。输出本身正如我们所期望的那样,是我们终端上的一个表格。
图 5-5
显示表中数据的脚本输出
现在已经有很多其他的模块可以让你开始用 Deno 编写高质量的软件了。社区不断地发布和移植来自 Node 或 Go 的包,或者只是抓住机会给这个新的生态系统带来新鲜的想法,所以开始浏览和测试那些看起来更有趣的包真的取决于你。
结论
本章的目的是让您了解 Deno 的生态系统已经有多成熟,正如您所看到的,社区不仅对缺乏提供浏览和可靠存储代码的方式的包管理器做出了回应,而且他们还一直在制作内容,就像没有明天一样。
如果您想知道这个新的运行时是否有足够的用户群来实际用于生产,考虑到在发布后的几个月内已经发布的所有内容,本章应该会给你答案。
事情才刚刚开始,所以在下一章,也是最后一章,我将展示几个例子,说明如何使用本章中的一些模块和一些新的模块来创建成熟的应用。
Footnotes 12
https://nest.land/package/deno-drash
3
4
5
6
https://rahmanfadhil.github.io/cotton/
7
8
https://eveningkid.github.io/denodb-docs/
9
10
https://github.com/c4spar/deno-cliffy/
11
https://github.com/c4spar/deno-cliffy/tree/master/packages/ansi-escape
12
https://github.com/c4spar/deno-cliffy/tree/master/packages/command
13
https://github.com/c4spar/deno-cliffy/tree/master/packages/flags
14
https://github.com/c4spar/deno-cliffy/tree/master/packages/keycode
15
https://github.com/c4spar/deno-cliffy/tree/master/packages/prompt
16
https://github.com/c4spar/deno-cliffy/tree/master/packages/table
六、将所有这些放在一起——示例应用
这是最后一章,到目前为止,我们不仅讨论了语言和运行时,还讨论了自发布之日起(老实说,在此之前)社区所做的令人惊叹的工作,构建工具和模块来帮助推动技术向前发展。
在这一章中,我将展示几个我用 Deno 构建的非常不同的项目,以便向您展示到目前为止所涉及的所有内容是如何组合在一起的。它们都是示例项目,当然还没有完全投入生产,但是它们应该涵盖所有感兴趣的领域,如果您将 GitHub 项目作为一个起点(所有这些项目都可以在 GitHub 帐户上使用),您应该能够对其进行定制,并很快使其成为您自己的项目。
所以事不宜迟,让我们开始检查项目。
Deno runner
我们要解决的第一个项目是一个简单但非常有用的项目。从我们到目前为止所介绍的内容来看,每次执行 Deno 脚本时,都需要指定权限标志,以便为脚本提供这些权限。这是事实,也是这个运行时背后的团队的设计决策。
然而,可以有另一种方式;如果您创建一个工具,从预设文件中读取这些权限,然后作为子进程执行预期的脚本,那么您可以为用户提供更好的体验。这就是这个项目的目的:简化执行 Deno 脚本的用户体验,而不必担心非常长的命令行,虽然很明确,但对于新手用户来说也可能很复杂和可怕。
目标是从这样的命令行移动:
$ deno run --allow-net --allow-read=/etc --allow-write=/output-folder --allow-env your-script.ts
取而代之的是,有一个专门设计的文件来存放你的安全标志,类似于清单 6-1 的东西,跑步者可以为你所用,而不必担心它。
--allow-net
--allow-read=/etc
--allow-write=/output-folder
--allow-env
Listing 6-1Content of the flags file
现在,一个更简单的命令行读取该文件,然后执行该脚本,如下所示:
$ the-runner your-script.ts
非常非常简单,如果您考虑一下,如果您尝试执行的脚本附带了 flags 文件,您会发现使用这个工具会更加友好,尤其是对新手而言。
计划
该工具很简单,并且使其工作所需的步骤也很简单:
-
构建一个入口点,它接收要作为参数执行的脚本的名称。
-
确保您可以找到标志文件(包含脚本安全标志的文件)。
-
使用标志文件中的标志和脚本的名称,创建执行脚本所需的命令。
-
然后,使用 Deno 的
run方法执行它。
为了使这个工作,我们将只使用标准库;在某种程度上,这也证明了 Deno 的创造者对其标准库的承诺。
这个项目的结构也很简单;为了让一切井然有序,我们只需要几个文件:
-
主脚本,即所谓的入口点,是将由用户执行的脚本,也是解析 CLI 参数的脚本。
-
所有外部依赖项都将从
deps.ts文件中导入,遵循已经覆盖的模式,以便于我们将来可能需要的任何更新或包含。 -
我们将要编写的三个函数将存在于一个
utils.ts文件中,只是为了将入口点的代码与这些支持函数分开。 -
最后,将代码捆绑到单个文件并使其最终可执行所需的脚本将是一个简单的 bash 脚本。这是因为我们需要运行一些终端命令,使用 bash 比使用 JS 要容易得多。
代码
这个小项目的完整源代码位于这里 1 ,以防你需要检查任何其他细节或者甚至克隆存储库。
也就是说,入口点的代码公开了整个脚本背后的主要逻辑,您可以在清单 6-2 中看到这一点。
import { parse, bold } from './deps.ts'
import { parseValidFlags, runScript } from './utils.ts'
// The only argument we care about: the script's name
const ARGS = parse(Deno.args)
const scriptName:string = <string>ARGS["_"][0]
const FLAGFILE = "./.flags" //this is the location and the name of the flags file
// Required to turn the binary array from Deno.readFile into a simple string
const decoder = new TextDecoder('UTF-8')
let secFlags = ""
try { //Make sure we capture any error reading the file...
const flags = await Deno.readFile(FLAGFILE)
secFlags = decoder.decode(flags)
} catch (e) {//... and in that case, just ignore privileges
console.log(bold("No flags file detected, running script without privileges"))
}
let validFlags:string[] = parseValidFlags(secFlags)
runScript(validFlags, scriptName)
Listing 6-2Code for the entry point script
脚本正在捕获位于Deno.args的命令行参数,由于parse方法(你将在deps.ts文件中看到)来自属于标准库的flags模块。然后,我们读取标志文件,如果脚本找不到它,就捕获它。有了这些内容,我们解析它,把它变成一个字符串列表,然后简单地请求运行它。
现在,关于代码的其余部分,我还想介绍两个细节。对标志的解析本质上需要读取一个带有标志列表的文件,每行一个标志,这有一个潜在的问题:如何将这些行转换成一个数组?请记住,换行字符并不总是相同的;这实际上取决于操作系统。幸运的是,Deno 为我们提供了一种方法来检测我们正在使用的行尾字符,因此脚本可以适应它运行的操作系统。您可以在清单 6-3 中看到我是如何做到的。
export function parseValidFlags(flags:string):string[] {
const fileEOL:EOL|string = <string>detect(flags)
if(flags.trim().length == 0) return []
return <string[]>flags.split(fileEOL).map( flag => {
flag = flag.trim()
let flagData = findFlag(flag)
if(flagData) {
return flagData
} else {
console.log(":: Invalid Flag (ignored): ", bold(flag))
}
}).filter( f => typeof f != "undefined")
}
Listing 6-3Parsing the flags
注意所使用的detect函数,以便理解使用的是哪一个行尾字符。然后我们对split方法也这样做。剩下的就是确保从文件中读取的标志是有效的,如果不是,我们就忽略它。
最后,转换这些读取标志并运行脚本所需的代码如清单 6-4 所示。你可以看到这段代码有多简单;我们只需要用正确的参数调用Deno.run方法。
export function runScript(flags:string[], scriptFile:string) {
flags.forEach( f => {
console.log("Using flag", bold(f))
})
let cmd = ["deno", "run", ...flags, scriptFile]
const sp = Deno.run({
cmd
})
sp.status()
}
Listing 6-4Running the script
在这个函数中,我们对标志列表进行了额外的迭代,只是为了通知用户哪些权限被授予了正在执行的脚本。但是这段代码真正的核心是我们如何使用数组析构将数组合并到另一个数组中。
我想介绍的最后一点并不是真正的 Deno 代码。相反,它是几行 bash 代码。请参见清单 6-5 ,我稍后会解释。
#!/bin/bash
DENO="$(which deno)"
SHEBANG="#!${DENO} run -A"
CODE="$(deno bundle index.ts)"
BOLD=$(tput bold)
NORMAL=$(tput sgr0)
echo "${SHEBANG}
${CODE}" > bundle/denorun.js
chmod +x bundle/denorun.js
echo "----------------------------------------------------------------------------------"
echo "Thanks for installing DenoRunner, copy the file in ${BOLD}bundle/denorun.js${NORMAL} to a folder
you have in your PATH or add the following path to your PATH variable:
${BOLD}$(pwd)/bundle/${NORMAL}"
echo "----------------------------------------------------------------------------------"
Listing 6-5Build script written in bash
这个脚本的第一行被称为 shebang ,如果您从未见过它,它会告诉解释器将执行这个脚本的实际二进制文件的位置。它允许您执行脚本,而不必从命令行显式调用解释器;相反,当前的 bash 将为您做这件事。理解这一点很重要,因为它可以用任何脚本语言来完成,不仅仅是 bash,正如你马上要看到的,我们正试图对我们的脚本做同样的事情。
然后,我们捕获 deno 二进制文件在系统中的安装位置,以便创建一个包含新 shebang 行的字符串。根据您的系统,它可能看起来像这样:
/home/your-username/.deno/bin/deno run -A
然后我们将继续使用deno bundle命令,它将获取我们所有的外部和内部依赖项并创建一个文件。这对于分发我们的应用来说是完美的,因为它允许您简化这个任务。现在你不必要求你的用户下载一个潜在的非常大的项目,你只需要要求他们下载一个文件并使用它。
但是,我们的问题是,我们需要让我们的最终包是一个自动可执行文件,所以我们需要了解您的 deno 安装在哪里,以便创建正确的 shebang 行。将我们的包代码放在我们的CODE变量中,将 shebang 行放在SHEBANG变量中,然后我们将两个字符串输出到bundle文件夹中的一个文件(我们的最终包)中。然后,我们为我们的文件提供执行权限,这样您就可以从命令行直接调用它,shebang 就会生效。
将这一行作为脚本的第一行,您的 bash 将知道调用 Deno,告诉它执行我们新构建的文件,并为它提供所有可用的特权。这是为了确保我们不会遇到任何问题;您可以像过去一样更改-A以获得更详细的权限列表,但是一旦准备好,并且您已经将文件复制到您的PATH中的某个地方(即,当键入命令时您的终端将查找的某个地方)或者将文件夹添加到其中(参见清单 6-6 中如何做的示例),您就可以简单地键入
$ denorun.js your-script.ts
它会正确执行您的脚本,如果您创建了正确的.flags文件,它会读取并列出所有权限,在执行您的文件之前,它会列出这些权限以确保用户知道它们。
# To test it inside your current terminal window (will only work for the currently opened session)
export PATH="/path/to/deno-runner/bundle:$PATH"
# In order to make it work on every terminal
export PATH="/path/to/deno-runner/bundle:$PATH" >> ~/.bash_profile
Listing 6-6Adding a folder to your PATH variable
Note
清单 6-6 中的例子只适用于 Linux 和 Mac 系统;如果你有一个 Windows 盒子,你必须搜索如何更新你的路径。可以做到;这并不难,但是只需点击几下鼠标,而不是命令行。此外,该示例假设您正在使用默认的 bash 命令行;如果你正在使用别的东西,比如 Zsh, 2 ,你必须相应地更新代码片段。
测试应用
对于下一个使用 Deno 可以实现什么的例子,我想介绍标准库中另一个强大的模块:测试。 3
正如我已经提到的,Deno 已经为您提供了一个测试套件。当然,如果您打算做更复杂的事情,比如创建存根或模拟,您可能需要额外的资源,但是对于基本的设置,Deno 的测试模块已经足够了。
为此,我们将回到第一个示例,我们将添加一些示例测试,以便您可以看到它实际上有多简单。
添加一个测试就像创建一个以_test.ts或.test.ts结尾的文件一样简单(如果您直接编写 JavaScript,则更改扩展名);这样,当您使用如下的测试命令执行它时,Deno 应该能够获得它并运行测试:deno test。
清单 6-7 显示了设置测试套件所需的代码。
Deno.test("name of your test", () => {
///.... your test code here
})
Listing 6-7Basic test code
正如您所看到的,启动和运行您的测试只需要很少的东西;事实上,你可以在清单 6-8 中看到一个如何测试让 deno-runner 工作的一些函数的例子。
import { assertEquals } from "../deps.ts"
import { findFlag, parseValidFlags } from '../utils.ts'
Deno.test("findFlag #1: Find a valid flag by full name", () => {
const fname = "--allow-net"
const flag = findFlag(fname)
assertEquals(flag, fname)
})
Deno.test("findFlag #2: It should not find a valid flag by partial name", () => {
const fname = "allow-net"
const flag = findFlag(fname)
assertEquals(flag, false)
})
Deno.test("findFlag #3: Return false if flag can't be found", () => {
const fname = "invalid"
const flag = findFlag(fname)
assertEquals(flag, false)
})
Deno.test("parseValidFlag #1: Should return an empty array if there are no matches", () => {
let flags = parseValidFlags("")
assertEquals(flags, [])
})
Listing 6-8Testing the deno-runner code
例如,如果您想做一些更复杂的事情并监视函数调用,您将需要一个外部模块,比如 mock。有了这个模块,你可以使用清单 6-9 中看到的间谍和模仿。
import { assertEquals } from "https://deno.land/std@0.50.0/testing/asserts.ts";
import { spy, Spy } from "https://raw.githubusercontent.com/udibo/mock/v0.3.0/spy.ts";
class Adder {
public miniAdd(a: number, b:number): number {
return a +b
}
public add( a: number, b: number, callback: (error: Error | void, value?: number) => void): void {
const value: number = this.miniAdd(a,b)
if (typeof value === "number" && !isNaN(value)) callback(undefined, value);
else callback(new Error("invalid input"));
}
}
Deno.test("calls fake callback", () => {
const adder = new Adder()
const callback: Spy<void> = spy();
assertEquals(adder.add(2, 3, callback), undefined);
assertEquals(adder.add(5, 4, callback), undefined);
assertEquals(callback.calls, [
{ args: [undefined, 5] },
{ args: [undefined, 9] },
]);
});
Listing 6-9Using spies to test your code
该示例展示了如何覆盖回调函数并检查执行情况,从而允许您检查诸如执行次数、收到的参数等内容。事实上,清单 6-10 展示了如何为Adder类的一个方法创建存根以控制其行为的例子。
Deno.test("returns error if values can't be added", () => {
const adder = new Adder()
stub(adder, "miniAdd", () => NaN);
const callback = (err: Error | void, value?: number) => {
assertEquals((<Error>err).message, "invalid input");
}
adder.add(2, 3, callback)
});
Listing 6-10Creating a stub for one of the methods
只需一行简单的代码,您就可以用一个您可以控制的方法替换原来的方法。在清单 6-10 的例子中,您正在控制来自miniAdd方法的输出,从而帮助您测试与add方法相关联的其余逻辑(即,确保在这种情况下返回值是错误对象)。
聊天服务器
最后,构建聊天服务器通常需要处理套接字,因为它们允许您打开一个双向连接,该连接在关闭之前一直保持打开状态,这与普通的 HTTP 连接不同,普通的 HTTP 连接只存在很短的一段时间,并且实际上只允许在客户机和服务器之间发送单个请求及其相应的响应。
如果您来自 Node,您可能见过类似的基于套接字的聊天客户端和服务器的例子,本质上是在套接字库发出的事件之上工作。然而,Deno 的架构有点不同,因为它不依赖事件发射器,而是使用流来处理套接字。
在这个例子中,我将快速浏览 Deno 官方文档中显示的客户端和服务器的简化版本(对于 WebSocket 模块,是标准库 5 的一部分)。清单 6-11 展示了如何处理套接字流量(基本上,新消息被接收或者甚至是一个关闭套接字的请求)。
let sockets: WebSocket[] = []
async function handleWs(sock: WebSocket) {
log.info("socket connected!");
sockets.push(sock)
try {
for await (const ev of sock) {
if (typeof ev === "string") {
log.info("ws:Text", ev);
for await(let s of sockets) {
log.info("Sending the message: ", ev)
await s.send(ev);
}
await sock.send(ev);
} else if (isWebSocketCloseEvent(ev)) {
// close
const { code, reason } = ev;
log.info("ws:Close", code, reason);
}
}
} catch (err) {
log.error(`failed to receive frame: ${err}`);
if (!sock.isClosed) {
await sock.close(1000).catch(console.error);
}
}
}
Listing 6-11Handling new message on the socket connection
一旦建立了套接字连接,就要调用这个函数(稍后将详细介绍)。如您所见,它的要点是一个主for循环,遍历套接字的元素(实质上是新消息到达)。接收到的所有文本消息都将通过异步for循环中的socket.send方法发送回客户端和所有其他打开的套接字(注意代码中加粗的部分)。
为了启动服务器并开始监听新的套接字连接,您可以使用清单 6-12 中的代码。
const port = Deno.args[0] || "8080";
log.info(`websocket server is running on :${port}`);
for await (const req of serve(`:${port}`)) {
const { conn, r: bufReader, w: bufWriter, headers } = req;
acceptWebSocket({
conn,
bufReader,
bufWriter,
headers,
})
.then(handleWs)
.catch(async (err:string) => {
log.error(`failed to accept websocket: ${err}`);
await req.respond({ status: 400 });
});
}
Listing 6-12Starting the server
使用serve函数启动服务器,这又创建了一个请求流,我们也在用异步for循环迭代这个请求流。在收到每个新的请求时(即打开一个新的套接字连接),我们调用acceptWebSocket函数。这个服务器和客户端的完整代码(我一会儿会讲到)可以在 GitHub、 6 上找到,所以一定要查看一下,以了解一切是如何组合在一起的。
简单的客户
没有合适的客户机,服务器什么也做不了,所以为了结束这个例子,我将向您展示如何使用标准库中的同一个模块来创建一个客户机应用,它将从前面连接到服务器并发送(和接收)消息。
清单 6-13 展示了客户端代码背后的基本架构;在使用了connectWebSocket函数之后,我们将创建两个不同的异步函数,一个用于从套接字读取消息,一个用于从标准输入读取文本。注意,除了标准库之外,我们没有使用任何外部库。
const sock = await connectWebSocket(endpoint);
console.log(green("ws connected! (type 'close' to quit)"));
// Read incoming messages
const messages = async (): Promise<void> => {
for await (const msg of sock) {
if (typeof msg === "string") {
console.log(yellow(`< ${msg}`));
} else if (isWebSocketCloseEvent(msg)) {
console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
}
}
};
// Read from standard input and send over socket
const cli = async (): Promise<void> => {
const tpr = new TextProtoReader(new BufReader(Deno.stdin));
while (true) {
await Deno.stdout.write(encode("> "));
const line = await tpr.readLine();
if (line === null || line === "close") {
break;
} else {
await sock.send(username + ":: " + line);
}
}
};
await Promise.race([messages(), cli()]).catch(console.error);
Listing 6-13Core of the client code
注意我之前提到的两个异步函数(messages和cli);它们都返回一个承诺,正因为如此,我们可以使用 Promise.race 让两个函数同时执行。使用这种方法,一旦任何一个承诺解决或失败,执行将结束。cli函数将从标准输入中读取输入,并使用socket.send方法通过套接字连接发送。
另一方面,messages函数就像在服务器端一样,迭代套接字的元素,本质上是对通过连接到达的消息做出反应。
通过将此客户端的实例连接到服务器,您可以在它们之间发送消息。服务器会负责将消息广播给每个人,客户端会用黄色显示从服务器收到的文本。如果你想测试这个项目,请参考完整代码 7 。
结论
这不仅是第六章的结尾,也是本书的结尾。希望到现在为止,您已经设法理解了创建 Deno 背后的动机,为什么提出 Node 并在后端开发行业留下印记的同一个人决定重新开始并更加努力。
Deno 远没有做到;事实上,当我开始编写这本书时,它的第一个版本刚刚发布,甚至不到两个月后,版本 1.2.0 就已经出来了,由于突破性的变化导致了一些问题。
但不要害怕;事实上,如果你对 Deno 背后的团队仍有疑虑,这就是你需要的证据。这不仅仅是一个人希望推翻后端的 JavaScript 国王,这是一个完整的团队,致力于满足不断增长的社区的需求,积极提供反馈和支持,以帮助生态系统每天都在增长。
如果你只是想从这本书里学到一样东西,我希望你带走玩一种全新技术的好奇心,希望你会爱上它。
感谢您阅读至此;下次再见!
Footnotes 1https://github.com/deleteman/deno-runner
2
3
4
5
6
https://github.com/deleteman/deno-chat-example
7
https://github.com/deleteman/deno-chat-example