Linuxword Global
当前位置: 建站相关 > Flutter,性能分析,工程架构与细节处理

一、为何 Flutter
跨端技术众多,为何选择 Flutter?它能带来哪些优势,有哪些缺点?
先看看具体的工程效果:
https://link.zhihu.com/?target=https%3A//v.qq.com/txp/iframe/player.html%3Fvid%3Dz316221rmve
web 端效果体验:
https://link.zhihu.com/?target=https%3A//test-pupilmath.youdao.com/pupil-flutter-test/web/%23/

1.1 Flutter VS 原生

无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,Flutter 是目前为止比较推荐的利器。

v2-2d5f62ac5c6fec8fb2579cfce7430ca3_720w-1

1.2 Flutter VS Web

任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如 React Native 等都是基于web的跨端性解决方案,但是大家都知道,web 在移动端上的运行效率和 PC 上有巨大差距的,这就导致 RN 不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了 Airbnb 的 Lottie 引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。

v2-302248d9c9af29c038b872a4b758328c_720w

1.3 Flutter 性能

Flutter 的编译方式和产物是决定其高效运行效率的前提,不同于 web 的跨端编译一样(web 的跨端编译大多是选择了使用 “桥” 的概念来调用编译产物,通常是使用了原生端的入口 + web 端的桥来实现),Flutter 几乎是把 dart 的源码通过不同平台的编译原理生成各平台的产物,这种“去桥”的产物正式我们所希望得到的、贴近原生运行性能的编译产物(当然,在 dart 最初设计的时候,是参考了很多前端的结构来完成的,特别从语法上面能够很明显地感受到前端的痕迹,而且最初的 dart2js 的原理也是同样“桥”的概念)。

例如 9月23号 google 发布的新 Flutter 版本中,在支持的 Windows 编译产物上,就是通过类似 Visual Studio 的编译工具(如果要将你的 Flutter 工程编译成 Windows 产物,需要提前安装一些 VS 相关的编译插件),生成了 Windows 下的工程解决方案 .sln,最终生成 dll 的调用方式,运行起来很流畅,可以下载附件中的 Release.zip 来尝试运行。(Release.zip 下载)

v2-ebc257942a3891bf99350464c6b08755_720w

v2-3bb127694a67694e72ce8ef22db39586_720w

(PS:这里所有编译工程都是通过同一套代码完成,包括上文中的 web 地址、移动端案例还有这里的 Windows 案例)

1.4 与 RN 的性能对比

以上是同样功能模块下,Flutter 和 RN 的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组。

v2-8f0e2ea45ae7e8e469fe81d27e33ffda_720w

v2-67a1604a3a511a019a18fe739f751156_720w

1.5 跨端平台的多样性

v2-7f7eb8e2b4a8762c3a93a15b03ead535_720w

1.6 引擎

Flare-Flutter 是一款十分优秀的 Flutter 动画引擎,编译出的动画已经在 Windows、移动端、web 上亲测验证过。

v2-53989031007d483faaf58a79997be4eb_720w

1.7 语法糖

A?.B 如果 A 等于 null,那么 A?.B 为 null 如果 A 不等于 null,那么 A?.B 等价于 A.B Animal animal = new Animal(‘cat’); Animal empty = null; //animal 非空,返回 animal.name 的值 cat print(animal?.name); //empty 为空,返回 null print(empty?.name); A??B 如果 A 等于 null,那么 A??B 为 B 如果 A 不等于 null,那么 A??B 为 A

1.8 综合测评

v2-c0bfd5744ad559ea8dedd00f73657ce1_720w

1.9 互动应用

Flutter 生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望)。

二、Flutter 业务架构

Flutter 中目前是没有现成的 mvvm 框架的,但是我们可以利用 Element 树特性来实现 mvvm。

2.1 ViewModel

 

 

abstract class BaseViewModel {
  bool _isFirst = true;
  BuildContext context;

  bool get isFirst => _isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    this.context = context;
    if (_isFirst) {
      _isFirst = false;
      doInit(context);
    }
  }

  // the default load data method
  @protected
  Future refreshData(BuildContext context);

  @protected
  void doInit(BuildContext context);

  void dispose();

class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {
  final T viewModel;
  final Widget child;

  ViewModelProvider({
    @required this.viewModel,
    @required this.child,
  });

  static T of<T extends BaseViewModel>(BuildContext context) {
    final type = _typeOf<_ViewModelProviderInherited<T>>();
    _ViewModelProviderInherited<T> provider =
        // 查询Element树中缓存的InheritedElement
        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.viewModel;
  }

  static Type _typeOf<T>() => T;

  @override
  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();
}

class _ViewModelProviderState<T extends BaseViewModel>
    extends State<ViewModelProvider<T>> {
  @override
  Widget build(BuildContext context) {
    return _ViewModelProviderInherited<T>(
      child: widget.child,
      viewModel: widget.viewModel,
    );
  }

  @override
  void dispose() {
    widget.viewModel.dispose();
    super.dispose();
  }
}

// InheritedWidget可以被Element树缓存
class _ViewModelProviderInherited<T extends BaseViewModel>
    extends InheritedWidget {
  final T viewModel;

  _ViewModelProviderInherited({
    Key key,
    @required this.viewModel,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;





 

2.2 DataModel

 

 

import 'dart:convert';

import 'package:pupilmath/datamodel/base_network_response.dart';
import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';
import 'package:pupilmath/utils/text_utils.dart';

///历史榜单
class ChallengeHistoryRankingListResponse
    extends BaseNetworkResponse<ChallengeHistoryRankingData> {
  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)
      : super.fromJson(json);

  @override
  ChallengeHistoryRankingData decodeData(jsonData) {
    if (jsonData is Map) {
      return ChallengeHistoryRankingData.fromJson(jsonData);
    }
    return null;
  }
}

class ChallengeHistoryRankingData {
  String props;
  int bestRank; //最佳排名
  int onlistTimes; //上榜次数
  int total; //总共挑战数
  List<ChallengeHistoryRankingItemData> ranks; //先给10天

  //二维码
  String get qrcode =>
      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';

  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {
    props = json['props'];
    bestRank = json['bestRank'];
    onlistTimes = json['onlistTimes'];
    total = json['total'];
    if (json['ranks'] is List) {
      ranks = [];
      (json['ranks'] as List).forEach(
          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));
    }
  }
}

///历史战绩的item
class ChallengeHistoryRankingItemData {
  ChallengeRankingListItemData champion; //当天最好成绩
  ChallengeRankingListItemData user;

  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {
    if (json['champion'] is Map)
      champion = ChallengeRankingListItemData.fromJson(json['champion']);
    if (json['user'] is Map)
      user = ChallengeRankingListItemData.fromJson(json['user']);
  }





 

2.3 View

 

 

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';
import 'package:pupilmath/entity_factory.dart';
import 'package:pupilmath/network/constant.dart';
import 'package:pupilmath/network/network.dart';
import 'package:pupilmath/utils/print_helper.dart';
import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';
import 'package:rxdart/rxdart.dart';

//每日挑战历史战绩
class ChallengeHistoryListViewModel extends BaseViewModel {
  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =
      BehaviorSubject();

  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>
      _challengeObservable.stream;

  @override
  void dispose() {
    _challengeObservable.close();
  }

  @override
  void doInit(BuildContext context) {
    refreshData(context);
  }

  @override
  Future refreshData(BuildContext context) {
    return _loadHistoryListData();
  }

  _loadHistoryListData() async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = 1;
    parametersMap["pageSize"] = 10; //拿10天数据

    handleDioRequest(
      () => NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap),
      onResponse: (Response response) {
        ChallengeHistoryRankingListResponse rankingListResponse =
            EntityFactory.generateOBJ(json.decode(response.toString()));

        if (rankingListResponse.isSuccessful) {
          _challengeObservable.add(rankingListResponse.data);
        } else {
          _challengeObservable.addError(null);
        }
      },
      onError: (error) => _challengeObservable.addError(error),
    );
  }

  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(
    int pageNum,
    int pageSize,
  ) async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = pageNum;
    parametersMap["pageSize"] = pageSize;

    try {
      Response response = await NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap);
      ChallengeHistoryRankingListResponse rankingListResponse =
          EntityFactory.generateOBJ(json.decode(response.toString()));
      if (rankingListResponse.isSuccessful) {
        return rankingListResponse.data;
      } else {
        return null;
      }
    } catch (e) {
      printHelper(e);
    }
    return null;
  }





 

2.4 一些基础架构

v2-e503c77ae3c17fc0186001ac581d63e8_720w

2.5 View 和 ViewModel 如何实现初始化和相互作用

v2-dbeebb242e10af336fbda0208ff5bf05_720w

2.6 Flutter 业务架构抽离

如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用 Flutter 的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。

v2-ff226c6d70bb46c0014063a5ef2df33a_720w

三、Flutter 适配

任何框架中的 UI 适配都是特别繁重的工作,跨端上的适配更是如此,因此在同一套布局里面,各个平台的换算过程显得尤为重要,起初的时候,Flutter 中并没有提供某种诸如 dp 或者 sp 的适配方式,而且考虑到直接更改底层 Matrix 换算比例的话可能会让原本高清分辨率的手机显示不是那么清楚,而 Flutter 的宽高单位都是 num,最后编译的时候才会去对应到各个平台的单位尺寸。

为了减轻设计师的设计负担,这里通常使用一套 iOS 的设计稿即可,以375 x 667的通用设计稿为例,转换过来到android上是360 x 640 (对应1080 x 1920),这里flutter的单位也是和对应手机的像素密度有关的。

3.1 构造一个转换工具类:

 

 

//目前适配iPhone和iPad机型尺寸
import 'dart:io';
import 'dart:ui';
import 'dart:math';

import 'package:pupilmath/utils/print_helper.dart';

bool initScale = false;
//针对iOS平台的scale系数
double iosScaleRatio = 0;
//针对android平台的scale系数
// (因为所有设计稿均使用iOS的设计稿进行,所以需要转换为android设计稿上的尺寸,
// 否则无法进行小屏幕上的适配)
double androidScaleRatio = 0;
//文字缩放比
double textScaleRatio = 0;

const double baseIosWidth = 375;
const double baseIosHeight = 667;
const double baseIosHeightX = 812;

const double baseAndroidWidth = 360;
const double baseAndroidHeight = 640;

void _calResizeRatio() {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final height = window.physicalSize.height;
    final ratio = window.devicePixelRatio;
    final widthScale = (width / ratio) / baseIosWidth;
    final heightScale = (height / ratio) / baseIosHeight;
    iosScaleRatio = min(widthScale, heightScale);
  } else if (Platform.isAndroid) {
    double widthScale = (baseAndroidWidth / baseIosWidth);
    double heightScale = (baseAndroidHeight / baseIosHeight);
    double scaleRatio = min(widthScale, heightScale);
    //取两位小数
    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));
  }
}

bool isFullScreen() {
  return false;
}

//缩放
double resizeUtil(double value) {
  if (!initScale) {
    _calResizeRatio();
    initScale = true;
  }

  if (Platform.isIOS) {
    return value * iosScaleRatio;
  } else if (Platform.isAndroid) {
    return value * androidScaleRatio;
  } else {
    return value;
  }
}

//缩放还原
//每个屏幕的缩放比不一样,如果在iOS设备上出题,则题目坐标值需要换算成原始坐标,加载的时候再通过不同平台换算回来
double unResizeUtil(double value) {
  if (iosScaleRatio == 0) {
    _calResizeRatio();
  }

  if (Platform.isIOS) {
    return value / iosScaleRatio;
  } else {
    return value / androidScaleRatio;
  }
}

//文字缩放大小
_calResizeTextRatio() {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;
  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;
  textScaleRatio = min(heightRatio, widthRatio);
}

double resizeTextSize(double value) {
  if (textScaleRatio == 0) {
    _calResizeTextRatio();
  }
  return value * textScaleRatio;
}

double resizePadTextSize(double value) {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final ratio = window.devicePixelRatio;
    final realWidth = width / ratio;
    if (realWidth > 450) {
      return value * 1.5;
    } else {
      return value;
    }
  } else {
    return value;
  }
}

double autoSize(double percent, bool isHeight) {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  if (isHeight) {
    return height / ratio * percent;
  } else {
    return width / ratio * percent;
  }





 

3.2 具体使用:

v2-6b6fd792509bdc6cc6fdb719b4e7f1d0_720w

这样每次如果有分辨率变动或者适配方案变动的时候,直接修改 resizeUtil 即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用 dart 语言的扩展函数特性,为 resizeUtil 做一些改进。

3.3 低侵入式的 resizeUtil

通过扩展 dart 的 num 来构造想要的单位,这里用 dp 和 sp 来举例,在 resizeUtil 中加入扩展:

 

 

extension dimensionsNum on num {
  ///转为dp
  double get dp => resizeUtil(this.toDouble());

  ///转为文本大小sp
  double get sp => resizeTextSize(this.toDouble());

  ///转为pad文字适配
  double get padSp => resizePadTextSize(this.toDouble());





 

然后在布局中直接书写单位即可:

v2-02c96bf78d9572cfbbaf10832db95ac2_720w

v2-dbc1a43a6628d9557d2c3dc74b2193cc_720w

四、Flutter 中的一些坑

4.1 泛型上的坑

刚开始在移动端上使用泛型来做数据的自动解析时,使用了 T.toString 来判断类型,但是当编译成 web 的 release 版本时,在移动端正常运行的程序在web上无法正常工作:

v2-c955c1a9536d53f4d2221fa47b970425_720w

刚开始的时候把目标一直定位在编译的方式上,因为存在 dev profile release 三种编译模式,只有在 release 上无法运行,误以为是 release 下编译有 bug,随着和 Flutter 团队的深入讨论后,发现其实是泛型在 release 模式下的坑,即在 web 版本的 release 模式下,一切都会进行压缩(包含类型的定义),所以在 release 下,T.toString() 返回的是 null,因此无法识别出泛型特征,具体的讨论链接:

Flutter application which use canvas to build self-CustomPainter cannot work on browser if i used the release mode by command “flutter run -d chrome –release” or “flutter build web”. · Issue #47967 · flutter/flutter​github.com

In release mode everything is minified, the (T.toString() == “Construction2DEntity”) comparison fails and you get entity null returned. If you change the code to (T ==Construction2DEntity) it will fix your app.

最后建议,无论在何种模式下,都直接写成T==的形式最为安全。

 

 

class EntityFactory {
  static T generateOBJ<T>(json) {
    if (1 == 0) {
      return null;
    } else if (T == "ChallengeRankingListDataEntity") {
      /// 每日挑战排行榜
      return ChallengeHomeRankingListResponse.fromJson(json) as T;
    } else if (T == "KnowledgeEntity") {
      return KnowledgeEntity.fromJson(json) as T;
    }
  }





 

4.2 在编译成 web 产物后如何使用 iframe 来加载其他网页

对于移动端来说,webview_flutter 可以解决掉加载 web 的问题,不过编译成 web 产物后,已经无法直接使用 WebView 插件来进行加载,此时需要用到 dart 最初设计来编写网页的一些方式,即 HtmlElmentView:

 

 

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:html' as html;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
           child: Iframe()
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}
class Iframe extends StatelessWidget {
  Iframe(){
    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {
      var iframe = html.IFrameElement();
      iframe.src='https://flutter.dev';
      return iframe;
  });
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width:400,
      height:300,
      child:HtmlElementView(viewType: 'iframe')
    );
  }





不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:

https://github.com/flutter/flutter/issues/53253

4.3 Flutter 如何加载本地的 html 并且进行通信

内置 html 是很多工程的需求,很多网上的资料都是通过把本地的 html 做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过 IFrameElement 来进行加载并通信,做法和前端很类似:

v2-6069b458d5e2fb521425698874f736ab_720w

v2-fc8739526e429088eec81a592d0d589e_720w

4.4 在 iOS 13.4 上 WebView 的手势无法正常使用

官方的 webview_flutter 在上一个版本当 iOS 升级到13.4之后会出现手势被拦截且无法正常使用的情况,换成flutter_webview_plugin后暂时解决掉该问题(目前 WebView 已经做了针对性的修复,但是还未验证),但是 flutter_webview_plugin 在 iOS 上又无法写入 user-agent,目前可以通过修改本地的插件代码进行解决:

文件位置为:

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m修改内容为在146行(initWebview方法中初始化WKWebViewConfiguration后)添加如下代码if (@available(iOS 9.0, *)) {if (userAgent != (id)[NSNull null]) {self.webview.customUserAgent = userAgent;}}

关于 webview_flutter 的手势问题还在不断的讨论中:

https://github.com/flutter/flutter/issues/53490

五、关于布局和运算

5.1 容器 Widget 和渲染 Widget

v2-377ff21184a9c9dcddc55648cdb67c3c_720w

5.2 GlobalKey

通过 GlobalKey 获取 RenderBox 来获取渲染出的控件的 size 和 position 等参数:

v2-64eb941646e56e8745e5c8602a9f2f00_720w

v2-633c34acfa94563a2f2961c2fb3a61d9_720w

5.3 浮点运算

在 dart 的浮点运算中,由于都是高精度的 double 运算,当运算长度过长的时候,dart 会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:

v2-a5f0338d5b88d4244ed36defc08e1a8c_720w

5.4 Matrix 的平移和旋转

在矩阵的换算过程中,如果使用普通的matrix.translate,会导致 rotate 之后,再进行 translate 会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有 rotate 操作时,应当使用 leftTranslate 来保证每次运算的独立性:

v2-d28819aab9130106cb2c9c10fe843bc5_720w

六、项目优化

6.1 避免 build() 方法耗时:

v2-3cc4afc9e52e3b7fa53f5ef657d85aa4_720w

v2-bb748df7dcaca1651e1f7d91dd899660_720w

v2-46890840fcbef57d9603b79ac4e37714_720w

6.2 重绘区域优化:

v2-70f34673a5928087943fadc9a67d7f33_720w

v2-b36d5bc8cfc61c6375a5ff37e637331f_720w

v2-0ccd47148d518e699c1b16b726270a49_720w

6.3 尽量避免使用 Opacity

v2-bfaa8ab2dd44743862620bef0783a284_720w

6.4 Flutter的单线程模型

优先全部执行完 Microtask Queue 中的 Event,直到 Microtask Queue 为空,才会执行 Event Queue 中的 Event。

v2-16e41b7003339854e98c2b1918efda99_720w

v2-2d9ed3d76de7f49e6014fbe04499e24f_720w

6.5 耗时方法放在 Isolate

Isolate 是 Dart 里的线程,每个 Isolate 之间不共享内存,通过消息通信。

Dart 的代码运行在 Isolate 中,处于同一个 Isolate 的代码才能相互访问。

七、杂谈总结

经历了对 Flutter 长期的探索和项目验证,目前对 Flutter 有自己的一些杂谈总结:

7.1

Flutter 在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的 app 运行在2013年的旧 Android 手机上面依然十分流畅,iOS 的流畅程度也堪比原生。

7.2

对于 web 的应用来说,Flutter 还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的 WebView 以及编程成的 web 应用,还不适合大面积的投入到 web 的生产环境中。

7.3

关于和 Native 的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的 Flutter 节点设计在叶子节点上,即业务栈跳转到 Flutter 后尽量完成结束后再回到Native栈中。

7.4

基于“去桥”的原生编译方式,Flutter 在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成 Windows 应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善。

7.5

语法方面,Flutter 中的 dart 正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如 react 等,kotlin 中也有很多相似的地方,感觉 Flutter 团队正在努力地促进大前端时代的发展。


通过定制Flutter编译链工具,可以实现很多个性化的能力,甚至提供flutter tool本身不支持的功能。 同时借力Dart全栈,可以搭建完整的前后端开发工具。

Flutter上手

上手Flutter比较简单,走官方的接入规则,我们可以快速实现Flutter的接入和开发。但是这里面也会存在一些问题,比如:

  • 端源码的重度耦合,Flutter与Native源码出现依赖关系
  • Flutter编译链侵入,增加Native开发,调试成本
  • 同时运行多个 Flutter 实例或在部分视图中运行实例可能会导致未定义行为。
  • 在后台模式下使用 Flutter 尚处于开发阶段。
  • 不支持将一个 Flutter 库打包到另一个可分享的库内,或者将多个 Flutter 库打包到同一个应用中。

在大型工程中接入Flutter,势必需要考虑上述这几个问题。为了便于Flutter的接入,我们尝试统一接入标准,简化成本,我们的预期是这样的:

  1. 通过提供定制的SDK,实现Android,iOS的Flutter统一接入
  2. 通过提供开发接入的workflow,实现内部标准化,降低成本

奔跑吧workflow

通过workflow实现另一种flutter模块的创建,开发,编译,打包流程。

workflow-design

在安装使用workflow前需要确保您本地的开发环境已经搭建完毕,并配置了相关的环境变量。

export PATH=/*flutter directory*/flutter/bin:$PATH

同时建议配置dart sdk的环境变量,方便pub的使用

export PATH=/*flutter directory*/flutter/bin/cache/dart-sdk/bin:$PATH

接下来就可以安装命令行工具,方便部署和启动workflow平台: 通过pub或者flutter pub安装mgpcli

pub global activate mgpcli

安装dart全局命令行后,就可以开始使用它进行项目创建等后续流程了。

cli-workflow

1.创建flutter模块工程

# 创建模板工程
mgpcli create

2.启动workflow

# 进入新创建的工程目录内
cd your-project
# 启动workflow
mgpcli start

3.进入workflow

现在已经为您打开了一个浏览器窗口,可以移步至窗口进行:编译、Attach

workflow-preview

Dart闭环开发

在整个workflow项目中,我们采取了分层架构设计,具体来说包括以下WorkflowArchitecture

在这一节中我们主要介绍前后端部分的架构设计。

前端页面基于Flutter-Web技术栈。开发Flutter的dart页面,编译时利用dart2js,将Flutter页面转为js,整体来说是一个SPA的单页面应用。

flutter-web

后端部分与Flutter基本没有关系,属于纯Dart工程,基于Dart Server开发的后端服务。

有了前后端工程之后,我们做到了业务上的隔离。具体开发的时候,既可以隔离开发,也可以通过组织结构,实现同IDE开发前后端。

在实践中为了减少沟通成本和打通Dart全栈,参与开发的RD既要负责各自模块的前端业务,同时要考虑后端接口开发。实现了提个模块的的闭环开发。

workflow-structure

编译介绍

0x1 cli编译
命令行源码位于cli下,是一个Dart Cmdline工程,推荐通过VS Code进行开发。

0x2 后端编译
workflow分为前后端,后端源码位于server下,是一个Dart工程,推荐通过VS Code进行开发。

0x2 前端编译
前端部分是一个flutter工程,采用了flutter-web技术栈,可以通过VS Code, Android Studio开发。

为了顺利联调,建议先启动workflow的后端部分,启动可以通过

./start-server

后端目前不支持热重载,如果您修改了server代码,需要重启server,也就是重新执行./start-server。

启动web可以走IDE,也可以走命令行:

flutter run -d chrome

0x3 编码规范
Dart编码遵守Effective Dart准则。项目已开启lint检测,不符合准则的代码,开发期间有会提示warning。

Effective Dart完整规范,请查看 https://dart.dev/guides/language/effective-dart/style

集成发布意味着把前后端workflow整体编译打包,用户可以通过mgpcli进行使用。

集成发布

发布workflow
执行以下命令完成前后端资源的构建和内置:

./deploy -cli

本地验证cli
通过pub –source进行本地部署,执行以下命令:

./setup-cli

如果你本地有dart、flutter环境,但是不支持flutter-web,也可以使用预编译的workflow进行体验:

./setup-cli -dry-run

完成上述步骤后,你应当可以使用mgpcli命令了。例如启动workflow可以执行:

mgpcli start

脚手架cli

在前面各小结中频繁出现的cli/mgpcli是我们的命令行工具。作为Workflow启动和门户,非常重要。

面向用户下载安装的mplci是我们发布的后的工具。那么如何在本机进行脚手架开发和环境控制呢?

本机运行开发cli,我们定义为Debug 模式:

Debug 模式为Magpie开发小组日常开发环境,感兴趣的同学也可以参与Magpie团队一起开发,大家一起创造更好的Flutter的开发环境。

具体操作如下:

  • cli本地部署
    git clone git@igit.58corp.com:flutter-lab/magpie_workflow.git 
    pub global activate --source path /*本地全路径*/magpie_workflow 
    
  • 源码运行环境配置

    1、进入用户根目录下的.pub-cache/bin/mgpcli
    2、vi 打开mgpcli脚本文件我们可以看到dart “//.pub-cache/global_packages/mgpcli/bin/mgpcli.dart.snapshot.dart2” “$@”
    3、我们可以把打开文件中dart后面的路径指向我们clone下来的源码路径”**/magpie_workflow/cli/bin/mgpcli.dart”
    4、用vscode打开工程,定位到mgpcli.dart文件,右键选择运行。至此Magpie工程就可以断点Debug了s

总之,Flutter 确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。

「梦想一旦被付诸行动,就会变得神圣,如果觉得我的文章对您有用,请帮助本站成长」

赞(0) 打赏
一分也是爱

支付宝扫一扫打赏

微信扫一扫打赏

上一篇:

下一篇:

相关推荐

博客简介

本站CDN采用VmShell免费提供离中国大陆最近的香港CMI高速网络做支撑,ToToTel打造全球最快速的边沿网络支撑服务,具体详情请见 :https://vmshell.com/ 以及 https://tototel.com/,网站所有的文件和内容禁止大陆网站搬迁复制,谢谢,VPS营销投稿邮箱: admin@linuxxword.com,我们免费帮大家发布,不收取任何费用,请提供完整测试文稿!

精彩评论

友情链接

他们同样是一群网虫,却不是每天泡在网上游走在淘宝和网游之间、刷着本来就快要透支的信用卡。他们或许没有踏出国门一步,但同学却不局限在一国一校,而是遍及全球!申请交换友链

站点统计

  • 文章总数: 2334 篇
  • 草稿数目: 12 篇
  • 分类数目: 6 个
  • 独立页面: 0 个
  • 评论总数: 2 条
  • 链接总数: 0 个
  • 标签总数: 5988 个
  • 注册用户: 139 人
  • 访问总量: 8,661,862 次
  • 最近更新: 2024年4月27日