做網(wǎng)站有2個前提條件 一個是網(wǎng)站如何做百度免費推廣
說起宏編程可能大家并不陌生,但是這對于 Flutter 和 Dart 開發(fā)者來說它一直是一個「遺憾」,這個「遺憾」體現(xiàn)在編輯過程的代碼修改支持上,其中最典型的莫過于 Dart 的 JSON 序列化。
舉個例子,目前 Dart 語言的 JSON 序列化高度依賴 build_runner
去生成 Dart 代碼,例如在實際使用中我們需要:
- 依賴
json_serializable
,通過注解聲明一個Event
對象 - 運行
flutter packages pub run build_runner build
生成文件 - 得到
Event.g.dart
文件,在項目中使用它去實現(xiàn) JSON 的序列化和反序列化
![]() | ![]() |
---|
這里最大的問題在于,我們需要通過命令行去生成一個項目文件,并且這個文件我們還可以隨意手動修改,從開發(fā)角度來說,這并不優(yōu)雅也不方便。
而宏聲明是用戶定義的 Dart 類,它可以實現(xiàn)一個或多個新的內(nèi)置宏接口,Dart 中的宏是用正常的命令式 Dart 代碼來開發(fā),不存在單獨的“宏語言”。
大多數(shù)宏并不是簡單地從頭開始生成新代碼,而是根據(jù)程序的現(xiàn)有屬性去添加代碼,例如向 Class 添加 JSON 序列化的宏,可能會查看 Class 聲明的字段,并從中合成一個
toJson()
,將這些字段序列化為 JSON 對象。
我們首先看一段官方的 Demo , 如下代碼所示,可以看到 :
MyState
添加了一個自定義的@AutoDispose()
注解,這是一個開發(fā)者自己實現(xiàn)的宏聲明,并且繼承了State
對象,帶有dispose
方法。- 在
MyState
里有多個a
、a2
、b
和c
三個對象,其中a
、a2
、b
都實現(xiàn)了Disposable
接口,都有dispose
方法 - 雖然
a
、a2
、b
和MyState
的dispose();
方法來自不同基類實現(xiàn),但是基于@AutoDispose()
的實現(xiàn),在代碼調(diào)用state.dispose();
時,a
、a2
、b
變量的dispose
方法也會被同步調(diào)用
import 'package:macro_proposal/auto_dispose.dart';void main() {var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');state.dispose();
}()
class MyState extends State {final ADisposable a;final ADisposable? a2;final BDisposable b;final String c;MyState({required this.a, this.a2, required this.b, required this.c});String toString() => 'MyState!';
}class State {void dispose() {print('disposing of $this');}
}class ADisposable implements Disposable {void dispose() {print('disposing of ADisposable');}
}class BDisposable implements Disposable {void dispose() {print('disposing of BDisposable');}
}
如下圖所示,可以看到,盡管 MyState
沒用主動調(diào)用 a
、a2
、b
變量的 dispose
方法,并且它們和 MyState
的 dispose
也來自不同基類,但是最終執(zhí)行所有 dispose
方法都被成功調(diào)用,這就是@AutoDispose()
的宏聲明實現(xiàn)在編譯時對代碼進(jìn)行了調(diào)整。
如下圖所示是 @AutoDispose()
的宏編程實現(xiàn),其中 macro
就是一個標(biāo)志性的宏關(guān)鍵字,剩下的代碼可以看到基本就是 dart 腳本的實現(xiàn), macro
里主要是實現(xiàn) ClassDeclarationsMacro
和buildDeclarationsForClass
方法,如下代碼可以很直觀看到關(guān)于 super.dispose();
和 disposeCalls
的相關(guān)實現(xiàn)。
import 'package:_fe_analyzer_shared/src/macros/api.dart';// Interface for disposable things.
abstract class Disposable {void dispose();
}macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {const AutoDispose();void buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder) async {var methods = await builder.methodsOf(clazz);if (methods.any((d) => d.identifier.name == 'dispose')) {// Don't need to add the dispose method, it already exists.return;}builder.declareInType(DeclarationCode.fromParts([// TODO: Remove external once the CFE supports it.'external void dispose();',]));}Future<void> buildDefinitionForClass(ClassDeclaration clazz, TypeDefinitionBuilder builder) async {var disposableIdentifier =// ignore: deprecated_member_useawait builder.resolveIdentifier(Uri.parse('package:macro_proposal/auto_dispose.dart'),'Disposable');var disposableType = await builder.resolve(NamedTypeAnnotationCode(name: disposableIdentifier));var disposeCalls = <Code>[];var fields = await builder.fieldsOf(clazz);for (var field in fields) {var type = await builder.resolve(field.type.code);if (!await type.isSubtypeOf(disposableType)) continue;disposeCalls.add(RawCode.fromParts(['\n',field.identifier,if (field.type.isNullable) '?','.dispose();',]));}// Augment the dispose method by injecting all the new dispose calls after// either a call to `augmented()` or `super.dispose()`, depending on if// there already is an existing body to call.//// If there was an existing body, it is responsible for calling// `super.dispose()`.var disposeMethod = (await builder.methodsOf(clazz)).firstWhere((method) => method.identifier.name == 'dispose');var disposeBuilder = await builder.buildMethod(disposeMethod.identifier);disposeBuilder.augment(FunctionBodyCode.fromParts(['{\n',if (disposeMethod.hasExternal || !disposeMethod.hasBody)'super.dispose();'else'augmented();',...disposeCalls,'}',]));}
}
到這里大家應(yīng)該可以直觀感受到宏編程的魅力,上述 Demo 來自 dart-language 的 macros/example/auto_dispose_main ,其中 bin/
目錄下的代碼是運行的腳本示例,lib/
目錄下的代碼是宏編程實現(xiàn)的示例:
https://github.com/dart-lang/language/tree/main/working/macros/example
當(dāng)然,因為現(xiàn)在是實驗性階段,API 和穩(wěn)定性還有待商榷,所以想運行這些 Demo 還需要一些額外的處理,比如版本強關(guān)聯(lián),例如上述的 auto_dispose_main
例子:
-
需要 dart sdk 3.4.0-97.0.dev ,目前你可以通過 master 分支下載這個 dark-sdk https://storage.googleapis.com/dart-archive/channels/main/raw/latest/sdk/dartsdk-macos-arm64-release.zip
-
將 sdk 配置到環(huán)境變量,或者進(jìn)入到 dart sdk 的 bin 目錄執(zhí)行 ./dart --version 檢查版本
-
進(jìn)入上訴的 example 下執(zhí)行 dart pub get,過程可能會有點長
-
最后,執(zhí)行
dart --enable-experiment=macros bin/auto_dispose_main.dart
,記得這個 dart 是你指定版本的 dart 。
另外,還有一個第三方例子是來自 millsteed 的 macros ,這是一個簡單的 JSON 序列化實現(xiàn) Demo ,并且可以直接不用額外下載 dark-sdk,通過某個 flutter 內(nèi)置 dart-sdk 版本就可以滿足條件:3.19.0-12.0.pre
:
在本地 Flutter 目錄下,切換到
git checkout 3.19.0-12.0.pre
,然后執(zhí)行 flutter doctor 初始化 dark sdk 即可。
代碼的實現(xiàn)很簡單,首先看 bin 下的示例,通過 @Model()
將 GetUsersResponse
和 User
聲明為 JSON 對象,然后在運行時,宏編程會自動添加 fromJson
和 toJson
方式。
import 'dart:convert';import 'package:macros/model.dart';()
class User {User({required this.username,required this.password,});final String username;final String password;
}()
class GetUsersResponse {GetUsersResponse({required this.users,required this.pageNumber,required this.pageSize,});final List<User> users;final int pageNumber;final int pageSize;
}void main() {const body = '''{"users": [{"username": "ramon","password": "12345678"}],"pageNumber": 1,"pageSize": 30}''';final json = jsonDecode(body) as Map<String, dynamic>;final response = GetUsersResponse.fromJson(json);final ramon = response.users.first;final millsteed = ramon.copyWith(username: 'millsteed', password: '87654321');final newResponse = response.copyWith(users: [...response.users, millsteed]);print(const JsonEncoder.withIndent(' ').convert(newResponse));
}
而 Model
的宏實現(xiàn)就相對復(fù)雜一些,但是實際上就是將類似 freezed
/ json_serializable
是實現(xiàn)調(diào)整到宏實現(xiàn)了,而最終效果就是,開發(fā)者使用起來更加優(yōu)雅了。
// ignore_for_file: depend_on_referenced_packages, implementation_importsimport 'dart:async';import 'package:_fe_analyzer_shared/src/macros/api.dart';macro class Model implements ClassDeclarationsMacro {const Model();static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];static const _collectionTypes = ['List'];Future<void> buildDeclarationsForClass(ClassDeclaration classDeclaration,MemberDeclarationBuilder builder,) async {final className = classDeclaration.identifier.name;final fields = await builder.fieldsOf(classDeclaration);final fieldNames = <String>[];final fieldTypes = <String, String>{};final fieldGenerics = <String, List<String>>{};for (final field in fields) {final fieldName = field.identifier.name;fieldNames.add(fieldName);final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;fieldTypes[fieldName] = fieldType;if (_collectionTypes.contains(fieldType)) {final generics = (field.type.code as NamedTypeAnnotationCode).typeArguments.map((e) => (e as NamedTypeAnnotationCode).name.name).toList();fieldGenerics[fieldName] = generics;}}final fieldTypesWithGenerics = fieldTypes.map((name, type) {final generics = fieldGenerics[name];return MapEntry(name,generics == null ? type : '$type<${generics.join(', ')}>',);},);_buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);_buildToJson(builder, fieldNames, fieldTypes);_buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);_buildToString(builder, className, fieldNames);_buildEquals(builder, className, fieldNames);_buildHashCode(builder, fieldNames);}void _buildFromJson(MemberDeclarationBuilder builder,String className,List<String> fieldNames,Map<String, String> fieldTypes,Map<String, List<String>> fieldGenerics,) {final code = ['factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),'return $className('.indent(4),for (final fieldName in fieldNames) ...[if (_baseTypes.contains(fieldTypes[fieldName])) ...["$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},".indent(6),] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...["$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),'.whereType<Map<String, dynamic>>()'.indent(10),'.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),'.toList(),'.indent(10),] else ...['$fieldName: ${fieldTypes[fieldName]}'".fromJson(json['$fieldName'] "'as Map<String, dynamic>),'.indent(6),],],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildToJson(MemberDeclarationBuilder builder,List<String> fieldNames,Map<String, String> fieldTypes,) {final code = ['Map<String, dynamic> toJson() {'.indent(2),'return {'.indent(4),for (final fieldName in fieldNames) ...[if (_baseTypes.contains(fieldTypes[fieldName])) ...["'$fieldName': $fieldName,".indent(6),] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...["'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),] else ...["'$fieldName': $fieldName.toJson(),".indent(6),],],'};'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildCopyWith(MemberDeclarationBuilder builder,String className,List<String> fieldNames,Map<String, String> fieldTypes,) {final code = ['$className copyWith({'.indent(2),for (final fieldName in fieldNames) ...['${fieldTypes[fieldName]}? $fieldName,'.indent(4),],'}) {'.indent(2),'return $className('.indent(4),for (final fieldName in fieldNames) ...['$fieldName: $fieldName ?? this.$fieldName,'.indent(6),],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildToString(MemberDeclarationBuilder builder,String className,List<String> fieldNames,) {final code = ['@override'.indent(2),'String toString() {'.indent(2),"return '$className('".indent(4),for (final fieldName in fieldNames) ...[if (fieldName != fieldNames.last) ...["'$fieldName: \$$fieldName, '".indent(8),] else ...["'$fieldName: \$$fieldName'".indent(8),],],"')';".indent(8),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildEquals(MemberDeclarationBuilder builder,String className,List<String> fieldNames,) {final code = ['@override'.indent(2),'bool operator ==(Object other) {'.indent(2),'return other is $className &&'.indent(4),'runtimeType == other.runtimeType &&'.indent(8),for (final fieldName in fieldNames) ...[if (fieldName != fieldNames.last) ...['$fieldName == other.$fieldName &&'.indent(8),] else ...['$fieldName == other.$fieldName;'.indent(8),],],'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildHashCode(MemberDeclarationBuilder builder,List<String> fieldNames,) {final code = ['@override'.indent(2),'int get hashCode {'.indent(2),'return Object.hash('.indent(4),'runtimeType,'.indent(6),for (final fieldName in fieldNames) ...['$fieldName,'.indent(6),],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}
}extension on String {String indent(int length) {final space = StringBuffer();for (var i = 0; i < length; i++) {space.write(' ');}return '$space$this';}
}
目前宏還處于試驗性質(zhì)的階段,所以 API 還在調(diào)整,這也是為什么上面的例子需要指定 dart 版本的原因,另外宏目前規(guī)劃里還有一些要求,例如
- 所有宏構(gòu)造函數(shù)都必須標(biāo)記為
const
- 所有宏必須至少實現(xiàn)其中一個
Macro
接口 - 宏不能是抽象對象
- 宏 class 不能由其他宏生成
- 宏 class 不能包含泛型類型參數(shù)
- 每個宏接口都需要聲明宏類必須實現(xiàn)的方法,例如,在聲明階段應(yīng)用的
ClassDeclarationsMacro
及其buildDeclarationsForClass
方法。
未來規(guī)劃里,宏 API 可能會作為 Pub 包提供,通過庫 dart:_macros
來提供支持 ,具體還要等正式發(fā)布時 dart 團(tuán)隊的決策。
總的來說,這對于 dart 和 flutter 是一個重大的厲害消息,雖然宏編程并不是什么新鮮概念,該是 dart 終于可以優(yōu)雅地實現(xiàn) JSON 序列化,并且還是用 dart 來實現(xiàn),這對于 flutter 開發(fā)者來說,無疑是最好的新年禮物。
所以,新年快樂~我們節(jié)后再見~