《More than React》系列的文章會一共分為五篇。本文是第一篇,介紹用ReactJS開發時遇到的種種問題。後面四篇文章的每一篇將會分別詳細討論其中一個問題,以及Binding.scala如何解決這個問題。
背景介紹
去年 4 月,我第一次在某個客戶的專案中接觸到ReactJS 。
我發現ReactJS要比我以前用過的AngularJS簡單很多,它提供了響應式的資料繫結功能,把資料對映到網頁上,使我可以輕鬆實現互動簡單的網站。
然而,隨著我越來越深入的使用ReactJS,我發現用ReactJS編寫互動複雜的網頁很困難。 我希望有一種方式,能夠像ReactJS一樣簡單解決簡單問題。此外,還要能簡單解決複雜問題。
於是我把ReactJS用Scala重新寫了一個。程式碼量從近三萬行降到了一千多行。
用這個框架實現的TodoMVC應用,只用了154行程式碼。而用ReactJS實現相同功能的TodoMVC,需要488行程式碼。
下圖是用Binding.scala實現的TodoMVC應用。
這個框架就是Binding.scala。
問題一:ReactJS元件難以在複雜互動頁面中複用
ReactJS中的最小複用單位是元件。ReactJS的元件比AngularJS的Controller和View 要輕量些。 每個元件只需要前端開發者提供一個 render
函式,把 props
和 state
對映成網頁元素。
這樣的輕量級元件在渲染簡單靜態頁面時很好用, 但是如果頁面有互動,就必須在元件間傳遞迴調函式來處理事件。
我將在《More than React(二)元件對複用性有害?》中用原生DHTML API、ReactJS和Binding.scala實現同一個需要複用的頁面,介紹Binding.scala如何簡單實現、簡單複用複雜的互動邏輯。
問題二:ReactJS的虛擬DOM 演算法又慢又不準
ReactJS的頁面渲染演算法是虛擬DOM差量演算法。
開發者需要提供 render
函式,根據 props
和 state
生成虛擬 DOM。 然後 ReactJS 框架根據 render
返回的虛擬 DOM 建立相同結構的真實 DOM.
每當 state
更改時,ReacJS 框架重新呼叫 render
函式,獲取新的虛擬 DOM 。 然後,框架會比較上次生成的虛擬 DOM 和新的虛擬 DOM 有哪些差異,然後把差異應用到真實DOM上。
這樣做有兩大缺點:
- 每次
state
更改,render
函式都要生成完整的虛擬 DOM. 哪怕state
改動很小,render
函式也會完整計算一遍。如果render
函式很複雜,這個過程就白白浪費了很多計算資源。 - ReactJS框架比較虛擬DOM差異的過程,既慢又容易出錯。比如,假如你想要在某個
<ul>
列表的頂部插入一項<li>
,那麼ReactJS框架會誤以為你修改了<ul>
的每一項<li>
,然後在尾部插入了一個<li>
。
這是因為 ReactJS收到的新舊兩個虛擬DOM之間相互獨立,ReactJS並不知道資料來源發生了什麼操作,只能根據新舊兩個虛擬DOM來猜測需要執行的操作。 自動的猜測演算法既不準又慢,必須要前端開發者手動提供 key
屬性、shouldComponentUpdate
方法、componentDidUpdate
方法或者 componentWillUpdate
等方法才能幫助 ReactJS 框架猜對。
我將在《More than React(三)虛擬DOM已死?》中比較ReactJS、AngularJS和Binding.scala渲染機制,介紹簡單效能高的Binding.scala精確資料繫結機制。
問題三:ReactJS的HTML模板功能既不完備、也不健壯
ReactJS支援用JSX編寫HTML模板。
理論上,前端工程師只要把靜態HTML原型複製到JSX原始檔中, 增加一些變數替換程式碼, 就能改造成動態頁面。 理論上這種做法要比Cycle.js、Widok、ScalaTags等框架更適合複用設計師提供的HTML原型。
不幸的是,ReactJS對HTML的支援殘缺不全。開發者必須手動把class
和for
屬性替換成className
和htmlFor
,還要把內聯的style
樣式從CSS語法改成JSON語法,程式碼才能執行。 這種開發方式下,前端工程師雖然可以把HTML原型複製貼上到程式碼中,但還需要大量改造才能實際執行。 比Cycle.js、Widok、或者、ScalaTags省不了太多事。
除此之外,ReactJS還提供了propTypes
機制校驗虛擬DOM的合法性。 然而,這一機制也漏洞百出。 即使指定了propTypes
,ReactJS也不能在編譯前提前發現錯誤。只有測試覆蓋率很高的專案時才能在每個元件使用其他元件時進行校驗。 即使測試覆蓋率很高,propTypes
仍舊不能檢測出拼錯的屬性名,如果你把onClick
寫成了onclick
, ReactJS就不會報錯,往往導致開發者額外花費大量時間排查一個很簡單的bug。
我將在《More than React(四)HTML也可以編譯?》中比較ReactJS和Binding.scala的HTML模板,介紹Binding.scala如何在完整支援XHTML語法的同時靜態檢查語法錯誤和語義錯誤。
問題四:ReactJS與伺服器通訊時需要複雜的非同步程式設計
ReactJS從伺服器載入資料時的架構可以看成MVVM(Model–View–ViewModel)模式。 前端工程師需要編寫一個資料庫訪問層作為Model,把ReactJS的state
當做ViewModel,而render
當做View。 Model負責訪問資料庫並把資料設定到state
(即View Model)上,可以用Promise和fetch API實現。 然後,render
,即View,負責把View Model渲染到頁面上。
在這整套流程中,前端程式設計師需要編寫大量閉包組成的非同步流程, 設定、訪問狀態的程式碼五零四散, 一不小心就會bug叢生,就算小心翼翼的處理各種非同步事件,也會導致程式變得複雜,既難除錯,又難維護。
我將在《More than React(五)為什麼別用非同步程式設計?》中比較ReactJS和Binding.scala的資料同步模型,介紹Binding.scala如何自動同步伺服器資料,避免手動非同步程式設計。
結論
儘管Binding.scala初看上去很像ReactJS, 但隱藏在Binding.scala背後的機制更簡單、更通用,與ReactJS和Widok截然不同。
所以,通過簡化概念,Binding.scala靈活性更強,能用通用的方式解決ReactJS解決不了的複雜問題。
比如,除了上述四個方面以外,ReactJS的狀態管理也是老大難問題,如果引入Redux或者react-router這樣的第三方庫來處理狀態,會導致架構變複雜,分層變多,程式碼繞來繞去。而Binding.scala可以用和頁面渲染一樣的資料繫結機制描述複雜的狀態,不需要任何第三方庫,就能提供伺服器通訊、狀態管理和網址分發的功能。
以下表格中列出了上述Binding.scala和ReactJS的功能差異:
Binding.scala | ReactJS | ||
---|---|---|---|
複用性 | 最小複用單位 | 方法 | 元件 |
複用難度 | 不論互動內容還是靜態內容都容易複用 | 容易複用靜態內容元件,但難以複用互動元件 | |
頁面渲染演算法 | 演算法 | 精確的資料繫結 | 虛擬 DOM |
效能 | 高 | 低 | |
正確性 | 自動保證正確性 | 需要開發者手動設定 key 屬性,不然複雜的頁面會錯亂。 |
|
HTML 模板 | 語法 | Scala XML 字面量 | JSX |
是否支援 HTML 或 XHTML 語法 | 完整支援 XHTML | 殘缺支援。正常的 XHTML 無法編譯。開發者必須手動把 class 和 for 屬性替換成 className 和 htmlFor ,還要把內聯的 style 樣式從 CSS 語法改成 JSON 語法。 |
|
如何校驗模板語法 | 自動編譯時校驗 | 執行時通過 propTypes 校驗但無法檢測簡單的拼寫錯誤。 |
|
伺服器通訊 | 機制 | 自動遠端資料繫結 | MVVM + 非同步程式設計 |
實現難度 | 簡單 | 複雜 | |
其他 | 如何分派網址或者錨點連結 | 支援把網址當成普通的繫結變數來用,無需第三方庫。 | 不支援,需要第三方庫 react-router |
功能完備性 | 完整的前端開發解決方案 | 本身只包含檢視部分功能。需要額外掌握 react-router 、 Redux 等第三方庫才能實現完整的前端專案。 | |
學習曲線 | API 簡單,對沒用過 Scala 的人來說也很好懂 | 上手快。但功能太弱導致後期學習第三方庫時曲線陡峭。 | |
Binding.scala | ReactJS |
兩個多月前,我在Scala.js的論壇釋出Binding.scala時,當時Scala.js社群最流行的響應式前端程式設計框架是Widok。Tim Nieradzik是Widok的作者。他在看到我釋出的框架後,稱讚這個框架是Scala.js社群最有前途的 HTML 5渲染框架。
他是對的,兩個月後,現在Binding.scala已經成為Scala.js社群最流行的響應式前端程式設計框架。
Awesome Scala網站對比了Scala的響應式前端程式設計框架,Binding.scala的活躍程度和流行度都比Udash、Widok等其他框架要高。
我在最近的幾個專案中,也逐漸放棄JavaScript和ReactJS,改用Scala.js和Binding.scala搭建新時代的前端技術棧。