Modal.confirm是否違反瞭React模式分析

引言

如何評價 Ant Design 這個項目(一個設計語言)?

這是一篇臨時起意的文章,我參與瞭一點上圖中的討論,正好有點時間,索性拉篇文章專門聊聊。

首先說結論:我不認為Modal.confirm以及類似的API是anti-pattern,盡管antd作者 

@偏右悄悄地 也說Modal.confirm“並不符合React哲學”。

什麼是“React模式”?

很多人都見過這個公式——

view = f(data)

React確實就是這麼運作的。

然而React從來沒有建議過,讓你的整個應用都這麼運作。很顯然,一個view=f(data),是涵蓋不瞭前端應用那麼多可能性的。那麼一個現實中有用的網頁,大致應該怎麼運作?

看圖說話——

這張圖我想所有人都能看懂,大部分給你科普前端架構的文章大致都是這麼畫的,這裡隻解釋兩點:

  • 第一,藍箭頭代表狀態傳遞,紅箭頭代表行為傳遞。比如vdom,其實就是一堆描述某一時刻html狀態的數據,但是經過diff,得到的指導react-dom進行操作的patch,其實描述的是“行為”,用現在時髦的說法叫“副作用”。
  • 紅框裡面為什麼除瞭data還有timing?timing指的是時機,就是說你什麼時候應該渲染,本質上是model那一層告訴你的,React組件不會自己沒事兒渲染著玩(先不考慮state)。

圖是很美好,然而如果一個網頁就這樣的話,它除瞭展示一下畫面(還不能做單頁),不會有任何用處。所以這個圖要完善一下——

經過完善,圖裡面多瞭框,就是紅框裡那個actions,指的是當你點按鈕的時候,發生的回調操作。有的框架裡面管這個叫action,有的是store的成員函數,還有的比如redux,雖然它有action概念,但其實真正的“action”是在reducer裡面實現的。

另外這裡又出現瞭timing(時機),這其實是我最近很喜歡的一個概念。所謂的timing,就是告訴另一個模塊“it’s time to do xxx”,trigger(event)是timing,dispatch(action)是timing,甚至直接調用成員函數,其實也是一種timing。

這張圖,看起來能幹一些事兒瞭,至少能點按鈕刷新數據瞭,能做單頁瞭。然而我們的網頁,並非是隻讀的,除瞭展示數據,還要往後臺提交數據,或者執行某個具體的功能(比如VScode裡點ctrl+s保存,它需要往硬盤裡面寫文件)。所以還要完善一下——

相對完整的前端應用架構

哎,這個版本看起來有具體功能瞭,因為除瞭瀏覽器html之外,我們還多瞭個“IO等”,這樣那些除瞭變動html之外的“副作用”,就可以實現瞭。基本上一個網頁的架構,就是這樣瞭。

註意,在這個版本中,Model也可以調用Actions,這是因為有些Model庫是響應式的,比如rx,它本身就可以通過subscribe驅動副作用。

到這裡,咱們終於可以進正題瞭。

我在上面的圖中花瞭三個虛線框,中間那個是“React掌管的部分”,也就是建議view=f(data)那部分。我們看下箭頭的顏色,Model到View,應該是狀態數據(data),View到Actions,應該是行為指令(timing)。

其他兩層,天生就不是React掌管的范圍,業務模型主要負責維護數據以及數據之間的驅動關系,Side effect backend主要負責實現各種副作用,React的那套模型,在這裡幫不上忙——註意,這不代表那兩層就完全不能用React,事實上confirm完全可以理解為IO的一種(類比下c語言的scanf)。

也就是說,哪怕一個帶有明顯數據驅動特色的React項目,也存在很多部分不是數據驅動而是事件驅動的(我更喜歡成為“時機驅動”),這是天經地義的事情,不然你的程序根本寫不出來。原因很簡單,數據隻能驅動出狀態,隻有時機才能驅動出行為——學過《數電》應該對這一點深有感觸。

那麼,對於“用戶點擊刪除按鈕,彈出確認框,點擊確定刪除條目,點擊取消不做任何操作”這樣一個功能——

  • 你覺得他是個狀態還是個行為?
  • 你覺得他是時機驅動的還是數據驅動的?

答:它是個行為,時機驅動。

那麼,對於一個時機驅動的行為,你非得把它硬坳成一個數據驅動的狀態,你不覺得很奇怪嗎?

一個comfirm,這麼寫有什麼問題呢(偽代碼,忽略一部分保護性代碼)?

class FoobarComponent extends React.Component {
  // ...
  async onRemoveBtnClick (id) {
    const yes = await Modal.confirm('確認刪除嗎?')
    if (!yes) return
    dispatch(actions.remove({ id }))
  }
  render () {
    const { items } = this.props
    return <ul>
      {
        items.map(el => <li key={el.id}>
          <span>{el.name}</span>
          <button onClick={this.onRemoveBtnClick.bind(this, el.id)}>刪除</button>
        </li>)
      }
    </ul>
  }
}

那麼,為什麼有人會覺的這樣寫是反模式呢?

我覺得可能是因為我們在編寫網頁的時候,一般都會比較註重縱向分層,卻常常忽視橫向切分。由於react-router的盛行,基本上我們是在組件這個層面配合前端路由來做橫向切分的,但實際上這隻是一種特殊情況,如果我們的應用更復雜,我們可能需要這樣的結構——

上圖中每個模塊,都和模塊A具有大致相同的結構,各個模塊之間的時機和狀態,可以通過總線一類的工具來中轉。

如果我們寫網頁的“起手式”就是這樣的,那麼Modal,可能會被設計成一個單獨的模塊M,Modal.comfirm,可能會被設計成這樣:

const yes = await globalBus.dispachAndWait(Actions.comfirm('確認刪除嗎?'))

這樣,你還會覺得“反模式”嗎?

PS:為什麼為這件小事兒囉嗦這麼一大篇文章呢?其實這本來也是我2018年前端技術總結的一部分想法,隻是臨時借著這個討論說出來瞭。2018年上半年,參與瞭一個臨時前端團隊,堆瞭不少業務表單頁面,期間,我跟人強調瞭無數次“不要無腦數據驅動”,還是有有人非得閃轉騰挪用數據驅動和render表達一切,留瞭一大堆屎給我重構,我這一股無名火憋到現在。

以上就是Modal.confirm是否違反瞭React模式分析的詳細內容,更多關於React模式剖析Modal.confirm的資料請關註WalkonNet其它相關文章!

推薦閱讀: