Trae&Flutter | 助力 TolyUI 模块管理与发布

随着 TolyUI 的维护,我遇到了一些模块管理上的问题。比如:

  • 一个子模块版本更新时,我需要把依赖它的父级模块更新一个小版本。
  • 我很难直观地看出当前所有模块的版本信息
  • markdown 版的CHANGELOG 不太好维护,更新时写起来也比较麻烦

手动处理比较麻烦,所以我打算写个 Dart 脚本,来分析模块依赖结构。完成自动发布和展示当前所有模块信息的功能。刚好最近 Trae 发布了,就用它来帮我分析和书写一些代码:


1. 让 AI 分析生成 module.json

首先我提前准备了一个 module.json, 问它:

这个文件的作用。

当编辑器打开一个文件时,对话框里可以识别到 这个文件 指代的是什么。

它的总结还是非常到位的,基本上是我模块化设计的初衷:

markdown 复制代码
这个文件的主要作用是:
1. 记录项目中各个模块的版本信息
2. 定义模块之间的依赖关系和层级结构
3. 便于项目的版本管理和模块更新
4. 可能用于自动化构建和依赖管理

这种模块化的结构设计有助于:
- 更好地组织和管理大型项目
- 实现模块的独立开发和维护
- 方便模块的复用和版本控制
- 清晰地展示项目的整体架构

然后向他索要自动生成这个 module.json 的方式。这一招叫 将欲取之,必先与之

用 dart 解析 modlues 下的模块,自动生成该文件。注意模块间的子模块校验方式是: 依赖中含 tolyui_ 的模块。

然后它给了我一段代码,我执行了一下,完美地满足了我的需求。控制台漂亮地打印出了一个树形结构。从解析 yaml 到树形节点的处理,如果全凭我自己敲,少说也要一个小时的分析调试。而 AI 可以在 1 分钟之内给我期望的结果。


2. 分析解析代码

人生是一场修炼,过程远比结果要重要得多。可以学到知识的地方都可以谓之 ,我将于 AI 老师为我节约的这一个小时,来优化一下代码,并写篇文章仔细分析其过程,汲取其中的养分。而不是得到结果就算完事了。

首先定义了一个 Module 对象,盛纳一个模块的必要信息,目前包括名称、版本和子模块:

dart 复制代码
import 'dart:convert';
import 'dart:io';
import 'package:yaml/yaml.dart';
import 'package:path/path.dart' as path;

class Module {
  final String name;
  final String version;
  final List<Module> children;

  Module({
    required this.name,
    required this.version,
    this.children = const [],
  });

  Map<String, dynamic> toJson() => {
    'name': name,
    'version': version,
    if (children.isNotEmpty) 'children': children.map((e) => e.toJson()).toList(),
  };
}

解析模块的方法 processModule 代码还不到 30 行,其中传入模块的文件夹路径,并读取其中的 pubspec.yaml ,然后通过 yaml 解析内容,得到名称版本、依赖库列表,当依赖以 tolyui_ 开头,则递归地分析子模块,并将解析结果添加到当前模块的 children 中;

dart 复制代码
Future<Module?> processModule(Directory dir) async {
  // 1. 读取并解析 pubspec.yaml
  final pubspecFile = File(path.join(dir.path, 'pubspec.yaml'));
  if (!await pubspecFile.exists()) return null;
  final yaml = loadYaml(await pubspecFile.readAsString());
  // 2. 创建当前模块
  final module = Module(
    name: yaml['name'],
    version: yaml['version'],
    children: [],
  );

  // 3.扫描依赖
  YamlMap dependencies = yaml['dependencies'];
  
  // 4. 递归处理子模块
  for (final dep in dependencies.keys) {
    if (dep.startsWith('tolyui_')) {
      Directory subModelDir = Directory(path.join(dir.path, '..', dep));
      final childModule = await processModule(subModelDir);
      if (childModule != null) {
        module.children.add(childModule);
      }
    }
  }
  return module;
}

最后将解析的 Module 对象通过 JsonEncoder 反序列化为字符串,写入到文件即可:

dart 复制代码
Future<void> main() async {
  String modulesPath = 'd:\\Projects\\Flutter\\Fx\\toly_ui\\modules';
  String root = path.join(modulesPath, 'tolyui');
  Module? moduleJson = await processModule(Directory(root));
  if (moduleJson == null) return;
  String data = const JsonEncoder.withIndent('  ').convert(moduleJson.toJson());
  print(data);
  final outputFile = File(path.join(modulesPath, 'module.json'));
  await outputFile.writeAsString(data);
}

3. Trae 的图片识别能力

Trae 还有一点非常惊艳,它可以识别图片。比如我给它一个截图,问:

以这种树形结构,输出 module.json 的核心数

它会为我在 Module 类中提供 displayTree 方法,获得直观版的树形结构。运行后,完美得到我预期的效果:

dart 复制代码
String displayTree({String prefix = '', bool isLast = true}) {
  final buffer = StringBuffer();
  final marker = isLast ? '└──' : '├──';
  buffer.writeln('$prefix$marker $name ($version)');
  if (children.isNotEmpty) {
    final childPrefix = prefix + (isLast ? '    ' : '│   ');
    for (var i = 0; i < children.length; i++) {
      buffer.write(children[i].displayTree(
        prefix: childPrefix,
        isLast: i == children.length - 1,
      ));
    }
  }
  return buffer.toString();
}

如果不太明白算法实现的细节,也可以继续追问,让 AI 详细分析一下:

核心实现解析:

  1. StringBuffer 的使用 [1]

    • 使用 StringBuffer 而不是直接字符串拼接
    • 可以高效处理大量字符串拼接操作
    • 避免产生中间字符串对象
  2. 树形标记符号 [2]

    • └── : 表示当前层级的最后一个节点
    • ├── : 表示当前层级的非最后节点
    • 通过 isLast 参数来判断使用哪种标记
  3. 当前节点输出 [3]

    • prefix : 当前缩进前缀
    • marker : 节点标记符号
    • name (version) : 显示模块信息
  4. 子节点缩进处理 [4]

    • 如果父节点是最后一个(isLast=true):用空格
    • 如果父节点不是最后一个:用竖线 │
    • 这样保证了树形结构的连续性
  5. 递归处理子节点 [5]

    • 遍历所有子节点
    • 递归调用 displayTree
    • 传递新的缩进前缀
    • 标记最后一个子节点
  6. 返回结果 [6]

    • 将 buffer 转换为最终字符串
    • 包含完整的树形结构

4.基于 module 的数据,更新版本

基于 module.json,现在我想实现一个功能,通过 Dart 提供一个 update 方法,指定模块名,和版本号,如果模块的版本号增加,将其父级的所有版本后缀 +1 个小版本,形成新的 module.json

拿当前的版本来说,如果 tolyui_message 有更新,我希望依赖它的上层组件都可以更新一个小版本:

scss 复制代码
└── tolyui (0.0.4+6)
    ├── tolyui_rx_layout (1.0.0)
    ├── tolyui_color (0.0.1)
    └── tolyui_navigation (0.1.0+3)
        └── tolyui_feedback (0.3.6+1)
            └── tolyui_message (0.2.4)

比如 tolyui_message 更新到 0.2.5, tolyui 版本系列应该如下所示:

scss 复制代码
└── tolyui (0.0.4+7)
    ├── tolyui_rx_layout (1.0.0)
    ├── tolyui_color (0.0.1)
    └── tolyui_navigation (0.1.0+4)
        └── tolyui_feedback (0.3.6+2)
            └── tolyui_message (0.2.5)

为了记录 TolyUI 的每个更新的点滴,我希望保留所有的 tolyui 版本树,让之前的 module.json 编程 tolyui 后面加上版本号:

下面是 AI 给出的版本更新方法,放在 Module 类中,传入待更新模块名、版本号。这个方法会将检查当前当 Module 树种匹配的模块名,更新版本号:

dart 复制代码
bool updateVersion(String targetName, String newVersion) {
  if (name == targetName) {
    version = newVersion;
    return true;
  }
  for (var child in children) {
    if (child.updateVersion(targetName, newVersion)) {
      _incrementVersion();
      return true;
    }
  }
  return false;
}
void _incrementVersion() {
  final parts = version.split('+');
  if (parts.length > 1) {
    // 处理带有 build number 的版本号
    final buildNumber = int.parse(parts[1]);
    version = '${parts[0]}+${buildNumber + 1}';
  } else {
    // 处理普通版本号
    version += "+1";
  }
}

我想了一下,也可能一次更新多个模块,并且最好记录一下更新信息。于是继续优化:

优化一下当前类的 updateVersion 方法,我想传入一个更新信息的列表,包括更新模块名、版本号、更新介绍。父级没有更新的模块,版本号自动 +1,并在更新信息上记录所有子模块的更新信息

期望调用方式如下:

dart 复制代码
module.updateVersion([
  UpdateInfo(
    moduleName: 'tolyui_message',
    newVersion: '0.2.5',
    changLog: '修复一些 bug',
  ),
  UpdateInfo(
    moduleName: 'tolyui_color',
    newVersion: '0.0.2',
    changLog: '优化颜色选择器',
  ),
]);

如下代码所示,在 Module 类中增加了 changLog 字段,updateVersion 的入参改成了 UpdateInfo 列表。其中会根据入参信息的模块名称和版本号,自动维护 changLog 信息:

dart 复制代码
final String changLog;

bool updateVersion(List<UpdateInfo> updates) {
  bool hasUpdates = false;
  StringBuffer childrenChanges = StringBuffer();
  // 检查当前模块是否在更新列表中
  final updateInfo = updates.where((u) => u.moduleName == name).firstOrNull;
  if (updateInfo != null) {
    version = updateInfo.newVersion;
    changLog = updateInfo.changLog;
    hasUpdates = true;
  }
  // 检查子模块更新
  for (var child in children) {
    if (child.updateVersion(updates)) {
      if (!hasUpdates) {
        _incrementVersion();
      }
      childrenChanges.writeln('* ${child.name} -> ${child.version}');
      hasUpdates = true;
    }
  }
  // 如果有子模块更新,将子模块信息添加到父模块的更新记录中
  if (childrenChanges.isNotEmpty) {
    if (changLog.isEmpty) {
      changLog = childrenChanges.toString();
    } else {
      changLog += '\n$childrenChanges';
    }
  }
  return hasUpdates;
}

此时跑一下脚本,完美符合我的预期:tolyui_0.0.4+7 中描述了 tolyui 在 0.0.4+7 版本中的所有信息:


5. 更新文件的修改信息

接下来就到了最后一步,根据 tolyui_0.0.4+7 中的描述,修改变化模块的版本信息,以及 CHANGELOG.md 变化记录文件。关于 changelog 的维护,我希望能有一个结构化的东西来组织好,而不是直接 markdown 文本来记录。毕竟文本可以随便写,自动维护时校验比较麻烦。

于是我在每个模块中新加了doc 文件夹,用于放置文档,changelog.json 就是更新的结构化数据来源。

所以根据 tolyui_0.0.4+7.json 中的信息,维护模块下的 doc/changelog.json 即可,然后根据 changelog.json 生成 CHANGELOG.md

写一个 addVersion(String version, String change) 的方法,为当前文件新增一个版本。注意:当版本号是最新版本的小版本时,添加到 changes 里,否则在最前面添加该版本;没有文件时,自动创建文件,并写入当前版本

调用 addVersion,传入 0.3.6+3, Fix some bugs 后,就会添加一个记录:

dart 复制代码
Future<void> addVersion(String path ,String version, String change) async {
  final file = File(path);

  if (!await file.exists()) {
    // 文件不存在,创建新文件
    final json = {
      "versions": {
        version: {
          "changes": [change],
          "timestamp": DateTime.now().toIso8601String()
        }
      }
    };
    await file.writeAsString(jsonEncode(json));
    return;
  }

  // 读取现有文件
  final content = await file.readAsString();
  final json = jsonDecode(content) as Map<String, dynamic>;
  final versions = json['versions'] as Map<String, dynamic>;

  // 检查是否为最新版本的小版本更新
  final latestVersion = versions.keys.first;
  if (version.startsWith(latestVersion.split('+')[0])) {
    // 添加到现有版本的 changes
    List<dynamic> changes = versions[latestVersion]['changes'];
    if(changes.isNotEmpty && changes.first.startsWith(version)) return;
    changes.insert(0, change);
  } else {
    // 添加新版本
    versions[version] = {
      "changes": [change],
      "timestamp": DateTime.now().toIso8601String()
    };
    // 重新排序版本
    final sortedVersions = Map.fromEntries(
        versions.entries.toList()
          ..sort((a, b) => compareVersions(b.key, a.key))
    );
    json['versions'] = sortedVersions;
  }

  // 写入文件
  await file.writeAsString(JsonEncoder.withIndent('  ').convert(json));
}

int compareVersions(String v1, String v2) {
  final v1Parts = v1.split('+')[0].split('.');
  final v2Parts = v2.split('+')[0].split('.');

  for (var i = 0; i < 3; i++) {
    final num1 = int.parse(v1Parts[i]);
    final num2 = int.parse(v2Parts[i]);
    if (num1 != num2) return num1.compareTo(num2);
  }

  // 比较小版本号
  final v1Build = v1.contains('+') ? int.parse(v1.split('+')[1]) : 0;
  final v2Build = v2.contains('+') ? int.parse(v2.split('+')[1]) : 0;
  return v1Build.compareTo(v2Build);
}

Trae 还有个比较好用的功能,可以选择关联的文件/夹, 比如这里让它根据 changelog.json 生成 CHANGELOG.md

dart 复制代码
import 'dart:convert';
import 'dart:io';

Future<void> jsonToMarkdown(String jsonPath,String mdPath) async {
 
  final file = File(jsonPath);
  if (!await file.exists()) return;
  
  final content = await file.readAsString();
  final json = jsonDecode(content) as Map<String, dynamic>;
  final versions = json['versions'] as Map<String, dynamic>;
  
  final StringBuffer buffer = StringBuffer();
  
  for (final entry in versions.entries) {
    final version = entry.key;
    final data = entry.value as Map<String, dynamic>;
    final changes = data['changes'] as List;
    
    buffer.writeln('## $version\n');
    for (final change in changes) {
      // 处理小版本号格式
      if (change.toString().startsWith('+')) {
        buffer.writeln('* $version$change');
      } else {
        buffer.writeln('* $change');
      }
    }
    buffer.writeln('\n');
  }
  
  await File(mdPath).writeAsString(buffer.toString());
}

到这里,基本功能都有了,整理串联一下:

6. 批量发布更新的模块

最后来到批量发布环节,我只需要对比当前版本和上个版本有差异的模块,通过命令发布到 pub 上即可。



dart 复制代码
Future<List<String>> collect(String oldPath, String newPath) async {
  String oldContent = await File(oldPath).readAsString();
  String newContent = await File(newPath).readAsString();

  Map<String, dynamic> oldJson = jsonDecode(oldContent);
  Map<String, dynamic> newJson = jsonDecode(newContent);

  List<String> updates = [];

  void compareVersions(
      Map<String, dynamic> oldModule, Map<String, dynamic> newModule) {
    // 递归比较子模块
    if (oldModule.containsKey('children') &&
        newModule.containsKey('children')) {
      List<dynamic> oldChildren = oldModule['children'];
      List<dynamic> newChildren = newModule['children'];

      for (int i = 0; i < oldChildren.length; i++) {
        compareVersions(oldChildren[i], newChildren[i]);
      }
    }

    // 比较当前模块版本
    if (oldModule['version'] != newModule['version']) {
      updates.add(
          '${newModule['name']}: ${oldModule['version']} -> ${newModule['version']}');
    }
  }

  compareVersions(oldJson, newJson);

  return updates;
}

发布 pub 包本质上就是调一下 dart pub publish 的命令,在桌面端可以通过 Process.start 来调用命令。由于发布 pub 要科学上网,可以在 environment 参数中设置网络代理:

dart 复制代码
Future<void> publishModule(String name) async{
  Directory dir = Directory.current;
  String path = p.join(dir.path, 'modules', name);
  print(path);
  await publish(path, port: 7890);
}

Future<void> publish(String dir, {required int port}) async {
  Process process = await Process.start(
    workingDirectory: dir,
    'dart',
    ['pub', 'publish', '--server', 'https://pub.dartlang.org', '-f'],
    environment: {'https_proxy': 'http://127.0.0.1:$port'},
  );

  process.stdout.listen((e) {
    String value = utf8.decode(e, allowMalformed: true);
    print(value);
  });

  process.stderr.listen((e) {
    String value = utf8.decode(e, allowMalformed: true);
    print(value);
  });
}

尾声

到这里,整体的流程就跑通了,从分析模块,构建结构化数据,到记录更新日志、对比更新模块,自动发布。一套下来,可以节约很多手动管理的成本。而到这里,我只用了不到 1 个小时。如果在一年前,从编码到调试,我估计要写上两天的时间。

Trae 在其中起到了很多作用,包括树形结构的各种操作、文件转换等。 AI 确实在真实地改变着编程工作的流程。特别是对于算法不是很好的朋友,他可以为你完成很多以前完不成的事。但是,在此基础上,也不能忘记审视 AI 的内容,好的地方可以学习;不好的地方也要及时更正。它将是你的一把趁手的兵器,而非你的大脑。

相比于 那本文就到这里,以后关于 Trae 帮助我解决实际问题的场景,我也会继续分享,下次再见 ~

相关推荐
恋猫de小郭2 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker7 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴8 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭18 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab19 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
明君879971 天前
Flutter 如何给图片添加多行文字水印
前端·flutter
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
四眼肥鱼1 天前
flutter 利用flutter_libserialport 实现SQ800 串口通信
前端·flutter
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试