此示例的代码可以在 GitHub 上找到:分支为 credentialssignup
。
地址:https://github.com/peterlidee/NNAS/tree/credentialssignup
我们已经有了表单,接下来该做什么呢?我们还需要完成哪些步骤?
- 调用 Strapi 的注册端点。
- 处理错误和成功情况。
- 验证字段。
- 向用户显示错误信息。
- 发送确认邮件。
- 设置加载状态。
在前面的章节中,我们构建的登录表单中,我们手动设置了数据、错误和加载状态。我们不得不这样做,因为 NextAuth 的 signIn
函数只能在客户端使用。
然而,现在我们不需要使用 NextAuth。我们只需要调用一个 Strapi 的端点,这可以在服务器端完成。怎么做呢?可以设置一个自定义的 Next.js API 端点(即路由处理器),或者更简单的方法是使用服务器操作。与服务器操作搭配得最好的是什么?是 useFormState
和 useFormStatus
钩子,用于处理我们的数据、错误和加载状态。
因此,我们将创建一个 signUpAction
服务器操作来:
- 使用 Zod(服务器端)验证输入字段。
- 调用 Strapi 注册端点并处理错误或成功情况。
如果一切顺利且没有错误,signUpAction
会将用户重定向到另一个页面,并显示反馈消息:“我们已向您发送了一封电子邮件……”。如果出现错误(无论是 Zod 还是 Strapi),我们的服务器操作会将带有错误属性的状态返回给 useFormState
。
signUpAction
我们来编写这段代码。由于我们使用了 useFormState
,我们的服务器操作将接收两个参数:
prevState
: 当前的useFormState
状态。formData
: 接口,允许读取和操作表单的所有字段。
我们首先使用 Zod 验证字段:
// frontend/src/components/auth/signup/signUpAction.ts
'use server';
import { z } from 'zod';
const formSchema = z.object({
username: z.string().min(2).max(30).trim(),
email: z.string().email('请输入有效的邮箱。').trim(),
password: z.string().min(6).max(30).trim(),
});
export default async function signUpAction(
prevState: SignUpFormStateT,
formData: FormData
) {
const validatedFields = formSchema.safeParse({
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password'),
});
if (!validatedFields.success) {
return {
error: true,
fieldErrors: validatedFields.error.flatten().fieldErrors,
message: '请核实您的数据。',
};
}
const { username, email, password } = validatedFields.data;
// 进一步处理
}
Zod 验证的部分应该很清晰。我们创建了一个模式,用于检查输入字段是否符合要求。如果不符合,我们返回一个带有 error
属性的对象给 useFormState
。需要注意的是:
- 这里的代码会报错,因为我们还没有定义
SignUpFormStateT
类型,我们稍后会处理这个问题。 - 如果你需要更高级的密码要求,可以添加它们。
接下来,我们使用验证过的字段调用 Strapi。我们将整个调用包装在 try-catch
块中:
// 进一步处理
try {
const strapiResponse = await fetch(
process.env.STRAPI_BACKEND_URL + '/api/auth/local/register',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, email, password }),
cache: 'no-cache',
}
);
// 处理 Strapi 错误
if (!strapiResponse.ok) {
const response = {
error: true,
message: '',
};
// 检查响应是否为 JSON 格式
const contentType = strapiResponse.headers.get('content-type');
if (contentType === 'application/json; charset=utf-8') {
const data = await strapiResponse.json();
response.message = data.error.message;
} else {
response.message = strapiResponse.statusText;
}
return response;
}
} catch (error) {
// 网络错误或其他问题
return {
error: true,
message: 'message' in error ? error.message : error.statusText,
};
}
我们之前已经介绍过 fetch
。我们然后检查 strapiResponse
是否有错误。注意,并非所有 Strapi 错误都会是 JSON 格式,因此我们需要进行内容类型检查。如果有错误,我们会返回一个带有一些属性的对象给 useFormState
。catch
块的处理方式类似。
这里还缺少一个部分:成功处理。在成功时,我们将使用 next/navigation
的 redirect
函数将用户重定向到消息页面。但你不应该在 try-catch
块内调用 redirect
。因此,我们让程序通过 try-catch
块,只有在通过后,我们才添加重定向:
// 页面尚未创建
redirect('/confirmation/message');
希望这部分内容易于理解。当 strapiResponse
成功时,我们希望将用户重定向到另一个页面。我们不需要从这个响应中获取数据。当没有错误时,JavaScript 将继续执行服务器操作中的下一行代码,即重定向。
现在我们完成了,除了 SignUpFormStateT
类型,稍后我们会涉及。
useFormState
现在我们已经有了服务器操作,我们回到表单中。我们需要设置 useFormState
钩子。useFormState
是一个将服务器操作与状态关联起来的钩子。使用 useFormState
意味着你不必手动设置状态。
我们用一个服务器操作(signUpAction
)和一个初始状态来初始化 useFormState
。它返回这个状态和一个 formAction
。基本上,formAction
是一个包装了我们传递的服务器操作的函数。任何从我们的服务器操作返回的内容都会成为我们的新状态。当调用服务器操作时,useFormState
也会将该状态和 formData
传递给服务器操作,这样我们就可以访问这些内容。我们在上面的 signUpAction
中已经看到了这一点:prevState
和 formData
参数。
让我们将它添加到我们的表单中:
const initialState = {
error: false,
};
const [state, formAction] = useFormState(signUpAction, initialState);
// ...
<form className='my-8' action={formAction}></form>;
如何为 useFormState
和服务器操作设置类型
为 useFormState
设置类型可能有点棘手,因为你需要考虑服务器操作的每个可能的返回值以及 initialState
。这需要你认真思考并付出努力。
在 signUpAction
中:我们知道它只在出现错误时返回:Zod 错误、Strapi 错误或 catch
错误。成功时,它不会返回而是重定向。那么我们在这些情况下返回了什么呢?
{
error: true,
message: '一些错误信息'
}
只有 Zod 错误有一个额外的属性,即 Zod 错误对象:
export type InputErrorsT = {
username?: string[],
email?: string[],
password?: string[],
};
由于 signUpAction
在成功时不会返回,其返回类型看起来像这样:
{
error: true,
message: string,
inputErrors?: InputErrorsT
}
inputErrors
(即 Zod 错误)是可选的。我们的 initialState
只有一个属性:error: false
。我们可以通过将 message
设置为可选来更新我们的类型。
TypeScript 会在 useFormState
钩子中正确推断这一点,而无需显式设置类型:
但我们确实需要在 signUpAction
操作中显式设置类型,因为 TypeScript 的工作方式就是如此:
export default async function signUpAction(
prevState: SignUpFormStateT,
formData: FormData
) {
//...
}
既然我们必须在服务器操作中显式设置它,我们也可以在 useFormState
钩子中显式设置它。以下是具体的实现:
export type InputErrorsT = {
username?: string[];
email?: string[];
password?: string[];
};
export type RegisterFormStateT = {
error: boolean;
inputErrors?: InputErrorsT;
message?: string;
};
// ...
const [state, formAction] = useFormState<SignUpFormStateT, FormData>(
signUpAction,
initialState
);
这个设置是正确的,没有 TS 错误。但我们可以做得更好。我们将更新类型为区分联合类型。这
很有意义:它要么是错误状态,要么是 initialState
。这是我们的更新:
type SignUpFormInitialStateT = {
error: false; // 不是 boolean
};
type SignUpFormErrorStateT = {
error: true; // 不是 boolean
message: string; // 不是可选的
inputErrors?: InputErrorsT;
};
// 区分联合类型
export type SignUpFormStateT = SignUpFormInitialStateT | SignUpFormErrorStateT;
// 在这里显式设置类型
const initialState: SignUpFormInitialStateT = {
error: false,
};
// ...
const [state, formAction] = useFormState<SignUpFormStateT, FormData>(
signUpAction,
initialState
);
希望这部分内容对你有意义。尽管这是一个简单的例子,但已经变得有些复杂了。这需要一些努力。
使用状态
我们还没有实际使用状态向用户显示反馈信息。现在我们来添加它。对于每个输入字段,我们检查相应的 Zod 错误并显示它。
<div className='mb-3'>
<label htmlFor='username' className='block mb-1'>
用户名 *
</label>
<input
type='text'
id='username'
name='username'
required
className='border border-gray-300 w-full rounded-sm px-2 py-1'
/>
{state.error && state?.inputErrors?.username ? (
<div className='text-red-700' aria-live='polite'>
{state.inputErrors.username[0]}
</div>
) : null}
</div>
// 处理其他输入字段
最后,在表单的底部显示 error.message
:
{
state.error && state.message ? (
<div className='text-red-700' aria-live='polite'>
{state.message}
</div>
) : null;
}
使用 useFormStatus
处理加载状态
最后一个任务是设置加载状态。为此,我们将使用 useFormStatus
钩子:
useFormStatus
是一个钩子,可以为你提供上一次表单提交的状态信息。
在登录功能中,我们手动设置了加载状态。由于我们在这个表单中使用了 useFormState
,我们还可以使用 useFormStatus
来为我们处理加载状态。有一个小问题,我们必须在与调用 useFormState
的组件不同的组件中调用 useFormStatus
。创建一个新组件:
// frontend/src/components/auth/PendingSubmitButton.tsx
import { useFormStatus } from 'react-dom';
export default function PendingSubmitButton() {
const { pending } = useFormStatus();
return (
<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={pending}
aria-disabled={pending}
>
发送
</button>
);
}
然后我们将其插入到我们的表单组件中。这样就完成了注册功能。我们进行了一些快速测试:
- 输入太短的内容会导致 Zod 错误。
- 如果用户名或邮箱已存在于数据库中,会导致 Strapi 错误。
- 提交按钮在提交时有一个(非常快的)加载状态。
- 成功后,我们将被重定向到尚未构建的页面。
一切正常!
结论
我们刚刚构建了注册页面。我们从 Google 登录按钮的问题开始,因为此按钮的错误处理仅在登录页面上有效,我们不得不将其从注册页面中去除。虽然这不是最优的解决方案,但有一些方法可以解决这个问题。
接下来,我们研究了如何为这个测试应用设置注册流程。这个流程有很多种实现方式,每种方式都有其自身的挑战和解决方案。我们选择了一种经典的方法,即不立即让用户登录。
由于这个流程的结果,我们能够将 NextAuth 从注册流程中移除。这反过来又意味着我们可以使用 useFormState
结合服务器操作。这非常好,因为它让我们的工作变得更容易。
最后,我们处理了为 useFormState
设置类型,这可能有点复杂,需要一些努力。
下一步将是发送电子邮件并设置电子邮件验证页面。
首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。