引言 - 組件依賴關(guān)系
寫(xiě)過(guò) flutter 的同學(xué)可能都用過(guò)命令行查看項(xiàng)目中各組件與主工程間的依賴關(guān)系亿遂,例如這樣一個(gè)項(xiàng)目:
image.png
- 紅色框住的為主工程
- 綠色框住的為組件模塊(各模塊內(nèi)部還依賴了一些三方庫(kù))
執(zhí)行命令行可查看組件依賴樹(shù):
// 遍歷組件依賴節(jié)點(diǎn)
pub deps --json
結(jié)果如下:
{
"root": "flutter_dependency_draw",
"packages": [
{
"name": "flutter_dependency_draw",
"version": "1.0.0+1",
"kind": "root",
"source": "root",
"dependencies": [
"flutter",
"cupertino_icons",
"common",
"menu",
"order",
"trade",
"cart",
"splash",
"upgrade",
"flutter_test",
"flutter_lints",
"yaml",
"gviz"
]
},
{
"name": "gviz",
"version": "0.4.0",
"kind": "dev",
"source": "hosted",
"dependencies": []
},
{
"name": "yaml",
"version": "3.1.1",
"kind": "dev",
"source": "hosted",
"dependencies": [
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "string_scanner",
"version": "1.1.1",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"source_span"
]
},
{
"name": "source_span",
"version": "1.9.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "term_glyph",
"version": "1.2.1",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "path",
"version": "1.8.2",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "collection",
"version": "1.16.0",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "flutter_lints",
"version": "2.0.2",
"kind": "dev",
"source": "hosted",
"dependencies": [
"lints"
]
},
{
"name": "lints",
"version": "2.0.1",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "flutter_test",
"version": "0.0.0",
"kind": "dev",
"source": "sdk",
"dependencies": [
"flutter",
"test_api",
"path",
"fake_async",
"clock",
"stack_trace",
"vector_math",
"async",
"boolean_selector",
"characters",
"collection",
"matcher",
"material_color_utilities",
"meta",
"source_span",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "stream_channel",
"version": "2.1.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"async"
]
},
{
"name": "async",
"version": "2.9.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "meta",
"version": "1.8.0",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.1.5",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.12",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"stack_trace"
]
},
{
"name": "stack_trace",
"version": "1.10.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"path"
]
},
{
"name": "characters",
"version": "1.2.1",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "boolean_selector",
"version": "2.1.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "vector_math",
"version": "2.1.2",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "clock",
"version": "1.1.1",
"kind": "transitive",
"source": "hosted",
"dependencies": []
},
{
"name": "fake_async",
"version": "1.3.1",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "test_api",
"version": "0.4.12",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph",
"matcher"
]
},
{
"name": "flutter",
"version": "0.0.0",
"kind": "direct",
"source": "sdk",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"vector_math",
"sky_engine"
]
},
{
"name": "sky_engine",
"version": "0.0.99",
"kind": "transitive",
"source": "sdk",
"dependencies": []
},
{
"name": "upgrade",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"common",
"net"
]
},
{
"name": "net",
"version": "0.0.1",
"kind": "transitive",
"source": "path",
"dependencies": [
"flutter",
"dio"
]
},
{
"name": "dio",
"version": "4.0.6",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"http_parser",
"path"
]
},
{
"name": "http_parser",
"version": "4.0.2",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "typed_data",
"version": "1.3.2",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"collection"
]
},
{
"name": "common",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter"
]
},
{
"name": "splash",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"login",
"common"
]
},
{
"name": "login",
"version": "0.0.1",
"kind": "transitive",
"source": "path",
"dependencies": [
"flutter",
"common_ui",
"net"
]
},
{
"name": "common_ui",
"version": "0.0.1",
"kind": "transitive",
"source": "path",
"dependencies": [
"flutter",
"flutter_screenutil"
]
},
{
"name": "flutter_screenutil",
"version": "5.7.0",
"kind": "transitive",
"source": "hosted",
"dependencies": [
"flutter"
]
},
{
"name": "cart",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"common_ui"
]
},
{
"name": "trade",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"common_ui",
"net"
]
},
{
"name": "order",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"common_ui",
"net"
]
},
{
"name": "menu",
"version": "0.0.1",
"kind": "direct",
"source": "path",
"dependencies": [
"flutter",
"common_ui",
"net"
]
},
{
"name": "cupertino_icons",
"version": "1.0.5",
"kind": "direct",
"source": "hosted",
"dependencies": []
}
],
"sdks": [
{
"name": "Dart",
"version": "2.18.6"
},
{
"name": "Flutter",
"version": "3.3.10"
}
],
"executables": []
}
對(duì)于這種文本樹(shù)狀結(jié)構(gòu)已维,不夠直觀且存在重復(fù)的連線關(guān)系皇钞,遠(yuǎn)遠(yuǎn)達(dá)不到初衷預(yù)期。
依賴關(guān)系可視化 - 轉(zhuǎn)樹(shù)狀 PNG
為此,小編使用腳本的方式涩赢,將上述樹(shù)狀結(jié)構(gòu)戈次,轉(zhuǎn)成更為直觀的樹(shù)狀連線圖。
1. 實(shí)現(xiàn)方案
使用gviz,通過(guò) DOT (一種描述語(yǔ)言來(lái)定義圖形)Graphviz 實(shí)現(xiàn)圖形可視化筒扒。
最終產(chǎn)物可生成 PNG怯邪、PDF、SVG 等格式花墩。
2. 前置工作
- 以 Mac 為例悬秉,需要在電腦使用命令行工具安裝
graphviz
// 我的電腦是 m1,命令行如下:
arch -arm64 brew install graphviz
- 在需要生成依賴關(guān)系圖形的項(xiàng)目根目錄下冰蘑,找到
pubspec.yaml
文件和泌,添加如下依賴
dev_dependencies:
yaml: ^3.1.1
gviz: ^0.4.0
3. (直接copy可用)執(zhí)行腳本生成依賴關(guān)系圖
import 'dart:convert';
import 'dart:io';
import 'package:gviz/gviz.dart';
import 'package:yaml/yaml.dart' as yaml;
void main() async {
final projectPath = await _getProjectPath();
final file = File('$projectPath/pubspec.yaml');
final fileContent = file.readAsStringSync();
final yamlMap = yaml.loadYaml(fileContent) as yaml.YamlMap;
final appName = yamlMap['name'].toString();
print('開(kāi)始 ...');
final dependencyContent = await _getComponentDependencyTree(
projectPath: projectPath,
);
// 獲取所有的組件依賴
print('... 開(kāi)始遍歷組件依賴節(jié)點(diǎn)');
print(dependencyContent);
final dependencyNodes = _traversalComponentDependencyTree(dependencyContent);
print('... 完成遍歷組件依賴節(jié)點(diǎn)');
final graph = Gviz(
name: appName,
graphProperties: {
'pad': '0.5',
'nodesep': '1',
'ranksep': '2',
},
edgeProperties: {
'fontcolor': 'gray',
},
);
print('... 開(kāi)始轉(zhuǎn)換 dot 節(jié)點(diǎn)');
_generateDotByNodes(
dependencyNodes,
graph: graph,
edgeCache: <String>[],
);
print('... 完成轉(zhuǎn)換 dot 節(jié)點(diǎn)');
final dotDirectoryPath = '$projectPath/dotGenerateDir';
final dotDirectory = Directory(dotDirectoryPath);
if (!dotDirectory.existsSync()) {
await dotDirectory.create();
print('... 創(chuàng)建 dotGenerate 文件夾');
}
final dotFileName = '$appName.dot';
final dotPngName = '$appName.png';
final dotFile = File('$dotDirectoryPath/$dotFileName');
final dotPngFile = File('$dotDirectoryPath/$dotPngName');
if (dotFile.existsSync()) {
await dotFile.delete();
print('... 刪除原有 dot 生成文件');
}
if (dotPngFile.existsSync()) {
await dotPngFile.delete();
print('... 刪除原有 dot 依賴關(guān)系圖');
}
await dotFile.create();
final dotResult = await dotFile.writeAsString(graph.toString());
print('dot 文件生成成功: ${dotResult.path}');
print('... 開(kāi)始生成 dot png');
await _runCommand(
executable: 'dot',
projectPath: projectPath,
commandArgs: [
'$dotDirectoryPath/$dotFileName',
'-T',
'png',
'-o',
'$dotDirectoryPath/$dotPngName'
],
);
print('png 文件生成成功:$dotDirectoryPath/$dotPngName');
await Process.run(
'open',
[dotDirectoryPath],
);
}
// 忽略這些組件庫(kù),不需要顯示出來(lái)
const List<String> ignoreDependency = <String>[
'flutter',
'flutter_test',
'flutter_lints',
'cupertino_icons',
'gviz',
'yaml',
];
/// 獲取組件依賴樹(shù)
Future<String> _getComponentDependencyTree({
required String projectPath,
}) {
return _runCommand(
projectPath: projectPath,
commandArgs: ['pub', 'deps', '--json'],
).then(
(value) {
if (value.contains('dependencies:') &&
value.contains('dev dependencies:')) {
final start = value.indexOf('dependencies:');
final end = value.indexOf('dev dependencies:');
return value.substring(start, end);
} else {
return value;
}
},
);
}
/// 遍歷組件節(jié)點(diǎn)
List<DependencyNode> _traversalComponentDependencyTree(
String dependencyContent,
) {
final dependencyJson = jsonDecode(dependencyContent) as Map<String, dynamic>;
final packages = dependencyJson['packages'] as List<dynamic>;
final dependencyNodeList =
packages.map((e) => DependencyNode.fromMap(e)).toList();
final rootNode =
dependencyNodeList.firstWhere((element) => element.isRootNode);
DependencyNode? matchNode(String nodeName) {
DependencyNode? target;
try {
target =
dependencyNodeList.firstWhere((element) => element.name == nodeName);
} catch (_) {
print(_);
}
return target;
}
void mapDependencies(DependencyNode node) {
final dependencies = node.dependencies;
for (int index = 0; index < dependencies.length; index++) {
final itemName = dependencies[index];
if (!ignoreDependency.contains(itemName)) {
final itemNode = matchNode(itemName);
if (itemNode != null) {
mapDependencies(itemNode);
node.children.add(itemNode);
}
}
}
}
mapDependencies(rootNode);
// 獲取子集中所有的依賴
void fetchChildrenDependency(
DependencyNode node, {
required List<String> dependencyContainer,
bool containSelf = false,
}) {
if (node.children.isEmpty) {
return;
} else {
for (int index = 0; index < node.children.length; index++) {
final itemNode = node.children[index];
if (containSelf && !dependencyContainer.contains(itemNode.name)) {
dependencyContainer.add(itemNode.name);
}
for (var element in itemNode.children) {
fetchChildrenDependency(
element,
dependencyContainer: dependencyContainer,
containSelf: true,
);
}
}
}
}
// 去掉重復(fù)的連線關(guān)系
void filterRepeatDependency(DependencyNode node) {
final childrenDependencyContainer = <String>[];
fetchChildrenDependency(node,
dependencyContainer: childrenDependencyContainer);
if (childrenDependencyContainer.isNotEmpty) {
node.children.removeWhere(
(element) => childrenDependencyContainer.contains(element.name));
}
for (var childNode in node.children) {
filterRepeatDependency(childNode);
}
}
filterRepeatDependency(rootNode);
return [rootNode];
}
/// 獲取項(xiàng)目根路徑
Future<String> _getProjectPath() async {
final originProjectPath = await Process.run(
'pwd',
[],
);
final projectPath = (originProjectPath.stdout as String).replaceAll(
'\n',
'',
);
return projectPath;
}
/// 轉(zhuǎn)換生成 dot 繪制節(jié)點(diǎn)
void _generateDotByNodes(
List<DependencyNode> nodes, {
required Gviz graph,
required List<String> edgeCache,
}) {
if (nodes.isEmpty) {
return;
}
for (int index = 0; index < nodes.length; index++) {
final itemNode = nodes[index];
final from = '${itemNode.name}\n${itemNode.version}';
if (!graph.nodeExists(from)) {
// 繪制節(jié)點(diǎn)
graph.addNode(
from,
properties: {
'color': 'black',
'shape': 'rectangle',
'margin': '1,0.8',
'penwidth': '7',
'style': 'filled',
'fillcolor': 'gray',
'fontsize': itemNode.isLevel1Node ? '60' : '55',
},
);
}
final toArr = itemNode.children.map((e) => '${e.name}\n${e.version}').toList();
for (var element in toArr) {
// 繪制連線
final edgeKey = '$from-$element';
if (!edgeCache.contains(edgeKey)) {
graph.addEdge(
from,
element,
properties: {
'penwidth': '2',
'style': 'dashed',
'arrowed': 'vee',
// 'weight': '2',
},
);
edgeCache.add(edgeKey);
}
}
_generateDotByNodes(
itemNode.children,
graph: graph,
edgeCache: edgeCache,
);
}
}
/// 執(zhí)行命令行
Future<String> _runCommand({
String executable = 'flutter',
required String projectPath,
required List<String> commandArgs,
}) {
return Process.run(
executable,
commandArgs,
runInShell: true,
workingDirectory: projectPath,
).then((result) => result.stdout as String);
}
/// 依賴的節(jié)點(diǎn)
class DependencyNode {
final String name;
final String version;
final String kind;
final String source;
final List<String> dependencies;
final children = <DependencyNode>[];
bool isLevel1Node = true; //是否一級(jí)節(jié)點(diǎn)
factory DependencyNode.fromMap(Map<String, dynamic> map) {
return DependencyNode(
name: map['name'] as String,
version: map['version'] as String,
kind: map['kind'] as String,
source: map['source'] as String,
dependencies: (map['dependencies'] as List<dynamic>)
.map((e) => e as String)
.toList(),
);
}
bool get isRootNode => kind == 'root';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DependencyNode &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
DependencyNode({
required this.name,
required this.version,
required this.kind,
required this.source,
required this.dependencies,
});
}
4. 產(chǎn)物
最終會(huì)生成 依賴關(guān)系.png 文件, 位于當(dāng)前項(xiàng)目的 dotGenerateDir 目錄下祠肥。
- 上述結(jié)構(gòu)的demo(flutter_dependency_draw)生產(chǎn)的依賴關(guān)系圖如下:
image.png
demo已上傳: https://github.com/liyufengrex/flutter_dependency_draw