Flutter TolyUI 框架#07 | 案例解析与管理


theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


《Flutter TolyUI 框架》系列前言:

TolyUI张风捷特烈 打造的 Fluter 全平台应用开发 UI 框架。具备 全平台组件化源码开放响应式 四大特点。可以帮助开发者迅速构建具有响应式全平台应用软件:

开源地址: https://github.com/TolyFx/toly_ui

image.png

该系列将详细介绍 TolyUI 框架使用方式、框架开发过程中的技术知识、设计理念、难题解决等。


一、为什么需要解析管理

TolyUI 中每个组件有若干个介绍的案例,随着组件的增加,介绍的节点的维护成了一个非常繁杂的事。如下所示,一个介绍节点包括 标题介绍代码内容 四个部分:

image.png


1. 目前遇到的问题

之前案例展示的信息内容通过 Map 对象进行维护,如下红框中是 BreadcrumbDemo1 的介绍信息。这种维护方式,任何部分的变更都需要及时同步,比如视图代码变化了,展示的代码信息如果没及时更新。使用者就会产生困惑:

image.png

靠手动来维护这些数据,正在变得越来越复杂。为了 TolyUI 更好的发展,我需要寻找一条可以自动解析和管理介绍文本信息的方式。也是文想要向大家分享的内容。


2. 解析和生成

面对当前的维护困境,我给出的方案是: 解析文件自动生成代码。首先需要明确,当前解析的目标以及想要生成的内容。解析的目标自然是对当前案例代码的介绍信息。自动生成的内容有三个部分:

  • [1]. 之前手动维护的 displayNodes 将是生成代码的核心内容。放在 node.g.dart 文件中。
  • [2]. 案例的展示代码属于大文本,并没有必要全部放入映射中占据内存。所以会将其抓取到 assets 资源文件之下,点击时按需加载。
  • [3]. 案例最终想要以组件的形式展示在界面上,节点数据以字符串作为标识,通过 widget_display_map.g.dart 来维护标识与具体组件间的映射关系。

image.png


3. 注解与信息维护

巧妇难为无米之炊 ,现在最重要的问题是:在哪里,如何向案例提供描述信息? 由于 Dart 即将支持宏编程,所以我决定采用 注解 的方式来维护某个案例的介绍数据。如下所示: @DisplayNode 是我自定义的注解类,包含标题和描述两个字段:

image.png

到这里,解析生成的路线基本就确定了:

  • [1]. 遍历案例文件夹,当文件内容有 @DisplayNode 注解时,通过正则解析文件,收录数据。
  • [2]. 通过解析收录的数据,操作文件生成对应代码。
  • [3]. 解析过程中提取案例代码到资源文件。

二、案例文件的解析逻辑

NodeMeta 是解析过程中承载数据的核心对象,每个案例文件将解析成一个 NodeMeta 对象。其中变化代码字符串、案例文件路径、案例名称、展示信息四个数据内容:

```dart class NodeMeta { final String code; final String name; final String filePath; final DisplayNode display;

const NodeMeta({ required this.filePath, required this.code, required this.name, required this.display, }); ```


1. 提取案例文件信息

拿上面的 CardDemo1 为例,该文件中已经包含了 NodeMeta 对象的所有信息数据。现在关键在于如何解析文本内容,生成 NodeMeta 对象。

image.png

想要匹配文本中的关键信息,很容易想到可以使用 正则表达式 。比如想要抓取注解的字符串内容,可以通过 @DisplayNode(.|\s)*?\) 进行匹配:

image.png

抓取到 DisplayNode 配置的字符串之后,可以继续通过正则表达式来匹配对应字段的数据。如下所示,匹配其中 title 对应的字符串信息:

image.png


通过 class (?<name>\w+)(.|\s)* 匹配第一个以 class 开头的文字及其之后的所有字符串。并且 class 后的类名通过 name 组名获取:

image.png


2. 单文件解析类 DisplayFileParser

一个园林中有很多树,想着为所有树木修葺是一件很复杂的事。但修葺一棵树就比较简单,而且修葺的任务是类似的工作,一棵修的好,那么其他的都只是时间问题。解析也是一样,一开始应该着眼于个体,做好第一个任务。
如下,定义 DisplayFileParser 类负责解析 path 对应的文件内容,通过 parser 方法异步解析,生成 NodeMeta 对象:

```dart class DisplayFileParser { final String path;

static final RegExp _codeRegex = RegExp(r'class (? \w+)(.|\s) '); static final RegExp _displayRegex = RegExp(r'@DisplayNode(.|\s)?)');

const DisplayFileParser(this.path);

Future parser() async {...} ```


在解析前,可以先基于一些既定规则,过滤一下不必要的文件读取和解析。比如这里所有的案例文件名都会包含 _demo 字符串。另外,读取内容中不包含 @DisplayNode( 的文件表示不需要解析。最终通过 _parserContent 方法处理具体的解析逻辑:

dart Future<NodeMeta?> parser() async { if (!path.contains('_demo')) return null; File file = File(path); String content = await file.readAsString(); bool hasDisplay = content.contains('@DisplayNode('); if (!hasDisplay) return null; return _parserContent(path, content); }

_parserContent 方法中基于两个正则表达式匹配得到数据,从而创建 NodeMeta 对象。其中 DisplayNode.fromString 构造方法是基于字符串,来匹配创建 DisplayNode 对象:

dart NodeMeta _parserContent(String filePath, String content) { RegExpMatch? codeMatch = _codeRegex.firstMatch(content); String? code = codeMatch?.group(0); String? name = codeMatch?.namedGroup('name'); String? display = _displayRegex.firstMatch(content)?.group(0); return NodeMeta( filePath: filePath, name: name ?? '', code: code ?? '', display: DisplayNode.fromString(display ?? ''), ); }


如下所示,这样就可以解析某个案例文件,通过正则匹配得到关键的数据内容:

image.png


3. 遍历解析与收集 NodeMeta 结果

所有的案例代码都放在了项目中的 widgets 文件夹下,接下来需要遍历文件夹,来逐一解析内容。得到每个案例文件对应的 NodeMeta 数据集:

image.png

下面代码中,通过 parserDir 方法遍历一个文件夹中的文件,处理解析逻辑。并将解析的结果放入 displayMap 中。由于一个组件有若干个案例,所以这里通过 Map<String, List<NodeMeta>> 记录一个组件对应的节点列表信息。解析完后,数据如下所示:

image.png

```dart void main() async { String widgetPath = path.join(Directory.current.path, 'lib', 'view', 'widgets'); Directory widgetDir = Directory(widgetPath);

Map > displayMap = {}; await parserDir(widgetDir, displayMap); }

Future parserDir(Directory dir, Map > displayMap) async { List displays = []; List entity = dir.listSync(); for (FileSystemEntity e in entity) { if (e is File) { NodeMeta? ret = await DisplayFileParser(e.path).parser(); if (ret != null) { displays.add(ret); ret.saveCode(); } } else if (e is Directory) { await parserDir(e, displayMap); } } if (displays.isNotEmpty) { displayMap[path.basename(dir.path)] = displays; } } ```


三、使用结果生成代码

上面已经完成了对案例代码的解析,得到了所有期望获取的数据。接下来就是基于这些数据,创建并写入代码文件,完成案例代码的自动维护。


1. 代码生成的格式

代码生成的核心是 node.g.dart ,其中 queryDisplayNodes 方法可以通过组件名称得到对应的案例列表数据。注意这里使用的是 switch 进行匹配,并不是将所有的数据通过 Map 全部加入到内存中。这种运行时的取用,可以降低内存的使用,特别是对于案例介绍这样的大量数据。

image.png

另外,这里将每个组件对应的案例列表数据拆散成 独立文件。通过 partpart of 关键字建立文件间的关系。将独立文件在逻辑上视为 node.g.dart 的一部分。单独分离文件的目的在于:让代码逻辑结构更加清晰,另外,单个大文件在多人协作时更容易产生冲突。

image.png


2.组件名到组件的映射

在案例介绍的信息中,记录着 String 类型的案例组件名,但在展示时需要将组件名映射为具体的组件。由于解析过程中,所有案例的组件名都可以收集到,因此可以自动生成 widgetDisplayMap 的映射关系,将字符串映射为对应的组件:

image.png

在视图层的使用中,通过组件标识调用 queryDisplayNodes 查找案例信息列表。然后遍历列表,根据案例组件的字符串名称,基于 widgetDisplayMap 得到对应的组件:

image.png


3. 代码生成逻辑

代码本质上也是字符串,基于解析得到的 displayMap 数据,我们可以通过字符串拼接得到代码字符串,然后写入到指定的文件中。这样就完成了用代码写代码的目的:通过 FileGen类来维护代码生成的逻辑,其中依赖解析后的数据对象 displayMap,通过构造函数传入:

```dart class FileGen { final Map > displayMap;

FileGen(this.displayMap); ```


代码的生成听起来好像挺高大上的,但本质上也只是一个基于模版的填词游戏。如下是 node.g.dart 的文件模版,需要结合 displayMap 中的数据,将 partcontent 的两处内容添到指定位置:

dart String nodeTemplate(String content, String part) { return """ /// =================================================== /// Power By 张风捷特烈 --- Generated file. Do not edit. /// github: https://github.com/toly1994328 /// =================================================== $part Map<String, dynamic> queryDisplayNodes(String name){ return switch(name){ $content _ => {}, }; } """; }

其中的内容,只需要遍历 displayMap 映射元素,拼接呈成目标字符串即可。如下代码在 nodePartsnodeContents 分别表示 node.g.dart 头部引入的部分和中间的具体内容字符串列表。生成代码字符串之后,写入对应文件中,将完成代码的生成任务:

dart Future<void> genNode(String outPath) async { List<String> nodeParts = []; List<String> nodeContents = []; File file = File(outPath); displayMap.forEach((k, v) { Map<String, dynamic> items = {}; nodeParts.add("part '$k.g.dart';\n"); nodeContents.add(' "$k" => _${k}Data,\n'); ///... }); String content = nodeTemplate(nodeContents.join(), nodeParts.join()); await file.writeAsString(content); }


单个组件对应的节点列表文件也是类似,定义模版之后,遍历映射关系,向其中插入期望的字符串,得到代码:

image.png

```dart String singleNodeTemplate(String content, String name) { content = content.replaceAll(r'$', r'\$'); return """/// =================================================== /// Power By 张风捷特烈 --- Generated file. Do not edit. /// github: https://github.com/toly1994328 /// ===================================================

part of 'node.g.dart';

Map get _${name}Data => $content;"""; } ```


4. 基于命令行工具使用生成器

到这里,已经完成了解析和代码生成的逻辑,以后任何的代码或描述信息的改动,或者新增组件案例介绍。只要运行一下工具就可以自动生成代码,同步所有的更新内容。从而大大简化了书写和维护案例介绍的 劳动成本
虽然现在已经挺好用了,但是作为 dart 文件来执行会比较麻烦,还需要手动点击运行。期间的编译、运行会耗个十几秒,也不是非常优雅。

之前在 《Flutter 知识集锦 | Dart 开发命令行工具》 一文中介绍过,Dart 文件可以作为打包为命令行工具,进行使用。所以为了更好地使用工具来生成代码,我将这个代码解析生成器集成到 toly 命令行工具中:

image.png

也就是说,当案例信息有任何变化,我只需要在命令行输入 toly ui ,就可以在 100ms 内完成代码生成来更新所有的案例信息。

image.png

工具可以让人从枯燥的繁杂任务中解脱出来,特别是重复性的有明确规则的任务。联合收割机、卡车、电饭锅,优秀的工具能更精准、迅速且正确地完成特定任务,从而可以大大提升生产的效率。希望 tolyui 中对于案例的解析管理,能让你对工具的使用有所启发。那本就到这里,谢谢观看 ~


四、小结

到这里 TolyUI 就完成了一个可以灵活定制的下拉菜单 TolyDropMenu。目前为止,TolyUI 已经完成了响应式布局和反馈模块的核心功能。导航模块也完成了三个非常重要的组件,下一步会继续对导航模块进行开发,敬请期待 ~

image.png

感谢你关注 tolyui 的成长,如果喜欢,也希望你能在 github 中点赞支持\~

github 开源地址: https://github.com/TolyFx/toly_ui\ TolyUI 官方案例演示网站:http://toly1994.com/ui

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值