Dart 腳本:生成樹(shù)狀圖,模塊依賴可視化

引言 - 組件依賴關(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

DOT 語(yǔ)言相關(guān)參考資料:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末武氓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子搪柑,更是在濱河造成了極大的恐慌聋丝,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件工碾,死亡現(xiàn)場(chǎng)離奇詭異弱睦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)渊额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)况木,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人旬迹,你說(shuō)我怎么就攤上這事火惊。” “怎么了奔垦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵屹耐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我椿猎,道長(zhǎng)惶岭,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任犯眠,我火速辦了婚禮按灶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筐咧。我一直安慰自己鸯旁,他們只是感情好噪矛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著铺罢,像睡著了一般艇挨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上畏铆,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天雷袋,我揣著相機(jī)與錄音,去河邊找鬼辞居。 笑死楷怒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瓦灶。 我是一名探鬼主播鸠删,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贼陶!你這毒婦竟也來(lái)了刃泡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤碉怔,失蹤者是張志新(化名)和其女友劉穎烘贴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撮胧,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桨踪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了芹啥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锻离。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墓怀,靈堂內(nèi)的尸體忽然破棺而出汽纠,到底是詐尸還是另有隱情,我是刑警寧澤傀履,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布虱朵,位于F島的核電站,受9級(jí)特大地震影響钓账,放射性物質(zhì)發(fā)生泄漏碴犬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一官扣、第九天 我趴在偏房一處隱蔽的房頂上張望翅敌。 院中可真熱鬧羞福,春花似錦惕蹄、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)遭顶。三九已至,卻和暖如春泪蔫,著一層夾襖步出監(jiān)牢的瞬間棒旗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工撩荣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铣揉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓餐曹,卻偏偏與公主長(zhǎng)得像逛拱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子台猴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容