Flutter

Flutter中api实现自动生成(简单记录下)

2020-11-30  本文已影响0人  在他乡_28d2

Flutter中api实现自动生成

最近公司项目中电商模块从H5迁移为Flutter,在此过程中难免要对之前的接口再实现一遍。
最初设计是定义一单例的ApiUtil,再实现一基于ApiUtil的扩展,在扩展类中添加各个接口
的实现,部分代码如下:

class ApiUtil {
  ApiUtil._();

  static final ApiUtil _instance = ApiUtil._();

  static ApiUtil get inst => _instance;

  static Dio _dio = getDio();

  static Dio getDefaultDio() {
    Dio result = Dio(BaseOptions(
      connectTimeout: 10000,
      receiveTimeout: 10000,
    ));

    final adapter = result.httpClientAdapter as DefaultHttpClientAdapter;
      adapter.onHttpClientCreate = (client) {
        client.findProxy = (uri) {
          return "PROXY ";
        };
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) {
          return true;
        };
      };

    return result;
  }

  static Dio getDio() {
    _dio = getDefaultDio();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      ...
    }, onError: (DioError error) {
      return error;
    }));
    return _dio;
  }

  Future<Response> get(
    String path, {
    data,
    Map<String, dynamic> queryParameters,
    CancelToken cancelToken,
    ProgressCallback onReceiveProgress,
    String contentType = Headers.jsonContentType,
  }) async {
    return _dio.get(path,
        options: Options(
          headers: {
            HttpHeaders.acceptHeader: "application/json,text/plain,*/*",
          },
          contentType: contentType,
        ),
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress);
  }

  Future<Response> post(
    String path, {
    data,
    Map<String, dynamic> queryParameters,
    CancelToken cancelToken,
    ProgressCallback onSendProgress,
    ProgressCallback onReceiveProgress,
    String contentType = Headers.formUrlEncodedContentType,
  }) async {
    return _dio.post(path,
        data: data,
        options: Options(
          headers: {
            HttpHeaders.acceptHeader: "application/json,text/plain,*/*",
          },
          contentType: contentType,
        ),
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress);
  }
}

扩展类:

extension BaseApiUtil on ApiUtil {
  Future<GetAppPageRsp> getAppPage(int pageType, {String pageId}) {
    Map<String, dynamic> data = {
      "pageType": pageType,
      "modeType": 1,
      "companyId": AppConfig.COMPANY_ID,
      "lang": "zh_CN",
      "platformId": 4
    };
    if (!StringUtil.isEmpty(pageId)) {
      data.addAll({"pageId": pageId});
    }
    Future<GetAppPageRsp> result = new Future(() async {
      Response rsp = await post(
        "${ApiConfig.base}/cms/page/getAppPage",
        data: data,
      );
      return GetAppPageRsp.fromJson(rsp.data);
    });
    return result;
  }

  Future<RecommendMpListRsp> recommendMpListByMpIds(List mpIds) {
    Map<String, dynamic> data = {
      "sceneNo": 2,
      "pageNo": 1,
      "pageSize": 24,
      "platformId": AppConfig.PLATFORM_ID,
      "mpIds": mpIds.join(','),
      "sessionId": GlobalData.sessionId,
      "areaCode": MallData.getAreaCode(),
    };
    Future<RecommendMpListRsp> result = new Future(() async {
      Response rsp = await get(
        "${ApiConfig.base}/search/rest/recommendMpList",
        queryParameters: data,
      );
      return RecommendMpListRsp.fromJson(rsp.data);
    });

    return result;
  }
  ...
}

查看扩展类中的部分代码,我们可以看到,其定义了基本的get/post方法,供其他接口实现时调用。相关模块在调用时使用ApiUtil.inst获取ApiUtil的实例,然后调用扩展中实现的方法。大致接口实现结构如下:

Future<应答类> 方法名(参数...) {
    Map<String, dynamic> data = {
      ...
    };
    Future<应答类> result = new Future(() async {
      Response rsp = await 请求方式(get/post)(
        "接口地址",
        "请求数据(get->queryParameters, post->data)": data,
      );
      return 应答类.fromJson(rsp.data);
    });

    return result;
  }

如果有新增接口实现的话,基本可以拷贝其中的接口实现,修改方法返回类型、方法名、参数列表、data中的Map数据、请求方式、接口地址。尽着能少写代码就少写代码的原则, 考虑接口实现部分代码能否自动生成呢?答案是肯定的,通过注解、source_gen和build_runner方式可以实现类似json_serializable那样的代码自动生成。我们在build.yaml中定义的builder,编译时扫描builder下的相关文件,搜集其中的注解,通过generator生成具体代码。以下是相关步骤:

class ApiGen {
  static const String GET = 'GET';
  static const String POST = 'POST';
  static const String PUT = 'PUT';
  static const String PATCH = 'PATCH';
  static const String DELETE = 'DELETE';

  final String target;// 目标类名
  final String url;// 接口地址
  final String method;// 请求方式
  final dynamic data;// 请求数据
  final String contentType;// 请求数据contentType
  final Map<String, dynamic> header;// 请求header
  final String requestName;// 请求方式名

  const ApiGen(this.url, {
    this.method = POST,
    this.data,
    this.contentType,
    this.header,
    this.requestName,
    this.target
  });
}
class ApiGenerator extends GeneratorForAnnotation<ApiGen> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    // 解析注解,生成目标代码
  }
}
Builder apiBuilder(BuilderOptions options) => LibraryBuilder(
  ApiGenerator(),
  generatedExtension: '.api.util.dart'
);
targets:
  $default:
    builders:
      package_name|api_builder:
        enabled: true
        generate_for:
          include: ['**.api_gen.dart']
builders:
  api_builder:
    import: 'package:package_name/annotation/api_builder.dart'
    builder_factories: ['apiBuilder']
    build_extensions: {'.api_gen.dart': ['.api.util.dart']}
    auto_apply: root_package
    build_to: source

当我们执行flutter packages pub run build_runner build --delete-conflicting-outputs执行编译时,build会读取build.yaml中的配置信息,读取到apiBuilder后触发注解生成器ApiGenerator,在GeneratorForAnnotation中调用generate来处理生成代码,可以看到generate中会调用generateForAnnotatedElement,故我们在generateForAnnotatedElement中实现注解相关解析处理即可。接口实现部分代码使用mustache模块来实现,相关语法说明可参考mustache

class ApiUtilTpl {
  static const String tpl = """
import 'package:dio/dio.dart';
{{#imports}}
import '{{{path}}}';
{{/imports}}

extension {{className}} on {{targetClassName}} {
  {{#functions}}
  {{{functionDefine}}} {
    {{#hasData}}
    {{{dataType}}} data = {{{dataValue}}};
    {{/hasData}}
    
    {{^withBodyWrapper}}
    {{#params}}
    if (null != {{paramName}}) {
      data["{{{paramName}}}"] = {{paramName}};
    }
    {{/params}}
    {{/withBodyWrapper}}
    
    {{{returnType}}} result = new Future(() async {
      Response rsp = await {{requestName}}(
          "{{{url}}}",
          {{#hasData}}{{#httpSendData}}data{{/httpSendData}}{{^httpSendData}}queryParameters{{/httpSendData}}: data,{{/hasData}}
          {{#hasContentType}}contentType: "{{{contentType}}}",{{/hasContentType}});
      {{#withBodyWrapper}}
      return {{{rspType}}}.fromJson(json.decode(rsp.data));
      {{/withBodyWrapper}}
      
      {{^withBodyWrapper}}
      return {{{rspType}}}.fromJson(rsp.data);
      {{/withBodyWrapper}}
    });

    return result;
  }
  {{/functions}}
}

""";
}

generateForAnnotatedElement中解析注解内容,并用mustache模板渲染代码

List<Map<String, dynamic>> functions = [];
List<Map<String, dynamic>> imports = [];
Map<String, bool> importMap = {};

class ApiGenerator extends GeneratorForAnnotation<ApiGen> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    String baseUrl = '';
    /// 注解修饰的是类,注解中可添加baseUrl及生成目标类名
    if (element is ClassElement) {
      baseUrl = annotation.peek('url')?.stringValue ?? '';
      print('ClassElement baseUrl : ' + baseUrl);
      if (baseUrl.isEmpty) {
        print('please check annotation url of class : ' + element.name);
        return;
      }
    }

    addDocumentImport(element, buildStep);

    /// 遍历含有注解的类的成员,本文只处理接口方法注解
    element.visitChildren(SimpleVisitor(buildStep, baseUrl));

    Template tpl = Template(ApiUtilTpl.tpl);
    String content = tpl.renderString({
      'imports': imports,
      'className': annotation.peek('target')?.stringValue,
      'targetClassName': 'ApiBase',
      'functions': functions,
    });
    imports.clear();
    functions.clear();
    importMap.clear();
    return content;
  }

  /// 添加import包信息
  static void addDocumentImport(Element element, BuildStep buildStep) {
    if (element.documentationComment != null) {
      List<String> comments = element.documentationComment.split('\n');
      for (String elem in comments) {
        if (elem?.isNotEmpty ?? false) {
          if (elem.contains('package:')) {
            ApiGenerator.addImport(
                buildStep, elem.substring(elem.indexOf('package')));
          } else if (elem.contains('dart:')) {
            ApiGenerator.addImport(
                buildStep, elem.substring(elem.indexOf('dart')));
          }
        }
      }
    }
  }

  static void addImport(BuildStep buildStep, String path) {
    String result = path;
    if (path.startsWith('/${buildStep.inputId.package}/lib/')) {
      result =
      "package:${buildStep.inputId.package}/${path.replaceFirst('/${buildStep.inputId.package}/lib/', '')}";
    }
    if (!importMap.containsKey(result)) {
      importMap[result] = true;
      print("addImport path[$path]");
      imports.add({"path": result});
    }
  }
}

class SimpleVisitor extends SimpleElementVisitor {
  String _baseUrl;
  BuildStep _buildStep;
  SimpleVisitor(this._buildStep, this._baseUrl);

  @override
  visitMethodElement(MethodElement element) {
    ConstantReader reader = ConstantReader(TypeChecker.fromRuntime(ApiGen).firstAnnotationOf(element));
    if (reader == null) {
      print('firstAnnotationOf ' + element.name + ' is null');
      return;
    }

    Map<String, dynamic> funcInfo = {};
    Map<String, dynamic> defaultParams = {};

    funcInfo['functionDefine'] = element.toString();

    if (element.returnType.isVoid) {
      print('please check return type of method : ' + element.name);
      return;
    }

    var url = reader.peek('url')?.stringValue ?? '';
    if (url.isEmpty) {
      print('please check annotation url of method : ' + element.name);
      return;
    }
    funcInfo['url'] = _baseUrl + url;

    funcInfo["withBodyWrapper"] = false;
    ApiGenerator.addDocumentImport(element, _buildStep);

    var requestName = reader.peek('requestName')?.stringValue ?? '';
    var method = reader.peek('method')?.stringValue ?? '';
    switch (method) {
      case ApiGen.POST:
        requestName = 'post';
        funcInfo['httpSendData'] = true;
        break;

      case ApiGen.GET:
        requestName = 'get';
        funcInfo['httpSendData'] = false;
        break;

      default:
        print('unsupportable method : ' + method);
        return;
    }
    funcInfo['requestName'] = requestName;

    var data = reader.peek('data');
    funcInfo["hasData"] = data != null && data.objectValue != null;
    if (funcInfo["hasData"]) {
      funcInfo["dataType"] = AnnotationUtil.getDataType(data.objectValue);
      funcInfo["dataValue"] = AnnotationUtil.getDataValue(data.objectValue);
    } else {
      if ((element.parameters?.length ?? 0) > 0) {
        funcInfo["hasData"] = true;
        funcInfo["dataValue"] = "{}";
        funcInfo["dataType"] = "Map<String, dynamic>";
      }
    }

    /// 函数参数,收集有默认值的参数
    List<Map<String, String>> params = [];
    element.parameters?.forEach((parameterElement) {
      params.add({"paramName": parameterElement.displayName});
      if (parameterElement.defaultValueCode != null) {
        defaultParams[parameterElement.displayName] = parameterElement.defaultValueCode;
      }
    });
    funcInfo["params"] = params;

    /// 函数参数有默认值的情况,更新函数定义
    if (defaultParams.isNotEmpty) {
      Iterator<String> iterator = defaultParams.keys.iterator;
      String funcDef = element.toString();
      while (iterator.moveNext()) {
        String key = iterator.current;
        funcDef = funcDef.replaceFirst(key, key + ' = ' + defaultParams[key]);
      }
      funcInfo["functionDefine"] = funcDef;
    }

    /// 函数返回值
    DartType returnType = element.returnType;
    funcInfo["returnType"] = returnType.toString();

    /// 返回值为泛型
    if (AnnotationUtil.canHaveGenerics(returnType)) {
      List<DartType> types = AnnotationUtil.getGenericTypes(returnType);
      if (types.length > 1) {
        throw Exception("multiple generics not support!!!");
      }
      funcInfo["rspType"] = types.first.toString();
    }

    /// http contentType
    funcInfo['hasContentType'] = reader.peek('contentType')?.stringValue != null;
    if (funcInfo['hasContentType']) {
      funcInfo['contentType'] = reader.peek('contentType')?.stringValue;
    }

    /// 获取此函数需要的引入的包
    /// 返回值的包
    ApiGenerator.addImport(_buildStep, returnType.element.librarySource.fullName);

    /// 返回值为泛型
    if (AnnotationUtil.canHaveGenerics(returnType)) {
      List<DartType> types = AnnotationUtil.getGenericTypes(returnType);
      for (DartType type in types) {
        ApiGenerator.addImport(_buildStep, type.element.librarySource.fullName);
      }
    }
    functions.add(funcInfo);
  }
}

其中类AnnotationUtil代码如下:

class AnnotationUtil {
  static const String KEEP_NAME_PREFIX = "@C_";

  /// 获取 DartObject 数据值字符串。代码格式
  static String getDataValue(DartObject dartObject) {
    String result = "";
    if (dartObject.type.isDartCoreMap) {
      Map<DartObject, DartObject> map = dartObject.toMapValue();
      result = "{";
      map.forEach((key, value) {
        result += "\n${getDataValue(key)} : ${getDataValue(value)},";
      });
      result += "\n}";
    } else if (dartObject.type.isDartCoreString) {
      if (dartObject.toStringValue().startsWith(KEEP_NAME_PREFIX)) {
        return dartObject.toStringValue().substring(KEEP_NAME_PREFIX.length, dartObject.toStringValue().length);
      }
      return "\"${dartObject.toStringValue()}\"";
    } else if (dartObject.type.isDartCoreList) {
      List<DartObject> list = dartObject.toListValue();
      result = "[";
      list.forEach((element) {
        result += "\n${getDataValue(element)},";
      });
      result += "\n]";
    } else if (dartObject.type.isDartCoreInt) {
      result = "${dartObject.toIntValue()}";
    } else if (dartObject.type.isDartCoreDouble) {
      result = "${dartObject.toDoubleValue()}";
    } else if (dartObject.type.isDartCoreBool) {
      result = "${dartObject.toBoolValue()}";
    } else if (dartObject.type.isDynamic) {
      result = "${dartObject.toString()}";
    } else {
      throw Exception("data value [${dartObject.type}] not support!!!");
    }
    return result;
  }

  /// 获取 DartObject 数据类型。代码格式
  static String getDataType(DartObject value) {
    if (value.type.isDartCoreMap) {
      return "Map<String, dynamic>";
    } else if (value.type.isDartCoreString) {
      return "String";
    } else if (value.type.isDartCoreList) {
      return "List";
    } else if (value.type.isDartCoreInt) {
      return "int";
    } else if (value.type.isDartCoreDouble) {
      return "double";
    } else if (value.type.isDartCoreBool) {
      return "bool";
    } else if (value.type.isDynamic) {
      return "dynamic";
    } else {
      throw Exception("data type not support!!!");
    }
  }

  static List<DartType> getGenericTypes(DartType type) {
    return type is ParameterizedType ? type.typeArguments : const [];
  }

  static bool canHaveGenerics(DartType type) {
    final element = type.element;
    if (element is ClassElement) {
      return element.typeParameters.isNotEmpty;
    }
    return false;
  }
}

因为注解中只能使用常量字符串,像AppConfig.COMPANY_ID(1)这种常量,MallData.getAreaCode()这种方法调用,为了能在生成的代码中保留原始展示,在注解处理时需要做下特殊处理,避免转换成为常量数值或者普通字符串。上面代码中的KEEP_NAME_PREFIX部分处理即是为了保留原始注解值做的处理。

/// package:package_name/api/api_base.dart
/// package:package_name/api/api_config.dart
@ApiGen('\${ApiConfig.base}', target: 'TestApi')
abstract class ApiInterface {
  /// package:package_name/api/base/constants.dart
  @ApiGen('/cms/page/getAppPage', data: {
    'platformId' : '@C_Constants.PLATFORM_ID'
  })
  Future<GetPageRsp> getAppPage(int pageType, {String pageId});
}

执行flutter packages pub run build_runner build --delete-conflicting-outputs后,会自动生成一文件test.api_gen.api.util.dart,其中内容如下:

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// ApiGenerator
// **************************************************************************

import 'package:dio/dio.dart';
import 'package:package_name/api/api_base.dart';
import 'package:package_name/api/api_config.dart';
import 'package:package_name/api/base/constants.dart';
import 'dart:async';
import 'package:package_name/api/get_app_page.dart';

extension TestApi on ApiBase {
  Future<GetPageRsp> getAppPage(int pageType, {String pageId}) {
    Map<String, dynamic> data = {
      "platformId": Constants.PLATFORM_ID,
    };

    if (null != pageType) {
      data["pageType"] = pageType;
    }
    if (null != pageId) {
      data["pageId"] = pageId;
    }

    Future<GetPageRsp> result = new Future(() async {
      Response rsp = await post(
        "${ApiConfig.base}/cms/page/getAppPage",
        data: data,
      );

      return GetPageRsp.fromJson(rsp.data);
    });

    return result;
  }
}

这种生成代码的方式有一个缺点是,因为代码需要自动生成,所以编译时间会稍微变长。示例完整代码详见flutter_api_gen

上一篇 下一篇

猜你喜欢

热点阅读