想寫好前端,先練好內功
前言
封不平聽在耳裡,暗叫:“到這地步,我再能隱藏甚麼?”仰天一聲清嘯,斜行而前,長劍橫削直擊,迅捷無比,未到五六招,劍勢中已發出隱隱風聲。他出劍越來越快,風聲也是漸響,劍鋒上所發出的一股勁氣漸漸擴充套件,旁觀眾人只覺寒氣逼人,臉上、手上被疾風颳得隱隱生疼,不由自主的後退,圍在相鬥兩人身周的圈子漸漸擴大,竟有四五丈方圓。泰山派的一個道士在旁說道:“氣宗的徒兒劍法高,劍宗的師叔內力強,這到底怎麼搞的?華山派的氣宗、劍宗,這可不是顛倒來玩了麼?
《笑傲江湖》中的“劍宗餘孽”封不平本想仗著有嵩山派撐腰,一舉奪了華山掌門寶座。可打了半天劍法上佔不了便宜,最後只能使出“狂風快劍”,企圖以內力取勝。可見,任何高明武功若無內功心法相輔,也是徒勞無功。
說回前端,如今的前端技術棧就如同武俠小說中的江湖一樣,各門各派自成一體,可謂“百花齊放”、“百家爭鳴”。
這邊 React 、Vue 、AngularJS 、JQuery 誰還都談不上能一統江湖。“武林新貴” Flux 、Redux 、Mobx 們已經忙著爭奪誰是資料流框架老大。Native 端 RN 剛偃旗息鼓,Weex 就大有“ I'm the everywhere ”之勢。連備受爭議的 GraphQL 內部都還有 Apollo、Relay 掐來掐去。
常聽到身邊的前端工程師抱怨,上週剛釋出的 XXX 新版本文件還沒看,今天 YYY 公司又釋出了新框架,到底先學哪個?其實,無論是哪種框架哪項技術都是解決實際業務需求的手段、方法,和武林中各門各派的武功招式是一樣的,各有所長,各有各的獨到之處。
我們學習技術,除了瞭解具體使用方法,還需要掌握技術背後的設計理念和工程思想,這些背後的東西是我們技術選型的依據,是架構設計的基礎,是軟體系統的靈魂。這就好比是的武功中“內功心法”催動拳腳刀槍,一招一式,虎虎生風,縱有大敵當前,亦是淡然自若。
接下來分別談一下三種工程思想,分別是:“開閉原則”、“函數語言程式設計”和“訊息機制”,這三種工程思想在後端開發中均有廣泛的使用,容易被大家忽略的是目前很多前端技術框架也應用了這三種思想,以下結合具體案例分析,希望能夠幫助大家加深對技術本身的理解。
開閉原則
說到面向物件設計,大部分人腦海中閃過的恐怕都是“23種設計模式”。設計模式代表的是業務場景中總結出的最佳實現方式,屬於實踐的範疇,在其之上是更為重要的“SOLID”五大原則:
- Single Responsibility Principle 單一責任原則
- The Open Closed Principle 開放封閉原則
- The Liskov Substitution Principle 里氏替換原則
- The Dependency Inversion Principle 依賴倒置原則
- The Interface Segregation Principle 介面分離原則
SOLID 五大原則的出發點也是軟體工程的終極目標:“高內聚、低耦合”。在後端開發中運用最多的是“依賴倒置原則”,與其相關的設計模式大約有5-6個。如下圖所示:
上圖也可以理解為從抽象概念到具體實踐的逐步演進。
在前端技術框架中,運用最多的是“開放封閉原則”,我們先來看一下這條原則是怎麼定義的:
A software artifact should be open for extension but closed for modification.
翻譯過來就是:軟體系統應當對擴充套件開放,對修改封閉(感覺像沒說)。這裡舉一個簡單的例子來說明開閉原則,先幫助大家理解概念:
public abstract class Shape
{
public abstract double Area();
}
public class Rectangle: Shape
{
public double Width { get; set;}
public double Height { get; set;}
public override double Area()
{
return Width*Height'
}
}
public class Circle: Shape
{
public double Radius { get; set}
public override double Area()
{
return Radius*Radius*PI;
}
}
public double Area(Shape [] shapes)
{
doubel area = 0;
foreach (var shape in shapes)
{
area += shape.Area();
}
return area;
}
複製程式碼
上例中無論場景如何擴充套件,Area 函式都無需修改,每個 Shape 類通過繼承介面和多型特性,各自實現面積計算。
總結一下開閉原則就是:軟體系統的核心邏輯都不應該輕易改變,否則會破壞系統的穩定性和增加測試成本。我們應當建立合適的抽象並統一介面,當業務需要擴充套件時,我們可以通過增加實體類來完成。
接下來我們看一個“開閉原則”在前端框架中的應用: Ant Design 元件庫中的 Form 表單元件。
和其它元件不同,Form 元件並沒有具體的形態,它更像是一個容器,提供了接入的標準,並提供了校驗、表單提交等功能。繪製表單中的一項如下所示:
<FormItem>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username" />
)}
</FormItem>
複製程式碼
Ant Design 元件庫中已經提供個幾乎所有的常見表單元件,如:Select 、Checkbox 、Radio 、Cascader 等,但在實際業務中,我們還是會需要設計業務相關的表單項,Form 表單通過統一元件介面的方式滿足了這個技術需求,具體規約如下:
自定義或第三方的表單控制元件,也可以與 Form 元件一起使用。只要該元件遵循以下的約定:
- 提供受控屬性 value 或其它與 valuePropName 的值同名的屬性。
- 提供 onChange 事件或 trigger 的值同名的事件。
- 不能是函式式元件。
這正是“開閉原則”的一個典型實踐案例,即表單核心邏輯(校驗、提交等)保持不變並封裝在 Form 元件中,自定義表單項只需要滿足上述三條規約,就能平滑接入到 Form 元件中,和 Ant Design 原生元件契合在一起。
Ant Design 中的 Form 元件通過這樣一個簡潔的設計,完美提供了表單型別頁面的統一解決方案。
函數語言程式設計
隨著人工智慧、區塊鏈、AR、VR、新零售等業務場景的出現,產品介面互動正在變得越來越複雜,這就對現代的前端開發者提出了更高的要求。如何快速、正確、高效地開發出高複雜度頁面是目前前端技術最需要解決的問題。
函數語言程式設計(以下簡稱 FP )憑藉其高複用性、易測試性和與之帶來的健壯性和簡潔開始逐漸佔據前端技術圈,我們發現越來越多的前端框架以 FP 為設計核心準則。
我們先簡單介紹一下 FP,函數語言程式設計的特徵主要包括以下幾個方面:
- 函式為“一等公民”
- 模組化、組合
- 引用透明
- 避免狀態改變
- 避免共享狀態
JS 語言中的函式不同於 Java ,C/C++ 等語言, 可以被當做引數和返回值進行傳遞,因此天生具備“一等公民”特性。“模組化、組合”、“引用透明”、“避免狀態改變”、“避免共享狀態”這四個特徵都需要通過特定程式碼模式實現。先舉兩個小例子:
找出字串中率先出現的四個非數字字元?
非 FP 風格
var words = [], count = 0;
var text = str.split('');
for (var i = 0; couont < 4, i < text.length; i++) {
if(!text[i].match(/[0-9]/)) {
words = words.concat(text[i]);
count++;
}
}
複製程式碼
FP 風格
var words = str.split('').filter(function(x){
return (!x.match(/[1-9]+/))}).slice(0,4);
複製程式碼
第二段程式碼中使用的 js 陣列方法 filter 和 slice,去掉了 for 迴圈,程式碼更簡潔流暢。在寫具體業務程式碼的時候,“模組化、組合”是 FP 最常用的技術,也是最重要的實現功能的手段。
分別實現陣列所有元素相加、相乘、相與?
非 FP 風格
function plus(array) {
var res = array[0];
for (let i = 1; i < array.length; i++) {
res += array[i];
}
}
function mul(array) {
var res = array[0];
for (let i = 1; i < array.length; i++) {
res *= array[i];
}
}
function and (array) {
var res = array[0];
for (let i = 1; i < array.length; i++) {
res = res & array[i];
}
}
plus(array);
mul(array);
and(array);
複製程式碼
FP 風格
var ops = {
"plus": (x,y)=>x+y,
"mul" : (x,y)=>x*y,
"and" : (x,y)=>x&y
}
function operation(op, array) {
return array.slice(1).reduce(ops[op], array[0]);
}
operation("plus", array);
operation("mul", array);
operation("and", array);
複製程式碼
後一段程式碼中,使用了 reduce 函式代替了 for 迴圈,並將數值計算部分作為模組提取出來,當有新的計算型別時,只需要在 ops 物件中定義計算過程。這裡就體現了 FP 中“模組化、組合”的特性。在 FP 風格下,我們習慣將複雜邏輯切割成一個個小模組,通過組合這些模組實現新的業務功能,當有新的需求到來時,我們儘可能地複用已有模組達到目標。FP 程式碼在複用性方面相比 OOD 有明顯的優勢。
React 中的 FP 思想
React 框架中,當用戶操作 UI 或者 API 的返回帶來了資料的改變,React 隨即進行 virtual dom diff 計算得到 dom 的修改指令,對 dom 元素應用修改指令便得到最新的 html 介面,如下圖所示:
不難發現,React 其實是應用資料對UI的一種對映,不同的資料會映射出不同樣式的 UI 介面,我們可以得出如下的表示式:
沒錯,React 的本質其實是一種函式,並且還是符合 FP 要求的“引用透明”函式。所謂“引用透明”就是指函式的輸出僅依賴函式引數,不受任何外部環境影響。這樣的函式可測試性強,也非常容易進行組合。
在 React 的體系下,任何元件都可由一個個更小的元件構成,每個元件都只關心自己的輸入,他們不斷地接受新的資料並輸出對應的新的UI介面。React 框架中常用的“高階元件”可以看作引用透明”函式的組合模式。
在具體業務中我們通常還需要權衡 React 元件的複用性和開發體驗,如果元件被拆分的過於細,固然複用性會提升,但檔案數量會增加,對應的文件和溝通成本也會增加,這也是 FP 在實踐過程中經常遭人詬病的點,即複用性提升後帶來的額外開發成本。
訊息機制
訊息機制是軟體工程中一個普遍運用的工程思想。“設計模式”中的觀察者模式、Windows 作業系統底層、Spring 框架中的 ApplicationListener 模組、Objective-C 語言中的函式呼叫、都是通過訊息機制驅動的。
使用訊息機制最大的好處在於可以做到業務模組間安全解耦,模組間通過傳送訊息的方式進行協作,我們先舉一個後端開發中的例子,下圖是一個簡單的預定系統的建模圖,並沒有使用訊息機制:
在沒有訊息機制的情況下,使用者模組需要知道訂單模組的存在,並向起進行介面呼叫,同理訂單模組需要向支付模組進行介面呼叫。這種設計下模組間是耦合的。
我們再來看一下使用訊息機制的情況:
上圖中,無論是客戶下訂單、支付還是預定都是通過訊息的方式傳遞的,每個模組都是向一個訊息處理器起發訊息,同時也監聽訊息處理器傳送回來的訊息。在這種模式下,模組完全不知道其它模組的存在,徹底做到了解耦。
在前端業務開發中,我們經常也會用到 EventEmitter 庫來進行訊息傳遞。比如頁面上有兩塊區域,一塊用 React 框架渲染,一塊用 D3 渲染的,當兩塊區域需要資料同步時,就可以使用訊息機制進行通訊,保證頁面資料整體一致。
如果你的業務中有不同生命週期的元件,建議採用訊息機制進行管理,不僅消除了耦合,邏輯關係部分的程式碼也集中到了一個檔案中,內聚性得到了提升。
使用訊息機制的一個附屬產物就是中介軟體,我們可以為訊息定製各種中介軟體,在中間中完成一些通用邏輯,讓業務程式碼更精煉。
說到前端框架中訊息機制的運用,當然首推 Redux 框架,在 Redux 框架中,任何資料互動都需要先轉化為一個 action,由 action 去觸發 reducer 和相關的 middleware 處理 action,改變資料,最終同步到頁面 UI 上,如下圖所示:
關於使用 Redux 的種種利弊,在各大社群中都有很深入的討論,本文不再贅述。
總結
“開閉原則”、“函數語言程式設計”、“訊息機制”這個三個軟體工程中重要的思想方法好比三套內功口訣,掌握了他們,才能更深刻地理解技術框架本身,發揮出技術框架的最大威力。
寫到這裡突然又想起《天龍八部》中一段:
喬峰眼見旁人退開,驀地心念一動,呼的一拳打出,一招“衝陣斬將”,也正是“太祖長拳”中的招數。這一招姿式既瀟灑大方已極,勁力更是剛中有柔,柔中有剛,武林高手畢生所盼望達到的拳術完美之境,竟在這一招中表露無遺。
一套平平無奇的“太祖長拳”在喬峰手中盡能有如此氣象!
多少年以後,每當人們聊起金庸,聊起那個武俠世界,想必都會津津有味地回味、談論起聚賢莊中這石破天驚的一拳。
參考
- Functional Programming in JavaScript — Dan Mantyla
- Functional JavaScript: Introducing Functional Programming with Underscore.js — Michael Fogus
- Clean Architecture — Robert C·Martin
- reactjs.org
- ant-design.gitee.io/docs/react/…
- redux.js.org/
- redux-saga.js.org/
- 《笑傲江湖》— 金庸
- 《天龍八部》— 金庸
文章可隨意轉載,但請保留此 原文連結。 非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 caijun.hcj(at)alibaba-inc.com 。