一、Design Token 体系设计
1.1 为什么先做 Token 再做组件
组件库最常见的失败模式是「组件写完了,样式改不动」:颜色硬编码在每个 wxss 里,品牌换色要改几百个文件。所以工程化的顺序必须是 Token 先行、组件后置。Design Token 是设计决策的最小可命名单元,把颜色、字号、间距、圆角、阴影、动效时长从组件实现里抽出来,成为一层可被替换的变量。
| Token 类别 | 示例 | 变更频率 |
|---|---|---|
| 基础色板 | --blue-500: #1989fa | 极低 |
| 语义色 / 文本色 | --color-primary、--text-secondary | 低 |
| 间距 / 字号 | --space-4、--font-size-md | 极低 |
| 圆角 / 阴影 | --radius-md、--shadow-card | 低 |
| 动效 | --duration-fast: 150ms | 中 |
1.2 三层命名结构
推荐「基础层、语义层、组件层」三层结构,组件只允许引用语义层与组件层,禁止直接引用基础色板:
/* tokens/base.wxss 基础层:只描述颜色本身 */
page {
--blue-500: #1989fa;
--blue-600: #0570db;
--red-500: #ee0a24;
--gray-100: #f7f8fa;
--gray-300: #ebedf0;
--gray-600: #969799;
--gray-900: #323233;
}
/* tokens/semantic.wxss 语义层:描述用途 */
page {
--color-primary: var(--blue-500);
--color-primary-active: var(--blue-600);
--color-danger: var(--red-500);
--color-bg-page: var(--gray-100);
--color-border: var(--gray-300);
--text-primary: var(--gray-900);
--text-secondary: var(--gray-600);
}
/* tokens/component.wxss 组件层:描述具体组件 */
page {
--button-height-md: 88rpx;
--button-radius: var(--radius-md);
}
组件内只允许写 color: var(--text-primary) 这类引用,一旦出现 #323233 就应该在 CI 检查里报错。
1.3 Token 的工程化产出
Token 不应该手写多份,而应该由单一数据源生成多份产物:
design-tokens.json 单一数据源(设计与研发共同维护)
│ build 脚本
├──> tokens.wxss 小程序使用
├──> tokens.scss Web 端 / Taro 使用
├──> tokens.js 运行时常量(如 canvas 绘制)
└──> tokens.d.ts TypeScript 类型提示
// design-tokens.json 片段
{
"color": {
"blue": { "500": { "value": "#1989fa" } },
"red": { "500": { "value": "#ee0a24" } }
},
"space": {
"1": { "value": "8rpx" },
"2": { "value": "16rpx" },
"4": { "value": "32rpx" }
}
}
构建脚本的核心逻辑只有三步:递归展平 design-tokens.json 得到 "blue-500": "#1989fa" 这样的扁平映射,拼成 page { --blue-500: #1989fa; } 形式的 wxss 字符串,再写入 src/styles/tokens.wxss。整个过程在 CI 里跑,产物不入库。
二、组件库目录结构与分层
2.1 分层原则
组件库不是一堆组件的平铺,而是按「依赖方向单向」分层:
packages/
├── tokens/ 第 0 层:设计变量,无依赖
├── icons/ 第 1 层:图标字体 / SVG 组件
├── base/ 第 2 层:button / icon / cell / divider
├── form/ 第 3 层:input / checkbox / picker / uploader
├── feedback/ 第 3 层:toast / dialog / action-sheet
├── display/ 第 3 层:card / tag / badge / steps
└── business/ 第 4 层:order-card / address-picker
依赖规则:上层可以依赖下层,下层永远不能依赖上层。业务组件放独立包,避免污染通用库。
2.2 单个组件的目录结构
每个组件目录保持固定形状,工具链才能自动化:index.js(逻辑)、index.json(usingComponents 声明)、index.wxml(模板)、index.wxss(只引用 token 与自身变量)、index.d.ts(类型声明,可选)、README.md(文档)与 __tests__/index.test.js(单测)。
// button/index.js
Component({
options: { multipleSlots: true, styleIsolation: 'apply-shared' },
properties: {
type: { type: String, value: 'default' }, // default | primary | danger
size: { type: String, value: 'medium' }, // small | medium | large
block: { type: Boolean, value: false },
disabled: { type: Boolean, value: false },
loading: { type: Boolean, value: false }
},
methods: {
onTap(e) {
if (this.data.disabled || this.data.loading) return;
this.triggerEvent('click', { detail: e.detail });
}
}
});
<view
class="ui-button ui-button--{{type}} ui-button--{{size}} {{block ? 'ui-button--block' : ''}} {{disabled ? 'is-disabled' : ''}}"
hover-class="{{disabled ? '' : 'ui-button--hover'}}"
hover-stay-time="60"
bindtap="onTap"
>
<ui-loading wx:if="{{loading}}" size="32rpx" color="currentColor" />
<slot wx:else />
</view>
.ui-button {
display: flex;
align-items: center;
justify-content: center;
height: var(--button-height-md);
padding: 0 var(--space-4);
border-radius: var(--button-radius);
font-size: var(--font-size-md);
line-height: 1;
}
.ui-button--primary { background-color: var(--color-primary); color: #fff; }
.ui-button--block { width: 100%; }
.ui-button.is-disabled { opacity: 0.5; }
2.3 组件通信规范
父传子用 properties(类型与默认值必须声明),子传父用 triggerEvent('click', detail)(事件名小写无前缀),跨层传递用 relations 或 provide/inject,全局配置走 Behavior 或全局 store。完整用法可对照小程序组件化架构
,组件库应当只暴露这两套机制,不引入私有通信方式。
三、npm 包发布与 miniprogram_npm 构建
3.1 发布到 npm
小程序支持从 npm 安装依赖,但要求包内是未编译的源码或已构建的小程序产物,不能用 Webpack 打成 bundle。
{
"name": "@yourorg/miniprogram-ui",
"version": "2.3.0",
"miniprogram": "dist",
"files": ["dist", "README.md"]
}
miniprogram 字段指向构建产物目录,开发者工具执行「构建 npm」时会读取该字段。产物目录里每个组件保持 index.js / index.json / index.wxml / index.wxss 四件套,发布前用 npm pack --dry-run 确认文件齐全。
# 发布流程
npm run build # 编译 src -> dist
npm version patch # 或 minor / major
npm publish --access public
3.2 项目侧构建 npm
// 项目 package.json
{
"dependencies": {
"@yourorg/miniprogram-ui": "^2.3.0"
}
}
{
"setting": {
"packNpmManually": true,
"packNpmRelationList": [
{ "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./miniprogram/" }
]
}
}
操作步骤是:项目根目录执行 npm install,在开发者工具里点击「工具 -> 构建 npm」,生成 miniprogram_npm/@yourorg/miniprogram-ui/ 目录后即可在 usingComponents 中引用。
当项目使用分包时,packNpmManually 必须为 true,并为每个分包声明依赖关系,否则构建出的 miniprogram_npm 会全部落在主包,导致主包体积超标。
3.3 构建产物的坑
| 坑 | 现象 | 规避 |
|---|---|---|
引入了 node_modules 里的第三方包 | 构建报错找不到模块 | 组件库零运行时依赖,或把依赖打进 dist |
用了 ES Module 的 import | 低版本基础库报错 | 产物统一 CommonJS,用 require |
wxss 里 @import 了包外文件 | 构建后路径失效 | 只 @import dist 内相对路径 |
版本号用了 ^ | 线上版本漂移 | 组件库锁精确版本,用 lock 文件固化 |
四、按需注入与 usingComponents
4.1 按需注入
从基础库 2.11.1 起支持 lazyCodeLoading,能显著降低启动耗时:
// app.json
{
"lazyCodeLoading": "requiredComponents"
}
开启后小程序只注入当前页面真正用到的自定义组件代码。这对组件库尤其重要:包含 60 个组件的库如果全量注入,启动时会白白执行几十个组件文件的顶层代码。
4.2 声明方式
页面与组件都必须显式声明 usingComponents,路径指向构建后的 miniprogram_npm:
{
"usingComponents": {
"ui-button": "@yourorg/miniprogram-ui/button/index",
"ui-cell": "@yourorg/miniprogram-ui/cell/index"
}
}
| 风格 | 写法 | 特点 |
|---|---|---|
| 全路径 | @yourorg/miniprogram-ui/button/index | 可静态分析、按需注入友好 |
| 聚合入口 | @yourorg/miniprogram-ui | 写法短,但可能引入整包,不推荐 |
4.3 全局注册与体积权衡
app.json 的 usingComponents 是全局注册,任何页面都能用,但会让所有页面都注入这些组件:
// app.json —— 只放真正高频的基础组件
{
"usingComponents": {
"ui-icon": "@yourorg/miniprogram-ui/icon/index",
"ui-button": "@yourorg/miniprogram-ui/button/index"
}
}
原则:全局只放 3 到 5 个最高频的基础组件,其余全部页面级声明。取舍要结合小程序性能优化 中的主包瘦身手段做判断。
五、主题与暗黑模式
5.1 用 CSS 变量实现主题切换
因为 Token 全部走 CSS 变量,主题切换只需覆盖变量值:
/* themes/light.wxss */
page, .theme-light {
--color-bg-page: #f7f8fa;
--color-bg-card: #ffffff;
--text-primary: #323233;
--color-border: #ebedf0;
}
/* themes/dark.wxss */
page.theme-dark, .theme-dark {
--color-bg-page: #1c1c1e;
--color-bg-card: #2c2c2e;
--text-primary: #f2f2f7;
--color-border: #3a3a3c;
}
组件内部只写 background: var(--color-bg-card),不写任何具体颜色,暗黑模式就自动生效。
5.2 跟随系统
// app.js
App({
globalData: { theme: 'light' },
onLaunch() {
this.globalData.theme = wx.getAppBaseInfo().theme || 'light';
this.applyTheme(this.globalData.theme);
wx.onThemeChange((res) => {
this.globalData.theme = res.theme;
this.applyTheme(res.theme);
});
},
applyTheme(theme) {
getCurrentPages().forEach((page) => {
if (typeof page.applyTheme === 'function') page.applyTheme(theme);
});
}
});
// app.json 声明支持暗黑模式
{
"darkmode": true,
"themeLocation": "theme.json"
}
// theme.json:让导航栏、tabBar 等原生 UI 跟随主题
{
"light": {
"navBgColor": "#ffffff", "navTxtStyle": "black", "bgColor": "#f7f8fa",
"tabbarColor": "#7a7e83", "tabbarSelectedColor": "#1989fa", "tabbarBgColor": "#ffffff"
},
"dark": {
"navBgColor": "#1c1c1e", "navTxtStyle": "white", "bgColor": "#1c1c1e",
"tabbarColor": "#8e8e93", "tabbarSelectedColor": "#1989fa", "tabbarBgColor": "#2c2c2e"
}
}
5.3 主题实现的三条铁律
组件内禁止硬编码颜色(包括 #fff、rgba(0,0,0,0.5)),全部走语义变量;图片资源准备两套,或改用 mask 加背景色方案让图标跟随主题色;主题切换后要验证 canvas 绘制,因为 wx.createCanvasContext 读不到 CSS 变量,需要用 tokens.js 里的运行时常量。
六、文档站与多端一致性
6.1 组件文档站
组件库没有文档就等于没有组件库。最小可用方案是「README 即文档 + 自动聚合」:
文档站结构为 docs/index.md(概览与安装)、docs/token.md(Token 清单,脚本生成)、docs/components/(由 packages/*/README.md 聚合)与 docs/changelog.md。每个组件的 README 遵循固定骨架(何时使用、代码演示、API 表、事件表),便于脚本解析。CI 里再检查「组件目录数等于文档文件数」,防止新增组件忘写文档。
6.2 多端一致性
同一套设计系统往往要落到小程序、H5、甚至 App,一致性靠三件事保障:Token 走单一数据源构建出 wxss / scss / js 三份产物;组件 API 的属性名、事件名、默认值三端对齐,禁止各端私自改名;关键组件维护视觉基准图,改动后逐端截图比对。
如果项目本身是多端框架,可以参考小程序多端框架对比 中关于样式隔离与组件适配的结论,再决定「一套组件三端复用」还是「按端维护薄封装层」。
七、版本管理与 breaking change 策略
7.1 语义化版本
组件库必须严格执行 SemVer,并明确「什么算 breaking」:
| 变更类型 | 版本位 | 例子 |
|---|---|---|
| 修复 bug、样式微调 | patch | 修复 button 加载态未禁用点击 |
| 新增组件、新增可选属性 | minor | 新增 size="large" |
| 删属性、改类型、改事件名、改默认值、Token 改名 | major | size 默认值由 large 改为 medium |
改默认值也算 breaking,这是最容易被忽略的一条:业务方依赖了旧默认值,升级后视觉就会变。
7.2 废弃流程
不要直接删 API,走三步废弃:
// 第一步:保留旧属性,打印警告,内部映射到新属性
Component({
properties: {
type: { type: String, value: '' },
kind: { type: String, value: 'default' } // 新属性
},
observers: {
type(val) {
if (val) {
console.warn('[ui-button] 属性 type 已废弃,请改用 kind');
this.setData({ kind: val });
}
}
}
});
第二步:文档与 CHANGELOG 标注 deprecated,给出替换示例与计划移除版本。第三步:跨一个 major 版本后移除,并在 CHANGELOG 的 Breaking Changes 段落写明。
7.3 发布与升级流程
# 组件库侧
npm run lint && npm run test && npm run build
npm version minor -m "feat(button): 新增 large 尺寸"
npm publish --access public
# 业务侧:升级 -> 开发者工具「构建 npm」-> 视觉基准图比对与关键页面走查
CHANGELOG 建议按 Added / Changed / Fixed / Deprecated / Breaking Changes 五段组织,并强制要求 major 版本必须写迁移指引。业务项目里再配一条 CI 检查:如果组件库版本跨了 major,必须人工确认后才能合入。
八、总结
小程序设计系统的工程化,本质是把「设计决策」变成「可构建、可发布、可回归的代码资产」:Token 是单一数据源,构建出多端产物;组件按依赖方向分层,每个组件目录形状固定;通过 npm 的 miniprogram 字段发布,业务侧用「构建 npm」产出 miniprogram_npm;用 lazyCodeLoading: requiredComponents 与页面级 usingComponents 控制注入体积;主题靠 CSS 变量与 theme.json 实现,组件内零硬编码颜色。
最容易被低估的是版本管理:删属性、改默认值、改 Token 名都是 breaking change,必须走「保留旧 API 打印警告、文档标注 deprecated、跨 major 再移除」的流程,否则每次升级都会变成一次全量视觉回归。把 Token 生成、文档聚合、变更检查这三条放进 CI,设计系统才能持续演进而不是逐渐腐化。组件通信与跨层状态的具体取舍可对照小程序组件化架构 与小程序状态管理 ,而组件体积与首屏耗时的关系则要在小程序性能优化 的框架下统一衡量。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。