什麼是無渲染組件?

大家好,我是 ConardLi

無頭用戶界面組件是一種不提供任何接口而提供最大視覺靈活性的組件。“等等,你是在提倡沒有用戶界面的用戶界面模式麼?”

是的,這正是我所提倡的。

擲硬幣組件

假設你現在需要實現一個擲硬幣的功能,當組件渲染時模擬一次擲硬幣!一半的時間組件應該渲染 “正面”,一半的時間應該渲染 “反面”。你對你的產品經理說 “這需要多年的研究!” 然後你繼續工作。

 const CoinFlip = () =>
  Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;

事實證明,模仿擲硬幣比你想象的要容易得多,所以你可以自豪地分享成果。你得到了回覆,“這真的是太棒了!請更新那些顯示很酷的硬幣的圖片好麼?” 沒問題!

 const CoinFlip = () =>
  Math.random() < 0.5 ? (
    <div>
      <img src=”/heads.svg” alt=”Heads” />
    </div>
  ) : (
    <div>
      <img src=”/tails.svg” alt=”Tails” />
    </div>
  );

很快,他們會在營銷材料中使用你的 <CoinFlip /> 組件,來向人們演示你的新功能有多麼炫酷。“我們想在博客上發表文章,但是我們需要標籤'Heads' 和'Tails',用於 SEO 和其他事情。” 哦,天啊,或許我們需要在商城網站中添加一個標誌?

 const CoinFlip = (
  // We’ll default to false to avoid breaking the applications
  // current usage.
  { showLabels = false }
 ) =>
  Math.random() < 0.5 ? (
    <div>
      <img src=”/heads.svg” alt=”Heads” />

      {/* Add these labels for the marketing site. */}
      {showLabels && <span>Heads</span>}
    </div>
  ) : (
    <div>
      <img src=”/tails.svg” alt=”Tails” />

      {/* Add these labels for the marketing site. */}
      {showLabels && <span>Tails</span>}
    </div>
  );

後來,出現了一個需求。“我們想知道你能否只給 APP 裏的 <CoinFlip /> 添加一個重擲硬幣的按鈕?” 事情開始變得糟糕,以致於我不敢再直視 Kent C. Dodds 的眼睛。

 const flip = () =({
   flipResults: Math.random()
 });

 class CoinFlip extends React.Component {
   static defaultProps = {
     showLabels: false,
     // We don’t repurpose `showLabels`, we aren’t animals, after all.
     showButton: false
   };

   state = flip();

   handleClick = () ={
     this.setState(flip);
   };

   render() {
    return (
      // Use fragments so people take me seriously.
      <>
      {this.state.showButton && (
        <button onClick={this.handleClick}>Reflip</button>
      )}
      {this.state.flipResults < 0.5 ? (
        <div>
          <img src=”/heads.svg” alt=”Heads” />
          {showLabels && <span>Heads</span>}
        </div>
      ) : (
        <div>
          <img src=”/tails.svg” alt=”Tails” />
          {showLabels && <span>Tails</span>}
        </div>
      )}
      </>
    );
  }
 }

很快就有同事找到你。“嗨,你的 <CoinFlip /> 性能太棒了!我們剛接到任務要開發新的 <DiceRoll /> 特性,我們希望可以重用你的代碼!” 新骰子的功能:

你現在有兩個選項,回覆 “對不起,我們不一樣。” 或着你一邊向 CoinFlip 中添加 DiceRoll 的複雜功能,一邊看着組件無法承受過多職責而崩潰。(是否有一個給憂鬱的程序員詩人的市場?我喜歡追求這種技術。)

無頭組件瞭解一下

無頭用戶界面組件將組件的邏輯和行爲與其視覺表現分離。當組件的邏輯足夠複雜並與它的視覺表現解耦時,這種模式非常有效。實現 <CoinFlip/> 的無頭將作爲函數子組件或渲染屬性,就像這樣:

 const flip = () =({
   flipResults: Math.random()
 });
 class CoinFlip extends React.Component {
   state = flip();
   handleClick = () ={
     this.setState(flip);
   };
   render() {
     return this.props.children({
       rerun: this.handleClick,
       isHeads: this.state.flipResults < 0.5
     });
   }
 }

這個組件是無頭的,因爲它沒有渲染任何東西,它期望當它在處理邏輯的時,各種 consumers 完成視覺表現。因此 APP 代碼看起來應該是這樣的:

 <CoinFlip>
   {({ rerun, isHeads }) =(
    <>
      <button onClick={rerun}>Reflip</button>
      {isHeads ? (
        <div>
          <img src=”/heads.svg” alt=”Heads” />
        </div>
      ) : (
        <div>
          <img src=”/tails.svg” alt=”Tails” />
        </div>
      )}
    </>
   )}
 </CoinFlip>

商場站點代碼:

 <CoinFlip>
  {({ isHeads }) =(
    <>
      {isHeads ? (
        <div>
          <img src=”/heads.svg” alt=”Heads” />
          <span>Heads</span>
        </div>
      ) : (
        <div>
          <img src=”/tails.svg” alt=”Tails” />
          <span>Tails</span>
        </div>
      )}
    </>
  )}
 </CoinFlip>

這很好不是麼!我們把邏輯與視覺表現完全解耦!這給我們視覺上帶來了很大的靈活性!我知道你正在思考什麼......

你這小笨蛋,這不就是一個渲染屬性麼?

這個無頭組件恰好是作爲渲染工具實現的,是的!它也可以作爲一個高階組件來實現。即使是簡單的實現,也可以到達我們的要求。它甚至可以作爲 ViewController 來實現。或者是 ViewModel 和 View。這裏的重點是將翻轉硬幣的機制和該機制的 “界面” 分離。

<DiceRoll /> 呢?

這種分離的巧妙之處在於,推廣我們的無頭組件以及支持我們同事的新的 <DiceRoll /> 的特性會很容易。拿着我的 Diet Coke™:

 const run = () =({
   random: Math.random()
 });

 class Probability extends React.Component {
   state = run();

   handleClick = () ={
     this.setState(run);
   };

   render() {
     return this.props.children({
       rerun: this.handleClick,

       // By taking in a threshold property we can support
       // different odds!
       result: this.state.random < this.props.threshold
     });
   }
 }

利用這個無頭組件,我們在沒有對 consumer 進行任何更改對情況下,交換的實現:

 const CoinFlip = ({ children }) =(
  <Probability threshold={0.5}>
    {({ rerun, result }) =>
      children({
        isHeads: result,
        rerun
    })}
  </Probability>
 );

現在我們的同事可以分享我們的 <Probability /> 模擬程序機制了!

 const RollDice = ({ children }) =(
   // Six Sided Dice
   <Probability threshold={1 / 6}>
     {({ rerun, result }) =(
       <div>
         {/* She was able to use a different event! */}
         <span onMouseOver={rerun}>Roll the dice!</span>
         {/* Totally different interface! */}
         {result ? (
           <div>Big winner!</div>
         ) : (
           <div>You win some, you lose most.</div>
         )}
       </div>
     )}
  </Probability>
 );

非常乾淨,不是麼?

分離原則 —— Unix 哲學

這表達了一個存在很長時間對普遍基本原則,“Unix 基礎哲學第四條”:

分離原則:將策略與機制分離,將接口和引擎分離 —— Eric S. Raymond。

我想借用書中的部分,並且用 “接口” 來替換 “策略” 一詞。

接口和機制都傾向於在不同時間範圍內變化,但接口的變化比機制要快得多。GUI 工具包那時尚的外觀和體驗會變,但是操作和組合卻不會。

因此,將接口和機制結合在一起有兩個不好的影響:它使得接口變的生硬,更難響應用戶的需求,這意味着試圖更改接口具有很強的不穩定性。

另一方面,通過將這兩者分開,我們可以在沒有中斷機制的情況下試驗新的接口。我們還可以更容易地爲該機制編寫好的測試(接口,因爲它們太新了,難以證明這樣的投資是合理的)。

我喜歡這裏的真知灼見!這也讓我們對何時使用無頭組件模式有了一些瞭解。

當你將 “機制” 和 “策略” 分離時,就會產生間接的成本。你需要確保分離的價值大於它的間接成本。我認爲這在很大程度上是過去許多 MV* 模式出問題的地方,它們從這樣一個公理開始,即所有的東西都應該以這種方式分開;而在現實中,機制和策略往往是緊密耦合的,或分離的成本並沒有超過分離的好處。

開源無頭組件和非平凡引用

要獲取一個真正的示例性非平凡無頭組件,可以瞭解一下我朋友 Kent C. Dodds 在 Paypal 上的項目:downshift 的文章。事實上,正是 downshift 給了這篇文章一些靈感。在不提供任何用戶界面的情況下,downshift 提供了複雜的自動完成、下拉、選擇體驗,這些體驗都是可以訪問的。在這裏看看它所有可用的方法。

我希望隨着時間的推移,會出現更多類似的項目。我無法計算有多少次我想使用一個特定的開源 UI 組件,但卻無法這樣做,因爲在滿足設計要求的方式上,它並不是 “主題化的” 或 “可剝離的”。無頭組件完全通過 “自帶接口” 的要求來解決這個問題。

在一個設計系統和用戶界面庫都是無頭的世界裏,你的界面可以有一種高端定製的感覺,以及優秀開源庫的持久性和可訪問性。你僅需要將時間花費在你所需要的部分 —— 一個獨特的,外觀及體驗都只屬於你 APP 的部分。

我可以繼續討論從國際化到 E2E 測試集成的好處,但我建議你最好自己去體驗。

最後

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Za_zSZd-v-IUqmoHCc684A