三、运行时和标准库
既然我们已经对 Deno 有了足够的了解,现在就可以用它来编写一些实际的应用了。 在本章中,我们不会使用任何库,因为它的主要目的是呈现运行时 api 和标准库。
我们将编写小型 CLI 实用程序,web 服务器,以及更多,始终利用官方 Deno 团队创建的功能,没有外部依赖。
Deno 名称空间将是我们的起点,因为我们相信首先研究运行时包含的内容是有意义的。 遵循这个思路,我们还将研究 Deno 与浏览器共享的 Web api。 我们将使用setTimeout
toaddEventListener
,fetch
等等。
在 Deno 命名空间中,我们将了解程序的生命周期,与文件系统交互,并构建小型命令行程序。 稍后,我们将了解缓冲区,并了解如何使用它们进行异步读写。
然后我们将快速转向标准库,并浏览一些有用的模块。 本章的目的不是取代标准库的文档; 相反,它将向您展示它的一些功能和用例。 我们会在写小程序的时候了解它。
在这个标准库的旅程中,我们将使用处理文件系统、ID 生成、文本格式化和 HTTP 通信的模块。 其中一部分将是对我们将在后面章节中更深入探讨的内容的介绍。 您将通过编写第一个 JSON API 并连接到它来完成本章。
以下是我们将在本章中讨论的主题:
- Deno 运行时
- 探索 Deno 名称空间
- 使用标准库
- 使用 HTTP 模块构建 web 服务器
技术要求
本章所有的代码文件都可以在 GitHub 链接中找到:https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03。
Deno 运行时
Deno 提供了一组函数,这些函数作为Deno
命名空间中的全局变量包含在运行时中。 运行时 api 记录在https://doc.deno.land/,可以用来做最基本、最低级的事情。
Deno 上有两种类型的函数,没有任何导入:Web api 和Deno
命名空间。 只要在 Deno 中有一个行为同时存在于浏览器中,Deno 就会模仿浏览器的 api——那些是 Web api。 由于您来自 JavaScript 世界,您可能对其中的大多数都很熟悉。 我们讨论的是诸如fetch
、addEventListener
、setTimeout
等函数,以及window
、Event
、console
等对象。
使用 Web api 编写的代码可以捆绑在一起,无需转换就可以在浏览器中运行。
运行时公开的 api 的另一个重要部分存在于一个名为Deno
的全局命名空间中。 您可以使用 REPL 和文档,我们探索的两个第二章,工具链,去探索它,得到一个快速掌握它包含的功能。 在本章的后面,我们还将试验一些最常见的方法。
如果您想访问 Deno 中包含的所有符号的文档,您可以运行带有--builtin
标志的doc
命令。
稳定性
Deno
命名空间中的函数从 1.0.0 版本开始被认为是稳定的。 这意味着 Deno 团队将努力在新版本中支持它们,并将尽最大努力使它们与未来的更改兼容。
正如您可能想象的那样,在--unstable
标记下仍然认为不稳定的特性存在于生产中,因为我们在前面的示例中使用了它们。
不稳定模块的文档可以通过doc
命令使用--unstable
标志或者通过https://doc.deno.land/builtin/unstable访问。
标准库还不是,Deno 团队认为它是稳定的,因此它们有一个不同于 CLI 的版本(在编写本文时,它的版本是 0.83.0)。
与Deno
命名空间函数相比,标准库通常不需要--unstable
标志来运行,除非标准库中的任何模块使用了Deno
命名空间中的不稳定函数。
程序生命周期
Deno 支持浏览器兼容load
和unload
事件,可用于运行设置和清理代码。
处理程序可以用两种不同的方式编写:使用addEventListener
和覆盖window.onload
和window.onunload
函数。 load
事件可以是异步的,但unload
事件并非如此,因为它们不能被取消。
使用addEventListener
可以注册无限句柄; 例如:
addEventListener("load", () => {
console.log("loaded 1");
});
addEventListener("unload", () => {
console.log("unloaded 1");
});
addEventListener("load", () => {
console.log("loaded 2");
});
addEventListener("unload", () => {
console.log("unloaded 2");
});
console.log("Exiting...");
如果我们运行前面的代码,我们会得到以下输出:
$ deno run program-lifecycle/add-event-listener.js
Exiting...
loaded 1
loaded 2
unloaded 1
unloaded 2
安排代码在设置和拆卸阶段运行的另一种方法是覆盖window
对象的onload
和onunload
函数。 这些函数具有只运行最后一个赋值函数的特殊性。 这是因为它们凌驾于彼此之上; 请看下面的代码,例如:
window.onload = () => {
console.log("onload 1");
};
window.onunload = () => {
console.log("onunload 1");
};
window.onload = () => {
console.log("onload 2");
};
window.onunload = () => {
console.log("onunload 2");
};
console.log("Exiting");
通过运行前面的程序,我们得到如下输出:
$ deno run program-lifecycle/window-on-load.js
Exiting
onload 2
onunload 2
如果我们看看我们编写的初始代码,我们可以理解前两个声明被后面的两个声明覆盖了。 这就是当我们覆盖onunload
和onload
时会发生的事情。
Web api
证明我们可以使用 Web api 相同的方式我们可以使用在浏览器上,我们将编写一个基本的程序,获取 Deno 网站标志,将其转换为 base64,并打印到控制台的 HTML 页面图像的 base64。 让我们按照以下步骤来做:
-
以请求https://deno.land/logo.svg:
fetch("https://deno.land/logo.svg")
开始 2. 转换成
blob
:fetch("https://deno.land/logo.svg") .then(r =>r.blob())
-
从
blob
对象中获取文本,并将其转换为base64
:fetch("https://deno.land/logo.svg ") .then(r =>r.blob()) .then(async (img) => { const base64 = btoa( await img.text() ) });
-
Print to the console an HTML page with an image tag using the base64 image:
fetch("https://deno.land/logo.svg ") .then(r =>r.blob()) .then(async (img) => { const base64 = btoa( await img.text() ) console.log(`<html> <img src="data:image/svg+xml;base64,${base64}" /> </html> ` ) })
当我们运行这个时,我们会得到预期的输出:
$ deno run --allow-net web-apis/fetch-deno-logo.js <html> <img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9Ijgx My4xODQiIHdpZHRoPSI4MTMuMTUiIHhtbG5zPSJodHRwOi8vd3d3Lncz Lm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzIyMiI+PHBhdGggZD0ibTM3 NC41NzUuMjA5Yy0xLjkuMi04IC45LTEzLjUgMS40LTc4LjIgOC4yLTE1 NS4yIDQxLjMtMjE4IDkzLjktMTEuNiA5LjYtMzggMzYtNDcuNiA0Ny42 LTUyIDYyLjEtODIuNCAxMzEuOC05My42IDIxNC4zLTIuNSAxOC4z …
现在,在*nix 输出重定向特性的帮助下,我们可以用脚本的输出创建一个 HTML 文件:
$ deno run --allow-net web-apis/fetch-deno-logo.js > web-apis/deno-logo.html
您现在可以检查文件,或者直接在浏览器中打开它来测试它是否工作。
这也是可能的,使用您的知识从上一章和直接运行脚本从 Deno 标准库服务当前文件夹:
$ deno run --allow-net --allow-read https://deno.land/std@0.83.0/http/file_server.ts web-apis
Check https://deno.land/std@0.65.0/http/file_server.ts
HTTP server listening on http://0.0.0.0:4507
然后,通过导航到http://localhost:4507/deno-logo.html
,我们可以检查图像是否在那里和工作:
图 3.1 -使用 Deno 访问网页 陆地标志作为一个 base64 图像
这些只是 Deno 支持的 Web api 的例子。 在这个具体的例子中,我们已经使用了fetch
和btoa
,但在整个章节中还会用到更多。
您可以自由地使用这些已经很熟悉的 api 进行试验,可以通过编写简单的脚本或使用 REPL。 在本书的其余部分,我们将使用 Web api 中的已知函数。 在下一节中,我们将了解 Deno 名称空间、仅在 Deno 内部工作的函数,并通常提供更低级的行为。
探索 Deno 命名空间
所有没有被 Web API 覆盖的功能都在 Deno 命名空间下。 这是 Deno 独有的功能,例如不能绑定到 Node 或浏览器中运行。
在本节中,我们将探索其中的一些功能。 我们将构建小型实用程序,模拟一些日常使用的程序。
如果你想在我们动手之前探索可用的功能,可以在https://doc.deno.land/builtin/stable上找到。
构建简单的 ls 命令
如果您曾经使用过*nix 系统的 Terminal 或 WindowsPowerShell,那么您可能对ls
命令很熟悉。 简单地说,它列出了目录中的文件和文件夹。 我们要做的是创建一个 Deno 实用程序,它模仿ls
的一些功能,也就是说,列出一个目录中的文件,并显示关于它们的一些细节。
最初的命令有无数的标志,为了简单起见,我们在这里不实现这些标志。
我们决定显示的信息是文件的名称、大小和最后修改日期。 让我们动手吧:
-
Create a file named
list-file-names.js
and useDeno.readDir
to get a list of all files and folders in the current directory:for await (const dir of Deno.readDir(".")) { console.log(dir.name) }
这将打印当前目录中的文件在不同的行:
$ deno run --allow-read list-file-names.ts .vscode list-file-names.ts
我们使用了来自 Deno 命名空间的
readDir
(https://doc.deno.land/builtin/stable#Deno.readDir)。正如在文档中提到的,它返回
AsyncInterable
,我们正在循环使用它并打印文件的名称。 由于运行时是用 TypeScript 编写的,所以我们有非常有用的类型补全,并且我们确切地知道每个dir
条目中都有哪些属性。现在,我们想要获取当前目录作为命令行参数。
-
Use
Deno.args
(https://doc.deno.land/builtin/stable#Deno.args) to get the command-line arguments. If no argument is sent, use the current directory as a default:const [path = "."] = Deno.args; for await (const dir of Deno.readDir(path)) { console.log(dir.name) }
我们利用数组解构来获得
Deno.args
的第一个值,同时使用默认属性来设置path
变量的默认值。 -
Navigate to the
demo-files
folder (https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/ls/demo-files) and run the following command:$ deno run --allow-read ../list-file-names.ts file-with-no-content.txt .hidden-file lorem-ipsum.txt
看起来它起作用了。 它从当前所在的文件夹中获取文件并列出它们。
现在,我们需要获取文件信息,以便显示它。
-
Use
Deno.stat
(https://doc.deno.land/builtin/stable#Deno.stat) to get information about the files:提示
如果您想研究命令的行为方式,您可以使用
--inspect-brk
在调试模式下运行它,或者您可以在 REPL 上尝试运行它。import { join } from "https://deno.land/std@0.83.0/path/mod.ts"; const [path = "."] = Deno.args; for await (const dir of Deno.readDir(path)) { let fileInfo = await Deno.stat(join(path, dir.name)) const modificationTime = fileInfo.mtime; const message = [ fileInfo.size.toString().padEnd(4), `${modificationTime?.getUTCMonth().toString().padStart(2)}/${modificationTime?.getUTCDay().toString().padEnd(2)}`, dir.name ] console.log(message.join("")) }
为了使它更清晰,我们使用了一个数组来组织我们的消息。 我们还添加了一些填充,通过使用
padEnd
,使输出对齐。 运行刚才编写的程序,在Chapter03/Is
文件夹(https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/ls/demo-files)中,得到如下输出:$ deno run --allow-read index.ts ./demo-files 12 7/4 .hidden 96 7/4 folder 96 7/4 second-folder 5 7/4 my-best-file 20 7/4 .file1 0 7/4 .hidden-file
我们得到了我们发送的deno-files
目录中的文件和文件夹列表,以及大小(以字节为单位)和创建月份和日期。
在这里,我们使用已经知道的和需要的--allow-read
标志来给 Deno 权限来访问文件系统。 然而,在前一章中,我们提到了 Deno 程序请求权限的另一种方式,即使用我们称为“动态权限”的方式。 这就是我们接下来要学习的内容。
使用动态权限
当我们自己编写 Deno 程序时,我们通常事先知道所需的权限。 然而,当编写或执行可能需要或不需要某些权限的代码或编写交互式 CLI 实用程序时,一次请求所有权限可能没有意义。 这就是动态权限的作用。
动态权限允许程序在需要时请求权限,允许任何正在执行代码的人交互地授予或拒绝特定权限。
这是一个仍然不稳定的特性,因此它的 api 可以改变,但我认为它仍然值得提及,因为它提供了大量的潜力。
您可以在https://doc.deno.land/builtin/unstable#Deno.permissions查看 Deno 的权限 API。
我们接下来要做的是确保我们的ls
程序请求文件系统读权限。 让我们按照以下步骤来做:
-
Use
Deno.permissions.request
to ask for read permissions before executing the program:… const [path = "."] = Deno.args; await Deno.permissions.request({ name: "read", path, }); for await (const dir of Deno.readDir(path)) { …
这要求对程序将要运行的目录的权限。
-
Run the program and grant permissions on the current directory:
$ deno run --unstable list-file-names-interactive-permissions.ts . Deno requests read access to ".". Grant? [g/d (g = grant, d = deny)] g list-file-names-color.ts list-file-names.ts demo-files list-file-names-interactive-permissions.ts
通过响应权限请求命令
g
,我们授予它对当前目录(.
)的访问权。现在,我们可以尝试运行相同的程序,但这次拒绝许可。
-
Run the program and deny read permissions on the current directory:
$ deno run --unstable list-file-names-interactive-permissions.ts . Deno requests read access to ".". Grant? [g/d (g = grant, d = deny)] d error: Uncaught (in promise) PermissionDenied: read access to ".", run again with the --allow-read flag at processResponse (deno:core/core.js:223:11) at Object.jsonOpAsync (deno:core/core.js:240:12) at async Object.[Symbol.asyncIterator] (deno:cli/rt/30_fs.js:125:16) at async list-file-names-interactive-permissions.ts:10:18
这就是动态权限的工作原理!
在这里,我们使用它们控制文件系统读取权限,但他们可以用来要求访问所有可用的权限(第二章中提到的【4】【5】,工具链)在运行时。 它们在编写 CLI 应用时非常有用,允许您交互地优化运行程序的权限。
使用文件系统 api
访问文件系统是我们在编写程序时的基本需求之一。 正如您可能已经在文档中看到的,Deno 提供了执行这些常见任务的 api。
由于决定标准化与 Rust 核心的通信,所有这些 api 都返回Uint8Array
,解码和编码应该由它们的消费者来完成。 这与 Node.js 有很大的不同,在 Node.js 中,有些函数返回转换后的格式,而其他函数返回 blob、buffer 等。
让我们研究一下这些文件系统 api 并读取文件的内容。
我们将使用TextDecoder
和Deno.readFile
api 读取https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/file-system/sentence.txt可用的示例文件,如下脚本所示:
const decoder = new TextDecoder()
const content = await Deno.readFile('./sentence.txt');
console.log(decoder.decode(content))
你可以注意到我们已经使用了TextDecoder
类,这是浏览器中出现的另一个 API。
当运行脚本时,不要忘记使用--allow-read
权限,以便它可以从文件系统中读取。
如果要将该文件的内容写入另一个文件,可以使用writeFile
:
const content = await Deno.readFile("./sentence.txt");
await Deno.writeFile("./copied-sentence.txt", content)
注意,我们不再需要TextEncoder
,因为我们使用从readFile
得到的Uint8Array
直接发送到writeFile
方法。 记得在运行它时使用--allow-write
标志,因为它现在正在写入文件系统。
正如您可能已经猜到或在文档中读到的,Deno 提供了一个 API,copyFile
:
await Deno.copyFile("./copied-sentence.txt",
"./using-copy-command.txt");
现在,您可能注意到我们总是在调用 Deno 名称空间函数的方法之前使用await
。
Deno 上的所有异步操作都返回一个承诺,这是我们这么做的主要原因。 我们可以使用等效的then
语法并在那里处理结果,但我们发现这样更可读。
Deno 名称空间中还包括用于删除、重命名、更改权限等的其他 api,您可以在文档中找到这些 api。
重要提示
Deno 中的许多异步 API 都有一个等效的同步API,可以用于您想要阻塞进程并获得结果的特定用例(例如,readFileSync
、writeFileSync
等)。
使用缓冲区
缓冲区表示在内存中用于存储临时二进制数据的区域。 它们通常用于处理 I/O 和网络操作。 由于异步操作是 Deno 的强项,我们将在本节中探索缓冲区。
Deno 缓冲区不同于 Node 缓冲区。 这是因为当 Node 被创建时,直到版本 4,JavaScript 中没有对ArrayBuffers
的支持。 由于 Node 为异步操作(缓冲区真正发挥作用的地方)进行了优化,其背后的团队不得不创建一个 Node 缓冲区来模拟本机缓冲区的行为。 后来,ArrayBuffers
被添加到语言中,Node 团队迁移了现有的缓冲区来利用它。 它目前不过是ArrayBuffers
的一个子类。 这个缓冲区随后在 Node 的 v10 中被弃用。 由于 Deno 是最近才创建的,它的缓冲深度利用了ArrayBuffer
。
阅读和写作来自 Deno。 缓冲
Deno 提供了一个动态长度缓冲区,实现在之上的ArrayBuffer
,一个固定的内存分配。 缓冲区提供了类似于队列的功能,不同的消费者可以在其中写入和读取数据。 正如我们最初提到的,它们大量用于网络和 I/O 等工作,因为它们允许异步读写。
举个例子,假设您有一个应用,该应用正在编写您想要处理的一些日志。 你可以在它们来的时候同步进行,或者你可以让应用写入缓冲区,让消费者异步处理它们。
让我们为这种情况写一个小程序。 我们将编写两个短程序。 第一个将模拟生成日志的应用; 第二种方法将使用缓冲区来消耗这些日志。
我们将从编写模拟生成日志的应用的代码开始。 在https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/buffers/logs/example-log.txt,有一个文件,其中有一些示例日志,我们将使用:
const encoder = new TextEncoder();
const fileContents = await Deno.readFile("./example-log.txt ");
const decoder = new TextDecoder();
const logLines = decoder.decode(fileContents).split("\n");
export default function start(buffer: Deno.Buffer) {
setInterval(() => {
const randomLine = Math.floor(Math.min(Math.random() * 1000, logLines.length));
buffer.write(encoder.encode(logLines[randomLine]));
}, 100)
}
这段代码从示例文件中读取内容,并将其分成几行。 然后,它获得一个随机行号,每 100 毫秒将该行写入缓冲区。 然后,该文件导出一个函数,我们可以调用该函数来开始“生成随机日志”。 我们将在下一个脚本中使用它来模拟生成日志的应用。
下面是有趣的部分:我们将按照以下步骤编写基本的日志处理器:
- 创建一个缓冲区,并将其发送到我们刚刚编写的日志生成器的
start
函数: -
Call the
processLogs
function to start processing the log entries present in the buffers:… start(buffer); processLogs(); async function processLogs() {}
如你所见,将调用
processLogs
函数,但不会发生任何事情,因为我们还没有实现一个程序来实现它。 -
Create an object type of
Uint8Array
inside theprocessLogs
function and read the content of the buffer there:… async function processLogs() { const destination = new Uint8Array(100); const readBytes = await buffer.read(destination); if (readBytes) { // Something was read from the buffer } }
文档(https://doc.deno.land/builtin/stable#Deno.Buffer)说明,当有东西要读时,
Deno.Buffer
中的read
函数返回读的字节数。 当没有东西可读时,缓冲区为空,并返回 null。 -
现在,在
if
内部,我们可以解码所读取的内容,因为我们知道它是Uint8Array
格式:const decoder = new TextDecoder(); … if (readBytes) { const read = decoder.decode(destination); }
-
To print the decoded value on the console, we can use the already known
console.log
. We can also do it differently, by usingDeno.stdout
(https://doc.deno.land/builtin/stable#Deno.stdout) to write to the standard output.Deno.stdout
是 Deno(https://doc.deno.land/builtin/stable#Deno.Writer)中的writer
对象。 我们可以使用它的write
方法将文本发送到那里:const decoder = new TextDecoder(); const encoder = new TextEncoder(); … if (readBytes) { const read = decoder.decode(destination); await Deno.stdout.write(encoder.encode(`${read}\n`)); }
通过这个,我们写入
Deno.stdout
,我们刚刚读取的值。 我们还在末尾添加了一个换行符(\n
),这样在控制台上就更容易读懂了。如果我们让它这样,这个
processLogs
函数将只运行一次。 当我们想要再次运行它并检查buffer
中是否有更多的日志时,我们需要安排它稍后再次运行。 -
使用
setTimeout
调用相同processLogs
功能 100ms 从现在起:async function processLogs() { const destination = new Uint8Array(100); const readBytes = await buffer.read(destination); if (readBytes) { … } setTimeout(processLogs, 10); }
例如,如果我们打开example-log.txt
文件,我们可以看到包含以下格式的日期行:Thu Aug 20 22:14:31 WEST 2020
。
让我们想象一下,我们只想打印带有Tue
的日志。 让我们来编写这样做的逻辑:
async function processLogs() {
const destination = new Uint8Array(100);
const readBytes = await buffer.read(destination);
if (readBytes) {
const read = decoder.decode(destination);
if (read.includes("Tue")) {
await Deno.stdout.write(encoder.encode(`${read}\n`));
}
}
setTimeout(processLogs, 10);
}
然后,我们在包含example-logs.txt
文件的文件夹中执行程序:
$ deno run --allow-read index.ts
Tue Aug 20 17:12:05 WEST 2019
Tue Sep 17 02:19:56 WEST 2019
Tue Dec 3 14:02:01 CET 2019
Tue Jul 21 10:37:26 WEST 2020
从缓冲区中读取日期为的日志行,符合我们的标准。
这是一个简短的演示,演示了如何使用缓冲区。 我们能够异步地从缓冲区写入和读取数据。 例如,这种方法允许使用者处理文件的一部分,而应用正在读取文件的其他部分。
Deno 名称空间提供了比我们在这里尝试的多得多的功能。 在本节中,我们决定挑选几个部分,让您了解它的支持程度。
从第 4 章、构建 web 应用开始,我们将使用这些功能,并结合第三方模块和标准库来编写 web 服务器。
使用标准库
在本节中,我们将探讨 Deno 标准库提供的行为。 它目前被运行时认为是不稳定的,因此模块是分开版本控制的。 在我们编写的时候,标准库是版本 0.83.0。
正如我们前面提到的,Deno 在添加标准库时非常谨慎。 核心团队希望它能提供足够的行为,这样人们就不需要依赖数百万个外部包来做某些事情,但同时又不想添加太多的 API 界面。 这是一个很难达到的微妙平衡。
基于 golang 的假设灵感,大多数 Deno 标准库函数模仿谷歌创建的语言。 这是因为 Deno 团队真正相信golang发展其标准库的方式,一个众所周知的很好的抛光库。 有趣的是,Ryan Dahl (Deno 和 Node 的创造者)在他的一次演讲中提到,当 pull 请求向标准库中添加新的 api 时,就需要相应的golang实现。
我们不会详细介绍整个库,原因和我们没有详细介绍整个 Deno 名称空间是一样的。 我们要做的是用它构建几个有用的程序,同时学习它能做什么。 我们将从生成 id、日志记录到 HTTP 通信,以及其他已知的用例。
为我们简单的 ls 增添色彩
几页前,我们在*nix 系统中构建了一个非常粗略的和简单的ls
命令的“克隆”。 当时我们列出了这些文件,以及它们的大小和修改日期。
为了开始研究标准库,我们将在该程序的终端输出中添加一些颜色。 让我们把文件夹名称打印成红色,这样我们就可以很容易地区分它们。
我们将创建一个名为list-file-names-color.ts
的文件。 这一次我们将使用 TypeScript,因为我们会得到更好的完成,因为标准库和 Deno 命名空间函数都是用 TypeScript 编写的。
让我们来看看允许我们给文本着色的标准库函数(https://deno.land/std@0.83.0/fmt/colors.ts)。
如果我们想看一个模块的文档,我们可以直接进入代码,但我们也可以使用doc
命令或文档网站。 我们将使用后者。
导航到 https://doc.deno.land/https/deno.land/std@0.83.0 / fmt / colors.ts。 所有列出的可用方法都显示在屏幕上:
- 从标准库的格式库中导入打印红色文本的方法:
- 在遍历当前目录中的文件的
async
迭代器中使用: -
通过运行它在
demo-files
文件夹(https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/ls),我们得到了印在红色的文件夹(不可能看到这个,在印刷书籍中,但是你可以在本地运行它):$ deno run –allow-read list-file-names-color.ts file-with-no-content.txt demo-folder .hidden-file lorem-ipsum.txt
我们现在有一个更好的ls
命令,使我们能够区分文件夹和文件,使用颜色功能从标准库。 标准库提供了许多其他模块,我们将在本书的过程中看到这些模块。 其中一些将在我们开始编写自己的应用时使用。
我们将特别注意的一个模块是 HTTP 模块,在下一节中我们将大量使用它。
使用 HTTP 模块构建 web 服务器
这本书的主要重点,连同介绍 Deno 和如何使用它,是学习如何使用它来构建 web 应用。 在这里,我们将创建一个简单的 JSON API 来介绍 HTTP 模块。
我们将构建一个 API 来保存和列出注释。 我们称之为便利贴。 想象一下,这就是为您的即时贴板提供信息的 API。
在 Web api 和 Deno 标准库 HTTP 模块的函数的帮助下,我们将创建一个非常简单的路由系统。 请记住,我们这样做是为了探索 api 本身,因此这不是适合生产的代码。
让我们首先创建一个名为post-it-api
的文件夹和一个名为index.ts
的文件。 再一次,我们将使用 TypeScript,因为我们相信它的自动补全和类型检查功能极大地改善了我们的体验,减少了可能出现的错误。
本节的最终代码可在https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/steps/7.ts获取:
- 首先将标准库 HTTP 模块导入到我们的文件:
-
Write the logic to handle requests by using
AsyncIterator
, as we did in previous examples:console.log("Server running at port 8080"); for await (const req of serve({ port: 8080 })) { req.respond({ body: "post-it api", status: 200 }); }
如果我们现在运行它,这是我们得到的。 记住,我们需要使用
--allow-net
标志,在权限部分提到,它有网络访问:deno run --allow-net index.ts Server running at port 8080
-
为了清晰起见,我们可以将端口和服务器实例提取到一个单独的变量:
const PORT = 8080; const server = serve({ port: PORT }); console.log("Server running at port", PORT); for await (const req of serve({ port: PORT })) { …
我们的服务器工作起来了,就像以前一样,有一个小的不同,现在代码看起来(可以说)更可读的配置变量在文件的顶部。 稍后我们将学习如何从代码中提取这些代码。
返回一份便签清单
我们的第一个必要条件是,我们有一个返回列表的便利贴的 API。 这些将由名称、标题和创建日期组成。 在我们到达那里之前,为了让我们有多个路径,我们需要一个路由系统。
为了这个练习的目的,我们将构建我们的练习。 这是我们了解 Deno 内建的一些 api 的方式。 我们稍后会同意,在编写生产应用时,有时重用经过测试和大量使用的软件要比重复发明轮子更好。 然而,为了学习的目的,完全可以重新发明轮子。
为了创建我们的基本路由系统,我们将使用一些您可能在浏览器中知道的 api。 对象如URL
、UrlSearchParams
等。
我们的目标是能够通过 URL 和路径定义路由。 像GET /api/post-its
这样的就好了。 让我们做它!
-
首先创建一个
URL
对象(https://developer.mozilla.org/en-US/docs/Web/API/URL)来帮助我们解析 URL 及其参数。 我们将提取HOST
和PROTOCOL
到另一个变量,这样我们就不必重复:const PORT = 8080; const HOST = "localhost"; const PROTOCOL = "http"; const server = serve({ port: PORT, hostname: HOST }); console.log(`Server running at ${HOST}:${PORT}`); for await (const req of server) { const url = new URL(`${PROTOCOL}://${HOST}${req.url}`); req.respond({ body: "post-it api", status: 200 }); }
-
Use the created
URL
object to do some routing. We'll use aswitch case
for that. When no route matches, a404
should be sent to the client:const pathWithMethod = `${req.method} ${url.pathname}`; switch (pathWithMethod) { case "GET /api/post-its": req.respond({ body: "list of all the post-its", status: 200 }); continue; default: req.respond({ status: 404 }); }
提示
您可以使用
--unstable
和--watch
标志一起运行您的脚本,在文件更改时重新启动它,如下:deno run``--allow-net``--watch``--unstable``index.ts
-
Access
http://localhost:8080/api/post-its
and confirm we have the correct response. Any other routes will get a 404 response.注意,我们使用
continue
关键字使 Deno 在响应请求后跳出当前迭代(记住,我们是在for
循环中)。你可能已经注意到,目前,我们只是根据路径而不是方法路由。 这意味着向
/api/post-its
发出的任何请求,无论是POST
还是GET
,都将得到相同的响应。 让我们继续前进来解决这个问题。 -
Create a variable that contains the request method and the pathname:
const pathWithMethod = `${req.method} ${url.pathname}` switch (pathWithMethod) {
我们现在可以定义我们的路线,我们希望的方式,
GET /api/post-its
。 现在我们有了路由系统的基础知识,我们将编写返回便利贴的逻辑。 -
创建 TypeScript 接口,帮助我们维护便利贴的结构:
-
Create a variable that will work as our in-memory database for this exercise.
我们将使用一个 JavaScript 对象,其中键是 id,值是我们刚刚定义的
PostIt
类型的对象:let postIts: Record<PostIt["id"], PostIt> = {}
-
Add a couple of fixtures to our database:
let postIts: Record<PostIt["id"], PostIt> = { '3209ebc7-b3b4-4555-88b1-b64b33d507ab': { title: 'Read more', body: 'PacktPub books', id: 3209ebc7-b3b4-4555-88b1-b64b33d507ab ', createdAt: new Date() }, 'a1afee4a-b078-4eff-8ca6-06b3722eee2c': { title: 'Finish book', body: 'Deno Web Development', id: '3209ebc7-b3b4-4555-88b1-b64b33d507ab ', createdAt: new Date() } }
注意,我们正在手工生成id。 稍后,我们将使用标准库中的另一个模块来完成这项工作。 让我们回到 API 并更改处理路由的
case
。 -
Change the
case
that will return all the post-its instead of the hardcoded message.由于我们的数据库是一个键/值存储,我们需要使用
reduce
来使用所有的 post-its 创建一个数组(删除代码块中高亮显示的行):case GET "/api/post-its": req.respond({ body: "list of all the post-its", status: 200 }); const allPostIts = Object.keys(postIts). reduce((allPostIts: PostIt[], postItId) => { return allPostIts.concat(postIts[postItId]); }, []); req.respond({ body: JSON.stringify({ postIts: allPostIts }) }); continue;
-
Run the code and go to
/api/post-its
. We should have our post-its listed there!您可能已经注意到,它仍然不是 100%正确的,因为我们的 API 正在返回 JSON,它的头与负载不匹配。
-
我们将使用一个我们从浏览器知道的 API,
Headers
对象(https://developer.mozilla.org/en-US/docs/Web/API/Headers)来添加content-type
。 删除下面代码块中高亮显示的行:
我们已经在上面创建了一个Headers
对象的实例,然后我们在响应req.respond
上使用它。 通过这种方式,我们的 API 现在更加连贯、易于理解并遵循标准。
在数据库中添加便利贴
现在我们有了一种方式来阅读我们的便利贴,我们将需要一种方式来添加新的便利贴,因为拥有一个完全静态内容的 API 没有多大意义。 这就是我们要做的。
我们将使用我们创建的路由基础设施来添加一个路由,该路由允许我们将记录插入到数据库中。 由于我们遵循 REST 准则,该路径将与列出post-its
的路径相同,但使用不同的方法:
- 定义一个总是返回
201
状态码的路由: -
Testing it, with the help of
curl
, we can see it's returning the correct status code:$ curl -I -X POST http://localhost:8080/api/post-its HTTP/1.1 201 Created content-length: 0
重要提示
我们正在使用
curl
,但您可以自由使用您最喜欢的 HTTP 请求工具,您甚至可以使用一个图形客户端,如 Postman(https://www.postman.com/)。让我们使这条新路线发挥它应有的作用。 它应该得到一个 JSON 有效负载并使用它创建一个新的便利贴。
通过查看标准库的 HTTP 模块(https://doc.deno.land/https/deno.land/std@0.83.0/ HTTP /server.ts#ServerRequest)的文档,我们知道请求的主体是一个Reader对象。 该文档包含了一个如何从它读取的示例。
-
按照建议,阅读这个值并打印出来,以便更好地理解它:
-
Make a request with
body
, with the help ofcurl
:$ curl -X POST -d "{\"title\": \"Buy milk\"}" http://localhost:8080/api/post-its
请求成功,但它只返回一个
201
状态码。 如果我们看看正在运行的服务器,像这样的东西会打印到控制台:Uint8Array(25) [ 123, 34, 116, 105, 116, 108, 101, 34,58,32,34,84, 101, 115, 116, 32, 112, 111, 115, 116, 45, 105, 116, 34, 125 ]
我们之前了解到,Deno 使用
Uint8Array
与 Rust 后端进行所有通信,这也不例外。 然而,Uint8Array
并不是我们当前想要的,我们想要的是请求体的实际文本。 -
Use
TextDecoder
to get the request body as a readable value. After doing this, we'll log the output again and we'll make a new request:$ deno -X POST -d "{\"title\": \"Buy milk\"}" http://localhost:8080/api/post-its
这是服务器这次打印到控制台的内容:
{"title": "Buy milk "}
我们到达那里!
-
Since the body is a string, we need to parse it into a JavaScript object. We'll use an old friend of ours,
JSON.parse
:const decoded = JSON.parse(new TextDecoder().decode(body));
现在我们有了可以操作的请求体格式,这几乎就是创建新数据库记录所需要的全部内容。 让我们按照以下步骤创建一个:
-
使用标准库中的
uuid
模块(https://deno.land/std@0.83.0/uuid)为我们的记录生成一个随机的 uuid: -
In our route's switch case, we'll create an
id
with the help of thegenerate
method and insert it in the database, adding thecreatedAt
date on top of what the user sent in the request payload. For the sake of this example, we're skipping validation:case "POST /api/post-its": … const decoded = JSON.parse(new TextDecoder().decode(body)); const id = v4.generate(); postIts[id] = { ...decoded, id, createdAt: new Date() } req.respond({ status: 201, body: JSON.stringify(postIts[id]), headers });
注意,我们使用的是之前定义的
headers
对象(在GET
路由中),因此我们的 API 使用Content-Type: application/json
进行响应。然后,当我们遵循REST指南时,我们返回
201``Created
代码和创建的记录。 -
Save the code, restart the server, and run it again:
$ curl -X POST -d "{\"title\": \"Buy groceries\", \"body\":\"1 x Milk\"}" http://localhost:8080/api/post-its {"title":"Buy groceries","body":"1 x Milk","id":"9a3c6a56-713b-4b8c-80a0-8d5a37aaefee","createdAt":"2020-09-08T00:13:46.124Z"}
我们已经成功了! 我们现在可以对列出所有便利贴的路由执行一个
GET
请求,以检查记录是否真的被插入到数据库中:$ curl http://localhost:8080/api/post-its {"postIts":[{"title":"Read more","body":"PacktPub books","id":"3209ebc7-b3b4-4555-88b1-b64b33d507ab","createdAt":"2021-01-10T16:28:52.210Z"},{"title":"Finish book","body":"Deno Web Development","id":"a1afee4a-b078-4eff-8ca6-06b3722eee2c","createdAt":"2021-01-10T16:28:52.210Z"},{"title":"Buy groceries","body":"1 x Milk","id":"b35b0a62-4519-4491-9ba9-b5809b4810d5","createdAt":"2021-01-10T16:29:05.519Z"}]}
和它的工作原理! 我们现在有了一个 API,可以返回并将便利贴添加到列表中。
这差不多概括了我们将在本章的 HTTP 模块中使用的 api。 由于大多数 api(比如我们编写的 api)都是供前端应用使用的,我们将这样做来结束本章。
提供一个前端
由于超出了本书的范围,我们将不再编写与此 API 交互的前端代码。 但是,如果您想在获取并显示便利贴的单页应用中使用它,我已经在书的文件中包含了一个便利贴(https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/index.html)。
我们将在这里学到的是如何使用我们刚刚构建的 web 服务器来服务一个 HTML 文件:
-
First, we need to create a route at the root of our server. Then, we need to set the correct
Content-Type
and return the file's content by using the already known filesystem APIs.为了获得 HTML 文件的路径在当前文件,我们将使用 URL 对象一起
import.meta
声明从 JavaScript (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta),它包含当前文件的路径:import { resolve, fromFileUrl } from "https://deno.land/std@0.83.0/path/mod.ts"; … case "GET /": const file = await Deno.readFile( resolve(fromFileUrl(import.meta.url), "..", "index.html") ); let htmlHeaders = new Headers(); htmlHeaders.set("content-type", "text/html"); req.respond({ body: new TextDecoder().decode(file), headers: htmlHeaders }) continue;
我们使用 Deno 的标准库中的
resolve
、and``fromFileUrl
方法来获取相对于当前文件的 URL。注意,我们现在需要使用
--allow-read
标志来运行它,因为我们的代码是从文件系统中读取的。 -
In order for us to be a little more secure, we will specify the exact folder the program can read, by sending it to the
--allow-read
flag:$ deno run --allow-net --allow-read=. index.ts Server running at http://0.0.0.0:8080
这将防止我们从任何可能允许恶意的人读取我们的文件系统的错误。
-
用浏览器访问 URL,你应该会进入一个页面,在那里我们可以看到我们添加的 fixture
post-its
。 要添加一个新的,您也可以点击添加一个新的便利贴文本,并填写表单:
图 3.2 -前端使用即时贴 API
重要提示
请记住,在许多生产环境中,不推荐使用 API 服务于前端代码。 在这里,我们这样做是为了学习,所以我们可以理解标准库 HTTP 模块的一些可能性。
在本节中,我们学习了如何使用标准库提供的模块。 我们制作了一个非常常见的命令ls
的简单版本,并使用来自标准库的输出格式函数给它添加了一些颜色。 为了完成本节,我们使用两个端点创建了一个 HTTP API,这些端点列出并保存了记录。 我们学习了不同的要求,并学习了如何使用 Deno 来完成这些要求。
小结
随着我们阅读这本书,我们对 Deno 的了解变得更加实用,我们开始将它用于更接近真实世界的用例。 这就是这一章的内容。
在本章的开始,我们学习了运行时的一些基本特征,即程序生命周期,以及 Deno 如何看待模块稳定性和版本控制。 我们通过编写一个简单的程序,从网站上获取 Deno 徽标,将其转换为 base64,并将其放入 HTML 页面,从而迅速转向 Deno 提供的 Web api。
然后,我们进入了Deno
名称空间,并探讨了它的一些低级功能。 我们用文件系统 API 构建了几个示例,最后用它构建了ls
命令的基本副本。
缓冲区是 Node 世界中大量使用的东西,具有执行异步读写行为的能力。 正如我们所知,Deno 与 Node.js 共享了许多用例,这使得在本章中不能不讨论缓冲区。 我们首先解释了 Deno 缓冲区与 Node.js 的不同之处,最后构建了一个小应用来处理异步读写。
为了结束这一章,我们接近了本书的主要目标之一,使用 Deno 进行 web 开发。 我们使用 Deno 创建了第一个 JSON API。 在这个过程中,我们了解了多个 Deno api,甚至构建了基本的路由系统。 然后,我们创建了一些路由,在我们的数据存储中列出并创建了记录。 在本章的末尾,我们学习了如何在 api 中处理头文件,并将它们添加到端点中。
我们通过直接从 web 服务器服务一个单页应用完成了本章; 使用我们的 API 并与之交互的单页应用。
这一章涉及了很多内容。 我们开始构建比以前更接近现实的 api。 我们还了解了如何使用 Deno 进行开发、使用权限和文档。
本章结束了我们的入门旅程,希望你对接下来会发生什么感到好奇。
在接下来的四章中,我们将构建一个 web 应用,并探讨在此过程中所做的所有决定。 到目前为止,你学到的大部分知识以后还会用到,但也会有大量新的、令人兴奋的东西出现。 在下一章中,我们将开始创建一个 API,随着章节的进展,我们将向其添加特性。
我希望你能加入我们!
版权属于:月萌API www.moonapi.com,转载请注明出处