Flutter TolyUI 框架#07 | 案例解析与管理
《Flutter TolyUI 框架》系列前言:TolyUI 是 张风捷特烈 打造的 Fluter 全平台应用开发 UI 框架。具备 全平台、组件化、源码开放、响应式 四大特点。可以帮助开发者迅速构建具有响应式全平台应用软件:
开源地址:https://github.com/TolyFx/toly_ui
image.png该系列将详细介绍 TolyUI 框架使用方式、框架开发过程中的技术知识、设计理念、难题解决等。
一、为什么需要解析管理TolyUI 中每个组件有若干个介绍的案例,随着组件的增加,介绍的节点的维护成了一个非常繁杂的事。如下所示,一个介绍节点包括 标题、介绍、代码、内容 四个部分:
image.png1. 目前遇到的问题之前案例展示的信息内容通过 Map 对象进行维护,如下红框中是 BreadcrumbDemo1 的介绍信息。这种维护方式,任何部分的变更都需要及时同步,比如视图代码变化了,展示的代码信息如果没及时更新。使用者就会产生困惑:
image.png靠手动来维护这些数据,正在变得越来越复杂。为了 TolyUI 更好的发展,我需要寻找一条可以自动解析和管理介绍文本信息的方式。也是文想要向大家分享的内容。
2. 解析和生成面对当前的维护困境,我给出的方案是: 解析文件 与 自动生成代码。首先需要明确,当前解析的目标以及想要生成的内容。解析的目标自然是对当前案例代码的介绍信息。自动生成的内容有三个部分:
[1]. 之前手动维护的 displayNodes 将是生成代码的核心内容。放在 node.g.dart 文件中。[2]. 案例的展示代码属于大文本,并没有必要全部放入映射中占据内存。所以会将其抓取到 assets 资源文件之下,点击时按需加载。[3]. 案例最终想要以组件的形式展示在界面上,节点数据以字符串作为标识,通过 widget_display_map.g.dart 来维护标识与具体组件间的映射关系。image.png3. 注解与信息维护巧妇难为无米之炊 ,现在最重要的问题是:在哪里,如何向案例提供描述信息? 由于 Dart 即将支持宏编程,所以我决定采用 注解 的方式来维护某个案例的介绍数据。如下所示:@DisplayNode 是我自定义的注解类,包含标题和描述两个字段:
image.png到这里,解析生成的路线基本就确定了:
[1]. 遍历案例文件夹,当文件内容有 @DisplayNode 注解时,通过正则解析文件,收录数据。[2]. 通过解析收录的数据,操作文件生成对应代码。[3]. 解析过程中提取案例代码到资源文件。二、案例文件的解析逻辑NodeMeta 是解析过程中承载数据的核心对象,每个案例文件将解析成一个 NodeMeta 对象。其中变化代码字符串、案例文件路径、案例名称、展示信息四个数据内容:
classNodeMeta{
finalStringcode;
finalStringname;
finalStringfilePath;
finalDisplayNodedisplay;
constNodeMeta({
requiredthis.filePath,
requiredthis.code,
requiredthis.name,
requiredthis.display,
1. 提取案例文件信息拿上面的 CardDemo1 为例,该文件中已经包含了 NodeMeta 对象的所有信息数据。现在关键在于如何解析文本内容,生成 NodeMeta 对象。
image.png想要匹配文本中的关键信息,很容易想到可以使用 正则表达式 。比如想要抓取注解的字符串内容,可以通过 @DisplayNode(.|\s)*?\) 进行匹配:
image.png抓取到 DisplayNode 配置的字符串之后,可以继续通过正则表达式来匹配对应字段的数据。如下所示,匹配其中 title 对应的字符串信息:
image.png通过 class (?name\w+)(.|\s)* 匹配第一个以 class 开头的文字及其之后的所有字符串。并且 class 后的类名通过 name 组名获取:
image.png2. 单文件解析类 DisplayFileParser一个园林中有很多树,想着为所有树木修葺是一件很复杂的事。但修葺一棵树就比较简单,而且修葺的任务是类似的工作,一棵修的好,那么其他的都只是时间问题。解析也是一样,一开始应该着眼于个体,做好第一个任务。
如下,定义 DisplayFileParser 类负责解析 path 对应的文件内容,通过 parser 方法异步解析,生成 NodeMeta 对象:
classDisplayFileParser{
finalStringpath;
staticfinalRegExp_codeRegex=RegExp(r'class(?name\w+)(.|\s)*');
staticfinalRegExp_displayRegex=RegExp(r'@DisplayNode(.|\s)*?\)');
constDisplayFileParser(this.path);
FutureNodeMeta?parser()async{...}
在解析前,可以先基于一些既定规则,过滤一下不必要的文件读取和解析。比如这里所有的案例文件名都会包含 _demo 字符串。另外,读取内容中不包含 @DisplayNode( 的文件表示不需要解析。最终通过 _parserContent 方法处理具体的解析逻辑:
FutureNodeMeta?parser()async{
if(!path.contains('_demo'))returnnull;
Filefile=File(path);
Stringcontent=awaitfile.readAsString();
boolhasDisplay=content.contains('@DisplayNode(');
if(!hasDisplay)returnnull;
return_parserContent(path,content);
}
_parserContent 方法中基于两个正则表达式匹配得到数据,从而创建 NodeMeta 对象。其中 DisplayNode.fromString 构造方法是基于字符串,来匹配创建 DisplayNode 对象:
NodeMeta_parserContent(StringfilePath,Stringcontent){
RegExpMatch?codeMatch=_codeRegex.firstMatch(content);
String?code=codeMatch?.group(0);
String?name=codeMatch?.namedGroup('name');
String?display=_displayRegex.firstMatch(content)?.group(0);
returnNodeMeta(
filePath:filePath,
name:name??'',
code:code??'',
display:DisplayNode.fromString(display??''),
}
如下所示,这样就可以解析某个案例文件,通过正则匹配得到关键的数据内容:
image.png3. 遍历解析与收集 NodeMeta 结果所有的案例代码都放在了项目中的 widgets 文件夹下,接下来需要遍历文件夹,来逐一解析内容。得到每个案例文件对应的 NodeMeta 数据集:
image.png下面代码中,通过 parserDir 方法遍历一个文件夹中的文件,处理解析逻辑。并将解析的结果放入 displayMap 中。由于一个组件有若干个案例,所以这里通过 MapString, ListNodeMeta 记录一个组件对应的节点列表信息。解析完后,数据如下所示:
image.pngvoidmain()async{
StringwidgetPath=path.join(Directory.current.path,'lib','view','widgets');
DirectorywidgetDir=Directory(widgetPath);
MapString,ListNodeMetadisplayMap=
awaitparserDir(widgetDir,displayMap);
}
FuturevoidparserDir(Directorydir,MapString,ListNodeMetadisplayMap)async{
ListNodeMetadisplays=
ListFileSystemEntityentity=dir.listSync();
for(FileSystemEntityeinentity){
if(eisFile){
NodeMeta?ret=awaitDisplayFileParser(e.path).parser();
if(ret!=null){
displays.add(ret);
ret.saveCode();
}
}elseif(eisDirectory){
awaitparserDir(e,displayMap);
}
}
if(displays.isNotEmpty){
displayMap[path.basename(dir.path)]=displays;
}
}
三、使用结果生成代码上面已经完成了对案例代码的解析,得到了所有期望获取的数据。接下来就是基于这些数据,创建并写入代码文件,完成案例代码的自动维护。
1. 代码生成的格式代码生成的核心是 node.g.dart ,其中 queryDisplayNodes 方法可以通过组件名称得到对应的案例列表数据。注意这里使用的是 switch 进行匹配,并不是将所有的数据通过 Map 全部加入到内存中。这种运行时的取用,可以降低内存的使用,特别是对于案例介绍这样的大量数据。
image.png另外,这里将每个组件对应的案例列表数据拆散成 独立文件。通过 part 和 part of 关键字建立文件间的关系。将独立文件在逻辑上视为 node.g.dart 的一部分。单独分离文件的目的在于:让代码逻辑结构更加清晰,另外,单个大文件在多人协作时更容易产生冲突。
image.png2.组件名到组件的映射在案例介绍的信息中,记录着 String 类型的案例组件名,但在展示时需要将组件名映射为具体的组件。由于解析过程中,所有案例的组件名都可以收集到,因此可以自动生成 widgetDisplayMap 的映射关系,将字符串映射为对应的组件:
image.png在视图层的使用中,通过组件标识调用 queryDisplayNodes 查找案例信息列表。然后遍历列表,根据案例组件的字符串名称,基于 widgetDisplayMap 得到对应的组件:
image.png3. 代码生成逻辑代码本质上也是字符串,基于解析得到的 displayMap 数据,我们可以通过字符串拼接得到代码字符串,然后写入到指定的文件中。这样就完成了用代码写代码的目的:通过 FileGen类来维护代码生成的逻辑,其中依赖解析后的数据对象 displayMap,通过构造函数传入:
classFileGen{
finalMapString,ListNodeMetadisplayMap;
FileGen(this.displayMap);
代码的生成听起来好像挺高大上的,但本质上也只是一个基于模版的填词游戏。如下是 node.g.dart 的文件模版,需要结合 displayMap 中的数据,将 part 和 content 的两处内容添到指定位置:
StringnodeTemplate(Stringcontent,Stringpart){
return"""
///===================================================
///PowerBy张风捷特烈---Generatedfile.Donotedit.
///github:https://github.com/toly1994328
///===================================================
$part
MapString,dynamicqueryDisplayNodes(Stringname){
returnswitch(name){
$content_={},
}
""";
}
其中的内容,只需要遍历 displayMap 映射元素,拼接呈成目标字符串即可。如下代码在 nodeParts 和 nodeContents 分别表示 node.g.dart 头部引入的部分和中间的具体内容字符串列表。生成代码字符串之后,写入对应文件中,将完成代码的生成任务:
FuturevoidgenNode(StringoutPath)async{
ListStringnodeParts=
ListStringnodeContents=
Filefile=File(outPath);
displayMap.forEach((k,v){
MapString,dynamicitems=
nodeParts.add("part'$k.g.dart';\n");
nodeContents.add('"$k"=_${k}Data,\n');
///...
Stringcontent=nodeTemplate(nodeContents.join(),nodeParts.join());
awaitfile.writeAsString(content);
}
单个组件对应的节点列表文件也是类似,定义模版之后,遍历映射关系,向其中插入期望的字符串,得到代码:
image.pngStringsingleNodeTemplate(Stringcontent,Stringname){
content=content.replaceAll(r'$',r'\$');
return"""///===================================================
///PowerBy张风捷特烈---Generatedfile.Donotedit.
///github:https://github.com/toly1994328
///===================================================
partof'node.g.dart';
MapString,dynamicget_${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
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线