Skip to content

30 · 包与插件开发:组件库抽离与发布 ​

本篇解决什么问题: 当同一个组件/工具在 HaierEnergy(lib/ 1118 个文件)里被复制粘贴了三遍, 或者你想把沉淀的能力复用到下一个项目时,该怎么抽、怎么组织、怎么发布。

⚠️ 易变信息说明:pub.dev 的评分规则、发布流程细节、命令参数会随时间变化, 按本库约定(正文写原理与判断框架)不写死具体版本,请以 pub.dev 官方文档 为准。 本篇讲抽离时机判断、三种形态选择、包设计原则、发布前自检——这些是稳定的。

📌 前置:11 篇(组件与主题)、16 篇(工程化)、 19 篇(测试)、21 篇(Platform Channel,插件篇会用到)。


开篇:前端概念对照 ​

前端Flutter / Dart说明
npm publishflutter pub publish发布到公共仓库
package.jsonpubspec.yaml包清单
node_modules.dart_tool/package_config.json依赖解析结果
npm 私有源(Verdaccio / Nexus)私有 pub server / git 依赖企业内部包
Monorepo(pnpm workspace)melos / pub workspace多包管理
"dependencies" vs "devDependencies"dependencies vs dev_dependencies运行时/开发时依赖
npm link(本地联调)path: 依赖本地路径引用
src/index.ts(导出入口)lib/<包名>.dart(barrel 文件)统一导出入口
Storybookwidgetbook / Monarch组件预览与走查
Semantic VersioningSemantic Versioning(强制语义化)版本规范

关键认知差:

  • npm 里 ^1.2.3 的语义很宽松;Dart 的 ^1.2.3 语义是 >=1.2.3 <2.0.0,且 pub 有严格的版本解析与冲突处理
  • Dart 生态对语义化版本执行更严格——破坏性变更必须升大版本,否则下游项目会被静默破坏
  • 发布到 pub.dev 有自动评分(like/trust/popularity),且发布后不可撤销、只能标记撤回

一、先判断:现在该抽离吗?(最重要的一节) ​

过早抽离是灾难——抽象错了,比重复三遍更糟。

1.1 抽离的三个信号(满足其一再动手) ​

信号说明
同一份代码复制了 3 次以上"三振出局"原则(Rule of Three):第 3 次复制时再抽
要跨项目复用下一个项目明确要用
它是稳定的领域概念有清晰边界、不会每周变

1.2 反信号(这些时候别抽) ​

反信号为什么
只用了 1 次抽了也是"假复用",增加跳转成本
需求还在剧烈变化抽象会跟着天天改,反而拖慢
三个"复用点"其实长得不一样强行统一会得到一堆 if 和可选参数
只是"看起来该抽"没有真实第二、第三个使用方,就是过度设计

💡 经验法则:先写重复,等到第 3 次再抽。 因为只有见过 3 个真实用例,你才知道哪些该变、哪些该固定—— 第 1、2 次就抽,抽出来的往往是错的抽象。

1.3 抽离粒度:先内后外 ​

第 1 步:先在项目内统一(lib/widgets/ 或 lib/core/)
        → 解决"同一组件有三种样式"的问题,成本最低
第 2 步:确认稳定 + 有第二个使用方 → 抽成独立 package(本地 path 依赖)
第 3 步:确认要公开或跨团队 → 发布(pub.dev 或私有仓库)

不要跳步。直接从"项目内复制粘贴"跳到"发布 pub.dev",是新手最常见的错误。


二、三种复用形态(按成本递增) ​

形态适用场景成本独立版本
① 项目内目录只在当前项目复用极低❌
② 本地 path: 依赖同一仓库多包 / 联调期低✅(独立 pubspec)
③ 发布到仓库跨项目、跨团队、公开高(要维护/发版/写文档)✅

形态 ①:项目内(先做这个) ​

结合 11 篇 的组件体系:

lib/
├── widgets/          通用无状态组件(按钮、输入框、卡片、空状态)
├── theme/            主题、颜色、字号(与 ThemeExtension 配合)
└── core/             工具类(日期格式化、校验、日志)

要点:组件不要持有业务状态,主题走 ThemeExtension 而非硬编码颜色。 详见 11 篇。

形态 ②:本地 path: 依赖(推荐的中转形态) ​

yaml
# 主项目的 pubspec.yaml
dependencies:
  my_ui_kit:
    path: ../packages/my_ui_kit      # 相对路径

优点:

  • 有独立的 pubspec.yaml,为将来发布做好了准备
  • 改了立即生效,不用发版(联调神器)
  • 主项目和包可以分开测试

缺点:路径依赖,别人 clone 你的仓库时目录结构要一致。

💡 这相当于前端的 npm link / workspace,是抽包过程中最舒服的中间态。

形态 ③:发布 ​

yaml
dependencies:
  my_ui_kit: ^1.0.0      # 从 pub.dev 拉
  # 或私有 git 仓库:
  # my_ui_kit:
  #   git:
  #     url: https://gitee.com/xxx/my_ui_kit.git
  #     ref: v1.0.0       # ← 一定要锁定 tag,别用分支

三、Package vs Plugin:别搞混 ​

这是最基础但最容易混的概念:

Package(纯 Dart 包)Plugin(插件)
是否含原生代码❌ 纯 Dart✅ 含 Android/iOS/Web 等平台代码
依赖 Platform Channel不需要需要(见 21 篇)
典型例子组件库、工具类、状态管理库相机、定位、推送、蓝牙
创建命令flutter create --template=packageflutter create --template=plugin
复杂度低高(要写多端原生代码)

判断口诀:要调用系统能力就必须是 Plugin,否则用 Package。

3.1 联邦插件(Federated Plugin) ​

当插件要支持多平台时,官方推荐拆成联邦结构:

my_plugin/                    ← 主包(app-facing):声明接口 + 平台注册
├── my_plugin_platform_interface/   ← 平台接口层(抽象)
├── my_plugin_android/              ← Android 实现
├── my_plugin_ios/                  ← iOS 实现
└── my_plugin_web/                  ← Web 实现

为什么这么拆:各平台实现可以独立发版,且用户只依赖 my_plugin 主包。

💡 只在要支持多平台且各端差异大时才需要。 单平台或简单插件,一个包写完即可。


四、创建一个 Package(完整流程) ​

4.1 创建 ​

bash
flutter create --template=package my_ui_kit
# 或指定平台(package 模板也支持)
flutter create --template=package --platforms=android,ios my_ui_kit

生成的目录:

my_ui_kit/
├── lib/
│   └── my_ui_kit.dart        ← 导出入口(barrel 文件)
├── test/
│   └── my_ui_kit_test.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
├── LICENSE
└── analysis_options.yaml     ← lint 规则(重要)

4.2 pubspec.yaml 关键字段 ​

yaml
name: my_ui_kit              # ① 包名:全局唯一,小写+下划线
description: A reusable UI component kit for HaierEnergy apps.
version: 0.0.1               # ② 语义化版本
homepage: https://gitee.com/xxx/my_ui_kit
# repository: https://...    # 可选,填了能提分

environment:                 # ③ SDK 约束(影响下游能否用)
  sdk: ^3.4.0
  flutter: ">=3.4.0"

dependencies:                # ④ 运行时依赖
  flutter:
    sdk: flutter

dev_dependencies:            # ⑤ 开发时依赖(不传给下游)
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

# publish_to: none           # ⑥ 私有包:加这行可防止误发布到 pub.dev

六个要点:

#要点
①name 全局唯一,先去 pub.dev 搜一下有没有重名
②版本必须语义化:MAJOR.MINOR.PATCH,破坏性变更升 MAJOR
③environment 的下限越低越好(能兼容更多下游),上限一般不设(避免卡住下游升级)
④dependencies 里的东西会传给下游,能不放就不放(依赖越少越受欢迎)
⑤测试/lint 相关放 dev_dependencies
⑥私有包一定要加 publish_to: none,防止误发到公网

4.3 导出入口(barrel 文件) ​

dart
// lib/my_ui_kit.dart
library;

export 'src/buttons/primary_button.dart';
export 'src/inputs/labeled_input.dart';
export 'src/feedback/empty_state.dart';
// ⚠️ 只导出"公开 API",内部实现放 lib/src/ 不导出

约定:

  • lib/src/ 下的文件是"私有"的(Dart 约定),下游不应直接 import
  • 只通过 lib/<包名>.dart 导出你想公开的 API
  • 这样内部重构不会破坏下游

五、组件库设计原则(抽包的"内功") ​

这部分比"怎么发布"更重要——包好不好用,取决于 API 设计。

5.1 五条原则 ​

原则说明反例
① 单一职责一个组件只做一件事一个"万能卡片"接受 20 个参数
② 参数少而正交参数之间不互相影响isLarge + size 同时存在
③ 不持有业务状态组件纯展示,状态由外部传入组件内部直接调接口取数据
④ 主题从 ThemeExtension 取不硬编码颜色/字号color: Color(0xFF1677FF)
⑤ 可访问性支持 Semantics、足够点击区域图标按钮小于 44×44

5.2 关于"配置化"的度(最容易过度设计的地方) ​

dart
// ❌ 过度配置:参数爆炸,没人记得住怎么用
MyCard(
  title: 'xx', subtitle: 'xx', showDivider: true, dividerColor: Colors.grey,
  borderRadius: 8, elevation: 2, padding: 16, onTap: ..., trailing: ...,
  leading: ..., backgroundColor: ..., showShadow: true, isHighlighted: false,
)

// ✅ 适度:常用参数暴露,细节走主题,特殊需求用组合
MyCard(
  title: 'xx',
  subtitle: 'xx',
  onTap: () {},
  child: const CustomContent(),   // 特殊需求用 child 组合,而不是加参数
)

判断标准:当参数超过 7 个,就该考虑拆组件或用 child 组合了。

5.3 与项目主题体系衔接 ​

结合 11 篇:组件库不要自带一套颜色,而要读 ThemeExtension:

dart
final theme = Theme.of(context);
final ext = theme.extension<AppColors>()!;   // 项目的主题扩展

return Container(
  color: ext.surface,          // ✅ 从主题取
  // color: Color(0xFF1677FF), ❌ 硬编码,换主题就废
);

好处:组件库在 HaierEnergy 和未来其他项目里,只要换 ThemeExtension 就能换肤。


六、文档与示例(决定别人会不会用) ​

一个没人会用的包 = 白写。

必须项说明
README一句话定位 + 安装 + 最小可用示例(复制就能跑)
CHANGELOG每个版本改了什么(pub.dev 会展示)
dartdoc 注释公开 API 都要有 /// 注释(影响评分,也影响 IDE 提示)
example/ 目录一个可运行的示例 App(最加分)
LICENSE开源必填(如 MIT / BSD)
dart
/// 主按钮组件。
///
/// 用于页面中的主要操作,一屏建议只出现一个。
/// 若 [onPressed] 为 null,按钮显示为禁用态。
///
/// 示例:
/// ```dart
/// PrimaryButton(text: '提交', onPressed: _submit)
/// ```
class PrimaryButton extends StatelessWidget { ... }

💡 example/ 目录很重要:pub.dev 会抓它展示,也是你自测包能否被正常使用的最好方式 (因为 example 是通过 path: 引用父包,能验证导出是否完整)。


七、发布前自检清单(逐条过) ​

7.1 代码质量 ​

bash
flutter analyze          # ① 静态分析:0 issues
dart format .            # ② 格式化(pub.dev 会检查)
flutter test             # ③ 测试全过
检查命令要求
静态分析flutter analyze0 issues(有 warning 也会扣分)
格式dart format --output=none --set-exit-if-changed .无变更
测试flutter test全过
文档生成dart doc无警告(可选但推荐)

7.2 发布演练(必做) ​

bash
flutter pub publish --dry-run

这个命令会:

  • 检查 pubspec.yaml 完整性
  • 列出将要上传的文件
  • 报出所有会阻止发布的问题

常见被拦原因:

原因解法
缺 LICENSE加一个(如 MIT)
缺 description 或太短写清楚(建议 60~180 字符)
有 path: 依赖发布前必须改掉(下游拉不到你的本地路径)
version 已存在升版本号
含无关大文件用 .pubignore 排除

7.3 CHANGELOG 与版本 ​

markdown
## 1.1.0
- 新增 `SecondaryButton` 组件
- `PrimaryButton` 支持 `icon` 参数

## 1.0.1
- 修复 `EmptyState` 在深色模式下文字不可见

## 1.0.0
- 首个稳定版本

语义化版本规则(Dart 生态执行严格):

变更类型版本位例子
破坏性变更(改/删 API)MAJOR1.x.x → 2.0.0
新增功能(向后兼容)MINOR1.0.x → 1.1.0
修 bug(向后兼容)PATCH1.0.0 → 1.0.1

7.4 正式发布 ​

bash
flutter pub publish       # 需要授权(首次会打开浏览器)

⚠️ 发布后不可撤销。pub.dev 的政策是只能标记"撤回"(retracted),不能删除。 所以 --dry-run 一定要先跑。

7.5 私有包(不想公开) ​

方案适用
publish_to: none + git 依赖小团队、Gitee/GitHub 私有仓库
私有 pub server(如 pub_server 包)企业级
本地 path:同一仓库内的多包
yaml
# 私有 git 依赖(注意锁 tag)
my_ui_kit:
  git:
    url: https://gitee.com/xxx/my_ui_kit.git
    ref: v1.0.0       # ← 锁死版本,别用分支名(会飘)

八、HaierEnergy 的抽离建议(落地) ​

结合项目现状(lib/ 1118 个文件,有 core/ data/ domain/ 等目录,见 07 篇):

抽离优先级 ​

优先级内容理由
P0通用 UI 组件(按钮/输入框/卡片/空状态/加载)复用最频繁、最稳定、视觉一致性收益最大
P1主题与 ThemeExtension换肤、多项目一致(见 11 篇)
P1纯工具函数(日期、校验、格式化)无状态、易测、零依赖
P2网络层封装(Dio 拦截器、错误处理)与 09 篇 强相关,但耦合业务较多,要小心
P3业务流程组件⚠️ 不建议抽,耦合业务,抽了反而难改

建议路径 ​

1. 先在 lib/widgets/ 统一(形态 ①)      ← 现在就做,成本最低
2. 稳定后抽成 packages/my_ui_kit(形态 ② path 依赖)
3. 有第二个项目要用时再考虑发布(形态 ③)

⚠️ 不要一开始就追求"完美组件库"。 先在项目内把重复的组件统一掉,这一步的收益已经很大了。


九、动手练习 ​

练习 A:抽一个真实组件(形态 ①) ​

从 HaierEnergy 里找一个被复制了多次的组件(如"空状态占位"), 统一到 lib/widgets/,要求:

  • 颜色从 ThemeExtension 取,不硬编码
  • 参数不超过 7 个
  • 有 /// 文档注释

练习 B:抽成独立 package(形态 ②) ​

bash
flutter create --template=package my_ui_kit

把练习 A 的组件迁进去,主项目用 path: 引用。 验证:

  • 在 example/ 里写一个 demo App,确认能正常 import 和使用
  • 故意漏掉一个 export,观察下游是否报错(体会 barrel 文件的作用)
  • 加一个 lib/src/ 下的私有文件,确认下游无法直接 import

练习 C:发布前自检(形态 ③ 演练) ​

对练习 B 的包执行:

bash
flutter analyze
dart format --output=none --set-exit-if-changed .
flutter test
flutter pub publish --dry-run

验证:

  • 故意删掉 LICENSE,看 --dry-run 是否报错
  • 故意写一个 path: 依赖,看是否被拦
  • 加 publish_to: none,确认不会误发到公网

十、自检清单 ​

学完本文,你应该能够:

  • [ ] 说出"三振出局"原则,并判断什么时候不该抽离
  • [ ] 说出三种复用形态(项目内 / path: 依赖 / 发布)及各自适用场景
  • [ ] 区分 Package(纯 Dart)与 Plugin(含原生 + Platform Channel)
  • [ ] 说出联邦插件的结构与适用时机
  • [ ] 写对一个可发布的 pubspec.yaml(6 个关键字段的含义)
  • [ ] 解释 lib/src/ 的作用,以及为什么要用 barrel 文件导出
  • [ ] 说出组件库 API 设计的 5 条原则,识别"参数爆炸"的过度设计
  • [ ] 说明为什么组件库应该读 ThemeExtension 而不是硬编码颜色
  • [ ] 列出发布前的完整自检清单(analyze / format / test / dry-run)
  • [ ] 解释 --dry-run 会检查什么,以及发布后能否撤销
  • [ ] 说出语义化版本三类变更对应的版本位
  • [ ] 说明私有包如何防止误发布(publish_to: none)
  • [ ] 针对 HaierEnergy 给出抽离优先级建议

十一、常见误区 ​

误区正解
"组件复用是好事,越早抽越好"❌ 过早抽象比重复更糟。等第 3 次复制再抽
"参数越多越灵活"❌ 参数 >7 个就是过度设计,改用组合(child)
"组件里直接调接口取数据"❌ 组件应无状态、纯展示,状态由外部传入
"颜色写死在组件里"❌ 走 ThemeExtension,否则换肤即失效
"path 依赖也能发布"❌ 发布前必须改成版本依赖,下游拉不到你的本地路径
"发布后可以删掉重发"❌ pub.dev 不可删除,只能标记撤回
"1.0.0 之前随便改"⚠️ 0.x.x 阶段确实允许破坏性变更,但一旦有下游用户就要守规矩
"私有包不用管发布安全"❌ 必须加 publish_to: none,否则手滑就发到公网了
"抽包能提升性能"❌ 抽包是为了复用和维护性,不是性能优化手段
"业务流程组件也抽出来"⚠️ 耦合业务的组件抽出来反而难改,留在项目内

相关文档 ​