2022 年 4 月 27 日

使用 Remix、Prisma 和 MongoDB 构建全栈应用:CRUD、过滤和排序

17 分钟阅读

欢迎来到本系列文章的第三篇,您将学习如何从头开始使用 MongoDB、Prisma 和 Remix 构建一个全栈应用程序!在本文中,您将构建应用程序的主要部分,该部分显示用户的点赞 (kudos) 信息流,并允许他们向其他用户发送点赞。

Build A Fullstack App with Remix, Prisma & MongoDB: CRUD, Filtering & Sorting

目录

简介

在本系列的上一部分中,您构建了应用程序的登录和注册表单,并实现了基于会话的身份验证。您还更新了 Prisma 模式,以考虑 User 模型中的新嵌入式文档,该文档将保存用户的个人资料数据。

在本文中,您将构建应用程序的主要功能:点赞 (kudos) 信息流。每个用户都会有一个其他用户发送给他们的点赞信息流。用户还可以向其他用户发送点赞。

此外,您将实现一些搜索和过滤功能,以便更轻松地在信息流中查找点赞 (kudos)。

本项目的起点可在 GitHub 存储库的 part-2 分支中找到。如果您想查看本文的最终结果,请前往 part-3 分支。

开发环境

为了跟上提供的示例,您需要...

注意:可选扩展为 Tailwind 和 Prisma 添加了一些非常好的智能感知和语法高亮显示功能。

构建主页路由

应用程序的主要部分将位于 /home 路由中。通过在 app/routes 文件夹中添加一个 home.tsx 文件来设置该路由。

这个新文件现在应该导出一个名为 Home 的函数组件,以及一个 loader 函数,该函数将未登录的用户重定向到登录屏幕。

这个 /home 路由将充当应用程序的主页,而不是基本 URL。

目前,app/routes/index.tsx 文件 / 路由) 渲染一个 React 组件。该路由应该只重定向用户:要么重定向到 /home 路由,要么重定向到 /login 路由。在其位置设置一个 资源路由 以实现该功能。

资源路由

资源路由是不渲染组件的路由,但可以返回任何类型的响应。可以将其视为一个简单的 API 端点。在您的 / 路由的情况下,您将希望它返回一个带有 302 状态代码的 redirect 响应。

删除现有的 app/routes/index.tsx 文件,并将其替换为一个 index.ts 文件,您将在其中定义资源路由

注意:文件的扩展名已更改为 .ts,因为此路由永远不会渲染组件。

上面的 loader 将首先检查用户在访问 / 路由时是否已登录。requireUserId 函数将在没有有效会话时重定向到 /login

如果存在有效会话,则 loader 返回到 /home 页面的 redirect

添加用户列表面板

首先构建主页,方法是构建一个组件,该组件将在屏幕左侧列出网站的用户。

app/components 文件夹中创建一个名为 user-panel.tsx 的新文件

这将创建侧面板,其中将包含用户列表。但是,该组件是静态的,这意味着它不执行任何操作或以任何方式变化。

在通过添加用户列表使此组件更动态之前,将其导入 app/routes/home.tsx 页面并将其渲染到页面上。

上面的代码导入了新组件和 Layout 组件,然后在布局中渲染了新组件。

查询所有用户并对结果进行排序

现在您需要实际在面板中显示用户列表。您应该已经有一个文件用于存放与用户相关的功能:app/utils/user.server.ts

向该文件添加一个新函数,该函数查询数据库中的任何用户。此函数应接收一个 userId 参数,并按用户的名字以升序对结果进行排序

where 过滤器排除任何 iduserId 参数匹配的文档。这将用于获取除当前登录用户之外的所有 user

注意:请注意按嵌入式文档中的字段排序是多么容易?

app/routes/home.tsx 中,导入该新函数并在 loader 中调用它。然后使用 Remix 的 json 助手返回用户列表

注意:在 loader 函数中运行的任何代码都不会暴露给客户端代码。您可以感谢 Remix 的这个很棒的功能!

如果您的数据库中有任何用户并输出了 loader 内部的 users 变量,您应该会看到除您自己之外的所有用户的列表。

注意:整个 profile 嵌入式文档都被检索为一个嵌套对象,而无需显式包含它。

您现在将拥有可用的数据。是时候用它做点什么了!

将用户提供给用户面板

UserPanel 组件中设置一个新的 users prop。

此处使用的 User 类型由 Prisma 生成,可通过 Prisma Client 获得。Remix 与 Prisma 非常配合,因为它非常容易在全栈框架中实现端到端类型安全。

注意:端到端类型安全发生在整个技术栈中的类型随着数据形状的变化而保持同步时。

app/routes/home.tsx 中,您现在可以将用户提供给 UserPanel 组件。导入 Remix 提供的 useLoaderData hook,它使您可以访问从 loader 函数返回的任何数据,并使用它来访问 users 数据

该组件现在将拥有 users 来处理。现在它需要显示它们。

构建用户显示组件

列表项现在将显示为一个圆圈,其中包含用户名字和姓氏的首字母。

app/components 中创建一个名为 user-circle.tsx 的新文件,并将以下组件添加到其中

此组件使用 Prisma 生成的 Profile 类型,因为您将仅传入来自 user 文档的 profile 数据。

它还具有一些可配置的选项,允许您提供单击操作并添加其他类来自定义其样式。

app/components/user-panel.tsx 中,导入这个新组件,并为每个用户渲染一个,而不是渲染 <p>Users go here</p>

太棒了!您的用户现在将在主页左侧的一个漂亮的列中渲染。此时侧面板中唯一非功能性的部分是注销按钮。

添加注销功能

app/routes 中添加另一个资源路由,名为 logout.ts,它将在调用时执行注销操作

此路由处理两个可能的 action:POST 和 GET

  • POST:这将触发本系列文章的上一部分中编写的 logout 函数。
  • GET:如果发出 GET 请求,用户将被发送到主页。

app/components/user-panel.ts 中,在您的注销按钮周围添加一个 form,该表单将在提交时发布到此路由。

您的用户现在可以注销应用程序了!与 POST 请求关联的会话的用户将被注销,并且他们的会话将被销毁。

添加发送点赞 (kudos) 的功能

当单击用户列表中的用户时,应弹出一个模态框,其中提供一个表单。提交此表单将在数据库中保存一个点赞 (kudo)。

此表单将具有以下功能

  • 显示您正在向哪个用户发送点赞 (kudos)。
  • 一个文本区域,您可以在其中填写给用户的消息。
  • 样式选项,允许您选择帖子的背景颜色和文本颜色。
  • 一个表情符号选择器,您可以在其中向帖子添加表情符号。
  • 您的帖子外观的准确预览。

更新 Prisma 模式

您将保存和显示的几个数据点尚未在您的模式中定义。以下是需要更改的内容列表

  1. 添加一个 Kudo 模型,其中包含一个嵌入式文档以保存样式自定义
  2. User 模型中添加 1:n 关系,以定义用户是作者的点赞 (kudos)。还添加一个类似的关系,以定义用户是接收者的点赞 (kudos)。
  3. 为表情符号、部门和颜色添加 enum,以定义可用选项。

注意:在将 @default 应用于字段后,如果您的集合中的记录没有新的必需字段,则下次读取时会将其更新为包含具有默认值的该字段。

这就是您现在需要更新的全部内容。运行 npx prisma db push,它将自动重新生成 PrismaClient

嵌套路由

您将使用嵌套路由来创建将容纳您的表单的模态框。这将允许您设置一个子路由,该子路由将在您定义的 Outlet 处渲染到父路由上。

当用户导航到此嵌套路由时,模态框将渲染到屏幕上,而无需重新渲染整个页面。

要创建嵌套路由,首先在 app/routes 中添加一个名为 home 的新文件夹。

注意:该文件夹的命名很重要。由于您有一个 home.tsx 文件,因此 Remix 会将新 home 文件夹中的任何文件识别为 /home 的子路由。

在新 app/routes/home 目录中,创建一个名为 kudo.$userId.tsx 的新文件。这将允许您像处理自己的路由一样处理模态框组件。

文件名中的 $userId 部分是一个 路由参数,它充当您可以通过 URL 为应用程序提供的动态值。然后,Remix 会将该文件名转换为路由:/home/kudos/$userId,其中 $userId 可以是任何值。

在该新文件中,导出一个 loader 函数和一个 React 组件,该组件渲染一些文本以确保动态值正在工作

上面的代码执行了几项操作

  1. 它从 loader 函数中提取 params 字段。
  2. 然后,它获取 userId 值。
  3. 最后,它使用 Remix 的 userLoaderData hook 从 loader 函数检索数据,并将 userId 渲染到屏幕上。

由于这是一个嵌套路由,为了显示它,您需要在其父路由中定义路由应输出的位置。

使用 Remix 的 Outlet 组件来指定您希望子路由作为 app/routes/home.tsxLayout 组件的直接子组件渲染

如果您前往 https://127.0.0.1:3000/home/kudo/123,您现在应该在页面顶部看到文本“User: 123”。如果您将 URL 中的值更改为 123 以外的其他值,您应该看到该更改反映在屏幕上。

通过 ID 获取用户

您的嵌套路由正在工作,但您仍然需要使用 userId 检索用户的数据。在 app/utils/user.server.ts 中创建一个新函数,该函数根据用户的 id 返回单个用户

上面的查询在数据库中查找具有给定 id 的唯一记录。findUnique 函数允许您使用唯一标识字段或值必须对数据库中该记录唯一的字段来过滤查询。

接下来

  1. app/routes/home/kudo.$userId.tsx 导出的 loader 中调用该函数。
  2. 使用 json 函数从该 loader 返回结果。

接下来,您需要一种导航到具有有效 id 的嵌套路由的方法。

app/components/user-panel.tsx 中,即您正在渲染用户列表的文件中,导入 Remix 提供的 useNavigation hook,并在单击用户时使用它导航到嵌套路由。

现在,当您的用户单击该面板中的另一个用户时,他们将被导航到包含该用户信息的子路由。

如果一切看起来都不错,则下一步是构建将显示您的表单的模态框组件。

打开一个 Portal

要构建此模态框,您首先需要构建一个 helper 组件,该组件创建一个 portal,它允许您在父文档对象模型 (DOM) 分支之外的某个位置渲染子组件,同时仍然允许父组件像管理直接子组件一样管理它。

注意:此 portal 将非常重要,因为它将允许您在没有来自可能影响模态框定位的父级的任何继承样式或定位的位置渲染模态框。

app/components 中创建一个名为 portal.tsx 的新文件,其中包含以下内容

以下是对此组件中发生的事情的解释

  1. 定义了一个函数,该函数生成一个带有 iddiv。然后将该元素附加到文档的 body
  2. 如果具有提供的 id 的元素尚不存在,则调用 createWrapper 函数以创建一个。
  3. Portal 组件卸载时,这将销毁该元素。
  4. 创建一个到新生成的 div 的 portal。

结果是,包装在此 Portal 中的任何元素或组件都将作为 body 标记的直接子组件渲染,而不是在当前 DOM 分支中作为其父级的子组件渲染。

试用一下,看看它的实际效果。在 app/routes/home/kudos.$userId.tsx 中,导入新的 Portal 组件,并用它包装返回的组件

如果您导航到嵌套路由,您将看到一个 div,其 id"kudo-modal",现在作为 body 的直接子组件渲染,而不是嵌套路由在 DOM 树中渲染的位置。

构建模态框组件

既然您有了一个安全的 portal,请开始构建模态框组件本身。此应用程序中将有两个模态框,因此以可重用的方式构建组件。

app/components/modal.tsx 创建一个新文件。此文件应导出一个具有以下 props 的组件

  • children:要在模态框中渲染的元素。
  • isOpen:一个标志,用于确定是否显示模态框。
  • ariaLabel(可选)用作 aria 标签的字符串。
  • className(可选)允许您向模态框内容添加其他类的字符串。

添加以下代码以创建 Modal 组件

Portal 组件已导入并包装了整个模态框,以确保它在安全的位置渲染。

然后,使用各种 TailwindCSS 助手将模态框定义为屏幕上的固定元素,并带有不透明的背景。

当背景(模态框本身以外的任何位置)被单击时,用户将被导航到 /home 路由,从而导致模态框关闭。

构建表单

app/routes/home/kudo.$userId.tsx 中,导入新的 Modal 组件并渲染一个 Modal 而不是当前渲染的 Portal

现在,当单击侧面板中的用户时,模态框应打开。

您的表单在显示消息预览时将需要登录用户的信息,因此在构建表单之前,将该数据添加到来自 loader 函数的响应中

然后对该文件中的 KudoModal 函数进行以下更改

这是一大块新代码,因此请查看进行了哪些更改

  1. 导入您将需要的一些组件和 hook。
  2. 设置您将需要处理表单数据和错误的各种表单变量。
  3. 创建将处理输入更改的函数。
  4. 渲染表单组件的基本布局,以代替之前的 <h2> 标记。

允许用户自定义他们的点赞 (kudo)

此表单还需要允许用户使用选择框选择自定义样式。

app/components 中创建一个名为 select-box.tsx 的新文件,该文件导出一个 SelectBox 组件

此组件类似于 FormField 组件,因为它是一个受控组件,它接收一些配置并允许其状态由其父级管理。

这些选择框将需要填充颜色和表情符号选项。创建一个 helper 文件以在 app/utils/constants.ts 中保存可能的选项

现在在 app/routes/home/kudo.$userId.tsx 中,导入 SelectBox 组件和常量。同时添加将它们连接到表单状态所需的变量和函数,并在 {/* Select Boxes Go Here */} 注释的位置渲染 SelectBox 组件。

现在选择框将显示所有可能的选项。

添加一个 kudo 显示组件

此表单将包含一个预览部分,用户可以在其中看到接收者将看到的组件的实际渲染效果。

app/components 中创建一个名为 kudo.tsx 的新文件

此组件接收以下 props

  • profile: 来自接收者 user 文档的 profile 数据。
  • kudo: Kudo 的数据和样式选项。

导入并使用带有颜色和 emoji 选项的常量来渲染自定义样式。

现在您可以将此组件导入到 app/routes/home/kudo.$userId.tsx 中,并在 {/* The Preview Goes Here */} 注释的位置渲染它

现在将渲染预览,显示当前登录用户的信息以及他们将要发送的样式化消息。

构建发送 kudos 的 action

现在表单在视觉上已完成,剩下的唯一部分是使其功能化!

app/utils 中创建一个名为 kudos.server.ts 的新文件,您将在其中编写任何与查询或存储 kudos 相关的函数。

在此文件中,导出一个 createKudo 方法,该方法接收 kudo 表单数据、作者的 id 和接收者的 id。然后使用 Prisma 存储该数据

上面的查询执行以下操作

  1. 传入 message 字符串和 style 嵌入式文档。
  2. 使用传递给函数的 id 将新的 kudo 连接到适当的作者接收者

将这个新函数导入到 app/routes/home/kudo.$userId.tsx 文件中,并创建一个 action 函数来处理表单数据和 createKudo 函数的调用

以下是上面代码片段的概述

  1. 导入新的 createKudo 函数,以及 Prisma 生成的一些类型、来自 Remix 的 ActionFunction 类型,以及您之前编写的 requireUserId 函数。
  2. 从请求中拉取所有表单数据和您需要的字段。
  3. 验证所有表单数据,并在出现问题时将相应的错误发送回表单以进行显示。
  4. 使用 createKudo 函数创建新的 kudo
  5. 将用户重定向到 /home 路由,导致模态框关闭。

构建一个 kudos feed

现在您的用户可以互相发送 kudos,您将需要一种在用户的 /home 页面 feed 中显示这些 kudos 的方法。

您已经构建了 kudo 显示组件,因此您只需检索并渲染主页上的 kudos 列表即可。

app/utils/kudos.server.ts 中创建并导出一个名为 getFilteredKudos 的新函数。

上面的函数接收几个不同的参数。以下是这些参数

  • userId: 用户 id,查询应检索该用户的 kudos。
  • sortFilter: 将传递到查询中的 orderBy 选项以对结果进行排序的对象。
  • whereFilter: 将传递到查询中的 where 选项以过滤结果的对象。

注意:Prisma 生成的类型可以安全地类型化查询的各个部分,例如上面使用的 Prisma.KudoWhereInput

现在在 app/routes/home.tsx 中,导入该函数并在 loader 函数中调用它。同时导入 Kudo 组件和渲染 Kudos feed 所需的类型。

Prisma 生成的 KudoProfile 类型被组合以创建 KudoWithProfile 类型。这是必需的,因为您的数组具有包含作者 profile 数据的 kudos。

如果您向一个帐户发送几个 kudos 并登录该帐户,您现在应该在您的 feed 上看到渲染的 kudos 列表。

您可能会注意到,当 getFilteredKudos 调用为排序和过滤器选项提供空对象时。这是因为 UI 中还没有过滤或排序 feed 的方法。接下来,您将在 feed 顶部创建搜索栏来处理此问题。

app/components 中创建一个名为 search-bar.tsx 的新文件。此组件将向 /home 页面提交表单,并传递查询参数,这些参数将用于构建您需要的排序和过滤器对象。

在上面的代码中,添加了 inputbutton 来处理文本过滤器和搜索参数的提交。

当 URL 中存在 filter 变量时,按钮将更改为“清除过滤器”按钮,而不是“搜索”按钮。

将该文件导入到 app/routes/home.tsx 中,并在 {/* Search Bar Goes Here */} 注释的位置渲染它。

这些更改将处理 feed 的过滤,但是您还希望按各种列对 feed 进行排序。

app/utils/constants.ts 中添加一个 sortOptions 常量,该常量定义了可用的列。

现在将该常量和 SelectBox 组件导入到 app/components/search-bar.tsx 文件中,并在 button 元素之前渲染带有这些选项的 SelectBox

现在您应该在搜索栏中看到一个带有选项的下拉菜单。

构建搜索栏 action

当提交搜索表单时,将向 /home 发送 GET 请求,并将过滤器和排序数据在 URL 中传递。在 app/routes/home.tsx 导出的 loader 函数中,从 URL 中拉取 sortfilter 数据,并使用结果构建查询

以上代码

  1. 拉取 URL 参数。
  2. 构建一个 sortOptions 对象,以传递到您的 Prisma 查询中,该对象可能会根据 URL 中传递的数据而有所不同。
  3. 构建一个 textFilter 对象,以传递到您的 Prisma 查询中,该对象可能会根据 URL 中传递的数据而有所不同。
  4. 更新 getFilteredKudos 调用以包含新的过滤器。

现在,如果您提交表单,您应该在 feed 上看到反映的结果!

显示最近的 kudos

您的 feed 需要的最后一件事是显示最近发送的 kudos 的方法。此组件将为最近三个 kudos 接收者显示一个 UserCircle 组件。

使用以下代码在 app/components 中创建一个名为 recent-bar.tsx 的新文件

此组件接收最近三个 kudos 的列表,并将它们渲染到一个面板中。

现在您需要编写一个查询来获取该数据。在 app/utils/kudos.server.ts 中添加一个名为 getRecentKudos 的函数,该函数返回以下查询

此查询

  1. createdAt降序排序结果,以获取从最新到最旧的记录。
  2. 仅从该列表中获取前三个,以获取最近的三个文档。

现在您将需要

  • RecentBar 组件和 getRecentKudos 函数导入到 app/routes/home.tsx 文件中。
  • 在文件的 loader 函数中调用 getRecentKudos
  • RecentBar 渲染到主页上,以代替 {/* Recent Kudos Goes Here */} 注释。

这样,您的主页就完成了,您应该看到您的应用程序中最近发送的三个 kudos 的列表!

总结 & 接下来是什么

在本文中,您构建了此应用程序的主要功能部分,并在此过程中学习了很多概念,包括

  • 在 Remix 中重定向
  • 使用资源路由
  • 使用 Prisma Client 过滤和排序数据
  • 在您的 Prisma Schema 中使用嵌入式文档
  • ... 以及更多!

在本系列的下一节中,您将完成此应用程序,构建站点的个人资料设置部分,并创建一个图像上传组件来管理个人资料图片。

不要错过下一篇文章!

注册 Prisma 新闻通讯