本章的所有代码都可以在 GitHub 上找到,分支为 credentialssignin

https://github.com/peterlidee/NNAS/tree/credentialssignin

我们还需要处理以下几个问题:

  1. 表单输入验证
  2. 处理 signIn 返回的错误
  3. 处理成功登录后的操作
  4. 处理加载状态

表单验证

我们将使用客户端的 Zod 进行输入验证。Zod 处理以 TypeScript 为优先的模式进行架构验证,并支持静态类型推断。这意味着它不仅会验证你的字段,还会为验证后的字段设置类型。

虽然我对 Zod 还不太熟悉,但它似乎与服务器操作和 useFormState 钩子配合得很好。我们稍后会在这个上下文中使用它,但现在我们会在客户端使用。首先安装 Zod:

npm install zod

我们需要验证用户名/电子邮件和密码输入字段。这些都在我们之前设置的 useState 钩子中。我们首先创建一个 formSchema

const formSchema = z.object({
  identifier: z.string().min(2).max(30),
  password: z
    .string()
    .min(6, { message: '密码长度至少为 6 个字符。' })
    .max(30),
});

这很容易理解。我们希望两个字段都是字符串,并且有最小和最大长度。当密码太短时,我们提供一个自定义错误信息。注意,关于密码验证的文章和观点很多,请根据自己的需要进行处理。

handleSubmit 函数中,在调用 signIn 之前,我们调用这个 formSchema。它将检查我们的输入字段值是否符合我们在 formSchema 中设置的条件。通过使用 validatedFields.success 属性,我们可以处理接下来需要发生的事情。

在出错时,Zod 会为我们生成错误消息,我们将其保存在状态中。成功时,由于我们的输入是有效的,我们可以简单地调用 signIn 函数。

type FormErrorsT = {
  identifier?: undefined | string[],
  password?: undefined | string[],
};

const [errors, setErrors] = useState < FormErrorsT > {};

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();

  const validatedFields = formSchema.safeParse(data);
  if (!validatedFields.success) {
    setErrors(validatedFields.error.formErrors.fieldErrors);
  } else {
    // 没有 Zod 错误
    // 调用 signIn
  }
};

最后,更新我们的表单 JSX 以显示错误消息。对于每个输入字段,我们可以这样处理:

{
  errors?.identifier ? (
    <div className='text-red-700' aria-live='polite'>
      {errors.identifier[0]}
    </div>
  ) : null;
}

以及在表单下方显示一般错误:

{
  errors.password || errors.identifier ? (
    <div className='text-red-700' aria-live='polite'>
      出了点问题。请检查您的数据。
    </div>
  ) : null;
}

image.png

处理 NextAuth signIn 函数返回的错误

在上一章中,我们通过在 authorize 函数中抛出错误来处理 Strapi 错误。在前端,我们在 signIn 函数的选项对象中添加了 redirect: false 属性。这使得 signIn 返回一个带有 error 属性的对象:

{
  error?: string;
  status: number;
  ok: boolean;
  url?: string;
}

现在我们将使用这个对象向用户提供反馈。我们检查是否有错误,然后将该错误放入我们已经用于 Zod 错误的错误状态中。我们扩展我们的错误类型,添加 strapiError 属性:

type FormErrorsT = {
  identifier?: undefined | string[];
  password?: undefined | string[];
  strapiError?: string;
};

监听 signInResponse 中的错误并将其放入错误状态中:

if (signInResponse && !signInResponse?.ok) {
  setErrors({
    strapiError: signInResponse.error
      ? signInResponse.error
      : '出了点问题。',
  });
} else {
  // 处理成功
}

在表单下方显示 strapiError

{
  errors.strapiError ? (
    <div className='text-red-700' aria-live='polite'>
      出了点问题:{errors.strapiError}
    </div>
  ) : null;
}

我们现在正在表单组件中显示我们在 authorize 中抛出的自定义错误!

image.png

处理成功登录

我们还没有实际处理通过凭据成功登录的过程。当我们没有错误时,表明我们已经成功登录。那么接下来要做什么呢?我们之前设置了 GoogleProvider,以在成功登录时重定向到上一页。

这是一个我不会在生产环境中采用的解决方案。例如,如果上一页是注册页面,那么在登录后将他们返回到注册页面将是糟糕的用户体验。但我将把这个决定留给你,并坚持简单的重定向。

如前所述,我们只需从 callbackUrl 搜索参数中获取我们的 URL:

const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/';
const router = useRouter();

然后我们只需推送新路由:

router.push(callbackUrl);

这里注意:signIn 函数选项对象有一个 callbackUrl 属性。如果你喜欢,可以使用它来重定向到一个固定的页面。

让我们测试一下我们刚刚编写的代码。运行我们的应用并尝试使用正确的凭据登录。一切正常,我们被重定向了

image.png

更新 getServerSession 客户端和服务器会话

事情出了点问题!显然的问题是我们的 <LoggedInClient /> 组件(使用 useSession 钩子)说我们已登录,但我们的服务器组件 <LoggedInServer />(使用 getServerSession)却说我们没有登录。我们的 <NavbarUser /> 组件(使用 getServerSession)也失败了。它应该显示用户名并跟随 <SignOutButton />,但它没有显示用户名并显示了登录链接。

因此,似乎 getServerSession 存在问题。它似乎没有更新。是的,我们确实已登录,因此 useSession 是正确的。转向 NextAuth 文档没有提供答案。没有 NextAuth 方法可以手动触发 getServerSession 刷新。

我花了相当多的时间试图弄清楚这个问题。我们做的一个改变是添加了 redirect: false 选项。我们暂时删除这个 redirect 选项并尝试登录时,我们注意到了一些事情。页面会完全刷新!这意味着 Next 和 NextAuth 获取了新的更新数据。

redirect: false 选项重新启用时,当凭据成功登录时,我们执行 router.push。这不会触发页面刷新,正如我们在单页应用程序中所预期的那样。但这也意味着我们的服务器组件(如 <LoggedInServer /><NavbarUser />)没有刷新并显示了过时的状态。

这就是导致我们问题的原因。我们如何解决这个问题?我查看了很多资料,谷歌搜索并阅读了许多内容,直到有人提到 router.refresh。根据 Next 文档:

router.refresh(): 刷新当前路由。向服务器发出新的请求,重新获取数据请求,并重新渲染服务器组件。客户端将合并更新的 React 服务器组件负载,而不会丢失未受影响的客户端 React(例如 `useState`)或浏览器状态(例如滚动位置)。

这解决了问题。我们添加这一行并再次测试:

// 处理成功
router.push(callbackUrl);
router.refresh();

image.png

设置加载状态

让我们处理在较慢的连接上快速点击按钮时禁用按钮的情况。

创建一个加载状态并将其初始化为 false。当我们提交时,将加载状态设置为 true。在 Zod 错误或 Strapi 错误时,将其设置为 false。更新按钮以包含 disabled 属性和一些禁用样式:

<button
  type='submit'
  className='bg-blue-400 px-4 py-2 rounded-md disabled:bg-sky-200 disabled:text-gray-400 disabled:cursor-wait'
  disabled={loading}
  aria-disabled={loading}
>
  登录
</button>

完成!请查看 GitHub 上完成的 <SignIn /> 组件。

结论

我们刚刚设置了一个凭据登录流程,并且花了一些时间(3 章)!以下是我们所做的事情的概述:

  • 将 CredentialsProvider 添加到 NextAuth 提供程序。
  • 设置一个带有受控输入的表单。
  • 在客户端使用凭据调用 signIn
  • 解释并写出 `authorize

` 函数/回调。

  • 调用 Strapi 登录端点。
  • authorize 中返回响应。
  • 更新 jwt 回调。
  • 修复回调类型。
  • 探索 NextAuth 默认的 authorize 函数错误处理。
  • authorize 中处理 Strapi 错误。
  • 使用 redirect: false 更新 signIn
  • 使用 Zod 进行表单验证。
  • 处理 signIn 的返回值。
  • 在成功登录时重定向用户。
  • 修复 getServerSession 未重新加载的问题。
  • 添加加载状态。

认证流程是复杂的。到现在为止,你应该对 NextAuth 有了很好的理解,并知道如何在自己的项目中使用它。希望我能够清楚地解释一切,并帮助你节省一些时间。在下一章中,我们将为 CredentialsProvider 实现一个注册流程。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.2k 声望105k 粉丝