2022年4月27日

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

阅读时长17分钟

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

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

目录

引言

在本系列的上一部分中,你构建了应用程序的登录和注册表单,并实现了基于会话的身份验证。你还更新了 Prisma schema,以便在 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 将返回一个 redirect 重定向到 /home 页面。

添加用户列表面板

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

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,该 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资源路由,该路由被调用时将执行退出登录操作。

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

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

app/components/user-panel.ts 中,在你的退出登录按钮周围添加一个 form,提交时会向此路由发送 POST 请求。

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

添加发送点赞功能

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

此表单将具有以下特性:

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

更新 Prisma schema

有几个你将要保存和显示的数据点尚未在你的 schema 中定义。以下是需要更改的列表:

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

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

使用 Remix 的 Outlet 组件来指定你希望子路由在 app/routes/home.tsx 中作为 Layout 组件的直接子元素进行渲染。

如果你访问 http://localhost: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

要构建此模态框,首先需要构建一个辅助组件来创建一个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 组件,并用它包裹返回的组件。

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

构建模态框组件

现在你已经有了一个通往安全位置的 portal,可以开始构建模态框组件本身了。此应用程序中将有两个模态框,因此请以可重用的方式构建组件。

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

  • children:要在模态框内渲染的元素。
  • isOpen:一个标志,用于确定模态框是否正在显示。
  • ariaLabel(可选) 用作 aria label 的字符串。
  • className(可选) 一个字符串,允许你为模态框的内容添加额外的类。

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

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

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

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

构建表单

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

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

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

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

这是一大块新代码,所以让我们看看做了哪些更改:

  1. 导入了一些你需要的组件和 hook。
  2. 设置了处理表单数据和错误所需的各种表单变量。
  3. 创建了处理输入变化的函数。
  4. 渲染了表单组件的基本布局,替换了原来的 <h2> 标签。

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

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

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

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

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

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

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

添加点赞显示组件

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

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

此组件接收以下 props:

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

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

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

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

构建发送点赞的 action

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

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

在此文件中,导出一个 createKudo 方法,该方法接收点赞表单数据、作者的 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 路由,从而关闭模态框。

构建 kudo 动态

现在用户可以互相发送 kudo 了,你需要一种方式将这些 kudo 显示在用户在 /home 页面上的动态中。

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

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

上面的函数接收几个不同的参数。它们是:

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

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

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

Prisma 生成的 KudoProfile 类型组合在一起创建了一个 KudoWithProfile 类型。这是必需的,因为你的数组中的 kudos 包含了作者的个人资料数据。

如果你向一个账户发送几个 kudos 并登录该账户,现在你应该能在你的动态中看到渲染出来的 kudo 列表。

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

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

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

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

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

这些更改将处理动态的过滤,但你也希望按不同的列对动态进行排序。

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 的调用以包含新的过滤器。

现在如果你提交表单,你应该能在动态中看到结果!

显示最近的 kudos

你的动态需要做的最后一件事是显示最近发送的 kudos。这个组件将为最近的三个 kudo 接收者显示一个 UserCircle 组件。

app/components 中创建一个新文件,命名为 recent-bar.tsx,代码如下:

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

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

这个查询:

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

现在你需要:

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

至此,你的主页就完成了,你应该能在应用程序中看到最近发送的三个 kudo 的列表!

总结与下一步

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

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

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

不要错过下一篇文章!

订阅 Prisma 新闻通讯