在個人前兩篇文章中, 提到了解決組件擴展性的第三條路線, 有別於當下標準件封裝和 CV 大法的新思路.前端
我將在本文聚焦於這第三條路線.react
本文的在線 demo 可在此查看git
structured-react-hook 項目: git地址github
---- 前兩文章 ----web
小明是一個雄心勃勃剛剛入行滿 1 年的前端工程師, 雖然經驗很少, 可是他已經成爲了他所在的團隊的核心人物.後端
某日小明接到一個需求, 須要開發一個較爲複雜的頁面, 有多複雜(此處自行腦補, 你本身遇到過最複雜的頁面), 但這難不倒小明, 畢竟他是一個擁有包漿開光 HHKB 鍵盤的男人.服務器
花了幾天功夫, 小明已經完成了工做, 代碼可能長這樣markdown
function 我是個很複雜的組件(){
// 此處省略 2000 行代碼
}
複製代碼
提測後順利經過了測試, 發佈上線, 小明志得意滿還獲得了產品小美的讚譽.
過了幾天, 隔壁業務組新上了一個產品, 爲了搶跑市場, 抓住商機, 隔壁業務組的產品小王(和小明同樣都暗戀產品小美), 在他的產品設計里加入了小美前幾天讓小明作的那個巨複雜的頁面, 以快速知足業務需求(同時打擊情敵)
小明天然識破了情敵的詭計, 在需求評審上迅速提出了複用他以前開發的組件的想法, 然而小王恬不知恥的要求對頁面中的一些 UI 和交互作修改, 還要求加入一些定製化的流程. 年輕的小明又豈會認輸.
心想 "不就是多加幾個 API 和參數麼, 等我提煉下就是了"
因而若干天后, 小明寫下了以下代碼
function 我是個巨複雜的能夠被複用的組件(xiaoWangeDeConfig, xiaoMeiDeConfig){
let config = xiaoMeiDeConfig
if(是小王這個渣渣的產品){
config = xiaoWangDeConfig
}
// 此處省略 3000 行代碼
}
複製代碼
相比第一次, 小明此次熬掉了很多頭髮, 同時雖然沒有增長什麼需求, 可是由於各類 if 判斷, 代碼也從 2000 行增加了到了 3000 行, 好在終於把小王需求都提煉成了 config, 同時爲了知足女神小美的要求, 把小美的需求做爲默認的 config, 小明樂滋滋的看着本身的組件, 暗自得意的提測發佈, 一切如期進行, 小明再次獲得了小美的誇讚, 同時狠狠打擊了小王的氣焰.
遭遇挫折的小王非常不爽, 因而找組裏的大明(資深前端老鳥, CV 大法第三十三代嫡傳)
小王: 大明你說我怎麼提需求才能讓小明的組件維護不下去? 加班加到禿瓢? 大明: 這還不簡單, 你只要這般這般... 小王 😏 嗯嗯嗯
過了 1 周, 小王再次整理好需求找到小美, 說業務發展很快, 如今要集成下三方服務, 爭取快速打造一個商業生態, 因此要在那個巨複雜的組件裏繼續增長一些新的流程, 大明提出要小明提供一些 callback, 可以加入他們本身的邏輯.
小美問小明能不能搞, 女神親問哪有不行, 況且小明一直都對 CV 大法嗤之以鼻, 早看不慣那個 30 多歲的大明瞭, 就喜歡擺資歷, 因而一口就答應了.
因而根據小王的需求, 小明再次修改了組件
function 我是個複雜的要死了的能夠被複用被擴展的組件(xiaoWangBaDeConfig, xiaoMeiDeConfig){
let config = xiaoMeiDeConfig
if(是小王這個渣渣的產品){
config = xiaoWangBaDeConfig
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
// 此處省略 500 行代碼
if(config.callBack){
callBack()
}
}
複製代碼
熬了幾個通宵的小明終於改完了需求, 此次平均每 500 行大明就要求加一個 callback, 好讓他執行一段邏輯, 雖然小明心裏深處對這種侵入式的參數心生疑慮, 可是大明滿口保證
"放心吧, 我就只是執行如下, 絕對不幹啥!" 😏
兩眼通紅的小明精力不濟也就無暇多想, 因而就這樣提測了.
測試大壯連續測了 3 次這個組件, 雖然心情煩躁, 但仍是耐着性子測完. 此次他提了幾個 bug
"小明, 這個提測質量有點降低啊, 好幾個地方你都影響到小美原來的需求了, 要注意下"
因爲提測質量通常, 加上不像以前那樣提早完成, 小美此次沒有誇讚小明, 辛苦了一週的小明未免有些情緒低落. 但他不知道, 遠處沆瀣一氣的小王和大明一直盯着他, 露出了陰森森的笑容😏
又過了一個月, 這一個月小明一直疲於奔命, 小王和大明總有需求提過來, 特別是大明, 由於傳入了各類 callback, 每次修改都須要小明配合聯調, 時不時跑不起來就要找小明, 連小美的需求都有些耽擱了, 直到月末, 小美找到小明
"小明, 小王他們那個新產品業務作得不錯, 老闆要求咱們把你提供的那個組件和他們的交互對齊, 你改下吧, 原來咱們那些交互就去掉吧, 這個應該不難吧, 這周能搞定麼?"
⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️ 小美的話對小明猶如晴天霹靂 ⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️⚡️
小明心想 "我@#!@#, 那組件已經被我改得面目全非, 近 10000 行代碼你說刪就刪, 就是我贊成, 大壯也要瘋了"
"哦對了, 大家技術老闆上次會上說你這個組件頗有共性, 最好是要提出來讓其餘業務也能夠複用, 時間上抓點緊, 我看好你的 😁"
"我特麼..."
隨後的一週小明熬夜通宵 007double, 苦逼大壯被拉着不停的迴歸, 終於在最後一天遇上了發佈日期, 同時也掉光了最後的幾根秀髮.
以後的日子, 小明由於禿瓢被技術部的人奉爲大神, 繼續維護他那個巨巨巨複雜的組件, 由於代碼量太大, 小明也不敢重構, 就不停的往裏加參數, 不行就 CV 一下, 雖然之前嗤之以鼻, 可是小明發現 CV 能減輕很多他的維護痛苦, 尤爲是那些活不長的業務, C 一份給他們本身搞, 本身就不用這麼累, 也不會出不少問題. 同時禿瓢的他也失去了小美的歡心, 小王由於白嫖了小明的資源, 成功快速上線了一個業務, 被老闆提拔爲新的產品組長, 同時管轄小美的業務, 近水樓臺先得月, 加上一頭秀髮, 不久就和小美在一塊兒了. 大明由於協助有功也被提拔爲技術組長, 繼續依靠 CV 大法支撐各種短命業務.
全劇終
事實上這樣的劇本並非我杜撰的, 除去狗血的三角戀, 這裏的角色大多數都是我職業經歷中所遇到過的.
整個劇本最終一個 HappyEnding 都創建在這 20 年互聯網的野蠻生長曆史之上, 由於大量短命業務的存在, 前端技術在軟件工程上難有所成, 大量的前端工程師就這樣被這些短命的業務生生耗盡了精力, 不管是雄心勃勃的年輕人仍是混跡多年的老油條, 最終都成了業務的工具人, 咱們寫出的大量代碼和積累的大量業務經驗就像這些短命的快速被熱錢燃燒的業務同樣 → 毫無價值.
不管你是否對上述例子有切身的感覺, 我想咱們均可以就如今的組件封裝方式達成一些共識. 現有的封裝方式主要的問題在於
二者疊加, 加重了組件的膨脹效應, 因而組件越改越難用, 越難用越改不動.
而在面向對象軟件設計中有一個核心的設計原則 開閉原則, 對修改關閉, 對擴展開放
事實上面向對象語言中的繼承多態封裝都是基於這一原則來設計的. 但顯然, 前端使用
JavaScript 開發的組件卻違反了對修改關閉這一條原則, 面向對象是爲了解決軟件複用問題, 開閉原則更像是軟件複用的基礎. 而咱們如今採用的組件擴展倒是反其道而行, 一遇到組件擴展問題, 咱們就須要對組件內部代碼進行修改. 但咱們又有什麼辦法呢?
JavaScript 自己並無完整的類特性, 而且像 Java 這樣基於類的語言也被實踐在現代複雜軟件中有諸多問題, 畢竟後端也沒比前端好到哪去呀. 一旦類被屢次繼承, 早就寫得不知道本身姓啥了, 可是至少服務端還不用面對邏輯和設計的衝突.
因此有沒有什麼辦法在技術上在前端領域實現開閉原則, 同時避免陷入像 Java 那樣複雜的類設計體系呢? 若是能夠, 咱們是否是可以編寫出 "真 ` 易於維護和擴展的組件?"
答案是 Membrane Mode
我在最近的文章中屢次提到 Membrane Mode, 在這裏我解釋下 Membrane Mode 的含義. 事實上 Membrane Mode 是對一些語言特性的整合, 從而實現我上述提到的幾個點
要實現上述兩個目標, Membrane Mode 必須被限制爲
下面的例子爲了能更具說服性, 我將採用 structured-react-hook, 一個實現了 Membrane Mode 的基於 React Hook 的狀態管理框架來編寫示例
一個基礎的帶有基本 UI 的 Button
import React from "react";
import createStore from "structured-react-hook";
function Button() {
return (
<div> <button> 我是一個按鈕 </button> </div>
);
}
export default Button
複製代碼
讓咱們添加一個點擊切換 Loading 文案的效果 效果大概是這樣
下面是代碼
import React from "react";
import createStore from "structured-react-hook";
function query(res) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(res);
}, 1000);
});
}
const storeConfig = {
initState: {
loading: false,
text: "點我發起請求"
},
controller: {
async onButtonClick() {
this.rc.setState({
loading: true,
text: "請求服務器中..."
});
const res = await query("請求完成");
this.rc.setState({
loading: false,
text: res
});
}
}
};
const useButtonStore = createStore(storeConfig);
function Button() {
const store = useButtonStore();
return (
<div> <button disabled={store.state.loading} onClick={store.controller.onButtonClick} > {store.state.text} </button> </div>
);
}
export default Button;
複製代碼
大明說: 咱們要複用這個組件, 麻煩你請求完成的時候給個 callback, 我要加邏輯 😏
看 Membrane Mode 如何解決這個場景
Membrane Mode 利用了 AOP 的特性來實現對邏輯的切入
// 獨立的給 大明修改的文件
membrane-daming.js
const membrane = {
controller: {
async onButtonClick() {
await this.super.controller.onButtonClick();
alert("我是大明");
}
}
};
複製代碼
那小明須要在原有的 Button 組件中添加 callback 參數麼? 答案是不須要. 小明只須要對 storeConifg 增長對 大明提供的 membrane 引用便可
import membrane from 'membrane-daming'
const storeConfig = {
initState: {
loading: false,
text: "點我發起請求"
},
controller: {
async onButtonClick() {
this.rc.setState({
loading: true,
text: "請求服務器中..."
});
const res = await query("請求完成");
this.rc.setState({
loading: false,
text: res
});
}
},
membrane
};
複製代碼
擴展以後大概是這麼個效果
眼瞅小明順利解決了大明的刁難, 做爲資深老鳥的大明又豈會善罷甘休, 眼尖的他發現了一個問題
"!!等, 我剛纔說錯了, 我要在請求邏輯觸發以前彈窗詢問用戶是否要繼續請求😏"
看 Membrane Mode 如何解決這個問題
答案是, 用結構替換來代替特殊邏輯.
我記得 Google 曾經計劃發佈一款手機, 叫模塊化手機, 用戶能夠自行替換其中的模塊, 只要這個手機支持. 在建築行業, 不少大樓的改造都不須要破壞原有的建築基礎, 只須要替換其中的結構就行. 那麼代碼呢?
面對修改關閉, 就徹底不能改本身的代碼了麼? 顯然不是. 面對修改關閉, 個人理解是不要修改你原有的代碼來實現擴展, 利用函數式編程的思惟, 咱們只須要將一個大的函數編程若干小函數, 並容許擴展方替換其中的函數便可, 這樣咱們既保護了咱們本身的代碼, 同時又知足了各類細粒度的擴展需求, 讓咱們回到上面的場景
聽到大明的要求, 小明不假思索的重構了本身代碼結構, 但依然不增長任何參數, 不給大明侵入本身代碼的機會
因而小明切分了本身的代碼, 提供了新的結構, 而後讓大明擴展他想要的效果, 效果以下
代碼以下
大明的文件
const membrane = {
service: {
beforeQuery() {
const res = window.confirm("真的須要發起請求麼?(大明)");
return res;
}
},
controller: {
async onButtonClick() {
const res = this.service.beforeQuery();
if (res) {
this.super.service.beforeQuery();
this.super.service.query();
}
}
}
};
複製代碼
小明的文件
const storeConfig = {
initState: {
loading: false,
text: "點我發起請求"
},
service: {
beforeQuery() {
this.rc.setState({
loading: true,
text: "請求服務器中..."
});
},
async query() {
const res = await query("請求完成");
this.rc.setState({
loading: false,
text: res
});
}
},
controller: {
async onButtonClick() {
this.service.beforeQuery();
this.service.query();
}
},
membrane
};
複製代碼
兩個模塊之間依然只保持 membrane 的聯繫, 同時大明和小明獨自維護各自的代碼.
重點在於, 小明自始至終沒有爲大明開聽任何參數, 包括 回調, params 等, 小明要作的只是切分, 切分再切分, 就好像函數式同樣, 原子化本身的代碼, 避免了外部擴展帶來的代碼侵入和代碼膨脹問題.
可是大明會放棄麼? 大明又將提出何種尖酸刻薄的新挑戰呢, 小明又將如何應對
請看下回分解.............
Membrane Mode 吸收了過往編程範式中的一些有價值的特性, 提供了基於單次繼承下的, 函數重載, 繼承, 封裝, 和採用 AOP 使用結構替換來代替一般的參數和 API 擴展思路. 若是你對本文, 對 Membrane Mode 感興趣能夠聯繫我, 歡迎交流😏