欢迎阅读本系列的第三篇文章,你将学习如何使用 MongoDB、Prisma 和 Remix 从头开始构建一个全栈应用程序!在这一部分中,你将构建应用程序的主要部分,它将显示用户的表扬动态并允许他们向其他用户发送表扬。
目录
- 简介
- 构建主页路由
- 添加用户列表面板
- 构建用户显示组件
- 添加注销功能
- 添加发送表扬的功能
- 构建表单
- 添加表扬显示组件
- 构建发送表扬的 action
- 构建表扬动态
- 构建搜索栏
- 显示最近的表扬
- 总结和下一步
简介
在本系列的上一部分中,你构建了应用程序的登录和注册表单,并实现了基于会话的身份验证。你还更新了 Prisma 模式,以适应 User 模型中新的嵌入式文档,该文档将保存用户的配置文件数据。
在这一部分中,你将构建应用程序的主要功能:表扬动态。每个用户都会有一个其他用户发送给他们的表扬动态。用户还可以向其他用户发送表扬。
此外,你还将实现一些搜索和过滤功能,以便更轻松地在动态中查找表扬。
此项目的起点可在 GitHub 仓库的 part-2 分支中找到。如果你想查看此部分的最终结果,请前往 part-3 分支。
开发环境
为了遵循所提供的示例,您需要...
- ... 已安装 Node.js。
- ...已安装Git。
- ...已安装TailwindCSS VSCode 扩展。(可选)
- ...已安装Prisma VSCode 扩展。(可选)
注意:可选扩展为 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 过滤器排除任何 id 与 userId 参数匹配的文档。这将用于获取除当前登录用户之外的所有 user。
注意:请注意通过嵌入式文档中的字段进行排序是多么容易?
在 app/routes/home.tsx 中,导入该新函数并在 loader 中调用它。然后使用 Remix 的 json 助手返回用户列表
注意:在
loader函数中运行的任何代码都不会暴露给客户端代码。你可以感谢 Remix 提供的这一强大功能!
如果你的数据库中有任何用户,并且在加载器中输出了 users 变量,你应该会看到一个除你之外的所有用户的列表。

注意:整个
profile嵌入式文档作为嵌套对象被检索,而无需显式包含它。
你现在将拥有可用的数据。是时候用它做点什么了!
向用户面板提供用户数据
在 UserPanel 组件中设置一个新的 users prop。
此处使用的 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>用户在此处</p>
太棒了!你的用户现在将以漂亮的列显示在主页的左侧。此时侧面板中唯一非功能性的部分是注销按钮。

添加注销功能
在 app/routes 中添加另一个资源路由,名为 logout.ts,它将在调用时执行注销操作
此路由处理两个可能的动作:POST 和 GET
POST:这将触发本系列上一部分中编写的logout函数。GET:如果发出GET请求,用户将被发送到主页。
在 app/components/user-panel.ts 中,在你的注销按钮周围添加一个 form,该表单将在提交时发布到此路由。
你的用户现在可以退出应用程序了!与 POST 请求关联的用户会话将被注销并销毁。
添加发送表扬的功能
当用户列表中的某个用户被点击时,会弹出一个包含表单的模态框。提交此表单将在数据库中保存一条表扬。
此表单将具有以下功能
- 显示你正在向哪个用户发送表扬。
- 一个文本区域,你可以在其中填写给用户的消息。
- 样式选项,允许你选择帖子的背景颜色和文本颜色。
- 一个表情符号选择器,你可以在帖子中添加表情符号。
- 帖子外观的准确预览。
更新 Prisma 模式
你将保存和显示一些尚未在你的模式中定义的数据点。以下是需要更改的列表
- 添加一个包含嵌入式文档的
Kudo模型,以保存样式自定义 - 在
User模型中添加一个 1:n 关系,定义用户是其作者的表扬。还添加一个类似的关系,定义用户是其接收者的表扬。 - 为表情符号、部门和颜色添加
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 组件,以确保动态值正常工作
上面的代码做了一些事情
- 它从加载器函数中提取
params字段。 - 然后它获取
userId值。 - 最后,它使用 Remix 的
userLoaderData钩子从loader函数中检索数据,并将userId渲染到屏幕上。
因为这是一个嵌套路由,为了显示它,你需要在其父级中定义路由应该输出到哪里。
使用 Remix 的 Outlet 组件来指定你希望子路由作为 app/routes/home.tsx 中 Layout 组件的直接子级渲染
如果你现在访问 https://:3000/home/kudo/123,你应该会在页面顶部看到“用户:123”的文本。如果你将 URL 中的值更改为 123 以外的其他值,你应该会看到屏幕上反映出该更改。

通过用户 ID 获取用户
你的嵌套路由正在工作,但你仍然需要使用 userId 检索用户数据。在 app/utils/user.server.ts 中创建一个新函数,该函数根据用户的 id 返回单个用户
上面的查询在数据库中查找具有给定 id 的唯一记录。findUnique 函数允许你使用唯一标识字段或数据库中该记录必须具有唯一值的字段来过滤查询。
下一步
- 在
app/routes/home/kudo.$userId.tsx导出的加载器中调用该函数。 - 使用
json函数返回该加载器的结果。
接下来,你需要一种方法来导航到具有有效 id 的嵌套路由。
在 app/components/user-panel.tsx(你渲染用户列表的文件)中,导入 Remix 提供的 useNavigation 钩子,并在点击用户时使用它导航到嵌套路由。
现在,当你的用户在该面板中点击另一个用户时,他们将被导航到带有该用户信息的子路由。

如果一切正常,下一步是构建将显示你的表单的模态组件。
打开一个 Portal
要构建此模态框,你首先需要构建一个辅助组件,该组件创建一个 portal,它允许你将子组件渲染到父文档对象模型 (DOM) 分支之外的某个位置,同时仍然允许父组件像管理直接子组件一样管理它。

注意:此 portal 将很重要,因为它将允许你将模态框渲染到不受父级任何继承样式或定位影响的位置,这可能会影响模态框的定位。
在 app/components 中创建一个名为 portal.tsx 的新文件,其内容如下
以下是此组件中发生的事情的解释
- 定义一个生成带
id的div的函数。然后将该元素附加到文档的body。 - 如果尚不存在具有提供
id的元素,则调用createWrapper函数来创建一个。 - 当
Portal组件卸载时,这将销毁该元素。 - 创建一个到新生成的
div的 portal。
结果将是,任何包含在此 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 函数进行以下更改
这是一大段新代码,所以让我们看看进行了哪些更改
- 导入了一些你需要的组件和钩子。
- 设置了你将需要处理表单数据和错误的各种表单变量。
- 创建了处理输入更改的函数。
- 渲染了表单组件的基本布局,取代了原来的
<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 的新文件
此组件接收以下 props
profile:收件人user文档中的profile数据。kudo:Kudo的数据和样式选项。
导入包含颜色和表情符号选项的常量,并用于渲染自定义样式。
你现在可以将此组件导入到 app/routes/home/kudo.$userId.tsx 并将其渲染到 {/* The Preview Goes Here */} 注释的位置
现在将渲染预览,显示当前登录用户的信息以及他们将要发送的样式化消息。

构建发送表扬的 action
表单现在在视觉上是完整的,剩下的唯一部分是使其功能化!
在 app/utils 中创建一个名为 kudos.server.ts 的新文件,你将在其中编写与查询或存储表扬相关的任何函数。
在此文件中,导出一个 createKudo 方法,该方法接收表扬表单数据、作者的 id 和接收者的 id。然后使用 Prisma 存储该数据
上面的查询执行以下操作
- 传入
message字符串和style嵌入式文档。 - 使用传递给函数的 ID 将新表扬连接到相应的作者和接收者。
将此新函数导入到 app/routes/home/kudo.$userId.tsx 文件中,并创建一个 action 函数来处理表单数据和 createKudo 函数的调用
以下是上述代码片段的概述
- 导入新的
createKudo函数,以及 Prisma 生成的一些类型、Remix 的ActionFunction类型,以及你之前编写的requireUserId函数。 - 从请求中提取所有表单数据和所需字段。
- 验证所有表单数据,如果出现问题,则将适当的错误发送回表单以显示。
- 使用
createKudo函数创建新的kudo。 - 将用户重定向到
/home路由,导致模态框关闭。
构建表扬动态
现在你的用户可以互相发送表扬了,你需要一种方法来在 /home 页面的用户动态中显示这些表扬。
你已经构建了表扬显示组件,因此你只需检索并在主页上渲染一个表扬列表。
在 app/utils/kudos.server.ts 中创建并导出一个名为 getFilteredKudos 的新函数。
上面的函数接收几个不同的参数。这些参数是
userId:查询应检索其表扬的用户的id。sortFilter:一个将传递到查询中的orderBy选项以对结果进行排序的对象。whereFilter:一个将传递到查询中的where选项以筛选结果的对象。
注意:Prisma 生成的类型可用于安全地键入查询的片段,例如上面使用的
Prisma.KudoWhereInput。
现在在 app/routes/home.tsx 中,导入该函数并在 loader 函数中调用它。还要导入 Kudo 组件和渲染表扬动态所需的类型。
Prisma 生成的 Kudo 和 Profile 类型组合在一起创建了一个 KudoWithProfile 类型。这是必需的,因为你的数组包含带有作者个人资料数据的表扬。
如果你向某个帐户发送几个表扬并登录该帐户,你现在应该会在你的动态中看到一个渲染的表扬列表。

你可能会注意到 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 文件中,并将 SelectBox 与这些选项一起渲染到 button 元素之前。
现在,你应该会在搜索栏中看到一个带有选项的下拉菜单。

构建搜索栏 action
当搜索表单提交时,将向 /home 发出 GET 请求,并在 URL 中传递筛选和排序数据。在 app/routes/home.tsx 导出的 loader 函数中,从 URL 中提取 sort 和 filter 数据,并使用结果构建查询
上面的代码
- 提取 URL 参数。
- 构建一个
sortOptions对象,以传递到你的 Prisma 查询中,该对象可能会根据 URL 中传递的数据而变化。 - 构建一个
textFilter对象,以传递到你的 Prisma 查询中,该对象可能会根据 URL 中传递的数据而变化。 - 更新
getFilteredKudos调用以包含新的过滤器。
现在,如果你提交表单,你应该会看到你的结果反映在动态中!

显示最近的表扬
你的动态最后需要一个显示最近发送的表扬的方式。此组件将为最近的三位表扬接收者显示一个 UserCircle 组件。
在 app/components 中创建一个名为 recent-bar.tsx 的新文件,其代码如下
此组件接收最近的三条表扬列表,并将它们渲染到一个面板中。
现在你需要编写一个获取该数据的查询。在 app/utils/kudos.server.ts 中添加一个名为 getRecentKudos 的函数,该函数返回以下查询
此查询
- 按
createdAt降序对结果进行排序,以获取从最新到最旧的记录。 - 只从该列表中获取前三个,以获取最近的三个文档。
现在你需要
- 将
RecentBar组件和getRecentKudos函数导入到app/routes/home.tsx文件中。 - 在该文件的
loader函数中调用getRecentKudos。 - 将
RecentBar渲染到主页上,替换{/* Recent Kudos Goes Here */}注释。
这样,你的主页就完成了,你应该会在你的应用程序中看到最近发送的三条表扬列表!

总结和下一步
在本文中,你构建了此应用程序的主要功能,并在此过程中学习了许多概念,包括
- 在 Remix 中重定向
- 使用资源路由
- 使用 Prisma Client 筛选和排序数据
- 在你的 Prisma 模式中使用嵌入式文档
- ... 还有更多!
在本系列的下一部分中,你将通过构建站点的个人资料设置部分和创建图像上传组件来管理个人资料图片来完成此应用程序。
不要错过下一篇文章!
订阅 Prisma 新闻通讯