1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        如何正確的實(shí)現(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)該如何去做?

        本文試圖圍繞這個主題,給出一些可能的解決思路。

        組件的實(shí)現(xiàn)

        狀態(tài)與渲染

        通常,我們會有一些簡單而通用的場景,需要處理狀態(tài)的存放:

        • 被單獨(dú)使用
        • 被組合使用

        一般來說,我們有兩種策略來實(shí)現(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)外置通常是更好的策略,因?yàn)樗目山M合性需求更強(qiáng)。

        使用上下文管控依賴項(xiàng)

        我們在實(shí)現(xiàn)一個相對復(fù)雜組件的時候,有可能面臨一些外部依賴項(xiàng)。

        比如說:

        • 選擇地址的組件,可能需要外部提供地址的查詢能力

        一般來說,我們給組件提供外置配置項(xiàng)的方式有這么幾種:

        • 通過組件自身的參數(shù)(props)傳入
        • 通過上下文傳入
        • 組件自己從某個全局性的位置引入

        這三種里面,我們需要盡可能避免直接引入全局依賴,舉例來說,如果不刻意控制外部依賴,就會存在許多在組件中直接引用 request 的情況,比如說:

        import request from 'xxx'

        const Component = () => {
          useEffect(() => {
            request(xxx)
          }, [])
        }

        注意這里,我們一般意識不到直接 import 這個 request 有什么不對,但實(shí)際上,按照這個實(shí)現(xiàn)方式,我們可能在一個應(yīng)用系統(tǒng)中,存在很多個直接依賴 request 的組件,它的典型后果有:

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

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

        1. 如果多個不同的項(xiàng)目合并集成了,就存在多種不同的數(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,我們可以更好地表達(dá)整組的狀態(tài)與操作,并且,當(dāng)下層組件結(jié)構(gòu)產(chǎn)生調(diào)整的時候,需要調(diào)整的數(shù)據(jù)連接關(guān)系較少(通常我們傾向于使用一些全局狀態(tài)管理方案的原因也是這樣)。

        狀態(tài)的可組合性

        在實(shí)現(xiàn)組件的時候,我們往往發(fā)現(xiàn)它們之間存在很多共性,比如:

        • 所有的表單輸入項(xiàng),都可以控制是否禁用
        • 多選項(xiàng)卡組件與卡片組,都是在一個列表形態(tài)上的擴(kuò)展

        從更深的層次出發(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>
        }

        這樣的一個組件,表達(dá)的就是對只讀狀態(tài)的讀寫操作。如果某個組件內(nèi)部需要這么一些功能,可以選擇直接將它組合進(jìn)去。

        更復(fù)雜的情況下,比如當(dāng)我們想要表達(dá)這樣一種特殊的表單卡片組,其主要功能包括:

        • 可迭代
        • 可動態(tài)添加刪除項(xiàng)
        • 可設(shè)置是否能編輯
        • 可緩存草稿,也可以提交
        • 可多選

        分析其特征,發(fā)現(xiàn)來自幾種互相不相關(guān)的原子交互:

        • 通用列表操作
        • 編輯狀態(tài)的啟用控制
        • 可編輯項(xiàng)
        • 列表多選

        它的實(shí)現(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)分解為若干獨(dú)立原子交互的組合,在組件實(shí)現(xiàn)中,只是組合并且使用它們。

        注意,有可能部分狀態(tài)組之間存在組合順序依賴關(guān)系,比如:“可選擇”依賴于“列表”,必須被組合在它下層,這部分可以在另外的體系中進(jìn)行約束。

        分層復(fù)用

        在業(yè)務(wù)中,組件的復(fù)用方式并不總是一樣的。我們有可能需要:

        • 復(fù)用一個交互方式
        • 復(fù)用一段邏輯
        • 復(fù)用一個組合了邏輯與交互的“業(yè)務(wù)組件”

        每當(dāng)我們需要設(shè)計(jì)一個“業(yè)務(wù)組件”的時候,就需要慎重考慮了??梢試L試詢問自己一些問題:

        • 我們在復(fù)用它的時候,會更改它的外部依賴嗎?
        • 它內(nèi)部的邏輯會被單獨(dú)復(fù)用嗎?
        • 這個交互形態(tài)會跟其他邏輯組合起來復(fù)用嗎?

        比如說,一個內(nèi)置了選擇省市縣的多級地址選擇器,它就是這么一種“業(yè)務(wù)組件”。我們以此為例,嘗試重新解構(gòu)它的可復(fù)用性。

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

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

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

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

        1. 這個交互形態(tài)會跟其他邏輯組合起來復(fù)用嗎?

        有可能被用來選擇其他東西。

        所以,回答了這些問題之后,我們就可以設(shè)計(jì)組件結(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>
        }

        在組件的實(shí)現(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)隔離

        在細(xì)分實(shí)現(xiàn)中,再考慮兩個部分分別由什么東西組合而成。

        在一些比較復(fù)雜的場景下,狀態(tài)結(jié)構(gòu)也很復(fù)雜,需要管理來自不同信息源的數(shù)據(jù)。在某些實(shí)踐中,選擇將一切狀態(tài)聚合到一個超大結(jié)構(gòu)中,然后分別訂閱,這當(dāng)然是可行的,但是對維護(hù)就提高了一些難度。

        通常,我們有機(jī)會把狀態(tài)去做一些分組,最容易理解的分組方式就是將業(yè)務(wù)和交互隔離。這種思考方式可以讓我們的關(guān)注點(diǎn)更聚焦:

        • 寫業(yè)務(wù)的時候,就不去思考交互形態(tài)
        • 寫交互形態(tài)的時候,就不去思考業(yè)務(wù)邏輯
        • 然后剩下的時間花在把它們連接起來

        ?? 看完三件事

        非常棒的一篇Ts實(shí)用文章,

        如果你覺得這篇內(nèi)容對你挺有啟發(fā),不妨:

        • 點(diǎn)個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容

        • 點(diǎn)擊↓面關(guān)注我們,一起學(xué)前端

        • 長按↓面二維碼,添加鬼哥微信,一起學(xué)前端



        瀏覽 53
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲图片小说另类 | 五月丁香激情四射 | 亚洲中文字幕免费视频 | 国产精品久久久久久久久久久免费看 | 操老女人逼视频 | 国产不卡区 | 麻豆午夜成人无码电影 | 亚洲最大成人7777777 | 夜夜骑夜夜 | 经典无码一区二区三区 |