阅读 336

Flutter自动生成代码之:build/source_gen

一、build

我们可以基于 build 库自动生成代码。

步骤很简单,就是编写

  • Builder
  • 编写工厂函数
  • 编写build.yaml文件。

1.实现Builder类

Builder源码如下:

abstract class Builder {
  FutureOr<void> build(BuildStep buildStep);
  Map<String, List<String>> get buildExtensions;
}
复制代码

我们需要实现buildbuildExtensions。看一个例子my_builders.dart(文末有来源)。

import 'package:build/build.dart';
import 'package:analyzer/dart/element/element.dart';

class CopyBuilder implements Builder {

  // 文件映射:.txt得到.txt.copy
  @override
  final buildExtensions = const {
    '.txt': ['.txt.copy']
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    // Each `buildStep` has a single input.
    // inputId是AssetId,相当于输入的文件(不一定是.dart文件)
    var inputId = buildStep.inputId;

    // Create a new target `AssetId` based on the old one.
    // 为文件添加扩展名.copy,得到新的文件名
    var copy = inputId.addExtension('.copy');
    // 读取输入文件的内容
    var contents = await buildStep.readAsString(inputId);

    // Write out the new asset.
    // 输入源文件内容到新文件中
    // await buildStep.writeAsString(copy, contents);
    await buildStep.writeAsString(copy, '// Copied from $inputId\n$contents');
  }
}
复制代码

这个例子就是复制所有的.txt文件,得到新的.txt.copy。

2、编写工厂函数

在example/builder.dart

import 'my_builders.dart';// 导入
Builder copyBuilder(BuilderOptions options) => CopyBuilder();
复制代码

3.配置build.yaml文件:

builders:
  copyBuilder:
    import: "package:example/builder.dart" # 导入工厂函数所在的文件
    builder_factories: ["copyBuilder"]     # 定义builder工厂函数
    build_extensions: {".txt": [".txt.copy"]}
    build_to: source                        
    auto_apply: root_package 
复制代码

最后看下文件结构:

example
   |- web
   |    |-a.txt
   |- build.yaml
   |- pubspec.yaml
   |- lib
        |-builder.dart
        |-my_builders.dart
复制代码

在example目下运行

pub run build_runner build
复制代码

web目录下的a.txt文件就会复制一份。

二、source_gen

source_gen 基于 analyzerbuild 库。 build库主要是资源文件的处理,analyser库是对dart文件生成语法结构,source_gen主要提处理dart源码,可以通过注解生成代码。

我们的起点是build的Builder和source_gen的_Builder。_Builder做了处理源码的基本流程,_Builder有各种Builder子类(SharedPartBuilder、PartBuilder、LibraryBuilder),分别处理生成不同的文件。在Builder需要生成器Generator,通过生成器生成代码。_Builder的流程已经完备,我们只需要实现生成器即可。

Builder的继承树:

Builder (builder.dart)
    |_Builder (builder.dart)
        |-LibraryBuilder (builder.dart)
        |-SharedPartBuilder (builder.dart)
        |-PartBuilder (builder.dart)
复制代码
  • 1.SharedPartBuilder If you want to write to .g.dart files which are referenced as a part in the original source file, use SharedPartBuilder. This is the convention for generated code in part files, and this file may also contain code from Generators provided by other packages.

    生成.g.dart文件,使用SharedPartBuilder

  • 2.PartBuilder If you want to write to .some_name.dart files which are referenced as a part in the original source file, use PartBuilder. You should choose an extension unique to your package. Multiple Generators may output to this file, but they will all come from your package and you will set up the entire list when constructing the builder.

    自定义part文件,使用PartBuilder

  • 3.LibraryBuilder If you want to write standalone Dart library which can be imported use LibraryBuilder. Only a single Generator may be used as a LibraryBuilder.

    生成单独的文件,使用LibraryBuilder

_Builder的构造方法需要Generator:

_Builder(this._generators,
      {String Function(String code) formatOutput,
      String generatedExtension = '.g.dart',
      List<String> additionalOutputExtensions = const [],
      String header})
复制代码

使用source_gen生成代码,步骤也类似的,而且我们有了多个Builder以及GeneratorForAnnotation/Generator可以使用,方便很多了。

我们需要实现的就是继承Generator

abstract class Generator {
  const Generator();
  // 根据输入文件(dart文件)library返回输出的源码内容
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) => null;

  @override
  String toString() => runtimeType.toString();
} 


复制代码

实现generate方法。或者继承GeneratorForAnnotation,实现generateForAnnotatedElement方法。

abstract class GeneratorForAnnotation<T> extends Generator {
  const GeneratorForAnnotation();

  TypeChecker get typeChecker => TypeChecker.fromRuntime(T);

  @override
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
    final values = Set<String>();

    for (var annotatedElement in library.annotatedWith(typeChecker)) {
      final generatedValue = generateForAnnotatedElement(
          annotatedElement.element, annotatedElement.annotation, buildStep);
      await for (var value in normalizeGeneratorOutput(generatedValue)) {
        assert(value == null || (value.length == value.trim().length));
        values.add(value);
      }
    }

    return values.join('\n\n');
  }

  // 根据被注解的元素(element)和注解实例(annotation)生成代码片段(一个输入文件可以包括多个注解)
  // 可以方法String,Stream<Iterable>,Iterable<String>,Future<String>,Future<Iterable<String>>,Future<Stream<String>>
  dynamic generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep);
}

复制代码

然后通过Generator定义builder工厂函数,其余的步骤和build是一样的。

三、源码分析

build 库的Builder开始

    // .pub-cache/hosted/pub.flutter-io.cn/build-1.2.1/lib/src/builder/builder.dart
    abstract class Builder {
        // 我们需要实现的
      FutureOr<void> build(BuildStep buildStep);
      
      // 文件映射
      Map<String, List<String>> get buildExtensions;
    }
复制代码

我们需要实现的是buildget buildExtensions;。source_gen的实现基类是_Builder:

    class _Builder extends Builder {
      /// Function that determines how the generated code is formatted.
      // 用于格式化输出生成的源码,默认为 _formatter.format,我们自己可以实现
      final String Function(String) formatOutput;
    
      /// The generators run for each targeted library.
      // 生成器,处理输入的源码文件
      final List<Generator> _generators;
    
      /// The [buildExtensions] configuration for `.dart`
      final String _generatedExtension;
    
      /// Whether to emit a standalone (non-`part`) file in this builder.
      bool get _isLibraryBuilder => this is LibraryBuilder;
    
      final String _header;
    
      @override
      final Map<String, List<String>> buildExtensions;
  
      // .pub-cache/hosted/pub.flutter-io.cn/source_gen-0.9.4+4/lib/src/builder.dart
      @override
      Future build(BuildStep buildStep) async {
        final resolver = buildStep.resolver;
        // 如果输入文件(buildStep.inputId)不是独立的dart源码,那么返回
        // 也就是只处理独立dart源码(a standalone Dart library)
        if (!await resolver.isLibrary(buildStep.inputId)) return;
        // 读取输入文件,得到LibraryElement
        // LibraryElement表示输入文件
        final lib = await buildStep.inputLibrary;
        await _generateForLibrary(lib, buildStep);
      } 
      
    ...
   }
复制代码

_Builder接收一个Generator列表,用于处理输入文件。

_generateForLibrary处理输入文件。

  Future _generateForLibrary(
      LibraryElement library, BuildStep buildStep) async {
      // 让_generators处理输入文件,得到多个GeneratedOutput
      // _generate在下面分析
    final generatedOutputs = await _generate(library, _generators, buildStep).toList();

    // Don't output useless files.
    //
    // NOTE: It is important to do this check _before_ checking for valid
    // library/part definitions because users expect some files to be skipped
    // therefore they do not have "library".
    if (generatedOutputs.isEmpty) return;
    // 根据_generatedExtension,生成新的输出文件(outputId),其实是修改扩展名
    final outputId = buildStep.inputId.changeExtension(_generatedExtension);

    // contentBuffer作为输出结果的 
    final contentBuffer = StringBuffer();

    // 写入头
    if (_header.isNotEmpty) {
      contentBuffer.writeln(_header);
    }

    if (!_isLibraryBuilder) {
        // 如果不是LibraryBuilder(那么是SharedPartBuilder或者是PartBuilder)
      final asset = buildStep.inputId;
      // 获取输入文件名字(这个貌似还加了引号 TODO)
      final name = nameOfPartial(library, asset);
      if (name == null) {
        // 这个不出出现?
        final suggest = suggestLibraryName(asset);
        throw InvalidGenerationSourceError(
            'Could not find library identifier so a "part of" cannot be built.',
            todo: ''
                'Consider adding the following to your source file:\n\n'
                'library $suggest;');
      }
      contentBuffer.writeln();

      String part;
      // 下面是获取生成的part文件的名字
      if (this is PartBuilder) {
        // 对于PartBuilder,输入part of部分 
        contentBuffer.writeln('part of $name;');
        part = computePartUrl(buildStep.inputId, outputId);
      } else {
        // 注意,对于SharedPartBuilder,没有输入part of,后面有分析
        assert(this is SharedPartBuilder);
        final finalPartId = buildStep.inputId.changeExtension('.g.dart');
        part = computePartUrl(buildStep.inputId, finalPartId);
      }
      
      // 检查源文件的part声明部分是否包含实际需要生成的代码文件(也就是源代码是否有part of部分),如果没有声明报错退出
      if (!library.parts.map((c) => c.uri).contains(part)) {
        // TODO: Upgrade to error in a future breaking change?
        log.warning('Missing "part \'$part\';".');
        return;
      }
    }

    for (var item in generatedOutputs) {
      contentBuffer
        ..writeln()
        ..writeln(_headerLine)
        ..writeAll(
            LineSplitter.split(item.toString()).map((line) => '// $line\n'))// 输出这个干吗?
        ..writeln(_headerLine)
        ..writeln()
        ..writeln(item.output);
    }

    var genPartContent = contentBuffer.toString();

    try {
      genPartContent = formatOutput(genPartContent);// 格式化输出
    } catch (e, stack) {
      log.severe(
          'Error formatting generated source code for ${library.identifier}'
          'which was output to ${outputId.path}.\n'
          'This may indicate an issue in the generated code or in the '
          'formatter.\n'
          'Please check the generated code and file an issue on source_gen if '
          'appropriate.',
          e,
          stack);
    }

    // 输出内容到输出文件
    unawaited(buildStep.writeAsString(outputId, genPartContent));
  } 
复制代码

其中_generate的源码如下:

    Stream<GeneratedOutput> _generate(LibraryElement library,
        List<Generator> generators, BuildStep buildStep) async* {
        // 每个Generator依次进行处理
        // LibraryReader包装LibraryElement
      final libraryReader = LibraryReader(library);
      for (var i = 0; i < generators.length; i++) {
        final gen = generators[i];
        try {
          var msg = 'Running $gen';
          if (generators.length > 1) {
            msg = '$msg - ${i + 1} of ${generators.length}';
          }
          log.fine(msg);
          // 调用Generator处理libraryReader,得到FutureOr<String>
          var createdUnit = await gen.generate(libraryReader, buildStep);
    
           // 如果生成的String为null或者为空,跳过
          if (createdUnit == null) {
            continue;
          }
    
          createdUnit = createdUnit.trim();
          if (createdUnit.isEmpty) {
            continue;
          }
    
          // 封装用GeneratedOutput生成的String
          yield GeneratedOutput(gen, createdUnit);
        } catch (e, stack) {
          log.severe('Error running $gen', e, stack);
          yield GeneratedOutput.fromError(gen, e, stack);
        }
      }
    } 
复制代码

先前我们看到SharedPartBuilder没有输入part of,为什么? 这是因为SharedPartBuilder只是生成中间文件,最终生成的.g.dart文件不是通过他生成的。我们下面分析。

我们先看下build.yaml中对于SharedPartBuilder一般配置:

builders:
      property_sum:                                         #// 我们定义的bulder名字
        import: "package:source_gen_example/builder.dart"   #// 导入builder函数所在的文件
        builder_factories: ["sumBuilder"]                   #// builder函数
        build_extensions: {".dart": [".sum.g.part"]}        #// 必须配置, .dart==>中间.sum.g.part文件,最后通过combining_builder转为.g.dart
        auto_apply: dependents
        build_to: cache                                     #// 必须配置为cache,生成的代码先缓存,后面会删除(combining_builder里面配置的)
        applies_builders: ["source_gen|combining_builder"]  #// 必须添加source_gen的combining_builder
    

复制代码

其中sumBuilder为:

//     
Builder sumBuilder(BuilderOptions options) {
    return ...;
}
 
复制代码

而且SharedPartBuilder的构造方法如下,我们看到,对于输入的.dart文件,生成的是.$partId.g.part文件:

      SharedPartBuilder(List<Generator> generators, String partId,
          {String Function(String code) formatOutput,
          List<String> additionalOutputExtensions = const []})
          : super(generators,
                formatOutput: formatOutput,
                generatedExtension: '.$partId.g.part',
                additionalOutputExtensions: additionalOutputExtensions,
                header: '') 
复制代码

那么这个combining_builder是什么呢? combining_builder是source_gen的build.yaml中定义的builder的名字。

我们看下source_gen的build.yaml配置:

    # Read about `build.yaml` at https://pub.dev/packages/build_config
    builders:
      combining_builder                             # 声明combining_builder
        import: "package:source_gen/builder.dart"
        builder_factories: ["combiningBuilder"]     # combiningBuilder函数得到CombiningBuilder
        build_extensions: {".dart": [".g.dart"]}    # 这个为什么是这样?不是.g.part文件吗?因为还是读取.dart得到.g.dart,同时也会使用到.$partId.g.part文件
        auto_apply: none
        build_to: source
        required_inputs: [".g.part"]                # 依赖.g.part
        applies_builders: ["source_gen|part_cleanup"]   # build后清除.g.part
    post_process_builders:      
      part_cleanup:
        import: "package:source_gen/builder.dart"
        builder_factory: "partCleanup"
 
复制代码

通过combining_builder合并.$partId.g.part得到.g.dart文件的,那么这个过程是什么样的呢?

CombiningBuilder的源码:

    class CombiningBuilder implements Builder {
      final bool _includePartName;
    
      // _outputExtensions是.g.dart
      // buildExtensions得到的是{'.dart': '.g.dart'}
      @override
      Map<String, List<String>> get buildExtensions => const {
            '.dart': [_outputExtensions]
          };
    
      /// Returns a new [CombiningBuilder].
      ///
      /// If [includePartName] is `true`, the name of each source part file
      /// is output as a comment before its content. This can be useful when
      /// debugging build issues.
      const CombiningBuilder({bool includePartName = false})
          : _includePartName = includePartName ?? false;
    
      @override
      Future build(BuildStep buildStep) async {
        // 这里的输入就是.dart文件
        // _partFiles是'.g.part'
        // 所以得到是源码对应的.g.part文件的路径(a.dart=>a.*.g.part)
        // Pattern used for `findAssets`, which must be glob-compatible
        final pattern = buildStep.inputId.changeExtension('.*$_partFiles').path;
    
        final inputBaseName =
            p.basenameWithoutExtension(buildStep.inputId.pathSegments.last);
    
        // Pattern used to ensure items are only considered if they match
        // [file name without extension].[valid part id].[part file extension]
        // 注释已经很明白了,restrictedPattern就是表示类似a.$partId.g.part的正则表达式
        final restrictedPattern = RegExp([
          '^', // start of string
          RegExp.escape(inputBaseName), // file name, without extension
          '\.', // `.` character
          partIdRegExpLiteral, // A valid part ID
          RegExp.escape(_partFiles), // the ending part extension
          '\$', // end of string
        ].join(''));
    
        final assetIds = await buildStep
            .findAssets(Glob(pattern))          // 查找所有的a.*.g.part文件
            .where((id) => restrictedPattern.hasMatch(id.pathSegments.last))// 过滤掉不匹配正则的
            .toList()
          ..sort();
    
        final assets = await Stream.fromIterable(assetIds)
            .asyncMap((id) async {
              var content = (await buildStep.readAsString(id)).trim();
              // 如果build.yaml文件设置了include_part_name为true,输出part来源
              if (_includePartName) {
                content = '// Part: ${id.pathSegments.last}\n$content';
              }
              return content;
            })
            .where((s) => s.isNotEmpty)
            .join('\n\n');// 合并part文件
        if (assets.isEmpty) return;
        // 获取输入的.dart文件名作为partOf,输出part of
        final partOf =
            nameOfPartial(await buildStep.inputLibrary, buildStep.inputId);
        final output = '''
    $defaultFileHeader
    
    part of $partOf;
    
    $assets
    ''';
        // 合并的结果输出到.g.dart文件
        await buildStep.writeAsString(
            buildStep.inputId.changeExtension(_outputExtensions), output);
      }
    }
 
复制代码

至此,我们知道了combining_builder合并.$partId.g.part得到.g.dart文件的过程。

下面我们看下Generator,这个是具体如何生成代码的。

 Generator (generator.dart)
    |-GeneratorForAnnotation (generator_for_annotation.dart)

复制代码

Generator只有一个直接子类GeneratorForAnnotation。

    abstract class GeneratorForAnnotation<T> extends Generator {
      const GeneratorForAnnotation();
    
        // T表示注解的类型,TypeChecker.fromRuntime(T)得到一个类型T的检测器,可以Element是否被T注解
      TypeChecker get typeChecker => TypeChecker.fromRuntime(T);
    
      @override
      FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
        final values = Set<String>();
    
        for (var annotatedElement in library.annotatedWith(typeChecker)) {
            // 获取被T注解的Element,然后调用generateForAnnotatedElement得到处理结果
            // generateForAnnotatedElement是我们需要实现的
          final generatedValue = generateForAnnotatedElement(
              annotatedElement.element, annotatedElement.annotation, buildStep);
              // generatedValue支持是Stream和Future,所以用await for,得到输出的String
          await for (var value in normalizeGeneratorOutput(generatedValue)) {
            assert(value == null || (value.length == value.trim().length));
            values.add(value);
          }
        }
    
        return values.join('\n\n');
      }
    
      /// Implement to return source code to generate for [element].
      ///
      /// This method is invoked based on finding elements annotated with an
      /// instance of [T]. The [annotation] is provided as a [ConstantReader].
      ///
      /// Supported return values include a single [String] or multiple [String]
      /// instances within an [Iterable] or [Stream]. It is also valid to return a
      /// [Future] of [String], [Iterable], or [Stream].
      ///
      /// Implementations should return `null` when no content is generated. Empty
      /// or whitespace-only [String] instances are also ignored.
      /// 这个ConstantReader是被注解的Element,annotation是注解实例
      /// 生成的结果可以是String,Stream<Iterable>,Iterable<String>,Future<String>,Future<Iterable<String>>,Future<Stream<String>>
      /// 可以通过normalizeGeneratorOutput看出来
      dynamic generateForAnnotatedElement(
          Element element, ConstantReader annotation, BuildStep buildStep);
    }
    
   }
      // normalizeGeneratorOutput:
       Stream<String> normalizeGeneratorOutput(Object value) {
            if (value == null) {
              return const Stream.empty();
            } else if (value is Future) {
              return StreamCompleter.fromFuture(value.then(normalizeGeneratorOutput));
            } else if (value is String) {
              value = [value];
            }
          
            if (value is Iterable) {
              value = Stream.fromIterable(value as Iterable);
            }
          
            if (value is Stream) {
              return value.where((e) => e != null).map((e) {
                if (e is String) {
                  return e.trim();
                }
          
                throw _argError(e);
              }).where((e) => e.isNotEmpty);
            }
            throw _argError(value);
          } 
 
复制代码

其中annotatedWith

   Iterable<AnnotatedElement> annotatedWith(TypeChecker checker,
          {bool throwOnUnresolved}) sync* {
          // 迭代输入文件的所有Element
        for (final element in allElements) {
            // 判断Element是否有T注解
          final annotation = checker.firstAnnotationOf(element,
              throwOnUnresolved: throwOnUnresolved);
          if (annotation != null) {
            // 如果被T注解,生成AnnotatedElement
            yield AnnotatedElement(ConstantReader(annotation), element);
          }
        }

复制代码

checker.firstAnnotationOf后面暂时先不分析了,以后继续。

参考:

1.build

2.source_gen

3.tencent

4.alibaba

关注下面的标签,发现更多相似文章
评论