1. 为什么要把 peanut 搬进鸿蒙生态
做过 Flutter Web 部署的人应该都有这种体会:本地跑flutter build web只是一条命令的事,但真正把产物推到线上,往往要手动完成一串脏活——切分支、清空旧文件、复制build/web目录、强制推送、再切回开发分支。这套流程重复三五次还能忍,要是每周发版、多环境并行,光是整理分支就能耗掉半天时间,而且特别容易出错。我见过不止一次因为误删文件、推错分支导致线上页面直接挂掉的现场。
peanut 这个三方库就是在解决这个问题。它把“构建 Web 产物、自动提交到部署分支、推送到远程仓库”这几件事封装成一条命令,底层自动完成 git 分支操作,不需要开发者手动切换上下文。传统的用法是直接服务 GitHub Pages 这类静态托管场景,而这次我们要做的,是把它适配到鸿蒙应用生态里,让鸿蒙侧的前端团队也能用同一套极简流程,把静态页面推送到自己的部署底座上。
鸿蒙化适配不是简单换一个编译目标,核心要处理的其实是三个层面的问题:
- 构建阶段的命令差异:Flutter 在鸿蒙侧的工程结构和标准 Flutter Web 工程不完全一致,peanut 内部默认执行的构建命令需要调整。
- Git 操作语义的一致性:peanut 是一个强依赖 git 操作流的工具,鸿蒙化项目里如果使用不同的远端托管平台,分支命名、推送协议、认证方式都要兼容。
- 分支治理模型的落地:peanut 的分支策略默认是“生成分支即部署分支”,而在鸿蒙多人协作场景下,分支治理必须更精细,避免“一个人推送覆盖所有人的产物”。
这篇文章不会只停留在怎么装依赖、怎么跑命令,而是会把适配过程中涉及的核心机制拆开讲清楚。我会按照实际操作的顺序,从设计思路、参数细节、完整流程、问题排查四个维度展开。如果你正在做 Flutter Web 工程化,或者计划把 Web 产物接入鸿蒙应用底座,这篇文章可以直接当操作手册用。
2. 整体设计与拆解:一条命令背后的三块积木
2.1 peanut 的运行模型:构建、提交、推送三段式
先看清楚 peanut 到底干了什么。它本质上是一个命令行工具,在 Flutter 项目里通过dart run peanut触发,内部依次执行:
- 读取项目配置,包括输出目录、部署分支名、远端地址。
- 执行 Flutter Web 构建命令,生成
build/web目录。 - 在当前 git 仓库里临时创建一个独立分支,把构建产物复制进去。
- 提交并推送这个分支到远端。
- 恢复原来的分支状态。
用生活化的类比来说,peanut 就像一个跑腿小哥:你告诉他“把这份文件送到街角那家店”,他会自己规划路线、自己打包、自己交到店员手里,最后回来告诉你送完了。你不需要亲自出门、不需要知道店在几楼、也不需要纠结走哪条路。
鸿蒙化适配的关键,就在于这个“跑腿路线”要重新规划。默认情况下 peanut 会调用flutter build web,并且把产物目录硬编码为build/web。鸿蒙侧如果使用自定义的构建脚本,或者产物目录有偏移,就必须告诉 peanut 新路线。
2.2 第一块积木:构建命令与产物目录的可配置化
peanut 支持在命令行里传入-D参数指定 dart define,也可以直接通过--flutter-command覆盖默认的构建命令。但真正影响鸿蒙化适配的,是构建产物路径。
在标准 Flutter 工程里,flutter build web的产物固定输出到build/web。鸿蒙侧如果引入了类似“多端产物归一化”的构建流程,产物可能被重定向到build/web_harmony或者某个共享目录。这时候 peanut 默认的产物识别逻辑就失效了,需要用--web-out参数显式指定实际目录,或者在 peanut 的配置脚本里提前做路径映射。
实操层面,我的建议是不要在命令行里拼太多参数,而是使用 peanut 的脚本配置方式(在tool/peanut.sh这类脚本里统一管理)。原因有两个:一是命令行参数太长容易出错,二是团队协作时,脚本文件可以进代码仓库做版本管理,谁改了配置、为什么改,都有迹可循。
# 推荐的做法:用脚本封装 peanut 调用 flutter pub global run peanut \ --web-out build/web_harmony \ --flutter-command "fledge build web" \ --branch harmony-pages这里--flutter-command我用了fledge build web作为示例,实际鸿蒙化 Flutter 工程可能仍然用flutter build web,关键是只要你改了构建方式,就必须同步到这个参数上,否则 peanut 推上去的还是旧产物。
2.3 第二块积木:git 分支操作的模板化防腐
peanut 在背后做的 git 操作,是适配中最容易踩坑的地方。它内部会执行类似这样的逻辑:
- 切换到部署分支(默认是
gh-pages)。 - 如果分支不存在,基于当前分支创建一个孤儿分支(orphan branch)。
- 把构建产物复制到工作区。
- 提交,推送。
问题出在“如果分支不存在”这个分支创建策略。在鸿蒙场景下,如果你的远端仓库已经有同名分支,而且这个分支是从某个旧项目的目录结构继承来的,peanut 默认的清空逻辑可能把不该删的.gitignore、CNAME、自定义域名配置全部干掉。
我的方案是:在接入 peanut 之前,手工初始化一次部署分支的基线结构。比如先把CNAME、_headers、.nojekyll这类站点配置文件放进去并提交,之后每次 peanut 推送就只增量更新build目录里的内容,站点配置不会被覆盖。
# 手工初始化部署分支(只做一次) git checkout --orphan harmony-pages git rm -rf . echo "example.com" > CNAME echo "/*.map" > .gitignore echo "" > .nojekyll git add . git commit -m "chore: init harmony pages baseline" git push origin harmony-pages之后 peanut 的每次推送都基于这个基线进行,不会再出现“推一次就丢一次站点配置”的尴尬局面。
2.4 第三块积木:分支治理模型,而不是简单推一次
标题里提到的“极致分支治理”,在鸿蒙化场景下具体体现为一个三级分支模型:
master/main:开发主干,所有功能分支从这里分出。release/web-x.y.z:每次发布前从主干拉出的发布分支,构建验证通过后,再由 CI 或本地工具触发布署。harmony-pages:专门的部署分支,peanut 只往这个分支写构建产物。
有些团队还想保留“每个大版本一个历史部署分支”的习惯,比如harmony-pages-v1、harmony-pages-v2。这也没问题,但我的建议是:日常自动化推送统一走harmony-pages,历史版本分支只在需要回滚时手动推送一次。如果每个版本都固定一个部署分支,很容易出现“某次操作推错了分支,线上白屏”的故障。
peanut 本身支持通过--branch参数指定任意推送目标,所以分支治理策略完全可以在流程层面控制,不需要改动 peanut 源码。
3. 核心细节解析与实操要点
3.1 环境变量的注入与认证配置
peanut 推送远端依赖 git 凭据。本地开发时通常已经配置好 SSH key,但在鸿蒙侧的 CI 环境里,往往是静态 token 方式。我的习惯是在 CI 配置里显式声明环境变量,并确保 peanut 脚本能读取到。
# 伪代码,示意 CI 环境变量配置 env: GH_TOKEN: ${{ secrets.PAGES_TOKEN }} GIT_AUTHOR_NAME: deploy-bot GIT_AUTHOR_EMAIL: bot@example.com GIT_COMMITTER_NAME: deploy-bot GIT_COMMITTER_EMAIL: bot@example.com要注意的是,peanut 底层走的是 git 命令,它不一定认GH_TOKEN。在鸿蒙化实践里,更稳妥的方案是在 CI 步骤里先配置 git 全局用户,再把 token 写入 remote URL,或者用GIT_ASKPASS机制。
git config --global user.name "deploy-bot" git config --global user.email "bot@example.com" git remote set-url origin "https://user:${GH_TOKEN}@host/org/repo.git"这步操作只要做一次。之后 peanut 推送时就不需要人为干预认证,也不会因为 CI 环境下没有 SSH key 而中断。
3.2 baseHref 与资源路径的鸿蒙化适配
鸿蒙应用的 Web 容器加载静态页面时,对资源路径的解析方式和普通浏览器略有差异。如果你的 Flutter Web 页面最终是嵌入在鸿蒙 Web 组件里展示,而不是独立域名访问,那么 Flutter 构建时的baseHref一定要设置正确。
peanut 的--flutter-command参数给了我们一个注入自定义构建参数的入口。比如构建时加上--base-href=/web/,这样页面内部所有资源请求都会带上/web/前缀,和鸿蒙容器里约定的资源映射路径保持一致。
# 带 baseHref 的 peanut 调用示例 flutter pub global run peanut \ --flutter-command "flutter build web --base-href=/web/" \ --web-out build/web \ --branch harmony-pages这里有个容易忽视的细节:修改baseHref之后,本地直接打开index.html可能会白屏,因为file://协议下没有/web/这个路径。所以验证时一定先起本地静态服务,判断路径是否正确。
3.3 产物目录中的敏感文件过滤
Flutter Web 构建产物里经常包含.map源码映射文件。调试时这玩意儿很有用,但部署到线上等于把源码结构直接暴露了。peanut 默认会把整个build/web目录推上去,所以必须在部署分支的.gitignore里加规则,或者在推送前自动清理。
我更推荐在构建后、推送前加一个清理步骤,因为.gitignore只影响新增文件,旧版本推上去的.map文件还留在 git 历史里。
# 推送前的产物清理 find build/web -name "*.map" -type f -delete这个清理步骤建议写进 peanut 的封装脚本,每次构建后自动执行。配合前面的部署分支基线配置,整个流程就能做到“每次推送的产物都是干净、完整、可回滚的”。
3.4 自定义域名与站点配置文件的保护
部署分支上的CNAME文件是很多静态站点的命根子。如果你原本在用其他静态托管服务,现在要切到鸿蒙 Web 容器,域名解析和容器访问路径可能不完全等价。但不管怎样,只要部署分支里存在CNAME,peanut 的孤儿分支重建操作就很可能把它覆盖掉。
所以实操上,我会在 peanut 脚本里加一个后置检查:推送完成后,校验远端分支上的CNAME内容和本地基线是否一致,不一致就自动修复并追加一次提交。这个检查逻辑虽然简单,但真的能避免很多线上故障。
4. 实操全过程:一次完整的鸿蒙化适配记录
4.1 环境准备与版本核对
开始之前,先确认本机环境:
- Flutter SDK 版本:建议 3.x 及以上,鸿蒙侧 Flutter 工程一般会附带自己的 toolchain。
- Dart 环境:peanut 是以 Dart 脚本方式运行的,需要确保
dart命令可用。 - Git 版本:2.20 以上,低版本对
git switch等命令支持不完整。
鸿蒙工程项目结构上,建议按照“标准 Flutter 工程 + 鸿蒙壳工程”的方式组织。peanut 只需要在 Flutter 工程根目录运行,它通过读取pubspec.yaml获取项目名,不关心鸿蒙壳工程的目录布局。
4.2 配置文件的整理与参数核对
进入实际项目,我先把 peanut 相关的配置整理到一个脚本里。这里有一个便于维护的做法:把命令行参数抽成脚本变量,这样即使后期要调整部署分支名,也只需要改一处。
#!/usr/bin/env bash set -euo pipefail PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" WEB_OUT="$PROJECT_DIR/build/web" DEPLOY_BRANCH="harmony-pages" REMOTE_NAME="origin" cd "$PROJECT_DIR" # 1. 构建 Web 产物 flutter build web --base-href=/web/ # 2. 清理源码映射文件 find "$WEB_OUT" -name "*.map" -type f -delete # 3. 调用 peanut 推送 flutter pub global run peanut \ --web-out "$WEB_OUT" \ --branch "$DEPLOY_BRANCH" \ --remote "$REMOTE_NAME"这组脚本在我的实际项目里跑了很久,稳定可靠。set -euo pipefail这里尤其重要:任何一个环节失败,脚本都会立即退出,不会带着半成品继续推送。
4.3 一次典型推送的命令行实录
下面是一次成功的推送过程(关键输出已精简):
$ ./tool/deploy_web.sh [INFO] Generating web assets... [INFO] Building with base href: /web/ [INFO] Cleaning source maps... [INFO] peanut: Running Flutter build... [INFO] peanut: Switching to branch: harmony-pages [INFO] peanut: Copying build output... [INFO] peanut: Committing changes... [INFO] peanut: Pushing to origin/harmony-pages... [INFO] peanut: Restoring original branch: feature/harmony-support [SUCCESS] Deployment complete.整个过程大概一分半钟左右,比手工操作快得多。而且因为是全自动的,不存在“忘记复制某个目录”的情况。
4.4 验证线上产物与分支状态
推送完成后,检查三件事:
- 远端分支的提交记录是否更新了。
- 线上地址能否正常访问,重点看页面资源路径是否和
baseHref一致。 - 本地分支状态是否已经切回开发分支。
git checkout feature/harmony-support git log --oneline -1 git ls-remote origin harmony-pages如果远端分支的最新提交时间戳和刚才推送的时间一致,说明推流成功。这时候哪怕线上页面还没刷新,也可以确认部署流程本身是通的。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
peanut 报错branch not found | 部署分支从未初始化 | 按本文 2.3 节手工初始化一次基线分支 |
| 推送成功后线上页面 404 | baseHref与容器路径不匹配 | 在 Flutter 构建命令里追加--base-href |
| 部署分支越来越臃肿 | 历史构建产物不断叠加 | 检查部署分支 .gitignore,并定期重建基线分支 |
| CI 推送报权限错误 | 远端认证方式与 CI 环境不匹配 | 改用 token 写入 remote URL |
| 本地构建后手动打开 index.html 白屏 | baseHref导致本地 file 协议无法解析 | 使用python3 -m http.server起本地服务 |
源码.map文件泄漏 | 未在推送前清理产物 | 在封装脚本中统一删除.map |
5.2 分支被占用的现场还原
有一段时间,我所在的项目组在同一个仓库里既放鸿蒙壳工程,又放 Flutter Web 工程。某个同事直接用git push origin master把代码推到主干,结果部署分支harmony-pages被远端规则保护,他误以为推送失败,又执行了强推。最后的结果是,线上页面回退到几个版本之前,所有缓存全部失效。
排查过程其实不复杂:先看远端分支保护规则,确认强推被拒绝;再看 peanut 推送时用的 token 权限,发现这个 token 有force_push权限。处理方式是重新生成一个低权限 token,只允许非强推方式更新部署分支。
5.3 缓存策略与 sync 时机
鸿蒙容器加载静态页面时,可能存在缓存层。如果 peanut 推送成功,但页面内容一直不变,先别急着怀疑 peanut,先确认缓存策略。
我通常在部署分支的_headers文件里显式设置缓存控制字段:
/* Cache-Control: no-cache这是为了让每次部署后,客户端都能尽快拿到新版本。如果你的页面在鸿蒙壳里只是一个 Web 组件,不需要强缓存来优化加载性能,这个设置可以保留;如果页面访问量大,可以把Cache-Control调整为max-age=3600,但那样每次发版后需要手动刷新缓存。
5.4 多环境并行的分支隔离技巧
团队里如果同时维护测试环境和生产环境,peanut 默认的“一个部署分支”模型可能会互相干扰。我的做法是引入环境后缀:
harmony-pages:生产环境产物。harmony-pages-staging:测试环境产物。
在 peanut 调用脚本里通过环境变量区分:
DEPLOY_BRANCH="harmony-pages" if [ "$ENV" = "staging" ]; then DEPLOY_BRANCH="harmony-pages-staging" fi这样两套环境互不干扰,而且推送逻辑完全复用。唯一要注意的是,这两个分支访问路径不同,baseHref也要对应调整,否则资源加载会乱。
5.5 断网、中断与半成品状态恢复
peanut 执行到一半断网,是一个很恶心但必须面对的场景。因为它的分支切换逻辑是:切到部署分支 → 复制产物 → 提交 → 推送 → 切回开发分支。如果在“复制产物”之后、提交之前断网,你的当前工作区会停留在部署分支上,而且带着未提交的产物文件。
恢复步骤:
git switch feature/harmony-support git branch -D harmony-pages git checkout orphan 的替代方案更稳妥的做法是:每次 peanut 推送前,先在脚本里自动保存当前分支名,并在脚本末尾显式恢复。如果脚本中途崩溃,至少可以通过读取保存的文件知道原来在哪个分支。
6. 扩展:从单点部署到“一键直达”的数字化底座
6.1 把 peanut 集成进鸿蒙侧的服务编排
适配完成之后,peanut 解决的不只是“部署”这一件事。在鸿蒙应用里,前端静态资源往往和其他服务(比如配置中心、离线包管理)紧密配合。peanut 推送产物的动作可以作为 CI 流水线的一个环节,与其他自动化任务串联。
举例来说,推送完成后,可以自动触发一个 Webhook,通知鸿蒙侧的服务编排模块“有新的页面版本可用”,由编排模块决定是静默更新还是提示用户刷新。这样一来,部署的终点不再是托管服务器,而是整个应用运行时的状态管理系统,也就是标题里说的“数字化底座”。
6.2 分支治理与版本回滚的联动
peanut 的推送分支可以打 tag。每次成功部署后,在部署分支上打个版本 tag,例如web-preview-20240601。这样出问题时,只需要把分支重置到对应 tag,再强推一次,就能快速回滚。
git tag "web-$(date +%Y%m%d-%H%M%S)" harmony-pages git push origin --tags这套机制让“极致分支治理”不再是一个空话:每个部署产物都有对应的 tag,每次回滚都有据可查。尤其是多人协作的项目,分支合并历史乱了以后,tag 就是一个稳定的回溯锚点。
6.3 团队协作时的角色隔离与权限边界
最后是权限治理的层面。peanut 推送时最好使用专门的部署账号或 bot token,而不是团队成员的私人 token。这样即使有人离职、token 失效,也不会影响部署流水线。同时在远端仓库层面,给部署分支配置“禁止强推”策略,从机制上防止意外覆盖历史版本。
我见过一个团队,因为所有开发人员共用同一个高权限 token,某次误操作把部署分支强推成空分支,导致线上服务中断小半天。这种事故完全可以靠本文提到的“最小权限 + 分支保护 + 基线分支”这套组合拳避免掉。
整体走完一遍之后,我的体会是:peanut 鸿蒙化适配本身没什么神秘技术,难的是把构建、推送、分支治理这几个环节在自动化脚本里做到“可重复、可回滚、可审计”。把这三个属性都处理好了,Web 部署这件事就能真正成为鸿蒙应用体系里一个低调但稳定的基石。如果你也在做类似的适配,建议先从一条脚本命令开始,把手工操作逐步替换成自动化,再慢慢加入分支保护和权限治理,每一步都能看到实实在在的效率提升。