为什么要自己写路由框架?
开发中最常用到的就是路由了,很多入坑flutter的小伙伴们也必然百度,google了一大堆的路由框架,五花八门,功能也是千奇百怪,我也是看了好多的路由框架。
路由框架那么多,到底哪一款适合自己。
我们基本都会遇到这样几个问题:
- 我就用个路由功能,但是它却自带了太多的其他功能,用不到,不用又浪费。
- 需要某个功能的时候,框架还不支持,可能已经有计划支持,但实际时间不确定,除了fork人家代码,改人家代码,还能有什么办法呢。
- 使用的时候遇到了一个问题,提了issue,但人不理你或者几个月后理你了,emmmm。
所以,最好的办法是什么,自己写一个。
所以,一个Idea,一个框架就这样诞生了。
思考路由框架的结构应该是什么样的?
很多时候,想要学习一个三方的优秀框架,如果一味地去看源码,可能会一团乱麻,并不是学习能力不够,是学习的方式不对。我们首先应该去思考,了解整个框架的基本构成,结构,然后再针对性地去了解,去学习。那么,写一个框架的前提,肯定也是构思框架应该具有什么,框架内部应该是一个什么样的流程。
当然,写之前也不需要考虑得太全面,那样会导致你不知道从何下手。
而一个最简易的路由框架,应该需要包含以下三个部分。后面就来手动实操一下最简易的一个路由框架吧。
实现一个最基础的路由框架
初始化项目
flutter create --template=package --project-name=router_anno
- 执行上述命令生成一个package项目,并在yaml中引用source_gen;如下:
dependencies:
source_gen: 1.0.3
build: 2.0.3
analyzer: 1.7.1
- 在lib同级目录创建一个build.yaml文件,并写入如下配置:
targets:
$default:
builders:
tdf_router_anno|tdf_router_builder:
enabled: true
builders:
tdf_router_builder:
import: 'package:tdf_router_anno/builder.dart' #1
builder_factories: ['tdfRouterBuilder'] #2
build_extensions: { '.dart': ['.tdf_router.dart'] } #3 路由文件后缀
auto_apply: dependents
build_to: source
applies_builders: ["source_gen|combining_builder"]
具体的参数意义和用法,官方可以直接找到,我就不详细赘述了。 3. 看到第二步中的#1处,找到这个builder.dart,在其内配置注解的生成器:
Builder tdfRouterBuilder(BuilderOptions options) =>
LibraryBuilder(TZRouterGenerator(), generatedExtension: '.tdf_router.dart');
到这里,已经将注解相关的配置都完成了,后面就是在生成器TZRouterGenerator中编码了。
定义、注册注解
在flutter中,定义一个注解十分简单,定义一个普通的类,其就能作为注解被使用。
// TZRouter注解
class TZRouter {
final String path; // 路由path
const TZRouter(
{required this.path});
}
上面的注解中,只定义了一个最基础的路由注解的path参数,先以最基本的构成来实现。其他的参数会在文末拓展中进行添加。
注解定义完成后,怎么跟生成器绑定呢?
class TZRouterGenerator extends GeneratorForAnnotation<TZRouter> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "";
}
上述代码中,可以看出,GeneratorForAnnotation内的泛型参数,传入了TZRouter,也就是说注解生成器TZRouterGenerator只会解析TZRouter这个注解的数据。(绑定)
确定想要生成什么样的代码
思考原生在跳转页面的时候,需要的是什么信息。
- 路由path
- path对应的Widget
getRouterPath() {
return "test/test1";
}
Widget getPageWidget(params) {
return MyApp(params);
}
我们生成以上两个方法即可。
到了这一步,我们已经拥有了所有的路由信息,下一步就是将其注册到flutter主工程中。
路由注册
含义:
将path和对应Widget,注册到路由管理map中。
这里有两种实现思路,
- 一种仍旧是采用注解的形式,定义一个新的注解,该注解给flutter主工程的main方法上。之后在注解解析时,去分析flutter整个项目的所有#3处作为后缀的文件,读取其路径并import,获取其内path和widget,注册到统合map中。
(这里就不好意思了,思路是这样的,但是我没写,我觉得太麻烦了,且通过注解的形式要分析整个项目,时间上比较长,flutter注解分析代码确实慢,跟原生要经过编译一个意思,且生成代码的方式编写较复杂。)
- 通过python,分析flutter主模块内依赖的所有模块,并找到其引用的位置,进入lib源代码目录,查找到所有#3处后缀的文件。后续就是跟注解的形式一模一样了。这种方式分析模块依赖很简单且不需要经过编译这一步,从命令开始执行到代码生成,一共就零点几秒。
大致讲一下怎么分析吧。
2.*版本的flutter,可以通过解析.packages文件,拿到所有依赖库的lib目录所在,再执行文件夹遍历即可。
3.*版本的flutter,可以通过分析.dart_tool/package_config.json文件,python分析json文件更是手到擒来了。
具体的实现方式这边就不写啦,没啥好多说的,主要还是思路。
拓展(框架迭代)
公司的路由框架还是经过了好几次的迭代了,有新的需求,就有新的迭代嘛。
迭代1.自动生成路由文档
第一次迭代,给了路由文档。那一天,领导说,flutter业务迭代比较快,但是路由管理这方面,还没有做起来,需要有一个完善的文档知道现在有哪些页面,具体在哪个模块内。
这次的迭代给路由注解增加了一个note必填参数,其中填写的是该路由页面的描述。路由注解在生成路由描述文件时,多生成一个方法如下:
getNote() {
return "页面描述文案";
}
再然后就通过python分析并生成代码后,再生成一份相应的路由表即可。
路由文档作用
- 能查看当前flutter具体的使用情况,
- 能快速查找定位页面所在位置,所在模块。
- 快速查看是否开发过类似页面或者相应业务的页面,这对于前期不熟悉业务的时候,很有帮助。
迭代2.统一完整的flutter页面入口
这一次迭代,是为了有一个快速入口,能够直接打开指定页面,如果原生已经有一个快速口子,那么可以将flutter的页面也接入到其内。
本次迭代对路由注解新增了一个isMainPage参数,代表该页面是否是某一块业务的首页。这里涉及到一个问题,如果某一个页面它需要接收一些参数才能正常展示,那么它就不需要配置到快速入口那边,因为快速入口一个是给开发使用,一个是给产品使用,产品肯定是不知道应该传递什么参数的。
实现方式还是采用python分析后,自动再生成一个debug的入口页面,并将配置了isMainPage为true的页面入坑按钮自动生成到入口页面内。
路由描述文件也相应增加一个方法:
bool isMainPage() {
return false;
}
迭代3.适配Flutter2.2.3 -> 3.3.10
本次迭代没什么好说的,就是分析文件由原来的.packages改为了.dart_tool/package_config.json
迭代4.注解参数增加params
为什么会有这次迭代呢
- 原因1
我们写代码时,需要写很多模版代码。举个例子,当Widget加了注解之后,原先采用的是单Map<String, dynamic类型的参数,作为Widget的唯一参数传入Widget,构造Widget。如下:
class MyApp extends StatelessWidget {
final Map<String, dynamic> params;
MyApp(this.params);
@override
Widget build(BuildContext context) {
}
}
如果想要在params内取一个key为name的String数据,那么就需要编写如下这样的模版代码:
late String name;
if (params.containsKey('name') && params['name'] is String) {
name = params['name'];
} else {
name = '';
}
或者使用 ?? 操作符去简化代码,但是遇到null情况或者外部传入的类型不是预期类型时,总是难以避免写一些上述的if模版代码
- 原因2 当页面入参复杂时,由于各人习惯不同,从params中获取参数的时机也各不相同,导致其他人使用已存在的页面时,无法第一时间知道需要传入哪些参数。可能造成参数漏传或错传。所以在注解中增加一个params参数,用法如下:
@TZRouter(
path: 'test/test1',
params: {
"name": "xj",
"age": 18,
"male": true,
},
defaultParameterFieldName: "params", # 这个可以不传,默认为"params"
note: "")
class MyApp extends StatelessWidget {
final Map<String, dynamic> params;
MyApp(this.params);
@override
Widget build(BuildContext context) {
}
}
加上注解后,执行注解生成代码命令,会生成如下拓展:
extension MyAppRouterParamsExtension on MyApp {
String get name {
if (params.containsKey('name') && params['name'] is String) {
return params['name'];
}
return 'xj';
}
int get age {
if (params.containsKey('age') && params['age'] is int) {
return params['age'];
}
return 1;
}
bool get male {
if (params.containsKey('male') && params['male'] is bool) {
return params['male'];
}
return true;
}
}
上述拓展拓展了MyApp这个页面类。当MyApp文件内import该拓展时:
- 可直接在MyApp中直接使用变量name,age,male,且已为其赋值,不再需要考虑空指针问题。
- 减少了手动编写模版代码的时间。
- 规范了页面获参的方式。
至此,路由注解已经变成了这样:
// TZRouter注解
class TZRouter {
final String path; // 页面path
final String note; // 页面描述
final Map<String, dynamic>? params; // 页面参数
final String defaultParameterFieldName; // 默认参数名
final bool isMainPage; // 是否是业务首页
const TZRouter({
required this.path,
required this.note,
this.isMainPage = false,
this.params,
this.defaultParameterFieldName = 'params',
});
}
最后还是提一句吧。
如果觉得三方框架不能很好地满足自身需求,那么,动起手来,写一个吧。
如果觉得自己没有的功能,别人的框架里有,又比较眼红,那么,拿来吧你。