[Flutter翻译]【第2部分】在Dart中生成代码:注解、source_gen和build_runner

2,875 阅读6分钟

原文地址:medium.com/flutter-com…

原文作者:medium.com/@jcocaramos

发布时间:2018年10月31日 - 7分钟阅读

在"【第1部分】Dart中的代码生成:基础知识 "中,我们介绍了代码生成背后的动机是什么,并列出了Dart中最重要的工具,让计算机为我们做艰苦的工作。在这篇文章中,我们将介绍如何创建和使用Dart注解,以及如何使用source_genbuild_runner开始处理这些注解。

Dart中的注解

注解是一种表示语法元数据的形式,它可以被添加到我们的Dart代码中;换句话说,是一种向我们的代码中的任何组件(如类或方法)添加额外信息的方式。注解在我们的Dart代码中随处可见:我们使用@required来指定一个命名的参数不是可选的,所以如果被注解的字段不存在,我们的代码就不会被编译,或者我们使用@override来识别那些由父类给出的API在子类中实现。我们怎么知道 它们是注解呢?嗯,它们很容易找到,因为它们的前缀是@。

但是我们如何创建我们的注解呢?

尽管在我们的类中拥有 "元数据 "的想法听起来非常异国情调和复杂,但事实上,注解是Dart中最简单的事情之一。在上面的段落中,我提到注解只是携带额外的信息。它们就像数据类一样。PODO...(Plain Old Dart Objects)。任何类都可以转化为一个注解,只要他们提供一个常量构造函数。

class Todo {
  final String name; final String todoUrl; final
  final String todoUrl.Const Todo(this.name, {this.todoUrl} : assert(name !
  const Todo(this.name, {this.todoUrl}) : assert(name != null);
}
@Todo('hello first annotation', todoUrl: 'https://www.google.com')
class HelloAnnotations {}

正如你所看到的,注解是非常简单的。重要的是我们如何使用这些注解;注解所包含的信息以及我们如何使用这些信息才是它们的特别之处。而这正是source_genbuild_runner会帮助我们的地方。

我们应该如何使用build_runner

build_runner是一个Dart包,它将帮助我们使用Dart代码生成文件。我们将通过build.yaml来配置Builder文件;一旦配置好了,一旦触发了build,或者文件发生了变化,我们就会收到更新,我们就可以解析那些发生了变化或者符合某个标准的代码。

source_gen来理解Dart的代码

在某种程度上,你可以把 build_runner 看成是回答什么时候需要生成代码的机制,而 source_gen 则回答了需要生成什么代码的问题。source_gen 提供了一个框架来构建 build_runner 期待的 Builders,同时暴露了一个友好的 API 来解析和生成代码。

把所有的部件放在一起:一个TODO报告器。

在文章的其余部分,我们将在一个名为todo_reporter.dart的宠物项目上工作,你可以在这个链接中找到它。

这是一个非书面规则,你可以在所有使用代码生成的项目中找到:你将为你的注解创建一个包,并为生成器创建一个不同的包,为这些增加价值。在Dart/Flutter中创建一个库包所需要的所有信息都可以在这个链接中找到。

因此,我们要做的是创建一个文件夹,我将命名为todo_reporter.dart。在这个文件夹中,我将添加我的todo_reporter,将包含注解,todo_reporter_generator处理代码,最后是一个example包,以演示我的库的功能。

我之所以把根文件夹后缀为.dart,是为了清晰明了;虽然这不是强制性的,但我喜欢遵循这一点,以明确这个包可以在任何Dart项目中使用。相反,如果我想把这个包只标记为Flutter,比如ozzie.flutter,那么我会使用不同的后缀。这不是必须要做的,只是我喜欢遵循的一个命名惯例。

创建todo_reporter,我们的注解包,也是最简单的一个包。

我们将在todo_reporter.dart中创建我们的todo_reporter,添加pubspec.yamllib文件夹。pubspec非常简单。

name: todo_reporter
description: Keep track of all your TODOs.
version: 1.0.0
author: Jorge Coca <jcocaramos@gmail.com>
homepage: https://github.com/jorgecoca/todo_reporter.dart 
environment:  
  sdk: ">=2.0.0 <3.0.0" 
dependencies: 
dev_dependencies:  
  test: 1.3.4

除了测试包之外,并没有真正的依赖关系,只是用于开发目的。

lib文件夹中,我们将做以下工作。

  • 我们将创建一个todo_reporter.dart,然后我们将在那里注册所有的类,这些类使用export来暴露我们包的公共API。 这是一个很好的做法,因为我们包中的任何公共类都可以通过import "package:todo_reporter/todo_reporter.dart"来导入。你可以在这里看到这个类的样子:github.com/jorgecoca/t…

  • lib文件夹内,我们现在要创建一个src文件夹,它将包含所有的代码,公共的或非公共的。

在我们的例子中,我们唯一需要包含的就是注释。让我们在里面创建一个包含这些内容的todo.dart文件。

class Todo {  
  final String name; final String todoUrl; final  
  final String todoUrl.Const Todo(this.name, {this.todoUrl} : assert(name !   
  const Todo(this.name, {this.todoUrl}) : assert(name != null);
}

好了,这就是我们需要的所有注释。我说过这很简单,对吧?好吧,我们还没有完成。让我们在测试包中添加一些单元测试。

import 'package:test/test.dart';

import 'package:todo_reporter/todo_reporter.dart';

void main() {
  group('Todo annotation', () {
    test('must have a non-null name', () {
      expect(() => Todo(null), throwsA(TypeMatcher<AssertionError>()));
    });

    test('does not need to have a todoUrl', () {
      final todo = Todo('name');
      expect(todo.todoUrl, null);
    });

    test('if it is a given a todoUrl, it will be part of the model', () {
      final givenUrl = 'http://url.com';
      final todo = Todo('name', todoUrl: givenUrl);
      expect(todo.todoUrl, givenUrl);
    });
  });
}

这是我们创建注解所需要的全部内容。你可以在这个链接中找到代码。

现在让我们在代码生成上下功夫。

做很酷的工作:todo_reporter_generator。

现在我们知道了如何创建包,让我们创建一个叫todo_reporter_generator的包。在它里面,你应该找到一个pubspec.yaml,一个build.yaml文件,一个lib文件夹,在lib文件夹里面,有一个src文件夹和一个todo_reporter_generator.dart文件,我们将在其中包含我们的export语句。我们的todo_reporter_generator被认为是一个不同的包,将作为dev_dependency添加到其他项目中。这是有道理的,因为我们只关心开发过程中的代码生成,而这并不包括在生产捆绑包中。

让我们来看看我们的pubspec.yaml应该是怎样的。

name: todo_reporter_generator
description: An annotation processor for @Todo annotations.
version: 1.0.0
author: Jorge Coca <jcocaramos@gmail.com>
homepage: https://github.com/jorgecoca/todo_reporter.dart 
environment:  
  sdk: ">=2.0.0 <3.0.0" 
dependencies:  
  build: '>=0.12.0 <2.0.0'  
  source_gen: ^0.9.0  
  todo_reporter:    
    path: ../todo_reporter/  
dev_dependencies:  
  build_test: ^0.10.0  
  build_runner: '>=0.9.0 <0.11.0'  
  test: ^1.0.0

现在,让我们完成build.yaml。这个文件将包含你的Builders所需的配置。你可以在这里找到更多信息:github.com/dart-lang/b…

我们的build.yaml此刻看起来会是这样的。

targets:
  $default:
    builders:
      todo_reporter_generator|todo_reporter:
        enabled: true

builders:
  todo_reporter:
    target: ":todo_reporter_generator"
    import: "package:todo_reporter_generator/builder.dart"
    builder_factories: ["todoReporter"]
    build_extensions: {".dart": [".todo_reporter.g.part"]}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

我们的import入口应该指向包含Builder的文件,而builder_factories入口应该指向那些将构建代码的方法。 那么让我们继续创建这些文件:让我们在lib里面创建一个builder.dart文件,在src里面让我们添加一个名为todo_reporter_generator.dart的文件,内容如下。

import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:todo_reporter_generator/src/todo_reporter_generator.dart';

Builder todoReporter(BuilderOptions options) =>
    SharedPartBuilder([TodoReporterGenerator()], 'todo_reporter');

build.dart

import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:source_gen/source_gen.dart';

import 'package:todo_reporter/todo_reporter.dart';

class TodoReporterGenerator extends GeneratorForAnnotation<Todo> {
  @override
  FutureOr<String> generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    return "// Hey! Annotation found!";
  }
}

todo_reporter_generator.dart

我们可以看到,在builder.dart中,我们有一个todoReporter方法,它将创建一个Builder;这个Builder是通过使用一个SharedPartBuilder来提供的,这个SharedPartBuilder接收了我们的TodoReporterGenerator。这就是build_runnersource_gen如何一起工作。

我们的TodoReporterGeneratorGeneratorForAnnotation类型的;也就是说,它只有在找到一段被给定注释的代码时才会执行generateForAnnotatedElement,在我们的例子中就是Todo

generateForAnnotatedElement的返回值是一个String值,将包含我们生成的代码;如果生成的代码没有编译,我们的构建阶段就会失败,这在避免bug时是非常整洁的。

在我们的todo_repoter_generator项目中使用这些文件,每次当尝试自动生成代码时,它将创建一个带有注释的part文件,写着 // Hey! Annotation found! . 我们将在下一篇文章中学习如何处理注释😉。

把所有的碎片放在一起:使用我们的 todo_reporter

开始使用我们的todo_repoter.dart的最后一块是在一个项目上展示它的功能。这是一个很好的做法,当工作包时,添加一个example项目,所以其他开发人员可以看到API是如何在现实世界的项目中使用。

让我们继续创建一个项目,并在pubspec.yaml文件中添加所需的依赖关系;在我的例子中,我只是在example文件夹内创建了一个Flutter项目,并添加了这些依赖关系。

dependencies:
  flutter:
    sdk: flutter
  todo_reporter:
    path: ../todo_reporter/

dev_dependencies:
  build_runner: 1.0.0
  flutter_test:
    sdk: flutter
  todo_reporter_generator:
    path: ../todo_reporter_generator/

现在,在得到包后(`flutter packages get`),我们使用我们的注解。

import 'package:todo_reporter/todo_reporter.dart';

@Todo('Complete implementation of TestClass')
class TestClass {}

有了所有这些部件,让我们继续运行我们的生成器。

$ flutter packages pub run build_runner build

一旦它完成执行这个命令,你会注意到在你的项目上有一个新文件:todo.g.dart,内容如下。

// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo.dart';
// *****************************************************************
// TodoReporterGenerator
// ********************************************************************
// Hey! Annotation found!

成功了! 我们已经完成了我们的任务!现在我们可以为每一个在我们的代码中找到的Todo注释生成一个有效的Dart文件。现在我们可以为代码中发现的每一个Todo注释生成一个有效的Dart文件。试试吧,你可以自由地创建你想要的任何数量的注释。

在下一篇文章中...

现在我们已经有了生成文件的正确设置,在下一篇文章中,我们将学习如何利用我们的注释,让我们的生成代码能够真正做一些很酷的事情,毕竟我们现在生成的代码没有任何目的。


你可以关注我 twitter.com/jcocaramos ,也可以在我的公共Github上看到更多的代码 github.com/jorgecoca


通过www.DeepL.com/Translator(免费版)翻译