挑战全网组件库的shadcn/ui向我们证明了复制粘贴才是最佳实践

2,809 阅读4分钟

前言

shadcn/ui在过去的一段时间中在nextjs生态中获得非常大的关注,它以独特的方式解决了我们之前开发中的许多痛点

传统组件库

以下仅是我个人对传统组件库例如antd的观点:

首先他们具有以下特点

  1. 将所有代码进行打包
  2. 开箱即用,容易上手
  3. 关注点分离

于是你只需这样就可以使用

 <Button type="dashed">Dashed Button</Button>

而对比shadcn/ui中,你还需要将button.tsx文件单独通过cli或是复制粘贴的方式下载到本地例如:

// button.tsx
import * as React from 'react';

import { Slot } from '@radix-ui/react-slot';
import { cn, withRef } from '@udecode/cn';
import { type VariantProps, cva } from 'class-variance-authority';

export const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    defaultVariants: {
      size: 'default',
      variant: 'default',
    },
    variants: {
      isMenu: {
        true: 'h-auto w-full cursor-pointer justify-start',
      },
      size: {
        default: 'h-10 px-4 py-2',
        icon: 'size-10',
        lg: 'h-11 rounded-md px-8',
        none: '',
        sm: 'h-9 rounded-md px-3',
        sms: 'size-9 rounded-md px-0',
        xs: 'h-8 rounded-md px-3',
      },
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive:
          'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        inlineLink: 'text-base text-primary underline underline-offset-4',
        link: 'text-primary underline-offset-4 hover:underline',
        outline:
          'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary:
          'bg-secondary text-secondary-foreground hover:bg-secondary/80',
      },
    },
  }
);

export const Button = withRef<
  'button',
  {
    asChild?: boolean;
  } & VariantProps<typeof buttonVariants>
>(({ asChild = false, className, isMenu, size, variant, ...props }, ref) => {
  const Comp = asChild ? Slot : 'button';

  return (
    <Comp
      className={cn(buttonVariants({ className, isMenu, size, variant }))}
      ref={ref}
      {...props}
    />
  );
});

乍一看shadcn复制粘贴的做法似乎既麻烦又看不到什么好处.

先别着急让我们慢慢进行分析:

在传统的组件库中,我们常常会认为将组件代码进行打包发布至npm之上是最好的方式,这样能保证稳定迭代风格统一

我猜还有一个重要的点是可以强制用户去贡献代码,修复一些奇奇怪怪的case

例如有下面的场景(公司真实遇到的)

优点

在我的前公司中沉淀了一套组件库,以及工具集

有一天,部门A的同学发现某组件有了一个很奇怪的边缘问题

于是部门A的同学开始着手去修复这个bug,当他修复完这个bug之后

部门B的同学同样也享受到了这个修复,这样能显著的提高工作效率

问题

但这样又出现了另外一个问题,就是

我想去自定义样式就非常麻烦

一般我们会采取下面的方式,但是我相信在座的各位都被下面的方式整出来奇奇怪怪的bug:

  • !important
  • :deep样式穿透

解决方案

在我前公司中的解决方案是,联合整个设计部门,对整个组件库进行重新设计

fork整个组件库对其每一个组件进行样式上的修改,这也确实是一种解决方案。

但并不是每一个公司的老板都会看重这样的事情,很多时候老板可能不会在意这些东西,不会给你资源去做这样的事情

该解决方案也会有缺点(都是我工作中遇到的):

  1. 无法享受开源世界中的版本迭代(需要处理冲突)
  2. 组件库迭代周期可能慢于产品迭代
  3. 没人给你提pr

其中最重要的是第二条,假设ui调整了一点组件库的样式,非常简单可能只是修改一下padding

于是你要回到组件库中发起一个pr

然而尴尬的是第二天就要上线了,你的pr还在review

我的做法的将组件复制粘贴到本地

然后这样就丧失了上面的优点,其他部门进行了bug修复我浑然不知

shadcn/ui pattern

现在再让我们回到上面提到的button.tsx文件

不被看好的复制粘贴往往才是最适合我们平时开发的

你直接拿到了所有的样式代码,而巧妙的是功能相关的代码却被打包了

这样你能随意修改样式的同时,还能享受开源世界的最新bug修复

也不用fork整个项目到本地,

甚至搭配monorepos使用单独的packages/ui的工作区对组件库进行单独开发/打包。

关于monorepos这块如何设计 后面会更新另一篇文章.