欢迎阅读本系列的第二篇文章,您将在这里学习如何使用 MongoDB、Prisma 和 Remix 从零开始构建一个全栈应用程序!在这一部分,您将为 Remix 应用程序设置基于会话的认证。
目录
引言
在本系列的上一部分中,您设置了 Remix 项目并运行了一个 MongoDB 数据库。您还配置了 TailwindCSS 和 Prisma,并开始在 schema.prisma
文件中建模 User
集合。
在这一部分,您将在应用程序中实现认证,允许用户通过登录和注册表单创建账户并登录。
注意:该项目的起始点可在 GitHub 仓库的 part-1 分支中找到。如果您想查看本部分的最终结果,请前往 part-2 分支。
开发环境
为了跟随提供的示例,您需要确保...
- ... 已安装 Node.js。
- ... 已安装 Git。
- ... 已安装 TailwindCSS VSCode 扩展。(可选)
- ... 已安装 Prisma VSCode 扩展。(可选)
注意:这些可选扩展为 Tailwind 和 Prisma 添加了一些非常好用的智能提示和语法高亮功能。
设置登录路由
要开始一切,您需要做的第一件事是设置一个 /login
路由,您的登录和注册表单将位于此处。
要在 Remix 框架中创建路由,请在 app/routes
文件夹中添加一个文件。该文件的名称将用作路由的名称。有关 Remix 中路由工作原理的更多信息,请查看其文档。
在 app/routes
中创建一个新文件,命名为 login.tsx
,内容如下
路由文件的默认导出是 Remix 渲染到浏览器中的组件。
使用 npm run dev
启动开发服务器,并导航到 http://localhost:3000/login
,您应该会看到路由被渲染出来。
这可行,但看起来还不太美观... 接下来您将通过添加实际的登录表单来稍作美化。
创建可重用的布局组件
首先,创建一个您将用于包装路由的组件,以提供一些共享的格式和样式。您将使用组合模式来创建这个 Layout
组件。
为了看到实际效果,在 app
文件夹内创建一个新文件夹,命名为 components
。在该文件夹内创建一个新文件,命名为 layout.tsx
。
在该文件中,导出以下函数组件
此组件使用 Tailwind 类指定您希望包裹在该组件内的任何内容都占据屏幕的完整宽度和高度,使用等宽字体,并显示适度深蓝色的背景。
注意 children
prop 被渲染在 <div>
内部。要查看投入使用时如何渲染,请查看以下代码片段
创建登录表单
现在您可以将该组件导入到 app/routes/login.tsx
文件中,并将您的 <h2>
标签包裹在新 Layout
组件内部,而不是它当前所在的 <div>
中
构建表单
接下来添加一个登录表单,它接受 email
和 password
输入并显示一个提交按钮。在顶部添加一个友好的欢迎消息,以便在用户进入您的网站时迎接他们,并使用Tailwind 的 flex 类将整个表单居中于屏幕。
此时,您无需担心 <form>
的 action 指向哪里,只需确保它的 method
值为 "post"
即可。稍后您将了解一些很酷的 Remix 魔术,它会为我们设置好 action!
创建表单字段组件
随着您添加更多表单,输入字段及其标签在本应用程序中将被大量重写,因此将它们提取到一个称为 FormField
的受控组件中,以避免代码重复。
在 app/components
中创建一个新文件,命名为 form-field.tsx
,您将在此构建 FormField
组件。然后添加以下代码以开始构建
这将定义并导出您之前在登录表单中拥有的完全相同的标签和输入组合,只是此组件将具有可配置的选项
htmlFor
:用于输入字段的id
和name
属性以及标签的htmlFor
属性的值。label
:在标签中显示的文本。value
:输入字段当前的受控值。type
:可选 允许您设置输入字段的type
属性,但默认为'text'
。onChange
:可选 允许您提供一个函数,在输入字段的值更改时运行。默认为一个空函数调用。
您现在可以用这个组件替换现有的标签和输入框
这将导入新的 FormField
组件,其状态将由父级管理(在这种情况下是登录表单)。对值的任何更新都将使用 handleInputChange
函数进行跟踪。
稍后您将回到 FormField
组件添加错误消息处理,但这暂时已经够用了!
添加注册表单
您还需要一种方式让用户注册账户,这意味着您将需要另一个表单。该表单将接受四个值
电子邮件
密码
名
姓
为了避免创建看起来与 /login
路由几乎完全相同的新 /signup
路由,我们将重新利用登录表单,使其可以在两个不同的操作之间切换:登录和注册。
将表单操作存储在状态中
首先,您需要一种方式让用户能够在表单之间切换,并让您的代码能够区分不同的表单。
在 Login
组件的顶部,在状态中创建另一个变量来存储您的 action
。
注意:默认状态将是登录屏幕。
接下来您需要一种方式来切换您想查看的状态。在“欢迎来到 Kudos”消息上方,添加以下按钮
此按钮的文本将根据 action
状态而改变。onClick
方法将在 'login
' 和 'register
' 值之间来回切换状态。
此页面上有几个静态文本位置,您需要根据您正在查看的表单进行调整。特别是“登录以点赞!”副标题以及表单内部的“登录”按钮。
更改表单的副标题,使其在每个表单上显示不同的消息
完全删除登录按钮,并将其替换为以下 <button>
这个新按钮有一个 name
和一个 value
属性。其值被设置为状态的 action
值。当您的表单提交时,该值将与表单数据一起作为 _action
传递。
注意:此技巧仅在
<button>
标签上有效,前提是name
属性以一个下划线开头。
根据您选择的表单,您现在应该会看到更新的消息。尝试点击几次“注册”和“登录”按钮。
添加可切换字段
此页面上的文本看起来不错,但两个表单都显示相同的输入字段。您最后需要做的是在显示注册表单时添加几个额外的字段。
在 password
字段后添加以下字段,并确保将新字段添加到 formData
对象中。
这里做了两个更改
- 您向
formData
状态添加了两个新键。 - 您添加了两个字段,这些字段根据您是查看登录表单还是注册表单而条件渲染。
您的登录和注册表单现在视觉上已经完成!是时候进入下一部分:让表单具备功能。
认证流程
这部分是有趣的部分,您将让您一直设计和构建的一切真正运作起来!
然而,在继续之前,您需要在项目中添加一个新的依赖项。运行以下命令
这将安装 bcryptjs
库及其类型定义。稍后您将使用它来哈希和比较密码。
认证将基于会话,遵循 Remix 的笑话应用教程中认证所使用的模式。
为了更好地可视化您的应用程序认证流程将是什么样子,请查看下面的图表。
为了认证用户,将有一系列步骤,包含两个潜在的路径(登录和注册)
- 用户将尝试登录或注册。
- 表单将被验证。
- 将调用
login
或register
函数。 - 如果是登录,服务器端代码将确保存在具有所提供登录详细信息的用户。如果是注册账户,它将确保不存在具有所提供电子邮件的账户。
- 如果上述步骤通过,将创建一个新的 cookie 会话,用户将被重定向到主页。
- 如果某个步骤未通过且出现问题,用户将被发送回登录或注册屏幕,并显示错误。
为了开始,在 app
目录内创建一个名为 utils
的文件夹。您将在此存储任何辅助函数、服务和配置文件。
在该新文件夹内,创建一个名为 auth.server.ts
的文件,您将在此编写与认证和会话相关的方法。
注意:Remix 不会将文件名中带有
.server
后缀的文件与发送到浏览器的代码一起打包。
构建注册函数
您将构建的第一个函数是注册函数,它将允许用户创建一个新账户。
从 app/utils/auth.server.ts
导出一个名为 register
的异步函数
在 app/utils
中另一个新文件 types.server.ts
中创建并导出一个定义注册表单将提供字段的 type
。
将该 type
导入到 app/utils/auth.server.ts
中,并在 register
函数中使用它来描述 user
参数,该参数将包含注册表单的数据
当调用此 register
函数并提供一个 user
时,您首先需要检查是否已存在具有所提供电子邮件的用户。
注意:请记住,
创建 PrismaClient
实例
您将使用 PrismaClient
执行数据库查询,但您的应用程序尚无法获得其实例。
在 app/utils
文件夹中创建一个新文件,命名为 prisma.server.ts
,您将在此创建并导出一个 Prisma Client 实例
注意:上面采取了预防措施,以防止在开发过程中实时重载使您的数据库连接饱和。
您现在有了访问数据库的方式。在 app/utils/auth.server.ts
中,导入已实例化的 PrismaClient
,并将以下内容添加到 register
函数中
注册函数现在将查询您的数据库中是否存在具有所提供电子邮件的任何用户。
这里使用 count
函数是因为它返回一个数值。如果没有匹配查询的记录,它将返回 0
,其求值结果为 false
。否则,将返回大于 0
的值,其求值结果为 true
。
如果找到用户,该函数将返回一个状态码为 400
的 json
响应。
更新您的数据模型
现在您可以确保当用户尝试注册时,不会存在具有所提供电子邮件的另一个用户。接下来,register
函数应该创建一个新用户。然而,我们将存储一些字段,这些字段在 Prisma schema 中尚不存在(firstName
和 lastName
)。
您将把这些数据存储在 User
模型中名为 profile
的字段中,该字段包含一个嵌入式文档。
打开您的 prisma/schema.prisma
文件并添加以下 type
块
type
关键字用于定义复合类型 – 允许您在文档内部定义一个文档。使用复合类型而非 JSON 类型的好处是,在查询文档时可以获得类型安全。
这超级有用,因为它使您能够明确定义数据的形状,否则由于 MongoDB 的灵活性,数据将是流动的并且能够包含任何内容。
您尚未将此新的复合类型(嵌入式文档的另一个名称)用于描述字段。在您的 User
模型中,添加一个新的 profile
字段,并使用 Profile
类型作为其数据类型
太棒了,您的 User
模型现在将包含一个 profile
嵌入式文档。重新生成 Prisma Client 以体现这些新更改
注意:您无需运行
prisma db push
,因为您没有添加任何新的集合或索引。
添加用户服务
在 app/utils
中创建另一个文件,命名为 user.server.ts
,您将在此编写任何用户特定的函数。在该文件中添加以下函数和导入语句
此 createUser
函数执行以下几项任务
- 它对注册表单中提供的密码进行哈希处理,因为您不应以明文形式存储密码。
- 它使用 Prisma 存储新的
User
文档。 - 它返回新用户的
id
和email
。
注意:您可以通过传入 JSON 对象直接在此查询中填充
profile
嵌入式文档的详细信息,并且由于 Prisma 生成的类型定义,您将看到一些很好的自动补全提示。
此函数将用于您的 register
函数中,以处理用户的实际创建。在 app/utils/auth.server.ts
中,导入新的 createUser
函数,并在 register
函数中调用它。
现在,当用户注册时,如果不存在具有所提供电子邮件的另一用户,则将创建一个新用户。如果在创建用户过程中出现问题,将向客户端返回一个错误,同时返回传入的 email
和 password
值。
构建登录函数
login
函数将接受一个 email
和一个 password
,因此要开始此函数,请在 app/utils/types.server.ts
中创建一个新的 LoginForm
类型来描述该数据
然后通过将以下内容添加到 app/utils/auth.server.ts
来创建 login
函数
上面的代码...
- ... 导入新的
type
和bcryptjs
库。 - ... 查询是否存在具有匹配电子邮件的用户。
- ... 如果找不到用户或提供的密码与数据库中的哈希值不匹配,则返回
null
值。 - ... 如果一切顺利,则返回用户的
id
和email
。
这将确保提供了正确的凭据,并将返回创建新 cookie 会话所需的数据。
添加会话管理
您现在需要一种方式,以便在用户登录或注册账户时为其生成一个 cookie 会话。Remix 提供了一种简单的方式来存储这些 cookie 会话,即使用其 createCookieSessionStorage
函数。
将该函数导入到 app/utils/auth.server.ts
中,并在您的导入语句之后直接添加一个新的 cookie 会话存储
上面的代码创建了一个具有以下几个设置的会话存储
name
:cookie 的名称。secure
:如果为true
,则只允许 cookie 通过 HTTPS 发送。secrets
:会话的密钥。sameSite
:指定 cookie 是否可以通过跨站点请求发送。path
:URL 中必须存在的路径,才能发送 cookie。maxAge
:定义 cookie 在自动删除前允许存在的时长。httpOnly
:如果为true
,则不允许 JavaScript 访问 cookie。
注意:在此了解更多关于不同 cookie 选项的信息。
您还需要在 .env
文件中设置一个会话密钥。添加一个名为 SESSION_SECRET
的变量,其值为一个密钥。例如
会话存储现已设置完成。在 app/utils/auth.server.ts
中再创建一个函数,用于实际创建 cookie 会话
此函数...
- ... 创建一个新会话。
- ... 将该会话的
userId
设置为登录用户的id
。 - ... 将用户重定向到您在调用此函数时可以指定的路由。
- ... 在设置 cookie header 时提交会话。
createUserSession
函数现在可以在用户成功注册或登录时用于 register
和 login
函数中。
处理登录和注册表单提交
您已经创建了创建新用户和登录所需的所有函数。现在您将在您构建的表单中使用它们。
在 app/routes/login.tsx
中,导出一个 action
函数。
注意:Remix 会寻找一个名为
action
的导出函数,以便为您定义的路由设置 POST 请求。
现在在 app/utils
中创建一个新文件,命名为 validators.server.ts
,其中包含将用于验证表单输入的几个验证器函数。
在 app/routes/login.tsx
的 action
函数中,从请求中获取表单数据并验证其格式是否正确。
上面的代码可能看起来有点复杂,但简而言之它...
- ... 从请求对象中提取表单数据。
- ... 确保提供了
email
和password
。 - ... 如果
_action
值为"register
",则确保提供了firstName
和lastName
。 - ... 如果发生任何问题,则返回错误以及表单字段的值,以便以后在这些字段无效时,您可以使用用户的输入和错误消息重新填充表单。
您最后需要做的是,如果输入看起来没问题,则实际运行您的 register
和 login
函数。
switch
语句将允许您根据表单中的 _action
值有条件地运行 login
和 register
函数。
为了实际触发此 action,表单需要 post 到此路由。幸运的是,Remix 会处理此事,因为它在识别到导出的 action
函数时,会自动将 POST
请求配置到 /login
路由。
如果您尝试登录或创建账户,您应该会看到之后您被发送到主屏幕。成功!🎉
在私有路由上授权用户
接下来您将做的事情是为了让用户体验更好,即根据用户是否具有有效会话,自动将他们重定向到主页或登录页。
在 app/utils/auth.server.ts
中,您需要添加几个辅助函数。
这是一个很多新功能。以下是上面的函数将执行的操作
requireUserId
检查用户的会话。如果会话存在,则成功并返回userId
。如果失败,则会将用户重定向到登录屏幕。getUserSession
根据请求的 cookie 获取当前用户的会话。getUserId
从会话存储中返回当前用户的id
。getUser
返回与当前会话关联的整个user
文档。如果未找到,用户将被注销。logout
销毁当前会话并将用户重定向到登录屏幕。
有了这些,您就可以在私有路由上实现一些不错的授权了。
在 app/routes/index.tsx
中,如果用户未登录,通过添加以下内容将用户返回到登录屏幕
注意:Remix 在提供页面之前运行
loader
函数 之前。这意味着 loader 中的任何重定向都会在您的页面提供之前触发。
如果您在未登录的情况下尝试导航到应用程序的基本路由 (/
),则应该被重定向到登录屏幕,并在 URL 中带有 redirectTo
参数。
注意:如果您已经登录,可能需要清除您的 cookie。
接下来,做的事情基本相反。如果已登录用户尝试访问登录页面,他们应该被重定向到主页,因为他们已经登录。将以下代码添加到 app/routes/login.tsx
添加表单验证
太棒了!您的登录和注册表单正在工作,并且您已经在私有路由上设置了授权和重定向。您几乎到达终点线了!
最后要做的是添加表单验证并显示从 action
函数返回的错误消息。
更新 FormField
组件,使其能够处理错误消息。
此组件现在将接收错误消息。当用户开始在该字段中输入时,如果显示任何错误消息,则会将其清除。
在登录表单中,您需要使用 Remix 的 useActionData
钩子访问从 action 返回的数据,以便提取错误消息。
此代码添加了以下内容
- 连接到从
action
函数返回的数据。 - 设置一个
errors
变量,它将以对象形式保存字段特定的错误,例如“无效电子邮件”。它还设置一个formError
变量,它将保存用于显示表单消息的错误消息,例如“登录信息不正确”。 - 更新
formData
状态变量,以便在可用时默认使用action
函数返回的任何值。
如果向用户显示错误并且用户切换表单,您需要清除表单以及显示的任何错误。使用这些 effects
来实现此目的
有了这些,您最终可以让您的表单和字段知道要显示哪些错误了。
现在您应该可以看到注册和登录表单上的错误消息和表单重置功能正常工作了!
总结与后续
为您坚持到本节结束点赞 (😉)!有很多内容需要介绍,但希望您能够从中理解到
- 如何在 Remix 中设置路由。
- 如何构建带验证功能的登录和注册表单。
- 基于会话的身份验证如何工作。
- 如何通过实现授权来保护私有路由。
- 在使用 Prisma 创建和验证用户时,如何存储和查询您的数据。
在本系列的下一节中,您将构建 Kudos 的主页和点赞分享功能。您还将为点赞动态添加搜索和过滤功能。
不要错过下一篇文章!
订阅 Prisma 新闻通讯