flutter create 流程分析
当我们第一次接触 Flutter, 跟着官方文档在命令行执行 flutter create 命令后,就会在当前目录新建一个 Flutter 示例项目。这个命令非常方便,它让开发者无需过多地为新建项目花费时间,而只需把精力花费在构建应用本身。那么这一切又是如何发生的呢?
我们先来看看 flutter 命令对应的可执行文件路径在哪:
which flutter
/Users/zuckjet/flutter/bin/flutter
flutter/bin/fluttter文件的主要内容如下:
#!/usr/bin/env bash
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
SHARED_NAME="$BIN_DIR/internal/shared.sh"
OS="$(uname -s)"
# To define `shared::execute()` function
source "$SHARED_NAME"
shared::execute "$@"
不难看出,在命令行执行 flutter 命令实际上是执行了这个 shell 脚本,且这个 shell 文件里面的主要操作是执行了 shared::execute 函数。该函数定义在 bin/internal/shared.sh 文件中:
function shared::execute() {
FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
DART="$DART_SDK_PATH/bin/dart"
case "$BIN_NAME" in
flutter*)
# FLUTTER_TOOL_ARGS aren't quoted below, because it is meant to be
# considered as separate space-separated args.
exec "$DART" --packages="$FLUTTER_TOOLS_DIR/.dart_tool/package_config.json" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
}
shared::execute 函数主要内容是使用了 dart 命令来执行 flutter_tools.snapshot 文件。这里有两个问题需要弄清楚:
- snapshot 文件是什么,dart 命令执行的不应该都是 dart 文件吗 ?
- flutter_tools.snapshot 是如何生成的。
Dart 除了可以直接执行 dart 文件,也可以执行 snapshot 文件,snapshot 文件有以下三种类型:
- Kernel Snapshots, 它使 Dart 程序员能够将应用程序打包成一个单一的文件,并减少启动时间。但缺少已解析的类和函数,也缺乏编译后的代码。
- JIT Application Snapshots,它和 Kernel Snapshots 的主要区别是它包含已解析的类和已编译的代码。
- AOT Application Snapshots。整个程序进行提前编译(AOT)。
关于 snapshot 的更多信息,这个和 Dart 虚拟机原理相关,后续会单独写一篇文章介绍,就不在此展开了。我们只需要知道为了提高性能和减少启动时间,flutter create 命令执行的是 snapshot 文件而非直接执行 dart 文件。接下来我们看看 flutter_tools.snapshot 文件是如何生成的:
FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
SCRIPT_PATH="$FLUTTER_TOOLS_DIR/bin/flutter_tools.dart"
"$DART" --verbosity=error $FLUTTER_TOOL_ARGS --snapshot="$SNAPSHOT_PATH" --snapshot-kind="app-jit" --packages="$FLUTTER_TOOLS_DIR/.dart_tool/package_config.json" --no-enable-mirrors "$SCRIPT_PATH"
在这里我们使用 dart --snapshot 命令将 flutter_tools.dart 转换为 flutter_tools.snapshot 文件,且生成的是 JIT Application Snapshots。现在我们知道了, 当我们在命令行执行 flutter 相关的命令,真正的入口文件都是在 flutter_tools.dart 文件。
flutter_tools.dart 文件本身没有什么内容,它只是所有命令的一个入口。安装 Flutter 以后命令行可以使用很多 flutter 命令,例如:flutter create、flutter run、flutter attach等。所有这些命令都有相对应的一个 dart 文件来实现,这些实现文件均在目录 flutter_tools/lib/src/commands 下面。本文分析的是 flutter create 命令,我们直接看 create.dart 文件就行:
Future<FlutterCommandResult> runCommand() async {
...
switch (template) {
case FlutterProjectType.app:
final bool skipWidgetTestsGeneration =
sampleCode != null || emptyArgument;
generatedFileCount += await generateApp(
<String>['app', if (!skipWidgetTestsGeneration) 'app_test_widget'],
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.create;
case FlutterProjectType.skeleton:
generatedFileCount += await generateApp(
<String>['skeleton'],
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
generateMetadata: false,
);
pubContext = PubContext.create;
case FlutterProjectType.module:
generatedFileCount += await _generateModule(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
);
}
}
create.dart 文件里的一个核心逻辑是在 runCommand 函数里调用 generateApp 函数生成整个应用。通过这里的 switch 语句可以推断出,flutter create 命令创建项目的时候可以指定不同的模板,例如 app, skeletion等等。对于这一发现我还是有点好奇的,之前从来不知道还可以指定模板参数,于是在命令行执行 flutter help create 查看文档确认了这一点:
flutter help create
-t, --template=<type> Specify the type of project to create.
[app] (default) Generate a Flutter application.
[module] Generate a project to add a Flutter module to an existing Android or iOS application.
[package] Generate a shareable Flutter project containing modular Dart code.
[skeleton] Generate a List View / Detail View Flutter application that follows community best practices.
...
验证了我的猜想以后,我们继续来看 generateApp 函数的实现。
Future<int> generateApp() async {
int generatedCount = 0;
generatedCount += await renderMerged(
<String>[...templateNames, 'app_shared'],
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
Future<int> renderMerged(}) async {
final Template template = await Template.merged(
names,
directory,
fileSystem: globals.fs,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
templateManifest: _templateManifest,
);
return template.render(
directory,
context,
overwriteExisting: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
}
generateApp 函数的主要逻辑是 renderMerged,而后者关键内容是调用 template.render 来生成应用所需要的文件:
_templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
finalDestinationFile.createSync(recursive: true);
final File sourceFile = _fileSystem.file(absoluteSourcePath);
// Step 2: If the absolute paths ends with a '.copy.tmpl', this file does
// not need mustache rendering but needs to be directly copied.
if (sourceFile.path.endsWith(copyTemplateExtension)) {
sourceFile.copySync(finalDestinationFile.path);
return;
}
// Step 3: If the absolute paths ends with a '.img.tmpl', this file needs
// to be copied from the template image package.
if (sourceFile.path.endsWith(imageTemplateExtension)) {
imageSourceFile.copySync(finalDestinationFile.path);
} else {
throwToolExit('Image File not found ${finalDestinationFile.path}');
}
return;
}
// Step 4: If the absolute path ends with a '.tmpl', this file needs
// rendering via mustache.
if (sourceFile.path.endsWith(templateExtension)) {
// Use a copy of the context,
// since the original is used in rendering other templates.
final Map<String, Object?> localContext = finalDestinationFile.path.endsWith('.yaml')
? _createEscapedContextCopy(context)
: context;
final String renderedContents = _templateRenderer.renderString(templateContents, localContext);
finalDestinationFile.writeAsStringSync(renderedContents);
return;
}
// Step 5: This file does not end in .tmpl but is in a directory that
// does. Directly copy the file to the destination.
sourceFile.copySync(finalDestinationFile.path);
});
_templateFilePaths 是一个 Map 对象,里面存储着模板文件的路径,它们均位于 packages/flutter_tools/templates 目录下。这里对每一个模板文件进行处理,当模板文件名以 .copy.tmpl 和 .img.tmpl 为后缀时直接复制过来。当模板文件名以 .tmpl为后缀时,通过 mustache_template 包渲染生成新的文件。我们拿 main.dart.tmpl 文件为例:
import 'package:flutter/material.dart';
{{#withPlatformChannelPluginHook}}
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPlatformChannelPluginHook}}
这是一个典型的 mustache 模板,Mustache 是一套轻逻辑的模板系统,它把模板中的标签展开成给定的数据映射或者对象中的属性值。执行完整个流程以后,一个全新的 flutter 示例项目就已经生成了。
我们通过 flutter create 命令生成了一个新的示例项目,那么如何把这个新生成的项目运行起来呢?这就是下期需要分享的内容,敬请期待。