揭秘 Flutter:探索 flutter create

0 阅读2分钟

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 文件。这里有两个问题需要弄清楚:

  1. snapshot 文件是什么,dart 命令执行的不应该都是 dart 文件吗 ?
  2. flutter_tools.snapshot 是如何生成的。

Dart 除了可以直接执行 dart 文件,也可以执行 snapshot 文件,snapshot 文件有以下三种类型:

  1. Kernel Snapshots, 它使 Dart 程序员能够将应用程序打包成一个单一的文件,并减少启动时间。但缺少已解析的类和函数,也缺乏编译后的代码。
  2. JIT Application Snapshots,它和 Kernel Snapshots 的主要区别是它包含已解析的类和已编译的代码。
  3. 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 命令生成了一个新的示例项目,那么如何把这个新生成的项目运行起来呢?这就是下期需要分享的内容,敬请期待。