React.Component 損害了複用性?

ThoughtWorks發表於2016-09-07

本系列的上一篇文章《為什麼ReactJS不適合複雜的前端專案》列舉了前端開發中的種種痛點。本篇文章中將詳細探討其中“複用性”痛點。我們將用原生 DHTML API 、 ReactJS 和 Binding.scala 實現同一個需要複用的標籤編輯器,然後比較三個標籤編輯器哪個實現難度更低,哪個更好用。

標籤編輯器的功能需求

在InfoQ的許多文章都有標籤。比如本文的標籤是“binding.scala”、“data-binding”、“scala.js”。

假如你要開發一個部落格系統,你也希望部落格作者可以新增標籤。所以你可能會提供標籤編輯器供部落格作者使用。

如圖所示,標籤編輯器在視覺上分為兩行。

標籤編輯器

第一行展示已經新增的所有標籤,每個標籤旁邊有個“x”按鈕可以刪除標籤。第二行是一個文字框和一個“Add”按鈕可以把文字框的內容新增為新標籤。每次點選“Add”按鈕時,標籤編輯器應該檢查標籤是否已經新增過,以免重複新增標籤。而在成功新增標籤後,還應清空文字框,以便使用者輸入新的標籤。

除了使用者介面以外,標籤編輯器還應該提供 API 。標籤編輯器所在的頁面可以用 API 填入初始標籤,也可以呼叫 API 隨時增刪查改標籤。如果使用者增刪了標籤,應該有某種機制通知頁面的其他部分。

原生 DHTML 版

首先,我試著不用任何前端框架,直接呼叫原生的 DHTML API 來實現標籤編輯器,程式碼如下:

為了實現標籤編輯器的功能,我用了 45 行 JavaScript 程式碼來編寫 UI 邏輯,外加若干的 HTML <div> 外加兩行 JavaScript 程式碼填入初始化資料。

HTML 檔案中硬編碼了幾個 <div>。這些<div> 本身並不是動態建立的,但可以作為容器,放置其他動態建立的元素。

程式碼中的函式來會把網頁內容動態更新到這些 <div> 中。所以,如果要在同一個頁面顯示兩個標籤編輯器,id 就會衝突。因此,以上程式碼沒有複用性。

就算用 jQuery 代替 DHTML API,程式碼複用仍然很難。為了複用 UI ,jQuery 開發者通常必須額外增加程式碼,在 onload 時掃描整個網頁,找出具有特定 class 屬性的元素,然後對這些元素進行修改。對於複雜的網頁,這些 onload 時執行的函式很容易就會衝突,比如一個函式修改了一個 HTML 元素,常常導致另一處程式碼受影響而內部狀態錯亂。

ReactJS 實現的標籤編輯器元件

ReactJS 提供了可以複用的元件,即 React.Component 。如果用 ReactJS 實現標籤編輯器,大概可以這樣寫:

以上 51 行 ECMAScript 2015 程式碼實現了一個標籤編輯器元件,即TagPicker。雖然程式碼量比 DHTML 版長了一點點,但複用性大大提升了。

如果你不用 ECMAScript 2015 的話,那麼程式碼還會長一些,而且需要處理一些 JavaScript 的坑,比如在回撥函式中用不了 this

ReactJS 開發者可以隨時用 ReactDOM.render 函式把 TagPicker 渲染到任何空白元素內。此外,ReactJS 框架可以在 stateprops 改變時觸發 render ,從而避免了手動修改現存的 DOM。

如果不考慮冗餘的 key 屬性,單個元件內的互動 ReactJS 還算差強人意。但是,複雜的網頁結構往往需要多個元件層層巢狀,這種父子元件之間的互動,ReactJS 就很費勁了。

比如,假如需要在 TagPicker 之外顯示所有的標籤,每當使用者增刪標籤,這些標籤也要自動更新。要實現這個功能,需要給 TagPicker 傳入 changeHandler 回撥函式,程式碼如下:

為了能觸發頁面其他部分更新,我被迫增加了一個 21 行程式碼的 Page 元件。

Page 元件必須實現 changeHandler 回撥函式。每當回撥函式觸發,呼叫 Page 自己的 setState 來觸發 Page 重繪。

從這個例子,我們可以看出, ReactJS 可以簡單的解決簡單的問題,但碰上層次複雜、互動頻繁的網頁,實現起來就很繁瑣。使用 ReactJS 的前端專案充滿了各種 xxxHandler 用來在元件中傳遞資訊。我參與的某海外客戶專案,平均每個元件大約需要傳入五個回撥函式。如果層次巢狀深,建立網頁時,常常需要把回撥函式從最頂層的元件一層層傳入最底層的元件,而當事件觸發時,又需要一層層把事件資訊往外傳。整個前端專案有超過一半程式碼都在這樣繞圈子。

Binding.scala 的基本用法

在講解 Binding.scala 如何實現標籤編輯器以前,我先介紹一些 Binding.scala 的基礎知識:

Binding.scala 中的最小複用單位是資料繫結表示式,即 @dom 方法。每個 @dom 方法是一段 HTML 模板。比如:

每個模板還可以使用bind語法包含其他子模板,比如:

你可以參見附錄:Binding.scala快速上手指南,學習上手Binding.scala開發的具體步驟。

此外,本系列第四篇文章《HTML也可以編譯》還將列出Binding.scala所支援的完整HTML模板特性。

Binding.scala實現的標籤編輯器模板

最後,下文將展示如何用Binding.scala實現標籤編輯器。

標籤編輯器要比剛才介紹的HTML模板複雜,因為它不只是靜態模板,還包含互動。

這個標籤編輯器的 HTML 模板一共用了 18 行程式碼就實現好了。

標籤編輯器中需要顯示當前所有標籤,所以此處用tags: Vars[String]儲存所有的標籤資料,再用for/yield迴圈把tags中的每個標籤渲染成UI元素。

Vars 是支援資料繫結的列表容器,每當容器中的資料發生改變,UI就會自動改變。所以,在x按鈕中的onclick事件中刪除tags中的資料時,頁面上的標籤就會自動隨之消失。同樣,在Add按鈕的onclick中向tags中新增資料時,頁面上也會自動產生對應的標籤。

Binding.scala不但實現標籤編輯器比 ReactJS 簡單,而且用起來也比 ReactJS 簡單:

只要用 9 行程式碼另寫一個 HTML 模板,在模板中呼叫剛才實現好的 tagPicker 就行了。

完整的 DEMO 請訪問 https://thoughtworksinc.github.io/Binding.scala/#4

在 Binding.scala 不需要像 ReactJS 那樣編寫 changeHandler 之類的回撥函式。每當使用者在 tagPicker 輸入新的標籤時,tags 就會改變,網頁也就會自動隨之改變。

對比 ReactJS 和 Binding.scala 的程式碼,可以發現以下區別:

  • Binding.scala 的開發者可以用類似 tagPicker 這樣的 @dom 方法表示 HTML 模板,而不需要元件概念。
  • Binding.scala 的開發者可以在方法之間傳遞 tags 這樣的引數,而不需要 props 概念。
  • Binding.scala 的開發者可以在方法內定義區域性變數表示狀態,而不需要 state 概念。

總的來說 Binding.scala 要比 ReactJS 精簡不少。

如果你用過 ASP 、 PHP 、 JSP 之類的服務端網頁模板語言, 你會發現和 Binding.scala 的 HTML 模板很像。

使用 Binding.scala 一點也不需要函數語言程式設計知識,只要把設計工具中生成的 HTML 原型複製到程式碼中,然後把會變的部分用花括號代替、把重複的部分用 for / yield 代替,網頁就做好了。

結論

本文對比了不同技術棧中實現和使用可複用的標籤編輯器的難度。

原生 HTML ReactJS Binding.scala
實現標籤編輯器需要程式碼行數 45行 51行 17行
實現標籤編輯器的難點 在程式碼中動態更新HTML頁面太繁瑣 實現元件的語法很笨重
使用標籤編輯器並顯示標籤列表需要程式碼行數 難以複用 21行 8行
阻礙複用的難點 靜態HTML元素難以模組化 互動元件之間層層傳遞迴調函式過於複雜

Binding.scala 不發明“元件”之類的噱頭,而以更輕巧的“方法”為最小複用單位,讓程式設計體驗更加順暢,獲得了更好的程式碼複用性。

本系列下一篇文章將比較 ReactJS 的虛擬 DOM 機制和 Binding.scala 的精確資料繫結機制,揭開 ReactJS 和 Binding.scala 相似用法背後隱藏的不同演算法。

相關連結

相關文章