Flutter - Melos Pub workspaces 实践

欢迎关注微信公众号:FSA全栈行动 👋

一、前言

为解决 App 代码臃肿、编译耗时的问题,我们进行了分包重构,核心思路如下:

  1. 业务分包 :将不同业务线的代码拆分成独立的包,开发者只需聚焦于各自包内的 example 工程进行开发,从而提升编译和运行效率。
  2. 功能沉淀 :把跨业务复用的功能(包括基础业务和非业务功能)也抽离成独立的包,逐步让主 App 轻量化为一个"空壳",负责集成所有模块。
  3. 依赖管理 :业务包之间使用 git 依赖,指向 master 分支;而非业务的功能包则发布到自建的 unpub 平台,通过版本号管理。
yaml 复制代码
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_overridespath 方式指定本地依赖,在多个 IDE 窗口间切换,并耗费大量时间处理依赖冲突,这在紧张的工期中尤其痛苦。

因此,在官方的 Pub workspaces 方案出现之前,Melos 的出现有效解决了这些痛点。

二、Melos

Melos 是一个管理多个项目的 CLI 工具,只需要在 melos.yaml 中的 packages 下声明各个包,如下所示

yaml 复制代码
# 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 内容如下

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

除此之外,它还提供了脚本功能,方便我们在各个包中去并发执行命令,如下所示

yaml 复制代码
# 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 功能强大,但它仍有两个核心缺陷:

  1. 依赖冲突 :由于每个包都维护着独立的 pubspec.lock 文件,Melos 无法从根本上保证所有包的依赖版本一致。
  2. 内存占用Dart 分析器会为每个包创建独立的分析上下文,导致了额外的内存开销。

三、Pub workspaces

Dart 3.6.0 的时候,官方带来了 Pub workspaces,解决了上述问题,它的优缺点总结如下

优点

优点 描述
统一依赖管理 所有工作区的包共享一个 pubspec.lock 文件,确保依赖版本一致性,避免版本冲突。
性能优化 Dart 分析器为整个工作区创建单一分析上下文,减少内存占用,提升大型仓库的分析性能。
简化操作 只需在仓库根目录运行一次 dart pub get,即可为所有工作区包获取依赖。
自动本地解析 工作区内包之间的相互依赖会自动解析到本地版本,无需手动配置 path 依赖。
灵活的依赖覆盖 支持在根 pubspec.yamlpubspec_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 实现的 MonorepoMelos 并不是必要的,只是它提供的脚本功能和 Hook 实在是太方便了,有助于提升效率,所以建议搭配使用!

下面我们进入实战

五、实战

环境要求

Dart 版本需 >=3.6.0,对应 Flutter 版本需 >=3.27.0

全局安装 Melos

shell 复制代码
dart pub global activate melos "7.0.0-dev.8"

如果你之前有安装过 Melos,可以先卸载再安装

csharp 复制代码
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,内容如下

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 更加干净,如下所示

yaml 复制代码
workspace:
  - apps/**
  - packages/**

调整所有工程包位置,如壳工程存放至 apps 目录,其它包存放至 packages

工作区结构

shell 复制代码
.
├── 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

如下所示

yaml 复制代码
# pubspec.yaml
environment:
 sdk: ">=3.6.0 <4.0.0"
resolution: workspace

注意,这些包可以有 dependency_overrides,但不可以同时对同一个包进行重写,否则会冲突!所以建议将这些重写统一放到 lxf_workspacepubspec.yaml中。

启动

执行 melos bs

注意:这里再强调一遍,melos bs 不是应用 Pub Workspaces 的必要流程,使用 Melos 的主要原因是为了使用其脚本功能和 Hook 来提效,如果你不需要这些,也可以直接使用 flutter pub get

javascript 复制代码
❯ 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 进行重写

yaml 复制代码
# pubspec.yaml
...
dependency_overrides:
  scrollview_observer: ^1.26.2
  # chat_bottom_container:
  #   path: packages/chat_bottom_container

workspace:
  ...

在完成这些调整后,后续的开发流程还是跟原先一样,只是现在统一在一个 IDE 窗口中操作罢了,这里就不再赘述。

六、最后

以上便是基于将所有内容(workspaceappspackages)都上传至一个大型仓库,并统一管理的 Monorepo 方案的实践。

而对于想继续多仓库管理工程包的我来说,还需要对该方案进行改造,因为我觉得分久必合,合久必分是迟早的事,再加上我也比较懒~

好了,下一篇来讲讲我的本地 Monorepo 的 "拼好包" 方案和一些优化。

资料

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
z10_144 分钟前
多协议网关架构
架构
Hilaku5 分钟前
就因为package.json里少了个^号,我们公司赔了客户十万块
前端·javascript·npm
岛屿旅人7 分钟前
英国国防部推进本土化开放架构建设
网络·人工智能·安全·web安全·架构
晴殇i12 分钟前
尤雨溪创立的 VoidZero 完成 1250 万美元 A 轮融资,加速整合前端工具链生态
前端·vue.js
一大树20 分钟前
MutationObserver 完整用法指南
前端
一晌小贪欢22 分钟前
【Html模板】赛博朋克风格数据分析大屏(已上线-可预览)
前端·数据分析·html·数据看板·看板·电商大屏·大屏看板
墨寒博客栈25 分钟前
Linux基础常用命令
java·linux·运维·服务器·前端
野生龟35 分钟前
designable和formily实现简单的低代码平台学习
前端
路多辛42 分钟前
为什么我要做一个开发者工具箱?聊聊 Kairoa 的诞生
前端·后端