前端架構之 React 領域驅動設計
來自前端備忘錄 - 江湖術士 [1]
https://hqwuzhaoyi.github.io/2021/01/14/74.HookDDD/
領域驅動,各自只管各自的模塊,頂層再來進行組裝和分配
-
堅持根據特性區命名目錄。
-
堅持爲每個特性區創建一個 NgModule。
能提供限界上下文,將某些功能牢牢地鎖在一個地方,開發某個功能時,只需要關心這個模塊就夠了。
視圖的歸試圖,邏輯的歸邏輯
function SomeComponent() {
const someService = useService();
return <div>{someService.state}</div>;
}
跨組件數據傳遞?
function useGlobalService() {
return { state: "" };
}
const GlobalService = createContext(null);
function SomeComponent() {
return (
<GlobalService.Provider value={useGlobalService()}></GlobalService.Provider>
);
}
function useSomeService() {
const globalService = useContext(GlobalService);
return <div>{globalService.state}</div>;
}
上下文注入節點,本身就是按照試圖來的
函數 DDD
只用函數實現 DDD,它有多優美
我們先比較一下這兩種寫法,對於一個類:
class SomeClass {
name:string,
password:string,
constructor(name,password){
this.name = name
this.password = password
}
}
const initValue = { name: "", password: "" };
function useClass() {
const [state, setState] = useState(initValue);
return { state, setState };
}
下面的自帶響應式,getter,setter 也自動給出了,同時使用了工廠模式,不需要了解函數內部的邏輯。
生命週期複用
每個 useFunc 都是 拆掉 的管道函數,框架幫你組裝,簡直就是一步到位!
效率
function useSomeService() {
const [form] = useForm();
const request = useRequest();
const model = useModel();
useEffect(() => {
form.onFieldsChange = () => {
request.run(form.getFieldsValue);
};
}, [form]);
return {
model,
form,
};
}
<Form form={someService.form}>
<Form.Item >
<!-- 沒你service啥事了!別看!這裏是純視圖 -->
</Form.Item>
</Form>
這個表單服務你想要在哪控制?
想要多個組件同時控制?
加個 token,也就是 createContext,把依賴提上去!
他特麼自然了!
React Hooks 版本架構
執行 LIFT 原則
-
頂層文件夾最多包含:assets,pages,layouts,app 四個(其中 pages,layouts 是爲了照顧某些 ssr 開發棧),名字可以變更,但是不可以有多餘文件夾,激進的話可以只有一個 app 文件夾
-
按功能劃分文件夾,每個功能只能包含以下四種文件:Xxx.less, Xxx.tsx, useXxx.ts,useXxx.spec.ts , 採用嵌套結構組織
-
一個文件夾包含該領域內所有邏輯(視圖,樣式,測試,狀態,接口),禁止將邏輯放置於文件夾以外
-
如果需要由其他功能調用,利用 SOA 反轉
爲何如此?
-
功能結構即文件結構,開發人員可以快速定位代碼,掃一眼就能知道每個文件代表什麼,目錄儘可能保持扁平,既沒有重複也沒有多餘的名字
-
當有很多文件時(例如 10 個以上),在專用目錄型結構中定位它們會比在扁平結構中更容易
-
惰性加載可路由的功能變得更容易
-
隔離、測試和複用特性更容易
-
管理上,相關領域文件夾可以分配給專人,開發效率高,可追責和計量工作量,很明顯應該禁止多人同時操作同一層級文件
-
只需要對 useXxx 進行測試,測試複雜度,工作量都很小,視圖測試交給 e2e
利用 SOA 實現跨組件邏輯複用
利用 注入令牌
+ 服務函數
+ 注入點
,實現靈活的 SOA
命名格式爲
XxxService = useToken(useXxxService)
XxxService
爲注入令牌 和文件名
useXxxService
爲服務函數
<XxxService.Provider value={useXxxService()} />
XxxService.Provider
爲注入點
注入令牌與服務函數緊挨
與注入節點處於同一文件結構層級
禁止除 SOA 以外的所有數據源
爲何如此?
-
符合單一數據,單以職責,接口隔離原則
-
通過泛型約束,可以有更加自然的 Typescript 體驗,不需要手動聲明注入數據類型,所有類型將自動獲得
-
層次化注入,可以實現 DDD,將邏輯全部約束與一處,方便團隊協作
-
當你在根注入器上提供該服務時,該服務實例在每個需要該服務的組件和服務中都是共享的。當服務要共享方法或狀態時,這是數學意義上的最理想的選擇。
-
配合組件和功能劃分,可以方便處理嵌套結構,防止對象複製被濫用,類似深複製之類的操作應該禁止
實現一個 IOC 注入令牌的方法爲
import { createContext } from 'react';
/**
* 泛型約束獲取注入令牌
*
* @export
* @template T
* @param {(...args: any[]) => T} func
* @param {(T | undefined)} [initialValue=undefined]
* @returns
*/
export default function useToken<T>(
func: (...args: any[]) => T,
initialValue: T | undefined = undefined,
) {
return createContext(initialValue as T);
}
一個典型可註冊服務爲:
import { useState } from "react";
import useToken from "./useToken";
export const AppService = useToken(useAppService);
export default function useAppService() {
// 可注入其他服務,以嵌套
// eq:
// const someOtherService = useSomeOtherService(SomeOtherService)
const [appName, setAppName] = useState("appName");
return {
appName,
setAppName,
// ...
};
}
最小權限 🐾
人爲保證代碼結構種,各個組成之間的最小權限,是一個好習慣
-
所有大寫字母開頭的 tsx 文件都是組件
-
所有 use 開頭的文件,都是服務,其中,useXxxService 是可注入服務,默認將所有組件配套的服務設置爲可注入服務,可以方便進行依賴管理
-
禁止在組件函數種出現任何非服務注入代碼,禁止在組件中寫入與視圖不想關的
-
爲複雜結構數據定義 class
-
如果可以的話,將單例服務由全局 service 組織,嵌套結構,共享實例,頁面初始化 除外
-
❌ 禁止深複製
爲何如此?
-
當邏輯被放置到服務裏,並以函數的形式暴露時,可以被多個組件重複使用
-
在單元測試時,服務裏的邏輯更容易被隔離。當組件中調用邏輯時,也很容易被模擬
-
從組件移除依賴並隱藏實現細節
-
保持組件苗條、精簡和聚焦
-
利用 class 可以減少初始化複雜度,以及因此產生的類型問題
-
局管理單例服務,可以一步消滅循環依賴問題(道理同 Redux 替代 Flux)
-
深複製有非常嚴重的性能問題,且容易產生意外變更,尤其是 useEffect 語境下
JUST USE REACT HOOKS
拋棄 class 這樣的,this 掛載變更的歷史方案,不可複用組件會污染整個項目,導致邏輯無法集中於一處,甚至出現耦合, LIFT,SOA,DDD 等架構無從談起
項目只存在
-
大寫並與文件同名的組件,且其中除了注入服務操作外,return 之前,無任何代碼
-
use 開頭並與文件夾同名的服務
-
use 開頭,Service 結尾,並與文件夾同名的可注入服務
-
服務中只存在 基礎 hooks,自定義 hooks,第三方 hooks,靜態數據,工具函數,工具類
以下爲細化闡述爲何如此設計的出發點
-
快速定位
Locate
-
一眼識別
Identify
-
儘量保持扁平結構 (Flattest)
-
嘗試
Try
遵循DRY
(Do Not Repeat Yourself
, 不重複自己)
此爲 LIFT 原則
-
優先將組件視爲元素,而並非功能邏輯單位(視圖的歸視圖,業務的歸業務)
-
隔離原則(屬於一個成員的工作,必定屬於該成員負責的文件夾,也只能屬於該成員負責的文件夾)
-
最小依賴(禁止不必要的工具使用,比如當前需求下,引入 Redux/Flux/Dva/Mobx 等工具,並沒有解決什麼問題,卻導致功能更加受限,影響隔離原則比如當兩個組件需要服務的不同實例的情況,以上工具屬於上個版本或某種特殊需求,比如前後端同構,不能影響這個版本當前需求的架構)
-
優先響應式(普及管道風格的函數式方案,大膽使用 useEffect 等 api,不提倡鬆散的函數組合,只要是視圖所用的數據,必須全部都爲響應式數據,並響應變更)
-
測試友好(邊界清晰,風格簡潔,隔離完整,即爲測試友好)
-
設計友好(支持模塊化設計)
建議的技術棧搭配
-
create-react-app + react-router-dom + antd + ahooks + styled-components (大多數場景下,強烈推薦!可以上 ProComponent,但是要注意提取功能邏輯,不可將邏輯寫於組件)
-
umi + ahooks (請刪除 models,services,components,utils 等非必要頂層文件夾,禁止使用 dva)
-
umi (ssr) + dva + ahooks(同上,但可僅基於 dva 溝通前後端和首屏數據,非 ssr 同樣禁用 dva)
-
next.js + react suite/material ui + swr(利用不到 useAntdTable 之類的功能,ahooks 就雞肋了)
Hook 使你在無需修改組件結構的情況下複用狀態邏輯
當你思維聚焦於組件時,在這種情況下,你是必須逼迫自己,在組件裏寫業務邏輯,或者重新組織業務邏輯!🙊
並且,因爲 state 是不反應業務邏輯的,它也天然不可以對業務邏輯進行組合
function useSomeTable() {
// 這個是個表單,抽象的
const [form] = Form.useForm();
// 這個是個表單聯動的表格
const { tableProps, loading } = useAntdTable(
// 自動處理分頁相關問題
({ curren, pageSize }, formData) => fetch("http://sdfdsfsdf"), // 抽象的狀態請求
{
form, // 表單在這裏與表格組合,實現聯動
defaultParams: {
// ...
},
// 很多功能都能集成,只需要一個配置項
debounceInterval: 300, // 節流
}
);
return {
form,
loading,
tableProps,
};
}
<Form form={SomeTable.form}><!--裏面全部狀態無關,不用看了--></Form>
<Table {...tableProps} columns={} rowKey="id"/>
這個組件,存粹只有注入和視圖,一丁點的邏輯都沒有
組件裏沒有邏輯, 相關的邏輯再 useFunc 種就能隨意組合,結構什麼都都能你來定,結構如果優秀,任何邏輯都是 use 個函數的問題,你不會出現需要寫第二遍邏輯的情況,通過組件的 props 進行分流(map + key),你能夠非常優雅地處理嵌套複雜結構
你能將視圖和邏輯完全組織爲一個結構,交給一個特定的人,完全不用關心他到底是怎麼開發的
這便是 —— 邏輯視圖分離👍
React SOA
基本的服務
function useSimpleService() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
useEffect(() => {
setVal2(val1);
}, [val1]);
return {
val1,
setVal1,
val2,
};
}
-
叫它 service,是 SOA 模型下的管用叫法,意思是 —— 我只會在這樣的結構種寫邏輯,組件中的邏輯全部消失(優先將組件視爲元素)
-
只暴露你需要暴露的狀態邏輯(狀態邏輯必須一起說,只做狀態複用很扯淡,畢竟 2021 年了)
-
useRef,同樣也可以封裝在 Service 中,而且建議如此做,ref 的獲取不是視圖,是邏輯
組合服務
有另外一個服務,useAnotherService
function userAnotherService() {
const [val, setVal] = useLocalstorage(0);
return { vale, setVal };
}
然後與基本服務進行組合
function useSimpleService() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const { setVal } = userAnotherService();
useEffect(() => {
setVal2(val1);
}, [val1]);
useEffect(() => {
setVal(val2);
}, [val2]);
return {
val1,
setVal1,
val2,
};
}
就能爲基本服務動態添加功能
-
爲什麼不直接
import
?因爲需要框架內的響應式能力,這個叫控制反轉,框架將響應式的控制權轉交給了開發者 -
如果有另外一個服務,單單隻要
AnotherService
的功能,你只需要調用useAnthor Service
就好了 -
最好是調用者修改被調用者,可以對比
ahooks
對代useRef
的改動,就是本着這個次序,因爲被調用者可能被多次調用,保證複用性 -
useEffect
是一種管道模型,如同rxjs
一般,只是框架幫你按順序組裝而已(你以爲爲啥非要你按順序來?),是極限的函數式方案,不存在純度問題,函數式得不要不要的。但是有個要求,依賴必須寫清楚,這個依賴是管道操作中的參數,React
將你的hook
重新組合成了管道,但是參數必須提供,在它能自動分析依賴之前 -
使用了
useAnotherService
的細節被隱藏,形成了一個樹形調用結構,這種結構被稱作 “依賴樹” 或者 “注入樹”,別盯着我,名字不是我定的
注入單例服務
當前服務如果需要被多個組件使用,服務會被初始化很多次,如何讓它只注入一次?
利用 createContext
export const SimpleService = createContext(null);
export default function useSimpleService() {
// ...
}
但是,單例需要注入到唯一節點,因此,你需要在所有需要用到這個服務的組件的最頂層:
<SimpleService.Provider value={useSimpleService()}>
{props.children}
</SimpleService.Provider>
這樣,這個服務的單例就對所有子孫組件敞開了懷抱,同時,所有子孫組件對其的修改都將生效
function SomeComponent(){
const {val1,setVal1} = useContext(SomeService)
return <div onClick={()=>{setVal1('fuck')}>val1</div>
}
-
直接在 jsx 的 provider 種 value = {useSomeService ()} 在本組件沒有任何其它響應式變量的情況下是可行的,因爲不會重新初始化,在良好的架構下 —— 組件除注入,無任何邏輯,return 之前沒有東西,同時,上下文單獨封裝組件,可以作爲 “模塊標識”
-
這個有共同單例 Service 的一系列組件,被稱爲模塊,它們有自己的 “限界上下文”,並且,視圖,邏輯,樣式都在其中,如果這個模塊是按照功能劃分的,那麼這種 SOA 實現被稱爲 領域驅動設計 (DDD) ,某些架構強推的所謂’微前端’,目的就是得到這個東西
-
一定要注意,這個模塊的上層數據變更,模塊的限界上下文會刷新,這個是默認操作,這也是爲何 jsx 直接賦值 的原因,如果你不需要這個東西,可以採用 const value = useService () 包裹,或者直接 memo 這個模塊標識組件
單例服務,解決深層嵌套對象問題
深層嵌套對象怎麼處理?useReducer?immutable? 還是直接深複製?
你首先明白你要實現什麼邏輯,深層嵌套對象之所以難處理,是因爲你想在子組件實現 對深層目標的部分變更邏輯
之前你之所以有這些奇奇怪怪工具甚至深複製的需求,是因爲你沒有辦法將邏輯也拆分給子組件,明白爲什麼如此
現在,邏輯可以拆分複用:
function useSomeService() {
const [value, setValue] = useState({
username: "",
password: "",
info: {
nickname: "",
others: [],
},
});
return { value, setValue };
}
// 注入部分省略...
修改 info:
setValue((res) => {
res.info.nickname === "fuck";
return res;
});
配合 map 修改數組:
// 分形部分:
new Array(5).map((_, key) => <SomeCompo index={key} key={key} />);
// 組件
function SomeComponent(props) {
const { setValue } = useContext(SomeService);
return (
<div
onClick={() => {
setValue((res) => {
res.info.others[props.index] = "fuck";
return res;
});
}}
></div>
);
}
如果需要劃分模塊,通過 getter,setter 傳遞這個嵌套結構:
function subInjectedService() {
const { value, setValue } = useContext(SomeService);
const info = useMemo(() => value.info.others, [value]);
const setInfo = useCallback((val) => {
setValue((res) => {
res.info.others[props.index] = val;
return res;
});
}, []);
return {
info,
setInfo,
};
}
// 忽略注入部分...
這樣的話,這個重新劃分的模塊內部,想要修改上層的數據,只需要通過 info,setInfo 即可
-
不用擔心純度和不變性的問題,因爲 hooks 都是純的,沒有不純的情況
-
全局副作用是狀態 + 函數全局邏輯封裝(分層)考慮的問題,將函數和組件,視圖功能邏輯樣式全部作爲模塊,副作用是以模塊爲單位的,而 info 和 setInfo 的 getter,setter 封裝,叫做 —— 模塊間通訊
-
useReducer 只涉及調試,也就是有個 action 名字方便你定位問題,模塊劃分如果足夠細,你根本不需要這個 action 來記錄你的變更,採用 useReducer 與 DDD 原則背離,但是也不會禁止。不過,全局 useReducer 必須明令禁止,這種方式是個災難,useReducer 必須是以模塊爲單位,不能更小,也不能更大
-
組件和服務一起,處理一部分數據,保證了單例修改,不變性也不用擔心,hooks 來保證這個
-
在這裏,你會發現 props 的功能好像只有’分形’,也就是 map 種將數據的標識傳遞給子組件,是的 —— 優先使用服務共享狀態邏輯
-
getter,setter 叫做響應式,如果你不需要響應式修改,setter 可以刪除,但是 getter 同時還有防止重新渲染的作用,保留即可,除非純組件
服務獲取時的類型問題
如果你使用的是 Typescript ,那麼,用泛型約束獲得自動類型推斷,會讓你如虎添翼
import { createContext } from 'react';
/**
* 泛型約束獲取注入令牌
*
* @export
* @template T
* @param {(...args: any[]) => T} func
* @param {(T | undefined)} [initialValue=undefined]
* @returns
*/
export default function useToken<T>(
func: (...args: any[]) => T,
initialValue: T | undefined = undefined,
) {
return createContext(initialValue as T);
}
然後將 createContext()
改爲 useToken(SomeService)
即可,這樣你就擁有了指哪打哪的類型支持,無需單獨的類型聲明,代碼更加清爽
-
如果是
Javascript
環境,建議老老實實寫createContext
的defaultValue
,雖然注入之後,子孫組件都不會出現defaultValue
,但是javascript
語境下有代碼提示 -
不建議
typescript
下聲明defaultValue
,因爲模塊外的服務調用,應該被禁止,這是DDD
架構的基礎,如果你想要在外部使用單例服務 —— 請將其提升至外部
頂層注入服務
平凡提升模塊服務層級,可能會產生循環依賴,而且會影響模塊的封裝度,因此:
⚠️優先思考清楚自己應用的模塊關係!
循環依賴產生根源是功能領域,功能模塊劃分有問題,優先解決根本問題,而不是轉移矛盾。如果你實在思考不清楚,又想要立刻開始開發,那麼可以嘗試頂層注入服務:
function useAppService(){
return {
someService:useSomeService()
anotherService:useAnotherService()
}
}
-
模塊間進行嵌套組合將變得無比困難,不再是一個 getter,setter 能夠搞定的,如果不是絕對的必要,儘量不要採用此種方式!它有悖於 DDD 原則 —— 分治
-
多組件共享不同實例將徹底失敗,這不是你願意看到的
可選服務
模塊服務劃分的另一個巨大優勢,就是將邏輯變爲可選項,這在重型應用中,幾乎就是採用 DDD 的關鍵
function useServiceByOneLogic() {
return {
activated,
// ...
};
}
function useServiceByAnotherLogic() {
return {
activated,
// ...
};
}
function useSomeService() {
const [...servicList] = [useServiceByOneLogic(), useServiceByAnotherLogic()];
// 選擇激活的服務
const usedService = useMemo(() => {
for (let service of serviceList) {
if (service.activated === true) {
return service;
}
}
}, [serviceList]);
return service;
}
// 注入過程省略...
-
你也可以通過各種條件篩選服務,這種方式是在前端實現的高可用
-
⚠ 注意,服務最好只是內部實現不同,接口應該儘可能相同,否者會出現可選類型
-
最典型的應用,就是多家雲服務廠商的短信驗證(驗證碼,人機校驗等),通過可選服務根據用戶網絡情況進行篩選,用最適合當前用戶的那一個
-
還有一個非常有意思的方案,通過服務來做數據 mock,因爲服務直接對接視圖,你只需要模擬視圖數據即可,提供兩個服務,一個真實服務,一個 mock 服務,這樣是用真實數據還是 mock 數據,都是服務自動判斷的,對你來說沒有流程差別
樣式封裝
注意,模塊是包含了樣式的,上文在講述邏輯和視圖的封裝,接下來說說樣式
-
典型的 cssModule, styled-components 之類的方案
-
shadowDom,仿真樣式(Angular 原生支持,React 可以用 cssModule 之類工具間接實現),可以實現跨技術棧樣式封裝(沒錯,所謂 ‘微前端’ 的樣式封裝)
-
樣式最好只包含排版,企業 vis 統一性是標準,沒有必要違背這個
繼續分析 SOA
從上一篇文章的例子可以看出什麼呢?
首先,按照功能領域劃分文件,可以很快分析出應用的邏輯結構
也就是邏輯可讀性更強,這個可讀性不只是針對用戶的,還有針對軟件的
比如,TodoService
和 TableHandlerService
有什麼關係?
useTableHandlerService
useTableHandlerService
useTodoService
這些邏輯關係,僅僅依靠相關工具就能定位,並生成圖形,輔助你分析領域間的關係
誰依賴誰,一目瞭然 —— 比如 有個 useState
的值 依賴 useLocalStorageState
,肉眼看起來比較困難,但是在圖中一目瞭然
只是,不具名這一點有點神煩!
還有,React
內部因爲沒有管理好這個部分傳遞,沒辦法像 Angular
一樣,瞬間生成一大堆密密麻麻的依賴樹,這就給 React
在大項目工程化上帶來了阻礙
不過一般項目做不到那麼大,領域驅動可以幫助你做到 Angular
項目極限的 95%,剩下那 5%,也只是稍稍痛苦些而已,並且,沒有辦法給管理者看到完整藍圖
不過就國內目前前端技術管理者和產品經理的水品,你給他們看 uml 藍圖,我擔心他們也看不懂,所以這部分不用太在意,感覺有地方依賴拿不準,只顯示這個領域的藍圖就好
其次,測試邊界清晰,且易於模擬
視圖你不用測試,因爲沒有視圖邏輯,什麼時候需要視圖測試?比如 Form
和 FormItem
等出現嵌套注入的地方,需要進行視圖測試,這部分耦合出現的概率非常小,大部分都是第三方框架的工作
你只需要測試這些 useFunction
就好,並且提供兩個個框,比如空組件直接 use
,嵌套組件先 provide
再 useContext
,然後直接只模擬 useFunction
邊界,並提供測試,大家可以嘗試一下,以前覺得測試神煩,現在可以試試在清晰領域邊界下,測試可以有多愉悅
最後
誰再提狀態管理我和誰急!
你看看這個應用,哪裏有狀態管理插手的地方?任何狀態管理庫都不行,它是上個時代的遮羞布
服務間通訊結構
全局單一服務(類 Redux
方案)
但是,單一服務是不得已而爲之,老版本沒有邏輯複用導致的
在這種方式下,你的調試將變得無比複雜,任何一處變更將牽扯所有本該封裝爲模塊的組件
所以必須配合相應的調試工具
所有多人協作項目,採用此種方式,最後的結果只有項目不可維護一條路!
中臺 + 其他服務(雙層結構)
由一個,appService
提供基礎服務,並管理服務間的調度,此種方式比第一種要好很多,但是還是有個問題,頂層處理服務關係,永遠比服務間處理服務關係來的複雜,具體問題詳見上文 “頂層注入”
樹形結構模塊
這是理論最優的結構,它的優勢不再贅述,上文有提到
劣勢有一個:
跨模塊層級的變更,容易形成循環依賴(也不叫劣勢,因爲此種變更對於其他方式來說,是災難)
理清自己的業務邏輯,有必要劃出功能結構圖,再開始開發,是個好習慣,同時,功能層級發生改變,應該敏銳意識到,及時提升服務的模塊層級即可
編程範式
首先,編程範式除了實現方式不同以外,其區別的根源在於 – 關注點不同
-
函數的關注點在於 —— 變化
-
面向對象的關注點在於 —— 結構
對於函數,因爲結構方便於處理變化,即輸入輸出是天然關注點,所以 ——
管理狀態
和副作用
很重要
js
var a = 1;
function test(c) {
var b = 2;
a = 2;
var a = 3;
c = 1;
return { a, b, c };
}
這裏故意用 var 來聲明變量,讓大家又更深的體會
在函數中變更函數外的變量 —— 破壞了函數的封裝性🚫
這種破壞極其危險,比如上例,如果其他函數修改了 a,在 重新 賦值之前,你知道 a 是多少麼?如果函數很長,你如何確定本地變量 a 是否覆蓋外部變量?
無封裝的函數,不可能有可裝配性
和可調試性
所以,使用函數封裝邏輯,不能引入任何副作用!注意,這個是強制的,在任何多人協作,多模塊多資源的項目中 ——
封裝是第一要務,更是基本要求❗
所以,你必須將數據(或者說狀態)全部包裹在函數內部,不可以在函數內修改任何函數以外的數據!
所以,函數天然存在一個缺點 —— 封裝性需要人爲保證(即你需要自己要求自己,寫出無副作用函數)
當然,還存在很多優點 —— 只需要針對輸入輸出測試,更加符合物體實際運行情況(變化是哲學基礎)
這部分沒有加重點符號,是因爲它不重要 —— 對一個思想方法提優缺點,只有指導意義,因爲思想方法可以綜合運用,不受限制
再來看看面相對象,來看看類結構:
class Test {
a = 1;
b = 2;
c = 3;
constructor() {
this.changeA();
}
changeA() {
this.a = 2;
}
}
這個結構一眼看去就具有 —— 自解釋性,自封裝性
還有一個涉及應用領域的優勢 —— 對觀念系統的模擬 —— 這個詞不打着重符,不需要太關心,翻譯過來就是,可以直譯人腦中的觀念(動物,人,車等等)
但它也有非常嚴重的問題 —— 初始化,自解耦麻煩,組合麻煩
需要運用到大量的’構建’,’運行’設計模式!
對的,設計模式的那些名字就是怎麼來的
其實,你仔細一想,就能明白爲什麼會這樣 ——
如果你關注變化,想要對真實世界直接模擬,你就需要處理靜態數據,需要自己對一個領域進行人爲解釋
如果你關注結構,想要對人的觀念進行模擬,你就需要處理運行時問題,需要自己處理一個運行時對象的生成問題
魚與熊掌,不可兼得,按住了這頭,那頭就會翹起來,你按住了那頭,這頭就會翹起來
想要只通過一個編程範式解決所有問題,就像用手去抓沙子,最後你什麼都得不到
極限的函數式,面向對象
通過函數和對象(注意是對象,類是抽象的,觀念中的對象)的分析,很容易發現他們的優勢
函數 —— 測試簡單,模擬真實(效率高)
對象 —— 自封裝性,模擬觀念(繼承多態)
將兩者發揚光大,更加極限地使用,你會得到以下衍生範式:
管道 / 流
既然函數只需要對輸入輸出進行測試,那麼,我將無數函數用函數串聯起來,就形成了只有統一輸入輸出的結構
聽不懂?換個說法 ——
只需要 e2e 測試,不需要單元測試!
如果我加上類型校驗,就可以構造出 —— 理想無 bug 系統
這樣的話,你就只剩調試,沒有測試(如果頂層加個校驗取代 e2e 的話)
而且,還有模式識別,異步親和性等很多好處,甚至可以自建設計語言(比如麻省老教材《如何設計計算機語言》就是以 lisp 作爲標準)
在 js 中, Cycle.js 和 Rxjs 就是極限的管道風格函數式,還有大家熟悉並且討厭的 Node.js 的 Stream 也是如此,即便他是用 類 實現的,骨子裏也是濃濃的函數式
分析一下這樣的系統,你會發現 ——
它首先關注底層邏輯 —— 也就是 or/c , is-a,and/c,not/c 這樣的函數,最後再組裝
按照範疇學的語言(就是函數式的數學解釋,不想看這個可以不看,只是補充說明):
範疇學
u of i2,i2 of g 的講法,與它的真實運行方向,是相反的!
函數的組合方式,與開發目標的構建方式,也是相反的!
它的構建方法叫做 —— 自底向上
這也是爲啥你在很多 JS 的庫中發現了好多零零碎碎的東西,還有爲何會有 lodash,ramda 等粒度非常小的庫了
在極限函數式編程下 ——
我先做出來,再看能幹什麼,比先確定幹什麼,再做,更重要!
因爲這部分,可以第三方甚至官方自己提供!
所以,函數式是庫的第一優先級構建範式!因爲作爲庫的提供者,你根本不可能預測用戶會用這個庫來幹什麼
領域模塊
函數式可以將其優勢通過管道發揮到極致,面向對象一樣可以將其優勢發揮到極致,這便是領域模塊
領域,就是一系列相同目的,相同功能的資源的集合
比如,學校,公司,這兩個類,如果分別封裝了大量的其他類以及相關資源,共同構成一個整體,自行管理,自行測試,甚至自行構建發佈,對外提供統一的接口,那這就是領域
這麼說,如果實現了一個類和其相關資源的自行管理,自行測試,這就是 —— DDD
如果實現了對其的自行構建發佈,這就是 —— 微服務
這種模型給了應用規模化的能力 —— 橫向,縱向擴展能力
還有高可用,即類的組合間的鬆散耦合範式
對於這樣的範式,你首先思考的是 —— 你要做什麼!
這就是 ——
** 這種模型給了應用規模化的能力 —— 橫向,縱向擴展能力
還有高可用,即類的組合間的鬆散耦合範式
對於這樣的範式,你首先思考的是 —— 你要做什麼!
這就是 —— 自頂向下
-
我要做什麼應用?
-
這個應用有哪些功能?
-
我該怎麼組織我的資源和代碼?
-
該怎麼和其他職能合作?
-
工期需要多久?
現實告訴你,單用任何一種都不行
開發過程中,不止有自底向上封裝的工具,還有自頂向下設計的結構
產品經理不會把要用多少個 isObject 判斷告訴你,他只會告訴你應用有哪些功能
同理,再豐富細緻的功能劃分,沒有底層一個個工具函數,也完成不了任何工作
這個世界的確處在變化之中,世界的本質就是變化,就是函數,但是軟件是交給人用,交給人開發的
觀念系統和實際運行,缺一不可!
凡是動不動就跟你說某某框架函數式,某某應用要用函數式開發的人,大多都學藝不精,根本沒有理解這些概念的本質
人類編程歷史如此久遠,真正的面向用戶的純粹函數式無 bug 系統,還沒有出現過……
當然,其在人工智能,科研等領域有無可替代的作用。不過,是人,就有組織,有公司,進而有職能劃分,大家只會通過觀念系統進行交流 —— 你所說的每一個詞彙,都是觀念,都是類!
React 提倡函數式
class OOStyle {
name: string;
password: string;
constructor() {}
get nameStr() {}
changePassword() {}
}
function OOStyleFactory() {
return new OOStyle(/* ... */);
}
這是面向對象風格的寫法(注意,只是風格,不是指只有這個是面向對象)
function funcStyle(name, password) {
return {
name,
password,
getName() {},
changePassword() {},
};
}
這個是函數風格的寫法(注意,這只是風格,這同時也是面向對象)
這兩種風格的邏輯是一樣的,唯一的區別,只在於可讀性
不要理解錯,這裏的可讀性,還包括對於程序而言的可讀性,即:
自動生成文檔,自動生成代碼結構
或者由產品設計直接導出代碼框架等功能
但是函數風格犧牲了可讀性,得到了靈活性這一點,也是值得考慮的
編程其實是個權衡過程,對於我來說,我願意
-
在處理複雜結構時使用 面向對象 風格
-
在處理複雜邏輯時,使用 函數 風格
各取所長,纔是最佳方案!
Redux
// redux reducer
function todoApp(state, action) {
if (typeof state === "undefined") {
return initialState;
}
// 這裏暫不處理任何 action,
// 僅返回傳入的 state。
return state;
}
這其實就是用函數風格實現的 面向對象 封裝,沒有這一步,你無法進行頂層設計!
用類的寫法來轉換一下 redux
的寫法:
class MonistRedux {
// initial,想要不變性可以將 name,password 組合爲 state
name = "";
password = "";
// 惰性初始化(配合工廠)
constructor() {
this.name = "";
this.password = "";
}
// action
changeName() {}
}
只有 函數 的封裝性才受副作用限制
注意這一點,React 程序員非常容易犯的錯誤,就是到了 class 裏面還在想純度的問題,恨不得將每個成員函數都變成純函數
沒必要以詞害意,需要融匯貫通
同樣,以上例子也說明,如果你的技術棧提供直接生成對象的方案 —— 你可以只用函數直接完成面向對象和函數式的設計
function ImAClass() {
return {
// ...
};
}
我就說這個是類!爲什麼不行?
他要成員變量有成員變量,要成員函數有成員函數,封裝,多態,哪個特性沒有?
什麼?繼承?這年頭還有搞面向對象的提繼承?組合優於繼承是常識!
拋棄了繼承,你需要 this
麼?你不需要,本來你就不需要 this
(除了裝飾器等附加邏輯,但是函數本身就能夠實現附加邏輯 —— 高階函數)
同樣,你也可以綜合面向對象和函數式的特點,各取所長,對你的項目進行頂層構建和底層實現
這也是很方便的
Hooks,Composition,ngModule
我們來看看上面的那個函數風格的類
像不像什麼東西?
function useThisClass() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
useEffect(() => {}, []);
const otherObject = useOtherClass();
return { val1, setVal1, val2, setVal2, otherObject };
}
Hooks 恭喜各位,用得開心!
以 React 爲例,老一代的 React 在 組件結構上是管道,也就是單向數據流,但是對於我們這些使用者來說,我們寫的邏輯,基本上是放養狀態,根本沒有接入 React 的體系,完全遊離在函數式風格以外:
老一代的 React
換句話來說,只有 React 寫的代碼才叫函數式風格,你寫的頂多叫函數!
你沒有辦法把邏輯寫在 React 的組件之外,注意,是完全沒有辦法!
好辦,我邏輯全部寫在頂層組件,那不就行了?
新一代的 React
(其中 s/a
指的是 state
,action
)
爲什麼要有 state
,action
?
爲什麼要在每個組件裏用 s/a
?
action
其實是用命令模式,將邏輯複寫爲狀態,以便 Context
傳遞,爲何?
因爲生命週期在組件裏,setState
在組件的 this
上
換句話說,框架沒有提供給你,將用戶代碼附加於框架之上的能力!
這個能力,叫做 IOC
控制反轉,即 框架將功能控制權移交到你的手上
不要把這個類 Redux
開發模式作爲最自然的開發方式,否則你會非常痛苦!
只有集成度不高的系統,才需要中介模式,才需要 MVC
之前的 React/Vue
集成度不高,沒有 Redux
作爲中介者 Controller
,你無法將用戶態代碼在架構層級和 React/Vue
產生聯繫,並且這個層級天然應該用領域模塊的思想方法來處理問題
因爲框架沒有這個能力,所以你才需要這些工具
所謂的狀態管理,所謂的單一 Store
,都是沒有 IOC
的妥協之舉,並且是在完全拋棄面向對象思想的基礎上強行用函數式語言解釋的後果,是一種畸形的技術產物,是框架未達到最終形態之前的臨時方案,不要作爲核心技術去學習,這些東西不是核心技術!
回頭看看 React
那些曖昧的話語,有些值得玩味:
-
Hook
使你在無需修改組件結構的情況下複用狀態邏輯 (注意!是狀態邏輯,不是狀態,是狀態邏輯一起復用,不是狀態複用) -
我們推薦用 自定義
hooks
探索更多可能 -
提供漸進式策略,提供
useReducer
實現大對象操作(好的領域封裝哪來的操作大對象?)
他決口不提 面向對象,領域驅動,和之前的設計失誤,是因爲他需要顧及影響和社區生態,但是使用者不要被這些欺騙!
當你有了 hooks
,Composition
,Service/Module
的時候,你應該主動拋棄所有類似
-
狀態管理
-
一定要寫純函數
-
副作用要一起處理
-
一定要保證不變形
這之類的所有言論,因爲在你手上,不僅僅只有函數式一件武器
你還有面向對象和領域驅動
用領域驅動解決高層級問題,用函數式解決低層級問題,纔是最佳開發範式
也就是說,函數式和麪向對象,沒有好壞,他們只是兩個關注點不同的思想方法而已
你要是被這種思想方法影響,眼裏只有對錯 ——
實際上是被忽悠了
管道風格的函數式(unidirectional network 單項數據流)
這是函數式語言基本特性,將一個個符合封裝要求的函數串聯起來,你就能得到統一輸入輸出
函數將淪爲算式,單測作用將消失,理想無 bug 系統呼之欲出
它會形如以下結構:
func1(func2(), func3(func4(startParams)));
或者:
func1().func2().func3().func4();
看到這些形態,大家會首先想到什麼?
沒錯 jsx
就是第一種結構(是的,jsx 是正宗函數式,純粹無副作用,無需測試,僅需輸入輸出校驗調試)
也沒錯,promise.then().then()
就是第二種,將函數式處理併發,異步的優勢發揮了出來
那第二種和第一種,有什麼區別呢?
區別就是 ——
調度
函數是運行時的結構,如果沒有利用模式匹配,每次函數執行只有一個結果,那麼整個串行函數管道的返回也只會有一個結果
如果利用了呢?它將會向路牌一樣,指示着邏輯倒流到特定的 lambda
函數中,形成分形結構
請注意,這個分形結構不只是空間上的,還有時間上的
這,就是調度!!!
說的很懸奧,大家領會一下意思就可以了,如果想要了解更多,直接去成熟工具處瞭解,他們有更多詳細的說明:
微軟出品的 ReactiveX[2] (脫胎於 linq
),就是這方面的集大成者,網絡上有非常多的講解,還有視頻演示
Hooks api 就是 React 的調度控制權
useState 整個單項數據流的調度發起者
React 將它的調度控制權,拱手交到了你的手上,這就是 hooks,它的意義絕對不僅僅只是 “在函數式組件中修改狀態” 那麼簡單
useState,加上 useReducer (個人不推薦,但是與 useState 返回類型一致)
屬於:響應式對象(注意,這裏的對象是 subject,不是 object,有人能幫忙翻譯一下麼?)
dispatch 是整個應用開始渲染的根源
沒錯,this.setState 也有同樣功能(但是僅僅是組件內)
useEffect 整個單項數據流調度的指揮者
useEffect 是分形指示器
在 Rxjs 中 被稱作 操作函數(operational function),將你的某部分變更,衍射到另一處變更
在這個 api 中,大量模式匹配得以正常工作
useMemo 整個單項數據流調度的控制者
最後,useMemo
它能夠通過只判斷響應值是否改變,而輸出控制
當然,你可以用 if 語句在 useEffect 中判斷是否改變來實現,但是 —— 模式匹配就是爲了不寫 if 啊~
單獨提出 useMemo,是爲了將 設計部分 和 運行時調度控制 部分分離,即靜態和動態分離
調度永遠是你真正開始寫函數式代碼,最應該考慮的東西,它是精華
糾結什麼是什麼並不重要,即便 useMemo = useEffect + if + useState
,這些也不是你用這部分 api 的時候應該考慮的問題
最後
你明白這些,再加上 hooks 書寫時的要求:
不要在循環,條件或嵌套函數中調用 Hook,確保總是在你的 React 函數的最頂層調用他們。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調用。這讓 React 能夠在多次的useState和useEffect調用之間保持 hook 狀態的正確。
你就能明白,爲什麼很多人說 React hooks
和 Rx
是同一個東西了
但是請注意,React 只是將它自己的調度控制權交給你,你若是自己再用 rx 等調度框架,且不說 requestAnimationFrame 的調度頻道React佔了,兩個調度機制,彌合起來會非常麻煩,小心出現不可調式的大bug
函數式在處理業務邏輯上,有着非常恐怖的鋒利程度,學好了百利而無一害
但是請注意,函數式作爲對計算過程的一般抽象,只在組件服務層級以下發揮作用,用它處理通訊,算法,異步,併發都是上上之選
但是在軟件架構層面,函數式沒有任何優勢,組件層級以上,永遠用面向對象的思維審視,是個非常好的習慣~
組件明明就只是邏輯和狀態(服務)調配的地方,它壓根就不應該有邏輯
把邏輯放到服務裏
-
堅持在組件中只包含與視圖相關的邏輯。所有其它邏輯都應該放到服務中。
-
堅持把可複用的邏輯放到服務中,保持組件簡單,聚焦於它們預期目的。
-
爲何?當邏輯被放置到服務裏,並以函數的形式暴露時,可以被多個組件重複使用。
-
爲何?在單元測試時,服務裏的邏輯更容易被隔離。當組件中調用邏輯時,也很容易被模擬。
-
爲何?從組件移除依賴並隱藏實現細節。
-
爲何?保持組件苗條、精簡和聚焦。
React DDD 下如何處理類型問題?
泛型約束 InjectionToken
/**
* 泛型約束,對注入數據的類型推斷支持
*
* @export
* @template T
* @param {(...args: any) => T} useFunc
* @param {(T | undefined)} [initialData=undefined]
* @returns
*/
export default function getServiceToken<T>(
useFunc: (...args: any) => T,
initialData: T | undefined = undefined
) {
return createContext(initialData as T);
}
這樣,你的 useContext
就能有完整類型支持了
虛擬數據泛型約束
/**
* 獲得虛擬的服務數據
*
* @export
* @template T
* @param {(...args: any) => T} useFunc
* @param {(T | undefined)} [initialData=undefined]
* @returns
*/
export function getMockService<T>(
useFunc: (...args: any) => T,
initialData: T | undefined = undefined
) {
return initialData as T;
}
類服務
類服務在功能上,其實和函數服務是一樣的
函數返回對象,本身也是構造方式之一,屬於 js 特色 (返回數組的話,本質也是對象)
所以功能上來說,函數服務完全可以覆蓋類服務,實現面向對象
但是,需求可不止有功能一說:
-
需要有更好自解釋性
-
需要更好自封裝性
-
需要更好的可讀性(自動生成文檔,自動分析)
類型自動化:
function getClassContext<T>(constructor: new (...args: any) => T) {
return createContext((undefined as unknown) as T);
}
實現一個類
/**
* some service
*
* @export
* @class SomeService
*/
export class SomeService {
static Context = getClassContext(SomeService);
name: string;
setName: Dispatch<SetStateAction<string>>;
constructor() {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [name, setName] = useState("");
this.name = name;
this.setName = setName;
}
}
使用這個類
最後,自動分析類的結構:
可自動文檔化,在配合其他工具實現框架外需求時,能夠帶給你方便的使用體驗
但是注意,目前官方是禁止在 class 中使用 hooks 的
需要禁用提示
同時,需要保證在 constructor 中使用 hooks
React DDD 下的一些常見問題 FAQ
React DDD 下,class 風格組件能用麼?
最好不要用,因爲 class 風格組件的邏輯無法提取,無法連接到統一的服務注入樹中,因此會破壞應用的單一數據原則,破壞封裝複用性
所以儘量不要使用,至於目前 getSnapshotBeforeUpdate
,getDerivedStateFromError
和 componentDidCatch
的等價寫法 Hooks
尚未加入,這其實並不是大問題,因爲在管道風格中,錯誤優先表徵爲狀態,比如 useRequest
中的 error
React DDD 是相比 之前的 類 redux 分層是更簡單還是更難?
簡單太多了,整個 React DDD
的體系只需要 useXxx 表示服務 Xxx 表示組件,只需要用幾個 hooks api,通過注入提供上下文,不需要高階組件,好的模式也不需要 render props,更不需要太過重視性能調優(別擔心性能問題,除了高消耗運算惰性加載以外),你只是無法在組件層級處理錯誤而已,個人認爲,錯誤還是在用戶端處理吧
尤其是在 Typescript 下,你代碼的幾乎任何一處都有完整的類型提示,不需要你去附加類型聲明或者指定 interface
但是,它會將業務問題提前暴露,在沒有想好業務邏輯關係的情況下,請不要下手寫代碼,但是這也不是它的缺點,因爲在邏輯錯亂的情況下,分層直接會變得不可維護
React DDD 需要用到什麼工具麼?
不需要,直接使用 React hooks
就好,沒有其他工具需求
React DDD 性能會有問題麼?
不會,React DDD 的方案性能比 class 風格組件 + 類 redux 分層要強得多,而且你可以精細化控制組件的調度和響應式,下限比 redux 上限還要高,上限幾乎可以摸到框架極限
React DDD 適合大體量項目麼?
是的,從最小體量項目,到超大體量項目,React DDD 都很適合,原因在於迴歸面向對象,承認面向對象對頂層設計有優勢的同時,業務邏輯採用極限函數式開發
無論在架構上,還是業務邏輯實現上,都會將效率,可複用性,封裝度,集成度,可調式,可測試性直接拉滿,所以不用擔心
React DDD 效率高麼?
它不是高不高的問題,它是直接可以將大部分業務效率直接提高十倍的大殺器,而且還有很多第三方庫可以被直接使用,讓第三方幫你處理邏輯,比如 ahooks,swr 等等
於此同時,它直接跟業界主流工程化模式對接,有領域模塊的加持,多人協作將變得更加有效率,也能形成特別多的技術資產
Redux 之類的工具還有意義麼?
沒有意義了,它只是解決框架沒有 IOC 情況下,保持和框架相同的單向數據流,保持用戶態代碼的脫耦而已,由於狀態分散不易測試,提供一個切面給你調試而已
這種方案相當於強制在前端封層,相當不合理,同時 typescript 支持還很差
在框架有 IOC 的情況下,用戶代碼的狀態邏輯實際上形成了一個和組件結構統一的樹,稱之爲邏輯樹或者注入樹,依賴樹,很自然地與組件相統一,很自然地保證單向數據流和一致性
所以,Redux 之類的工具最好不要用,妄圖在應用頂層一個服務解決問題的方法,都很傻
現有項目能直接改成 React DDD 麼?
這是它最大的缺點,不能!
因爲問題的根源出在框架上,IOC 應該是框架的大變更,個人認爲 React 應該直接暴力更新,摒棄所有老舊寫法,如同 15 年的 Angular 一樣,雖然有陣痛,但是對提升社區的好處大於壞處,當然,這是沒有考慮市場的想法
如果你想更純粹使用 React DDD,最好還是採取重構的方案
React DDD 下,應該怎麼劃分文件結構?
按照功能劃分,你的功能有哪些包含關係,你的文件結構就是如何
你的功能在哪個範圍需要提供限界上下文,哪裏就進行服務注入
所以類似拆分 store,action,models 之類的文件夾,就不要有了,前端沒有數據庫,即便有,也沒有到需要抽象 dao 的程度,即便抽象 dao,dao 本身也是不符合工程化和 DDD 的
所以微前端可以由 React DDD 實現是麼?
是的,有了限界上下文,分開開發,分開測試,分開部署都可以實現
但是一定要在相同框架內,個人認爲前端採用不同框架開發是個僞需求
現如今 Angular
,React
,Vue
,都有 IOC
,寫法都可以互通
你要講模塊剝離,直接構建的時候把模塊文件夾 cp
到指定目錄,覆蓋掉佔位的文件夾即可
不過注意,‘微應用’需要有模擬頂層 context
的可選服務,當然,這些東西不管你怎麼實現都是需要的
至於說重構兼容老代碼而採用 shadowDom
和 iframe
的,我只能說,作爲終端開發,重構兼容老代碼只是臨時狀態,他不能作爲架構,更不能作爲一個技術來推廣
React DDD 和 Angular 的架構好像,爲什麼?
因爲 Angular 15 年首先實現了 IOC,組件中推薦不要寫邏輯,也是 Angular 最早提出的方案
service
(狀態邏輯單元),module
(service+component+template+css+staticAssets…),領域模塊封裝也是 Angular
最早提出的,但是因爲當時它走太快,社區沒跟上,生命週期複用也麻煩,因爲採用裝飾器,組件還保留了一個殼,推進最佳實踐的難度比較大
而 React
的 hooks
可以更加抽象,也更簡單直接,直接就是兩個函數,服務注入也是通過組件,也就是強制與組件保持一致
這時候再推動 DDD
就非常容易且水到渠成了
但是 Angular
的很多特性 React
還不支持,比如組件樣式封裝,多語言依賴到視圖,服務搖樹,動態組件搖樹,異步服務(suspense
,concurrent
還在試驗階段),還有真正解決性能問題的大殺器 platform-webworker
(能夠在應用層級支持瀏覽器高刷)
但是這些需求,在沒有超大體量(世界級應用)下,用到的可能性很小,不妨礙 React
的普適性
而且 React 社區更活躍,管道風格函數是對社區的依賴是很強的,這方面 Angular
幹寫 rxjs
管道有些磨人
React DDD 會是未來趨勢麼?
這點我覺得毫無疑問,因爲 DDD
是整個軟甲開發架構設計的趨勢,而且這個趨勢伴隨着 微服務 的普及已經不可逆轉,只要前端承認自己是編程,這個趨勢同樣也逃不過去
前端還是單節點,但是未來會有端到端
這個東西現在的可用性易用性在 React
語境下已經相當高了,未來也只會更有用
其他的不用說,光效率的提升,就可以讓開發者大呼過癮了
只是 React/Vue
還有很多歷史問題需要解決,等這些問題解決了, DDD
肯定會大跨步向前的
管道流難度會不會很高?
是的,作爲極限函數式開發,在給你提供更好的類型支持,容易調試測試的支持後,首當其衝的,就是純粹函數式的爆炸難度
正常模式下,你是需要先化簡範疇運算式再寫代碼的,不過這明顯老學究了,哈哈哈
但是,React hooks
有非常活躍的社區,你不需要自己實現封裝很多邏輯,這部分可以直接求助於社區實現
需要你實現的管道功能很少
不像 Angular
寫 rxjs
,管道需要自己根據一百多個操作函數配置,腦力負擔太大,並且操作函數都是抽象的,調度權限給到你之後,複雜度又加了個 3 次方
React
的管道複用第三方,大多都是直接面向業務的,比如 swr
和 ahooks
,要直接很多
所以在,真正需要你寫的管道邏輯並不多,這一點值得慶幸
但是,管道風格也是未來趨勢,可以說管道和領域,分別是函數式和麪向對象推演到極致的結果,兩者都是最佳範式,兩者都得學習
你只需要學思想方法,而且這樣的思想方法,放諸四海皆可,任何編程平臺,除了特別純粹的那種:無類型 lisp
和無 lambdajava
,基本上這些概念都是想通且能夠交流的
管道也是存在於編程的方方面面,elasticSearch
,mongo aggregation
,node stream
,graphQL
,等等等等…
參考資料
[1]
前端備忘錄 - 江湖術士: https://www.zhihu.com/column/plightfield
[2]
ReactiveX: https://mcxiaoke.gitbooks.io/rxdocs/content/Intro.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qoH1kLcIxPLGeojZ98In9Q