数据从前端传递到后端
对于Next.js项目来说,有两种方式可以把数据从前端传递到后端,一种是传统的api方式,一种是server action方式。
为了让没有开发经验的小伙伴理解这两种方式的区别,我来举个通俗的例子。
想象一下你去一家餐厅吃饭,你到餐厅门口找到服务员,告诉ta你要点的菜,ta在系统上一顿操作,下完单,你就找个位置坐着等上菜。后来你约的朋友也来了,你朋友一看你抠抠搜搜点的太少了,于是扫了桌上的二维码,直接在小程序里又加了几个菜。
你的点餐方式就是API的方式,你把你的需求数据告诉点餐的服务员(API),服务员通过操作点餐系统把数据传到后厨(后端),后厨师傅开始给你准备菜,做好了再给你端上来(返回数据)。
而你朋友点餐的方式,就是server action的方式,直接在系统里提交需求,不用通过服务员这个API,后厨直接在系统里就能看到你新增的需求,原则上只要人不多(没有阻塞),立马就能开始给你准备。
当然,这只是一个便于理解的过度简化的例子。关于API和server action的详细区别,大家有兴趣的话可以看下面的解释:
API和Server Action各自的优势使用场景
特点总结 | 优势 | 使用场景 |
---|---|---|
1. 跨平台和跨语言支持 | 可以被不同的前端应用(如 Web、移动端、桌面应用等)调用,支持多种编程语言和平台。使得开发者可以在不同的环境中重用相同的后端服务。 | 当需要为多个客户端(如 iOS、Android 和 Web 应用)提供相同的功能时,API 是理想的选择。 |
2. 松耦合架构 | 允许前端和后端独立开发和部署,减少了对彼此的依赖。使得团队可以并行工作,快速迭代。 | 在大型项目中,前后端团队分开工作时,API 可以帮助保持清晰的界限和责任分离。 |
3. 缓存和性能优化 | 可以利用缓存机制(如 CDN、反向代理等)来提高性能,减少服务器负担。通过缓存,重复请求可以更快地响应。 | 对于高流量的应用,尤其是需要频繁读取数据的场景,API 的缓存机制可以显著提高响应速度。 |
4. 安全性和认证 | 可以实现更复杂的认证和授权机制(如 OAuth、JWT 等),确保数据的安全性。通过 API 网关,可以集中管理安全策略。 | 在需要处理敏感数据或需要复杂权限管理的应用中,API 提供了更灵活的安全控制。 |
5. 版本控制和向后兼容性 | 可以通过版本控制来管理不同版本的接口,确保向后兼容性。这使得开发者可以在不影响现有用户的情况下推出新功能。 | 当需要频繁更新和迭代功能时,API 的版本控制可以帮助管理不同的客户端需求。 |
6. 集成和扩展性 | API 允许与第三方服务和系统进行集成,扩展应用的功能。通过 API,开发者可以轻松地接入外部服务(如支付、社交媒体等)。 | 在需要与其他系统(如 CRM、ERP 等)集成的企业应用中,API 是必不可少的。 |
总结:虽然 Server Action 在某些场景下提供了更直接的交互和性能优势,但 API 方式在跨平台支持、松耦合架构、缓存优化、安全性、版本控制和集成扩展性等方面具有明显的优势。因此,在选择使用哪种方式时,开发者需要根据具体的项目需求和场景进行权衡。
Server Action
特点总结 | 优势 | 使用场景 |
---|---|---|
1. 更快的响应时间 | 直接在服务器上处理请求,减少了网络延迟和中间环节,通常可以提供更快的响应时间。 | 要求快速响应的实时交互应用场景,如在线游戏或实时协作工具。 |
2. 简化的数据处理 | 直接访问服务器端的状态和数据,简化了数据处理的复杂性。 | 在需要频繁更新和渲染数据的应用中,例如动态仪表盘或数据可视化工具 |
3. 更好的状态管理 | 更容易地管理应用的状态,因为它们可以直接访问服务器的状态,而不需要通过 API 进行状态同步。 | 在需要复杂状态管理的应用中,例如电子商务网站的购物车功能 |
4. 减少的网络请求 | 可以在同一请求中处理多个操作,减少了对网络的依赖和请求次数,提高整体性能。 | 在需要批量处理数据的场景中,例如用户注册时同时创建用户资料和发送欢迎邮件 |
5. 更强的安全性 | 可以在服务器端直接处理敏感数据,避免将敏感信息暴露给客户端。由于不需要通过 API 传输数据,减少了数据被拦截的风险。 | 在处理敏感信息的应用中,例如金融应用或医疗应用,Server Action 提供了更高的安全性。 |
6. 更好的开发体验 | 允许开发者在同一文件中处理数据和视图逻辑,提供了更一致的开发体验。开发者可以更容易地理解和维护代码,因为数据处理和渲染逻辑紧密结合。 | 在小型项目或快速原型开发中,Server Action 可以提高开发效率,减少上下文切换。 |
总结:虽然 API 方式在跨平台支持、松耦合架构和集成扩展性等方面具有优势,但 Server Action 在响应时间、数据处理简化、状态管理、减少网络请求、安全性和开发体验等方面提供了明显的优势。因此,在选择使用哪种方式时,开发者需要根据具体的项目需求和场景进行权衡。
由于我们这个项目默认的场景是Web全栈项目,所以我们会使用server action的方式进行后端的数据处理。
初识Server Action
接下来,就让我们来创建一个register action,感受一下server action的运行机制。在项目根目录下创建action文件夹,并在其中创建register.ts文件,写入如下代码:
"user server";
export const register = async (values: any) => {
console.log(values);
};
注意,我们这里给values的类型设置为any是为了方便,随后我们会给它设置正确的类型。所以可以先不用管vscode中any的类型错误提示。
然后我们在register-form.tsx文件中,引入register action,并调用它:
"use client";
import { useState } from "react";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { RegisterSchema } from "@/schemas/index";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { CardWrapper } from "./card-wrapper";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { PasswordValidation } from "./password-validation";
import { FiCheckCircle, FiEye, FiEyeOff } from "react-icons/fi";
import { register } from "@/actions/register";
export const RegisterForm = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [isEditingPassword, setIsEditingPassword] = useState(false);
const [isEditingConfirmPassword, setIsEditingConfirmPassword] =
useState(false);
const [strength, setStrength] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// 验证密码强度级别
const checkPasswordStrength = (password: string) => {
let strength = 0;
if (password.length >= 6) {
if (
password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password)
) {
if (password.length >= 10 && /[0-9]/.test(password)) {
strength++;
}
strength++;
}
strength++;
}
setStrength(strength);
};
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
defaultValues: {
email: "",
username: "",
password: "",
confirmPassword: "",
},
});
const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
register(values);
};
return (
<CardWrapper
headerLabel="创建账号"
backButtonLabel="已有账号?直接登录"
backButtonHref="/sign-in"
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input
{...field}
placeholder="zhangsan@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormControl>
<Input {...field} placeholder="superhero" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>密码</FormLabel>
<div className="relative">
<FormControl>
<Input
{...field}
className="text-sm"
placeholder="至少10个字符,包含大小写字母和数字。"
type={showPassword ? "text" : "password"} // 根据状态切换类型
onChange={(e) => {
field.onChange(e);
setPassword(e.target.value); // 更新密码状态
checkPasswordStrength(e.target.value); // 检查密码强度
setIsEditingPassword(true);
}}
/>
</FormControl>
<Button
variant="ghost"
className="absolute inset-y-0 right-0 flex items-center pr-3"
onClick={() => setShowPassword(!showPassword)} // 切换显示状态
>
{showPassword ? <FiEyeOff /> : <FiEye />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
{isEditingPassword && ( // 仅在输入密码时显示颜色条
<PasswordValidation strength={strength} />
)}
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>确认密码</FormLabel>
<div className="relative">
<FormControl>
<Input
{...field}
className="text-sm"
placeholder="请再次确认密码"
type={showConfirmPassword ? "text" : "password"} // 根据状态切换类型
onChange={(e) => {
field.onChange(e);
setConfirmPassword(e.target.value);
checkPasswordStrength(e.target.value); // 检查密码强度
setIsEditingConfirmPassword(true);
}}
/>
</FormControl>
<Button
variant="ghost"
className="absolute inset-y-0 right-0 flex items-center pr-3"
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
>
{showConfirmPassword ? <FiEyeOff /> : <FiEye />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
{isEditingConfirmPassword && ( // 仅在输入确认密码时显示颜色条
<PasswordValidation strength={strength} />
)}
{password && confirmPassword && password === confirmPassword && (
<div className="text-sm text-emerald-500 flex items-center">
<FiCheckCircle className="mr-1" /> 两次密码输入一致
</div>
)}
</div>
<Button type="submit" className="w-full">
立即注册
</Button>
</form>
</Form>
</CardWrapper>
);
};
现在打开注册页面 http://localhost:3000/sign-up ,输入必要信息,点击”立即注册”按钮,我们就可以在vscode终端看到我们提交的信息:
注意,server action默认都要放在根目录下的actions文件夹下,文件类型为.ts
文件,并且文件开头都要加上”user server”;,否则在前端的表单组件中引入会被当做前端函数处理。大家可以试试把上面的"use server";
去掉,看看会发生什么。
如果大家分不清楚项目中哪些文件是前端的,哪些是后端的,可以暂时先记住一个点,就是前端代码中的console.log
是打印在浏览器控制台,后端代码中的console.log
是打印在vscode终端。
所以前端是怎么把用户输入的数据传递到后端的呢?首先form表单的提交按钮默认会调用onSubmit函数,而onSubmit函数又调用了register action,register action接收到了form表单提交的数据,就在服务端的控制台打印了出来。
使用Zod进行数据验证
明白了流程,我们现在开始使用Zod进行数据验证。虽然前端已经做了数据验证,但是为了防止用户在表单中作弊,我们还是需要再在后端进行一次数据验证。
我们前面已经创建了注册表单的验证规则RegisterSchema,现在我们只需要在register action中引入这个验证规则,并使用Zod进行数据验证即可:
"use server";
import * as z from "zod";
import { RegisterSchema } from "@/schemas/index";
export const register = async (values: z.infer<typeof RegisterSchema>) => {
const validatedFields = RegisterSchema.safeParse(values);
if (!validatedFields.success) {
return {
error: "Invalid content!"
};
}
const { email, password, username } = validatedFields.data;
console.log(email, password, username );
};
关于新增代码的解释:
z.infer<typeof RegisterSchema>
:从RegisterSchema获取values的类型。RegisterSchema.safeParse(values)
:使用Zod的safeParse方法进行数据验证。if (!validatedFields.success)
:如果数据验证失败,返回错误信息。const { email, password, username } = validatedFields.data
:解构验证后的数据。
现在我们可以再访问注册页面 http://localhost:3000/sign-up ,测试一下,不出意外的话我们仍然可以在vscode终端看到我们提交的信息,当然这次是分开呈现的,因为我们把这几个数据从一个对象中解构出来了:
给密码加密
相信大家也注意到了,现在从前端传过来的密码是明文密码,这怎么能行呢?明文密码如果直接存到网站的服务器里那还了得,如果被黑客获取,或者被网站服务商拿来作恶,后果不堪设想,所以我们需要给密码加密。
怎么加密呢?当然不用我们自己去研究密码学和加密算法,使用现成的第三方库就行。这里我们使用bcryptjs库来给密码加密。
首先,我们安装bcryptjs库:
npm install bcryptjs
然后,我们在register action中引入bcryptjs库,并使用它来给密码加密:
"use server";
import * as z from "zod";
import { RegisterSchema } from "@/schemas/index";
import bcrypt from "bcryptjs";
export const register = async (values: z.infer<typeof RegisterSchema>) => {
const validatedFields = RegisterSchema.safeParse(values);
if (!validatedFields.success) {
return {
error: "Invalid content!"
};
}
const { email, password, username } = validatedFields.data;
const hashedPassword = await bcrypt.hash(password, 10);
console.log("加密后的密码:", hashedPassword);
};
再操作一遍注册流程,然后看vscode终端,我们就可以看到加密后的密码了:
那么现在,密码也加密好了,邮箱、用户名也都有了,就差一个数据库来存储用户的数据了。所以下一章,我们来配置数据库。