2022年4月28日

使用 Remix、Prisma 和 MongoDB 构建全栈应用:参照完整性与图片上传

12分钟阅读

欢迎阅读本系列的第四篇文章,您将学习如何使用 MongoDB、Prisma 和 Remix 从头开始构建一个全栈应用!在本文中,您将构建应用的个人资料设置部分,包括一个图片上传组件,并配置您的 schema 为数据提供参照完整性。

Build A Fullstack App with Remix, Prisma & MongoDB: Referential Integrity & Image Uploads

目录

引言

在本系列的上一部分中,您构建了应用的主要区域,包括点赞动态、用户列表、最新点赞列表和点赞发送表单。

在本文中,您将通过构建用户更新个人资料信息和上传个人资料图片的方式来完成应用的开发。您还将对 schema 进行一些更改,为数据库提供参照完整性。

注意:该项目的起始点可在 GitHub 仓库的 part-3 分支中找到。如果您想查看此部分的最终结果,请前往 part-4 分支。

开发环境

为了跟随提供的示例,您需要具备...

注意:可选扩展为 Tailwind 和 Prisma 提供了非常好的智能感知和语法高亮。

构建个人资料设置模态框

应用的个人资料设置页面将显示在一个模态框中,通过点击页面右上角的个人资料设置按钮即可访问。

app/components/search-bar.tsx

  • 为导出的组件添加一个名为 profile 的新 prop,其类型是 Prisma 生成的 Profile 类型
  • 导入 UserCircle 组件。
  • form 内容的最末尾渲染 UserCircle 组件,并将新的 profile prop 数据传递给它。这将作为您的个人资料设置按钮。

如果您的开发服务器已经在运行,这会导致您的主页抛出错误,因为 SearchBar 组件现在需要个人资料数据。

app/routes/home.tsx 文件中,使用本系列第二部分中编写的来自 app/utils/auth.server.tsgetUser 函数。使用此函数在 loader 函数内部加载已登录用户的数据。然后将该数据提供给 SearchBar 组件。

您的 SearchBar 现在将能够访问它所需的 profile 数据。如果您之前由于缺少此数据而收到错误,在浏览器中刷新页面后,应该会在页面右上角看到个人资料按钮成功渲染。

创建模态框

目标是在点击个人资料设置按钮时打开一个个人资料设置模态框。类似于本系列上一节中构建的点赞模态框,您需要设置一个嵌套路由来渲染新的模态框。

app/routes/home 中添加一个名为 profile.tsx 的新文件,并包含以下内容作为开头

以上代码片段...

  • ... 将一个模态框渲染到新的 ProfileSettings 组件中。
  • ... 在 loader 函数中检索并返回已登录用户的数据。
  • ... 使用 useLoaderData hook 访问从 loader 函数返回的 user 数据。

要打开这个新模态框,在 app/components/search-bar.tsx 中为 UserCircle 组件添加一个 onClick 处理函数,使用 Remix 的 useNavigate hook 将用户导航到 /home/profile 子路由。

如果您现在点击个人资料设置按钮,应该会在屏幕上看到新的模态框显示出来。

构建表单

您将构建的表单将包含三个字段,允许用户修改其个人资料详情:名字、姓氏和部门。

通过添加名字和姓氏输入框开始构建表单

以下是上面添加内容的概述

  1. 添加了更改中所需的 import。
  2. 在 state 中创建了一个 formData 对象,用于保存表单的值。这些值默认为已登录用户的现有个人资料数据。
  3. 创建了一个函数,它接受 HTML change 事件和一个字段名作为参数。这些参数用于在组件中输入字段的值发生变化时更新 formData state。
  4. 渲染了表单的基本布局以及两个输入字段。

此时,尚未进行错误处理,并且表单不执行任何操作。在添加这些部分之前,您需要添加部门下拉菜单。

app/utils/constants.ts 中添加一个新的 departments 常量,用于保存 Prisma schema 中定义的可能选项。将以下 export 添加到该文件

departments 以及 SelectBox 组件导入到 app/routes/home/profile.tsx 文件中,并使用它们为表单添加一个新的输入框

此时,您的表单应该渲染正确的输入框及其选项。它将把它们的值默认设置为与已登录用户个人资料关联的当前值。

允许用户提交表单

接下来您将构建 action 函数,它将使此表单具备功能。

在您的 app/routes/home/profile.tsx 中,添加一个 action 函数,该函数从 request 对象中检索表单数据,并验证 firstNamelastNamedepartment 字段

上面的 action 函数执行以下操作

  1. request 对象中提取您所需的表单数据点。
  2. 确保您关心的每一块数据都是 string 数据类型。
  3. 使用之前编写的 validateName 函数验证数据。
  4. 重定向到 /home 路由,关闭设置模态框。

上面的代码片段还在各种验证失败时抛出相关错误。为了使用经过验证的数据,编写一个允许您更新用户的函数。

app/utils/user.server.ts 中,export 以下函数

此函数允许您传入任何 profile 数据,并更新其 id 与提供的 userId 匹配的用户。

回到 app/routes/home/profile.tsx 文件中,导入新函数并在 action 函数中使用它来更新已登录用户

现在,当用户点击保存按钮时,其更新后的个人资料数据将被存储,并且模态框将关闭。

添加图片上传组件

设置 AWS 账户

您的用户现在可以更新其个人资料中的一些关键信息,然而如果能让用户设置个人资料图片以便其他用户更容易识别他们,那就更好了。

为此,您将设置一个 AWS S3 文件存储桶来保存上传的图片。如果您还没有 AWS 账户,可以在这里注册。

注意:亚马逊提供免费套餐,您可以免费使用 S3。

创建 IAM 用户

拥有账户后,您需要在 AWS 中设置一个身份访问管理 (IAM) 用户,以便生成与 S3 交互所需的访问密钥 ID密钥

注意:如果您已有 IAM 用户及其密钥,请随意跳过。

前往 AWS 控制台主页。在页面右上角,点击标有您用户名的下拉菜单,然后选择 Security Credentials(安全凭证)。

进入该部分后,在左侧菜单的访问管理下点击用户选项。

在此页面上,点击页面右上角的添加用户按钮。

这将引导您完成一个简短向导,允许您配置用户。按照以下步骤进行

第一部分要求填写

  1. 用户名:提供任意用户名。
  2. 选择 AWS 访问类型:选择访问密钥 - 程序化访问选项,这将启用访问密钥 ID密钥的生成。

在向导的第二步,进行以下选择

  1. 选择“直接附加现有策略”选项。
  2. 搜索术语“S3”。
  3. 勾选标有AmazonS3FullAccess的选项旁的复选框。
  4. 点击表单底部的“下一步”。

如果您想为您的用户添加标签,以便更轻松地管理和组织您账户中的用户,请在向导的第三步在此处添加。在此页面完成后,点击下一步

如果此页面上的摘要看起来不错,请点击页面底部的创建用户按钮。

点击该按钮后,您将进入一个页面,其中包含您的访问密钥 ID密钥。复制这些密钥并存储在一个您能轻松访问的地方,因为您很快就会用到它们。

设置 S3 存储桶

现在您拥有了用户和访问密钥,前往AWS S3 控制面板,您将在那里设置文件存储桶。

在此页面的右上角,点击创建存储桶按钮。

系统将要求您为存储桶指定名称和区域。填写这些详细信息,并将您选择的值与您之前保存的访问密钥 ID密钥一起保存。您稍后也将需要这些信息。

填写完这些信息后,点击表单最底部的创建存储桶

存储桶创建完成后,您将被发送到存储桶控制面板页面的对象选项卡。导航到权限选项卡。

在此选项卡中,点击阻止公共访问部分下的编辑按钮。在此表单中,取消勾选阻止所有公共访问框,然后点击保存更改。这将把您的存储桶设置为公共,从而允许您的应用访问图片。

在该部分下方,您将看到存储桶策略部分。粘贴以下策略,并确保将 <bucket-name> 替换为您的存储桶名称。此策略将允许您的图片被公开读取

您现在已经设置好了 AWS 用户和 S3 存储桶。接下来,您需要将密钥和存储桶配置保存到您的 .env 文件中,以便以后使用。

更新 Prisma schema

现在您将在数据库中创建一个字段,用于存储上传图片的链接。这些链接应与 Profile 嵌入文档一起存储,因此在您的 Profile 类型块中添加一个新字段。

要使用这些更改更新 Prisma Client,请运行 npx prisma generate

构建图片上传组件

app/components 中创建一个名为 image-uploader.tsx 的新文件,内容如下

上面的代码片段是完整的图片上传组件。以下是其工作原理概述

  1. 定义了一个 preventDefault 函数,用于阻止组件中文件输入的默认行为。
  2. 定义了一个 handleDrop 函数,用于处理组件中文件输入框的 drop 事件。
  3. 定义了一个 handleChange 函数,用于处理组件中文件输入框的任何 change 事件。
  4. 渲染了一个定义了各种事件处理函数的 div,使其能够响应文件拖放、拖动事件和点击。这些事件处理函数用于触发图片上传以及仅在元素接收到拖动事件时出现的样式变化。

每当此组件中 input 的值发生变化时,就会调用来自 propsonChange 函数,并传递文件数据。这些数据就是要上传到 S3 的内容。

接下来创建处理图片上传的服务。

构建图片上传服务

要构建图片上传服务,您将需要两个新的 npm 包

  • aws-sdk:提供一个 JavaScript API,允许您与 AWS 服务交互。
  • cuid:一个用于生成唯一 id 的工具。您将使用它来生成随机文件名。

您的图片上传服务将位于一个新的工具文件中。在 app/utils 中创建一个名为 s3.server.ts 的文件。

为了处理上传,您将使用 Remix 的 unstable_parseMultipartFormData 函数,该函数处理 request 对象的 multipart/form-data 值。

注意multipart/form-data 是在表单中上传整个文件时的表单数据类型。

unstable_parseMultipartFormData 将接受两个参数

  1. 从表单提交中检索到的 request 对象。
  2. 一个 uploadHandler 函数,它流式处理文件数据并处理上传。

注意unstable_parseMultipartFormData 函数的使用方式类似于我们之前使用过的 Remix 的 request.formData 函数。

将以下函数和 import 添加到您创建的新文件中

此代码设置了您的 S3 API,以便您可以与您的存储桶交互。它还添加了 uploadHandler 函数。此函数

  1. 使用您在设置 AWS 用户和 S3 存储桶时存储的环境变量来设置 S3 SDK。
  2. 只要数据键的名称是 'profile-pic',就从 request 中流式传输文件数据。
  3. 将文件上传到 S3。
  4. 返回 S3 返回的 Location 数据,其中包含新文件在 S3 中的 URL 位置。

现在 uploadHandler 已完成,添加另一个函数,该函数实际接受 request 对象,并将其与 uploadHandler 一起传递给 unstable_parseMultipartFormData 函数。

此函数被传递一个 request 对象,该对象稍后将从 action 函数发送过来。

文件数据通过 uploadHandler 函数传递,该函数处理上传到 S3 的过程,而 formData 则在一个表单数据对象中返回新文件的位置。然后从该对象中提取 'profile-pic' 的 URL,并由函数返回。

使用组件和服务

现在,实现个人资料图片上传所需的两个部分已经完成,将它们组合起来。

通过在 app/routes 中创建一个名为 avatar.ts 的新文件,并包含以下 action 函数,来添加一个处理上传表单数据的资源路由

上面的函数执行以下步骤来处理上传表单

  1. 获取发起请求用户的 id
  2. 上传请求数据中包含的文件。
  3. 使用新的 profilePicture URL 更新发起请求用户的个人资料数据。
  4. 使用 imageUrl 变量响应 POST 请求。

现在您可以使用 ImageUploader 组件来处理文件上传,并将文件数据发送到这个新的 /avatar 路由。

app/routes/home/profile.tsx 中,导入 ImageUploader 组件,并将其添加到表单中,放在输入字段的左侧。

同时添加一个新的函数来处理 ImageUploader 组件发出的 onChange 事件,并在 formData 变量中添加一个新字段来存储个人资料图片数据。

现在,如果您打开该表单并尝试上传文件,数据应该会正确地保存在 S3、数据库以及表单的状态中。

显示个人资料图片

太棒了!图片上传工作顺利,现在您只需要在网站上用户头像圆圈显示的地方展示这些图片即可。

打开 app/components/user-circle.tsx 中的 UserCircle 组件,并进行这些更改,以便在个人资料图片可用时将其设置为圆圈的背景图片

如果您现在给几个用户设置个人资料图片,您应该会在网站上看到这些图片显示出来!

添加账户删除功能

您的个人资料设置模态框所需的最后一个功能是删除账户的能力。

删除数据,尤其是在无模式数据库中,可能会产生“孤儿文档”,即曾经与父文档相关联,但其父文档在某个时候被删除的文档。

在本节中,您将采取防护措施来应对这种情况。

添加删除按钮

您将以类似于处理登录和注册表单的方式来处理此表单。此表单将发送一个 _action 键,让 action 函数知道它接收到的是哪种请求。

app/routes/home/profile.tsx 中,对 ProfileSettings 函数中返回的 form 进行以下更改

现在根据点击的按钮,您可以在 action 函数中处理不同的 _action

更新 action 函数,使用 switch 语句执行不同的操作

现在,如果用户保存表单,将命中 'save' 分支,并执行现有功能。然而,'delete' 分支目前不执行任何操作。

app/utils/user.server.ts 中添加一个新函数,该函数接受一个 id 并删除与其关联的用户

现在您可以填充个人资料页面中 "delete" 分支的其余部分了。

您的用户现在可以删除他们的账户了!

更新数据模型以添加参照完整性

此用户删除功能唯一的问题是,当用户被删除时,其所有撰写的点赞都变成了孤儿

您可以使用参照操作在删除撰写者时触发删除与其相关的任何点赞。

运行 npx prisma db push 以传播这些更改并生成 Prisma Client。

现在,如果您删除一个账户,该账户撰写的任何 Kudos 都将随之被删除!

添加表单验证

快要接近尾声了!最后一部分是在个人资料设置表单中连接错误消息处理。

您的 action 函数已经返回所有正确的错误消息;只需要处理它们即可。

app/routes/home/profile.tsx 中进行以下更改来处理这些错误

上面的代码片段中进行了以下更改

  1. 使用了 useActionData hook 来检索错误消息。这些错误消息存储在 state 变量中,并在用户提交错误表单后返回到模态框时用于填充表单。
  2. 添加了错误输出以显示任何表单级别的错误。
  3. 将错误数据传递给了 FormField 组件,以便在需要时显示字段级别的错误。

完成以上更改后,您将看到任何表单和验证错误都会显示在表单上。

总结与下一步

通过本文中的更改,您成功完成了 Kudos 应用!网站的所有部分都已功能完备,可以交付给您的用户了。

在本节中,您学习了

  • Remix 中的嵌套路由
  • AWS S3
  • 使用 Prisma 和 MongoDB 实现参照操作和完整性

在本系列的下一部分中,您将通过将构建好的应用程序部署到 Vercel 来完成收尾工作!

不要错过下一篇文章!

订阅 Prisma 新闻通讯