五、向 Oak 添加用户和迁移

至此,我们已经为 web 应用的结构打下了基础,该结构将使我们能够添加更多的功能。 正如你可能已经猜到这一章的名字,我们将从添加我们选择的中间件框架到当前的 web 应用 Oak 开始这一章。

与 Oak 一起,由于我们的应用开始有更多的第三方依赖项,我们将使用我们在前几章学到的创建一个锁文件,并在安装依赖项时执行完整性检查。 这样,我们就可以保证我们的应用在没有依赖问题的情况下顺利运行。

随着我们进入这一章,我们将开始理解如何使用 Oak 的特性来简化我们的代码。 我们将使我们的路由逻辑更可扩展,也更可扩展。 我们的第一个解决方案是使用if语句和标准库一起创建一个 DIY 路由解决方案,我们将在这里对其进行重构。

一旦我们这样做了,我们将得到更清晰的代码,并能够使用 Oak 的特性,如自动内容类型定义、处理不允许的方法和路由前缀。

然后,我们将添加一个对几乎所有应用都至关重要的特性:用户。 我们将在博物馆旁边创建一个模块来处理所有与用户相关的内容。 在这个新模块中,我们将开发创建用户的业务逻辑,以及在数据库中创建新用户的代码,方法是使用散列和盐等常见实践。

在实现这些特性时,我们将了解 Deno 提供的其他模块,例如标准库的散列特性或运行时中包含的加密 api。

添加这个新模块并让它与应用的其余部分交互将是测试应用架构的一种很好的方法。 通过这样做,我们将了解它如何在将与上下文相关的所有内容保持在单个位置的情况下伸缩。

本章将涵盖以下主题:

  • 管理依赖项和锁定文件
  • 用 Oak 编写 web 服务器
  • 向应用添加用户
  • 让我们开始吧!

技术要求

这一章将建立在我们在前一章开发的代码之上。 本章的所有代码文件都可以在本书的 GitHub 仓库https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections中找到。

管理依赖和锁定文件

第二章工具链中,我们学习了 Deno 如何帮助我们进行依赖管理。 在本节中,我们将在更实际的环境中使用它。 我们将从开始,从我们的代码中删除所有分散的带有 url 的导入,并将它们移动到一个中央依赖文件中。 在此之后,我们将创建一个锁文件,以确保我们仍然年轻的应用在安装的任何地方都能顺利运行。 最后,我们将学习如何安装基于锁文件的项目依赖项。

使用集中式依赖文件

在前一章中,你可能注意到我们在代码中直接使用 url 来依赖。 尽管这是可能的,但这是我们在几章之前就不鼓励的。 它在第一个阶段对我们是有效的,但是随着应用开始发展,我们必须正确地管理我们的依赖关系。 我们希望避免依赖版本冲突、url 中的拼写错误以及依赖分散在文件中。 要解决这个问题,我们必须做到以下几点:

  1. Create a deps.ts file at the root of the src folder.

    这个文件可以有您喜欢的任何名称。 我们目前称它为deps.ts,因为它是在 Deno 的文档中提到的,这是许多模块使用的命名约定。

  2. Move all the external dependencies from our code to deps.ts.

    目前,我们拥有的唯一依赖是来自标准库的 HTTP 模块,可以在src/web/index.ts文件中找到。

  3. 将导入移动到deps.ts文件中,并将import更改为export:

    export { serve } from "https://deno.land/std@0.83.0/http/server.ts"

  4. Notice how the fixed version is on the URL:

    export { serve } from "https://deno.land/std@0.83.0/http/server.ts"

    这就是在 Deno 中版本控制的工作方式,正如我们在第二章工具链学到的。

    我们现在需要更改依赖文件,以便它们直接从deps.ts文件导入,而不是直接从 URL 导入。

  5. src/web/index.ts中,从deps.ts中导入serve方法:

    import { serve } from "../deps.ts";

通过有一个集中的依赖文件,我们也有一个简单的方法来确保我们在本地下载了所有的依赖,而不需要运行任何代码。 这样,我们现在就有了一个文件,可以在其中运行deno cache命令(在第 2 章工具链中提到)。

创建锁文件

在集中了我们的依赖之后,我们需要保证无论谁安装了这个项目,都会得到与我们相同的依赖版本。 这是我们能够保证代码以相同方式运行的唯一方法。 我们将使用一个锁文件来做到这一点。 我们在第二章The Toolchain中学习过。 这里,我们将把它应用到我们的应用中。

让我们运行带有locklock-write标志的cache命令,加上一个到锁文件的路径和一个到集中式依赖文件deps.ts的路径:

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

应该在当前文件夹中生成一个lock.json文件。 如果您打开它,它应该包含 URL 的键值对,以及用于执行完整性检查的散列。

然后应该将这个锁文件添加到版本控制中。 之后,如果一个同事想要安装这个相同的项目,他们只需要运行没有--lock-write标志的相同命令:

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

这样,src/deps.ts(这应该是所有的)中的依赖项将被安装,并根据lock.json文件检查它们的完整性。

现在,每当我们在项目中安装一个新的依赖项时,我们必须运行带有locklock-write标志的deno``cache命令,以确保锁文件被更新。

这部分差不多就到这里了!

在本节中,我们学习了确保应用平稳运行的一个简单但非常重要的步骤。 这有助于我们避免未来的麻烦问题,如依赖冲突和跨版本的行为不匹配。 我们还保证了资源的完整性,这一点在 Deno 中甚至更为重要,因为它的依赖项托管在 URL 中,而不是注册表中。

在下一节,我们将开始重构我们的应用从标准库 HTTP 服务器到 Oak,这将导致我们的 web 代码被简化。

用 Oak 编写 web 服务器

在上一章的结尾,我们看到了不同的网络图书馆。 经过简短的分析,我们最终选择了 Oak。 在本节中,我们将重写部分 web 应用,以便使用它来代替标准库中的 HTTP 模块。

让我们打开src/web/index.ts,一步一步地解决它。

根据 Oak 的文档(https://deno.land/x/oak@v6.3.1),我们需要做的唯一一件事就是实例化Application对象,定义中间件,并调用listen方法。 让我们做到:

  1. Add Oak's import to the deps.ts file:

    export { Application } from "https://deno.land/x/oak@v6.3.1/mod.ts"

    如果你正在使用 VSCode,那么你可能已经注意到有一个警告,说它无法在本地找到这个版本的依赖。

  2. Let's run the commands from the previous section to download it and add it to the lock file.

    不要忘记这样做,每次我们添加依赖,这样我们有更好的自动完成,我们的锁文件总是更新:

    $ deno cache --lock=lock.json --reload --lock-write src/deps.ts Download https://deno.land/std@0.83.0/http/server.ts Download https://deno.land/x/oak@v6.3.1/mod.ts Download https://deno.land/std@0.83.0/encoding/utf8.ts …

    下载了所有必需的依赖项后,让我们开始在代码中使用它们。

  3. 删除src/web/index.tscreateServer函数的所有代码。

  4. src/web/index.ts内部,导入Application类并实例化它。 创建一个非常简单的中间件(如文档中所述)并调用listen方法:

记住,在删除旧代码的同时,我们也删除了,因此它还不会打印任何东西。 让我们运行它并验证它没有问题:

$ deno run --allow-net src/index.ts  

现在,如果我们访问http://localhost:8080,我们将看到“Hello World!”响应。

现在,您可能想知道 Oak 应用的use方法是什么。 我们将使用这个方法来定义中间件。 现在,我们只希望它修改响应并向其主体添加一条消息。 在下一章中,我们将更深入地学习中间件函数。

还记得吗?当我们删除console.log时,如果应用正在运行,我们不会得到任何反馈。 我们将学习如何做到这一点,同时学习如何向 Oak 应用添加事件监听器。

向 Oak 应用添加事件监听器

到目前为止,我们已经设法让应用运行,但在时刻,我们没有任何消息来确认这一点。 我们将以此为借口来学习 Oak 中的事件监听器。

Oak 应用分派两种不同类型的事件。 其中一个是listen,而另一个是the listen event,当应用运行时,我们将使用它来记录到控制台。 另一个是error,当发生错误时,我们将使用它来记录到控制台。

首先,让我们在app.listen语句之前为listen事件添加事件监听器:

app.addEventListener("listen", e => {
  console.log(`Application running at 
    http://${e.hostname || 'localhost'}:${port}`)
})
…
await app.listen({ port });

注意,我们不仅将消息打印到控制台,还将从事件中打印hostname并发送一个默认值(以防未定义)。

为了安全和确保捕获任何意外错误,我们还可以添加一个错误事件侦听器。 如果在我们的应用中发生了一个未被处理的错误,这个错误事件将被触发:

app.addEventListener("error", e => {
  console.log('An error occurred', e.message);
})

这些处理程序,特别是error,将帮助我们很多,当我们正在开发和想要收集关于正在发生的事情的快速反馈。 稍后,当接近生产阶段时,我们将添加适当的日志中间件。

现在,你可能会认为我们仍然缺少我们开始这个章节时拥有的功能,你是对的:我们已经从应用中删除了列出所有博物馆的端点。

让我们再次添加它,并学习如何在 Oak 应用中创建路由。

在 Oak 应用中处理路由

除了Application类,Oak 还提供了另一个对象,允许我们定义路由——Router类。 我们将使用它来重新实现之前的路由,该路由列出了应用中的所有博物馆。

让我们通过将 prefix 属性发送给构造函数来创建它。 这样做意味着在那里定义的所有路由都将以该路径作为前缀:

import { Application, Router } from "../deps.ts";

const apiRouter = new Router ({ prefix: "/api" })

现在,让我们回到我们的功能,它通过一个GET请求返回/api/museums的博物馆列表:

apiRouter.get("/museums", async (ctx) => {
  ctx.response.body = {
    museums: await museum.getAll()
  }
});

这里发生了一些事情。

这里,我们使用 Oak 的路由 API 通过发送 URL 和处理函数来定义路由。 然后我们的处理程序被一个上下文(ctx)对象调用。 所有这些都在 Oak 的文档(https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#Router)中有详细的解释,但我将留给你一份简短的简历。

在 Oak 中,处理器所能做的一切都是通过 context 对象完成的。 发出的请求在ctx.request属性中可用,而当前请求的响应在ctx.response中可用。 在这些对象中可以使用头文件、cookie、参数、主体等等。 一些属性,如ctx.response.body,是可写的。

提示

你可以通过查看 Deno 的文档网站:https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts来更好地了解 Oak 的功能。

在本例中,我们使用响应主体属性来设置其内容。 当 Oak 可以推断出响应的类型(这里是 JSON)时,它会自动将正确的Content-Type标头添加到响应中。

我们将在本书中学习更多关于 Oak 及其特性的知识。 下一步是连接我们最近创建的路由。

连接路由到应用

既然已经定义了路由,我们需要在应用上注册它,以便它可以开始处理请求。

为了做到这一点,我们将使用之前使用过的应用实例的方法——use方法。

在 Oak 中,一旦定义了Router(及其注册),它就提供两个返回中间件函数的方法。 然后可以使用这些函数在应用上注册路由。 它们是:

  • routes:在应用中注册已注册的路由处理程序。
  • allowedMethods:为路由中没有定义的 API 调用注册自动处理程序,返回一个405 – Not allowed响应。

我们将使用两个在主应用中注册我们的路由,如下:

const apiRouter = new Router({ prefix: "/api" })
apiRouter.get("/museums", async (ctx) => {
  ctx.response.body = {
    museums: await museum.getAll()
  }
});
app.use(apiRouter.routes());
app.use(apiRouter.allowedMethods());
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

这样,我们的路由就可以在应用中注册它的处理器,它们就可以开始处理请求了。

请记住,我们必须在前面定义的 Hello World 中间件之前注册它们。 如果我们不这样做,Hello World 处理程序将在所有请求到达我们的路由之前响应它们,因此它不会工作。

现在,我们可以通过运行以下命令来运行我们的应用:

$ deno run --allow-net src/index.ts
Application running at http://localhost:8080

然后,我们可以对 URL 执行一个curl:

$ curl http://localhost:8080/api/museums
{"museums":[{"id":"1fbdd2a9-1b97-46e0-b450-62819e5772ff","name":"The Louvre","description":"The world's largest art museum and a historic monument in Paris, France.","location":{"lat":"48.860294","lng":"2.33862"}}]}

正如我们所看到的,一切都按照预期进行! 我们已经成功地将应用迁移到 Oak。

通过这样做,我们极大地提高了代码的可读性。 我们还使用 Oak 处理我们不想处理的东西,我们设法专注于我们的应用。

在下一节中,我们将向应用添加用户的概念。 将创建更多的路由,以及一个全新的模块和一些业务逻辑来处理用户。

提示

本章代码可用,按章节分开,地址为https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections

现在,让我们向应用添加一些用户!

添加用户到应用

我们目前有的第一个端点运行,并列出了应用中的所有博物馆,但我们仍然远远不能满足最终的要求。

我们希望添加用户,以便能够使用标识注册、登录和与应用交互。

我们将从创建定义用户的对象开始,然后进入业务逻辑来创建和存储它。 在此之后,我们将创建允许我们通过 HTTP 与应用交互的端点,从而允许用户注册。

创建用户模块

我们目前在应用中有一个我们可以称为单个“模块”的东西:museums模块。 与博物馆相关的一切都在那里,从控制器到存储库、对象定义,等等。 这个模块有一个单一的接口,这是它的index.ts文件。

我们这样做是为了我们可以在模块内部自由工作,同时维护它的外部 API,因此它总是稳定的。 这给我们提供了模块之间良好的解耦程度。 确保零件在一个模块解耦合理,我们也必须通过构造函数注入他们的依赖关系,让我们轻松地交换块和测试它们在隔离(您将看到在第八章,测试-单元测试和集成)。

按照这些指导方针,我们将继续使用这个“模块”系统,并通过以下步骤为我们的用户创建一个:

  1. 创建一个名为src/users的文件夹,并将index.ts文件放入其中。
  2. Create the src/users/types.ts file. This is where we'll define the User type:

    export type User = { username: string, hash: string, salt: string, createdAt: Date }

    我们的用户对象将非常简单:它将有一个username,一个createdAt日期,以及两个属性:hashsalt。 我们将使用这些来保护用户的密码,当我们存储它。

  3. src/users/controller.ts中使用register方法创建用户控制器。 它应该接收一个用户名和密码,然后在数据库中创建一个用户:

  4. Define RegisterPayload in src/users/types.ts and export it in src/users/index.ts, removing it from src/users/controller.ts

    src/users/types.ts内添加以下内容:

    // src/users/types export type RegisterPayload = { username: string; password: string };

    src/users/index.ts内添加以下内容:

    export type { RegisterPayload, } from "./types.ts";

    现在让我们在这里停止,并考虑寄存器逻辑。

    要创建用户,必须检查数据库中是否存在该用户。 如果没有,我们将使用输入的用户名和密码创建它们,然后返回一个不包含敏感数据的对象。

    在前一章中,我们每次想要与数据源交互时都使用存储库模式。 存储库保存所有的数据访问逻辑(src/museums/repository.ts)。

    这里,我们要做同样的事。 我们已经注意到控制器需要在UserRepository中调用两个方法:一个检查用户是否存在,另一个创建用户。 这就是我们接下来要定义的接口。

  5. Go to src/users/types.ts and define the interface for UserRepository:

    export type CreateUser = Pick<User, "username" | "hash" | "salt">; … export interface UserRepository { create: (user: CreateUser) => Promise<User> exists: (username: string) => Promise<boolean> }

    注意,我们创建的CreateUser类型包含User的所有属性,createdAt除外,该属性应该由存储库添加。

    定义了UserRepository接口后,我们现在可以转移到用户控制器,并确保它在构造函数中接收到存储库的实例。

  6. In src/users/controller.ts, create a constructor that receives the user repository as an injected parameter and sets the class property with the same name:

    import type { UserRepository } from "./types.ts"; type RegisterPayload = { username: string, password: string }; … interface ControllerDependencies { userRepository: UserRepository; } export class Controller { userRepository: UserRepository; constructor({ userRepository }: ControllerDependencies) { this.userRepository = userRepository; } public async register(payload: RegisterPayload) { // Logic to register users } }

    现在我们可以访问userRepository了,我们可以开始为register方法编写逻辑。

  7. Write the logic for the register method, check if the user exists, and create them if not:

    async register(payload: RegisterPayload) { if (await this.userRepository.exists(payload.username)) { return Promise.reject("Username already exists"); } const createdUser = await this.userRepository.create( { username: payload.username, hash: "random-hash", salt: "random-salt" } ) return createdUser; }

    注意,我们是如何将一个直接字符串作为 hash 和 salt 属性发送到userRepositorycreate方法,以确保它遵循前面定义的CreateUser类型。 这些必须是自动生成的,但现在不要担心。

    有了这个,我们就基本完成了对当有人试图注册我们的应用时会发生什么情况的研究。

    不过,我们还缺一样东西。 您可能已经注意到,我们直接从存储库返回User对象,其中可能包含敏感信息,即hashsalt属性。

  8. Create a type called UserDto in src/users/types.ts that defines the format of the User object without sensitive data:

    export type User = { username: string, hash: string, salt: string, createdAt: Date } export type UserDto = Pick<User, "createdAt" | "username">

    注意,我们使用 TypeScript 的PickUser对象中选择两个属性; 即createdAtusername

    定义了UserDto(https://en.wikipedia.org/wiki/Data_transfer_object)后,我们现在可以确保我们的寄存器正在返回它。

  9. 创建一个名为src/users/adapter.ts的文件,其中有一个名为userToUserDto的函数,该函数将用户转换为UserDto:

    import type { User, UserDto } from "./types.ts"; export const userToUserDto = (user: User): UserDto => { return { username: user.username, createdAt: user.createdAt } }

  10. 在 register 方法中使用最近创建的函数,以确保返回一个UserDto:

    import { userToUserDto } from "./adapter.ts"; … public async register(payload: RegisterPayload) { … const createdUser = await this.userRepository.create( payload.username, payload.password ); return userToUserDto(createdUser); }

这样,register方法就完成了!

我们目前正在发送 hash 和 salt 作为两个没有任何意义的普通字符串。

你可能想知道为什么我们不直接发送密码。 这是因为我们希望确保在任何数据库中都没有以明文形式存储密码。

为了确保我们遵循最佳实践,我们将使用散列和盐处理将用户的密码存储在数据库中。 在这样做的同时,我们还想了解更多的 Deno api。 这就是我们下一节要做的。

在数据库中存储用户

即使我们使用一个内存数据库,我们已经决定我们不会以纯文本的方式存储密码。 相反,我们将使用一种叫做哈希和盐的常用方法来存储密码。 如果你不熟悉它,auth0 有一篇很棒的文章,我绝对推荐(https://auth0.com/blog/adding-salt-to-hashing-a-better-way-to-store-passwords/)。

模式本身并不复杂,您可以通过遵循代码来学习它。

我们要做的是存储哈希密码。 我们不会存储用户输入的精确的散列密码,而是密码加上一个随机生成的字符串,称为 salt。 这种盐会和密码一起储存,以便以后使用。 在此之后,我们将不再需要解码密码。

使用 salt,每当我们想检查密码是否正确时,我们只需将 salt 添加到用户输入的任何密码中,并对其进行散列,并验证输出与存储在数据库中的内容匹配。

如果您仍然觉得这很奇怪,那么我可以保证,当您查看代码时,它会变得更简单。 让我们按照以下步骤来实现这些函数:

  1. Create a utils file called src/users/util.ts with a hashWithSalt function inside it that hashes a string with the provided salt:

    提示

    标准库在https://deno.land/std@0.83.0/hash上提供了一组哈希方法。

    import { createHash } from "https://deno.land/std@0.83.0/hash/mod.ts"; export const hashWithSalt = (password: string, salt: string) => { const hash = createHash("sha512") .update(`${password}${salt}`) .toString(); return hash; };

    现在应该很清楚,这个函数将返回一个字符串,该字符串是提供的字符串的hash值,加上一个salt

    对不同的密码使用不同的 salt 也被认为是一种最佳实践(如前面提到的文章所述)。 通过为每个密码生成不同的salt,我们确保所有密码仍然是安全的,如果一个密码的盐泄漏。

    让我们通过创建一个将生成salt的函数来继续。

  2. Create a generateSalt function using the crypto API (https://doc.deno.land/builtin/stable#crypto) to get random values and generate a salt string from there:

    提示

    https://doc.deno.land/https/deno.land/std@0.83.0/encoding/hex.ts

    import { encodeToString } from "https://deno.land/std@0.83.0/encoding/hex.ts" … export const generateSalt = () => { const arr = new Uint8Array(64); crypto.getRandomValues(arr) return encodeToString(arr); }

    这就是为应用生成哈希密码所需要的全部内容。

    现在,我们可以开始使用刚刚在控制器中创建的实用函数。 让我们创建一个方法,这样我们就可以哈希我们的密码。

  3. UserController中创建一个名为getHashedUser的私有方法,该方法接收用户名和密码,并返回用户,以及他们的 hash 和 salt:

  4. 在中使用新近创建的getHashedUser方法register方法:

    public async register(payload: RegisterPayload) { if (await this.userRepository.exists(payload.username)) { return Promise.reject("Username already exists"); } const createdUser = await this.userRepository.create( await this.getHashedUser (payload.username, payload.password) ); return userToDto(createdUser); }

我们完成了! 这样,我们就可以确保没有存储任何纯文本密码。 在此过程中,我们了解了 Deno 可用的cryptoapi。

我们在使用之前定义的UserRepository接口时完成了所有这些实现。 然而,目前我们还没有实现它的类,所以让我们创建一个。

创建用户库

在上一节中,我们创建了定义UserRepository的接口,所以接下来,我们将创建一个实现它的类。 让我们开始:

  1. Create a file called src/users/repository.ts with an exported Repository class inside it:

    import type { CreateUser, User, UserRepository } from "./types.ts"; export class Repository implements UserRepository { async create(user: CreateUser) { } async exists(username: string) { } }

    该接口保证这两个公共方法必须存在。

    现在,我们需要一种方式来存储用户。 出于本章的目的,我们将再次使用内存数据库,非常类似于我们对博物馆所做的。

  2. Create a property inside the src/users/repository.ts class called storage. It should be a JavaScript Map, and it will work as the users' database:

    import { User, UserRepository } from "./types.ts"; export class Repository implements UserRepository { private storage = new Map<User["username"], User>(); …

    数据库就绪后,我们现在可以实现这两个方法的逻辑。

  3. Get the user from the database in the exists method, returning true if it is there and false if not:

    async exists(username: string) { return Boolean(this.storage.get(username)); }

    如果不能得到记录,Map#get函数将返回 undefined,因此我们将其转换为布尔值,以确保它总是返回 true 或 false。

    exists方法比较简单; 它只需要检查用户是否在数据库中,并相应地返回一个boolean

    要创建用户,我们确实需要多执行一到两个步骤。 不仅仅是创建它,我们还必须确保它还添加了一个createdAt日期给用户,这是由调用这个函数的人发送的。

    现在,让我们返回并完成我们的主要任务:在数据库中创建一个用户。

  4. Open the src/users/repository.ts file and implement the create method, creating a user object in the proper format.

    记住在发送给函数的user对象中添加createdDate:

    async create(user: CreateUser) { const userWithCreatedAt = { ...user, createdAt: new Date() } this.storage.set (user.username, { ...userWithCreatedAt }); return userWithCreatedAt; }

    这样,我们的存储库就完成了!

    它完全实现了我们之前在UserRepository接口中定义的内容,可以使用了。

    下一步是将所有这些部分连接在一起。 我们已经创建了User控制器和User存储库,但它们仍然没有被使用。

    在我们继续之前,我们需要从用户模块向外部世界公开这些对象。 我们将遵循前面定义的规则; 也就是说,模块接口将始终是其根目录下的index.ts文件。

  5. Open src/users/index.ts and export the Controller, the Repository classes, and their respective types from the module:

    ``` export { Repository } from './repository.ts'; export { Controller } from './controller.ts';

    export type { CreateUser, RegisterPayload, User, UserController, UserRepository, } from "./types.ts"; ```

    现在,我们可以确保 user 模块中的每个文件都是从这个文件(src/users/index.ts)直接导入类型,而不是直接导入其他文件。

现在,任何想要从用户模块导入内容的模块都必须通过index.ts文件。 现在,我们可以开始考虑用户将如何与我们刚刚编写的业务逻辑进行交互。 由于我们正在构建一个 API,我们将在下一节中学习如何通过 HTTP 公开它。

创建寄存器端点

业务逻辑和数据访问逻辑就绪后,唯一缺少的是用户可以调用来注册自己的端点。

对于注册请求,我们将实现一个POST /api/users/register,期望一个 JSON 对象带有一个名为user的属性,其中包含两个属性usernamepassword

我们要做的第一件事是声明src/web/index.ts中的createServer函数依赖于要注入的UserController接口。 让我们开始:

  1. In src/users/types.ts, create the UserController interface. Make sure it is also exported in src/users/index.ts:

    export type RegisterPayload = { username: string, password: string }; export interface UserController { register: (payload: RegisterPayload) => Promise<UserDto> }

    记住,我们也从之前的src/users/controller.ts移动了RegisterPayload

  2. 现在,为了保持整洁,转到src/users/controller.ts,确保类实现了UserController:

    import { RegisterPayload, UserController, UserRepository } from "./types.ts"; export class Controller implements UserController

  3. Back inside src/web/index.ts, add UserController to the createServer dependencies:

    import { UserController } from "../users/index.ts"; interface CreateServerDependencies { configuration: { port: number }, museum: MuseumController, user: UserController } export async function createServer({ configuration: { port }, museum, user }: CreateServerDependencies) { …

    我们现在准备创建我们的寄存器处理程序。

  4. Create a handler that responds to a POST request in /api/users/register and creates a user using the injected controller's register method:

    apiRouter.post("/users/register", async (ctx) => { const { username, password } = await ctx.request.body({ type: 'json' }).value; if (!username || !password) { ctx.response.status = 400; return; } try { const createdUser = await user.register({ username, password }); ctx.response.status = 201; ctx.response.body = { user: createdUser }; } catch (e) { ctx.response.status = 400; ctx.response.body = { message: e.message }; } });

    这里正在发生一些事情。 让我们来分析一下。

    首先,我们使用post方法来定义一个接受 POST 请求的路由。 然后,我们使用请求的body方法(https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#ServerRequest)来获得 JSON 格式的输出。 然后我们做一个简单的验证,检查用户名和密码是否出现在请求主体中,在底部,我们使用控制器注入的 register 方法。 我们将它包装在try``catch中,因此可以在发生错误时返回 HTTP 状态码400

这应该足以让网络层能够完美地回答我们的请求。 现在,我们只需要把所有东西联系起来。

连接用户控制器和 web 层

我们已经创建了应用的基本部分。 有业务逻辑,有数据访问逻辑,还有处理请求的 web 服务器。 唯一缺少的是把他们联系起来的东西。 在本节中,我们将实例化已经定义的接口的实际实现,并将它们注入到需要它们的内容中。

回到src/index.ts。 让我们做一些类似于我们做的museums模块。 在这里,我们将导入用户存储库和控制器,实例化它们,并将控制器发送到createServer函数。

按照以下步骤来做:

  1. src/index.ts中,从用户模块中导入用户ControllerRepository并实例化它们,同时发送必要的依赖项:
  2. 发送用户控制器到createServer功能:

    createServer({ configuration: { port: 8080 }, museum: museumController, user: userController })

这样,我们就完成了! 最后,让我们运行下面的命令来运行我们的应用:

$ deno run --allow-net src/index.ts
Application running at http://localhost:8080

现在,让我们通过使用curl/api/users/register发出请求来测试注册端点:

$ curl -X POST -d '{"username": "alexandrempsantos", "password": "testpw" }' -H 'Content-Type: application/json' http://localhost:8080/api/users/register
{"user":{"username":"alexandrempsantos","createdAt":"2020-10-06T21:56:54.718Z"}}

如我们所见,它正在工作并返回UserDto的内容。 我们本章的主要目标已经实现了:我们已经创建了用户模块,并添加了一个端点来注册用户!

小结

我们的应用在本章中经历了一个巨大的变化!

我们首先将应用从标准库 HTTP 模块迁移到 Oak。 我们不仅迁移了服务于应用的逻辑,还开始使用 Oak 的路由定义一些路由。 我们注意到,由于 Oak 封装了以前手工完成的部分工作,应用逻辑开始变得更简单。 我们成功地从标准库迁移了所有的 HTTP 代码,而无需更改业务逻辑,这是一个很好的迹象,表明我们在应用架构方面做得很好。

我们继续前进,并学习了如何在 Oak 应用中监听和处理事件。 随着我们开始编写更多代码,我们也越来越熟悉 Oak,理解它的功能,探索它的文档,并对它进行试验。

用户是任何应用的重要组成部分,考虑到这一点,我们也花了很大一部分时间来关注他们。 我们不仅将用户添加到我们的应用中,还将其作为一个独立的、自包含的模块,与博物馆一起添加。

一旦我们开发了在应用中注册用户的业务逻辑,对它的持久化层的需求就迫在眉睫了。 这意味着我们必须开发一个用户存储库,该存储库负责在数据库中创建用户。 在这里,我们稍微深入了一点,实现了一个散列和 salt 机制来安全地将用户的密码存储在数据库中,同时在此过程中学习了一些 Deno api。

用户业务逻辑完成后,我们继续讨论缺少的部分:HTTP 端点。 我们向 HTTP 路由添加了注册路由,并在 Oak 的帮助下完成了所有设置。

最后,我们使用依赖注入将所有内容重新连接起来。 因为我们所有模块的依赖关系都是基于接口的,所以我们可以很容易地注入所需的依赖关系,并让代码正常工作。

这一章是让我们的应用更具可扩展性和可读性的旅程。 我们开始移除我们的 DIY 路由代码并将其转移到 Oak 中,最后我们添加了一个大而重要的业务实体——用户。 后者还可以作为我们体系结构的测试,并演示它如何在不同的业务领域进行扩展。

在下一章中,我们将通过添加一些有趣的特性对应用进行迭代。 通过这样做,我们将完成这里创建的功能,例如用户登录、授权和真实数据库中的持久性。 我们将处理的其他事情将包括常见的 API 实践,比如基本的日志记录和错误处理。

兴奋? 我们也是——我们走吧!