mini-form-小程序表单开发.md 16 KB


description: "小程序表单开发指令"

小程序表单开发规范 (Tailwind CSS版)

概述

本规范定义了基于Taro框架的小程序表单开发标准,采用react-hook-form进行状态管理,zod进行表单验证,Tailwind CSS v4进行样式设计。

技术栈

  • Taro 4 - 跨端小程序框架
  • React 18 - 前端框架
  • React Hook Form 7 - 表单状态管理
  • Zod 4 - 模式验证
  • @hookform/resolvers - 验证器集成
  • Tailwind CSS v4 - 原子化CSS框架

目录结构

推荐结构(大型/复用表单)

mini/
├── src/
│   ├── components/
│   │   └── ui/
│   │       ├── form.tsx        # 表单核心组件
│   │       ├── input.tsx       # 输入框组件
│   │       ├── label.tsx       # 标签组件
│   │       └── button.tsx      # 按钮组件
│   ├── utils/
│   │   ├── cn.ts               # 类名合并工具
│   │   └── validators.ts       # 验证规则(可选)
│   └── schemas/
│       └── user.schema.ts      # 表单验证模式(可选)

简化结构(小型/单次使用表单)

mini/
├── src/
│   ├── components/
│   │   └── ui/
│   │       ├── form.tsx        # 表单核心组件
│   │       ├── input.tsx       # 输入框组件
│   │       └── button.tsx      # 按钮组件
└── src/pages/
    └── your-page/
        └── index.tsx           # 验证规则直接定义在页面中

核心组件

1. 表单组件系统

1.1 Form 组件

// mini/src/components/ui/form.tsx
import { createContext, useContext, forwardRef } from 'react'
import { useFormContext } from 'react-hook-form'
import { cn } from '@/utils/cn'

const Form = forwardRef<
  React.ElementRef<typeof TaroForm>,
  React.ComponentPropsWithoutRef<typeof TaroForm>
>(({ className, ...props }, ref) => {
  return (
    <TaroForm
      ref={ref}
      className={cn('space-y-6', className)}
      {...props}
    />
  )
})
Form.displayName = 'Form'

1.2 FormField 组件

const FormField = forwardRef<
  React.ElementRef<typeof Controller>,
  React.ComponentPropsWithoutRef<typeof Controller>
>(({ render, ...props }, ref) => {
  return (
    <Controller
      ref={ref}
      render={({ field, fieldState, formState }) => (
        <FormItemContext.Provider value={{ name: props.name, fieldState, formState }}>
          {render({ field, fieldState, formState })}
        </FormItemContext.Provider>
      )}
      {...props}
    />
  )
})

1.3 FormItem 组件布局

const FormItem = forwardRef<
  React.ElementRef<typeof View>,
  React.ComponentPropsWithoutRef<typeof View>
>(({ className, ...props }, ref) => {
  const id = useId()
  
  return (
    <FormItemContext.Provider value={{ id }}>
      <View
        ref={ref}
        className={cn('space-y-2', className)}
        {...props}
      />
    </FormItemContext.Provider>
  )
})

1.4 FormLabel 组件

const FormLabel = forwardRef<
  React.ElementRef<typeof Text>,
  React.ComponentPropsWithoutRef<typeof Text>
>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField()
  
  return (
    <Label
      ref={ref}
      className={cn(
        error && 'text-destructive',
        className
      )}
      htmlFor={formItemId}
      {...props}
    />
  )
})

1.5 FormControl 组件

const FormControl = forwardRef<
  React.ElementRef<typeof View>,
  React.ComponentPropsWithoutRef<typeof View>
>(({ ...props }, ref) => {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
  
  return (
    <View
      ref={ref}
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  )
})

1.6 FormMessage 组件

const FormMessage = forwardRef<
  React.ElementRef<typeof Text>,
  React.ComponentPropsWithoutRef<typeof Text>
>(({ className, children, ...props }, ref) => {
  const { error, formMessageId } = useFormField()
  const body = error ? String(error?.message) : children

  if (!body) {
    return null
  }

  return (
    <Text
      ref={ref}
      id={formMessageId}
      className={cn('text-sm font-medium text-destructive', className)}
      {...props}
    >
      {body}
    </Text>
  )
})

2. 表单验证集成

2.1 验证模式定义

方式1:大型/复用表单(推荐结构)
// mini/src/schemas/user.schema.ts
import { z } from 'zod'

export const userSchema = z.object({
  username: z.string()
    .min(2, '用户名至少2个字符')
    .max(20, '用户名最多20个字符')
    .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
  
  phone: z.string()
    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
  
  email: z.string()
    .email('请输入正确的邮箱地址'),
  
  password: z.string()
    .min(6, '密码至少6个字符')
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/, '密码必须包含大小写字母和数字'),
  
  confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
  message: '两次输入的密码不一致',
  path: ['confirmPassword']
})

export type UserFormData = z.infer<typeof userSchema>
方式2:小型/单次使用表单(简化结构)
// 直接在页面文件中定义
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'

const loginSchema = z.object({
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
  password: z.string().min(1, '请输入密码')
})

type LoginFormData = z.infer<typeof loginSchema>

// 使用方式
const form = useForm<LoginFormData>({
  resolver: zodResolver(loginSchema),
  defaultValues: {
    phone: '',
    password: ''
  }
})

2.2 验证器配置(可选)

// 对于复用验证规则,可以创建 utils/validators.ts
// 小型表单可直接在页面中定义,无需单独文件

// 常用验证规则(可选)
export const phoneSchema = z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码')
export const emailSchema = z.string().email('请输入正确的邮箱地址')

3. 表单组件使用示例

3.1 完整表单示例

// mini/src/pages/register/index.tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { userSchema, UserFormData } from '@/schemas/user.schema'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

export default function RegisterPage() {
  const form = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
    defaultValues: {
      username: '',
      phone: '',
      email: '',
      password: '',
      confirmPassword: ''
    }
  })

  const onSubmit = async (data: UserFormData) => {
    try {
      // 提交表单逻辑
      console.log('表单数据:', data)
    } catch (error) {
      console.error('提交失败:', error)
    }
  }

  return (
    <View className="min-h-screen bg-gray-50 p-4">
      <View className="max-w-md mx-auto">
        <Text className="text-2xl font-bold text-center mb-6">用户注册</Text>
        
        <Form {...form}>
          <TaroForm onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            <FormField
              control={form.control}
              name="username"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>用户名</FormLabel>
                  <FormControl>
                    <Input 
                      placeholder="请输入用户名" 
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="phone"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>手机号</FormLabel>
                  <FormControl>
                    <Input 
                      type="tel"
                      placeholder="请输入手机号" 
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>邮箱</FormLabel>
                  <FormControl>
                    <Input 
                      type="email"
                      placeholder="请输入邮箱" 
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>密码</FormLabel>
                  <FormControl>
                    <Input 
                      type="password"
                      placeholder="请输入密码" 
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="confirmPassword"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>确认密码</FormLabel>
                  <FormControl>
                    <Input 
                      type="password"
                      placeholder="请再次输入密码" 
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <Button 
              type="submit" 
              className="w-full h-10 bg-blue-500 text-white"
              loading={form.formState.isSubmitting}
            >
              注册
            </Button>
          </TaroForm>
        </Form>
      </View>
    </View>
  )
}

3.2 登录表单示例

// mini/src/pages/login/index.tsx
const loginSchema = z.object({
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
  password: z.string().min(1, '请输入密码')
})

export default function LoginPage() {
  const form = useForm({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      phone: '',
      password: ''
    }
  })

  return (
    <View className="min-h-screen bg-gray-50 p-4">
      <View className="max-w-md mx-auto">
        <Text className="text-2xl font-bold text-center mb-6">用户登录</Text>
        
        <Form {...form}>
          <TaroForm onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            <FormField
              control={form.control}
              name="phone"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>手机号</FormLabel>
                  <FormControl>
                    <Input 
                      type="tel"
                      placeholder="请输入手机号"
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>密码</FormLabel>
                  <FormControl>
                    <Input 
                      type="password"
                      placeholder="请输入密码"
                      {...field}
                      className="h-10"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <Button type="submit" className="w-full h-10 bg-blue-500 text-white">
              登录
            </Button>
          </TaroForm>
        </Form>
      </View>
    </View>
  )
}

4. 表单验证最佳实践

4.1 实时验证

const form = useForm({
  resolver: zodResolver(userSchema),
  mode: 'onChange', // 实时验证
  reValidateMode: 'onChange',
  defaultValues: {
    username: '',
    phone: ''
  }
})

4.2 异步验证

const asyncUsernameSchema = z.object({
  username: z.string()
    .min(2, '用户名至少2个字符')
    .max(20, '用户名最多20个字符')
    .refine(async (username) => {
      const response = await checkUsernameAvailability(username)
      return response.available
    }, '用户名已被占用')
})

4.3 错误处理

const onSubmit = async (data: UserFormData) => {
  try {
    await registerUser(data)
    Taro.showToast({ title: '注册成功' })
    Taro.navigateBack()
  } catch (error) {
    if (error.response?.data?.errors) {
      Object.keys(error.response.data.errors).forEach(field => {
        form.setError(field as any, {
          message: error.response.data.errors[field][0]
        })
      })
    }
  }
}

5. 样式规范

5.1 表单布局

// 标准间距
const formSpacing = {
  item: 'space-y-2',      // 表单项内部间距
  section: 'space-y-6',   // 表单区域间距
  group: 'space-y-4'      // 表单组间距
}

// 输入框样式
const inputStyles = {
  base: 'h-10 px-3 bg-white border border-gray-300 rounded-md',
  focus: 'focus:border-blue-500 focus:ring-1 focus:ring-blue-500',
  error: 'border-red-500 focus:border-red-500 focus:ring-red-500'
}

5.2 响应式设计

<View className="max-w-md mx-auto p-4 sm:p-6 md:p-8">
  <Form className="space-y-4 sm:space-y-6">
    <FormItem className="grid grid-cols-1 sm:grid-cols-2 gap-4">
      <FormControl>
        <Input className="w-full" />
      </FormControl>
    </FormItem>
  </Form>
</View>

6. 性能优化

6.1 表单防抖

import { debounce } from 'lodash'

const debouncedValidate = debounce((value) => {
  form.trigger('username')
}, 300)

const handleUsernameChange = (value: string) => {
  form.setValue('username', value)
  debouncedValidate(value)
}

6.2 条件渲染

// 避免不必要的重渲染
const FormFieldMemo = React.memo(({ name, control, render }) => (
  <FormField
    name={name}
    control={control}
    render={render}
  />
))

7. 无障碍支持

7.1 语义化标签

<FormItem>
  <FormLabel>
    <Text className="sr-only">用户名</Text>
  </FormLabel>
  <FormControl>
    <Input 
      aria-label="用户名"
      aria-required="true"
      aria-invalid={!!form.formState.errors.username}
    />
  </FormControl>
</FormItem>

7.2 键盘导航

// 支持Tab键导航
const handleKeyPress = (e) => {
  if (e.key === 'Enter') {
    form.handleSubmit(onSubmit)()
  }
}

注意事项

  1. 类型安全:始终使用TypeScript定义表单数据类型
  2. 验证时机:根据业务需求选择合适的验证时机(onChange/onBlur/onSubmit)
  3. 错误处理:提供清晰的用户友好的错误消息
  4. 性能考虑:避免在大型表单中使用实时验证
  5. 无障碍:确保表单对所有用户可访问
  6. 移动端适配:测试在小屏幕设备上的显示效果
  7. 状态管理:合理使用React Hook Form的API管理复杂表单状态