随着 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 详细分析一下:
核心实现解析:
-
StringBuffer 的使用 [1]
- 使用 StringBuffer 而不是直接字符串拼接
- 可以高效处理大量字符串拼接操作
- 避免产生中间字符串对象
-
树形标记符号 [2]
- └── : 表示当前层级的最后一个节点
- ├── : 表示当前层级的非最后节点
- 通过 isLast 参数来判断使用哪种标记
-
当前节点输出 [3]
- prefix : 当前缩进前缀
- marker : 节点标记符号
- name (version) : 显示模块信息
-
子节点缩进处理 [4]
- 如果父节点是最后一个(isLast=true):用空格
- 如果父节点不是最后一个:用竖线 │
- 这样保证了树形结构的连续性
-
递归处理子节点 [5]
- 遍历所有子节点
- 递归调用 displayTree
- 传递新的缩进前缀
- 标记最后一个子节点
-
返回结果 [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 帮助我解决实际问题的场景,我也会继续分享,下次再见 ~