"这个表单样式改不动啊!"周二下午,小王抓着头发对我说。我走过去一看,原来他正在尝试修改 Ant Design 的 Form 组件样式,想让它符合新的设计规范。这已经是本周第三次遇到类似的问题了。

作为一个运行了两年的后台管理系统,我们一直在使用 Ant Design。但随着产品的不断发展,定制化需求越来越多,覆盖第三方组件库的样式变得越来越困难。一次技术分享会上,我了解到了 shadcn/ui 这个解决方案,它的理念让我眼前一亮。经过团队讨论,我们决定尝试将部分模块从 Ant Design 迁移到 shadcn/ui。

迁移准备

首先,我们需要评估迁移的成本和风险。我列了一个清单:

  • 需要迁移的组件数量:约 30 个
  • 受影响的业务模块:用户管理、订单管理
  • 预计工作量:2-3 周
  • 潜在风险:样式不一致、功能缺失

就像装修房子要先搭脚手架,我们也需要先做好准备工作:

// 创建迁移配置文件
// migration.config.ts
interface MigrationConfig {
  // 需要迁移的路由
  routes: string[]
  // 组件映射关系
  componentMap: Record<string, string>
  // 特性对比
  featureMap: Record<
    string,
    {
      antd: string[]
      shadcn: string[]
      missing: string[]
    }
  >
}

const migrationConfig: MigrationConfig = {
  routes: ['/users', '/orders'],
  componentMap: {
    'antd/lib/button': '@/components/ui/button',
    'antd/lib/input': '@/components/ui/input',
    'antd/lib/select': '@/components/ui/select'
  },
  featureMap: {
    Button: {
      antd: ['loading', 'ghost', 'danger'],
      shadcn: ['loading', 'variant', 'size'],
      missing: ['ghost']
    }
  }
}

渐进式迁移

为了降低风险,我们采用了渐进式迁移策略。首先从最简单的按钮组件开始:

// 迁移前的 Ant Design 按钮
import { Button } from 'antd'

function UserActions({ user }) {
  return (
    <div className='actions'>
      <Button type='primary' onClick={() => handleEdit(user)}>
        编辑
      </Button>
      <Button danger onClick={() => handleDelete(user)}>
        删除
      </Button>
    </div>
  )
}

// 迁移后的 shadcn/ui 按钮
import { Button } from '@/components/ui/button'

function UserActions({ user }) {
  return (
    <div className='flex gap-2'>
      <Button variant='default' onClick={() => handleEdit(user)}>
        编辑
      </Button>
      <Button variant='destructive' onClick={() => handleDelete(user)}>
        删除
      </Button>
    </div>
  )
}

然后是表单组件,这是最具挑战性的部分:

// 迁移前的 Ant Design 表单
import { Form, Input, Select } from 'antd'

function UserForm({ initialValues, onSubmit }) {
  const [form] = Form.useForm()

  return (
    <Form form={form} initialValues={initialValues} onFinish={onSubmit} labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
      <Form.Item label='用户名' name='username' rules={[{ required: true }]}>
        <Input />
      </Form.Item>
      <Form.Item label='角色' name='role'>
        <Select>
          <Select.Option value='admin'>管理员</Select.Option>
          <Select.Option value='user'>普通用户</Select.Option>
        </Select>
      </Form.Item>
    </Form>
  )
}

// 迁移后的 shadcn/ui 表单
import { useForm } from 'react-hook-form'
import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'

function UserForm({ defaultValues, onSubmit }) {
  const form = useForm({
    defaultValues,
    resolver: zodResolver(userSchema)
  })

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
        <FormField
          control={form.control}
          name='username'
          render={({ field }) => (
            <FormItem>
              <FormLabel>用户名</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name='role'
          render={({ field }) => (
            <FormItem>
              <FormLabel>角色</FormLabel>
              <Select onValueChange={field.onChange} defaultValue={field.value}>
                <SelectTrigger>
                  <SelectValue placeholder='选择角色' />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value='admin'>管理员</SelectItem>
                  <SelectItem value='user'>普通用户</SelectItem>
                </SelectContent>
              </Select>
            </FormItem>
          )}
        />
      </form>
    </Form>
  )
}

样式迁移

样式迁移是个大工程,我们采用了"主题变量映射"的方式:

// styles/theme-mapping.ts
const antdToShadcnMapping = {
  // 颜色映射
  '@primary-color': 'hsl(var(--primary))',
  '@success-color': 'hsl(var(--success))',
  '@warning-color': 'hsl(var(--warning))',
  '@error-color': 'hsl(var(--destructive))',

  // 字体映射
  '@font-size-base': '14px',
  '@font-size-lg': '16px',
  '@font-size-sm': '12px',

  // 圆角映射
  '@border-radius-base': 'var(--radius)',
  '@border-radius-sm': 'calc(var(--radius) - 2px)'
}

// 生成 CSS 变量
function generateCSSVariables() {
  return Object.entries(antdToShadcnMapping)
    .map(([antd, shadcn]) => {
      const name = antd.replace('@', '--')
      return `${name}: ${shadcn};`
    })
    .join('\n')
}

遇到的挑战

迁移过程中我们遇到了不少挑战:

  1. Form 组件的心智模型完全不同
  2. 一些 Ant Design 的特性在 shadcn/ui 中没有对应实现
  3. 团队需要适应新的开发方式

但这些挑战也带来了意外收获:

  1. 代码更容易维护了
  2. 打包体积减小了
  3. 定制化变得更简单了
  4. 团队对组件原理理解更深了

效果验证

迁移完成后,我们对比了一些关键指标:

  • 打包体积:从 2.8MB 减少到 1.2MB
  • 首屏加载时间:从 2.1s 减少到 1.3s
  • 样式覆盖代码:减少了 80%
  • 开发效率:提升了约 30%

最让我印象深刻的是小王的反馈:"现在改样式太舒服了,再也不用和 Ant Design 的样式战斗了!"

经验总结

这次迁移让我们学到了很多:

  1. 渐进式迁移比全量替换更可行
  2. 组件库的选择要考虑长期维护成本
  3. 看似简单的改变可能带来意想不到的好处
  4. 团队的技术成长比迁移本身更重要

就像装修房子,有时候推倒重来比反复修补更划算。但推倒重来的过程要有计划,分步进行。

写在最后

组件库迁移不仅仅是技术栈的更新,更是一次团队能力的提升。正如那句话说的:"工具是死的,人是活的。"选择合适的工具很重要,但更重要的是团队能够驾驭这些工具。

有什么问题欢迎在评论区讨论,让我们一起探讨组件库迁移的经验!

如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~