深入解析 @dub/ui:Dub 全站统一的 React 组件库设计与工程实践
【免费下载链接】dubThe modern link attribution platform. Loved by world-class marketing teams like Framer, Perplexity, Superhuman, Twilio, Buffer and more.项目地址: https://gitcode.com/GitHub_Trending/du/dub
@dub/ui是 Dub(现代链接归因平台)monorepo 中负责全站 UI 的 React 组件库,被apps/web下 admin、app、partners 等多个子应用广泛引用。本文以 packages/ui/README.md 为骨架,结合 packages/ui/package.json 与packages/ui/src下的真实源码,完整讲解它的安装方式、工程构建、组件/图标/图表三大导出面,以及 Button、Modal 等核心组件的内部实现,帮助你理解一套面向大型 SaaS 产品的企业级组件库该如何组织。
一、组件库的角色定位
README 开门见山地定义了它的职责:
@dub/uiis a library of React components that are used across Dub's web applications.
也就是说,它不是一个面向公众发布的通用 UI 框架,而是 Dub 产品线内部共享的"设计系统实现层"——所有跨应用复用的组件、图标、Hooks 与图表都被收敛到这个包中。从仓库目录结构看,它处于packages/下,与@dub/utils、@dub/tailwind-config、@dub/email等包平级,属于 pnpm workspace 的内部共享包。
在apps/web中,从管理后台到合作伙伴门户都能看到对它的引用,例如apps/web/app/(ee)/admin.dub.co/(dashboard)/components/user-info.tsx、apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/fraud/page.tsx等 460+ 处文件均直接import自@dub/ui。这意味着理解了@dub/ui的导出面,就等于掌握了 Dub 前端的"通用积木"。
二、安装与工程配置
README 给出了唯一的安装命令:
pnpm i @dub/ui作为 pnpm workspace 成员,在 monorepo 内部它通过workspace:*协议被引用,例如 packages/ui/package.json 的 devDependencies 中声明了"@dub/tailwind-config": "workspace:*"与"@dub/utils": "workspace:*"。
围绕安装使用,还有几点工程层面的关键信息:
- 版本与发布:当前版本
0.3.19,publishConfig.access为public,即该包可以发布到公共 npm registry 供外部消费。 - 包入口(exports):
main指向./dist/index.js(CJS),module指向./dist/index.mjs(ESM),types为./dist/index.d.ts,同时提供了三个子路径导出:.→ 主入口(全部组件、Hooks、图标、布局)./icons→ 图标子包./charts→ 图表子包
- 副作用标记:
"sideEffects": false,配合 tree-shaking,未使用的组件会被打包器安全移除。 - Peer 依赖:要求
next@15.5.7、react@19.1.3、react-dom@19.1.3。这是因为部分组件(如 Modal、hooks 中的useRouter)直接依赖 Next.js 路由能力,且 React 19 是运行时基线。
三、构建管线:tsup 与 "use client" 注入
构建配置位于 packages/ui/tsup.config.ts,核心要点如下:
export default defineConfig((options: Options) => ({ entry: { index: "src/index.tsx", "icons/index": "src/icons/index.tsx", "charts/index": "src/charts/index.ts", }, format: ["esm"], esbuildOptions(options) { options.banner = { js: '"use client"' }; }, dts: true, minify: true, external: ["react"], ...options, }));- 三个独立入口与
package.json的 exports 一一对应,产出index、icons/index、charts/index三份产物。 format: ["esm"]:只输出 ESM 格式,契合现代 Next.js App Router 生态。- 自动注入
"use client":通过 esbuild banner 在产物文件头部写入"use client"指令,使组件在 Next.js 中被视为客户端组件,避免消费方手动标注。这是 Dub 面向 Next.js 场景的关键设计。 dts: true生成类型声明,minify: true压缩产物,external: ["react"]将 React 留给宿主环境,避免重复打包。- 开发时可通过
pnpm dev(即tsup --watch)开启监听式增量构建,配合pnpm lint、pnpm check-types保证代码质量。
四、导出面全景:80+ 组件、Hooks、图标与布局
packages/ui/src/index.tsx 是包的总出口,其导出组织清晰反映了设计系统的模块划分:
| 类别 | 导出内容 | 代表实现 |
|---|---|---|
| 基础组件 | accordion、alert、avatar、badge、button、checkbox、input、label、slider、switch、radio-group、tooltip、popover、combobox、tab-select、toggle-group 等 | button.tsx、modal.tsx |
| 复合组件 | card-list、card-selector、carousel、date-picker、filter、form、multi-value-input、number-stepper、pagination-controls、utm-builder、smart-datetime-picker、rich-text-area、file-upload 等 | src/card-list/、src/date-picker/、src/rich-text-area/ |
| 数据展示 | table、status-badge、dub-status-badge、empty-state、truncated-list、timestamp-tooltip、mini-area-chart、link-preview、activity-ring | src/table/、src/charts/ |
| 浮层交互 | modal、sheet、popup、progress-bar、progressive-blur、scroll-container | modal.tsx、src/sheet.tsx |
| 布局 | background、footer、max-width-wrapper、nav | src/nav/ |
| Hooks | 23 个自定义 Hook(见下节) | src/hooks/index.ts |
| 图标 | 338 个图标组件(含品牌、大洲、默认域名目录) | src/icons/ |
| 图表 | time-series-chart、funnel-chart、bars、areas 等 | src/charts/ |
| 品牌 Logo | composite-logo、logo、nav-wordmark、wordmark | src/logo.tsx等 |
从依赖清单(packages/ui/package.json)还能反推出其技术栈选型:Radix UI(对话框、弹层、开关、工具提示等无头组件)、visx(图表)、TipTap(富文本编辑器)、@tanstack/react-table(表格)、embla-carousel(轮播)、cmdk(命令菜单)、sonner(Toast)、vaul(抽屉)、motion(动画)、lucide-react(通用图标)、swr(数据请求)等,是典型的"无头组件 + 业务封装"架构。
五、核心组件源码深读
5.1 Button:基于 cva 的变体体系
packages/ui/src/button.tsx 使用class-variance-authority(cva)定义了一套语义化变体:
export const buttonVariants = cva("transition-all", { variants: { variant: { primary: "border-black bg-black dark:bg-white dark:border-white text-content-inverted hover:bg-inverted hover:ring-4 hover:ring-border-subtle", secondary: "border-border-subtle bg-bg-default text-content-emphasis hover:bg-bg-muted focus-visible:border-border-emphasis outline-none", outline: "border-transparent text-content-default hover:bg-neutral-900/5", success: "border-blue-500 bg-blue-500 text-white hover:bg-blue-600 hover:ring-4 hover:ring-blue-100", danger: "border-red-500 bg-red-500 text-white hover:bg-red-600 hover:ring-4 hover:ring-red-100", "danger-outline": "border-transparent bg-white text-red-500 hover:bg-red-600 hover:text-white", }, }, defaultVariants: { variant: "primary" }, });颜色统一使用bg-bg-default、text-content-emphasis、border-border-subtle这类设计令牌(而非写死的色值),说明样式主题由 packages/tailwind-config/themes.css 统一驱动。
Button的 API 设计也相当克制而完整:
text:按钮文本,支持ReactNode;loading:加载态,自动渲染LoadingSpinner并禁用按钮;icon/right:前后插槽;shortcut:渲染<kbd>快捷键提示,且在不同 variant 下自动适配配色;disabledTooltip:禁用原因提示——当传入该属性时,按钮整体被包在Tooltip中,展示不可用原因,这是产品后台常见的"权限不足"交互;type推断:源码注释明确"if onClick is passed, it's a 'button' type, otherwise it's being used in a form, hence 'submit'"——有onClick时自动为type="button",否则默认为表单提交按钮,兼顾了表单场景的便利性。
5.2 Modal:移动端 Drawer 与桌面端 Dialog 的响应式切换
packages/ui/src/modal.tsx 展示了 Dub 在"一套代码适配多端"上的取舍,核心逻辑是:
const { isMobile } = useMediaQuery(); if (isMobile && !desktopOnly) { return <Drawer.Root ...>; // 移动端:vaul 底部抽屉 } return <Dialog.Root ...>; // 桌面端:Radix Dialog 居中弹窗- 移动端使用
vaul的Drawer,带拖拽关闭、顶部"小岛"手柄(DrawerIsland),底部圆角、毛玻璃遮罩; - 桌面端使用
@radix-ui/react-dialog,居中卡片、backdrop-blur-md遮罩、animate-fade-in/animate-scale-in动效; desktopOnly属性可强制桌面弹窗形态;- 关闭策略:优先调用
onClose回调;若提供了setShowModal则走受控关闭;否则视为拦截路由@modal的平行路由场景,直接router.back()返回上一页; - 防误关细节:监听
onPointerDownOutside,当点击目标是[data-sonner-toast](sonner Toast)时阻止关闭,避免用户点 Toast 时误关弹窗; - 无障碍:使用
VisuallyHidden.Root注入Dialog.Title/Dialog.Description,保证屏幕阅读器可读且不破坏视觉布局。
这种"移动端抽屉、桌面端弹窗"的模式,与 Dub 链接管理、邀请弹窗等高频交互场景直接对应,是移动端体验打磨的典型实现。
六、Hooks 工具集
src/hooks/index.ts集中导出了 23 个自定义 Hook,几乎覆盖了 SaaS 后台的全部交互诉求:
- 设备与视口:
use-media-query(Modal 依赖它判断移动端)、use-resize-observer、use-in-viewport、use-intersection-observer; - 路由与导航:
use-router-stuff、use-current-anchor、use-current-product、use-current-subdomain; - 数据交互:
use-copy-to-clipboard、use-pagination、use-optimistic-update、use-toast-with-undo、use-cookies、use-local-storage; - 键盘与输入:
use-keyboard-shortcut、use-enter-submit、use-input-focused; - 工程辅助:
use-column-visibility(表格列显隐)、use-remove-ga-params、use-click-handlers、use-latest-callback、use-scroll、use-scroll-progress。
这些 Hooks 与组件形成互补:组件负责"长得怎样",Hooks 负责"行为如何"。例如use-toast-with-undo提供"操作成功 + 撤销"的反馈模式,这在链接删除、标签管理等场景中尤为重要。
七、图表子系统:@dub/ui/charts
./charts是独立的子导出入口,源码位于 packages/ui/src/charts(入口 index.ts),基于 visx 构建,提供:
time-series-chart:时间序列折线/面积图,对应 Dub 仪表盘的访问趋势;funnel-chart:漏斗图,用于转化链路(点击 → 访客 → 销售)可视化;bars/areas:柱状图与面积图基元;x-axis/y-axis:坐标轴组件;- 配套
chart-context、tooltip-sync、use-tooltip、utils、types:负责跨图表联动(tooltip 同步)与类型约束。
Dub 的链接分析、销售归因报表都可以在apps/web/ui/analytics/下找到这些图表组件的消费场景。
八、图标系统:品牌图标与通用图标
src/icons/目录下共有 338 个图标文件,除lucide-react提供的通用图标外,@dub/ui/icons还内置了大量业务图标:
- AI/模型品牌:anthropic、claude、chatgpt-icon、grok、cursor 等;
- 流量平台:google、facebook、linkedin、instagram、github、bing 等;
- Dub 产品矩阵:dub-analytics、dub-api、dub-links、dub-partners、dub-product-icon、dub-crafted-shield;
- 业务专用:continents(大洲)、default-domains(默认域名)、ios-app-store、crown-small、expanding-arrow、file-pen、file-send 等。
这些图标以组件形式导出(如LoadingSpinner直接被 Button 使用),保证了尺寸、颜色与当前主题令牌的一致性。
九、样式与主题:Tailwind Preset 复用
packages/ui/tailwind.config.ts 明确指出其作用:
// tailwind config is required for editor support import sharedConfig from "@dub/tailwind-config/tailwind.config.ts"; const config: Pick<Config, "presets"> = { presets: [sharedConfig] };组件库自身并不重复定义设计令牌,而是通过presets继承 packages/tailwind-config/tailwind.config.ts 与 packages/tailwind-config/themes.css。因此所有bg-bg-default、text-content-emphasis类语义色都由全局主题统一解析,组件消费方只需在自己的 Tailwind 配置中加入同一 preset,即可获得完全一致的视觉结果。这也是上文 Button、Modal 中大量语义化 class 能跨应用生效的根本原因。
十、在 Dub 应用中的落地方式
实际消费端apps/web的用法非常直接,例如在 admin 后台组件中:
import { Button, Modal, Tooltip, useToastWithUndo } from "@dub/ui"; import { LoadingSpinner } from "@dub/ui/icons";- 组件与 Hooks 从主入口导入;
- 品牌图标走
@dub/ui/icons子路径; - 图表从
@dub/ui/charts导入; - 消费方只需保证
next/react/react-dom版本满足 peer 依赖,且自身 Tailwind 配置引用了同一份@dub/tailwind-configpreset。
结语
@dub/ui虽然 README 只有寥寥数行,其工程内涵却相当完整:ESM-only 的 tsup 构建、自动注入"use client"、三个精细的导出入口、cva 变体体系、Radix + vaul 的多端适配弹窗、23 个业务 Hooks、基于 visx 的图表子系统和 338 个图标。对于想要自建前端设计系统的团队,它提供了一个"组件 + Hooks + 图标 + 图表 + 主题令牌"五层结构的可参考范本;对于 Dub 的二次开发与贡献者,它则是理解 Dub 全站 UI 的入口。
【免费下载链接】dubThe modern link attribution platform. Loved by world-class marketing teams like Framer, Perplexity, Superhuman, Twilio, Buffer and more.项目地址: https://gitcode.com/GitHub_Trending/du/dub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考