揭秘 Flutter:探索 flutter create

Flutter Create 流程分析

当我们第一次接触 Flutter,跟着官方文档在命令行执行 flutter create 命令后,就会在当前目录新建一个 Flutter 示例项目。这个命令非常方便,它让开发者无需过多地为新建项目花费时间,而只需把精力花费在构建应用本身。那么这一切又是如何发生的呢?

我们先来看看 flutter 命令对应的可执行文件路径在哪:

bash 复制代码
which flutter
/Users/zuckjet/flutter/bin/flutter

flutter/bin/flutter 文件的主要内容如下:

bash 复制代码
#!/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 文件中:

bash 复制代码
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" "$@"
  esac
}

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 文件是如何生成的:

bash 复制代码
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 createflutter runflutter attach 等。所有这些命令都有相对应的一个 dart 文件来实现,这些实现文件均在目录 flutter_tools/lib/src/commands 下面。本文分析的是 flutter create 命令,我们直接看 create.dart 文件就行:

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 命令创建项目的时候可以指定不同的模板,例如 appskeleton 等等。对于这一发现我还是有点好奇的,之前从来不知道还可以指定模板参数,于是在命令行执行 flutter help create 查看文档确认了这一点:

bash 复制代码
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 函数的实现。

dart 复制代码
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 来生成应用所需要的文件:

dart 复制代码
_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](https://zhida.zhihu.com/search?content_id=249696704&content_type=Article&match_order=1&q=mustache&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDI0NTY5NjEsInEiOiJtdXN0YWNoZSIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjI0OTY5NjcwNCwiY29udGVudF90eXBlIjoiQXJ0aWNsZSIsIm1hdGNoX29yZGVyIjoxLCJ6ZF90b2tlbiI6bnVsbH0.qL_WjFaIeywNUgNZDH6Qq7Q9LgM5oBnHQB5xyWh3Mlg&zhida_source=entity) 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 文件为例:

dart 复制代码
import 'package:flutter/material.dart';
{{#withPlatformChannelPluginHook}}
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPlatformChannelPluginHook}}

这是一个典型的 mustache 模板,Mustache 是一套轻逻辑的模板系统,它把模板中的标签展开成给定的数据映射或者对象中的属性值。执行完整个流程以后,一个全新的 Flutter 示例项目就已经生成了。

使用 Appuploader 简化 iOS 应用发布流程

在 Flutter 项目开发完成后,下一步就是将其发布到 App Store。对于 iOS 开发者来说,发布流程可能会比较复杂,尤其是涉及到证书管理、配置文件、以及上传 IPA 文件等步骤。为了简化这一过程,开发者可以使用 Appuploader,这是一款专为 iOS 开发者设计的工具,能够帮助开发者快速生成和管理证书、配置文件,并轻松上传 IPA 文件到 App Store Connect。

Appuploader 的主要功能包括:

  • 证书管理:自动生成和管理开发证书、发布证书,避免手动操作的繁琐。
  • 配置文件生成:自动生成和更新 App ID 和 Provisioning Profile,确保应用能够正确签名。
  • IPA 上传:支持一键上传 IPA 文件到 App Store Connect,省去使用 Xcode 或 Application Loader 的麻烦。

通过使用 Appuploader,开发者可以大大减少在发布流程中花费的时间,专注于应用的开发和优化。

总结

我们通过 flutter create 命令生成了一个新的示例项目,那么如何把这个新生成的项目运行起来呢?这就是下期需要分享的内容,敬请期待。同时,如果你正在开发 iOS 应用,不妨试试 Appuploader,它可以帮助你更高效地完成应用的发布流程。

相关推荐
追逐时光者6 分钟前
C#/.NET/.NET Core技术前沿周刊 | 第 32 期(2025年3.24-3.31)
后端·.net
uhakadotcom7 分钟前
轻松掌握XXL-JOB:分布式任务调度的利器
后端·面试·github
小杨4048 分钟前
springboot框架项目实践应用十三(springcloud alibaba整合sentinel)
spring boot·后端·spring cloud
程序员一诺27 分钟前
【Python使用】嘿马python数据分析教程第1篇:Excel的使用,一. Excel的基本使用,二. 会员分析【附代码文档】
后端·python
神奇侠20241 小时前
快速入手-基于Django-rest-framework的serializers序列化器(二)
后端·python·django
Asthenia04121 小时前
基于Segment-Mybatis的:分布式系统中主键自增拦截器的逻辑分析与实现
后端
Asthenia04121 小时前
Seata:为微服务项目的XID传播设计全局的RequestInterceptor-将XID传播与具体FeignClient行为解耦
后端
无奈何杨1 小时前
Docker/Compose常用命令整理总结
后端
搬砖的阿wei1 小时前
从零开始学 Flask:构建你的第一个 Web 应用
前端·后端·python·flask
草巾冒小子1 小时前
查看pip3 是否安装了Flask
后端·python·flask