如何正確的實現(xiàn)組件復(fù)用
在業(yè)務(wù)開發(fā)過程中,我們總是會期望某些功能一定程度的復(fù)用。很基礎(chǔ)的那些元素,比如按鈕,輸入框,它們的使用方式都已經(jīng)被大部分人熟知,但是一旦某塊功能復(fù)雜起來,成為一種業(yè)務(wù)組件的時候,就會陷入一些很奇怪的境況,最初是期望抽出來的這塊組件能有比較好的復(fù)用性,但是,可能當(dāng)另外一個業(yè)務(wù)想要復(fù)用它的時候,往往遇到很多問題:
不能滿足需求 為了滿足多個業(yè)務(wù)的復(fù)用需求,不得不把組件修改到很別扭的程度 參數(shù)失控 版本無法管理
諸如此類,時常使人懷疑,在一個業(yè)務(wù)體系中,組件化到底應(yīng)該如何去做?
本文試圖圍繞這個主題,給出一些可能的解決思路。
組件的實現(xiàn)
狀態(tài)與渲染
通常,我們會有一些簡單而通用的場景,需要處理狀態(tài)的存放:
被單獨使用 被組合使用
一般來說,我們有兩種策略來實現(xiàn),分別是狀態(tài)外置和內(nèi)置。
有狀態(tài)組件:
const StatefulInput = () => {
const [value, setValue] = useState('')
return <input value={value} onChange={setValue} />
}
無狀態(tài)組件:
type StatelessInputProps = {
value: string
setValue: (v: string) => void
}
const StatelessInput = (props: StatelessInputProps) => {
const { value, setValue } = props
return <input value={value} onChange={setValue} />
}
通常有狀態(tài)組件可以位于更頂層,不受其他約束,而無狀態(tài)組件則依賴于外部傳入的狀態(tài)與控制。有狀態(tài)組件也可以在內(nèi)部分成兩層,一層專門處理狀態(tài),一層專門處理渲染,后者也是一個無狀態(tài)組件。
一般來說,對于純交互類組件,將最核心的狀態(tài)外置通常是更好的策略,因為它的可組合性需求更強。
使用上下文管控依賴項
我們在實現(xiàn)一個相對復(fù)雜組件的時候,有可能面臨一些外部依賴項。
比如說:
選擇地址的組件,可能需要外部提供地址的查詢能力
一般來說,我們給組件提供外置配置項的方式有這么幾種:
通過組件自身的參數(shù)(props)傳入 通過上下文傳入 組件自己從某個全局性的位置引入
這三種里面,我們需要盡可能避免直接引入全局依賴,舉例來說,如果不刻意控制外部依賴,就會存在許多在組件中直接引用 request 的情況,比如說:
import request from 'xxx'
const Component = () => {
useEffect(() => {
request(xxx)
}, [])
}
注意這里,我們一般意識不到直接 import 這個 request 有什么不對,但實際上,按照這個實現(xiàn)方式,我們可能在一個應(yīng)用系統(tǒng)中,存在很多個直接依賴 request 的組件,它的典型后果有:
一旦整體的請求方式被變更,比如添加了統(tǒng)一的請求頭或者異常處理,那就可能改動每個組件。
這個問題,可能有的研發(fā)團隊中會選擇先封裝一下 request,然后再引入,這是可以消除這種問題的。
如果多個不同的項目合并集成了,就存在多種不同的數(shù)據(jù)來源,不一定能做到直接統(tǒng)一這個請求配置。
因此,要盡量避免直接引入全局性的依賴,哪怕它當(dāng)前真的是某種全局,也要假定未來是可能變動的,包括但不限于:
請求方式 用戶登錄狀態(tài) 視覺主題 多語言國際化 環(huán)境與平臺相關(guān)的 API
需要盡可能把這些東西控制住,封裝在某種上下文里,并且提供便利的使用方式:
// 統(tǒng)一封裝控制
const ServiceContext = () => {
const request = useCallback(() => {
return // 這里是統(tǒng)一引入控制的 request
}, [])
const context: ServiceContextValue = {
request,
}
return <ServiceContext.Provider value={context}>{children}</ServiceContext.Provider>
}
// 包裝一個 hook
const useService = () => {
return useContext(ServiceContext)
}
// 在組件中使用
const Component = () => {
const { request } = useService()
// 這里使用 request
}
這樣,我們在整個大組件樹上的視角就是:某一個子樹往下,可以統(tǒng)一使用某種控制策略,這種策略在模塊集成的時候會比較有用。
使用 Context,我們可以更好地表達整組的狀態(tài)與操作,并且,當(dāng)下層組件結(jié)構(gòu)產(chǎn)生調(diào)整的時候,需要調(diào)整的數(shù)據(jù)連接關(guān)系較少(通常我們傾向于使用一些全局狀態(tài)管理方案的原因也是這樣)。
狀態(tài)的可組合性
在實現(xiàn)組件的時候,我們往往發(fā)現(xiàn)它們之間存在很多共性,比如:
所有的表單輸入項,都可以控制是否禁用 多選項卡組件與卡片組,都是在一個列表形態(tài)上的擴展
從更深的層次出發(fā),我們可以意識到,幾乎任意一個組件,它所使用的狀態(tài)與控制能力都是由若干原子化的能力組合而出,這些原子能力可能是相關(guān)的,也可能是不相關(guān)的。
舉例來說:
const Editable = (props: PropsWithChildren<{}>) => {
const { children } = props
const [editable, setEditable] = useState<boolean>(false)
const context: EditableContextValue = {
editable,
setEditable,
}
return <EditableContext.Provider value={context}>{children}</EditableContext.Provider>
}
這樣的一個組件,表達的就是對只讀狀態(tài)的讀寫操作。如果某個組件內(nèi)部需要這么一些功能,可以選擇直接將它組合進去。
更復(fù)雜的情況下,比如當(dāng)我們想要表達這樣一種特殊的表單卡片組,其主要功能包括:
可迭代 可動態(tài)添加刪除項 可設(shè)置是否能編輯 可緩存草稿,也可以提交 可多選
分析其特征,發(fā)現(xiàn)來自幾種互相不相關(guān)的原子交互:
通用列表操作 編輯狀態(tài)的啟用控制 可編輯項 列表多選
它的實現(xiàn)就可能是這樣:
const CardList = () => {
const { list, setList, addItem } = useContext(ListContext)
const { editable, setEditable } = useContext(EditContext)
const { commit } = useContext(DraftContext)
const { selectedItems, setSelectedItems } = useContext(ListSelectionContext)
// 然后內(nèi)部組合使用
}
由此,我們有可能在每個組件開發(fā)的時候,將其內(nèi)部結(jié)構(gòu)分解為若干獨立原子交互的組合,在組件實現(xiàn)中,只是組合并且使用它們。
注意,有可能部分狀態(tài)組之間存在組合順序依賴關(guān)系,比如:“可選擇”依賴于“列表”,必須被組合在它下層,這部分可以在另外的體系中進行約束。
分層復(fù)用
在業(yè)務(wù)中,組件的復(fù)用方式并不總是一樣的。我們有可能需要:
復(fù)用一個交互方式 復(fù)用一段邏輯 復(fù)用一個組合了邏輯與交互的“業(yè)務(wù)組件”
每當(dāng)我們需要設(shè)計一個“業(yè)務(wù)組件”的時候,就需要慎重考慮了。可以嘗試詢問自己一些問題:
我們在復(fù)用它的時候,會更改它的外部依賴嗎? 它內(nèi)部的邏輯會被單獨復(fù)用嗎? 這個交互形態(tài)會跟其他邏輯組合起來復(fù)用嗎?
比如說,一個內(nèi)置了選擇省市縣的多級地址選擇器,它就是這么一種“業(yè)務(wù)組件”。我們以此為例,嘗試重新解構(gòu)它的可復(fù)用性。
存在外部依賴嗎?它有可能被更改嗎?
對于地址的查詢,就是外部依賴。注意,盡管大部分情況下這個是不會改的,但是仍然存在這個可能性,需要提前考慮這類事情,通常,遇到有數(shù)據(jù)請求之類的東西,盡量去抽象一下。
邏輯會被單獨復(fù)用嗎?
如果需要建立另外一種選地址的組件,交互形態(tài)不同,但邏輯可以是一樣的。
這個交互形態(tài)會跟其他邏輯組合起來復(fù)用嗎?
有可能被用來選擇其他東西。
所以,回答了這些問題之后,我們就可以設(shè)計組件結(jié)構(gòu)了:
業(yè)務(wù)上下文
const Business = () => {
const [state, setState] = useState()
return <BusinessContext.Provider value={context}>{children}</BusinessContext.Provider>
}
交互上下文
const Interaction = () => {
const [state, setState] = useState()
return <InteractionContext.Provider value={context}>{children}</InteractionContext.Provider>
}
在組件的實現(xiàn)中:
const ComponentA = () => {
const {} = useContext(BusinessContext)
const {} = useContext(InteractionContext)
// 在這里連接業(yè)務(wù)與交互
}
使用的時候:
const App = () => {
// 下面每層傳入各自需要的配置信息
return (
<Business>
<Interaction>
<ComponentA />
</Interaction>
</Business>
)
}
在這個部分,總的原則是:
業(yè)務(wù)狀態(tài)與 UI 狀態(tài)隔離 UI 狀態(tài)與交互呈現(xiàn)隔離
在細分實現(xiàn)中,再考慮兩個部分分別由什么東西組合而成。
在一些比較復(fù)雜的場景下,狀態(tài)結(jié)構(gòu)也很復(fù)雜,需要管理來自不同信息源的數(shù)據(jù)。在某些實踐中,選擇將一切狀態(tài)聚合到一個超大結(jié)構(gòu)中,然后分別訂閱,這當(dāng)然是可行的,但是對維護就提高了一些難度。
通常,我們有機會把狀態(tài)去做一些分組,最容易理解的分組方式就是將業(yè)務(wù)和交互隔離。這種思考方式可以讓我們的關(guān)注點更聚焦:
寫業(yè)務(wù)的時候,就不去思考交互形態(tài) 寫交互形態(tài)的時候,就不去思考業(yè)務(wù)邏輯 然后剩下的時間花在把它們連接起來
?? 看完三件事
非常棒的一篇Ts實用文章,
如果你覺得這篇內(nèi)容對你挺有啟發(fā),不妨:
點個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
點擊↓面關(guān)注我們,一起學(xué)前端
長按↓面二維碼,添加鬼哥微信,一起學(xué)前端

