1. 程式人生 > 程式設計 >淺談鴻蒙 JavaScript GUI 技術棧

淺談鴻蒙 JavaScript GUI 技術棧

作者:doodlewind
連結:https://juejin.im/post/6872154561574862855

眾所周知,剛剛開源的「鴻蒙 2.0」以 JavaScript 作為 IoT 應用開發的框架語言。這標誌著繼 SpaceX 上天之後,JavaScript 再一次蹭到了新聞聯播級的熱點。這麼好的機會,只拿來陰陽怪氣實在太可惜了。作為科普,這篇文章不會拿著放大鏡找出程式碼中的槽點來吹毛求疵,而是希望通俗地講清楚它所支援的 GUI 到底是怎麼一回事。只要對計算機基礎有個大概的瞭解,應該就不會對本文有閱讀上的障礙。

我們已經知道在「鴻蒙 2.0」上,開發者只需編寫形如 Vue 元件式的 JavaScript 業務邏輯,即可將其渲染為智慧手錶等嵌入式硬體上的 UI 介面。這個過程中需要涉及哪些核心的模組呢?這些模組中又有哪些屬於自研,哪些使用了現成的開源專案呢?這裡將其分為自上而下的三個抽象層來介紹:

  • JS 框架層,可理解為一個大幅簡化的 Vue 式 JavaScript 框架
  • JS 引擎與執行時層,可理解為一個大幅簡化的 WebKit 式執行時
  • 圖形渲染層,可理解為一個大幅簡化的 Skia 式圖形繪製庫

這三個抽象層,整體構成了一套面向嵌入式硬體的 GUI 技術棧。不同於許多高呼「不明覺厲 / 深不可測」的輿論,個人認為至少對於 GUI 部分,國內凡是接觸過目前主流 Hybrid 式跨端方案或 JS 執行時研發的一線開發者,都很容易從原始碼出發來理解它。下面逐層對其做一些解讀和分析。

JS 框架層

從最頂層的視角出發,要想用「鴻蒙 2.0」渲染出一段動態的文字,你只需要編寫如下的 HML(類 XML)格式程式碼:

<!-- hello.hml -->
<text onclick="boil">{{hello}}</text>

然後在同級目錄編寫這樣的 JavaScript:

// hello.js
export default {
 data: {
 hello: 'PPT'
 },boil() {
 this.hello = '核武器';
 }
}

這樣只要點選文字,就會呼叫 boil 方法,讓 PPT 變成 核武器。

這背後發生了什麼呢?熟悉 Vue 2.0 的同學應該會立刻聯想到下面這幾件事:

  • 需要對 XML 的預處理機制,將其轉換為 JS 中的巢狀函式結構。這樣只需在執行時做一次簡單 eval ,即可用 JS 生成符合 XML 結構的 UI。
  • 需要事件機制,使得觸發 onclick 事件時能執行相應回撥。
  • 需要資料劫持機制,使得對 this.hello 賦值時能執行相應回撥。
  • 需要能在回撥中更新 UI 物件控制元件。

這幾件事分別是怎麼實現的呢?簡單說來是這樣的:

  • XML 預處理依賴現成的 NPM 開源包,從而把 XML 中的 onclick 屬性轉換為 JS 物件的屬性欄位。
  • 事件的註冊和觸發都直接由 C++ 實現。如上一步所獲得的 JS 物件 onclick 屬性會在 C++ 中被檢查和註冊,相當於全部元件均為原生。
  • 資料劫持機制用 JS 實現,是個基於 Object.defineProperty 的(幾百行量級的)ViewModel。
  • UI 控制元件的更新,會在 ViewModel 自動執行的 JS 回撥中,呼叫 C++ 的原生方法實現。這部分完全隱式完成,並未開放 document.createElement 式的標準化 API。

由於大量常見 JS 框架中的能力都直接做進了 C++,所以整套 GUI 技術棧裡用純 JavaScript 所實現的東西(主要見 ace_lite_jsfwk 倉庫下的 core/index.jsobserver.js subject.js),相當於有且只有這麼一個功能:

一個可以 watch 的 ViewModel。

至於純 JS 框架部分的實現複雜度和質量,客觀地說如果是個人業餘作品,可以當作校招面試中不錯的加分項。

JS 引擎與執行時層

理解了 JS 框架層之後,我們既可以認為「鴻蒙 2.0」選擇把高度簡化後的 Vue 深度定製進了 C++ 裡,也可以認為它緊密圍繞著高度簡化(且私有)的 DOM 實現了配套的前端框架。因此要想繼續探索這套 GUI 的原理,我們就必須進入其 C++ 部分,瞭解其 JS 引擎與執行時層的實現。

JS 引擎和執行時之間,有什麼區別與聯絡呢?JS 引擎一般只需符合 ECMA-262 規範,其中沒有對任何帶「副作用」的平臺 API 的定義。從 setTimeoutdocument.getElementById console.log 再到 fs.readFile,這些能執行實際 IO 操作的功能,都需要由「將引擎 API 和平臺 API 膠合到一起」的執行時提供。執行時本身的原理並不複雜,譬如在個人的文章《從 JS 引擎到 JS 執行時》中,你就可以看到如何藉助現成的QuickJS 引擎,自己搭建一個執行時。

那麼在「鴻蒙 2.0」中,JS 執行時是如何搭建出來的呢?有這麼幾條重點:

  • JS 引擎選擇了 JerryScript,這是一款由三星開發的嵌入式 JS 引擎。
  • 每種形如 <text> <div> 的 XML 標籤元件,都對應一個繫結到 JerryScript 上的 C++ Component 類,如 TextComponent DivComponent 等。
  • 除 UI 原生物件外,還有一系列在 JS 中以 @system 為字首的 built-in 模組,它們提供了 JS 中可用的 Router / Audio / File 等平臺能力(參見 ohos_module_config.h)。

這裡特別值得一提的是 Router。它和 vue-router 等常見 Web 平臺路由的實現原理有很大區別,是專門在執行時內深度定製的(參見 router_module.cppjs_router.cpp js_page_state_machine.cpp)。簡單說來這個「路由」是這樣實現的:

  • 在 JS 中呼叫切換頁面的 router.replace 原生方法,走進 C++。
  • C++ 中根據新頁面 URI 路徑(如 pages/detail)載入新頁面 JS,新建頁面狀態機例項,將其切換至 Init 狀態。
  • 在新狀態機的 Init 過程中,呼叫 JS 引擎去 eval 新頁面的 JS 程式碼,獲得新頁面的 ViewModel。
  • 將路由引數附加到 ViewModel 上,銷燬舊狀態機及其上的 JS 物件。

所以我們可以發現,這裡所謂的「切換路由」,其實更接近 Web 瀏覽器的「重新整理頁面」。那麼我們可以認為這個 JS 執行時的能力,已經可以對標 WebKit 級的瀏覽器核心了嗎?

當然還差得很遠。與 WebKit 相比,它並未支援對 HTML 和 CSS 的解析(二者都會在開發階段被解析轉換成同等執行效果的 JS),也沒有瀏覽器中持續動態載入、解析與執行資源的挑戰(小程式不外乎是幾個本地的靜態 JS 檔案)。至於排版佈局和渲染方面自然也有很大差距,這點會在最後一節提及。

另外,相信很多同學都會對 JerryScript 引擎感到好奇。本部分最後分享一些個人對此所掌握的訊息。

JerryScript 引擎是一款專為嵌入式硬體實現的 JS 直譯器,只支援到 ES5.1 標準。在 QuickJS Benchmark 中,可以檢視到它們的效能對比結果:

淺談鴻蒙 JavaScript GUI 技術棧

可以看到論效能,JerryScript 在無 JIT 的引擎中大幅弱於 QuickJS 和 Hermes。如果和開啟了 JIT 的 V8 相比,甚至會慢出兩個數量級。因此這是非常特定於低端裝置的引擎,如果需要支援 React 和 Vue 這類中大型前端專案中標配的基礎庫(甚至其相應全家桶),仍然可能需要使用更強大的引擎。

對於 JerryScript 的使用,有同場景重度應用經驗的當屬 RT-Thread 創始人 @午夜熊,他們和某國內一線廠商合作研發的智慧手錶就用 JerryScript 實現了 UI,目前產品馬上就要上市了。他們團隊對 JerryScript 的一些使用反饋也吻合上述評價,概括說來是這樣的:

  • JerryScript 在體積和記憶體佔用上,相比 QuickJS 有更好的表現。
  • JerryScript 的穩定性弱於 QuickJS,有一些難以繞過的問題。
  • JerryScript 面對稍大(1M 以上)的 JS 程式碼庫,就有些力不從心了。

那麼師出名門的 QuickJS 和 Facebook 的 Hermes,是否就是無 JIT 式 JS 引擎的下一代標杆了嗎?倒也未必如此。這方面可以參考個人的知乎回答:隨著 TypeScript 繼續普及,會不會出現直接跑 TypeScript 的執行時?這裡提到的微軟為教育專案 MakeCode 研發的 Static TypeScript,就相當有潛力成為下一代的高效能 JS 系語言環境。通過限定 TypeScript 的靜態強型別子集併為其搭建工具鏈,STS 可以做到無需 JIT 也能接近 V8 的效能水平,同時記憶體佔用比 V8 少兩個數量級。這使得 STS 不光能用於開發普通 app 這類 IO 密集的應用,還能順利在嵌入式硬體上開發小遊戲這類更偏計算密集(需逐幀更新渲染)的應用,在工程能力上是一項很大的突破。

所以說,當「鴻蒙 2.0」還需要熟練開發者勉強搭建出環境跑通 Hello World 時,微軟已經讓上百萬小朋友都能用 TypeScript 在網頁裡給教學用的掌上游戲機寫小遊戲入門程式設計了。這裡沒什麼唱反調的意思,只希望提醒一下我們在為國產「里程碑」歡呼時,也要清醒地看到業界前沿的動向,僅此而已。

圖形繪製層

理解 JS 執行時之後,還剩最後一個問題,即 JS 執行時中的各種 Component 物件,是如何被繪製為手錶等裝置上的畫素的呢?

這就涉及「鴻蒙 2.0」中的另一個 graphic_lite 倉庫了。可以認為,這裡才是真正執行實際繪製的 GUI。像之前的 TextComponent 等原生元件,都會對應到這裡的某種圖形庫 View。它以一種相當經典的方式,在 C++ 層實現並提供了「Canvas 風格的立即模式 GUI」和「DOM 風格的保留模式 GUI」兩套 API 體系(對於立即模式和保留模式 GUI 的區別與聯絡,可參見個人這篇IMGUI 科普回答)。概括說來,這個圖形子系統的要點大致如下:

  • 圖形庫提供了 UIView 這個 C++ 控制元件基類,其中有一系列形如 OnClick / OnLongPress / OnDrag 的虛擬函式。基本每種 JS 中可用的原生 Component 類,都對應於一種 UIView 的子類。
  • 除了各種定製化 View 之外,它還開放了一系列形如 DrawLine / DrawCurve / DrawText 等命令式的繪製方法。
  • 這個圖形庫具備名為 GFX 的 GPU 加速模組,但它目前似乎只有象徵性的 FillArea 矩形單色填充能力。

在基礎 UI 控制元件方面,不難找到一些值得一提的自研模組特性:

  • 支援了簡易的 RecycleView 長列表。
  • 支援了簡易的 Flex 佈局。
  • 支援了內部的 Invalidate 髒標記更新機制。

至於 2D UI 渲染中的幾項關鍵能力,則基本可分為路徑、點陣圖和文字三類。這個圖形庫在這幾個方面都有涉及,最後簡單介紹一下。

首先對於點陣圖,這個圖形庫依賴了 libpng libjpeg 做影象解碼,然後即可使用記憶體中的 bitmap 影象做繪製。

然後對於路徑,這個圖形庫自己實現了各種 CPU 中的畫素繪製方法,典型的例子就是這個貝塞爾曲線的繪製原始碼:

void DrawCurve::DrawCubicBezier(const Point& start,const Point& control1,const Point& control2,const Point& end,const Rect& mask,int16_t width,const ColorType& color,OpacityType opacity)
{
 if (width == 0 || opacity == OPA_TRANSPARENT) {
 return;
 }

 Point prePoint = start;
 for (int16_t t = 1; t <= INTERPOLATION_RANGE; t++) {
 Point point;
 point.x = Interpolation::GetBezierInterpolation(t,start.x,control1.x,control2.x,end.x);
 point.y = Interpolation::GetBezierInterpolation(t,start.y,control1.y,control2.y,end.y);
 if (prePoint.x == point.x && prePoint.y == point.y) {
  continue;
 }

 DrawLine::Draw(prePoint,point,mask,width,color,opacity);
 prePoint = point;
 }
}

基於高中的數學知識,我們不難明白這種曲線是如何繪製出來的:取足夠多的點(也就是那個預設 1000 的 INTERPOLATION_RANGE)作為插值輸入,逐點計算出曲線表示式的 XY 座標,然後直接修改畫素位置所在的 framebuffer 記憶體即可。這種教科書式的實現是最經典的,不過如果要拿它對標 Skia 裡的黑魔法,還是不要勉為其難了吧。

最後對於文字的繪製,會涉及一些字型解析、定位、RTL和折行等方面的處理。這部分實際上也是組合使用了一些業界通用的開源基礎庫來實現的。比如對於「牢」這個字,就可以找到圖形庫的這麼幾個開源依賴,它們各自扮演不同的角色:

  • harfbuzz - 用來告訴呼叫者,應該把「牢」的 glyph 字形放在哪裡。
  • freetype - 從宋體、黑體等字型檔案中解碼出「牢」的 glyph 字形,將其光柵化為畫素。
  • icu - 處理 Unicode 中許多奇葩的特殊情況,這塊個人不瞭解,略過。

到這裡,我們就可以理出一個非常概括性的渲染流程了:

  • JS 中執行 this.hello = 'PPT' 之類的程式碼,觸發依賴追蹤。
  • JS 依賴追蹤回撥觸發原生函式,更新 C++ 的 Component 元件狀態。
  • Component 更新其繫結的 UIView 子類狀態,觸發圖形庫更新。
  • 圖形庫更新記憶體中的畫素狀態,完成繪製。

這就是個人對「鴻蒙 2.0」這套 GUI 技術棧的解讀了。時間有限並未進一步深挖,歡迎(文明的)批評指正。

總結

特別宣告:本部分主觀評論僅針對「鴻蒙 2.0」當前的 GUI 框架部分,請勿隨意曲解。

對於「鴻蒙 2.0」在 GUI 部分的亮點,個人能想到這些:

  • 確實有務實(但和當年 PPT 介紹完全兩碼事)的程式碼。
  • 不是 WebView 套殼,佈局和繪製是自己做的。
  • 無需超過大學本科水平的計算機知識,也能順利閱讀理解。

而至於明顯(不只是某幾行程式碼寫得醜)的缺失或問題,目前看來則有這麼一些:

JS 框架層

  • 沒有基本的元件間通訊(如 props / emit 等)能力
  • 沒有基本的自定義元件能力
  • 沒有除基礎依賴追蹤以外的狀態管理能力

JS 引擎與執行時層

  • 標準支援過低,無法執行 Vue 3.0 這類需 Proxy 的下一代前端框架
  • 效能水平弱,難以支援中大型 JS 應用
  • 沒有開放 DOM 式的物件模型 API,不利於上層抹平差異

圖形渲染層

  • 沒有實質可用的 GPU 加速
  • 沒有 SVG 和富文字等高階渲染能力
  • Canvas 完成度低,缺狀態棧和很多 API

看起來槽點很多,但是你會指責汽車沒有噴氣式發動機嗎?對於不同複雜度的場景,自然存在著不同的最優架構設計。目前看來,這套設計確實很適合嵌入式硬體和簡易「小程式」的場景。但如果按照所謂「分散式全場景跨平臺」的要求來審視,那麼不管比起現代的 Web 瀏覽器還是 iOS 和安卓的 GUI,這套架構的複雜度都是完全無法相提並論的。如果想在手機上實裝,幾乎必定還需要追加大量複雜模組,進行大幅的架構演化與重新設計。

當然,汽車廠商也不會說自己造的是飛機,對吧?

總之這確實是一盤自己做的麻婆豆腐,但不是某些人口中的滿漢全席。
最後是個人的主觀評論:
首先,這套 GUI 技術棧達到了組裝和借鑑開源產品時所能獲得的主流水平。但論效能和表現力上限,其核心模組距離微軟MakeCode 這類業界 cutting-edge 級的產學研結合前沿方案,仍然有數量級的代際差距。

其次,不必把它當作需要海量專家精密計算的 Rocket Science——不是貶低自主研發,而是真心地希望大家能明白,「這件事我也可以實際參與進來!」作業系統和 GUI 沒有那麼神祕,已有很多國產的成熟開源產品可供學習、使用與貢獻(這裡順便推薦極易體驗且同為國產的 RT-Thread 作為嚐鮮入門之用)。畢竟只有真正搞懂了某個產品在技術上到底是怎麼一回事,才不容易被別有用心的人帶節奏,對吧?

最後,對於所有熟悉 JavaScript 的前端開發者們,你們為什麼還要陰陽怪氣地嘲笑鴻蒙呢?鴻蒙就是 JavaScript 在中國的財富密碼啊!JavaScript 被鴻蒙這樣的「國之重器」採用,可以大大增強前端的道路自信、理論自信、文化自信和技術棧自信。只要以這種形式結合拼接與自研,就可以一舉在全國上下獲得崇高的聲望,這條路真是太讓人心馳神往了呀(小聲)

我們要團結起來,大力弘揚和宣傳 JavaScript 在大國競爭中的核威懾級地位,爭取上升到只要說自己會寫JavaScript,大家就會對你肅然起敬的高度——只要你是前端程式設計師,買票可以插隊,搭車可以讓座,開房可以白嫖……好時代,來臨了!

想成為國之棟樑嗎?來寫 JavaScript 吧!

不多說了,我要去實幹興邦啦!

以上就是淺談鴻蒙 JavaScript GUI 技術棧的詳細內容,更多關於鴻蒙 JavaScript GUI 技術棧的資料請關注我們其它相關文章!