ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony Flutter工程import_rules依赖控制

OpenHarmony Flutter工程import_rules依赖控制 上个月我梳理一个 OpenHarmony 平板上的 Flutter 工程时被dart analyze的报错清单吓了一跳presentation 层的页面直接 import 了 data 层的 Repository 实现类domain 层的接口和 data 层的 DTO 互相引用core 层里不知道什么时候混进了一个业务模块的入口。说白了Dart 这门语言根本不关心你的import语句写得合不合理模块边界拆得再漂亮只要有人在代码里写了一条跨层导入架构红线就形同虚设。这次要写的这套import_rules鸿蒙适配指南解决的正是这个问题。它本质上是挂在 Dart analyzer 插件机制下的规则包能在flutter analyze阶段对每个 Dart 文件做包级导入依赖控制让每一层只能 import 允许联通的那几个包违规直接报错。文中涉及的配置、命令、踩坑记录全来自我最近给公司平板项目从零落地这套规则的真实过程适合正在做 Flutter 模块化改造、且打算把工程迁移到 OpenHarmony 的团队参考。1. 包级依赖失控的两种典型现场为什么必须上硬规则1.1 现场一剪不断理还乱的循环依赖先说一个我见过无数次的循环依赖场景。假设你有chat和user两个 feature 包chat里的MessageRepository需要调用user模块的UserApi拿当前用户信息而user模块的UserProfileWidget又想展示消息未读数于是调了chat的MessageUnreadProvider。第一次这么写的时候两个包都能编译通过flutter analyze也不会说半个不字。但两个月后你就知道疼了改user的接口chat要跟着动改chat的模型user又要发布新版本。在普通 App 工程里这种循环依赖还能靠大家都小心点维持。到了 OpenHarmony 这种要面对多种异构设备、需要按模组裁剪包体的场景循环依赖就是灾难。鸿蒙侧的har包有严格的模块依赖声明两个 har 互相依赖时构建系统会直接报错或者无限递归。你不可能靠开发自觉去约束这种问题只能让静态分析在代码提交前拦截。1.2 现场二跨层导入抽象接口形同虚设比循环依赖更隐蔽的是跨层导入。分层架构里我们通常约定presentation 只依赖 domaindomain 不依赖任何具体实现data 依赖 domain 和 core。但实际代码里你随手一搜就能看到这样的写法// 在 presentation/xxx_page.dart 里 import package:myapp/data/repository/user_repository_impl.dart; class UserPage extends StatelessWidget { // 直接 new 了一个 data 层的实现类 final _repository UserRepositoryImpl(); }这条代码在 IDE 里不会飘红编译能过单测能跑。但它把 domain 层定义的抽象接口完全架空了以后你要换数据源实现就得跑到 UI 层去改代码。你可能会说代码评审的时候注意一下不就行了。说实话在团队超过五个人、迭代速度上来之后人肉 review 这种跨层导入根本看不住reviewer 不可能记住每个文件的归属层。这就是为什么必须让机器去盯。1.3 import_rules 的能力边界它管什么不管什么先给 import_rules 划个边界免得你期待过高。它管的是静态import语句谁导入了谁、导入的是 barrel 出口还是 src 内部文件、导入是否跨了被禁止的包边界。它是基于 analyzer 的 AST 分析你写下的每一条import package:xxx/yyy.dart都会变成 AST 里的 ImportDirective 节点规则引擎拿到这个节点里的 URI再结合当前文件所在包匹配预先定义的依赖矩阵。它管不了的是运行时间接依赖。比如你用get_it这类 ServiceLocator在 composition root 里注册了一堆实现类Service 内部通过getIt.getUserRepository()拿对象——只要 register 的代码没有直接 import 类型比如用了Type注册静态分析就查不出来。这不算缺陷反而是在倒逼你把谁依赖谁收敛到容器的注册表里让依赖关系显式化。只要理解了这条边界后面配置规则的时候就不会产生为什么我已经禁止了 A 包导入 B 包代码还是跑通了这种误解。2. import_rules 的规则引擎拆解从 analysis_options.yaml 到依赖矩阵2.1 它是怎么在 flutter analyze 里“插一脚”的import_rules 不是 Dart SDK 自带的 rule它走的是 analyzer 插件机制。安装之后你在analysis_options.yaml里通过analyzer: plugins:把它挂载上去然后flutter analyze执行时插件就会收到 analyzer 回调逐个文件检查。这里有个常见误区很多人以为在pubspec.yaml里把包加进dev_dependencies就生效了。不是的这只是装了依赖真正激活它必须同步修改analysis_options.yaml。漏掉后半步的话你会看到依赖装好了但任何规则都不生效也不报错非常迷惑。插件的加载还受 Dart SDK 版本约束。analyzer 插件的 API 在 5.x、6.x、7.x 之间是有差异的import_rules 发布时通常声明了自己的 sdk 约束区间。你在鸿蒙的 flutter 分支上跑Dart 版本往往滞后于官方主线装完插件后发现flutter analyze直接抛The import_rules plugin is not compatible这类错误多半就是版本没对齐。2.2 两条核心规则banned_imports 和 direct_barrel_onlyimport_rules 我最常用的两套规则是黑白名单和 barrel 直达控制。banned_imports黑名单用来禁止特定路径的导入配置格式类似于import_rules: banned_imports: - from: package:myapp/domain/** to: package:myapp/data/** - from: package:myapp/data/** to: package:myapp/presentation/**from是当前文件的路径模式to是被导入文件路径的模式**表示任意层级。配置左右两边都用package:前缀的 URI而不是相对路径是为了避免不同开发机上的绝对路径漂移。上面这个配置的意思很清楚domain 层文件不得导入 data 层data 层文件不得导入 presentation 层。direct_barrel_only直达控制则更进一步它强制跨包导入必须走 barrel 文件不允许直接 import 到某个包内部的src/目录。例如import_rules: direct_barrel_only: - package: package:myapp/domain/** allow_from: - package:myapp/** export_roots: - package:myapp/domain/domain.dart意思是所有想 import domain 内部文件的代码只能通过domain.dart这个总出口。如果团队有人写import package:myapp/domain/src/entity/user.dart即使这条导入不在黑名单里也会触发直达控制报错。这招对保护包封装边界非常有效但实施成本也高——你必须维护好每个包的 barrel 文件否则等于逼着所有人从没定义的出口导入。2.3 依赖矩阵三层架构的典型配置模板拿我们项目来举例目录长这样lib/ core/ # 基础能力网络、日志、工具 domain/ # 领域层实体、接口抽象、用例 data/ # 数据层仓储实现、DTO presentation/ # UI 层页面、组件、状态依赖约定如下当前包允许导入禁止导入core仅 Dart/Flutter SDK 及已声明的三方库业务模块全部禁止domaincoredata、presentationdatadomain、corepresentationpresentationdomain、coredata若要数据走 domain 接口落到 import_rules 配置上就是上面黑白名单模板的组合。这套矩阵几乎覆盖了团队里 99% 的违规导入场景。剩下 1% 是 test 目录和 generated 文件我后面专门讲。3. OpenHarmony 工程适配里最容易被忽略的四个差异点3.1 从 pub.dev 到镜像源package_config 路径变化对规则的影响把 Flutter 工程迁移到 OpenHarmony 端第一件事通常是换依赖镜像源。因为鸿蒙开发环境的网络策略和 pub.dev 直连不一定顺畅很多团队会配置华为云镜像或者其他内部制品库。这本身没什么问题但 import_rules 在解析规则时要读取.dart_tool/package_config.json这个文件里面描述的是包名 - 实际路径的映射。诡异的地方在于不同镜像源、不同操作系统上这个映射里的rootUri前缀不一样。Windows 开发机上是file:///C:/Users/xxx/AppData/Local/Pub/Cache/hosted/pub.flutter-io.cn/...Linux CI 构建机上可能是file:///home/runner/.pub-cache/...。如果你的规则里用了基于绝对路径的 exclude 或 include 模式很容易出现我本地 build 没问题CI 上死活跑不通的灵异事件。我的建议是所有路径模式一律用package:前缀的 URI不要用file://。import_rules 的路径匹配是基于 package_config 解析后的 URI 做的只要两边都用 package 形式镜像源差异就不会影响规则判定。3.2 Dart SDK 版本是硬约束鸿蒙分支的 analyzer 兼容性这是我在鸿蒙适配时踩得最重的一个坑。OpenHarmony 的 Flutter 分支通常不是官方 release而是由 OpenHarmony SIG 维护的 forkDart SDK 版本会比官方主线滞后不少。换句话说官方 Flutter 已经到 3.24 甚至更高时鸿蒙分支可能还停留在 3.7 左右的 Dart 版本。import_rules 作为第三方插件它对 analyzer API 的版本要求比较敏感。版本不匹配时flutter analyze启动后不会加载插件但也不会有刺眼的报错唯一的症状是你故意写一条违规导入它居然不报。解决办法是在pubspec.yaml里锁版本dev_dependencies: import_rules: 1.2.x # 用你本机验证过的 minor 版本 analyzer: 6.5.0 # 与 import_rules 声明的依赖范围对齐必要时用dependency_overrides强制对齐 analyzer 版本。别小看这条我见过不只一个团队插件装了半天不生效最后查了半天发现就是 analyzer 版本撞了。3.3 ohos 目录与 dart 目录混编规则只管 Dart 这一侧OpenHarmony 的 Flutter 工程在结构上和 Android 类似会有一个ohos/平台目录里面是 ArkTS 代码、hvigor构建脚本、module.json5等。这意味着一个工程里同时存在 Dart 和 ArkTS 两种源码。import_rules 本质上只分析 Dart 文件它对ohos/目录下的.ets文件完全无感。万一有人在 ArkTS 侧做的依赖是反模式的比如某个 Page 直接 import 了一个业务 SDK 的内部类import_rules 不会帮你拦。这不是规则缺陷而是分工问题。OpenHarmony 平台侧的依赖控制应该交给鸿蒙自己的 lint 工具ohos-lint和hvigor的模块依赖声明去管。你在 CI 上应该两条检查并行一条跑flutter analyze盯 Dart 侧一条跑 ohos-lint 盯 ArkTS 侧。我在项目里就是这么配的两条都过才能继续构建。3.4 别让 lint 堵住构建与 hvigor 构建流程的时序配合提一个容易忽略的配合细节。OpenHarmony 的构建链路是hvigor主导的它负责把 ArkTS 编译、资源打包、har 依赖解析最终生成 hap。Flutter 侧的构建实际是作为 hvigor 的一个 task 被调起来的。如果你把flutter analyze带 import_rules挂在flutter build hap之前执行那你一定要想清楚一个事analyze 报错时会终止构建但它的运行环境是 Flutter SDK 自己的 Dart VM不是 hvigor 的环境。这意味着 CI 上要先 ensure Flutter SDK 已被正确配置flutter 命令可用、pub get 已跑再执行 analyze最后才轮到 hvigor。正确顺序是flutter pub get flutter analyze --no-pub hvigorw assembleHap --mode module -p productdefault先分析后构建。这样 import 违规会在构建之前被拦下而不是等 hvigor 跑了一半才报一个莫名其妙的依赖错误。4. 从零到一落地一套可在 CI 上运行的完整配置4.1 环境准备与版本锁定我落地这套规则时用的环境大致是这样具体版本以你本机flutter doctor为准OpenHarmony 的 flutter forkflutter --version输出里 Dart 版本 3.ximport_rules 锁在 1.2.x工程使用单仓多包结构包名是myapp准备阶段不要省事。先跑一次flutter pub get然后盯一眼.dart_tool/package_config.json确认 import_rules 确实在这个文件里被解析出来了。如果 package_config 里没有这个包后面一切配置都是白搭。4.2 分析配置文件的完整写法下面这份配置是我在项目里实际用过的简化版可以直接抄。放在工程根目录的analysis_options.yaml里analyzer: plugins: - import_rules language: strict-casts: true import_rules: prefer_package_imports: true banned_imports: - from: package:myapp/core/** to: package:myapp/presentation/** - from: package:myapp/core/** to: package:myapp/data/** - from: package:myapp/core/** to: package:myapp/domain/** - from: package:myapp/domain/** to: package:myapp/data/** - from: package:myapp/domain/** to: package:myapp/presentation/** - from: package:myapp/data/** to: package:myapp/presentation/** direct_barrel_only: - package: package:myapp/domain/** allow_from: [package:myapp/lib/**] export_roots: [package:myapp/domain/domain.dart] - package: package:myapp/core/** allow_from: [package:myapp/lib/**] export_roots: [package:myapp/core/core.dart] excluded_paths: - **/*.g.dart - test/** - integration_test/**几个值得展开说的地方prefer_package_imports是强制用package:导入禁止相对路径导入import ../data/xxx.dart。相对路径在重构时最容易漏改特别是在多个 feature 包之间移动文件时IDE 会因为相对路径变化而飘红但你根本不知道是该改路径还是该改 import。用 package 导入后文件移动基本不影响 import。excluded_paths是规避误报的关键。*.g.dart是 json_serializable、freezed 等生成的文件它们内部自动生成的 import 不该受业务规则约束。test/和integration_test/放开是为了让集成测试能随意 import 各层来做端到端验证这个后面细说。4.3 故意写一条违规代码验证规则生效配置写完之后最重要的动作是验证它真的生效。我在初次接入时总会专门做一次负向测试——故意在 presentation 层写一条 import data 层实现类的代码// lib/presentation/pages/login_page.dart import package:myapp/data/repository/user_repository_impl.dart; class LoginPage extends StatelessWidget { // 这里故意违规用来验证规则 }然后执行flutter analyze --no-pub预期输出应该类似info • lib/presentation/pages/login_page.dart:1:1 • The package myapp/data is not allowed to be imported from this file. banned_imports • import_rules如果你的 import_rules 配置了error级别这条会直接以 error 形式中断 analyze退出码非 0。看到报错后把刚才的违规代码删掉再跑一次确认干净。这个正向反向的验证动作不要省略它能帮你确定规则是在工作的而不是假装在工作。4.4 把检查接进 GitLab CI / 本地脚本CI 脚本我用的比较简单核心就三步。在.gitlab-ci.yml里flutter_analyze: stage: test script: - flutter pub get - flutter analyze --no-pub建议再叠加一个本地检查脚本tools/check_imports.sh方便开发者在 push 之前自查#!/bin/bash set -e cd $(dirname $0)/.. flutter pub get flutter analyze --no-pub git diff --exit-code -- *.dart :!**/*.g.dart第二行git diff --exit-code是为了顺带检查格式化防止有人提交了没有跑过dart format的代码。这个习惯在多人协作里特别管用能把我本地能跑和CI 能跑之间的偏差提前暴露。鸿蒙工程和普通 Flutter 工程在 CI 上的最大区别是构建时长。hap 的打包比 apk 慢不少analyze 放在测试阶段跑能在构建之前快速失败省下的不只是一次构建时间还有排查到底是谁改坏了模块依赖的沟通成本。5. 排查 lint 报错的三板斧误报、漏报与规则误伤5.1 误报generated 文件怎么豁免json_serializable 生成的文件自动 import 了package:json_annotation/json_annotation.dart。如果这个包在你的 banned 名单里比如你禁止 core 层之外的包导入某个内部库.g.dart文件就会被误伤。我在 4.2 节用了excluded_paths来豁免全部*.g.dart这是最省力的方式。但有个细节要留意excluded_paths的匹配是按文件名 glob 做的如果某个团队的代码生成目标路径不统一比如有人把生成文件输出到.dart_tool/build/这两类路径都要写进排除列表。另一种场景是你不想整体豁免某一个生成文件只想豁免某条规则。import_rules 通常支持行内 ignore 注释// ignore: banned_imports import package:some_pkg/src/internal.dart;我个人非常不建议滥用 ignore 注释。它会让规则出现漏洞而且 review 时看不到上下文无法判断这个豁免是否合理。能用路径统一豁免的就不要再给开发者留手动 ignore 的口子。5.2 漏报动态条件导入很容易看漏Dart 支持条件导入import adapter_stub.dart if (dart.library.io) adapter_io.dart if (dart.library.js) adapter_web.dart;import_rules 在分析这类语句时会把每个分支的 URI 都当作一个独立的 import 来匹配规则。容易漏的是你把adapter_stub.dart放进了白名单却忘了adapter_io.dart和adapter_web.dart也在导入名单里。鸿蒙端做平台适配时这个场景特别常见。很多插件为了兼容 Android/iOS/Web会写条件导入迁移到 OpenHarmony 时如果有人新增了一个adapter_ohos.dart分支而 import_rules 配置里的白名单没有同步更新那这条新增分支就会变成漏网之鱼。排查技巧在 CI 上跑一次dart analyze后把输出里的 warning 列表人工过一遍重点看有没有和条件导入相关的提示。养成习惯后配白名单时你自然会想到这个包在条件导入里出现过没有。5.3 规则误伤测试目录要不要放开我见过不少团队把 test 目录也纳入严格规则结果就是单测代码里到处都是 ignore 注释。测试代码的价值恰恰在于它可以访问各层内部来验证行为你把它和业务代码用同一套黑名单约束其实是给自己找麻烦。我的策略是测试目录整体豁免规则但在单独的analysis_options.yaml里保留 Dart 自带的核心 linter这样测试代码的规范性靠通用 linter 兜底包级依赖的严格约束只针对lib/下的业务代码。在工程里可以用子配置实现# test/analysis_options.yaml include: ../analysis_options.yaml import_rules: enabled: false注意子配置的 include 机制在不同 analyzer 版本下的行为略有差异如果遇到include 了之后仍报 banned_imports的情况检查一下 import_rules 是否支持在子配置里整体 disabled不支持的话就继续用excluded_paths的方式整体跳过test/**。5.4 我踩过的一个坑Windows 大小写不一致导致本地与 CI 结果不一致这个坑非常隐蔽值得单独写出来。Windows 文件系统默认大小写不敏感你在 Windows 上写import package:MyApp/domain/domain.dartDart 分析器能找到文件一切正常。但 Linux CI 上文件系统大小写敏感MyApp和myapp是两个不同的包路径analyze 直接报Target of URI doesnt exist。import_rules 的匹配基于包名包名来自 pubspec 里的name字段大小写不对时它可能根本没匹配到任何规则规则等于被架空。这个问题在日常开发里很难察觉因为本地总是绿的。我的习惯是约定所有 import 一律小写包名并在 CI 上跑dart format --set-exit-if-changed .。格式化工具会强制统一 import 排序和包名形式把大小写问题直接消灭在格式检查阶段不会拖到 analyze 才暴露。把规则写得严一点不如分阶段落地最后说说我在实际接入这套规则后的体会。import_rules 这类包级依赖控制工具最大的价值不是把违规全杀光而是把架构约束从口头约定变成可自动执行的检查项。鸿蒙端的工程结构天然比 Android 更强调模块边界har 的依赖声明、XTS 认证对包体积和权限的最小化要求都逼着你在 Flutter 侧也必须把分层做干净。如果你准备在团队里推这套规则我建议分三步第一周先把规则设成 warning 级别让所有人看到违规提示但不阻塞构建同时导出一次全量违规清单把存量问题逐条评审能修的修暂时不能修的用excluded_paths或路径豁免收拢第二周再把flutter analyze挂进 CI警告数量降不下来就 continue-on-failure第三周改成 error 级别正式把红线焊死。我试过一上来就 error 级别结果团队怨声载道每天光处理历史遗留违规就花掉大量时间反而推进不下去。一个小技巧是:在 pubspec 里把import_rules锁到 minor 版本不要用 caret 范围放开到下一个大版本因为 analyzer 插件 API 一旦更新规则行为可能会有细微变化。等鸿蒙的 flutter fork 升级 Dart SDK 后再手动评估新版本兼容性逐版升级。这套工具配合好之后每次flutter analyze跑完看着满屏的绿色那种依赖再也不会乱掉的确定性是真的让人睡得踏实。
返回列表