Appearance
渐进增强(Progressive Enhancement)
学习目标
通过本章学习,你将掌握:
- 渐进增强的核心概念
- JavaScript禁用时的表单行为
- 无JavaScript的表单提交
- 优雅降级策略
- 服务端渲染优势
- 可访问性提升
- 实际应用场景
- 最佳实践
第一部分:渐进增强基础
1.1 什么是渐进增强
渐进增强是一种Web设计策略:基础功能在所有环境下都能工作,而高级功能在支持的环境中增强体验。
jsx
// Form Actions天然支持渐进增强
// Server Action
'use server';
export async function submitContact(formData) {
const name = formData.get('name');
const email = formData.get('email');
await db.contacts.create({
data: { name, email }
});
redirect('/thank-you');
}
// 表单
function ContactForm() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">提交</button>
</form>
);
}
// 工作方式:
// JavaScript启用:异步提交,无刷新
// JavaScript禁用:标准表单POST,页面刷新
// 两种情况都能工作!1.2 渐进增强的核心原则
jsx
// 原则1:HTML优先
// 基础功能应该使用纯HTML实现
function BasicForm() {
return (
<form action="/api/submit" method="POST">
<input name="username" required />
<input name="password" type="password" required />
<button type="submit">登录</button>
</form>
);
}
// 原则2:CSS增强样式
// 样式应该是增强,不影响基本功能
// 原则3:JavaScript增强交互
// JavaScript应该是可选的增强
function EnhancedForm() {
return (
<form action={serverAction}>
{/* HTML提供基础功能 */}
<input name="username" required />
{/* JavaScript增强交互 */}
<ValidationMessage field="username" />
<button type="submit">登录</button>
</form>
);
}
// 原则4:渐进式增强层次
/*
第1层:内容(HTML)
- 所有用户都能访问
- 搜索引擎可索引
- 无需任何技术
第2层:表现(CSS)
- 视觉增强
- 布局优化
- 响应式设计
第3层:行为(JavaScript)
- 交互增强
- 动态内容
- 用户体验优化
*/1.3 传统方式对比
jsx
// ========== 传统SPA(不支持渐进增强) ==========
function TraditionalForm() {
const [data, setData] = useState({});
const handleSubmit = async (e) => {
e.preventDefault(); // 阻止默认行为
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data)
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={data.name || ''}
onChange={e => setData({...data, name: e.target.value})}
/>
<button type="submit">提交</button>
</form>
);
}
// 问题:JavaScript禁用时完全不工作
// ========== Form Actions(支持渐进增强) ==========
'use server';
async function submitContact(formData) {
await db.contacts.create({
data: {
name: formData.get('name'),
email: formData.get('email')
}
});
redirect('/thank-you');
}
function ModernForm() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">提交</button>
</form>
);
}
// 优势:JavaScript禁用时仍然工作
// ========== 传统AJAX方式 ==========
function AjaxForm() {
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: formData
});
if (response.ok) {
alert('提交成功');
}
} catch (error) {
alert('提交失败');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="data" />
<button type="submit">提交</button>
</form>
);
}
// 问题:
// 1. 必须依赖JavaScript
// 2. 错误处理复杂
// 3. SEO不友好
// 4. 首次加载慢
// ========== React 19 Form Actions方式 ==========
'use server';
async function submitData(formData) {
const data = formData.get('data');
await db.save(data);
revalidatePath('/');
redirect('/success');
}
function React19Form() {
return (
<form action={submitData}>
<input name="data" />
<button type="submit">提交</button>
</form>
);
}
// 优势:
// 1. JavaScript可选
// 2. 自动错误处理
// 3. SEO友好
// 4. 快速首次加载1.4 渐进增强的层次
第1层(基础):HTML表单
- 所有浏览器支持
- JavaScript不需要
- 标准表单提交
- 页面刷新
第2层(增强):客户端验证
- JavaScript启用时
- 即时反馈
- 减少服务器请求
第3层(优化):异步提交
- 现代浏览器
- 无刷新体验
- 保持页面状态
第4层(高级):乐观更新
- 最佳体验
- 即时UI反馈
- 错误时回滚1.5 渐进增强的实际场景
jsx
// 场景1:搜索表单
function SearchForm() {
return (
<form action="/search" method="GET">
{/* 基础层:HTML表单,GET请求 */}
<input
name="q"
type="search"
placeholder="搜索..."
required
/>
<button type="submit">搜索</button>
{/* JavaScript增强:实时搜索建议 */}
<SearchSuggestions />
</form>
);
}
// 场景2:分页
function Pagination({ currentPage, totalPages }) {
return (
<nav>
{/* 基础层:普通链接 */}
<a href={`?page=${currentPage - 1}`}>上一页</a>
{Array.from({ length: totalPages }, (_, i) => (
<a
key={i}
href={`?page=${i + 1}`}
className={currentPage === i + 1 ? 'active' : ''}
>
{i + 1}
</a>
))}
<a href={`?page=${currentPage + 1}`}>下一页</a>
{/* JavaScript增强:无刷新分页 */}
<ClientPagination />
</nav>
);
}
// 场景3:登录表单
'use server';
async function login(formData) {
const username = formData.get('username');
const password = formData.get('password');
const user = await authenticate(username, password);
if (user) {
await createSession(user.id);
redirect('/dashboard');
} else {
redirect('/login?error=invalid');
}
}
function LoginForm() {
return (
<form action={login}>
{/* 基础层:标准表单 */}
<input
name="username"
type="text"
required
autoComplete="username"
/>
<input
name="password"
type="password"
required
autoComplete="current-password"
/>
<button type="submit">登录</button>
{/* JavaScript增强:记住密码、密码强度提示 */}
<RememberMe />
<PasswordStrength />
</form>
);
}
// 场景4:文件上传
'use server';
async function uploadFile(formData) {
const file = formData.get('file');
if (file && file.size > 0) {
const buffer = Buffer.from(await file.arrayBuffer());
await saveFile(buffer, file.name);
redirect('/files?uploaded=true');
} else {
redirect('/upload?error=nofile');
}
}
function FileUploadForm() {
return (
<form action={uploadFile}>
{/* 基础层:标准文件输入 */}
<input
name="file"
type="file"
required
accept="image/*,.pdf"
/>
<button type="submit">上传</button>
{/* JavaScript增强:拖拽上传、预览 */}
<DragDropZone />
<FilePreview />
</form>
);
}第二部分:无JavaScript支持
2.1 基础表单实现
jsx
// Server Action
'use server';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
const post = await db.posts.create({
data: { title, content }
});
// 重定向到新文章页
redirect(`/posts/${post.id}`);
}
// Server Component
async function NewPostPage() {
return (
<div>
<h1>新建文章</h1>
<form action={createPost}>
<div>
<label htmlFor="title">标题</label>
<input
id="title"
name="title"
required
minLength={3}
maxLength={200}
/>
</div>
<div>
<label htmlFor="content">内容</label>
<textarea
id="content"
name="content"
required
minLength={100}
rows={10}
/>
</div>
<button type="submit">发布</button>
</form>
</div>
);
}
// JavaScript禁用时:
// 1. 用户填写表单
// 2. 点击提交
// 3. 浏览器发送POST请求
// 4. 服务器处理
// 5. 重定向到新页面
// 6. 页面刷新显示结果2.2 复杂表单示例
jsx
// 多步骤注册表单
'use server';
import { cookies } from 'next/headers';
export async function saveStep1(formData) {
// 保存到session
const data = {
name: formData.get('name'),
email: formData.get('email')
};
cookies().set('signup-step1', JSON.stringify(data), {
maxAge: 3600, // 1小时
httpOnly: true
});
redirect('/signup/step2');
}
export async function saveStep2(formData) {
// 读取之前的数据
const step1Data = JSON.parse(cookies().get('signup-step1').value);
const password = formData.get('password');
const confirmPassword = formData.get('confirmPassword');
if (password !== confirmPassword) {
redirect('/signup/step2?error=mismatch');
}
cookies().set('signup-step2', JSON.stringify({ password }), {
maxAge: 3600,
httpOnly: true
});
redirect('/signup/step3');
}
export async function completeSignup(formData) {
// 合并所有步骤的数据
const step1 = JSON.parse(cookies().get('signup-step1').value);
const step2 = JSON.parse(cookies().get('signup-step2').value);
const interests = formData.getAll('interests');
// 创建用户
const user = await db.users.create({
data: {
...step1,
password: await hashPassword(step2.password),
interests
}
});
// 清理cookies
cookies().delete('signup-step1');
cookies().delete('signup-step2');
// 创建session并重定向
await createSession(user.id);
redirect('/welcome');
}
// Step 1 Component
async function SignupStep1() {
return (
<form action={saveStep1}>
<h2>第1步:基本信息</h2>
<div>
<label htmlFor="name">姓名</label>
<input id="name" name="name" required />
</div>
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" required />
</div>
<button type="submit">下一步</button>
</form>
);
}
// Step 2 Component
async function SignupStep2({ searchParams }) {
const error = searchParams.error;
return (
<form action={saveStep2}>
<h2>第2步:设置密码</h2>
{error === 'mismatch' && (
<div className="error">密码不匹配,请重新输入</div>
)}
<div>
<label htmlFor="password">密码</label>
<input
id="password"
name="password"
type="password"
required
minLength={8}
/>
</div>
<div>
<label htmlFor="confirmPassword">确认密码</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
required
minLength={8}
/>
</div>
<button type="submit">下一步</button>
</form>
);
}
// Step 3 Component
async function SignupStep3() {
const interests = await db.interests.findMany();
return (
<form action={completeSignup}>
<h2>第3步:选择兴趣</h2>
{interests.map(interest => (
<label key={interest.id}>
<input
type="checkbox"
name="interests"
value={interest.id}
/>
{interest.name}
</label>
))}
<button type="submit">完成注册</button>
</form>
);
}2.3 URL参数传递状态
jsx
// Server Action
'use server';
import { redirect } from 'next/navigation';
export async function submitContact(formData) {
const name = formData.get('name');
const email = formData.get('email');
try {
await db.contacts.create({
data: { name, email }
});
// 成功:通过URL参数传递
redirect('/contact?success=true');
} catch (error) {
// 失败:通过URL参数传递错误
redirect(`/contact?error=${encodeURIComponent(error.message)}`);
}
}
// Server Component
async function ContactPage({ searchParams }) {
const success = searchParams.success === 'true';
const error = searchParams.error;
return (
<div>
<h1>联系我们</h1>
{success && (
<div className="success">
提交成功!我们会尽快回复您。
</div>
)}
{error && (
<div className="error">
提交失败:{error}
</div>
)}
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">提交</button>
</form>
</div>
);
}2.4 会话状态传递
jsx
// Server Action
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function submitForm(formData) {
const result = await processForm(formData);
if (result.success) {
// 设置cookie传递成功状态
cookies().set('form-message', JSON.stringify({
type: 'success',
message: '提交成功!'
}), {
maxAge: 5 // 5秒后过期
});
} else {
// 设置cookie传递错误状态
cookies().set('form-message', JSON.stringify({
type: 'error',
message: result.error
}), {
maxAge: 5
});
}
redirect('/form');
}
// Server Component
async function FormPage() {
const messageCookie = cookies().get('form-message');
const message = messageCookie ? JSON.parse(messageCookie.value) : null;
// 读取后立即删除
if (messageCookie) {
cookies().delete('form-message');
}
return (
<div>
{message && (
<div className={message.type}>
{message.message}
</div>
)}
<form action={submitForm}>
<input name="data" />
<button type="submit">提交</button>
</form>
</div>
);
}2.5 表单数据回填
jsx
// 编辑表单示例
'use server';
import { redirect } from 'next/navigation';
export async function updatePost(formData) {
const id = formData.get('id');
const title = formData.get('title');
const content = formData.get('content');
try {
await db.posts.update({
where: { id },
data: { title, content }
});
redirect(`/posts/${id}?updated=true`);
} catch (error) {
// 失败时保留用户输入
redirect(`/posts/${id}/edit?error=${encodeURIComponent(error.message)}&title=${encodeURIComponent(title)}&content=${encodeURIComponent(content)}`);
}
}
// Server Component
async function EditPostPage({ params, searchParams }) {
const post = await db.posts.findUnique({
where: { id: params.id }
});
// 如果有错误,使用URL中的数据(用户之前的输入)
const title = searchParams.title || post.title;
const content = searchParams.content || post.content;
const error = searchParams.error;
return (
<div>
<h1>编辑文章</h1>
{error && (
<div className="error">
保存失败:{error}
</div>
)}
<form action={updatePost}>
<input type="hidden" name="id" value={post.id} />
<div>
<label htmlFor="title">标题</label>
<input
id="title"
name="title"
defaultValue={title}
required
/>
</div>
<div>
<label htmlFor="content">内容</label>
<textarea
id="content"
name="content"
defaultValue={content}
required
rows={10}
/>
</div>
<button type="submit">保存</button>
</form>
</div>
);
}第三部分:增强用户体验
3.1 JavaScript启用时的增强
jsx
'use client';
import { useActionState } from 'react';
import { submitForm } from './actions';
function EnhancedForm() {
const [state, formAction, isPending] = useActionState(
submitForm,
{ success: false }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
{/* JavaScript启用时显示的增强UI */}
{state.success && (
<div className="success">提交成功!</div>
)}
{state.error && (
<div className="error">{state.error}</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
</form>
);
}
// 行为:
// - JavaScript禁用:标准表单提交,页面刷新
// - JavaScript启用:异步提交,无刷新,即时反馈3.2 加载状态
jsx
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
<span>提交中...</span>
</>
) : (
'提交'
)}
</button>
);
}
function Form() {
return (
<form action={submitAction}>
<input name="data" />
{/* JavaScript禁用:普通按钮 */}
{/* JavaScript启用:显示加载状态 */}
<SubmitButton />
</form>
);
}
// 复杂加载状态示例
function AdvancedSubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<div className="loading-state">
<Spinner />
<span>正在处理...</span>
<span className="hint">请稍候,不要关闭页面</span>
</div>
) : (
<div className="normal-state">
<span>提交</span>
<Icon name="arrow-right" />
</div>
)}
</button>
);
}
// 多步骤表单的加载状态
function MultiStepForm() {
const { pending } = useFormStatus();
const [currentStep, setCurrentStep] = useState(1);
return (
<form action={submitAction}>
{/* 步骤指示器 */}
<StepIndicator current={currentStep} total={3} />
{/* 表单字段 */}
{currentStep === 1 && <Step1Fields />}
{currentStep === 2 && <Step2Fields />}
{currentStep === 3 && <Step3Fields />}
{/* 操作按钮 */}
<div className="actions">
{currentStep > 1 && (
<button
type="button"
onClick={() => setCurrentStep(currentStep - 1)}
disabled={pending}
>
上一步
</button>
)}
{currentStep < 3 ? (
<button
type="button"
onClick={() => setCurrentStep(currentStep + 1)}
disabled={pending}
>
下一步
</button>
) : (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '完成'}
</button>
)}
</div>
</form>
);
}3.3 客户端验证
jsx
'use client';
import { useState } from 'react';
function ValidatedForm() {
const [clientErrors, setClientErrors] = useState({});
const validateEmail = (email) => {
if (!email.includes('@')) {
return '邮箱格式不正确';
}
return null;
};
const handleBlur = (e) => {
// JavaScript启用时的实时验证
const { name, value } = e.target;
if (name === 'email') {
const error = validateEmail(value);
setClientErrors(prev => ({
...prev,
email: error
}));
}
};
return (
<form action={submitAction}>
<input
name="email"
type="email"
required
onBlur={handleBlur}
/>
{/* JavaScript启用时显示 */}
{clientErrors.email && (
<span className="error">{clientErrors.email}</span>
)}
<button type="submit">提交</button>
</form>
);
}
// 行为:
// - JavaScript禁用:HTML5验证
// - JavaScript启用:实时客户端验证
// 高级客户端验证示例
'use client';
function AdvancedValidationForm() {
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validators = {
username: (value) => {
if (!value) return '用户名不能为空';
if (value.length < 3) return '用户名至少3个字符';
if (!/^[a-zA-Z0-9_]+$/.test(value)) return '用户名只能包含字母、数字和下划线';
return null;
},
email: (value) => {
if (!value) return '邮箱不能为空';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '邮箱格式不正确';
return null;
},
password: (value) => {
if (!value) return '密码不能为空';
if (value.length < 8) return '密码至少8个字符';
if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
if (!/[a-z]/.test(value)) return '密码必须包含小写字母';
if (!/[0-9]/.test(value)) return '密码必须包含数字';
return null;
},
confirmPassword: (value, formData) => {
if (value !== formData.get('password')) {
return '两次密码输入不一致';
}
return null;
}
};
const validateField = (name, value, formData) => {
const validator = validators[name];
if (validator) {
const error = validator(value, formData);
setErrors(prev => ({
...prev,
[name]: error
}));
return error;
}
return null;
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const formData = new FormData(e.target.form);
validateField(name, e.target.value, formData);
};
const handleChange = (e) => {
if (touched[e.target.name]) {
const formData = new FormData(e.target.form);
validateField(e.target.name, e.target.value, formData);
}
};
return (
<form action={submitAction}>
<div>
<label htmlFor="username">用户名</label>
<input
id="username"
name="username"
required
onBlur={handleBlur}
onChange={handleChange}
/>
{touched.username && errors.username && (
<span className="error">{errors.username}</span>
)}
</div>
<div>
<label htmlFor="email">邮箱</label>
<input
id="email"
name="email"
type="email"
required
onBlur={handleBlur}
onChange={handleChange}
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<label htmlFor="password">密码</label>
<input
id="password"
name="password"
type="password"
required
onBlur={handleBlur}
onChange={handleChange}
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
<PasswordStrengthMeter password={touched.password} />
</div>
<div>
<label htmlFor="confirmPassword">确认密码</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
required
onBlur={handleBlur}
onChange={handleChange}
/>
{touched.confirmPassword && errors.confirmPassword && (
<span className="error">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">提交</button>
</form>
);
}
// 密码强度指示器
function PasswordStrengthMeter({ password }) {
const getStrength = (pwd) => {
if (!pwd) return 0;
let strength = 0;
if (pwd.length >= 8) strength++;
if (/[a-z]/.test(pwd)) strength++;
if (/[A-Z]/.test(pwd)) strength++;
if (/[0-9]/.test(pwd)) strength++;
if (/[^a-zA-Z0-9]/.test(pwd)) strength++;
return strength;
};
const strength = getStrength(password);
const labels = ['', '很弱', '弱', '中等', '强', '很强'];
const colors = ['', 'red', 'orange', 'yellow', 'lightgreen', 'green'];
return (
<div className="password-strength">
<div className="strength-bar">
<div
className="strength-fill"
style={{
width: `${(strength / 5) * 100}%`,
backgroundColor: colors[strength]
}}
/>
</div>
<span className="strength-label">{labels[strength]}</span>
</div>
);
}3.4 乐观更新
jsx
'use client';
import { useOptimistic } from 'react';
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const handleSubmit = async (formData) => {
// 乐观更新
const newTodo = {
id: crypto.randomUUID(),
text: formData.get('text'),
completed: false
};
addOptimisticTodo(newTodo);
// 提交到服务器
await createTodo(formData);
};
return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" required />
<button type="submit">添加</button>
</form>
</div>
);
}
// 行为:
// - JavaScript禁用:标准表单提交
// - JavaScript启用:即时显示新项,后台提交3.5 动画和过渡
jsx
'use client';
import { useTransition } from 'react';
function AnimatedForm() {
const [isPending, startTransition] = useTransition();
const handleSubmit = async (formData) => {
startTransition(async () => {
await submitForm(formData);
});
};
return (
<form action={handleSubmit}>
<input name="data" />
<button
type="submit"
className={isPending ? 'submitting' : ''}
>
{isPending ? (
<span className="fade-in">提交中...</span>
) : (
'提交'
)}
</button>
</form>
);
}
// CSS过渡
/*
.submitting {
animation: pulse 1s infinite;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
*/第四部分:服务端渲染优势
4.1 SEO友好
jsx
// Server Component - 完全在服务端渲染
async function BlogPost({ params }) {
const post = await db.posts.findUnique({
where: { id: params.id }
});
return (
<>
{/* 搜索引擎可以直接索引 */}
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* 表单支持渐进增强 */}
<CommentForm postId={post.id} />
</article>
</>
);
}
// 优势:
// - 内容立即可见
// - SEO完美支持
// - 无需JavaScript即可访问
// SEO优化的商品页面
async function ProductPage({ params }) {
const product = await db.products.findUnique({
where: { slug: params.slug },
include: {
category: true,
reviews: {
take: 5,
orderBy: { createdAt: 'desc' }
},
_count: {
select: { reviews: true }
}
}
});
// 计算平均评分
const avgRating = await db.review.aggregate({
where: { productId: product.id },
_avg: { rating: true }
});
// 结构化数据(JSON-LD)
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images[0],
sku: product.sku,
brand: {
'@type': 'Brand',
name: product.brand
},
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'CNY',
availability: product.stock > 0 ? 'InStock' : 'OutOfStock'
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: avgRating._avg.rating,
reviewCount: product._count.reviews
}
};
return (
<>
{/* SEO标签 */}
<title>{product.name} - {product.category.name}</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.images[0]} />
<meta property="og:type" content="product" />
<meta property="product:price:amount" content={product.price} />
<meta property="product:price:currency" content="CNY" />
{/* 结构化数据 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
{/* 页面内容 */}
<article>
<h1>{product.name}</h1>
<div className="price">¥{product.price}</div>
<div className="description">{product.description}</div>
{/* 加入购物车表单 - 渐进增强 */}
<AddToCartForm product={product} />
{/* 评论列表 */}
<section className="reviews">
<h2>用户评价</h2>
{product.reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</section>
{/* 添加评论表单 - 渐进增强 */}
<AddReviewForm productId={product.id} />
</article>
</>
);
}4.2 更快的首次加载
jsx
// Server Component - 数据已在服务端获取
async function ProductPage({ params }) {
// 服务端直接获取数据
const product = await db.products.findUnique({
where: { id: params.id },
include: {
reviews: {
take: 5,
orderBy: { createdAt: 'desc' }
}
}
});
return (
<div>
{/* 内容立即显示,无需等待JS */}
<h1>{product.name}</h1>
<p>¥{product.price}</p>
<div>
{product.reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
{/* 表单立即可用 */}
<AddToCartForm productId={product.id} />
</div>
);
}
// 时间对比:
// 传统SPA:HTML → JS → 数据 → 渲染
// Server Components:HTML(含数据)→ 显示
// 性能优化的仪表板
async function Dashboard() {
// 并行获取多个数据源
const [stats, recentOrders, topProducts, notifications] = await Promise.all([
db.stats.findFirst(),
db.orders.findMany({
take: 10,
orderBy: { createdAt: 'desc' }
}),
db.products.findMany({
take: 5,
orderBy: { sales: 'desc' }
}),
db.notifications.findMany({
where: { read: false },
orderBy: { createdAt: 'desc' }
})
]);
return (
<div className="dashboard">
{/* 所有数据已在服务端获取,立即显示 */}
<StatsCards stats={stats} />
<RecentOrders orders={recentOrders} />
<TopProducts products={topProducts} />
<Notifications items={notifications} />
</div>
);
}
// 优势:
// - 单次往返获取所有数据
// - 无需多次API请求
// - 内容立即可见
// - 更快的首次渲染4.3 降低客户端负担
jsx
// Server Component处理复杂逻辑
async function Dashboard() {
// 在服务端执行复杂查询和计算
const stats = await db.$queryRaw`
SELECT
COUNT(*) as total_users,
AVG(order_value) as avg_order,
SUM(revenue) as total_revenue
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.created_at > NOW() - INTERVAL '30 days'
`;
const topProducts = await db.products.findMany({
take: 10,
orderBy: { sales: 'desc' },
include: {
_count: {
select: { orders: true }
}
}
});
// 客户端收到的是已处理好的数据
return (
<div>
<StatsCards stats={stats[0]} />
<TopProductsList products={topProducts} />
</div>
);
}
// 优势:
// - 复杂计算在服务器
// - 客户端bundle更小
// - 低端设备也流畅
// 数据处理示例
async function ReportPage({ params }) {
// 在服务端处理大量数据
const rawData = await db.transactions.findMany({
where: {
date: {
gte: params.startDate,
lte: params.endDate
}
}
});
// 在服务端聚合和计算
const processedData = rawData.reduce((acc, transaction) => {
const date = transaction.date.toISOString().split('T')[0];
if (!acc[date]) {
acc[date] = {
total: 0,
count: 0,
categories: {}
};
}
acc[date].total += transaction.amount;
acc[date].count += 1;
if (!acc[date].categories[transaction.category]) {
acc[date].categories[transaction.category] = 0;
}
acc[date].categories[transaction.category] += transaction.amount;
return acc;
}, {});
// 客户端只接收处理后的数据
return (
<div>
<h1>交易报告</h1>
<ReportChart data={processedData} />
<ReportTable data={processedData} />
</div>
);
}
// 优势:
// - 大数据处理在服务器
// - 减少数据传输量
// - 客户端性能更好4.4 安全性提升
jsx
// Server Component - 敏感操作在服务端
async function AdminPanel() {
// 在服务端验证权限
const session = await getServerSession();
if (!session || !session.user.isAdmin) {
redirect('/unauthorized');
}
// 在服务端访问敏感数据
const apiKeys = await db.apiKeys.findMany({
where: { userId: session.user.id }
});
// API密钥永不暴露给客户端
return (
<div>
<h1>API管理</h1>
{apiKeys.map(key => (
<div key={key.id}>
<span>创建于:{key.createdAt}</span>
<span>最后使用:{key.lastUsed}</span>
{/* 只显示部分密钥 */}
<code>{key.key.substring(0, 8)}...</code>
</div>
))}
</div>
);
}
// 优势:
// - 敏感数据不传输到客户端
// - 权限验证在服务端
// - 减少安全风险第五部分:可访问性
5.1 语义化HTML
jsx
function AccessibleForm() {
return (
<form action={submitAction}>
{/* 使用label关联input */}
<div>
<label htmlFor="name">姓名</label>
<input
id="name"
name="name"
required
aria-required="true"
/>
</div>
{/* 使用fieldset分组 */}
<fieldset>
<legend>联系方式</legend>
<div>
<label htmlFor="email">邮箱</label>
<input
id="email"
name="email"
type="email"
required
aria-required="true"
/>
</div>
<div>
<label htmlFor="phone">电话</label>
<input
id="phone"
name="phone"
type="tel"
/>
</div>
</fieldset>
{/* 清晰的按钮文本 */}
<button type="submit">提交表单</button>
</form>
);
}5.2 错误提示可访问
jsx
'use client';
import { useActionState } from 'react';
function AccessibleErrors() {
const [state, formAction] = useActionState(submitForm, { errors: {} });
return (
<form action={formAction}>
<div>
<label htmlFor="email">邮箱</label>
<input
id="email"
name="email"
type="email"
aria-invalid={!!state.errors?.email}
aria-describedby={state.errors?.email ? 'email-error' : undefined}
/>
{state.errors?.email && (
<span id="email-error" className="error" role="alert">
{state.errors.email}
</span>
)}
</div>
<button type="submit">提交</button>
</form>
);
}5.3 键盘导航
jsx
function KeyboardFriendlyForm() {
return (
<form action={submitAction}>
{/* 自然的tab顺序 */}
<input name="field1" tabIndex={0} />
<input name="field2" tabIndex={0} />
{/* 按钮可通过Enter或Space触发 */}
<button type="submit">提交</button>
{/* 或使用链接样式按钮 */}
<button type="button" onClick={handleCancel}>
取消
</button>
</form>
);
}5.4 屏幕阅读器支持
jsx
function ScreenReaderFriendly() {
return (
<form action={submitAction}>
{/* ARIA标签提供额外信息 */}
<div role="group" aria-labelledby="personal-info">
<h2 id="personal-info">个人信息</h2>
<div>
<label htmlFor="firstName">名字</label>
<input
id="firstName"
name="firstName"
required
aria-required="true"
aria-describedby="firstName-hint"
/>
<span id="firstName-hint" className="hint">
请输入您的真实姓名
</span>
</div>
</div>
{/* 进度指示器 */}
<div role="status" aria-live="polite" aria-atomic="true">
<span className="visually-hidden">
表单已填写 60%
</span>
</div>
{/* 提交按钮 */}
<button type="submit" aria-label="提交个人信息表单">
提交
</button>
</form>
);
}注意事项
1. 确保基础功能工作
jsx
// ✅ 好:基础功能不依赖JavaScript
<form action={serverAction}>
<input name="email" required />
<button type="submit">提交</button>
</form>
// ❌ 不好:必须JavaScript才能工作
<form onSubmit={clientHandler}>
<input value={email} onChange={...} />
<button>提交</button>
</form>2. 使用标准HTML属性
jsx
// ✅ 使用HTML5验证属性
<input
name="email"
type="email"
required
minLength={5}
maxLength={100}
/>
// ✅ 使用合适的input类型
<input type="tel" />
<input type="date" />
<input type="number" />3. 提供清晰的反馈
jsx
// ✅ 通过URL或cookies提供反馈
redirect('/form?success=true');
// ✅ 使用语义化的成功页面
redirect('/thank-you');4. 避免过度依赖JavaScript
jsx
// ❌ 不好:关键功能依赖JavaScript
function BadExample() {
const [data, setData] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// 只有JavaScript启用才能提交
submitData(data);
};
return (
<form onSubmit={handleSubmit}>
<input value={data} onChange={e => setData(e.target.value)} />
<button>提交</button>
</form>
);
}
// ✅ 好:基础功能不依赖JavaScript
function GoodExample() {
return (
<form action={serverAction}>
<input name="data" />
<button type="submit">提交</button>
</form>
);
}5. 合理使用noscript标签
jsx
function FormWithFallback() {
return (
<>
{/* JavaScript启用时的增强表单 */}
<EnhancedForm />
{/* JavaScript禁用时的提示 */}
<noscript>
<div className="warning">
<p>JavaScript已禁用,部分功能可能受限。</p>
<p>但您仍然可以正常提交表单。</p>
</div>
</noscript>
</>
);
}6. 测试无JavaScript环境
bash
# Chrome DevTools禁用JavaScript
# 1. 打开DevTools (F12)
# 2. 按Cmd+Shift+P (Mac) 或 Ctrl+Shift+P (Windows/Linux)
# 3. 输入"JavaScript"
# 4. 选择"Disable JavaScript"
# Firefox禁用JavaScript
# 1. 在地址栏输入 about:config
# 2. 搜索 javascript.enabled
# 3. 切换为false7. 保持URL友好
jsx
// ✅ 好:使用有意义的URL
redirect('/posts/123/edit');
// ❌ 不好:使用状态值作为URL
redirect('/posts?id=123&action=edit&state=editing');8. 处理表单重复提交
jsx
'use server';
import { cookies } from 'next/headers';
export async function submitForm(formData) {
// 使用token防止重复提交
const token = formData.get('_token');
const storedToken = cookies().get('form-token')?.value;
if (token !== storedToken) {
redirect('/form?error=invalid_token');
}
// 处理表单
await processForm(formData);
// 删除token
cookies().delete('form-token');
redirect('/success');
}
async function FormPage() {
// 生成新token
const token = crypto.randomUUID();
cookies().set('form-token', token, { httpOnly: true });
return (
<form action={submitForm}>
<input type="hidden" name="_token" value={token} />
<input name="data" />
<button type="submit">提交</button>
</form>
);
}常见问题
Q1: 渐进增强会增加开发成本吗?
A: 使用Form Actions几乎没有额外成本,因为它天然支持渐进增强。相比传统SPA,你甚至可能减少代码量,因为不需要处理表单状态、事件处理等客户端逻辑。
Q2: 真的有人禁用JavaScript吗?
A: 虽然主动禁用JavaScript的用户很少,但考虑渐进增强还能带来其他好处:
- 更好的SEO
- 更快的首次加载
- 更好的可访问性
- 更低的客户端要求
- JavaScript加载失败时的备用方案
Q3: 如何测试JavaScript禁用情况?
A: 有几种方法:
- 浏览器开发者工具可以禁用JavaScript
- 使用隐私模式或无痕模式
- 使用Lynx等文本浏览器
- 使用自动化测试工具模拟
Q4: 渐进增强是否意味着功能降级?
A: 不是。渐进增强确保核心功能始终可用,然后在支持的环境中提供增强体验。这与优雅降级不同:
- 渐进增强:从基础开始,逐步增强
- 优雅降级:从完整功能开始,在不支持时降级
Q5: Server Actions是否总是需要服务器?
A: 是的,Server Actions在服务端执行。但这带来了:
- 更好的安全性(敏感逻辑在服务端)
- 更小的客户端bundle
- 更好的SEO
- 自动的渐进增强
Q6: 如何在渐进增强中处理复杂交互?
A: 将交互分层:
jsx
// 基础层:HTML表单
<form action={serverAction}>
<input name="search" />
<button type="submit">搜索</button>
</form>
// 增强层:自动完成(JavaScript)
'use client';
function EnhancedSearch() {
return (
<form action={serverAction}>
<input name="search" />
<SearchSuggestions /> {/* 仅JavaScript启用时 */}
<button type="submit">搜索</button>
</form>
);
}Q7: 渐进增强是否影响用户体验?
A: 恰恰相反!渐进增强提供了最佳的用户体验:
- JavaScript禁用:核心功能仍可用
- 慢速网络:内容立即显示
- 低端设备:性能更好
- 现代浏览器:获得所有增强功能
Q8: 如何权衡SEO和用户体验?
A: 使用React 19的Server Components和Form Actions,你可以同时获得:
- 完美的SEO(服务端渲染)
- 出色的用户体验(渐进增强)
- 快速的首次加载
- 现代的交互体验
总结
渐进增强的价值
✅ 更好的可访问性
✅ 更快的首次加载
✅ 更好的SEO
✅ 更强的兼容性
✅ 更低的客户端要求
✅ 更好的用户体验
✅ 更高的可靠性
✅ 更少的JavaScript依赖实现要点
1. 基础功能使用HTML
- 使用标准表单元素
- 利用HTML5验证
- 语义化标签
2. JavaScript作为增强
- 不阻塞基础功能
- 提供额外便利
- 改善用户体验
3. 服务端处理核心逻辑
- 业务逻辑在服务端
- 数据验证在服务端
- 敏感操作在服务端
4. 客户端优化交互
- 即时反馈
- 动画过渡
- 乐观更新
5. 使用语义化HTML
- 正确的标签
- ARIA属性
- 可访问性
6. 提供清晰反馈
- 成功提示
- 错误信息
- 加载状态
7. 考虑可访问性
- 键盘导航
- 屏幕阅读器
- 视觉障碍支持Form Actions优势
✅ 天然支持渐进增强
✅ JavaScript禁用时工作
✅ 代码简单统一
✅ 无需特殊处理
✅ 自动优化
✅ 更好的SEO
✅ 更快的首次加载
✅ 更小的客户端bundle
✅ 更好的安全性最佳实践
javascript
// 1. 使用Server Actions作为表单处理
'use server';
export async function submitForm(formData) {
// 处理逻辑
}
// 2. 使用HTML表单元素
<form action={submitForm}>
<input name="field" required />
<button type="submit">提交</button>
</form>
// 3. 在客户端组件中添加增强
'use client';
function EnhancedForm() {
const [state, formAction] = useActionState(submitForm, {});
// 添加客户端验证、加载状态等
}
// 4. 通过URL或cookies传递状态
redirect('/success?message=submitted');
// 5. 使用语义化HTML
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" required />关键收益
渐进增强不是额外的工作,而是一种更好的架构方式:
- 更快的开发:Form Actions简化了表单处理
- 更好的性能:服务端渲染提供即时内容
- 更好的SEO:搜索引擎可以索引全部内容
- 更好的可访问性:所有用户都能访问核心功能
- 更好的可靠性:JavaScript失败时仍可工作
- 更好的安全性:敏感逻辑在服务端执行
渐进增强让应用对所有用户都友好!