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 帮助我解决实际问题的场景,我也会继续分享,下次再见 ~

相关推荐
每次的天空39 分钟前
Android学习总结之Binder篇
android·学习·binder
峥嵘life1 小时前
Android 有线网开发调试总结
android
是店小二呀2 小时前
【算法-链表】链表操作技巧:常见算法
android·c++·算法·链表
zhifanxu3 小时前
Kotlin 遍历
android·开发语言·kotlin
肥肥呀呀呀4 小时前
flutter 资料收集
前端·flutter
程序猿阿伟4 小时前
《社交应用架构生存战:React Native与Flutter的部署容灾决胜法则》
flutter·react native·架构
肥肥呀呀呀4 小时前
flutter利用 injectable和injectable_generator 自动get_it注册
flutter
追随远方4 小时前
Android NDK版本迭代与FFmpeg交叉编译完全指南
android·ffmpeg
柯南二号14 小时前
Android Studio根目录下创建多个可运行的模块
android·ide·android studio
恋猫de小郭17 小时前
Compose Multiplatform iOS 稳定版发布:可用于生产环境,并支持 hotload
android·flutter·macos·ios·kotlin·cocoa