鸿蒙 / Flutter 如何生成代码?

158 阅读8分钟

大家好。我用 typescript 语言编写了一个命令行工具,有如下功能

  1. 可以通过 json 生成 class 代码。
  2. 生成的 class 具备序列化与序列化能力。
  3. 可以适用于所有有效 json 数据格式。
  4. 支持 json,也可以使用 json5
  5. 可以通过 ref 语法,引用已经定义的结构,生成递归类型
  6. 目前已经支持 dartarkTs 语言,后续支持其他语言也不是问题

工具地址

为什么要开发这工具

  1. 当初开发 Flutter 的时候,使用官方推荐的 json_serializable 实在觉得有点麻烦,用起来需要定义一堆东西,还需要按约定的规则对应好属性。如果跟 http 请求结合起来使用,需要手写的模版代码就更多了。
  2. 类型的命名我个人觉得是个很痛苦的事情,就算是一个人开发一个项目,也很难从头到尾保持一致的命名规范,如果多人协同开发,可能连基本的驼峰命名都有可能难以保证。但本质上,class 的名称其实是无关紧要的,只需要保证命名规范的完全一致性和可识别性即可。
  3. 只要是强类型语言,在与动态数据交互的时候,为了保证类型安全,都需要提前定义 class 类型并实现序列化与反序列化。但不同的语言处理方案却完全不同。但本质上,不同语言的 class 结构,与序列化与反序列化逻辑基本类似。

例子

下面我介绍下这个工具如何使用。这是我第一次写开源工具,难免有所考虑不到的地方,欢迎大家给我提出意见或建议。

如何开始

因为这是个命令行工具,所以需要从命令行开始。我以 npx 这个工具开始。其他执行方式基本类似,可以在上面的 工具地址 的文档中找到使用方法。

npx json2class build -l dart@3

-l--language 的缩写,用于指定需要构建的语言。目前已经支持 dart@3arkTs@12。很显然,@后面的数字代表版本号

执行该条命令后,默认会在执行命令所在的目录去查找所有的 .json(5) 文件,也支持文件夹查找,但最多支持三层。

如果一切没有问题,默认就会在执行命令所在的文件夹生成一个构建好的代码文件。如果是 dart 生成的就是 json2class.dart。如果是 arkTs,生成的就是 json2class.ets

可以通过 -s--search 指定一个查找 .json(5) 的目录,支持绝对路径和相对路径

可以通过 -o--output 指定一个输出目录。

推荐将生成的文件 json2class.* 加入 git 忽略。

JSON 如何配置

只要你的 json 是合法的,就是一个有效的配置,就可以完成代码的构建

1. 一个最简单的例子

// 文件名: test.json5
{
  a: 1,
  b: "bbb",
  c: true,
  o: {
    o1: 100,
    o2: 'ooo',
    o3: false,
  },
}

生成的代码 dart 代码大致如下

class test {
  num a = 0;
  String b = '';
  bool c = false;
  testo o = testo();
}

class testo {
  num o1 = 0;
  String o2 = '';
  bool o3 = false;
}

没错,我使用了文件名 + 属性名直接拼接的方式当做 class 类名。这在使用上完全没有问题,既保证了命名规范从一而终的一致性、也保留了充分的可识别性。

从生成的代码中,你可以看到属性类型属性默认值与JSON的大致关系。亦或是如果不想设置属性默认值,这些在上面的 工具地址 中都有详细描述,这里不再赘述。

2. 递归类型如何创建?

之前有提到,只要你的 json 是合法的,构建就不会有问题。但合法的 json 并不能一定保证能生成我需要的类型。

class test {
  num a = 0;
  String b = '';
  bool c = false;
  testo o = testo();
}
class testo {
  num o1 = 0;
  String o2 = '';
  bool o3 = false;
  testo? child; // 类型递归
}

像这样的一个递归类型,只使用 json 就无法描述,我们需要约定特殊语法。

// test.json5
{
  a: 123,
  b: 'abc',
  c: true,
  o: {
    o1: 100,
    o2: 'ooo',
    o3: false,
    child: { $meta: { ref: '/test#/o' } }
  },
}

ref 引用# 分为两个部分:

/test 是文件名,这里当然也可以是其他文件名,只要存在于你的 json 查找目录中的文件。如果是多层目录,需要带上文件夹层级。

/o 就是你需要引用的字段对应的类型,在这个例子中就是 testo。如果你需要引用的类型就是当前文件的,可以省略前半部分,{ ref: '#/o' },此写法与当前例子等效,但需要注意的是 # 一定是必须的。

目前 $metaref 是唯一支持的配置。如果大家在使用过程中,有更好的想法可以告诉我,可以通过 $meta 加入更多特性的支持。

3. 冲突、特殊字符怎么办?

类名通过 文件名 + 字段名 拼接的方式,在一些极端情况下,会产生类型冲突的情况,此时命令行工具会给出提示,你只需要修改文件名即可消除冲突。

修改文件名会产生新的类名,这里再次强调,类名具体是什么其实真的不太重要,重要的是

  • 类名生成规则的从一而终的一致性
  • 类名的可识别性

如果字段中,有特殊的字符,这些字符是属于该语言的关键字,命令行工具在识别到以后,会通过特定算法做转换,不影响属性的使用,也不会影响序列化后的字段名。

生成后代码如何使用

1. 序列化与反序列化

import 'json2class.dart';

main() {
  final t = testo();
  t.fromJson({
    "test": {
      "o1": 100,
      "o2": 'ooo',
      "o3": true,
     }
  });
  print(t.toJson());
}

生成的代码最核心功能就是序列化反序列化

  • fromJson:反序列化
  • toJson:序列化

为了方便字符串格式的JSON数据转换,还提供了一个 fromAny,他可以接收任意参数,尝试转换成 Map 后在调用 fromJson 实现反序列化。

完整的生成代码的使用文档可以参见上面的 工具地址,这里不再赘述。

2. 构造 mock 数据

可以预见,该工具生成的代码,最佳的使用场景是在获取网络数据。在后端没有准备好联调时,我们就需要构造假数据进行开发了。我们在拿到后端接口返回定义时就可以同时配置好 mock 数据

// test.json
{
  "result": {
     "statusCode": "",
     "statusMessage": "",
     "data": {
        "userName": "json2class",
        "userPhone": "13888888888",
        "userAvator": "http://xxx.yyy.com/avator.png"
     }
  }
}

main() {
  final result = testresult();
  result.fromPreset();
  setState(() {});
}

通过 fromPreset 方法,就可以将 json 中配置的数据填充到对象中,去完成业务逻辑的编码。当后面后端具备联调条件时,只需要将 fromPreset,替换成真实的接口调用即可。当然你也可以把这里封装一下,通过泛型,实现所有接口统一入口,通过一个参数实现 mock 数据 的一键切换。

3. 填充规则

为什么会设计填充规则这个玩意?因为前端在与后端交互时,为了保证前端代码的健壮性,一定不能信任任何后端数据,这也是为什么我们拿到后端的 json 数据后,要做反序列化、类型化的原因。

那么在真实的业务场景下,如果接口返回数据的结构、类型与我们定义的不一致,怎么办?直接报错?这肯定不行。当发生结构或类型与定义不一致时,填充 null 应该可行,填充该类型的一个 默认值 似乎也不错。为了满足不同的需求,所以设计了 填充规则

  • 针对对象 Key 的填充

很好理解。无非就三种情况。

  1. 输入与定义的字段都存在,且类型相同。这是最完美的情况,直接填充赋值。
  2. 输入与定义的字段都存在,但类型不同。那么通过 DiffType 枚举值决定如何填充。
  3. 定义的字段不存在。那么通过 MissKey 枚举值决定如何填充。
  • 针对数组长度一样的数组

这个应该也容易理解,输入的数组长度与带填充的数组长度一样,那么按元素位置逐个填充,如果发生对应位置类型不匹配的情况,按 DiffType 处理。

  • 针对数组长度不一样的数组

针对长度超出的那部分元素,按如下规则处理。

  1. 枚举值 MoreIndex:输入数组长度 > 原数组
枚举值效果
Fill按输入值插入,类型不一致时,根据字段是否可选,设置默认值 / null
Drop丢弃多余的输入数据,数组长度与原数组长度一致
Null多出的数据填充为 null 值(非可选字段强制 Null 等同 Fill
  1. 枚举值 MissIndex:输入数组长度 < 原数组
枚举值效果
Fill填充默认值,多维数组会递归填充
Drop丢弃多余的原始数据,数组长度与输入数组长度一致
Null多出的数据填充为 null 值(非可选字段强制 Null 等同 Fill
Skip原数组中多余的数据不做处理,保留原值

一般情况下,我们在接收后端数组数据的时候,一般会 new 一个新对象,那么接收数组长度为 0,即 输入数组长度 > 原数组,受枚举值 MoreIndex 影响,而默认配置的 MoreIndex MoreIndex.Fill,所以最后表现的行为是将数据逐个填充到原数组中,符合预期。

最后

这就是全部内容,感谢您看到这里。该项目也是在多年的实际项目中逐渐迭代、优化、提炼而形成的。非常希望大家能在各自的项目中用起来,如果在使用过程中遇到问题,或者有任何改进的建议,欢迎通过如下方式反馈: