<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何正確的實現(xiàn)組件復(fù)用

          共 6251字,需瀏覽 13分鐘

           ·

          2021-09-25 00:07

          在業(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 的組件,它的典型后果有:

          1. 一旦整體的請求方式被變更,比如添加了統(tǒng)一的請求頭或者異常處理,那就可能改動每個組件。

          這個問題,可能有的研發(fā)團隊中會選擇先封裝一下 request,然后再引入,這是可以消除這種問題的。

          1. 如果多個不同的項目合并集成了,就存在多種不同的數(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ù)用性。

          1. 存在外部依賴嗎?它有可能被更改嗎?

          對于地址的查詢,就是外部依賴。注意,盡管大部分情況下這個是不會改的,但是仍然存在這個可能性,需要提前考慮這類事情,通常,遇到有數(shù)據(jù)請求之類的東西,盡量去抽象一下。

          1. 邏輯會被單獨復(fù)用嗎?

          如果需要建立另外一種選地址的組件,交互形態(tài)不同,但邏輯可以是一樣的。

          1. 這個交互形態(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é)前端



          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久人人操人人 | aaa高清在线 | 天天天天天天天天天干 | 四虎成人视频 | 偷拍色区图 |