Skip to content

渐进增强(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. 切换为false

7. 保持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: 有几种方法:

  1. 浏览器开发者工具可以禁用JavaScript
  2. 使用隐私模式或无痕模式
  3. 使用Lynx等文本浏览器
  4. 使用自动化测试工具模拟

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 />

关键收益

渐进增强不是额外的工作,而是一种更好的架构方式:

  1. 更快的开发:Form Actions简化了表单处理
  2. 更好的性能:服务端渲染提供即时内容
  3. 更好的SEO:搜索引擎可以索引全部内容
  4. 更好的可访问性:所有用户都能访问核心功能
  5. 更好的可靠性:JavaScript失败时仍可工作
  6. 更好的安全性:敏感逻辑在服务端执行

渐进增强让应用对所有用户都友好!