開發

重磅|庖丁解牛之——Flutter for Web

廣告
廣告

Flutter for Web

在2018年冬的Flutter 1.0倫敦發布會上,Flutter產品經理Tim Sneath通過一個滑動拼圖的例子介紹了如何讓Flutter運行在Web之上。這一當時代號HummingBird的項目后來被重命名為flutter_web,并最終合入了master分支。

Flutter Web想在單代碼庫的情況下,使Flutter應用擁有Web支持。這樣開發者使用Dart編寫的Flutter應用可以被部署到任意的Web服務器上,或嵌入到瀏覽器中。開發者可以使用Flutter的所有特性,也不需要特殊的瀏覽器插件支持。

就最新的Flutter1.9.x而言,Flutter Web還處于技術預覽版階段,離真正應用到生產環境中還是有一些距離的。

設計

那么Flutter Web是怎么做到這一切的呢?這就要從Flutter的原理說起。Flutter框架的設計如下所示:

其中,Flutter Framework是使用純Dart開發的。我們將其分為兩部分,渲染和邏輯。就渲染而言,其最終會表示為dart:ui中提供的TextBox,Picture,Image等實例對象,再通過native方法(實現dart調用C++)調用Skia,Text等C++庫,最終渲染在屏幕上,邏輯部分則被Dart Runtime執行。不難看出,要實現在Web上運行Flutter,要解決兩個問題。Dart如何運行在Web上以及dart:ui中的native方法如何通過標準Web的方式來實現。就前者而言,dart2js是一個已有的成熟框架,所以問題的重點就在于如何通過標準Web的方式去實現一個dart:ui庫。這也就是目前Flutter Web的設計原理:

在Flutter Web的設計之初,主要考慮了兩個方案用于Web支持:

  • HTML+CSS+Canvas
  • CSS Paint API

方案1具有最好的兼容性,它優先考慮HTML+CSS表達,當HTML+CSS無法表達圖片的時候,會使用Canvas來繪制。但2D Canvas在瀏覽器中是位圖表示,會造成像素化下的性能問題。

方案2是新的Web API, 屬于Houdini的組成部分。Houdini提供了一組可以直接訪問CSS對象模型的API,使得開發者可以去書寫代碼并被瀏覽器作為CSS加以解析,這樣在無需等待瀏覽器原生的支持下,創造了新的CSS特性。它的繪制并非由核心Javascript完成,而是類似Web Worker的機制。其繪制由顯示列表支持,而不是位圖。但目前CSS Paint API不支持文本,此外各家廠商對齊支持也并不統一。

鑒于此,目前Flutter Web使用的是基于方案1的實現。

環境準備

Flutter環境

flutter doctor -v[?] Flutter (Channel master, v1.10.6-pre.61, on Mac OS X 10.15 19A558d, locale en-CN)    ? Flutter version 1.10.6-pre.61 at /Users/kylewong/Codes/Flutter/alibaba-flutter/flutter    ? Framework revision 7bf9aea254 (4 hours ago), 2019-09-25 00:37:12 -0700    ? Engine revision 63949eb0fd    ? Dart version 2.6.0 (build 2.6.0-dev.0.0 69b5681546)...[?] Chrome - develop for the web    ? Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome[?] Android Studio (version 3.5)    ? Android Studio at /Applications/Android Studio.app/Contents    ? Flutter plugin version 39.0.3    ? Dart plugin version 191.8423    ? Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)

Web環境

在flutter的master分支上,開發者可以通過下方命令檢查當前是否打開了Web支持:

[email protected] web_dig % flutter devices3 connected devices:MHA AL00          ? GWY7N16A31002764                         ? android-arm64  ? Android 9 (API 28)Chrome            ? chrome                                   ? web-javascript ? Google Chrome 77.0.3865.90Server            ? web                                      ? web-javascript ? Flutter Tools

如果不能看到Chrome/Server這兩個設備,可以通過以下命令打開支持:

flutter config --enable-web

這個命令會將配置項保存到用戶Home目錄下的.flutter_settings中,一個典型的內容如下所示:

{  "enable-web": false,  "ios-signing-cert": "iPhone Developer: Kang Wang (xxx)"}

dart2js配置修改

以flutter自帶的gallery為例,默認的flutter web實現下,生成的js如下所示:

可以看到此js代碼可讀性很差(變量名,格式等),大小為2.2MB。這是因為flutter構建過程中開啟了dart2js命令的O4優化項所致。為了方便我們分析和調試,我們對此其進行如下修改:

O0將禁止很多優化,修改后的效果如下所示:

可以看到,大小增加了不少,但可讀性上好很多,除特殊說明外,本文將在O0優化項下展開。

原理剖析

Gallery上的表現對比

我們首先基于Flutter提供的Gallery項目,比較下其在Mobile和Web上的表現(此處使用Flutter Web默認優化級別):

Flutter Native vs Flutter Web:

可以看出,Flutter Web在完備性上還是比較不錯的,但依然有一些問題,比如本地圖片在Android設備上顯示正常,在iOS上卻無法正常顯示,網絡圖片則是正常的。

在Mobile/Web開發中,常見的元素包括圖片,文字,形狀,手勢等,接下來,我們逐一進行剖析。

圖片的實現

以如下代碼為例:

import 'package:flutter/material.dart';void main() => runApp(Image.asset('assets/1.png'));

其運行效果如下(左側為Native,右側為Web):

其Native與Web簡要原理對比如下所示:

在flutterwebsdk中最終調用html庫(dart-sdk自帶)繪制的代碼如下:

flutter_web_sdk/lib/_engine/engine/[email protected] drawImageRect(    ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) {  // TODO(het): Check if the src rect is the entire image, and if so just  // append the imgElement and set it's height and width.  print('KWLM04');  final HtmlImage htmlImage = image;  ctx.drawImageScaledFromSource(    htmlImage.imgElement,    src.left,    ...    dst.height,  );}

相對應地,通過 flutter build web--release--verbose生成的main.dart.js中部分代碼如下:

此部分對應上述bitmap_canvas.dart中的drawImageRectdrawImageRect$4: function(image, src, dst, paint) {    ...    P.print("KWLM04");    H.interceptedTypeCheck(image, "$isHtmlImage");    J.drawImageScaledFromSource$9$x(this.get$ctx(), image.imgElement, src.left, src.top, src.get$width(src), src.get$height(src), dst.left, dst.top, dst.get$width(dst), dst.get$height(dst));},??drawImageScaledFromSource$9$x: function(receiver, a0, a1, a2, a3, a4, a5, a6, a7, a8) {    return J.getInterceptor$x(receiver).drawImageScaledFromSource$9(receiver, a0, a1, a2, a3, a4, a5, a6, a7, a8);},??

最終調用到了CanvasRenderingContext2D.drawImage這一標準W3C的API。

文本的實現

以如下代碼為例:

import 'package:flutter/material.dart';void main() => runApp(Text('Hello Flutter!'));

其運行效果如下(左側為Native,右側為Web):

其Native與Web簡要原理對比如下所示:

在flutterwebsdk中最終調用html庫(dart-sdk自帶)構建和添加Element的代碼如下:

flutter_web_sdk/lib/_engine/engine/dom_canvas.dart@overridevoid drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {  print('KWLM18');  final html.Element paragraphElement =      _drawParagraphElement(paragraph, offset, transform: currentTransform);  currentElement.append(paragraphElement);}
flutter_web_sdk/lib/_engine/engine/engine_canvas.darthtml.Element _drawParagraphElement(  EngineParagraph paragraph,  ui.Offset offset, {  Matrix4 transform,}) {  print('KWLM25');  assert(paragraph._isLaidOut);  final html.Element paragraphElement = paragraph._paragraphElement.clone(true);  final html.CssStyleDeclaration paragraphStyle = paragraphElement.style;    ...  return paragraphElement;}

相對應地main.dart.js中部分代碼如下:

此部分對應上述dom_canvas.dart中的drawParagraphdrawParagraph$2: function(paragraph, offset) {   var paragraphElement;   H.interceptedTypeCheck(paragraph, "$isParagraph");   H.interceptedTypeCheck(offset, "$isOffset");   P.print("KWLM18");   paragraphElement = H._drawParagraphElement(paragraph, offset, this.get$currentTransform(this));   J.append$1$x(this.get$currentElement(), paragraphElement);},??_drawParagraphElement: function(paragraph, offset, transform) {  var paragraphElement, paragraphStyle, style, t1;  P.print("KWLM25");  paragraphElement = H.interceptedTypeCheck(J.clone$1$x(paragraph._paragraphElement, true), "$isElement0");  paragraphStyle = paragraphElement.style;  (paragraphStyle && C.CssStyleDeclaration_methods).set$position(paragraphStyle, "absolute");  ...  return paragraphElement;},??

從文本這個例子不難看出,對于可以通過HTML+CSS形式表達的元素,flutter web將其最終翻譯成Element+CSS Style形式動態生成類似靜態HTML+CSS描述的內容,最終完成內容的渲染。

形狀的實現

以如下代碼為例:

import 'package:flutter/material.dart';void main() => runApp(Container(decoration: BoxDecoration(color: Colors.red)));

其運行效果如下(左側為Native,右側為Web):

其Native與Web簡要原理對比如下所示:

在flutterwebsdk中最終調用html庫(dart-sdk自帶)構建Element(添加部分同文本)的代碼如下:

flutter_web_sdk/lib/_engine/engine/[email protected] drawRect(ui.Rect rect, ui.PaintData paint) {  print('KWLM47');  assert(paint.shader == null);  final html.Element rectangle = html.Element.tag('draw-rect');  ...  currentElement.append(rectangle);}

相對應地main.dart.js中部分代碼如下:

此部分對應上述dom_canvas.dart中的drawRectdrawRect$2: function(rect, paint) {    var rectangle, isStroke, t1, t2, t3, left, right, $top, bottom, effectiveTransform, translated, style, cssColor, _this = this;    H.interceptedTypeCheck(rect, "$isRect");    H.interceptedTypeCheck(paint, "$isPaintData");    P.print("KWLM47");    rectangle = W.Element_Element$tag("draw-rect");    ...    J.append$1$x(_this.get$currentElement(), rectangle);}??

對于本例中的形狀,也是通過HTML+CSS的方式實現的。

觸摸事件的實現

以如下代碼為例:

import 'package:flutter/material.dart';void main() => runApp(GestureDetector(child: Text('Click me!',style: TextStyle(fontSize: 50),), onTap: (){  print('KWLM called!');}));

其運行效果如下(左側為Native,右側為Web):

其Native與Web簡要原理對比如下所示:

此例中的PointerBinding由dartsdk.js提供,其提供了從Window獲取事件回調的機制,并最終調用到了WidgetsFlutterBinding(也是GestureBinding)的handlePointerDataPacket$1方法,后續的路由機制同Native情景下的Flutter部分。

優缺點

優點

從目前Flutter Web選取的技術路線來說,HTML+CSS+Canvas這種方式具有最好的兼容性,這樣開發者開發的Flutter代碼(不包括Plugin部分對于Native的擴展)將零成本地轉成標準Web展示,這一低成本擴展到Web平臺帶來的優勢還是很明顯的。

不足

盡管其優勢很明顯,也面臨一些不足的問題 a. 包大小過大的問題

目前dart2js本身并沒有針對小型程序做出優化,即使是本文中的手勢這么簡單的代碼,Flutter Web最終生成的大小也有560KB, 無法滿足要求。

但從理論上來說,通過對dart2js本身做出合理的優化(鑒于dart/flutter整個的開源設計),我們可以將Flutter Web依賴的基礎SDK全集嵌入應用中(或者按需下載的方式),將真正的業務代碼與SDK分離,也是有可能將其大小降低的。

b. 功能不完備的問題 比如在flutter_gallery的例子中Safari上圖標展示為方框的問題。

c. 性能的問題 當需要用到BitmapCanvas比較多的時候,Element對象直接的光柵化,會導致在一些諸如縮放等的場景下,面臨性能的問題。當然縮放的問題在移動設備的場景下也是有可能避免的。

小結

總體而言,Flutter Web具有優秀的設計。它基于dart:js和dart:html這些成熟的框架,通過將與Native相關的dart:ui庫重寫的方式,很好地解決了Flutter擴展到Web平臺上的問題。對于上層開發者而言,完全不用去做任何修改,即可產生一套符合Web標準的代碼,顯示和行為也同原始設計保持一致。雖然目前Flutter Web還不夠成熟,存在一些諸如包大小性能等問題,但基于Flutter和Flutter Web的良好分層設計,我們有理由相信隨著時間的推移和社區成熟,這些問題終將得到改善或解決。

我還沒有學會寫個人說明!

"加班文化"到底是如何流行起來的

上一篇

聯想+AMD=兩全其美

下一篇

你也可能喜歡

重磅|庖丁解牛之——Flutter for Web

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
双色球常规走势图 888棋牌游戏网站平台 体彩网 梦幻西游25级怎么赚钱攻略 河北十一选五走试图 河南快赢481手机版 天天棋牌首页 福建十一选五中奖规则 澳洲幸运10是哪里开奖 516棋牌游戏平台下载 山东十一选五走势图彩乐乐 qq分分彩人工免费计划 山东黄金股票行情