七、浏览器中的 HTTPS、提取配置和 Deno

在上一章中,我们基本上总结了应用的特性。 我们添加了授权和持久性,最终使应用连接到 MongoDB 实例。 在本章中,我们将重点讨论生产应用中一些已知的标准最佳实践:基本安全实践和配置处理。

首先,我们将添加几个基本安全特性我们应用编程接口(API),从跨源资源共享(【显示】歌珥)保护,使过滤请求基于他们的起源。 然后,我们将学习如何在我们的应用中启用HyperText Transfer Protocol Secure(HTTPS),以支持加密连接。 这将允许用户使用安全连接执行对 API 的请求。****

到目前为止,我们已经使用了一些秘密值,但我们并不担心在代码中包含它们。 在本章中,我们将提取配置和秘密,以便它们不必存在于代码库中。 然后我们将学习如何安全地将它们存储并注入到应用中。 通过这种方式,我们可以确保这些值是保密的,并且没有出现在代码中。 通过这样做,我们还可以使用不同的配置实现不同的部署。

接下来,我们将探索一个特定的 Deno 特性所支持的功能:在浏览器中编译和运行代码的能力。 通过使用 Deno 与 ECMAScript 6(由现代浏览器支持)的兼容性,我们将在 API 和前端之间共享代码,实现一个全新的可能性世界。

利用这个特定的特性,我们将探索一个特定的场景:为 API 构建一个 JavaScript 客户端。 这个客户机将使用同样在服务器上运行的相同类型和部分代码构建,我们将探讨它提供的好处。

本章总结了本书的构建应用部分,在这部分中我们一步一步地构建应用,用增量的方法添加一些常见的应用功能。 在学习的同时,我们也确保这个应用尽可能接近真实的入门书籍。 这使我们能够在创建功能应用时了解 Deno、它的许多 api 和一些社区包。

在本章结束时,你将熟悉以下主题:

  • 启用 CORS 和 HTTPS
  • 提取配置和秘密
  • 在浏览器中运行 Deno 代码

技术要求

本章需要的所有代码文件都可以在下面的 GitHub 链接中找到:

https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections

启用 CORS 和 HTTPS

CORS 保护和 HTTPS 支持被认为是任何正在运行的生产应用中至关重要的两件事。 本节将解释如何将它们添加到正在构建的应用中。

还有许多其他的安全实践可以添加到任何 API 中。 因为这些都不是 Deno 的具体细节,所以我们决定将重点放在这两个元素上。

我们将从学习 CORS 开始,以及我们如何利用oak和我们所知道的中间件功能特性来实现它。 然后,我们将学习如何使用自签名证书并使 API 处理安全的 HTTP 连接。

开始吧,从 CORS 开始。

启用 CORS

如果您不熟悉 CORS,可以告诉您这是一种机制,它使服务器能够指示浏览器应该从哪个来源加载资源。 当应用运行在与 API 相同的域上时,甚至不需要 CORS,因为名称会直接显示出来。

我将为您提供以下的引用自 Mozilla Developer Network(MDN),解释 CORS:

跨源资源共享(CORS)是一种基于 http 报头的机制,它允许服务器指明任何其他来源(域、协议或端口),而浏览器应该允许从它自己的来源加载资源。 CORS 还依赖于一种机制,通过这种机制,浏览器向承载跨源资源的服务器发出“预飞行”请求,以检查服务器是否允许实际的请求。 在预飞行中,浏览器发送指示 HTTP 方法和实际请求中将使用的头信息。”

给你一个更具体的例子,假设你有一个运行在the-best-deno-api.com的 g API,你想处理来自the-best-deno-client.com的请求。 在这里,您将希望您的服务器为the-best-deno-client.com域启用 CORS。

如果你没有启用它,浏览器会向你的 API 发出一个预飞行请求(使用OPTIONS方法),这个请求的响应将没有一个Access-Control-Allow-Origin: the-best-deno-client.com头,导致请求失败,浏览器阻止进一步的请求。

我们将学习如何在应用中启用此机制,允许从下面示例中的http://localhost:3000发出请求。

由于我们的应用使用oak框架,我们将学习如何使用这个框架。 然而,这与任何其他 HTTP 框架都非常相似。 我们基本上想要添加一个中间件功能来处理请求,并根据允许的域列表验证它们的来源。

我们将使用一个名为cors(https://deno.land/x/cors@v1.2.1)的社区包,但是实现非常简单。 如果您对它的功能感到好奇,可以查看https://deno.land/x/cors@v1.2.1/oakCors.ts,因为代码非常简单。

重要提示

我们将使用在上一章中创建的代码来启动这个实现。 可在https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/sections/4-connecting-to-mongodb/museums-api上下载。 你也可以在这里查看完成的代码:

https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections/3-deno-on-the-browser/museums-api

在这里,我们将把cors包和我们自己的允许域列表一起添加到我们的应用中。 最终目标是我们可以从一个可信的网站向这个 API 执行请求。

让我们做它。 进行如下:

  1. 通过更新deps文件安装cors模块(参考第 3 章Runtime and Standard Library,)。 代码可以在下面的代码片段中看到:
  2. 下一步,使用cache命令更新lock文件,更新方法如下:

    $ deno cache --lock=lock.json --lock-write --unstable src/deps.ts

  3. Import oakCors on src/web/index.ts and register it on the application, before the router is registered, as follows:

    import { Algorithm, oakCors } from "../deps.ts" … app.use( oakCors({ origin: ['http://localhost:3000'] }) ); const apiRouter = new Router({ prefix: "/api" });

    注意我们是如何使用oakCors中间件创建者函数的,通过向它发送一个允许的来源数组——在本例中是http://localhost:3000。 这将使 API 用一个Access-Control-Allow-Origin: http://localhost:3000报头响应OPTIONS请求,该报头将向浏览器发出信号,如果请求的网站运行在http://localhost:3000上,它应该允许进一步的请求。

    这将工作只是很好。 然而,有这个硬编码域似乎有点奇怪。 我们已经向应用注入了所有类似的配置。 还记得我们对port配置做了什么吗? 我们对允许的域做同样的处理。

  4. Change the createServer function parameters to receive an array of string named allowedOrigins inside configuration and later send it to the oakCors middleware creator function. The code for this is shown here:

    interface CreateServerDependencies { configuration: { port: number, authorization: { key: string, algorithm: Algorithm }, allowedOrigins: string[] }, museum: MuseumController, user: UserController } … export async function createServer({ configuration: { port, authorization, allowedOrigins, }, museum, user }: CreateServerDependencies) { … app.use( oakCors({ origin: allowedOrigins }) );

    我们改变了函数参数的类型,使用解构从参数中获取变量,并将其发送给oakCors中间件创建者。

  5. There's one thing missing, though—we need to send this array of allowedOrigins from src/index.ts. Let's do this, as follows:

    createServer({ configuration: { port: 8080, authorization: { key: authConfiguration.key, algorithm: authConfiguration.algorithm }, allowedOrigins: ['http://localhost:3000'] }, museum: museumController, user: userController })

    这应该就是我们所需要的! 我们现在可以尝试从浏览器环境访问它,从运行在http://localhost:3000的应用。

  6. 让我们从运行 API 开始测试,如下所示:

  7. To test it, create an HTML file named index.html in the root folder (museums-api), with a script that performs a POST request to http://localhost:8080/api/users/register. The code for this is shown here:

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test CORS</title> </head> <body> <div id="status"></div> <script type="module"> fetch("http://localhost:8080/api/users/register", { body: JSON.stringify({ username: "abcd", password: "abcd" }), method: "POST", }) .then(() => { document.getElementById("status").innerHTML = "WORKING"; }) .catch(() => { document.getElementById("status").innerHTML = "NOT WORKING"; }); </script> </body> </html>

    我们还添加了一个div标签,并在请求工作或失败的情况下更改其内部 HTML 代码,以便我们更容易诊断。

    为了让我们服务 HTML 文件并进行测试,您可以利用 Deno 及其运行远程脚本的能力。

  8. 在创建index.html文件的同一个文件夹中,让我们运行 Deno 的标准库 web 服务器,使用-p标志将端口设置为3000--host将主机设置为localhost。 代码如下:

    $ deno run --allow-net --allow-read https://deno.land/std@0.83.0/http/file_server.ts -p 3000 --host localhost HTTP server listening on http://localhost:3000/

  9. Access http://localhost:3000 with your browser and you should see a WORKING message, as illustrated in the following screenshot:

    Figure 7.1 – Testing that the CORS API is working

    图 7.1 -测试 CORS API 是否工作

  10. If you want to test what happens when the origin is not in the allowedOrigins list, you can run the same command but with a different port (or host) and check the behavior. The code for this is shown here:

    $ deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts -p 3001 --host localhost HTTP server listening on http://localhost:3001/

    您现在可以在浏览器上导航到新的统一资源定位器(URL),而您应该看到一个NOT WORKING消息。 如果您查看浏览器的控制台,您还可以确认浏览器正在警告您 CORS 飞行前请求失败。 这就是我们想要的行为。

这就是我们在 API 上启用 CORS 所需要的一切!

我们使用的第三方模块有更多的选项可以探索,比如过滤特定的 HTTP 方法或用不同的状态码回答飞行前的请求。 目前,默认选项对我们是有效的。 现在,我们将继续讨论如何让用户通过 HTTPS 连接到应用,并添加额外的安全和加密层。

启用 HTTPS

现在任何面向用户的应用不仅应该允许而且应该强制用户通过 HTTPS 连接。 这是在 HTTP 之上添加的安全层,确保所有连接都通过可信证书加密。 同样,我们不会尝试给出定义,而是使用来自 MDN(https://developer.mozilla.org/en-US/docs/Glossary/https)的以下定义:

“HTTPS (HyperText Transfer Protocol Secure)是 HTTP 协议的加密版本。 它使用 SSL 或 TLS 加密客户机和服务器之间的所有通信。 这种安全连接允许客户与服务器安全地交换敏感数据,例如在执行银行活动或在线购物时。”

通过在我们的应用中启用 HTTPS 连接,我们可以确保拦截和解释请求变得更加困难。 如果不这样做,恶意用户就可以拦截登录请求,并访问用户的密码和用户名组合。 我们在保护用户的敏感数据。

当我们在应用中使用oak时,我们将在其文档中寻找如何支持 HTTPS 连接的解决方案。 通过查看https://doc.deno.land/https/deno.land/x/oak@v6.3.1 mod.ts,我们可以看到Application.listen方法接收一个configuration对象,同样的我们以前用来发送port变量。 还有其他选项,我们可以在这里看到:https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#Application。 我们将使用它来启用 HTTPS。

让我们看看如何改变的配置,使oak支持安全连接,通过以下步骤:

  1. Go to src/web/index.ts and add the secure, keyFile, and certFile options to the listen method call, as follows:

    await app.listen({ port, secure: true, certFile: './certificate.pem', keyFile: './key.pem' });

    certFilekeyFile属性期望有一个到证书和密钥文件的路径。

    如果您没有证书,或者不知道如何创建自签名证书,请不要担心。 因为这只是为了学习的目的,你可以从书的文件https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections/1-enabling-cors-and-https/museums-api使用我们的。 在这里,您可以找到certificate.pemkey.pem文件,您可以下载和使用。 您可以在计算机中的任何位置下载它们,但是在下一个代码示例中,我们将假设它们位于项目根文件夹(museums-api)。

  2. 为了保持代码的整洁和可配置性,让我们提取这些选项,并将它们作为参数发送到createServer函数,如下所示:

  3. 参数类型应该是这样的:
  4. 这就是createServer函数之后的样子,带有破坏参数:

    export async function createServer({ configuration: { port, authorization, allowedOrigins, secure, keyFile, certFile, }, museum, user }: CreateServerDependencies) { … await app.listen({ port, secure, keyFile, certFile });

  5. To wrap up, we will now send the paths to the certificate and key files from the src/index.ts file, as follows:

    createServer({ configuration: { port: 8080, authorization: { key: authConfiguration.key, algorithm: authConfiguration.algorithm }, allowedOrigins: ['http://localhost:3000'], secure: true, certFile: './certificate.pem', keyFile: './key.pem' }, museum: museumController, user: userController })

    现在,为了保持日志的准确性,我们需要修复之前创建的事件监听器,该监听器记录了应用正在运行的日志。 这个处理程序现在应该考虑到应用可能在 HTTP 或 HTTPS 上运行,并根据这一点进行日志记录。

  6. 返回src/web/index.ts并修复正在监听listen事件的事件监听器,以便它检查连接是否安全。 代码如下:

    app.addEventListener('listen', e => { console.log(`Application running at ${e.secure ? 'https' : 'http'}://${e.hostname || 'localhost'}:${port}`) })

  7. 让我们运行应用,看看它是否工作,如下所示:

您现在应该能够访问该 URL 并连接到应用。

您可能仍然会看到安全警告,但不用担心。 您可以点击AdvancedProceed to localhost (unsafe),如下图所示:

Figure 7.2 – Chrome security warning screen

图 7.2 - Chrome 安全警告界面

这是因为证书是自签名的,而不是由受信任的证书颁发机构签名的。 然而,这并不重要,因为过程与生产证书完全相同。

如果你仍然有问题,你可能需要在打开这个页面(https://localhost:8080/)之前直接访问 API URL。 从这里开始,您可以按照下面链接(https://jasonmurray.org/posts/2021/thisisunsafe/)上的步骤来启用与不使用受信任证书的 API 的通信。 之后,访问https://localhost:8080将起作用。

一旦您拥有了由受信任的证书颁发机构签署的正确证书,您就可以像使用这个证书一样使用它,一切都会正常工作。

这部分就到这里吧! 我们已经在现有的应用中添加了 CORS 和 HTTPS 支持,提高了它的安全性。

在下一节中,我们将看到如何从代码中提取配置和秘密,使其更加灵活和可从外部配置。

我们走吧!

提取配置和秘密

任何应用,独立于其维度,将有配置参数。 通过查看我们在之前的章节中构建的应用,即使我们查看它们最简单的版本——Hello Worldweb server,我们也会找到配置值,比如port值。

我们在createServer函数(启动 web 服务器的函数)内发送一个名为configuration的完整对象,这也不是巧合。 与此同时,我们还知道一些值在应用中应该是保密的。 它们目前存在于代码库中,因为它一直在为我们的目的(即学习)而工作,但我们想要改变它。

我们正在考虑的事情,如JSON Web Token(JWT)加密密钥,或 MongoDB 凭证。 这些绝对不是您想要检查到您的版本控制系统中的内容。 这就是这一节的内容。

我们将查看当前存在于代码库中的配置值和秘密。 我们将提取它们,以便它们可以被保密,并且只在应用运行时传递给它。

当应用中的配置值分散在多个模块和文件中时,执行这个过程可能是一项艰巨的工作。 然而,由于我们遵循一些体系结构最佳实践,并考虑保持代码的解耦和可配置性,因此我们的工作变得简单了一些。

通过查看src/index.ts,您可以确认我们正在使用的所有配置值和秘密都在那里。 这意味着所有其他模块都不知道配置,这就是它应该做的。

我们将分两个阶段进行“迁移”。 首先,我们将把所有配置值提取到configuration模块中,然后提取秘密。

创建配置文件

首先,让我们找出在配置文件中应该存在的代码中的硬编码值。 下面的代码片段突出显示了我们不希望在代码中出现的值:

client.connectWithUri("mongodb+srv://deno-
  api:password@denocluster.wtit0.mongodb.net/
 ?retryWrites=true&w=majority")
const db = client.database("getting-started-with-deno");

const authConfiguration = {
  algorithm: 'HS512' as Algorithm,
  key: 'my-insecure-key',
  tokenExpirationInSeconds: 120
}
createServer({
  configuration: {
    port: 8080,
    authorization: {
      key: authConfiguration.key,
      algorithm: authConfiguration.algorithm
    },
    allowedOrigins: ['http://localhost:3000'],
    secure: true,
    certFile: './certificate.pem',
    keyFile: './key.pem'
  },

通过查看应用代码中的这个片段,我们已经可以识别一些东西,如下所示:

  • 集群 URL 和数据库名称(用户名和密码保密)
  • JWT 算法,过期时间(密钥是秘密)
  • web 服务器端口
  • 歌珥允许起源
  • HTTPS 证书和密钥文件路径

这些是我们要提取的元素。 首先,我们将创建包含所有这些值的配置文件。

我们将使用YAML Ain't Markup Language(YAML),因为这是一种常用的配置文件类型。 如果你对它不熟悉,不用担心,它很容易掌握。 您可以在官网https://yaml.org/了解它的工作原理。

我们还将确保针对不同的环境有不同的配置文件,从而创建一个名称中包含环境的文件。

接下来,我们将实现一个特性,允许我们将配置存储在一个文件中,首先创建文件本身,如下所示:

  1. Create a config.dev.yaml file at the root of the project and add all the configurations there, like this:

    web: port: 8080 cors: allowedOrigins: - http://localhost:3000 https: key: ./key.pem certificate: ./certificate.pem jwt: algorithm: HS512 expirationTime: 120 mongoDb: clusterURI: deno-cluster.wtit0.mongodb.net/ ?retryWrites=true&w=majority database: getting-started-with-deno

    现在我们需要一种将该文件加载到应用中的方法。 我们将在src文件夹中创建一个名为config的模块。

    为了读取配置文件,我们将使用在第 2 章工具链中学到的文件系统函数,以及 Deno 标准库中的encoding包。

  2. Create a src/config folder with a file named index.ts inside.

    在这里,我们将定义并导出一个名为load的函数。 这个函数将负责加载配置文件。 代码如下所示:

    export async function load() { }

  3. 因为我们使用的是 TypeScript,所以我们将定义配置文件的类型,并将其添加为load函数的返回类型。 这应该与我们先前创建的配置文件的结构匹配。 代码如下:

    import type { Algorithm } from "../deps.ts"; type Configuration = { web: { port: number }, cors: { allowedOrigins: string[], }, https: { key: string, certificate: string }, jwt: { algorithm: Algorithm, expirationTime: number }, mongoDb: { clusterURI: string, database: string }, } export async function load(): Promise<Configuration> { …

  4. Inside the load function, we should now try to load the configuration file we previously created, by using Deno filesystem APIs. As there can be multiple files depending on the environment, we'll also add env as a parameter to the load function, with the default value of dev, as follows:

    export async function load(env = 'dev'): Promise<Configuration> { const configuration = await Deno.readTextFile (`./config.${env}.yaml`) as Configuration; …

    这样,我们就可以访问配置文件的内容了。 然而,它目前只是一个文本文件。 我们想把它作为一个 JavaScriptObject,这样我们就可以访问它。 为此,我们将使用标准库中的 YAML 编码功能。

  5. 安装 YAML 编码器模块从 Deno 标准库,使用deno cache``lock确保我们更新文件(指【5】3 章【显示】,*运行时和标准库),和出口src/deps.ts,

    export { parse } from "https://deno.land/std@0.71.0/encoding/yaml.ts"

    如下: * Import it on src/config/index.ts and use it to parse the contents of the read file, as follows:

    import { Algorithm, parse } from "../deps.ts"; … export async function load(env = 'dev'): Promise<Configuration> { const configuration = parse(await Deno.readTextFile (`./config.${env}.yaml`)) as Configuration; return configuration }

    现在应该可以在应用中使用配置的内容了。 让我们回到src/index.ts开始吧。

    • Import the config module, call its load function, and use the configuration values, which previously were hardcoded values.

    这是之后的src/index.ts文件应该是这样的:

    import { load as loadConfiguration } from './config/index.ts'; const config = await loadConfiguration(); … client.connectWithUri(`mongodb+srv:// deno-api:password @${config.mongoDb.clusterURI}`); … const authConfiguration = { algorithm: config.jwt.algorithm, key: 'my-insecure-key', tokenExpirationInSeconds: config.jwt.expirationTime } … createServer({ configuration: { port: config.web.port, authorization: { key: authConfiguration.key, algorithm: authConfiguration.algorithm, }, allowedOrigins: config.cors.allowedOrigins, secure: true, certFile: config.https.certificate, keyFile: config.https.key }, …

    现在,我们应该能够像以前那样运行应用,不同的是,我们的所有配置现在都位于一个单独的文件中。*

*这就是关于配置的内容! 我们已经将配置从代码中提取到一个config文件中,使它们更容易阅读和维护。 我们还创建了一个模块来抽象所有配置文件的读取和解析,以确保应用的其余部分不关心这一点。

接下来,我们将学习如何扩展这个config模块,以便它也合并从环境中读取的秘密值。

访问秘密值

正如我前面提到的,我们使用了一些应该保密的值,但我们最初将它们保留在代码中。 这些值可能会随着环境的变化而变化,出于安全原因,我们希望对配置保密。 这一需求使得不可能将它们检出到版本控制中,因此它们必须存在于其他地方。

一种常见的做法是使用环境变量从环境中获取这些值。 Deno 提供了一个 API,我们将使用它从环境变量中读取数据。 我们将扩展config模块,以便它也包含其导出的Configuration类型对象的秘密值。

以下是一些被认为是秘密的值,它们仍然存在于代码中:

  • MongoDB username
  • MongoDB 密码
  • JWT 加密密钥

让我们将它们从代码中取出,并按照以下步骤将它们添加到configuration对象中:

  1. In src/config/index.ts, add the MongoDB username and password to the configuration and the key to JWT in the configuration type, as follows:

    type Configuration = { web: {…}; cors: {…}; https: {…}; jwt: { algorithm: Algorithm; expirationTime: number; key: string; }; mongoDb: { clusterURI: string; database: string; username: string; password: string; }; }

    在配置类型上已经有了这些属性,我们现在需要一种方法将它们添加到那里。 让我们扩展load函数,以便它扩展configuration对象。

  2. Extend the configuration object to include the username and password missing properties on mongoDb and key on jwt, as follows:

    export async function load(env = 'dev'): Promise<Configuration> { const configuration = parse(await Deno.readTextFile (`./config.${env}.yaml`)) as Configuration; return { ...configuration, mongoDb: { ...configuration.mongoDb, username: 'deno-api', password: 'password' }, jwt: { ...configuration.jwt, key: 'my-insecure-key' } }; }

    唯一要做的就是从环境中获取这些值,而不是在这里硬编码它们。 我们将为此使用 Deno 的 API,以便访问环境(https://doc.deno.land/builtin/stable#Deno.env)。

  3. 使用Deno.env.get从环境中获取变量。 我们还应该设置一个默认值,以防env变量不存在。 代码如下所示:

    export async function load(env = 'dev'): Promise<Configuration> { const configuration = parse(await Deno.readTextFile (`./config.${env}.yaml`)) as Configuration; return { ...configuration, mongoDb: { ...configuration.mongoDb, username: Deno.env.get ('MONGODB_USERNAME') ||'deno-api', password: Deno.env.get ('MONGODB_PASSWORD') || 'password' }, jwt: { ...configuration.jwt, key: Deno.env.get('JWT_KEY') || 'insecure-key' } } }

  4. Let's get back to src/index.ts and use the secret values that we just added to the configuration object, as follows:

    client.connectWithUri (`mongodb+srv://${config.mongoDb.username}: ${config.mongoDb.password} @${config.mongoDb.clusterURI}`); const db = client.database(config.mongoDb.database); … const authConfiguration = { algorithm: config.jwt.algorithm, key: config.jwt.key, tokenExpirationInSeconds: config.jwt.expirationTime }

    现在我们在正确的条件下运行应用,将秘密值作为环境变量发送。 请记住,应用要访问环境,它需要--allow-env权限。 让我们试一试。

    只要确保添加了之前创建的用户名和密码即可。 代码可以在以下代码片段中看到:

$ MONGODB_USERNAME=add-your-username MONGODB_PASSWORD=add-your-password JWT_KEY=add-your-jwt-key deno run --allow-net --unstable --allow-env --allow-read --allow-plugin src/index.ts
Application running at https://localhost:8080

现在,如果我们尝试注册和登录,我们将验证一切正常。 应用连接到 MongoDB,并正在正确地检索 JWT 令牌——秘密正在工作!

Windows 用户注意事项

在 Windows 操作系统下,可以使用set命令(https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1)设置环境变量。 Windows 不支持内联设置环境变量,因此,您必须在运行 API 之前运行这些命令。 在整本书中,我们将使用*nix 语法,但如果使用 Windows,则必须使用set命令,如下代码所示。

以下是 Windows 的set命令:

C:\Users\alexandre>set MONGODB_USERNAME=your-username
C:\Users\alexandre>set MONGODB_PASSWORD=your-password
C:\Users\alexandre>set JWT_KEY=jwt-key

我们只是设法从代码中提取了所有配置和秘密! 此步骤通过将配置写入文件,使配置更容易读取和维护,并通过环境将机密信息发送到应用,而不是将它们放在代码库中,从而使机密信息更加安全。

我们正在接近一个可以在不同环境中轻松部署和配置的应用,我们将在第 9 章部署 Deno 应用中做一些事情。

在下一节中,我们将利用 Deno 的功能为浏览器绑定代码,创建一个连接到 API 的非常简单的 JavaScript 客户机。 前端客户端可以使用该客户端,这样 HTTP 连接就被抽象了; 它还将与 API 代码共享代码和类型。

快上车!

在浏览器中运行 Deno 代码

我们在之前的章节中提到过一件事,我们认为 Deno 的卖点之一就是它与 ECMAScript6 的完全兼容性。 这使得在浏览器上编译和运行 Deno 代码成为可能。 这个编译是由 Deno 自己完成的,而捆绑器包含在工具链中。

这个特性提供了大量的可能性。 其中很多都是由于 API 和客户端之间共享代码的能力,这也是我们将在本节探讨的内容。

我们将构建一个非常简单的 JavaScript 客户端来与刚刚构建的 Museums API 交互。 然后,任何想要连接到 API 的浏览器应用都可以使用这个客户机。 我们将在 Deno 中编写这个客户机,并将其捆绑在一起,以便客户机可以使用它,甚至由应用本身提供服务。

我们将编写的客户端将是一个非常基本的 HTTP 客户端,因此我们不会过多地关注代码。 我们这样做是为了演示如何重用来自 Deno 的代码和类型来生成在浏览器上运行的代码。 同时,我们还将解释将客户机及其 API 放在一起的一些好处。

让我们从在应用中创建一个新模块开始,我们将其命名为client,如下所示:

  1. src中创建一个名为client的文件夹,其中包含一个名为index.ts的文件。
  2. Let's create an exported method, getClient, which should return an instance of our API client with three functions: login, register, and getMuseums. The code for this is shown in the following snippet:

    interface Config { baseURL: string; } export function getClient(config: Config) { return { login: () => null, register: () => null, getMuseums: () => null, }; }

    注意我们如何得到一个托管baseURLconfig对象。

  3. Now, it's just a matter of implementing the HTTP logic to make requests to the API. We'll not do a step-by-step guide to implementing this as it is quite straightforward, but you can access the full client on the book files (https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter07/sections/3-deno-on-the-browser/museums-api/src/client/index.ts).

    这就是register方法的样子:

    import type { RegisterPayload, LoginPayload, UserDto } from "../users/types.ts"; … const headers = new Headers(); headers.set("content-type", "application/json"); … register: ({ username, password }: RegisterPayload): Promise<UserDto> => { return fetch( `${config.baseURL}/api/users/register`, { body: JSON.stringify({ username, password }), method: "POST", headers, }, ).then((r) => r.json()); }, …

    注意我们是如何从users模块导入类型,并将它们添加到我们的应用中。 这将使我们的函数更具可读性,并且它将允许我们在使用 TypeScript 客户端编写测试时进行类型检查和完成。 我们还创建了一个带有content-type头的对象,将在所有请求中使用。

    通过创建一个 HTTP 客户端,我们可以自动处理诸如身份验证之类的事情。 在这种特定的情况下,我们的客户端可以自动保存令牌,并在用户登录后的未来请求中发送它。

    这就是login方法的样子:

    export function getClient(config: Config) { let token = ""; … return { … login: ( { username, password }: LoginPayload, ): Promise<{ user: UserDto; token: string }> => { return fetch( `${config.baseURL}/api/login`, { body: JSON.stringify({ username, password }), method: "POST", headers }, ).then((response) => { const json = await response.json(); token = json.token; return json; }); },

它目前正在设置客户端实例上的token变量。 该令牌随后被添加到经过身份验证的请求(如getMuseums函数)中,如下面的片段所示:

getMuseums: (): Promise<{ museums: Museum[] }> => {
  const authenticatedHeaders = new Headers();
authenticatedHeaders.set("authorization", `Bearer
 ${token}`);
  return fetch(
    `${config.baseURL}/api/users/register`,
    {
 headers: authenticatedHeaders,
    },
).then((r) => r.json());
},

在创建客户端之后,我们想要分发它。 我们可以使用 Deno bundle 命令来完成它,正如我们在第 2 章,the Toolchain中学到的。

*如果我们想让我们的 web 服务器来服务它,我们也可以通过添加一个处理程序来服务我们的客户端文件的捆绑内容。 它看起来是这样的:

apiRouter.get("/client.js", async (ctx) => {
 const {
 diagnostics,
 files,
 } = await Deno.emit(
 "./src/client/index.ts",
 { bundle: "esm" },
 );
 if (!diagnostics.length) {
 ctx.response.type = "application/javascript";
 ctx.response.body = files["deno:///bundle.js"];
 return;
 }
 });

您可能需要返回到您的.vscode/settings.json文件,并启用unstable属性,以便识别我们正在使用不稳定的 api。 下面的代码片段演示了这一点:

{
  …
 "deno.unstable": true
}

注意我们是如何使用不稳定的Deno.emitAPI 并将content-type设置为application/javascript

然后我们将 Deno(deno:///bundle.js)发出的文件作为请求体发送。

这样,如果一个客户端执行一个GET请求到/api/client.js,它将捆绑并服务我们刚刚写的客户端内容。 最终的结果将是一个捆绑的、与浏览器兼容的 JavaScript 文件,然后可以被应用使用。

最后,我们将在一个 HTML 文件中使用这个客户机,该文件将验证并从 API 获取博物馆。 进行如下:

  1. 在项目的根目录下创建一个 HTML 文件,命名为index-with-client.html,如下代码片段所示:
  2. 创建一个script标签,并直接从 API URL 导入脚本,如下所示:
  3. Now, it's just a matter of using the client we built. We'll use it to log in (with a user you previously created) and get a list of museums. The code is illustrated in the following snippet:

    async function main() { const client = getClient ({ baseURL: "https://localhost:8080" }); const username = window.prompt("Username"); const password = window.prompt("Password"); await client.login({ username, password }); const { museums } = await client.getMuseums(); museums.forEach((museum) => { const node = document.createElement("div"); node.innerHTML = `${museum.name} – ${museum.description}`; document.body.appendChild(node); }); }

    当用户访问页面时,我们将使用window.prompt获取用户名和密码,然后我们将使用该数据登录并使用它来获取博物馆。 在此之后,我们将把它添加到文档对象模型(DOM),创建一个博物馆列表。

  4. 让我们再次启动应用,如下所示:

  5. 然后,服务于前端应用,这次添加带有路径的–cert--key标志到各自的文件,以运行带有 HTTPS 的文件服务器,如下面的代码片段所示:
  6. 我们现在可以通过 https://localhost:3000/index-with-client.html 访问网页,输入用户名和密码,屏幕上就会出现博物馆列表,如下面的截图所示:

Figure 7.3 – Web page with a JavaScript client getting data from the API

图 7.3 -使用 JavaScript 客户端从 API 获取数据的 Web 页面

要在上一步中登录,需要使用之前在应用上注册的用户。 如果你没有,你可以使用下面的命令创建它:

$ curl -X POST -d'{"username": "your-username", "password": "your-password" }' -H 'Content-Type: application/json' https://localhost:8080/api/users/register

请确保用所需的用户名替换your-username,用所需的密码替换your-password

这样,我们就完成了在浏览器上使用 Deno 的部分!

我们刚刚做的事情还可以进一步探索,释放出巨大的潜力; 这只是应用于我们用例的一个快速示例。 这种做法使得任何浏览器应用都更容易与我们刚刚编写的应用集成。 客户机将不必处理 HTTP 逻辑,而只需调用方法并接收它们的响应。 正如我们看到的,这个客户机还可以自动处理身份验证和 cookie 等主题。

本节探讨了 Deno 支持的一个特性:为浏览器编译代码。

我们已经将它应用到应用的上下文中,创建了一个 HTTP 客户机,该客户机将用户从 API 中抽象出来。 这个特性可以用来做很多事情,目前正在 Deno 内部编写前端 JavaScript 代码。

像我们解释第二章,工具链,我们唯一编写代码时必须考虑到浏览器并不是使用Deno函数名称空间。 通过遵循这些限制,我们可以很容易地使用 Deno 的所有优点编写代码,并将其编译为 JavaScript 以供发布。

这只是对一个非常有前景的特性的介绍。 与 Deno 一样,该功能仍处于初始阶段,社区将发现它的许多重要用途。 既然你也意识到了这一点,我相信你也会想出好主意的。

小结

在这一章中,我们重点讨论了一些实践,这些实践使我们的应用更接近可以部署到生产环境中的状态。 我们从探索基本的安全实践开始,将 CORS 机制和 HTTPS 添加到 API 中。 这两个特性在任何应用中都是非常标准的,在我们现有的基础上进行了很大的安全改进。

此外,考虑到部署应用,我们还从代码库中抽象了配置和秘密。 我们首先创建一个抽象来处理它,这样配置就不会分散,模块只接收它们的配置值,而不知道它们是如何加载的。 然后,我们继续在我们当前的代码库中使用这些值,这看起来相当容易。 此步骤从代码中删除所有配置值,并将它们移动到配置文件中。

完成配置之后,我们使用创建的相同抽象来处理应用中的秘密。 我们实现了一个从环境变量加载值并将它们添加到应用配置的特性。 然后,我们在需要的地方使用这些秘密值,比如 MongoDB 凭据和令牌签名密钥。

我们通过探索 Deno 最初提供的一种可能性来结束本章:为浏览器绑定代码。 将这个特性应用到我们的应用上下文中,我们决定编写一个 JavaScript HTTP 客户机来连接到 API。

这一步探索了 API 和客户端之间共享代码的部分潜力,开启了一个充满可能性的世界。 通过这些,我们探索了如何使用 Deno 的这个绑定特性在运行时编译文件并将其提供给用户。 这个特性的部分优点也将在下一章中探讨,在那里我们将为我们的应用编写单元和集成测试。 这些测试的一部分将使用这里创建的 HTTP 客户机,利用这种做法的一个巨大优势:客户机和服务器处于相同的代码库中。

在下一章中,我们将深入讨论测试。 我们将为我们在本书其余部分中编写的逻辑编写测试,从业务逻辑开始。 我们将学习如何通过添加测试来提高代码库的可靠性,以及我们创建的层和架构在编写时是如何至关重要的。 我们将编写的测试将从单元测试转到集成测试,并且我们将探索应用它们的用例。 当涉及到编写新特性和维护旧特性时,我们将看到测试所增加的价值。 在此过程中,我们将学习一些新的 Deno api。

在编写测试之前,代码还没有完成,因此我们将编写它们来结束 API。

我们走吧!**