软件开发架构师

写给前端工程师的 Flutter 教程(上)-InfoQ

前端 41 2019-11-09 01:36

最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。最爱跨屏的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。
图啥?低成本地为用户带来更优秀的用户体验。目前来说 Flutter 可能是其中最优秀的一种方案了。

Flutter 是什么?

Flutter 是由原 Google Chrome 团队成员,利用 Chrome 2D 渲染引擎,然后精简 CSS 布局演变而来。

写给前端工程师的 Flutter 教程(上)-InfoQ-1

Flutter 架构

或者更详细的版本:

写给前端工程师的 Flutter 教程(上)-InfoQ-2

  • Flutter 在各个原生的平台中,使用自己的 C++ 的引擎渲染界面,没有使用 webview,也不像 RN、NativeScript 一样使用系统的组件。

  • 简单来说平台只是给 Flutter 提供一个画布。

  • 界面使用 Dart 语言开发,貌似唯一支持 JIT,和 AOT 模式的强类型语言。

  • 写法非常的现代,声明式,组件化,Composition > inheritance,响应式……就是现在前端流行的这一套 :smile:

  • 一套代码搞定所有平台。

Flutter 为什么快?Flutter 相比 RN 的优势在哪里?

从架构中实际上已经能看出 Flutter 为什么快,至少相比之前的当红炸子鸡 React Native 快的原因了。

  • Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作为渲染引擎。
  • Dart 语言可以 AOT 编译成 ARM Code,让布局以及业务代码运行的最快,而且 Dart 的 GC 针对 Flutter 频繁销毁创建 Widget 做了专门的优化。
  • CSS 的子集 Flex like 的布局方式,保留强大表现能力的同时,也保留了性能。
  • Flutter 业务书写的 Widget 在渲染之前 diff 转化成 Render Object,对,就像 React 中的 Virtual DOM,以此来确保开发体验和性能。

而相比 React Native:

  • RN 使用 JavaScript 来运行业务代码,然后 JS Bridge 的方式调用平台相关组件,性能比有损失,甚至平台不同 js 引擎都不一样。
  • RN 使用平台组件,行为一致性会有打折,或者说,开发者需要处理更多平台相关的问题。

而具体两者的性能测试,可以看这里,结论是 Flutter,在 CPU,FPS,内存稳定上均优于 ReactNative。

Dart 语言

在开始 Flutter 之前,我们需要先了解下 Dart 语言。Dart 是由 Google 开发,最初是想作为 JavaScript 替代语言,但是失败沉寂之后,作为 Flutter 独有开发语言又焕发了第二春。

实际上即使到了 2.0,Dart 语法和 JavaScriptFlutter 非常的相像。单线程,Event Loop……

写给前端工程师的 Flutter 教程(上)-InfoQ-3

Dart Event Loop 模型

当然作为一篇写给前端工程师的教程,我在这里只想写写 JavaScript 中暂时没有的,Dart 中更为省心,也更“甜”的东西。

  • 不会飘的 this
  • 强类型,当然前端现在有了 TypeScript :grimacing:
  • 强大方便的操作符号:
    • ?. 方便安全的 foo?.bar 取值,如果 foo 为 null,那么取值为 null
    • ?? condition ? expr1 : expr2 可以简写为 expr1 ?? expr2
    • = 和其他符号的组合: *=、~/=、&=、|= ……
    • 级联操作符 (Cascade notation …)
复制代码
// 想想这样省了多少变量声明
querySelect('#button')
. .text = "Confirm"
. .classes .add( 'important')
. .onClick .listen((e) => window.alert( 'Confirmed'))

甚至可以重写操作符

复制代码
class Vector {
final int x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// Operator == and hashCode not shown. For details, see note below.
// ···
}
void main () {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}

注:重写 ==,也需要重写 Object hashCodegetter

复制代码
class Person {
final String firstName, lastName;
Person( this.firstName, this.lastName);
// Override hashCode using strategy from Effective Java,
// Chapter 11.
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
// You should generally implement operator == if you
// override hashCode.
@override
bool operator ==( dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.firstName == firstName &&
person.lastName == lastName);
}
}
void main() {
var p1 = Person( 'Bob', 'Smith');
var p2 = Person( 'Bob', 'Smith');
var p3 = 'not a person';
assert(p1.hashCode == p2.hashCode);
assert(p1 == p2);
assert(p1 != p3);
}

这点在 diff 对象的时候尤其有用。

1. lsolate

Dart 运行在独立隔离的 iSolate 中就类似 JavaScript 一样,单线程事件驱动,但是 Dart 也开放了创建其他 isolate,充分利用 CPU 的多和能力。

复制代码
load Data() async {
// 通过 spawn 新建一个 isolate,并绑定静态方法
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 获取新 isolate 的监听 port
SendPort sendPort = await receivePort.first;
// 调用 sendReceive 自定义方法
List dataList = await send Receive(sendPort, 'https://hicc.me/posts');
print('dataList $dataList');
}
// isolate 的绑定方法
static data Loader(SendPort sendPort) async{
// 创建监听 port,并将 sendPort 传给外界用来调用
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
// 监听外界调用
await for (var msg in receivePort) {
String requestURL =msg [0];
SendPort callbackPort =msg [1];
Client client = Client();
Response response = await client.get(requestURL);
List dataList = json.decode(response.body);
// 回调返回值给调用者
callbackPort.send(dataList);
}
}
// 创建自己的监听 port,并且向新 isolate 发送消息
Future send Receive(SendPort sendPort, String url) {
ReceivePort receivePort = ReceivePort();
sendPort.send( [url, receivePort.sendPort]);
// 接收到返回值,返回给调用者
return receivePort.first;
}

当然 Flutter 中封装了 compute,可以方便的使用,譬如在其它 isolate 中解析大的 json。

2. Dart UI as Code

在这里单独提出来的意义在于,从 React 开始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 声明式组件写法越发流行,Web 前端使用 JSX 来让开发者更方便的书写,而 Flutter,SwiftUI 则直接从优化语言本身着手。

3. 函数类的命名参数

复制代码
void test({ @required int age, String name}) {
print(name);
print(age);
}
// 解决函数调用时候,参数不明确的问题
test(name: "hicc",age: 30)
// 这样对于组件的使用尤为方便
class MyApp extends StatelessWidget {
@override
Widget build( BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(),
floatingActionButton: FloatingActionButton()
);
}
}

大杀器:Collection If 和 Collection For

复制代码
// collection If
Widget build(BuildContext context) {
return Row(
children: [
IconButt on(icon: Ic on(Icons.menu)),
Expanded(child: title),
if (!isAndroid)
IconButt on(icon: Ic on(Icons.search)),
],
);
}
复制代码
// Collect For
var command = [
engineDartPath,
frontendServer,
for ( var root in fileSystemRoots) "--filesystem-root=$root",
for ( var entryPoint in entryPoints)
if (fileExists( "lib/$entryPoint.json")) "lib/$entryPoint",
mainPath
];

Flutter 怎么写

到这里终于到正题了,如果熟悉 Web 前端,熟悉 React 的话,你会对下面要讲的异常的熟悉。

写给前端工程师的 Flutter 教程(上)-InfoQ-4

UI=F(state)

Flutter App 的一切从 lib/main.dart 文件的 main 函数开始:

复制代码
import 'package:flutter/material.dart';
void main() => runApp( MyApp());
class MyApp extends StatelessWidget {
@override
Widget build( BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text( 'Welcome to Flutter'),
),
body: Center(
child: Text( 'Hello World'),
),
),
);
}
}

Dart 类 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限于

  • 结构性元素,menu,button 等
  • 样式类元素,font,color 等
  • 布局类元素,padding,margin 等
  • 导航
  • 手势

Widget 是 Dart 中特殊的类,通过实例化 (Dart 中 new 是可选的) 相互嵌套,你的这个 App 就是形如下图的一颗组件树 (Dart 入口函数的概念,main.dart -> main())。

写给前端工程师的 Flutter 教程(上)-InfoQ-5

Flutter Widget Tree
文章评论