Flutter鸿蒙混合开发:用sass_builder将SCSS接入编译链,根治WebView样式失控
2026/9/23 6:38:26 网站建设 项目流程

凌晨一点,我还在改第四版栅格类名。那个页面是典型的电商运营落地页,WebView承载,CSS 早就过万行了。商品卡片网格的类名组合几十种,断点值散落各处,设计师一句“间距再大一点”,成本就是一次全量搜索替换。

那时候我们刚从纯 Flutter 页面切到 Flutter + 鸿蒙 WebView 混合架构,样式层几近失控。改完那次 class 名之后我做了个决定:把 sass_builder 接进编译链,让 SCSS 成为样式唯一真源,CSS 由机器生成。跑通之后,光“跨度渲染网格”这一块,手写 CSS 就从 1.2 万行降到 300 行 SCSS 逻辑,编译产物 42KB 全量覆盖。

这篇文章是这次适配的完整复盘,覆盖选型理由、编译链挂载方式、跨度网格实战,以及四个真实踩坑的排查链路。如果你也在做 Flutter 鸿蒙混合开发,或者被重复 CSS 搞得心力交瘁,这篇文章应该能帮你少走不少弯路。

1. 先从我们为什么要在鸿蒙 Flutter 项目里碰 CSS 说起

1.1 混合开发场景里,CSS 是绕不过去的存在

很多 Flutter 开发者的第一反应是:Flutter 不是用 Widget 画界面的吗?为什么要碰 CSS?

答案是:只要你的页面里还有 WebView,CSS 就躲不开。

我们的鸿蒙 App 里,商品详情、运营活动页、富文本公告,全部是 Web 页面。原因很现实——这些页面更新频繁,运营团队要能随时改稿,不可能每次改动都走一次 Flutter 发版。WebView 承载这些页面,是混合开发里最成熟的方案。鸿蒙的 Web 组件能力也够用,本地资源加载、JS Bridge 调用都没问题。

但问题出在别的地方:这些 Web 页面的样式代码,是在一次次的“加需求”里野蛮生长起来的。

一个商品卡片网格,从最初 3 列,变成 4 列、6 列,再加左窄右宽、上窄下宽,再到“每行按比例混合排布”——每一轮需求都对应一批新类名。等接手的时候,光这个网格相关的 CSS 就有 1.2 万行。大部分是重复的grid-column: span Xgrid-row: span Y,只是断点不同、列数不同、嵌套层级不同。

这种代码的特征很明显:

  • 找类名靠搜索,改值靠全局替换
  • 断点散落,没有任何变量抽象
  • 加一个间距尺寸,要同时改十几个地方
  • 最致命的是——你以为你改了,但总有漏掉的那一个

我们的做法是:让这套网格式样表的生成逻辑从“人手枚举”变成“程序生成”。

1.2 三个备选方案,为什么最后选了 sass_builder

当时摆在桌面上的方案有三个:

方案集成路径增量编译Flutter 生态契合度结论
PostCSS + Tailwind 类名方案Node 脚本打包时转译一般,需要自己管理监听低,Node 工具链和 Dart 构建链是两套体系放弃
Less 命令行编译shell 脚本手动调用弱,每次全量编译,几十秒中,只能当外部工具用放弃
sass_builderDart Builder 原生挂载强,build_runner 自动增量高,跑在 Dart 虚机上,与 Flutter 构建天然同链选用

放弃 PostCSS 的原因很简单:我们的构建链是 Flutter / Dart 体系,引入 Node 工具链意味着 CI 机器上要额外维护一套 Node 环境。项目里谁都不想再背一个运行时依赖,尤其是在鸿蒙构建链路本来就比较复杂的情况下。

sass_builder 则完全规避了这个问题。它是一个纯 Dart 实现的 Builder,跑在 build_runner 体系内,不需要外部编译器和二进制依赖。Flutter 工程里加一行 dev_dependencies 就能跑。

1.3 sass_builder 解决了我们的什么核心诉求

我们需要的不是一个“能编译 SCSS 的工具”,而是一个能挂在自动化编译链上的转译节点

具体来说:

  • SCSS 文件变更后,增量编译,不能每次全量跑
  • 产物路径可控,能把 CSS 输出到鸿蒙 WebView 能访问的资源目录
  • 编译过程可编程,能通过 mixin、函数、循环批量生成样式类
  • 不引入额外的运行时依赖

sass_builder 天然满足这些条件。它本质上就是 build_runner 生态里众多 Builder 中的一个,而 build_runner 本身就是 Flutter / Dart 工程里管理代码生成的标准方案。把 Sass 编译变成 Builder 体系的一部分,意味着 SCSS 转译这件事从“外部独立环节”变成了“构建流水线里的一个普通节点”。

这就是标题里说的“整合预处理语言转译挂载节点”——本质上是在 Flutter 的编译链路里,给 Sass 转 CSS 这件事留了一个正式席位。

2. sass_builder 到底凭什么能挂在 Flutter 编译链上

2.1 它不是一个孤立的编译工具

很多人对 sass_builder 的理解是“用 Dart 写的一个 Sass 编译器”。对,但不完整。

它真正厉害的地方在于:它是一个Dart 原生 Builder。这意味着它的生命周期是由 build_runner 管理的,而不是你手动调用的。

build_runner 是 Dart 官方的代码生成引擎。它做的核心事有两件:构建一个有向无环图,理清所有 Builder 和输入文件之间的依赖关系;然后按图执行增量编译,只处理变更过的部分。

sass_builder 在这张图里的角色很简单:输入是.scss文件,输出是.css文件。当你的 SCSS 文件发生变化,build_runner 会单独重新编译那一个文件,而不是把整个项目的样式都重来一遍。

听起来可能觉得“这不就是文件监听吗”。区别在于:它不单是“监听文件变更”,而是参与到整个构建图里去。这意味着它可以和别的 Builder 组合使用——今天的我们对它的使用还比较克制,只是让它把 SCSS 变成 CSS,但理论上你可以在编译链后面再接一个 Builder 去做 CSS 压缩、加哈希指纹、生成 Dart 常量文件。这就是“挂载节点”的含义——它是一个节点,而不是一条死路。

2.2 为什么“增量编译”在鸿蒙混合开发里格外重要

在 PC 或服务端项目里,CSS 编译慢一点,忍忍就过去了。但在鸿蒙 + Flutter 混合开发的场景里,事情没那么简单。

我们的构建流程是:Flutter 代码和 Web 资源在鸿蒙侧打包成 HAP(鸿蒙应用安装包)。如果 CSS 编译是外部脚本,每次都要全量跑一遍,几十秒的时间,在开发态试一次样式,人就得等一次。这种挫败感会让人渐渐放弃“编译前先跑一遍 SCSS”的好习惯,直接手改 CSS 产物,然后源文件失同步,下游全盘崩塌。

sass_builder 的增量编译特性,让 SCSS 变更在 1 秒内完成转译,基本无感。这才是它能真正促使团队改变工作流的原因——工具链足够快,人才愿意走正道。

2.3 它是怎么输出产物的:build_to 与 source 语义

sass_builder 有一个关键配置:build_to: source

这个配置的意思是:编译产物直接写到源目录对应位置,而不是被打进 Dart 的构建缓存。默认情况下,build_runner 的产物都放在内存缓存或.dart_tool里,编译完直接进最终产物——但 WebView 需要的是一个物理存在的 CSS 文件路径,它不可能去读 Dart 的内存缓存。

设成build_to: source之后,.scss文件旁边会多出同名.css文件。结构类似:

lib/ src/ styles/ scss/ grid.scss css/ grid.css

这里就出现了一个关键决策点:CSS 产物到底落在哪个目录,才能被鸿蒙 WebView 加载?

我们踩的第一个坑,就跟这个有关。后面会有专门一节讲排查链路。

3. 把 sass_builder 接进鸿蒙构建流程的完整落地步骤

3.1 第一步:版本与工程目录规划

依赖方面,我们用的是:

dev_dependencies: build_runner: ^2.4.11 sass_builder: ^2.1.5

sass_builder2.1.x 版本对应的底层 Dart Sass 版本在 1.6x 附近,支持@use语法、模块系统、还有强大的内建函数。这里多说一句:不要为了省事把两个依赖写进dependencies,它们是纯编译期工具,放 dev 依赖就够了,这样生产产物不会带上这些包。

目录结构上,我们规划成:

lib/ src/ styles/ scss/ _variables.scss # 变量:断点、间距、颜色 _mixins.scss # 混合宏:网格、省略号、清除浮动 _grid.scss # 栅格系统核心 _components.scss # 组件级样式 main.scss # 入口文件,@use 以上所有 css/ main.css # sass_builder 编译产物

main.scss是所有样式的统一入口。业务页面只引用编译后的main.css,不直接碰_*.scss文件。这样页面哪怕只用到一两个组件,加载的是同一个全量 CSS,省去了在 WebView 里做 CSS 分包的复杂度。

3.2 第二步:build.yaml 声明,把 Builder 挂进构建图

项目根目录下没有build.yaml的话,手动建一个:

builders: sass_builder: import: "package:sass_builder/builder.dart" builder_factories: ["sassBuilder"] build_extensions: .scss: - .css auto_apply: dependents build_to: source

逐行解释这几个配置的含义:

  • builder_factories: ["sassBuilder"]:告诉 build_runner,启动这个 Builder 时该调用包里的哪个工厂函数
  • build_extensions:定义输入与输出的映射关系,.scss文件编译为同名.css
  • auto_apply: dependents:只要项目里任何依赖(包括自身模块)包含.scss文件,Builder 就自动处理,不需要手动标注
  • build_to: source:产物直接落到源码目录,而不是进入 Dart 的构建缓存

这里面有几个细节,纯粹是排坑排出来的:

细节一:build_extensions 里的输出文件也可以加路径前缀。比如想让所有.css输出到一个统一目录,可以把build_extensions写成.scss: ["css/{}.css"]。但我们实测下来,不建议这么做——{}占位符处理嵌套目录时偶尔会出问题,在鸿蒙这种资源路径本来就敏感的平台上,保持“同名同目录”最简单可靠。

细节二:不建议在build.yaml里写source过滤规则。默认情况下,Builder 会扫描整个lib目录。如果你有其他目录下的临时 SCSS 文件,也会被它扫到并尝试编译。最稳妥的是约定规则:所有 SCSS 统一放在lib/src/styles/scss/下,不允许散落。

3.3 第三步:执行编译,验证产物

配置完成后,跑一次:

flutter pub run build_runner build --delete-conflicting-outputs

--delete-conflicting-outputs参数的作用是:如果检测到已有的产物与最新编译结果存在冲突,自动删除旧产物而不是报错。多轮迭代之后这个参数几乎必带,否则 CI 上经常因为残留文件冲突而失败。

第一次跑完,观察产物。正常情况下会生成:

lib/src/styles/css/main.css

这个路径就是后面要映射给鸿蒙 WebView 的 CSS 地址。

在开发态,我们开启的是 watch 模式:

flutter pub run build_runner watch

这个进程会常驻,监听 SCSS 文件变更并增量编译。实测编译延迟基本在几百毫秒到 1 秒之间,改完保存,切到真机刷新,样式已经是新版了——这个体感对开发效率的帮助非常大。

3.4 第四步:把 CSS 产物映射给鸿蒙 WebView

鸿蒙 WebView 加载本地 CSS 的方式,不同 API 版本略有差异,但思路一致:把 CSS 文件打入 HAP 的 resources/rawfile 目录,然后在 WebView 加载 HTML 时,通过 base URL 或资源路径引到它。

我们的做法是:在鸿蒙侧的构建脚本阶段,把flutter build产出的lib/src/styles/css/main.css复制到 HAP 的资源目录。用 shell 脚本,写在构建流程里:

# 假设鸿蒙工程在 harmony/ 目录下 cp lib/src/styles/css/main.css harmony/entry/src/main/resources/rawfile/styles/main.css

这一步看着简单,但它是整个编译链里最容易出错的地方。因为 Flutter 构建和鸿蒙构建是两段流程,中间如果没有人去同步 CSS 产物,WebView 加载到的就是上一次打包的旧文件。

我们在 CI 上遇到过一次“样式改了但线上没变”的问题,最后发现就是同步步骤被跳过了。后来我们直接把这个 copy 动作写进了 Flutter 的构建脚本 hook,确保每次flutter build之后自动同步,人工干预的空间缩到最小。

3.5 一个容易忽略的点:SCSS 的 includePath

sass_builder 的在 build.yaml 里可以指定include_path参数(通过 builder_options 传),但我们实测下来,在鸿蒙平台的环境下,指定绝对路径会导致 CI 和本机行为不一致——因为 CI 的工作目录和本地开发目录往往不一样。

我们的解决方案很粗暴:所有 SCSS 使用@use 'src/styles/scss/variables'这种相对 lib 根目录的路径写法,配合 Dart 的 package 解析机制,让它自己找到模块。这样无论在本机还是在 CI 上跑的都是一套逻辑。

4. 跨度渲染网格实战:30 行 SCSS 替代 1200 行手写 CSS

4.1 “跨度渲染网格”在业务里到底是什么

标题里的“跨度渲染网格”,在我们的业务里是一个被反复复用的卡片网格布局。它长什么样?

  • 视口宽度变化时,列数响应式变化(手机 2 列、平板 4 列、桌面 6 列)
  • 每张卡片可以单独指定占几列(span 语义),比如“特价商品占整行的 2/3”
  • 部分卡片要留出左侧或右侧偏移,形成错落感

如果用手写 CSS,两个维度一组合——5 个断点 × 12 列 × 2 种动作(span 和 offset),就是 120 种组合。如果每个组合再配上 padding 和对齐,工作量就失控了。

这套需求如果只服务某一个页面,手写忍忍也能过去。但它是全站通用布局系统,后续所有营销页、专题页、商品列表页都在上面搭。这套网格的第一版,是用两天时间手工写的 CSS,后面每一轮设计稿调整都掏一份额外成本。

4.2 用 SCSS 的循环把网格类名“算”出来

核心代码长这样:

// _grid.scss $grid-columns: 12; $breakpoints: ( 'xs': 0px, 'sm': 576px, 'md': 768px, 'lg': 992px, 'xl': 1200px, 'xxl': 1400px ); // 六档断点,每档 12 列 span 类 @each $bp, $width in $breakpoints { @media (min-width: $width) { @for $col from 1 through $grid-columns { .col-#{$bp}-#{$col} { grid-column: span #{$col}; } .col-#{$bp}-offset-#{$col} { grid-column-start: #{$col + 1}; } } } }

这段代码编译之后,直接生成 6 × 12 × 2 = 144 个类名,覆盖六档断点的 span 与 offset。

页面里怎么用?比如一个 iPad 竖屏的页面上,想做一个“左侧 4 列、右侧 8 列”的卡片布局:

<div class="grid"> <div class="col-md-4">卡片A</div> <div class="col-md-8">卡片B</div> </div>

商品卡片本身的一些视觉规则,我们封装成 mixin:

@mixin card-shadow($level: 1) { @if $level == 1 { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); } @else if $level == 2 { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } } .product-card { @include card-shadow(2); border-radius: 12px; overflow: hidden; }

4.3 转译前后的代码量对比

这是我们当时实测的数据:

指标手写 CSSSCSS 编译产物
代码量约 1.2 万行(含重复类名)约 300 行编译逻辑
类名规模零散不可枚举144 个网格类 + 若干组件类
产物体积96KB42KB
改一个断点值全局搜索替换,容易漏改一行变量
新增一种卡片组合手写 N 行 CSS改用类名组合,无需新增 CSS

代码行数减少看着夸张,但更关键的是“新增成本”趋近于零。运营要一个“右侧 10 列、左侧 2 列”的极窄边栏布局,以前要写一坨规则,现在直接col-lg-10+col-lg-2两个类名套上去就行。样式的可枚举性,才是这次改造最大的收益。

4.4 为什么这套方案能扛住设计稿反复改

有一个真实案例:某次大促,运营两天内改了五版设计稿。前四版只是间距、列数的微调,换了以前,每版都要花近半天去改 CSS,而且风险极高——网格一改动,很容易把某个页面的布局冲垮。

用 SCSS 之后,变量全在最上面,间距在_variables.scss里,改一个数:

$grid-gutter: 12px;

所有页面的间距同步更新。列数变了,改$grid-columns的定义。如果只是某个区域换成非对称布局,直接改页面上的类名组合就行,CSS 层一行都不用动。

这次之后团队里基本形成了共识:凡是可枚举的样式规则,一律进 SCSS 生成;凡是只出现一次的样式,才允许手写。

5. 适配过程中踩过的四个坑与完整排查链路

5.1 坑一:CSS 产物路径与鸿蒙资源目录错位

现象:SCSS 编译正常,控制台没有任何报错,但鸿蒙 WebView 页面上样式全无,所有元素排列成一列,CSS 文件请求返回 404。

排查过程:第一步,用 DevTools 工具看 WebView 的 network 请求,发现 CSS 请求的 URL 指向的是file:///resources/rawfile/styles/main.css,而这时 rawfile 目录下根本没这个文件。第二步,检查 HAP 包的压缩内容,发现main.css压根没被打进包里。第三步,回看构建日志,发现 Flutter 构建产物里的 CSS 在lib/src/styles/css/main.css,而鸿蒙 rawfile 目录的同步命令根本没在 CI 上执行。

根因:我们在 3.4 节提到的同步步骤,只写在了本地开发文档里,没有真正写进 CI 脚本。Flutter 构建产物和鸿蒙资源目录之间缺了一道“搬运工”。

解决:把复制命令写进项目根目录的build.sh,在 Flutter 构建完成后自动执行。同时加了一个校验:如果同步后 rawfile 下不存在main.css,构建脚本直接抛错退出。用失败来倒逼流程完善——宁可构建失败,也不能放一个样式缺失的包出去。

5.2 坑二:Dart Sass 模块系统迁移,@import 全面失效

现象:某次升级sass_builder依赖之后,编译直接报错,提示@import即将不被支持,建议迁移到@use

排查过程:一开始以为是版本不兼容,回退了sass_builder版本,问题消失。但后来发现,只要依赖锁定在旧版本,就无法使用 Dart Sass 1.80 之后引入的一些新特性(比如更强大的颜色函数)。于是决定做迁移。迁移时发现项目里有 30 多个_*.scss文件全部用了@import方式互相引用,如果一次性全部改,一个全量回归测试都没时间做。

根因:这不是 bug,是 Dart Sass 官方的渐进式弃用计划。@import规则因为容易造成全局命名冲突,官方早在几年前就宣布未来会移除,1.80 版本开始正式要求迁移。

解决:分两步走。第一步,先把所有@import换成@use,这是机械操作,用 IDE 的全局替换就行。难点在于@use是模块化引用,引用方必须通过模块名前缀调用变量和 mixin,原来@import下直接写的变量名、mixin 名全都要加前缀。这步做完跑一次全量编译,把所有报错一次性修复。第二步,把_variables.scss_mixins.scss里的公开接口(变量名、mixin 名)整理成文档,要求团队新写的代码只能通过@use引模块,禁止再出现@import

留意点:如果你的项目还在用低版本 sass_builder,依赖解析可能给你装一个兼容版本的 Dart Sass,新的@use语法支持不完整,也会出怪问题。建议直接把依赖升到当前可用最新版本,一次性把语法问题暴露完。

5.3 坑三:build_runner 增量缓存导致 CSS 更新不及时

现象:开了build_runner watch模式,改了一个 SCSS 变量,保存后命令行提示编译成功,但页面上的样式完全没有变化。

排查过程:第一步,确认 watch 进程还活着,输出里确实显示编译了受影响的文件。第二步,用 cat 查看产物 CSS,发现文件内容确实是旧的——也就是编译动作发生了,但产物没更新。第三步,检查文件时间戳,产物文件的修改时间远早于代码修改时间,这说明 build_runner 认为这个文件“没有变化”,跳过了写入。

根因:build_runner 的增量缓存里记录了上一次的构建状态,如果 SCSS 文件的内容被修改后又改回去了(比如临时改一行验证效果,随后撤销),缓存会认为产物没有增量变化,直接跳过输出。还有一种更隐蔽的情况:多个 Builder 同时处理同一个文件,产物被另一个 Builder 覆盖,build_runner 的缓存认为“已经构建过”,不会重新执行我们的 Builder。

解决:最直接的命令是:

flutter pub run build_runner clean flutter pub run build_runner build --delete-conflicting-outputs

但这相当于全量重跑,在大型项目里要花不少时间。我们后来的做法是:只在怀疑缓存异常时才 clean;平时 watch 模式照常用,但每次保存后留意终端输出的编译文件名,确认目标 CSS 确实被重新生成了。另外,保证同一时间只有一个 build_runner 进程在跑。多人协作时,如果两个人各开了一个 watch,很容易出现互相覆盖缓存的情况——这一点我们的教训很深刻。

5.4 坑四:WebView 缓存把新 CSS 吞了

现象:代码和产物都对,CSS 文件也确认到了 rawfile 目录,但页面加载的仍是旧样式。开发机上,第一次加载是新的,后面改了几次,全被缓存盖住。

排查过程:第一步,检查 WebView 的 network 面板,发现加载 main.css 的状态码是 304(Not Modified)。第二步,查看响应头,没有 Cache-Control 配置。第三步,把加载 CSS 的 HTML 里引用的路径加了?v=时间戳参数,强制浏览器跳出缓存——问题即时“解决”了。

根因:鸿蒙 WebView 的缓存策略与 Chromium 系浏览器类似,对没有明确 Cache-Control 的本地资源,会按启发式规则缓存。CSS 文件名不变时,WebView 会认为资源没有更新。这个问题的本质不是 sass_builder 的锅,而是混合开发里“资源更新了但 WebView 不知道”的老难题。

解决:在 HTML 模板里,CSS 引用路径统一加版本参数化:

<link rel="stylesheet" href="styles/main.css?v=__BUILD_VERSION__">

__BUILD_VERSION__在构建时由脚本替换成时间戳或 Git commit hash。这样每次发版,CSS 都会重新加载。调试态下,我们给 WebView 开了强制禁用缓存:

// 鸿蒙侧 WebViewController 初始化时 webController.setCacheMode(CacheMode.NONE);

这个开关只建议在 debug 构建里打开,release 版本用版本号参数就足够了——强制禁用缓存会让页面每次加载都重新读取资源,白白增加首屏时间。

6. 这套编译链后续还能榨出什么价值

6.1 一套样式驱动双端:用 CSS 产物反推 Dart 常量

编译链稳定跑通之后,我们想了一个更野的玩法的念头:既然 SCSS 是所有样式的唯一真源,那能不能让 Flutter Widget 侧和 WebView 侧共用同一套样式变量?

实现方式不复杂:写一个自定义 Builder(Dart Builder 也在 build_runner 生态里),它在 sass_builder 完成转译之后,读取_variables.scss里的颜色、间距、字体大小变量,生成一个app_theme.dart文件:

class AppTheme { static const double gridGutter = 12; static const Color primaryColor = Color(0xFF2B6AE0); static const double fontSizeBody = 14; }

这样 Flutter 原生 Widget 用的是这套值,WebView 页面渲染也是这套值。设计改了一个主色,SCSS 里改一处,运行构建,两端同步生效。跨端样式一致性,从“靠自觉”变成了“靠机制”。

这个扩展涉及写自定义 Builder,门槛比单纯接 sass_builder 高一些,但收益是实打实的。等这个 Builder 在我们的下一次迭代里跑完回归,我再单独写一篇分享具体的实现细节。

6.2 给 CSS 产物加哈希指纹,干掉一类缓存问题

前面 5.4 节里用版本号参数手动解决缓存问题的方式,在需要长期维护的项目里还是不够优雅。下一步我们的方案是:在 build.yaml 里再挂一个后置 Builder,专门读取 sass_builder 生成的 CSS 文件,计算内容哈希,然后把文件名改写为main.a1b2c3d4.css这种带指纹的形态。

这样天然解决了缓存问题:文件内容变了,文件名就变了,WebView 自然加载到的是新文件。这个方案唯一要处理的是 HTML 模板里的 CSS 引用路径需要同步变更——用模板占位符 + 构建脚本替换来做,逻辑不复杂。

6.3 我个人的一些体会

整套适配做下来,我最大的感受是:编译链不是一次性体力活,它是团队里最值得投入的隐性资产。

把 sass_builder 接进鸿蒙 Flutter 混合开发链路,本质上是做了一个很小的投资,但收益每天都在发生。设计师改间距不再需要前端“全局替换”,运营加新模块不再需要等样式代码,WebView 的样式一致性问题也从“靠人盯”变成了“靠流程管”。

如果非要总结成一条方法论,那就是:当你发现一类样式在反复手写时,说明它已经从“一次性页面样式”变成了“可枚举的设计系统规则”,这时候就该把它搬进编译链,让 SCSS 替你生成。

最后再分享一个小经验:任何构建链改动,第一件事永远是在干净环境里跑通全量构建。本地跑通了不算数,CI 上能复现才算真的完成了——鸿蒙侧的构建链路长,任何一步出问题,线上要付的代价都远比本地多得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询