作者|王威威
编辑|王文婧
本文将从环境配置、项目分析、构建与开发过程等方面详细介绍Flutter for Web。
前 言
在 9.11 上海举办的谷歌开发者大会(GDD)上,谷歌 Flutter 团队宣布 Flutter 1.9 版本正式发布了,这是迄今为止 Flutter 最大的一次版本更新,除了超过 1500 的 PR 和支持直接调用 C 的 Dart 新版本以外,我更关心的一个关于 Flutter For Web 的重要更新是:开发团队已成功将此前独立的 Flutter for Web 集成到 Flutter 的主仓库中。
在此之前,我们先了解下什么是 Flutter For Web。Flutter 团队自 1.5 版本发布了 Flutter For Web 的预览版本,对此官方的解释是:Flutter For Web 支持是 Flutter 的代码兼容实现,使用基于标准的 Web 技术呈现:HTML、CSS 和 JavaScript。通过 Web 支持,您可以将使用 Dart 编写的现有 Flutter 代码编译为可嵌入浏览器并部署到任何 Web 服务器的客户端体验。您可以使用 Flutter 的所有功能,而不需要浏览器插件。
区别于 Flutter For Mobile,Flutter For Web 的技术框架是这样的:
从上图可以看到,在 Framework 层,Flutter 是基本一样的,只是在应用层,Flutter For Web 是依托于 Dart2JS 转换成了浏览器可以识别的元素,通过结合使用 DOM、Canvas 和 CSS 来呈现给用户,目的是最终向用户提供高质量、高性能的用户体验。而对于开发者而言,Dart 优化的 JavaScript 编译器将 Flutter 核心和框架与您的应用程序一起编译成一个可以部署到任何 Web 服务器的简化源文件,这个源文件包含 Html 文件、JS 文件以及一些资源类文件。而所有的 dart 文件最终将会被编译到一个 main.dart.js 的文件中。
在 1.5 刚发布的时候,Flutter For Web 的支持在单独的 git 中的,地址是:https://github.com/flutter/flutter_web。
而如果需要开发 web 项目的话,你需要单独引入 web 项目支持,在 yaml 的配置中如下所示:
这样会有一个什么样的问题呢,你的 Flutter 代码是基于 Flutter 主库来编写的,引入的都是 Flutter 开头的库,如:
import 'package:flutter/material.dart';
而在这里你都需要引入 Flutter_web 开头的库,如:
import 'package:flutter_web/material.dart';
这样会导致你写的代码其实是无法在 web 上直接运行的,如果需要运行的话,你就必须要再复制出来改动。
而在刚发布的 1.9 Release 版本中,Flutter For Web 合入了 Flutter 主工程,原来的 flutter_web 工程已经废弃,而且从预览版变成了技术预览版,这样的好处在于无需单独引入 web 库了,当然,谷歌特别提示了,技术预览版还没有达到 alpha 版本,不建议在生产环境使用,而如果想要在 Flutter 上面提 issue 的话,建议在标题里带上 web 标签。
新版本的 Flutter For Web 项目和 Flutter 项目已经基本无差。
而作为对 Flutter For Web 这个方向极为看好的我们,率先探索了 Flutter For Web 的能力和可行性,在趟了一些坑后,我们正式上线了用 Flutter For Web 开发的几个需求,接下来我们从开发样一个正式需求的方向来了解下 Flutter For Web。
需求分析
对于一个 Preview 版本的框架,其支持的元素和功能可能并不是特别完善,这可能导致我们需求的部分功能无法实现,所以我们需要分析下当前 Flutter For Web 的功能或者组件是否可以支持我们的需求,我们首先分析了下自己的需求,我们的需求并不是一些简单的展示页面,是一个包含图片、列表、文字、富文本、输入框、单选框、复选框、按钮、loading 框、网络请求、页面跳转、跟客户端 jsbride 交互等各种操作并且包含六个页面的一个完整需求(具体功能可见 Now 直播客户端的注销账户页面)。这样的需求基本上是客户端上 web 页面的一个完整需求。
环境配置
这里已经不再推荐以前老式的 1.5 的 flutter_web 项目,而是直接采用最新的 flutter 主工程来创建。首先你需要运行“flutter upgrade”命令更新 Flutter 版本到 1.9 以上,我这里用的是 master 版本,master 已经更新到 1.10 了,截图下我这里的配置:
然后运行“flutter config --enable-web”命令启用web支持,然后你可以运行“flutter devices”命令,如果出现一个“chrome”的device,就说明你环境已经设置好了。
根据官方说明,如果没有,你可以使用 flutter doctor 检测下你的配置,如果提示“CHR- OME_EXECUTABLE not set”的话,你需要在环境变量里配置下 CHROME_EXECUTABLE,然后就可以了。
之后你可以在当前项目中运行“ flutter run -d chrome”命令来启动 web 应用,如果你当前没有其他 device 的话,可以不需要指定 chrome。这个命令的好处是,如果你当前不是 web 应用,flutter 会提示你创建:
这时候你可以再运行“flutter create .”命令来添加 web 环境:
这个时候就已经可以了,然后你可以运行 run 命令,这个时候会自动编译成 web 应用,然后启动一个 chrome:
可以看到已经启动成功了,并且提示你可以使用“R”来支持 hot restart,这里是在命令行界面按 R,不是在编辑区域。
控制台里面有这句输出 :
???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ????Warning: Flutter's support for building web applications is highly experimental. For more information see https://github.com/flutter/flutter/issues/34082.???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ???? ????
感兴趣的可以打开这个链接看下,我就发下下面这个提示,请大家注意一下。
请注意这段提示:
Flutter for web remains experimental at least until this is resolved, and in all likelihood longer.
Flutter For Web Project 分析
OK,接下来我们来分析下这里的 Flutter 项目。
我们首先分析下 Flutter Web Project,看下面的截图:
重点分析下项目中的三个文件夹:
.vscode:这一个文件夹是 vscode 创建项目自动生成的,为什么要提下呢,因为这里面有个文件叫 launch.json,这个文件后面分析下。
lib:这就是写 dart 代码的主要区域,这里面你可以像移动端的 Flutter 项目一样写 dart 代码。我们的项目目录由 pages、utils、widget 等组成,并且下面有一个自动生成的 main.dart 文件。
web:这就是 web 的主目录了,这个目录主要是包含 index.html 和一个 main.dart 文件。但是如果你需要有资源文件、js 文件等 web 所需资源,比如我们的 assets 文件夹用来放图片资源。
这里再重点看下项目中的这五个文件,
launch.json: 为什么要提下自动生成的文件呢,我们先看下这个文件中的内容,
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Dart",
"program": "lib/main.dart",
"request": "launch",
"type": "dart"
}
]
}
这个文件有指出,需要调用的主 dart 是 lib/main.dart,这里是可以配置的,如果错误的设置成 bin/main.dart,会报错。
pubspec.yaml:这个文件主要是 flutter 的项目配置文件,对比 flutter for mobile 项目,还不支持 plugin。项目依赖还是 flutter_web。
web 下的 main.dart:
import 'package:flutter_web_ui/ui.dart' as ui;
import 'package:logout_account/main.dart' as app;
main() async {
await ui.webOnlyInitializePlatform();
app.main();
}
这个文件是整个 web app 启动的入口,这里会调用平台初始化任务,然后调用整个 app 的 mian 来调用 lib 下的 main.dart 中的 main 方法来启动页面。
lib 下的 main.dart:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '注销账户',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: '注销账户'),
);
}
}
这里就和 flutter for mobile 中的 main.dart 是一样的,主要是 main 方法 run app,开始启动 MaterialApp,加载 widget。
web 下的 index.html:
注销账户
这里就更简单了,这里就是一个简单的 html 文件,这个 html 引入一个叫 main.dart.js 的文件,你会发现新建的项目中是没有这个 js 文件的,那么这个文件是从哪里来的呢?这里其实是项目自动生成的一个 js 文件。我们后面会分析下这里。我们不建议在这个 html 文件中创建任何 element、css 样式等,但是你可以在这里面加载 js 文件,如果你需要在 dart 中调用 js 文件的方法,请记得加载顺序。
通过上面的分析,你应该知道,这里的开发其实比较简单,如果你完全不需要任何 js 的引入,那么你直接可以在 lib 下面开发你的功能就可以,如果你需要引入一些第三方 js 库或者实现其他的一些 js 方法,那么你可以在 web 目录下面开发,在 index.html 中引入,如果你想自己在 index.html 中嵌入 element、css 样式的话,你会发现并不行,但是如果你想嵌入 html 页面的话,后面我们会教你如何去做。
如何构建
开发完成后我们需要将生产的文件发布,那需要发布哪些文件呢?我们来对 Flutter For Web 的构建和产物来做下分析。上面的调试在启动后,其实是会在.dart_tool 文件夹下生成一个 build 文件夹,这个文件夹中都是生成的中间产物。
而由于需要支持 hot reload,所以在 debug 模式下会生成多个 JS 文件,这点我们可以在一个页面加载的资源中看到:
可以看到,这个页面加载了很多 js、html 等文件。
那我们如果需要发布的时候,是不是也需要这么多文件呢,如果这样的话,将会对发布造成很大负担,因为要发很多文件。根据官方的指引,我们是用“flutter build web”命令生成发布产物看下。
执行这个命令后,会在项目的 build 目录下生成一个 web 文件夹,而不是在 dart_tool 文件夹下生成的。这个文件夹下生成的内容如下:
这个 web 目录下有一个文件夹、三个文件,我们来分析下这下面的几个文件夹和文件。
assets: 资源文件夹,打开后发现这里存的是 web 目录下的资源文件,那说明这个是必须要发布的。这里面有自动帮你生成了 AssetManifest.json 和 FontManifest.json 两个文件,这两个文件在线上访问是必须会加载的,目前没有发现有屏蔽方法,比较建议加上,否则会出现 404 错误,如果在部分服务器上可能会引发跳转,可能会导致出现异常。
index.html:这里和 web 目录下的 index.html 一样,是整个 web app 的访问入口,必须要发布。
main.dart.js:
这是通过 dart2js 将使用 dart 写的 flutter 代码生成了 js 便于加载,你所写的 flutter ui 样式、dart 业务逻辑等都将转成 js 输出到这个文件中,然后被 index.html 加载,最后渲染呈现在 webview 上。所以这里就是为什么开始时候说不需要在 index.html 中写 css 样式,因为在这里是不会被识别的。main.dart.js.map:这是一个 js 的 map 文件,不需要发布,主要用于出错时候方便定位使用。
之后我们将这里的所有文件复制到我们的 web 服务器下就可以了。
Package & Plugin
Flutter 团队一直声称 Flutter For Web 暂时不支持插件系统,这个从 1.5 到 1.9 都没有支持。但是这个只是说不支持特定平台的 Plugin 调用,并不意味着你无法使用 yaml 配置一些纯 dart 的 package,所以,你还是可以在 yaml 里面引入外部的 package 使用的,如 fish-redux 等都是可以支持的。最近 dio 也支持了 web 版本,是可以直接引用使用的。
如何开发
说了这么多,我们聊一聊如何在开发 Flutter For Web 中的注意事项。Flutter For Web 开发本质上和 Flutter For Mobile 开发无任何区别,所有的逻辑都可以用 Dart 实现,你可以使用任何控件,甚至可以使用各种状态管理等,这里 Flutter 官方都有比较详细的教程,我们这里就不需要详细说明了,可以参考官网教程。但是目前由于生态不太完善的原因,我们又无可避免的需要引用到第三方,或者一些特殊的 Web 相关逻辑,那么 dart 和 js 的交互就必不可少了。我们期待最优秀的交互方式肯定还是 Plugin 的方式,但是由于不支持,所以如果现在有需要的话 还是需要用代码和 JS 交互。
HTML
其实 Flutter For Web 最终还是编译成了 html 和 js 来运行,那么我们就需要一些 html 相关的操作,如 document、ua、cookie 等等。而在 Flutter 里,是有专门的“dart:html”库的,你可以 import 后使用。
比如 navication 的实现:
import 'dart:html';
/**
* @description: 跳转某个 weburl
* @param {url}
*/
navigation(String url){
document.window.location.href = url;
}
比如获取 cookie 的实现:
import 'dart:html';
Map _readCookie(String cookies) {
var cookies = document.cookie;
var cookie = Map();
cookies.split(';').forEach((e) {
var k = e.indexOf('=');
if (k > 0) {
cookie[Uri.decodeComponent(e.substring(0, k)).trim()] =
Uri.decodeComponent(e.substring(k + 1)).trim();
}
});
return cookie;
}
JavaScript
Flutter For Web 的目的其实是用完全 dart 来开发 web 应用,但是目前市场很多第三方 web 插件,如统计等,还是没有 dart 版本的,那么我们就需要用 datr 和 js 互相调用的方式。而 Flutter 也提供了“dart:js”库可以导入后和 JS 进行交互。
JS 的交互里面有个很重要的概念是 context,我们需要拿到这个 context 上下文来和 js 交互。可以给 js 方法传参,甚至还可以将 js 的参数带回等等。比如以下代码:
import 'dart:js' as js;
/**
* @description: 从 dart 里面 直接调用 js 方法
* @param {method} 方法名
* @param {[List args]} 可选参数
* @return:
*/
calljsmethod(String method,[List args]){
js.context.callMethod(method,args);
}
/**
* @description: 从 dart 里面 直接调用 js 方法 并且传递一个 callback 过去 以便于回调
* @param {method} 方法名
* @param {callback} 回调函数
* @param {[List args]} 可选参数
* @return:
*/
calljswithcallback(String method,Function callback,[List args]){
js.context.callMethod(method,[js.allowInterop(callback),args]);
}
/**
* @description: 将 dart 方法设置成 js 方法 以供调用
* @param {functioname} 设置的函数名称 js 那边调用
* @param {callback} 设置的函数
* @return:
*/
setjsmethod(String functioname,Function callback){
print("setjsmethod ${functioname}");
js.context[functioname] = callback;
}
网络相关
网络请求从来都是开发一个 App 的重点功能,而 Flutter 开发中用的最广泛的 dio 插件,目前在最新版本是已经可以支持 web 的。
我们可以直接用 dio 进行网络请求,也可以通过“package:http/http.dart”库来进行网络请求。
doRequest(String url, Function success, {body, Function error}) {
if (!isEmpty(body)) {
url += "?data=${Uri.encodeFull(body)}";
}
print("url is $url");
http.get(url).then((resp) {
print('Response status: ${resp.statusCode} Response body: ${resp.body}');
String bodyString = resp.body;
Map data = JsonDecoder().convert(bodyString);
print('data is $data');
if (resp.statusCode == 200) {
success(resp.statusCode, data);
} else {
if (error != null) {
error(resp.statusCode, resp.body);
} else {
print('error is null not callback');
}
}
}).catchError(( ) {
print("catch some error ${ }");
if (error != null) {
error(-10001, "");
}
}).timeout((Duration(seconds: 15)));
}
这里我没有用 Completer 来实现,而是直接用的比较简单的回调,也可以封装一下。
性 能
性能方面才是衡量一个技术的最重要的指标,当前阶段的 Flutter For Web 还只是技术预览版本,官方也承认性能是有问题的,Flutter For Web 目前也是单页面应用,但是浏览器还不支持其而其渲染方式采用的是 Canvas 加原生 Dom 的方式来渲染的,这其实就会带来一些性能问题,比如 main.dart.js 轻松过 1M,导致首次加载时长可能过慢,比如 canvas 绘制可能会导致性能问题。
我们以下面这一项目为例:
https://github.com/arcticfox1919/flutter_new_retail
这个项目本来是一个 Flutter For Mobile 的电商项目,原作者也未对 web 进行任何兼容。但是我们可以直接编译成 web,并且跑起来,这也侧面说明了 Flutter 的强大之处,一份代码多端运行。
由于这个项目页面算是比较复杂,并且有带图的列表,我们用这个项目来测试下 Flutter 列表的性能,有兴趣的也可以自己试试。
这是 PC 端的数据,PC 端首次加载的速度略慢,但是在接受范围,而手机端就非常慢了,会有超过 2S 的延迟,但是这个不是最关键的,最关键的是根据我们的测试,手机端比如米 9 这种旗舰机,当这个列表滑动的时候帧率也只有十几,这就非常卡了,导致体验非常糟糕,所以这里还有待官方后面对性能进行优化。
问 题
最大的问题当然是 Flutter For Web 至今未 Release,而且官方团队并没有任何 Release 计划和时间点,这就导致 Flutter For Web 的未来充满了不可确定性。其次就是性能问题,目前 Flutter For Web 这种形式确实会带来不少的性能问题,但是 1.9 版本已经比之前的版本性能好很多了。我们期待官方的最终优化。最后就是生态问题,Flutter For Web 方面目前的生态还不完整,希望官方能尽快推出 Release 版本,然后希望社区可以完善下整个生态。
后 记
一份代码多端运行,在 Flutter 这里我们真正看到了可能,终于看到大前端技术的曙光了。
作者介绍
王威威,腾讯高级工程师,多年移动端开发经验,目前技术栈在大前端,对新技术很感兴趣,对 Flutter 相关技术栈以及 Web 方向有较多研究。
今日推荐
使用Flutter一年后,这是我得到的经验
2019 年终总结,245+篇,已分类整理
如果你想要跟大家分享你的文章
欢迎投稿