Flutter - Melos Pub workspaces 实践
# 一、前言
为解决 App 代码臃肿、编译耗时的问题,我们进行了分包重构,核心思路如下:
- 业务分包:将不同业务线的代码拆分成独立的包,开发者只需聚焦于各自包内的
example
工程进行开发,从而提升编译和运行效率。 - 功能沉淀:把跨业务复用的功能(包括基础业务和非业务功能)也抽离成独立的包,逐步让主
App
轻量化为一个“空壳”,负责集成所有模块。 - 依赖管理:业务包之间使用
git
依赖,指向master
分支;而非业务的功能包则发布到自建的unpub
平台,通过版本号管理。
cache:
version: ">=1.0.0 <2.0.0"
hosted:
url: http://unpub.lxf.dev/
package_a:
git:
url: git@code.gitlab.com:lxf/package_a.git
ref: master
分包后虽然利于单个工程的独立开发,但一旦涉及跨包联调,就会变得非常低效。
开发者需要手动修改 dependency_overrides
以 path
方式指定本地依赖,在多个 IDE
窗口间切换,并耗费大量时间处理依赖冲突,这在紧张的工期中尤其痛苦。
因此,在官方的 Pub workspaces
方案出现之前,Melos
的出现有效解决了这些痛点。
# 二、Melos
Melos
是一个管理多个项目的 CLI
工具,只需要在 melos.yaml
中的 packages
下声明各个包,如下所示
# melos.yaml
packages:
- packages/package_a
- packages/package_a/example
- packages/package_b
- packages/package_b/example
执行 melos bs
即可自动在各个包中创建 pubspec_overrides.yaml
并重写必要的包依赖,顺带执行 flutter pub get
。
生成的 pubspec_overrides.yaml
内容如下
# pubspec_overrides.yaml
# melos_managed_dependency_overrides: package_a ...
dependency_overrides:
# 同步 pubspec.yaml 中的 dependency_overrides
scrollview_observer: ^1.26.2
# 如果只依赖了 package_a,则只重写 package_a 依赖
package_a:
path: ../../packages/package_a
除此之外,它还提供了脚本功能,方便我们在各个包中去并发执行命令,如下所示
# melos.yaml
scripts:
fluttergen:
# 各个包都执行
exec: fluttergen
pod_install:
exec: pod install --project-directory=ios
packageFilters:
# 只在各个包的 example 中执行
scope: "*example"
iconfont:
exec: sh ./run.sh
packageFilters:
# 只在 package_a 中执行
scope: package_a
pub_upgrade:
run: flutter pub upgrade
exec:
# 设置并发数
concurrency: 1
可配置执行范围,比如只在某个包或各个包的 example
下执行命令;配置执行的并发数等。定义的脚本通过 melos run
去执行,如 melos run iconfont
。
还可以做到自动生成 CHANGELOG
和发包到 pub
等,有兴趣的小伙伴可自行到官网了解更多的功能。
尽管 Melos
功能强大,但它仍有两个核心缺陷:
- 依赖冲突:由于每个包都维护着独立的
pubspec.lock
文件,Melos
无法从根本上保证所有包的依赖版本一致。 - 内存占用:
Dart
分析器会为每个包创建独立的分析上下文,导致了额外的内存开销。
# 三、Pub workspaces
在 Dart 3.6.0
的时候,官方带来了 Pub workspaces
,解决了上述问题,它的优缺点总结如下
# 优点
优点 | 描述 |
---|---|
统一依赖管理 | 所有工作区的包共享一个 pubspec.lock 文件,确保依赖版本一致性,避免版本冲突。 |
性能优化 | Dart 分析器为整个工作区创建单一分析上下文,减少内存占用,提升大型仓库的分析性能。 |
简化操作 | 只需在仓库根目录运行一次 dart pub get ,即可为所有工作区包获取依赖。 |
自动本地解析 | 工作区内包之间的相互依赖会自动解析到本地版本,无需手动配置 path 依赖。 |
灵活的依赖覆盖 | 支持在根 pubspec.yaml 或 pubspec_overrides.yaml 中进行依赖覆盖。 |
便捷的命令执行 | 可以使用 -C 选项在特定工作区包中执行 pub 命令,无需切换目录,如:flutter pub get -C apps/app_a 。 |
清晰的包列表 | dart pub workspace list 命令可列出所有工作区包及其路径。 |
# 缺点
缺点 | 描述 |
---|---|
迁移成本 | 现有 Monorepo 迁移到 Pub workspaces 需要修改 pubspec.yaml 文件并确保 SDK 约束(至少 ^3.6.0 )。 |
“游离”的 pubspec.yaml 文件 | 工作区根目录与工作区包之间存在非工作区成员的 pubspec.yaml 文件会导致 pub get 报错。 |
依赖覆盖限制 | 一个包只能被覆盖一次。 |
版本约束匹配 | 即使使用本地版本,包之间的依赖版本约束仍需匹配。 |
发布行为差异 | 工作区包发布到 pub.dev 时,将使用托管版本的依赖,而非工作区内的本地版本。 |
Pub Workspaces
的一个核心原则:整个工作区只有一个统一的依赖解析。
这意味着在工作区的根目录会有一个全局的 pubspec.lock
和一个全局的 .dart_tool/package_config.json
文件,所有工作区内的包都使用这两个文件来管理它们的依赖。
# 四、迁移优化成效
这是从 Melos
迁移至 Melos
+ Pub Workspaces
后的提升数据
- 设备:M1 16G
- 方式:集成
37
个包(包含example
)并等待代码分析完成
优化前
优化后
优化前 | 优化后 | 提升 | |
---|---|---|---|
CPU 时间 | 9:35.46 | 7:43.69 | 19.4% |
内存占用 | 12.23 GB | 2.62 GB | 78.6% |
这里说一点,基于
Pub Workspaces
实现的Monorepo
,Melos
并不是必要的,只是它提供的脚本功能和Hook
实在是太方便了,有助于提升效率,所以建议搭配使用!
下面我们进入实战
# 五、实战
# 环境要求
Dart
版本需 >=3.6.0
,对应 Flutter
版本需 >=3.27.0
全局安装 Melos
dart pub global activate melos "7.0.0-dev.8"
如果你之前有安装过 Melos
,可以先卸载再安装
dart pub global deactivate melos
那为什么要指定 7.0.0-dev.8
这个版本?这是因为我们现在使用的 Flutter
版本是 3.29.3
,对应的 Dart
版本是 3.7.2
,结合 Melos
的当前部分版本要求,如下所示
Version | Min Dart SDK | Flutter 版本 |
---|---|---|
7.1.1 | 3.9 | 3.35.0 |
7.0.0 | 3.9 | 3.35.0 |
7.0.0-dev.10 | 3.8 | 3.32.0 |
7.0.0-dev.8 | 3.6 | 3.27.0 |
所以只能挑个 7.0.0-dev.8
先用用,如果你已经用上了 Flutter 3.35.0
,则可使用当前最新正式版 7.1.1
。
# 初始化仓库
创建 workspace
仓库(当然,你也可以基于现有仓库进行改造),名字你随意,这里我以 lxf_workspace
为例。
创建 pubspec.yaml
,内容如下
name: lxf_workspace
publish_to: none
environment:
sdk: ^3.6.0
dev_dependencies:
# 与全局安装的保持一致
melos: 7.0.0-dev.8
workspace:
- apps/app_a
- packages/package_a
- packages/package_a/example
- packages/package_b
- packages/package_b/example
从 Dart 3.11
开始支持 globs
语法,可以让 pubspec
更加干净,如下所示
workspace:
- apps/**
- packages/**
调整所有工程包位置,如壳工程存放至 apps
目录,其它包存放至 packages
。
# 工作区结构
.
├── README.md
├── apps
│ └── app_a
├── melos.yaml
├── packages
│ ├── package_a
│ ├── package_b
│ ├── package_c
│ └── ...
├── pubspec.lock
└── pubspec.yaml
文件(夹) | 作用 |
---|---|
apps | 存放壳工程,或其它 app 工程 |
packages | 存放各个仓库的工程,如:业务工程,组件包 |
pubspec.yaml | 声明 workspace ,重写依赖,定义 Melos 脚本 |
# 调整 pubspec.yaml
lxf_workspace
中涉及到的包(即 workspace
下声明的那些),其 pubspec.yaml
都需要做如下两个调整
environment.sdk
需>=3.6.0
- 新增
resolution: workspace
如下所示
# pubspec.yaml
environment:
sdk: ">=3.6.0 <4.0.0"
resolution: workspace
注意,这些包可以有 dependency_overrides
,但不可以同时对同一个包进行重写,否则会冲突!所以建议将这些重写统一放到 lxf_workspace
的 pubspec.yaml
中。
# 启动
执行 melos bs
注意:这里再强调一遍,
melos bs
不是应用Pub Workspaces
的必要流程,使用Melos
的主要原因是为了使用其脚本功能和Hook
来提效,如果你不需要这些,也可以直接使用flutter pub get
。
❯ melos bs
melos bootstrap
└> /Users/lxf/lxf_workspace
Running "flutter pub get" in workspace...
> SUCCESS
Generating IntelliJ IDE files...
> SUCCESS
-> 5 packages bootstrapped
根据上述的 Pub Workspaces
的核心原则,它会将所有包的 pubspec.lock
都删除,所有包都会新增 .dart_tool/pub/workspace_ref.json
并指向根,即 lxf_workspace
目录。
如果你需要更新依赖,则直接在 lxf_workspace
目录下执行 flutter pub upgrade
即可,这些跟原来的一样。
如果你想重写一些第三方库,可以在 pubspec.yaml
中的 dependency_overrides
进行重写
# pubspec.yaml
...
dependency_overrides:
scrollview_observer: ^1.26.2
# chat_bottom_container:
# path: packages/chat_bottom_container
workspace:
...
在完成这些调整后,后续的开发流程还是跟原先一样,只是现在统一在一个 IDE
窗口中操作罢了,这里就不再赘述。
# 六、最后
以上便是基于将所有内容(workspace
、apps
和 packages
)都上传至一个大型仓库,并统一管理的 Monorepo
方案的实践。
而对于想继续多仓库管理工程包的我来说,还需要对该方案进行改造,因为我觉得分久必合,合久必分是迟早的事,再加上我也比较懒~
好了,下一篇来讲讲我的本地 Monorepo
的 "拼好包" 方案和一些优化。
# 资料

- 01
- Flutter - 详情页初始锚点与优化08-24
- 02
- Flutter - 详情页 TabBar 与模块联动?秒了!08-17
- 03
- Flutter - 使用本地 DevTools 验证 SVG 加载优化08-07