[Flutter翻譯]【第2部分】在Dart中生成程式碼:註解、source_gen和build_runner

Sunbreak發表於2020-07-22

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

原文作者:medium.com/@jcocaramos

釋出時間:2018年10月31日 - 7分鐘閱讀

[Flutter翻譯]【第2部分】在Dart中生成程式碼:註解、source_gen和build_runner

在"【第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(免費版)翻譯

相關文章