拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

mason_api鸿蒙适配实践:模板生成与代码生成工程化

mason_api鸿蒙适配实践:模板生成与代码生成工程化 1. 为什么我会把mason_api搬进鸿蒙工程这事得从一次常规的App迁移说起。公司要把现有移动端产品移植到鸿蒙生态业务侧最不缺的就是结构高度相似的代码页面骨架、API服务封装、数据模型、路由配置一套模板换个模块名就是新代码。以前在Android和iOS端我们靠脚本和代码片段库解决这类重复劳动到了鸿蒙这边工具链突然变得尴尬——底层运行环境换了脚本还是那套脚本但模板里写死的路径、平台判断、包管理器行为全都不一样等于重新趟一遍水。我当时的判断是与其继续维护那堆越来越难看的Shell和Python脚本不如直接引入成熟的模板引擎。于是mason_api成了第一选择。它是Flutter生态里mason工具的编程式接口底层是brick概念把一套代码骨架定义为模板暴露变量通过渲染引擎在指定目录里生成完整代码。用代码自动写代码这正是当时的团队最缺的能力。1.1 企业级模板生成到底解决什么问题先说痛点再讲方案这样你就能理解我为什么放着简单的代码片段方案不用非要去啃适配。移动端业务开发里重复度最高的不是业务逻辑本身而是围绕接口和页面产生的样板代码。以接口模块为例任何一个服务都需要request模型、response模型、API方法、错误处理、日志上报。你手写第一个接口可能要40分钟写到第20个接口时速度会快一点但质量开始不稳——漏掉空值判断、状态码处理不统一、字段类型抄错这些问题会随着接口数量线性增长。模板生成解决的就是这个数量越大越容易出错的问题。把稳定骨架固定下来把变化的部分抽象成变量一个接口模块从40分钟压缩到几秒而且生成的代码每个文件结构完全一致review成本也随之下降。mason的brick机制做得很轻一个brick就是一个目录里面有brick.yaml、template文件夹、可选的hooks文件夹。template里的文件用Mustache语法写变量占位brick.yaml里声明变量和默认值hooks用来做生成前后的定制处理比如自动格式化、自动注册路由。1.2 为什么选择mason_api而不是手写字符串拼接很多团队解决代码生成的方式是硬拼字符串用StringBuffer把代码一行行拼出来再写成文件。说实话对于一两个固定场景这招够用。但一旦业务模板数量过10个就会踩到三个问题。第一字符串拼接的代码本身就是意大利面模板和逻辑混在一起改一个缩进都可能动到拼装逻辑。第二模板无法在IDE里高亮和格式化你写模板的时候等于在写盲文。第三硬拼接几乎没有渲染能力循环、条件分支、嵌套变量全靠手写逻辑去控制复杂度一上去就是灾难。mason_api的方案是模板与逻辑分离。模板文件保持原始代码的样子还带着.tmpl后缀做标识IDE高亮、格式化工具都能用渲染逻辑交给Mustache引擎你只需要维护变量列表和生成目标。mason_api提供的MasonGenerator类还能直接读取brick目录、解析brick.yaml、把渲染结果写入目标目录API设计得相当克制没有多余的东西。顺带一提mason还自带CLI工具mason make可以交互式输入变量。但CLI更适合个人开发者本地使用到了企业级场景我们需要在业务系统里调用、要跟CI/CD集成、要动态传参CLI就顶不住了必须上mason_api这个编程接口。这也是我做鸿蒙化适配时始终坚持用API而非命令行的原因。1.3 鸿蒙Flutter工程里的真实痛点公司选择用Flutter迁移鸿蒙这对Dart生态的好消息是纯Dart代码能在鸿蒙的Flutter运行时里跑mason_api这种纯Dart包理论上可以直接用。但理论上和实际上之间隔着三条鸿沟。第一条是运行环境差异。鸿蒙应用跑在自己的沙盒里文件读写受到管控不像桌面Linux那样可以随意访问任意目录。第二条是模板资源来源。Android端以前的模板放在SD卡或应用私有目录里鸿蒙这边需要重新设计模板的打包和解压方式。第三条是mason_api的版本和API调整不同版本的MasonGenerator构造方式不一样hooks的执行逻辑也有差异。这三条鸿沟说白了就是适配的核心工作。接下来的内容就是我把这三条沟一个个填平的过程。2. 适配前的技术摸底mason_api的依赖面与鸿蒙运行时我不喜欢拿着枪乱扫做适配之前习惯先把目标拆开看一遍。mason_api能不能在鸿蒙上跑最终取决于它的依赖有没有碰平台通道。2.1 先确认Flutter能不能在鸿蒙上稳定跑这步是地基地基不稳后面全是白搭。OpenHarmony SIG维护了一个fork版的Flutter引擎配合DevEco Studio可以创建鸿蒙Flutter工程Dart代码最终会运行在鸿蒙的Flutter运行时里。我当时的做法是在DevEco Studio里新建一个空白Flutter工程确认flutter run能跑到模拟器上跑通之后再开始引依赖。这一步的时间比想象中要久。因为鸿蒙的Flutter工具链对Flutter版本有对应关系你本地的Flutter SDK和OpenHarmony SIG要求的不一致会在构建阶段报各种奇怪错误。建议直接用SIG仓库里带的SDK版本别用自己电脑上最新的Flutter主分支版本省掉一整天的时间。2.2 拆开mason_api看依赖哪些是纯Dart哪些碰了平台通道拿mason_api的pubspec.yaml过一遍是我这次适配里最安心的环节。以我锁定的0.1.x版本为例它的核心依赖集中在path、meta、pub_semver、yaml这类包上全部是纯Dart实现没有调用Android的原生View也没有走到iOS的MethodChannel更没有用到dart:ffi去做外部函数调用。这意味着mason_api在鸿蒙的Flutter运行时里具备运行条件不会被平台通道卡死。路径处理是另一个需要重点看的模块。mason_api生成文件时大量使用path包拼接路径、转换相对路径这个包在鸿蒙上的行为和Linux一致POSIX风格的路径分隔符能让brick内部的模板定位正常工作。我用一个最小的brick样本直接跑了一个生成实验当时的结果是可用的这给了后面改造足够的信心。我整理了一份风险清单列了下mason_api各能力在鸿蒙上的状态能力模块关键技术点鸿蒙化风险处理方式模板渲染Mustache语法引擎低纯Dart逻辑直接使用文件生成dart:io文件读写中受沙盒路径限制限定在应用沙盒内操作路径拼接path包低POSIX兼容直接使用配置解析brick.yaml的YAML读取低纯Dart逻辑直接使用hooks执行生成前后调用外部脚本高沙盒限制与解释器问题逐步禁用/替换为Dart hook交互式提示prompt输入高面向终端设计不用改为API动态传参2.3 鸿蒙的沙盒和路径规则跟你想的不一样这里要专门展开讲因为这是坑最深的地方。鸿蒙应用默认跑在沙盒里应用可以自由读写的目录通常是自己沙盒下的文件区用绝对路径表示的话是一串带应用标识的路径。mason_api拿到一个DirectoryGeneratorTarget之后会在这个目录下创建子文件所以目标目录必须指向沙盒内的可写路径否则会抛权限异常。模板所在的位置也一样。以前在Android上brick目录可能随便放在assets目录或者扩展卡里鸿蒙这边建议把brick放到沙盒的files目录下面由应用启动时负责把模板从安装包资源里面解压出来。这个过程需要应用自己实现mason_api只管读目录不管你的目录从哪里来。我再强调一遍路径分隔符一定要用POSIX风格。有些代码在Windows上开发久了会在模板路径里留下反斜杠这在Linux和鸿蒙沙盒里都会导致定位失败。适配第一天我就吃了一次这个亏后面专门写了章节来复盘。2.4 版本组合建议适配工作开始前把版本钉死能少掉一半头发。我这次最终用的组合如下可以作为参考组件版本建议备注Flutter SDKOpenHarmony SIG维护的flutter_flutter分支对应版本不要用daocloud的通用版除非你只是跑纯Dart逻辑Dart SDK随Flutter SDK自带确保Dart版本3.0老版本对record语法支持不全mason_api0.1.xAPI较稳定MasonGenerator构造方式清晰path1.9.x跟随mason_api间接依赖即可DevEco Studio5.x以上支持鸿蒙Flutter工程的构建这里有一个容易忽略的细节mason_api对Dart版本有要求如果SDK版本太低编译期会直接卡在pub get解析上。建议在最初建工程时就把依赖加上跑一次flutter pub get尽早暴露版本冲突免得适配到一半才发现。3. 鸿蒙化改造实操三条主链路的替代方案摸底做完心里有数了接下来就是真刀真枪的改造。我把整个适配工作拆成了三条链路模板资源怎么进鸿蒙工程、生成目标目录怎么设置、hooks怎么处理。3.1 资源打包brick从本地目录变成应用资产以前在Android上跑模板生成brick目录就直接放在工程项目眼皮底下程序启动时通过相对路径就能找到。鸿蒙这边的坑在于应用安装后工程目录并不等于运行时目录你开发机上放模板的位置到了真机上根本不存在。最省事的方案是把brick目录打包成压缩包放进Flutter应用的assets里运行时再解压。我用的打包格式是tar.gz解压时全程用POSIX路径避免Windows下zip的路径分隔符问题。打包很简单在项目根目录放一个tool/package_brick.sh内容大致是这样#!/bin/bash set -euo pipefail BRICK_SRCbricks/api_client BRICK_DESTassets/bricks mkdir -p $BRICK_DEST tar -czf $BRICK_DEST/api_client.tar.gz -C $BRICK_SRC .然后在pubspec.yaml里声明assetsflutter: assets: - assets/bricks/api_client.tar.gz运行时解压这一步需要拿到应用沙盒下的可写目录。鸿蒙Flutter应用里可以通过path_provider或者直接使用dart:io的Directory.systemTemp来获取临时目录。我当时为了减少依赖直接用Directory.systemTemp创建了一个唯一子目录把tar.gz解压进去。解压逻辑用archive库实现代码不复杂import dart:io; import package:archive/archive.dart; import package:flutter/services.dart; FutureDirectory unpackBrick() async { final data await rootBundle.load(assets/bricks/api_client.tar.gz); final archive TarDecoder().decodeBytes(data.buffer.asUint8List()); final target Directory.systemTemp.createTempSync(mason_brick_); for (final file in archive) { if (file.isFile) { final output File(${target.path}/${file.name}); output.createSync(recursive: true); output.writeAsBytesSync(file.content as Listint); } } return target; }这个方案的好处是模板和业务代码彻底分离换模板不用改Dart代码替换assets重新打包就行。坏处是模板不能实时热更新但这在企业级场景里反而不是问题——模板本来就属于代码资产应该走版本管理。3.2 生成目标目录DirectoryGeneratorTarget的鸿蒙化封装mason_api生成文件的入口是MasonGenerator.generate它接收一个GeneratorTarget对象。最常用的实现是DirectoryGeneratorTarget直接把一个Directory对象传进去生成的代码就落到这个目录。鸿蒙这边的适配点在于目标目录绝不能写死。因为每个应用沙盒路径不同不同设备之间的绝对路径也有差异正确做法是运行时动态获取沙盒路径再传给DirectoryGeneratorTarget。我封装了一个小工具类来做这事import dart:io; import package:mason_api/mason_api.dart; class HarmonyMason { static FutureDirectoryGeneratorTarget resolveTarget( String relativePath, ) async { final base Directory.systemTemp; final dir Directory(${base.path}/$relativePath); await dir.create(recursive: true); return DirectoryGeneratorTarget(dir); } }注意这里我没有对DirectoryGeneratorTarget做子类化因为它的实现本身已经足够通用关键反而是传进去的路径要限制在沙盒范围内。如果传了一个沙盒外的路径鸿蒙文件系统会拒绝写入报错还很隐蔽只会在写文件时抛一个IOException不提前给任何警告。3.3 Hook策略默认hooks在鸿蒙下要管住mason的brick可以带hooks分为pre_gen和post_gen在模板渲染前后执行。常见用途是生成完成后自动执行dart format格式化代码或者自动把新模块注册到路由文件里。但hooks是适配过程中最大的变数。在PC上跑mason会直接用本机的Shell执行hook脚本在鸿蒙沙盒里跑Shell脚本的可用性、解释器路径、工作目录都和预期不同甚至可能因为权限策略而静默失败。更麻烦的是某些hook方案会去探测本机是否有mason CLI没有CLI的情况下直接报错导致整个生成流程挂掉。我的处理策略是分两步第一步在适配阶段把hooks目录直接置空先保证渲染链路通。跑通之后再逐个hooks回填看哪些能在鸿蒙沙盒里工作。第二步对于必须执行的hook优先改写成Dart hook。Dart hook和模板渲染在同一个进程里跑不需要外部解释器也不依赖Shell它是鸿蒙环境里最稳定的hook形式。如果你的brick确实写了shell hook又没有精力改写成Dart至少要在调用MasonGenerator.generate()的时候设置一个开关跳过hooks执行别让生成流程白白中断。说实话hooks在鸿蒙上的完整兼容是一个持续演进的话题取决于鸿蒙沙盒对进程管理和脚本执行的支持程度。我的建议是企业级模板生成里尽量把hook逻辑往前挪挪到调用mason_api之前去处理而不是依赖趋势不明确的hook机制。4. 实测复现在鸿蒙真机上生成一个可编译的业务模块理论讲再多都不如直接跑一遍。我挑了一个最经典的业务场景——API客户端模块生成做了一个完整的可复现实验。这套流程你照着走一遍大概率能跑通。4.1 设计一个api_client brickbrick的目录结构如下api_client/ ├── brick.yaml ├── hooks/ │ └── post_gen.dart └── template/ ├── {{service_name}}_api.dart.tmpl └── {{service_name}}_model.dart.tmplbrick.yaml里声明变量name: api_client description: Generate API client module. version: 0.1.0 vars: service_name: type: string description: The service name default: demo api_path: type: string description: The API endpoint path default: /api/demo模板文件用Mustache语法写变量占位。比如{{service_name}}_api.dart.tmpl里面会有一段这样的内容class {{#pascalCase}}{{service_name}}{{/pascalCase}}Api { final String baseUrl; {{#pascalCase}}{{service_name}}{{/pascalCase}}Api(this.baseUrl); Future{{#pascalCase}}{{service_name}}{{/pascalCase}}Model fetch() async { // generated request logic } }mason的内置格式化函数pascalCase可以把user_profile这样的蛇形命名转成UserProfile这个功能在企业级场景里非常实用——只要一个变量名从文件名到类名全部自动统一再也不会出现模块名和类名对不上的低级错误。4.2 生成调用链路的完整代码鸿蒙侧调用mason_api的代码我放在了后台Isolate里避免生成过程阻塞UI线程。整体代码如下import dart:isolate; import dart:io; import package:mason_api/mason_api.dart; FutureGenerateResult generateModule(MapString, dynamic vars) async { return await Isolate.run(() async { final brickDir await unpackBrick(); final generator await MasonGenerator.fromBrick( Brick.path(${brickDir.path}/api_client), ); final target DirectoryGeneratorTarget( Directory(${Directory.systemTemp.path}/output), ); final result await generator.generate( target, vars: vars, logger: null, ); return result; }); }这里的GenerateResult是我自己定义的一个返回类型用来携带生成文件列表和耗时信息。重点是MasonGenerator.fromBrick这一步它会把brick目录里的brick.yaml读进来解析变量定义然后拿着vars里的实际值去渲染template目录下所有文件。实际测试时我传入的变量是{ service_name: user_profile, api_path: /api/user/profile, }4.3 结果验证文件生成和编译通过是两码事第一次跑通生成时我特别兴奋因为日志里显示生成了两个文件结构完全正确。但冷静下来之后我意识到生成文件成功不等于代码可用必须把生成的代码塞进鸿蒙工程里编译一把才算真正验证通过。我把生成目标目录指向一个鸿蒙Flutter模块的lib目录子文件夹然后一路执行build。结果真的踩到问题了——生成的代码里有一行import语句用了相对路径在Android工程的包结构里没问题到了鸿蒙的模块结构里就找不到文件。解决方法是把brick模板里的import路径全部改成package前缀的绝对引用而不是相对引用。这个问题提醒我一个更重要的原则模板生成的代码必须从一开始就在目标平台上编译验证而且要覆盖真实业务里的包结构否则模板看起来再漂亮落地就是废纸。我去验证了三次生成耗时都在毫秒级别对比人工手写一个接口模块的40分钟这个效率提升是本质性的。5. 我在适配中踩过的坑完整排查链路这一部分记录的是真问题。每一个坑从出现到定位都花了我不少时间但定位之后回看其实都有规律可循。按排查链路写出来是希望你能少走这些弯路。5.1 模板定位失败路径分隔符惹的祸现象解压brick目录之后调用MasonGenerator.fromBrick抛异常说找不到brick.yaml。我当时第一反应是解压逻辑有问题于是把解压后的目录结构打出来看。目录里明明有brick.yaml路径也对为什么找不到接着我把传入的路径打印出来发现字符串里混着反斜杠。原因是打包brick时用的脚本是在Windows环境下跑的tar包内文件的路径被写成了反斜杠风格。解压到鸿蒙沙盒之后File对象对这个路径的处理和Linux不一致导致定位失败。解决办法很直接path包统一处理路径分隔符打包和解压都强制使用POSIX风格。当时也意识到一个更深的教训模板资源一旦进入构建链路就不能再假设它是在哪个平台上打出来的包。最好的规避方法是把打包动作放进CI里统一在Linux容器里执行这样产出物的路径风格永远是确定的。5.2 在UI线程跑生成界面卡成PPT现象第一次测试时我没用Isolate直接在按钮点击回调里同步调用generate()。点击之后界面当场冻结大概两秒后才恢复转场动画直接卡死。排查时先在代码里加了好几个时间点打点发现耗时主要集中在两个地方brick目录解压以及Mustache渲染大量模板文件时的字符串处理。这两段逻辑都是CPU密集型放在UI线程就是找死。修复方案就是我前面展示的Isolate.run。Dart的Isolate拥有独立的事件循环mason_api的所有操作都可以放进去跑完之后通过Future把结果传回UI线程。实测这一改动没有任何复杂度生成结果和之前的同步调用完全一致界面再没有卡过。如果你用的是低端鸿蒙设备建议在Isolate里再加一个超时控制避免模板文件特别大时Isolate长时间占住CPU。我用来兜底的是一个Future.timeout配上错误日志既能发现异常模板又不至于让整个App卡住。5.3 hooks脚本静默失败生成的代码没格式化现象生成的Dart文件缩进完全对但格式和项目规范不一致看代码像另一个团队写的。检查发现是post_gen里的dart format没有生效。为什么没生效我在PC上跑同一套hook脚本是可以的但鸿蒙沙盒里根本没有可用的dart命令执行环境。mason_api尝试执行hook时大概率走了shell脚本的路径但脚本进程的启动环境和预期不同异常又被吞掉了所以整个流程看起来是跳过了hook没有抛错。定位思路是先看brick里hooks目录下实际有什么。如果是shell脚本就默认它不可靠如果改成Dart hook那么在mason_api的进程内执行Dart函数是可控的。最后我选择把格式化逻辑从hook里拿出来放在生成完成之后、由上层Dart代码直接调用格式化库来处理彻底绕开注解执行环境问题。5.4 pub依赖解析时卡死差点以为环境坏了现象在鸿蒙Flutter工程里添加mason_api之后执行flutter pub get进度停在某个包上长达几分钟最后超时。这不是包的问题是网络问题。pub默认的仓库源在国内网络环境下解析不稳定mason_api又带了一堆间接依赖导致整个解析过程特别脆弱。解决方式是切换到可用的pub镜像源并在工程根目录的pubspec.yaml旁边配置好。这个步骤最好在搭建环境时完成别等到加依赖时才想起否则你分不清到底是镜像没配好还是依赖本身有冲突。6. 跑通之后的工程化补充这样用才像企业级方案适配完成、验证通过这只是起点。mason_api的鸿蒙化落地如果只停留在自己调用一下价值撑不起企业级三个字。我后续做了三件事让这套能力真正融入到研发流程里。6.1 用JSON配置驱动模板变量模板变量直接写死在Dart代码里会让生成能力变成开发人员的私有工具。要让整个团队都能用就得把变量抽出去。我设计了一个JSON配置文件里面描述本次要生成哪些模块、每个模块的变量是什么、生成目标目录在哪。这样产品和后端也能参与进代码生成流程他们只改JSON不需要碰Dart代码。我在内部把这个JSON叫做生成订单一次生成就是一次下单模板引擎就是生产线。6.2 和CI/CD联动代码生成进入自动化流程代码生成最理想的落地场景是合并到CI/CD里。我在CI流水线里加了一个stage开发提交一个生成订单的JSON之后CI自动执行一个小程序调用mason_api在构建机里生成代码再把生成的代码提交回仓库接着跑编译。整个过程不依赖任何开发者的本机环境模板和生成器都固化成构建产物。这么做的额外好处是模板的任何变更都会在CI里经过编译验证一旦模板生成出来的代码有问题构建会直接红就不会带病上线。6.3 可扩展的方向还有很多只要有固定结构的地方就能用模板生成。我已经在陆续把路由注册、网络拦截器、数据层仓储这些固定模式都固化成了brick。每个brick之间还能互相组合比如生成API客户端的同时自动生成对应的mock数据和单元测试骨架一条命令把整个闭环的代码都产出来。mason_api在鸿蒙上的适配本质上是在验证一个更大的命题Dart生态里的纯逻辑工具链有相当一部分可以平移到鸿蒙Flutter运行时缺的不是代码层面的兼容而是我们愿不愿意把这些工具一个个搬过去适配。我在实际适配中的体会是mason_api这块骨头啃下来之后再遇到其他纯Dart库的鸿蒙化问题思路就会清晰很多——先摸依赖面再管资源路径最后控执行环境。只要把这三件事想清楚很多库的鸿蒙化都没想象中那么难。
返回列表