Flutter For Web 編譯的兩種方案

語言: CN / TW / HK

前言

要問現在最火的移動端的框架是什麼,每個人心中自有自己的答案。不過就筆者人而言,前端開發所做的更多是在顯示卡上繪製每一個畫素的藝術。從這一出發點來看,Flutter 基於瀏覽器上的 DOM 樹、安卓的 View、IOS 的 UIVeiw,從底層的自建渲染引擎來構建我們的應用 UI,並提供相關介面。目前 Flutter 關注度還是比較高的,Flutter 的熱度已經⽼牌跨平臺框架 React Native。不過吹捧了那麼多,可能就會有小夥伴們要問了,Flutter 到底是個什麼東西。接下來我們就一起來認識它。

Flutter 原理簡介

Flutter 是由 Google 推出的開源的高效能跨平臺框架,一個 2D 渲染引擎。在 Flutter中,Widget 是 Flutter 使用者介面的基本構成單元,可以說一切皆 Widget。下面來看下 Flutter 框架的整體結構組成。

Flutter 框架的設計如下所示:

​ Flutter 框架是一個分層的結構,每個層都建立在前一層之上。

  • Framework(框架層):這是一個純 Dart 實現的 SDK;

    【Foundation】在最底層,主要定義給其他層使用的底層工具類和方法。

    【Animation】是動畫相關的類。

    【Painting】封裝了 Flutter Engine 提供的繪製介面,例如繪製縮放影象、插值生成陰影、繪製盒模型邊框等。

    【Gesture】提供處理手勢識別和互動的功能。

    【Rendering】是框架中的渲染庫。

    【Widgets 】是 Flutter 提供的的一套基礎元件庫。Material 和 Cupertino 是兩種視覺風格的元件庫。

  • Engine(引擎層 :是 Flutter 的核心,這是一個純 C++ 實現的 SDK,其中包括了 Skia 引擎、Dart 執行時、文字排版引擎等。在程式碼呼叫 dart:ui 庫時,呼叫最終會走到 Engine 層,然後實現真正的繪製邏輯。

  • Embedder(嵌入層 ): 主要是將 Flutter 引擎 “安裝” 到特定平臺上,做好這一層的適配 Flutter 基本可以嵌入到任何平臺上去。

Flutter 在移動端的實踐中,目前來說已經有很成熟的業界方案了,但是 Flutter 在 web 的環境裡面的應用還是有所欠缺的。今天我們先來研究下 Flutter 構建 web 程式的相關技術棧。

用於 Web 支援的兩個方案

其實,最早在 2018 Flutter 1.0 的時候,Flutter 的產品經理 Tim Sneath 就推出了 Flutter Web。Flutter Web 想在單程式碼庫的情況下,讓 Flutter 應用擁有 Web 支援。開發者可以使用 Dart 編寫應用並部署到任意的 Web 伺服器上,或嵌入到瀏覽器中。甚至其他的 IOS、安卓、windows 裝置,開發者都可以使用 Flutter 所具有的特性,也不需要特殊的瀏覽器外掛支援。在 Flutter Web 的設計之初,主要考慮了兩個方案用於 Web 支援:

  1. HTML + CSS + Canvas
  2. CSS Paint API ( http://zhuanlan.zhihu.com/p/39931190 )

優缺點:

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

方案 2: 是新的 Web API , 屬於 CSS Houdini 的組成部分。CSS Houdini 提供了一組可以直接訪問 CSS 物件模型的 API ,使得開發者可以去書寫程式碼並被瀏覽器作為 CSS 加以解析,這樣在無需等待瀏覽器原生的支援下,創造了新的 CSS 特性。它的繪製並非由核心 JavaScript 完成,而是類似 Web Worker 的機制。但目前 CSS Paint API 不支援文字,此外各家廠商對其支援也並不統一。

Flutter for Web 的兩種編譯器

Flutter 官方給我們提供了 dart2js 和 dartdevc 兩個編譯器,我們不僅可以將程式碼直接執行在 chrome 瀏覽器,也可以將 Flutter 程式碼編譯為 js 檔案部署在服務端。

1、dart2js 編譯器

我們在呼叫 flutter run build 命令後會將專案的 main.dart 傳入編譯流程,最終輸出的是構建產物中的 .dill 檔案 。這個 .dill 檔案很關鍵,筆者的理解是一種包含了 dart 程式的抽象語法樹生成的 AST 檔案,能執行在所有的作業系統和 CPU 架構上。

在構建過程中 Flutter_tools 首先會將傳入的引數進行組裝,然後呼叫 dart2jsSnapshot 。進行 dart 檔案編譯,生成 Weget 樹的二進位制檔案的 .dill 檔案,這個程式碼的位置在 dart-sdk/html/dart2js/html_dart2js.dart 路徑下(對應版本:Flutter 2.5.3 Tools • Dart 2.14.4)。

dart2jsSnapshot 是一個專門為 web 平臺轉換做的直譯器,類似於 Flutter Web_sdk。只不過 Flutter Web_sdk 的原始碼更多的是在除錯時候做 debugger,效率很低。在 build 的時候,顯然利用快照的方式比較合理。

dart2js 編譯流程:

dart2js 呼叫的快照檔案示例圖:

如何生成 web 端程式碼

具體執行看這裡: http://dart.dev/tools/dart2js

我們再來看下 build 之後的生成目錄:

通過上面的介紹,我們知道整個轉換流程中承上啟下的關鍵產物就是 .dill 檔案。那麼他是如何通過程式碼生成的呢?

我們,首先通過 Flutter_tools 呼叫到 dart2jsSnapshot 檔案。呼叫的引數如下:

--libraries-spec=/Users/beike/Flutter/bin/cache/Flutter Web_sdk/libraries.json 
--native-null-assertions
-Ddart.vm.product=true 
-DFlutter Web_AUTO_DETECT=true 
--no-source-maps // 是否生成sourcemap的選項;
-O1 
-o 
--cfe-only // 代表只完成前端編譯,生成kernel檔案後就不繼續下面的後端編譯流程。
/Users/beike/path_to_js/main.dart.js 
/Users/beike/path_to_dill/app.dill

其中 O1 代表優化等級,dart2js 支援 O0 - O4 共 5 種優化,O4 的優化程度最高。通過優化可以減少產物的大小並且優化程式碼的效能。

Dart2js 的後端編譯主要包括以下程式碼:

  1. 首先,編譯器會將傳入的 .dill 通過 BinaryBuilder 載入到 Component 中並存儲在 KernelResult 中;
KernelResult result = await kernelLoader.load(uri);
  1. computeClosedWorld() 方法會將第一步解析出來的所有 Library 解析成 JsClosedWorld。
JsClosedWorld closedWorld = selfTask.measureSubtask("computeClosedWorld", () => computeClosedWorld(rootLibraryUri, libraries));

​ JsClosedWorld 代表了通過 closed-world 語義編譯之後的程式碼。它的結構如下:

class JsClosedWorld implements JClosedWorld {
    static const String tag = 'closed-world';
    @override final NativeData nativeData;
    @override final InterceptorData interceptorData;
    @override final BackendUsage backendUsage;
    @override final NoSuchMethodData noSuchMethodData;
    FunctionSet _allFunctions;
    final Map<classentity, Set> mixinUses;
          Map<classentity, List> _liveMixinUses;
    final Map<classentity, Set> typesImplementedBySubclasses; 
    final Map<classentity, Map> _subtypeCoveredByCache = <classentity, Map>{};
    // TODO(johnniwinther): Can this be derived from [ClassSet]s? 
    final Set implementedClasses;
    final Set liveInstanceMembers;
    // Members that are written either directly or through a setter selector.
    final Set assignedInstanceMembers;    
    @override final Set liveNativeClasses;
    @override final Set processedMembers;
    ...
  }
  1. 然後,使用 JsClosedWorld() 方法進行程式碼優化,包括下面程式碼中的 performGlobalTypeInference() 方法。
GlobalTypeInferenceResults globalInferenceResults = performGlobalTypeInference(closedWorld);
  1. 最終, generateJavaScriptCode() 方法會將上邊返回的結果通過 JSBuilder 生成最終的 js AST 也就是 .dill 檔案。
generateJavaScriptCode(globalInferenceResults);

2、dartdevc 編譯器

在 dartdevc 我們不僅可以將程式碼直接執行在 chrome 瀏覽器,也可以將 flutter 程式碼編譯為 js 檔案部署在服務端。如果程式碼執行在 chrome 瀏覽器,flutter_tools 會使用 dartdevc 編譯器進行編,如下圖:

dartdevc 是支援增量編譯的,開發者可以像除錯 Flutter Mobile 程式碼一樣使用 hot reload 來提升除錯效率。Flutter for Web 除錯也是非常方便的,編譯後的程式碼是預設支援 source map ,當執行在 web 瀏覽器時,開發者是不用關心生成的 js 程式碼是怎樣的。

好了,接下來我們從一個簡單的 案例 入手,看看 Flutter,是如何一步一步將 web 轉換為我們的 js,並在瀏覽器中使用和繪製出一個頁面。

關鍵程式碼部分:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title, style: TextStyle(color: Colors.white),),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            width: 250,
            height: 250,
            color: Colors.orange,
            child: Center(
              child: Text("6", style: TextStyle(fontSize: 200.0, color: Colors.green, fontWeight: FontWeight.bold),),
            ),
          )
        ],
      ),
    ), // This trailing comma makes auto-formatting nicer for build methods.
  );
}
abstract class c {
  void drawRect(Rect rect, Paint paint);
}
html.HtmlElement_drawRect(ui.Offset p, SurfacePaintData paint) {
 [省略部分程式碼]
  Element = _drawRect(paint); // 繪製,
 [省略部分程式碼]
 final String cssTransform = float64ListToCssTransform(
 transformWithOffset(_canvasPool.currentTransform, p).storage);
 imgElement.style
 ..transformOrigin = '0 0 0' ..transform = cssTransform 
 ..removeProperty('width')
 ..removeProperty('height');
 rootElement.append(imgElement);
 _children.add(imgElement); return imgElement;
 }

當排程任務呼叫到 drawRect() 方法之後,drawRect() 方法中會建立 canvas 元素,並且將 dart 的繪製邏輯重新實現一遍,最終將 Element 新增到 rootElement,也就是當前的 flt-canvas 元素中。生成的 html 如下:

Flutter 總結展望

dart2js 和 dartdevc 本質上是一件事情,但這兩種編譯器是應用在不同場景。在開發應用程式時選擇 dartdevc,它支援增量編譯,因此你可以快速檢視編輯結果。在構建要部署的應用程式時,選用 dart2js,它使用搖樹等技術來生成優化的且精簡的程式碼。

dart2js 提供了更快的編譯時間,並且編譯後的執行效果與之前相比更加一致、完整,更重要的是,輸出的程式碼更加整潔。Dart 團隊正在努力使 dart2js 編譯後的程式碼比手寫 JS 更快地執行。

通過以上的簡單分析,我們發現通過 Flutter 的編譯,重寫了大量的繪製的 Class,這對於前端開發來說可能提供了一個新的思路。當然本次有些地方還是很粗略的分析。只是初步介紹了 Flutter 打包構建流程,並沒有給出完整的思路。後面會繼續努力,將在後續的文章中與大家分享。希望隨著 Flutter 社群方案的愈加完善,利用 Flutter 技術棧上線的 web 產品也會越來越多。

引用

  1. Flutter渲染原理解析
  2. CSS Paint API
  3. 如何評價 Flutter for Web?
  4. Dart

❉ 作者介紹 ❉