接手一套存量不小的 Flutter 工程最怕的不是功能写不出来而是代码哪儿都能长。业务层偷偷 import 了底层通道UI 文件里塞满了状态流转新人的 PR 里一个功能横跨六个模块——这些问题是常规 Lint 规则查不出来的因为不是格式问题是结构问题。团队今年把整套 App 往鸿蒙方向做适配这个问题被彻底放大了。鸿蒙侧的 Flutter 生态还没那么厚封装边界一旦失守后面改起来真要命。这篇想聊的是我们内部沉淀的一套 Lint 规则集 rexios_lints 在鸿蒙适配中的完整落地过程。它不是什么新框架而是把架构规范变成编译期强制性检查的一组工具链组合一半是声明式规则另一半是自定义插件规则。它解决的问题很具体在 Dart 编译阶段直接拦截跨层依赖、非法 API 调用、边界封装绕过等架构违规让不合规的代码根本走不到合入那一步。如果你也在做 Flutter 鸿蒙适配或者单纯想给团队代码库上一道工艺纪律的防线这篇的配置方法和踩坑记录应该都能直接用上。1. 为什么需要一套会咬人的 Lint 规则集1.1 存量代码失控的五个典型信号先说我们当时看到的现状。团队规模不大但仓库不小几个业务模块加起来有几十万行。做过多年 Flutter 的同学对这类问题应该不陌生。第一个信号是分层边界形同虚设。按规范UI 层只允许依赖服务层服务层才能访问基础设施。但在存量代码里随意跨层导入的情况到处都是某个页面文件直接去调底层数据库封装中间跳过了整个业务服务层。这会让改动一个底层接口时牵扯出几十个调用点。第二个信号是啥都能干的全局工具类。项目里有个 common 目录里面什么都有工具函数、网络实例、埋点封装、路由跳转甚至还有几个常量定义。新同学进来第一周大概率会把新代码塞进这个万能筐里。它就像一个城市的中央广场所有道路都往这儿汇一旦要改它的签名全城堵车。第三个信号是渠道能力绕过统一封装。当时我们接入鸿蒙推送、支付等能力社区方案不统一不同厂商的原生调用方式各有差异。如果没有约束业务代码里直接铺开一堆 platform channel 调用后期升级 SDK 或切换厂商就是一次灾难。第四个信号是异常的静默吞掉。在 UI 层到处能看到catch (e) {}这种空捕获或者只打印一行 log 就假装处理完了。这类问题在 CR 里很难一条条盯住因为每次看都不一样。第五个信号是命名与文件组织的随意性。同一类能力有人放 utils有人放 helper有人放 service再配合拼音缩写文件名索引一个模块的代码往往要翻半天。这些信号单拎出来都不致命但它们累积在一起代码库就像一个没有交通规则的城市所有人都能走所有人都走得慢。以前靠 Code Review 来拦截这些但 CR 本来就有幸存者偏差——盯得紧的模块相对干净盯不紧的地方就会失控。尤其鸿蒙适配这种需要全局铺开的工作光靠人盯绝对来不及。1.2 Lint 从风格约束到架构防线的三层递进很多人对 Lint 的理解停留在字符串必须单引号缩进必须两个空格这类风格约束上。实际上静态分析工具的能力远不止于此它完全可以承担架构治理的工作。我习惯把 Lint 的用途分成三层。第一层是风格统一层。这一层解决的是看起来一致命名风格、导入顺序、空行规则、格式化。规则属于低级劳动交给机器去跑就行人的精力不应该花在这上面。第二层是正确性兜底层。这一层解决的是是不是写错了例如使用已废弃的 API、空安全违规、无效的BuildContext跨异步使用、错误参数类型等。官方提供的flutter_lints大部分规则落在这一层。第三层是架构治理层。这一层约束的是什么地方能碰什么地方也就是跨层依赖、模块边界、API 使用范围。这才是 Lint 规则集真正的价值所在。举个例子禁止 UI 页面直接导入平台通道封装这条规则靠 CR 盯一百个 PR 就会有漏网的但靠静态分析工具每次flutter analyze都会准确地指出是哪一行出了问题。rexios_lints 的设计思路就是把这套三层体系做成一套可复用的规则集。它不是简单地include一份配置就完事而是把架构层的约束编写成自定义插件规则在编译阶段强制执行。这样规则就成了会咬人的防线——你不碰它它不咬你你一违规它当场就不放行。1.3 鸿蒙适配的特殊性千疮百孔的边界有人可能会问鸿蒙适配和 Lint 规则有什么关系不就是在 CI 上多跑一个 target 嘛。实际做下来完全不是这回事。鸿蒙侧的 Flutter 工程通常是基于社区维护的分支 SDK 来构建的这意味着你用的 analyzer 版本、SDK 路径、可用的平台通道封装都和官方 Flutter 有差异。最常见的情况是官方 Flutter 的某个 API 被标记为废弃但鸿蒙分支上还没有对应的替代品反过来鸿蒙分支引入了一些自有扩展 API而官方规则并不知道这些 API 的存在不仅不会拦甚至可能给出错误提示。更麻烦的是边界问题。鸿蒙适配阶段团队通常会做一个能力适配层或者叫平台桥接层把原生能力统一封装成 Dart 接口。这个封装层本身是合理的但它的存在也带来了新的架构诱惑业务代码为了图省事完全绕过适配层直接去调用底层的 platform view 或 method channel。这种绕过在官方 Flutter 时代也存在但在鸿蒙适配期会更严重因为很多能力只有原生侧才有开发者一着急就会直接往底层钻。如果不靠 Lint 规则把必须走封装层这条边界卡死等适配结束后你会发现所谓的适配层只是个摆设真正调用原生能力的代码散落在各个业务模块里后续升级 SDK 时每一个调用点都可能出错。这就是我们决定把 rexios_lints 完整引入鸿蒙工程的核心原因。2. rexios_lints 规则体系从宣言到代码2.1 规则集的四象限分类rexios_lints 在规则设计上没有做大而全而是针对代码库的痛点做了一套四象限分类对应四类不同的治理目标。这样分类的好处是规则之间不重叠出现问题时可以快速定位到规则源头。第一类是命名与组织规范。这一类基本继承了flutter_lints和dart_lint的合理部分再补充团队自身的命名偏好。比如禁止拼音命名、统一服务类后缀、强制 Dart 私有成员带下划线等。它们在规则集中属于基础盘。第二类是生命周期与异常处理规范。包括禁止空 catch、禁止在build方法里做耗时操作、禁止在State.dispose之后访问状态、禁止在异步回调中无保护地使用mounted等。这类规则是崩溃率的重要守门员尤其是鸿蒙适配期不少稳定性问题都出在异步生命周期错配上。第三类是跨层依赖约束。这是规则集的核心也是自定义插件规则发挥主要作用的地方。我们会给工程目录定义层级角色ui、application/service、infrastructure、platform_bridge。规则会检查文件实际所在目录与它导入的模块层级是否匹配一旦发现 UI 层直接导入了 infrastructure 层的类直接报 Error。第四类是鸿蒙能力使用约束。这类规则是适配期新加的专门约束平台通道相关代码的写法和调用路径。例如禁止业务代码直接使用MethodChannel、强制使用统一的适配层入口、限制原生插件注册的位置等。把规则分成四类之后工程里每个文件的责任边界就被描述清楚了。审查代码的时候不需要再靠人肉判断这段代码该不该出现在这里工具会替你把边界擦亮。2.2 从 AST 层面拦截违规导入一段可运行的规则示例自定义 Lint 规则的原理并不复杂用 Dart 的 analyzer 库解析源码生成 AST然后遍历 AST 里的节点按规则条件去匹配并报告问题。我写一个我们在 rexios_lints 里用到的规则精简版用来禁止 UI 层直接导入基础设施层。import package:analyzer/dart/ast/ast.dart; import package:analyzer/dart/ast/visitor.dart; import package:custom_lint_builder/custom_lint_builder.dart; class AvoidInfrastructureImportInUiRule extends DartLintRule { AvoidInfrastructureImportInUiRule() : super(code: _code); static const _code LintCode( name: avoid_infrastructure_import_in_ui, problemMessage: UI 层禁止直接导入基础设施层请通过服务层访问。, errorSeverity: ErrorSeverity.ERROR, ); override void run( CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context, ) { context.registry.addCompilationUnit((unit) { final path unit.declaredElement?.source.uri.path ?? ; final isUiFile path.contains(/ui/) || path.contains(/pages/); if (!isUiFile) return; for (final directive in unit.directives) { if (directive is! ImportDirective) continue; final uri directive.uri.stringValue ?? ; if (uri.contains(/infrastructure/) || uri.startsWith(package:xxx/infrastructure/)) { reporter.reportErrorForNode(_code, directive); } } }); } }这段代码做的事情很简单找到所有被分析的 Dart 文件判断它是不是 UI 层的文件如果是就再看它导入了哪些模块一旦发现infrastructure路径就报告一个 Error 级别的诊断。在custom_lint里注册规则也很直接入口文件里把规则实例加进去就行ListDartLintRule getLintRules(DartLintRuleConfig config) [ AvoidInfrastructureImportInUiRule(), // 其他规则... ];当然真实工程里的判断逻辑会比这个复杂得多。比如infrastructure在某些业务模块里路径会带参数或者一些公共类型定义放在共享目录里需要加白名单。规则如果写得太粗暴会带来大量误报反而降低团队对规则集的信任。这一点后面单独展开说。2.3 规则等级设计Warning 与 Error 的取舍自定义规则发布时每条规则要明确自己的定位是 Warning 还是 Error。这个选择直接影响团队的使用体验。我们的原则是可以立刻改、影响面小的问题设为 Warning架构边界类、蛰伏期长、后期修复成本高的问题直接设 Error。举个例子命名不规范日志必须带 tag这类规则设 Warning因为即便暂时不改也不会造成系统性问题而UI 层直接导基础设施绕过平台桥接层调用原生能力这类规则必须设 Error因为它们一旦上生产后期迁移改造的成本是整个模块级别。在 rexios_lints 里我们同时使用了两套机制来处理等级。声明式规则基于linter的规则通过analysis_options.yaml里的errors字段来控制告警等级自定义插件规则则在LintCode里直接定义errorSeverity。两条路都通向同一个结果flutter analyze的退出码会带上这些告警的严重程度CI 就可以据此决定是否拦截。等级设计里还有一个细节值得提一下存量代码和新代码要区别对待。架构类规则如果一开始就全面设 Error存量代码的报错可能上千条团队会直接被淹没在错误里。我们的做法是分两阶段推进存量先按 Warning 观察、按模块逐步清理新代码从一开始就走 Error 流程。过渡期大概持续了两周等存量收口到可控范围内再统一提高到 Error。这个渐进式收口策略比一次性宣布所有不规范都不许合入要顺利得多。3. 鸿蒙工程接入全流程3.1 在 Flutter 鸿蒙分支工程中引入 rexios_lints鸿蒙 Flutter 工程本质上还是一个 Flutter 工程目录结构、依赖管理方式与官方版本基本一致所以引入 Lint 规则集的流程也通用。只是有一点要注意鸿蒙分支的 SDK 版本可能落后于官方analyzer API 也可能有差异引入前最好先确认规则包中使用的 analyzer 版本与工程能够兼容。第一步是在工程的pubspec.yaml里添加依赖dev_dependencies: rexios_lints: ^1.0.0 custom_lint: ^0.6.0第二步是在工程根目录的analysis_options.yaml里引入规则集并启用插件include: package:rexios_lints/analysis_options.yaml analyzer: plugins: - custom_lint这里的include会把规则包内预置的声明式规则全部带进来而plugins则负责加载自定义插件规则。两者缺一不可。配置完成后执行flutter pub get flutter analyze如果一切正常你会看到flutter analyze的日志里多出rexios_lints相关的诊断信息。没有看到的话可以执行dart run custom_lint来单独验证自定义插件是否被正确加载。这一步是最容易出问题的位置下文排查部分会细讲。3.2 把架构规则升级为编译期 Error默认情况下include进来的规则可能有部分只是 Warning甚至 Info 级别。要让架构规则真正变成防线需要在analysis_options.yaml里把关键规则的等级调高。以我们工程为例子下面这段配置把几个核心规则全部提升为 ERRORanalyzer: errors: avoid_infrastructure_import_in_ui: error avoid_platform_channel_in_business: error avoid_empty_catch_block: error prefer_service_layer_access: error配置完之后任何一条规则被触发flutter analyze的退出码都会是非零后续 CI 就可以据此直接阻断合入。这里有个操作细节要提醒一下flutter analyze默认情况下某些 WARNING 不会导致非零退出码但一旦你把它提升为error它就会进入致命错误列表。所以升级 Error这个动作一定要配合 CI 的--fatal-infos --fatal-warnings参数使用才能确保退出码严格反映诊断结果。还有一个细节规则等级修改后本地 IDE 的提示也会同步更新。在 VSCode 或者 Android Studio 里打开文件你会发现违规行直接标红而不是原来的黄色下划线。这样开发者在写代码的当下就能看到问题不必等到 CI 跑完才返工。3.3 鸿蒙 SDK 与多目录工程的细节配置鸿蒙 Flutter 工程的目录结构和官方工程有一个明显差异除了lib/之外还多了一层鸿蒙原生工程的目录通常叫harmony或entry。analyzer 默认只会扫描lib/目录下由pubspec.yaml指定的部分但你大概率会有一些脚本、工具类、或者测试目录需要纳入检查。我们工程的做法是在analysis_options.yaml中显式声明分析范围analyzer: exclude: - harmony/** - build/** errors: # ... 规则等级配置harmony/**是鸿蒙原生侧代码由 ArkTS 编译器负责检查不该让 Dart analyzer 去碰。如果不排除analyzer 会因为无法解析.ets文件而产生大量虚假诊断CI 上会跑出莫名其妙的红点。还有一类路径需要注意部分鸿蒙适配工程会把平台桥接代码放到了lib/platform/ohos/这类目录下而通用业务代码仍然在lib/features/。这种多目录结构下自定义规则里对路径的判断要能涵盖所有场景不能只写死一个pages目录。我们的做法是把目录角色定义抽成了一份公共配置文件规则实现里统一读取方便后续新增业务模块时只需要改配置不需要改规则代码。鸿蒙 SDK 相关的环境变量也要在 CI 机器上提前配好。常见的是DEVECO_SDK_HOME指向鸿蒙 SDK 的安装目录以及 Flutter 鸿蒙分支 SDK 的路径配置。analyzer 虽然不直接依赖这些变量但flutter pub get在解析某些鸿蒙特有依赖时会读取它们。这一块我踩过几次坑稍后一起写在排查清单里。4. 在 CI 流水线中构建合规防线4.1 本地门禁提交前跑一遍架构防线不应该只在 CI 上存在本地开发阶段就要先拦一道。我们团队的统一流程是提交代码前跑一次flutter analyze把输出贴在 commit message 里。这条规矩光靠自觉不现实所以我们在 Git 的 pre-commit 钩子上做了自动化。钩子脚本的逻辑很简单#!/bin/sh echo Running rexios_lints... flutter analyze --fatal-infos --fatal-warnings if [ $? -ne 0 ]; then echo ❌ 存在代码规范/架构违规请先修复再提交。 exit 1 fi这个钩子一开始用的是dart run custom_lint但实测下来在大型工程里耗时偏长后来改成了flutter analyze。因为flutter analyze本身会加载analysis_options.yaml中启用的所有检查包括自定义插件退出码也会如实反映 Error 级别的问题。钩子只要判断退出码非零就拒绝提交简单粗暴但有效。唯一的问题是在存量代码还没清理完的那些天这个钩子会让不少同事抓狂。我们的处理办法是在钩子脚本里加了一个--baseline参数支持允许存量代码的已知违规记录在文件中豁免新出现的违规不会豁免。等存量收口完成再移除豁免文件。这个方案相对平滑团队接受度也高。4.2 服务端门禁让不合规代码无法合并本地钩子防君子不防小人真正的防线必须设在服务端。我们的合并请求流水线里加了一个静态分析阶段核心命令就三条fvm flutter pub get fvm flutter analyze --fatal-infos --fatal-warnings dart run custom_lint三条命令的执行顺序有讲究。pub get用来解析依赖并锁定版本flutter analyze同时覆盖声明式规则和自定义插件规则custom_lint单独跑一遍主要是为了拿到更详细的规则报告输出方便在流水线页面上展示问题列表。如果flutter analyze失败流水线直接中断这条合并请求就没法继续往下走了。对于流水线产物我们还会顺手生成一份 JSON 格式的分析结果作为合并请求的附件。这样评审人不用在日志里翻直接看规则报告就能判断遗留问题是什么。数据也能汇总起来按周统计各个模块的违规数量变化趋势用来向团队展示治理进展。这个统计本身也能反哺规则设计——哪类问题每周都在涨说明现行规则的约束力度还不够。4.3 存量代码的渐进式治理存量代码的治理可能是整个项目里最需要耐心的一环。我们的策略分三步走。第一步摸底。规则集刚接入时先把所有问题跑出来按模块分类统计。比如某个订单模块有 200 条违规其中 120 条是跨层依赖60 条是异常处理不规范。把数字摆在桌面上团队才知道从哪里下手。第二步定优先级。架构类规则优先收口因为这一类影响最大、越晚改越难改命名、注释、格式化这类问题可以往后放甚至留给代码格式化工具去处理。每个模块认领自己的存量问题估算工作量按迭代分批推进。第三步冻结增量。从规则集生效那天起新增代码必须零违规。存量可以慢慢还但新增代码不允许再产生新的架构债。这一步一旦被团队认可存量会在两三轮迭代内快速下降因为重构过程中顺手就把旧问题也修掉了。渐进式治理最怕的是急着清零。如果强行要求团队在同一周内把上千条违规全部清完那几乎注定会导致规则被绕过或废弃。慢一点稳一点每个迭代清一批两个月后回头看效果会非常明显。5. 落地一个月实测效果与排查实录5.1 常见问题速查表把规则集接入鸿蒙工程后团队陆续遇到了一些问题。这些问题很多都是有一定共性的我整理成一张速查表方便大家直接对照排查。问题现象可能原因解决办法添加自定义规则后flutter analyze无任何反应pubspec.yaml中未声明custom_lint插件或analysis_options.yaml里没加plugins确认dev_dependencies里有custom_lint并检查analyzer.plugins配置规则包升级后报 analyzer API 不兼容鸿蒙分支 SDK 的 analyzer 版本与规则包存在的版本冲突对齐 SDK 版本或使用dependency_overrides临时固定 analyzer 版本自定义规则在 IDE 有提示但 CI 不报错CI 上用的 Flutter SDK 与本地不一致CI 统一使用 fvm 锁定 Flutter 鸿蒙分支版本规则误报路径判断覆盖不全自定义规则内的路径匹配写死了某个目录名将目录角色配置抽成公共文件规则统一读取harmony/**目录产生大量虚假诊断analyzer 尝试解析 ArkTS 文件在exclude中排除鸿蒙原生目录规则太严导致本地 hook 频繁拦截存量大、等级设置过激进使用 baseline 机制豁免存量增量从严每一条问题都不是玄学基本都能在配置层面找到原因。排查时有一个通用的调试姿势先跑dart run custom_lint看插件是否加载成功再跑flutter analyze -v看分析器实际读了哪些配置文件和目录。这两步能过滤掉八成的问题。5.2 调试自定义 Lint 规则的两个实用技巧自定义 Lint 规则是 Dart 代码调试起来和调普通 Dart 代码完全一样可以用 IDE 的断点调试。但要真正快速调好规则有两个技巧值得分享。第一个技巧善用最小的复现工程。不要在大仓库里反复跑全量分析来验证规则那样又慢又难定位。单独建一个只有几十行代码的小工程专门用来触发目标规则。在这个小工程里改规则代码跑dart run custom_lint看到效果后再把规则搬回正式工程。这样一次验证的耗时从分钟级降到秒级调试体验完全不同。第二个技巧把规则里的判断逻辑全部抽成纯函数。比如判断文件是否属于 UI 层判断导入路径是否命中基础设施目录这些逻辑抽成独立的函数后可以直接写单元测试。规则在正式环境出错时也能只跑对应的单测来定位是匹配逻辑的问题还是 AST 节点拿法的问题。我们的规则包里有不少测试用例就是这个思路实测下来对回归保护非常有效。5.3 实际收益与体验规则集上线运行一个多月后最直观的变化是合并请求里的架构争议少了。以前评审时一半的精力都花在争论这个依赖该不该放这儿这种边界问题上现在有规则兜底评审人只需要关注实现逻辑本身边界问题交给工具去判定。团队的新人也能通过规则报告快速了解项目的架构约定——与其背诵架构文档不如让工具在写代码时直接告诉你哪里越界了。鸿蒙适配期间平台通道的调用路径是我们盯得最紧的。应用规则之后业务代码直接MethodChannel的场景基本绝迹所有原生能力调用都收敛到了适配层。这个收敛效果不是靠几条规章制度实现的而是靠每次本地分析、每次 CI 检查在编译期就把问题拦在了外面。如果说把代码写规范是工艺那这套规则集就是一套工艺纪律让团队在不自觉间保持一致的架构口吻。回头再看这个过程我最大的体会是架构规范只有进入工具链才真正具备了约束力。文档里的架构约定写得再漂亮也不如一次编译报错来得有效。鸿蒙适配不是一个另起炉灶的项目恰恰是这类工程最需要编译期防线的时候——因为边界最容易被突破也最容易在事后付出代价。