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

閒魚技術發表於2022-12-07

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 for Web

其中,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 for 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支援:

kylewong@KyleWongdeMacBook-Pro 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如下所示:

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

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

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

O0將禁止很多最佳化,修改後的效果如下所示:

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

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

原理剖析

Gallery上的表現對比

我們首先基於Flutter提供的Gallery專案,比較下其在Mobile和Web上的表現(此處使用Flutter Web預設最佳化級別):

Flutter Native vs Flutter Web:

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

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

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

圖片的實現

以如下程式碼為例:

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

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

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

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

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

在flutterwebsdk中最終呼叫html庫(dart-sdk自帶)繪製的程式碼如下:

flutter_web_sdk/lib/_engine/engine/bitmap_canvas.dart@overridevoid 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。

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

文字的實現

以如下程式碼為例:

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

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

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

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

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

在flutterwebsdk中最終呼叫html庫(dart-sdk自帶)構建和新增Element的程式碼如下:

  1. flutter_web_sdk/lib/_engine/engine/dom_canvas.dart

  2. @override

  3. void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {

  4.  print('KWLM18');

  5.  final html.Element paragraphElement =

  6.      _drawParagraphElement(paragraph, offset, transform: currentTransform);

  7.  currentElement.append(paragraphElement);

  8. }


  9. flutter_web_sdk/lib/_engine/engine/engine_canvas.dart

  10. html.Element _drawParagraphElement(

  11.  EngineParagraph paragraph,

  12.  ui.Offset offset, {

  13.  Matrix4 transform,

  14. }) {

  15.  print('KWLM25');

  16.  assert(paragraph._isLaidOut);

  17.  final html.Element paragraphElement = paragraph._paragraphElement.clone(true);

  18.  final html.CssStyleDeclaration paragraphStyle = paragraphElement.style;

  19.    ...

  20.  return paragraphElement;

  21. }

相對應地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):

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

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

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

在flutterwebsdk中最終呼叫html庫(dart-sdk自帶)構建Element(新增部分同文字)的程式碼如下:

flutter_web_sdk/lib/_engine/engine/dom_canvas.dart@overridevoid 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):

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

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

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

重磅|庖丁解牛之——Flutter for 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的良好分層設計,我們有理由相信隨著時間的推移和社群成熟,這些問題終將得到改善或解決。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900359/viewspace-2658858/,如需轉載,請註明出處,否則將追究法律責任。

相關文章