微信小程序轻量级自定义组件库BetterWx-UI的设计与实践
2026/9/16 7:46:43 网站建设 项目流程

做了几年微信小程序开发,我最大的感受就是:官方基础组件真的够用,但真的不够好看。同一个页面里既要圆角按钮又要胶囊按钮,还要适配不同机型上的视觉差异,每次都要手撸一套公共样式,项目一多就是无尽的复制粘贴。后来我索性把自己的组件沉淀成了一个独立的组件库,就是今天要说的 BetterWx-UI。它不是一个商业产品,也不是什么大厂框架,就是一套面向微信小程序开发者的轻量级自定义组件库,解决的是小程序日常开发里最琐碎、最重复的 UI 问题。这篇博客我会从组件库的定位聊到核心实现,再复盘接入过程和踩坑记录,希望对正在纠结“要不要造轮子”或“该选哪个组件库”的你有点参考价值。

1. 为什么还要再造一个组件库

1.1 从一次需求改造说起

去年我接了一个零售类小程序的需求,业务方一口气提了十几个页面,每个页面都有表单、弹窗、Toast、空状态、骨架屏这些基础模块。用官方组件硬写当然可以,但你会发现官方 button 在不同机型上默认边框粗细不一样,picker 的样式又很难覆盖,弹窗状态一多,页面代码就失控了。我花了两个晚上把公共样式和组件抽出来,才发现这类需求几乎每家业务都在重复造轮子。

市面上其实已经有了不少成熟的小程序组件库,比如有赞的 Vant Weapp、腾讯的 TDesign、还有 Wux-Weapp 等。它们功能齐全、文档完善,直接用完全没问题。那为什么我还要自研一套 BetterWx-UI?核心原因有三点:

  • 体积与依赖:全量引入大型组件库会让主包体积明显上升,而 BetterWx-UI 采用按需引入,用到哪个组件就只打包哪个组件。
  • 设计语言不匹配:业务有自己的一套视觉规范,通用组件库的样式覆盖成本常常比重写还高,自研组件可以从一开始就把设计 token 统一起来。
  • 性能可控:大型组件库为了兼容各种场景,内部逻辑较重,在小程序这种“setData 有开销、页面栈有限”的环境里,轻量组件带来的收益是实打实的。

1.2 BetterWx-UI 的设计目标

在动手之前我给自己立了几条规矩,也是这个项目后续所有设计的判断标准:

  1. 轻。单个组件源码尽量控制在一个文件、两百行以内,不引入第三方依赖。
  2. 纯。全部基于小程序自定义组件能力实现,不侵入业务逻辑,不要求业务方改架构。
  3. 可定制。UI 相关的都通过 CSS 变量和 externalClasses 暴露出来,业务方不用改源码就能换主题。
  4. 易调试。组件内部只负责渲染和交互反馈,数据校验、请求逻辑全部留在页面层,这样出问题时定位更快。

这几条听起来简单,实际执行起来牵扯到的取舍很多。例如“纯”和“易定制”之间就存在张力:引入 externalClasses 会让组件样式结构复杂一点,但换来的是业务方可控性大幅提升,这个交换我认为值得。

2. 组件库的核心设计与关键实现

2.1 组件清单与分类

BetterWx-UI 并不是要把所有 UI 组件都重写一遍,而是优先覆盖那些在日常业务里“官方不好用、自己写又烦”的部分。目前组件大致分四类:

分类组件说明
基础类button、icon、tag、badge对官方组件的视觉增强与状态补充
反馈类toast、loading、dialog、skeleton、empty高频交互反馈,支持挂载在页面顶层
表单类input、textarea、picker、switch、rate带统一的校验提示和错误状态样式
业务类navbar、tabbar、search、goods-card常见电商/资讯类业务模块的通用封装

组件数量不求多,但每个组件都保证可以独立使用、可以单独打包。实际接入时,页面的 json 文件里注册哪个就只编译哪个,不会出现“装了个组件库,主包多了几百 KB”的情况。

2.2 一个组件从设计到落地的完整链路

以 picker 组件为例,这是小程序表单里最容易被吐槽的组件之一。官方 picker 的列数、联动逻辑、回显格式都要业务方自己处理,每个页面写一遍,逻辑散落各处。我在 BetterWx-UI 里把它做成了可配置的数据驱动组件,核心思路是让业务方只传数据、接事件,不关心渲染细节。

组件定义在 components/bwx-picker 目录下,结构如下:

components/bwx-picker/ ├── bwx-picker.json ├── bwx-picker.wxml ├── bwx-picker.wxss ├── bwx-picker.js └── bwx-picker.wxs

json 文件里把组件声明为自定义组件,重点在于设置 styleIsolation:

{ "component": true, "styleIsolation": "apply-shared" }

这里要说明一下为什么用 apply-shared。默认情况下小程序自定义组件的样式隔离是 isolated,页面 wxss 样式进不到组件内部,这保证了组件的独立性。但 picker 这种业务味很重的组件,往往需要由外部传入一些间距或颜色变量,用 apply-shared 可以让页面样式有限地渗入组件,配合 CSS 变量实现主题粒度的定制,又不会引发全局污染。

js 部分的核心是数据驱动的列联动。这里我用最简单的方式实现了一个“省市区”三级联动,列与列之间通过 observer 监听并重置下级数据:

Component({ properties: { columns: { type: Array, value: [] }, value: { type: Array, value: [] } }, data: { selectedIndex: [0, 0, 0] }, observers: { 'columns[0]': function () { this.resetColumn(1); this.resetColumn(2); this.triggerChange(); } }, methods: { onColumnChange(e) { const index = e.detail.column; this.setData({ [`selectedIndex[${index}]`]: e.detail.value }); if (index < 2) { this.resetColumn(index + 1); if (index === 0) { this.resetColumn(2); } } this.triggerChange(); }, resetColumn(level) { let resetValue = 0; if (this.data.selectedIndex[level] && this.data.columns[level]) { resetValue = Math.min(this.data.selectedIndex[level], this.data.columns[level].length - 1); } this.setData({ [`selectedIndex[${level}]`]: resetValue }); }, triggerChange() { const pickerValue = this.data.selectedIndex.map((idx, level) => { return this.data.columns[level] ? this.data.columns[level][idx] : ''; }); this.triggerEvent('change', { value: pickerValue }); } } });

这段代码里最关键的是触发自定义事件时的数据格式。我刻意把 change 事件返回的值设计成“完整文案数组”而不是索引数组,因为业务方回显时通常需要的是省市区名称,而不是三个下标。多花两行代码把数据转换成业务友好格式,能让使用方的代码少掉一大堆冗余逻辑。

wxml 部分就相对直白,核心是把 columns 拆成对应的 picker-view-column:

<view class="bwx-picker"> <picker-view value="{{selectedIndex}}" bindchange="onColumnChange" > <picker-view-column wx:for="{{columns[0]}}" wx:key="*this"> <view class="bwx-picker__item">{{item}}</view> </picker-view-column> <picker-view-column wx:for="{{columns[1]}}" wx:key="*this"> <view class="bwx-picker__item">{{item}}</view> </picker-view-column> <picker-view-column wx:for="{{columns[2]}}" wx:key="*this"> <view class="bwx-picker__item">{{item}}</view> </picker-view-column> </picker-view> </view>

如果业务只需要两级、甚至一级,只需要在 columns 里传空数组,多余列会自动渲染为空白,不会报错。这个容错设计非常实用,很多通用组件库在这个环节直接写死列数,导致扩展性很差。

2.3 主题与样式定制方案

小程序里做主题定制一直是个老大难。CSS 变量在小程序基础库 2.11.0 之后全面支持,现在可以放心用。BetterWx-UI 的做法是把所有颜色、圆角、间距统一到一组全局变量上,组件内部全部引用变量,不写死任何颜色值。

在组件库的根目录里,我维护了一份theme.wxss,大致长这样:

page { --bwx-primary: #1677ff; --bwx-success: #00b578; --bwx-warning: #ff8f1f; --bwx-danger: #ff3141; --bwx-border-radius-sm: 8rpx; --bwx-border-radius-md: 16rpx; --bwx-font-size-md: 28rpx; }

业务方只需要在 app.wxss 里覆盖这几个变量,整个组件库的配色就统一变了。这比用 classic 模式逐层覆盖样式要干净得多。另外,我也要求每个组件至少暴露一个 externalClasses 入口,比如custom-classcustom-header-class,用于处理变量覆盖不到的特殊场景,比如某个按钮在某个页面要单独加大内边距。

3. 从零接入:实操过程全记录

3.1 安装与构建

BetterWx-UI 目前通过私有 npm 包维护,安装方式与其他库没有区别。我这里用一个新项目演示完整流程。前提是开发者工具已经开启了 npm 构建能力,这个开关在“详情 - 本地设置 - 使用 npm 模块”里。

npm init -y npm install betterwx-ui

安装完成后,打开开发者工具,点击菜单栏中的“工具 - 构建 npm”,等待构建完成。构建结束后,项目根目录会多出一个miniprogram_npm目录,这才是小程序真正引用的代码位置。

提示:每次安装新依赖或修改了 node_modules 里的文件,都要重新执行一次构建 npm,否则不会生效。这个坑我踩过不止一次,习惯性地以为安装完就能直接用。

3.2 在页面中使用组件

假设我要在一个商品编辑页里使用 picker 和 toast。首先在页面的 json 文件里做两件事:一是开启自定义组件模式(默认是开启的,但明确写出来更稳妥),二是注册要使用的组件:

{ "navigationBarTitleText": "商品编辑", "usingComponents": { "bwx-picker": "betterwx-ui/picker/index", "bwx-toast": "betterwx-ui/toast/index", "bwx-button": "betterwx-ui/button/index" } }

然后在 wxml 里写业务结构:

<view class="form-item"> <text class="form-item__label">所在地区</text> <bwx-picker columns="{{regionColumns}}" value="{{regionValue}}" bindchange="onRegionChange" /> </view> <bwx-toast id="bwx-toast" /> <bwx-button type="primary" block loading="{{submitting}}" bind:click="onSubmit" > 保存商品 </bwx-button>

页面 js 里只需要维护数据、监听事件。toast 因为需要从逻辑层调用,我在组件内部封装了一个简单的全局方法,页面通过this.selectComponent('#bwx-toast')拿到实例后调用show方法:

Page({ data: { regionColumns: [ ['北京市', '上海市', '广东省'], ['朝阳区', '浦东新区', '天河区'], ['望京街道', '陆家嘴街道', '猎德街道'] ], regionValue: [0, 0, 0], submitting: false }, onRegionChange(e) { this.setData({ regionValue: e.detail.value }); }, onSubmit() { if (!this.selectedRegionText) { this.selectComponent('#bwx-toast').show({ message: '请先选择地区', type: 'warning' }); return; } this.setData({ submitting: true }); // 业务请求逻辑... } });

接入过程非常顺,因为组件不关心页面怎么取数、怎么提交,页面也不关心组件内部怎么渲染。这种松耦合关系,让新手改起来也很快,因为每一个文件的职责都极其清楚。

3.3 主题定制的一次实战

之前有个项目要求做一个“大促限定版”,整个页面要临时变成红色系。如果没有主题方案,我可能需要写一堆覆盖样式。用 BetterWx-UI,我只需要在页面的 wxss 里临时覆盖变量:

/* 大促活动页限定样式 */ page { --bwx-primary: #ff3141; --bwx-border-radius-md: 24rpx; }

组件库内部的所有按钮、标签、价格文本瞬间全部变成红色系,并且圆角也一并调整。活动结束后,删掉这段 wxss 就恢复默认,不用动任何组件内部代码。这就是 CSS 变量方案最舒服的地方:它把样式变化从代码逻辑里剥离出来,真正实现了“一个页面一个主题”。

4. 接入路上的那些坑

4.1 npm 构建后找不到组件路径

这是最高频的问题。很多人在 json 里写"bwx-picker": "betterwx-ui/picker/index",然后构建时报“组件未找到”。排查思路按顺序来:

  • 先确认miniprogram_npm目录下有没有betterwx-ui这个文件夹,没有就是 npm 构建没成功。
  • 如果构建成功但仍找不到,检查 json 文件里的路径是否多写了.././。正确的写法是从 app.json 所在目录的相对路径出发,miniprogram_npm目录名不需要写出来。
  • 最后确认小程序基础库版本,组件库用到了较新的 API 时,低版本基础库会出现静默失败。

4.2 样式隔离导致的二次污染

早期版本的组件默认用了isolated隔离,结果业务方反馈“页面里给 view 写的背景色不生效了”。这是正常的,因为页面样式根本进不了组件。后来我把部分业务型组件改成了apply-shared,又出现了新的问题:页面全局样式会渗透进来,偶尔会把组件的内部结构挤乱。

最终的解决方案是在组件 wxss 里把所有关键布局写在组件根节点的 class 下,不依赖任何标签选择器,也不写没有任何 class 前缀的裸样式。这样即使页面样式渗入,也很难覆盖到组件内部的关键布局。这是一个非常实用的经验:自定义组件的样式表里,永远只使用带.bwx-prefix的类选择器。

4.3 setData 滥用导致页面卡顿

组件库内部也不可避免地使用 setData。最初 picker 的 onColumnChange 里为了更新联动状态,每次都会同时 setData 两到三个字段,在低端安卓机上滑动手感明显卡顿。后来我用两个手段优化:

  • 减少 setData 频率:把列联动中的中间态合并成一次 setData,而不是分步更新。
  • 缩小数据量:组件只回传需要的数据结构,不在事件触发时同步整个 columns 数组。

优化之后,picker 在真机上基本能保持 60 帧滑动。这里我想强调:小程序里 setData 的昂贵程度远超大多数前端开发者的直觉,它走的是一整条 native 通道,数据越大、调用越频繁,卡顿越明显。做组件库尤其要把这类性能问题提前考虑掉。

4.4 表单校验状态怎么沉淀

表单类组件最容易遇到的问题就是校验逻辑写得越来越长。我参考了前端主流表单库的思路,在组件外部提供了一套统一的“校验提示”约定:组件暴露error属性,传入{{true}}时自动切换为错误态(红框、错误提示图标),页面通过单独的逻辑层做校验。

<bwx-input value="{{phone}}" error="{{phoneError}}" error-message="请输入正确的手机号" bindinput="onPhoneInput" />

页面里的校验逻辑始终在 Page 里维护,组件只负责展示。这个约定本身不复杂,但它统一了团队里“错误态长什么样”这个问题,后续新成员写表单时不用再猜。

5. 值得沉淀的经验与后续计划

5.1 组件库维护的节奏感

维护 BetterWx-UI 的过程中我最大的体会是:组件库不是一锤子买卖,而是一个持续演进的项目。每做一个新业务,就可能会暴露一个新的通用诉求。但我不建议一有想法就加组件,而是先在业务里用两次以上,确认模式足够稳定后再沉淀进组件库。这套流程帮我挡掉了很多“拍脑袋”的组件设计,也保证了库里的每个组件都是经受过真实场景检验的。

5.2 后续想做的扩展方向

目前组件库已经覆盖了业务中大约七成的高频 UI 场景,后续我最想补的是暗黑模式支持。小程序基础库已经支持prefers-color-scheme媒体查询,配合 CSS 变量方案,暗黑模式只需要设计一套暗色 token,理论上所有组件可以无痛切换。

另一个方向是导出 Sketch/Figma 设计资源。很多组件库在工程上做得好,但设计师和前端之间总有说不清的“视觉还原偏差”。如果能把设计资源与代码中的 CSS 变量一一对应,团队协作的摩擦会小很多。这也是我下一步想尝试的事情。

5.3 最后分享一个底层经验

如果让我给想自研组件库的朋友一句忠告,那就是:先想清楚你的组件与业务之间的边界。组件越通用,它的 API 就会越复杂;组件越贴合业务,它就越难复用。BetterWx-UI 选择的是“通用交互 + 业务样式变量”的折中方案,这让它在多个项目里都能稳定落地。

另外,别一上来就想做一个大而全的框架。从两三个真正高频的组件开始,跑通“设计-实现-使用-反馈”这个循环,比一次性铺开几十个组件靠谱得多。组件库的成长是跟着业务需求慢慢长出来的,不是靠加班堆出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询