软件开发架构师

Flutter For Web:人人都是大前端开发-InfoQ

前端 6 2019-11-09 01:34

Flutter For Web 已经发布半年多时间,跑在 Flutter 实践道路上的腾讯企鹅辅导团队是如何应用的?

今年 9 月,作为腾讯 Flutter 实践团队之一的我们,有幸参与了 GDD 大会上 Flutter 应用视频的录制,感受到国内众多开发者对 Flutter 的热情。两天时间,讲道理,其实没有太多的干货,但收获还是满满的。有那么点空闲的时间,也关注了一下其他的同学,大家讨论的重心还是第一天主会场的内容。我想吸引大家的一直是 Flutter 新版本 stable1.9 的重磅发布。

新版本一个重要功能就是 Flutter For Web 仓库合入 Flutter master 主仓库,意味着我们可以真正地使用一套代码、一套资源部署大前端。辅导团队经过一段时间的准备,使用 Flutter 开发的 Web 页面也即将发布,希望和大家分享下实践过程和踩坑实例,欢迎一起交流探讨。

一、发布准备

我们对辅导 App 中上课页学习简报页面进行改造,支持浏览器中打开。

iOS 中打开 Flutter Web 页面

浏览器中打开 Flutter Web 页面

网络 cgi 接口现网暂时没有配置,先使用假数据展示。开发过程中,我们涉及到 Flutter 页面转换、网络请求、与 Native 交互、cookie 等,最终的页面发布我们还需要在发布系统中配置。有关基础开发及发布的知识,大家可以关注互动视频大神 weiwei 的一篇分享:《如何使用 Flutter For Web 开发一个需求并上线》,文章介绍的很清楚,最终发布上线需要的产物是:

  • index.html:h5 访问入口;

  • main.dart.js:dart 代码转化成 js 后的产物;

  • assets:静态资源,包括图片、字体,以及我们定制的 js 文件。

Flutter For Web:人人都是大前端开发-InfoQ-1

二、页面改造

Flutter For Web 谷歌官方一直建议暂时不要在生产环境使用。但如果不尝试,我们永远不知道这里会藏有什么样的秘密,什么样的场景适合。对现有 Flutter 改造,增加对浏览器的支持,突破而不失稳重。

1. 增加 Web 支持

我们原先的版本使用 Flutter 1.5.4,增加 Web 支持,我们需要切换到最新的 master 分支,改造运行。

Flutter For Web:人人都是大前端开发-InfoQ-2

如果我们只是新建一个新的项目,只需切换到最新的 master 分支,创建一个新的项目即可。创建完成之后,根目录下会多出一个 web 文件夹,里面只有一个 web 入口文件 index.html。

Flutter For Web:人人都是大前端开发-InfoQ-3

2. 修改 dart:io 库

原有项目中如果直接运行 flutter run -d chrome,会发现控制器中 import 报错,原因是 dart: io 库不支持 web。io 库是 Flutter 中非常常用的库,主要是平台相关的一些 api,改造的第二步我们就需要屏蔽 dart:io 的引入。

Flutter For Web 最终运行的是在浏览器中的 js 代码,Flutter For Phone 使用,Platform 引擎与 Native 通信。js 在平台上是通过其他的系统支持(iOS 中的 JavaScriptCore) 与 Native 通信,二者是完全不同的方式,所以 dart: io 无法继续支持。

既然 dart: io 不支持 web,那我们仍然想使用原先的 Flutter 业务 UI 代码,该 如何实现(上文我们说过,我们想使用同一套代码、同一套资源整合大前端)?我们使用不同平台下支持的能力库区分。

复制代码
import 'main_web.dart' if (dart. library.io) "main_io.dart"; //dart.library.io

Flutter For Web:人人都是大前端开发-InfoQ-4

UI 侧我们仍然使用同一套代码,逻辑侧通过 library 库分区 native 还是 web,逻辑侧对应不同的平台实现。这几天,我看到一个新的区分方法,简单而且实用:web 侧我们可以判断 0 和 0.0 是否是一个对象。

Flutter For Web:人人都是大前端开发-InfoQ-5

区分完不同平台,我们还需要辨别不同设备,这里我建议搭建 app 初始化的时候配置完成,后续方便实用。

复制代码
/ /init
Config.inWeb = identical(0, 0.0);
/ // // // // // // // // // // // // // // // // // //
enum K12Platform {iPhone, iPad, android}
class Config {
static bool inWeb = false;
static K12Platform k12platform = K12Platform.iPhone;
static bool inProduction = bool.fromEnvironment( "dart.vm.product");
}

3. 网络请求

原有 Flutter 项目我们通过 MJFlutter 调用 Native 的网络接口,实现数据请求。辅导 web 侧是通过 CGI 请求获得后台数据,现在 web 需要另一种方式,给大家推荐几种方式:

a. package:http/http.dart

pub 链接: https://pub.dev/packages/http#-readme-tab-

复制代码
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;
main( List< String> arguments) async {
// This example uses the Google Books API to search for books about http.
// https://developers.google.com/books/docs/overview
var url = "https://www.googleapis.com/books/v1/volumes?q={http}";
// Await the http get response, then decode the json-formatted responce.
var response = await http. get(url);
if (response.statusCode == 200) {
var jsonResponse = convert.jsonDecode(response.body);
var itemCount = jsonResponse[ 'totalItems'];
print( "Number of books about http: $itemCount.");
} else {
print( "Request failed with status: ${response.statusCode}.");
}
}

b. package:http/html.dart

复制代码
import 'dart:html' as html;
var req = html.HttpRequest.getString( "https://www.googleapis.com/books/v1/volumes?q={http}");

c. package:dio/dio.dart

pub 链接: https://pub.dev/packages/dio#-installing-tab-

dio 这个网络库,大家之前可能用过,需要注意一点,3.0 之前的版本 Flutter For Web 不支持 (dart:io)。3.0 之后的版本,这个库经过官方大改造,现已支持 Flutter For Web 开发。

我推荐的是 dio 这个库,使用起来会比较方便,参数简单好理解。

复制代码
var url = 'https://fudao.qq.com/xxx';
BaseOptions options = BaseOptions(
method: 'get',
baseUrl: url,
connectTimeout: 5000,
receiveTimeout: 100000,
contentType: 'json',
responseType: ResponseType.plain,
headers: {
'host' : "fudao.qq.com", // 一般就是 webview 的 url 的 host
'cookieHeader': getWebCookie(),
'accept': "*",
'referer': "https://fudao.qq.com/"
}
);
Dio dio = Dio(options);
try {
Response response = await dio. get(url);
if (callback != null) {
callback(response.statusCode, response.statusMessage, response.data);
}
} on DioError catch(e) {
if(e.response != null) {
print(e.response.data);
print(e.response.headers);
print(e.response.request);
} else{
print(e.request);
print(e.message);
}
}

有了基本的网路请求,配置完 CGI,接下来就是界面的展示。但是你会发现,所有的业务请求都没有回包,无法得到数据。如果作为移动端开发,没有经常接触到 http 请求,一下子发蒙,我觉得可以理解。这个是 Web 开发中经常遇到的 跨域问题。前端开发们这里可以举手了,这个问题我会。简单的处理方式就是开发过程中挂上一个代理,具体就不详细展开了。

网路问题解决,进入发布调试,上文我们介绍过 Flutter For Web 的最终产物是三个文件。如何发布,前端同学又可以举手了。有一个简单的方式:github pages(配置方法: https://pages.github.com/)。

三、JavaScript 扩展

与使用 Flutter 实现的页面不同,Flutter For Web 的页面使用 Http 请求获取后台数据。Http 请求中很重要的一点是 header 中的 cookie,这里就需要通过 dart 与 js 之间的交互,还是要称赞下 Flutter 团队的实力。

官方说明: https://dart.dev/web/js-interop

pub 地址: https://pub.dev/packages/js

Flutter For Web:人人都是大前端开发-InfoQ-6

新建 native_api.js 文件

Flutter For Web:人人都是大前端开发-InfoQ-7

dart 封装 js 方法

Flutter For Web:人人都是大前端开发-InfoQ-8

index 入口文件中引入 js 文件

Flutter For Web:人人都是大前端开发-InfoQ-9

添加完 js 支持后,dart 侧调用 getStringFromJS 方法,并打印结果。

通过 js.dart 这个库,可以实现 web 侧的主要功能。例如 cookie、localStorage。

Flutter For Web:人人都是大前端开发-InfoQ-10

native_api.js 中声明基本方法

Flutter For Web:人人都是大前端开发-InfoQ-11

dart 封装 js 方法

处理完 Web 侧的基本能力后,需要部署 dart 转成 js 后,与 native 交互的能力。我们知道,目前 app 中 native 与 web 交互的方式主要通过 jsBridge。Native 侧我们已经有了成熟的体系主动调用和接收调用 web 的能力,这部分我们可以不需要修改,减少开发工作量。在 Flutter 侧我们需要添加交互支持,也就是需要在 native_api.js 中添加与 native 交互的能力。

JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME,你可以根据自己的业务能力,选择合适的方式。相较于 JavaScript 调用 Native,Native 调用 JavaScript 比较简单,不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。

例如 iOS 中 JavaScript 调用 Native,设置 webView 的 title:

复制代码
function callNativeFunction() {
var url = 'jsbridge://edu/setCenterTitle?p=%7B%22text%22%3A%22%E9%B1%BC%E9%A5%BC%22%7D#2?title=aaa&desc=bbb&link=http%3A%2F%2Fwww.baidu.com';
var jsbridgeNode = document.createElement( 'iframe');
var removeTimeStamp;
jsbridgeNode.style.cssText = 'display:none;width:0px;height:0px;';
jsbridgeNode.onerror = function(e) {
// 在 android 4.0-4.3 中,script 节点的 src 赋值成 jsbridge://ui/showDialog 的形式会报错
e.stopPropagation();
}
/*
ios 必须先赋值, 然后 append, 否者连续的 api 调用会间隔着失败
也就是 api1(); api2(); api3(); api4(); 的连续调用,
只有 api1 和 api3 会真正调用到客户端
*/
jsbridgeNode.src = url;
var root = document.body || document.documentElement;
root.appendChild && root.appendChild(jsbridgeNode);
setTimeout( function() {
jsbridgeNode &&
jsbridgeNode.parentNode &&
jsbridgeNode.parentNode.removeChild(jsbridgeNode);
}, 500);
}

四、展望

整体来说,Flutter For Web 达到成熟还有一段时间。在这段时间里,尽早布局,未免不是一件好事。Flutter For Web 现阶段的使用场景并不多,不如直接使用 Web 开发便捷、稳定。但我们可以通过 Flutter,拉近与大前端的距离,感受大前端的魅力,做一个真正的大前端开发。

腾讯企鹅辅导团队会继续实践,不管是 Flutter 实现的页面,还是 Flutter 转 Web 的页面。Flutter For Web 一个复杂的场景,我们会尝试降级及热更新能力,与 Web 同学通力合作,封装 App 和 Web 的基础 API、集成 CI 等,欢迎与我们一起交流。

Flutter For Web:人人都是大前端开发-InfoQ-12

技术服务于业务,业务推动技术,二者并行,提升用户体验,Flutter 道路上期待大家的反馈。

专家介绍:
涂金林,来自江苏苏州。2014 年和 2017 年分别获得东南大学本科和硕士学位。2017 年加入腾讯在线教育部,主要从事移动客户端开发。入职以来,参与腾讯企鹅辅导、腾讯课堂 iOS 和 Android 开发,腾讯企鹅辅导 iOS 负责人。目前主要担任腾讯在线教育 Flutter 项目技术负责人。

文章评论