2022 年 4 月 27 日

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

阅读 17 分钟

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

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

目录

引言

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

在本部分中,您将构建应用程序的主要功能:称赞动态。每个用户都将拥有一个其他用户发送给他们的称赞动态。用户还将能够向其他用户发送称赞。

此外,您将实现一些搜索和过滤功能,以便更轻松地在动态中查找称赞。

该项目的起点可在 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 提供了这个很棒的功能!

如果您的数据库中有任何用户,并且在加载器中输出了 users 变量,您应该会看到一个包含所有用户(除了您自己)的列表。

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

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

向用户面板提供用户数据

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

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

注意:当整个技术栈中的类型随着数据形状的变化而保持同步时,就会实现端到端类型安全。

现在,在 app/routes/home.tsx 中,您可以将用户提供给 UserPanel 组件。导入 Remix 提供的 useLoaderData 钩子,并使用它来访问 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资源路由,该路由在调用时将执行退出登录操作。

此路由处理两种可能的操作:POST 和 GET

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

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

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

添加发送称赞功能

当用户列表中的某个用户被点击时,会弹出一个模态框,其中包含一个表单。提交此表单会将称赞保存到数据库中。

此表单将具有以下功能:

  • 显示您正在称赞哪个用户。
  • 一个文本区域,您可以在其中填写给用户的消息。
  • 样式选项,允许您选择帖子的背景颜色和文本颜色。
  • 一个表情符号选择器,您可以在帖子中添加表情符号。
  • 一个准确的预览,显示您的帖子将是什么样子。

更新 Prisma 模式

有一些数据点您将保存和显示,但这些数据点尚未在您的模式中定义。以下是需要更改的列表:

  1. 添加一个 Kudo 模型,其中包含一个嵌入式文档来保存样式自定义。
  2. User 模型中添加一个 1:n 关系,该关系定义了用户是其作者的称赞。同时添加一个类似的关系,定义用户是其接收者的称赞。
  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. 它从加载器函数中提取 params 字段。
  2. 然后它获取 userId 值。
  3. 最后,它使用 Remix 的 userLoaderData 钩子从 loader 函数中检索数据,并将 userId 渲染到屏幕上。

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

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

如果您前往 https://: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 导出的加载器中调用该函数。
  2. 使用 json 函数返回该加载器的结果。

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

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

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

如果一切看起来都很好,下一步是构建将显示表单的模态组件。

打开一个门户

要构建此模态框,您首先需要构建一个帮助组件来创建一个门户,它允许您在父文档对象模型 (DOM) 分支之外的某个位置渲染子组件,同时仍允许父组件像处理其直接子组件一样对其进行管理。

注意:这个门户将很重要,因为它将允许您将模态框渲染到一个不会从父级继承任何样式或定位的位置,这可能会影响模态框的定位。

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

以下是该组件中发生的事情的解释:

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

结果是,任何包裹在此 Portal 中的元素或组件都将作为 body 标签的直接子元素呈现,而不是在当前 DOM 树中作为其父元素的子元素呈现。

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

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

构建模态框组件

既然您已经有了一个安全的门户,那么就开始构建模态框组件本身吧。这个应用程序中将有两个模态框,因此以可重用的方式构建组件。

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. 导入您需要的几个组件和钩子。
  2. 设置您需要处理表单数据和错误的各种表单变量。
  3. 创建将处理输入更改的函数。
  4. 在原来是 <h2> 标签的位置渲染表单组件的基本布局。

允许用户自定义他们的称赞

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

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

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

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

现在,在 app/routes/home/kudo.$userId.tsx 中,导入 SelectBox 组件和常量。同时添加所需的变量和函数,将它们与表单状态关联起来,并用 SelectBox 组件替换 {/* Select Boxes Go Here */} 注释。

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

添加称赞显示组件

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

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

此组件接受以下属性:

  • profile:来自收件人 user 文档的 profile 数据。
  • kudoKudo 的数据和样式选项。

导入了包含颜色和表情符号选项的常量,并用于渲染自定义样式。

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

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

构建发送称赞的操作

表单在视觉上已经完成,剩下的唯一部分就是使其具有功能性!

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

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

上面的查询执行以下操作:

  1. 传入 message 字符串和 style 嵌入式文档。
  2. 使用传递给函数的 ID 将新的称赞连接到相应的作者接收者

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

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

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

构建称赞动态

现在您的用户可以互相发送称赞了,您需要一种方式来在 /home 页面的用户动态中显示这些称赞。

您已经构建了称赞显示组件,因此您只需在主页上检索并渲染称赞列表。

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

上述函数接受几个不同的参数。它们是:

  • userId:查询应检索其称赞的用户的 id
  • sortFilter:一个对象,将传递到查询中的 orderBy 选项以排序结果。
  • whereFilter:一个对象,将传递到查询中的 where 选项以过滤结果。

注意:Prisma 会生成可用于安全地对查询片段进行类型定义的类型,例如上面使用的 Prisma.KudoWhereInput

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

Prisma 生成的 KudoProfile 类型组合在一起创建了一个 KudoWithProfile 类型。这是因为您的数组中包含带有作者资料数据的称赞。

如果您向某个帐户发送了几个称赞并登录该帐户,您现在应该会在您的动态中看到已渲染的称赞列表。

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

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

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

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

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

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

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

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

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

构建搜索栏操作

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

上面的代码

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

现在,如果您提交表单,您应该会在动态中看到您的结果!

显示最新的称赞

您的动态所需的最后一件事是显示最新发送的称赞。此组件将显示三个最新称赞接收者的 UserCircle 组件。

app/components 中创建一个名为 recent-bar.tsx 的新文件,其中包含以下代码:

此组件接受最近三个称赞的列表,并将其渲染到面板中。

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

此查询

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

现在你需要:

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

至此,您的主页已完成,您应该会在应用程序中看到最近发送的三个称赞的列表!

总结与展望

在本文中,您构建了此应用程序的主要功能,并在此过程中学到了许多概念,包括:

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

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

不要错过下一篇文章!

订阅 Prisma 电子邮件简报

© . All rights reserved.