从零开始打造高复用性React组件库的实战经验

___子睿 框架 阅读 879
赞 30 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

去年下半年接手一个内部运营后台,目标是把原来散落在七八个 Vue 2 单页里、逻辑重复又难维护的「活动配置模块」抽出来,做成一套可复用的组件库。一开始团队还讨论过要不要上 Web Components,甚至有人提了 Stencil,但我翻了翻文档,再看了下我们 CI/CD 流程和老 IE11 兼容要求(别问,问就是政企客户),最后还是定了 Vue 3 + Composition API + <script setup> 的组合。

从零开始打造高复用性React组件库的实战经验

不是最潮,但够稳——至少上线前不用花三天配 rollup 插件。而且说实话,我们团队对 Vue 的熟悉度远高于 Svelte 或 Qwik,真搞出问题,我还能半夜爬起来 debug。

最大的坑:性能问题

第一版做完,本地跑得飞快。结果一丢到测试环境,打开带 20+ 表单字段的活动页,Chrome DevTools 直接报「Layout Thrashing」,滚动卡顿明显,尤其在 iPad 上,input 失焦后键盘收起那一秒,整个页面像被按了慢放键。

查了半天,发现是组件里用了太多响应式依赖:每个字段都绑了 v-model,每个字段的校验规则、错误提示、禁用状态、联动显示逻辑全塞在一个 computed 里做判断。更糟的是,有个「动态表单项」组件,每次添加新字段就 push 进一个对象数组,而这个数组又被 v-for 驱动渲染,每 push 一次,所有字段的 watchEffect 全部重跑一遍。

亲测有效:把 v-for 换成 :key="item.id" 并不能解决问题;v-memo 在 Vue 3.2+ 支持,但我们锁死在 3.1.5(公司基础包版本)。折腾了半天发现,根本不是 key 的问题,是响应式系统被过度触发了。

最终的解决方案

砍掉所有「为复用而复用」的设计,回归务实。

核心改法就两条:

  • 把表单字段的校验逻辑从响应式计算中剥离,改为「显式触发」:用户 blur 或点击提交时才调用 validateField(id),而不是每个字段值变化都实时算一遍
  • 对动态表单项做「浅层响应式隔离」:用 shallowRef 包裹整个字段数组,只监听数组 length 变化,字段内部属性不再响应式追踪,改用 toRaw + 手动 triggerRef 控制更新时机

代码其实就这几行关键改动:

// 表单字段列表(原来是 ref([]))
const fields = shallowRef([
  { id: 'title', value: '', type: 'text' },
  { id: 'desc', value: '', type: 'textarea' }
])

// 添加字段时,不直接 push,而是生成新数组并手动触发更新
const addField = () => {
  const newFields = [...toRaw(fields.value), { id: field-${Date.now()}, value: '', type: 'text' }]
  fields.value = newFields
  triggerRef(fields) // 显式通知视图更新
}

// 字段值修改走普通对象操作,不走响应式 set
const updateFieldValue = (id, value) => {
  const field = toRaw(fields.value).find(f => f.id === id)
  if (field) field.value = value
}

另外,把所有 v-model 换成 :model-value + @update:model-value,彻底掌控更新节奏。虽然写起来多几行,但性能提升肉眼可见——iPad 上滚动帧率从 12fps 拉到了 58fps。

样式隔离没想透,最后靠 CSS Modules 硬扛

原计划用 <style scoped>:deep() 解决组件样式穿透,结果遇到两个现实问题:

  • 第三方 UI 库(比如 Element Plus 的 el-input)内部用了 !important:deep(.el-input__inner) 压不住
  • 某些业务方要覆盖组件默认色,但又不想全局污染,<style scoped> 下无法用 :global() 注入外部变量

最后妥协方案:所有组件级样式统一用 CSS Modules,文件名改成 FormInput.module.css,然后在 script 里 import styles from './FormInput.module.css',绑定 :class="styles.input"。好处是类名自动 hash,完全隔离;坏处是没法用 @apply(我们没上 Tailwind),也不支持嵌套写法。但胜在简单、稳定、不踩坑。

顺手把主题色抽成 CSS 变量,暴露给父组件通过 :style="{ '--primary-color': themeColor }" 注入,虽然不算优雅,但业务方能快速换肤,验收时没被揪着问。

API 设计上的小遗憾

组件 props 定义最初参考了 Ant Design Vue,写了整整 17 个可配项……上线后发现,90% 的业务场景只用到了 modelValuerulesdisabled 这三个。剩下那些 labelAligntooltipPlacementshowClear 全是“以防万一”加的,结果没人用,文档也懒得写。

后来删掉一半,把 rules 从对象改成函数形式,允许动态返回校验规则:

// 以前这样写,静态且冗长
rules: {
  title: [{ required: true, message: '标题必填' }],
  category: [{ required: true, message: '请选择分类' }]
}

// 现在这样,灵活多了
rules: (form) => ({
  title: form.type === 'vip' 
    ? [{ required: true, message: 'VIP 活动标题必填' }] 
    : [],
  category: [{ required: true }]
})

不过这里有个小问题至今没解决:当 rules 是函数时,内部如果用到了 refcomputed,会导致响应式失效(因为函数本身不被 track)。目前 workaround 是让调用方自己 watch 相关依赖,手动调用 revalidate()。不是最优解,但影响不大——毕竟只有 2 个页面用了动态规则。

回顾与反思

这套组件库上线三个月,支撑了 14 个运营活动,平均每个活动节省 3 小时开发时间。最大的收获不是技术细节,而是再次确认了一件事:**组件的复用性不等于接口的复杂度,而在于它是否能让业务同学‘抄完就能跑’**。

有些地方确实没做到最好:比如错误提示的 i18n 还是硬编码在组件里;比如移动端键盘遮挡 input 的问题,我们只加了 scrollIntoViewIfNeeded,没做 viewport 适配;比如 Storybook 文档写了一半就搁置了……但它们都没阻住交付节奏。

现在回头看,如果重来一遍,我会更早引入 defineModel()(Vue 3.4+),会尝试用 useTemplateRef 替代 ref 做 focus 管理,也会把校验器单独拆成 composable。但这些优化都不如第一版能按时上线重要。

以上是我踩坑后的总结,希望对你有帮助。如果你也遇到过类似问题,或者有更好的解法(比如怎么优雅处理动态 rules 的响应式),欢迎评论区交流。后续我还会分享这套组件如何对接低代码平台——那才是真正的炼狱模式。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论