[译] 无渲染组件

无头用户界面组件是一种不提供任何接口而提供最大视觉灵活性的组件。“等等,你是在提倡没有用户界面的用户界面模式么?”前端

是的,这正是我所提倡的。android

掷硬币组件

假设你如今须要实现一个掷硬币的功能,当组件渲染时模拟一次掷硬币!一半的时间组件应该渲染“正面”,一半的时间应该渲染“反面”。你对你的产品经理说“这须要多年的研究!”而后你继续工做。ios

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

事实证实,模仿掷硬币比你想象的要容易得多,因此你能够自豪地分享成果。你获得了回复,“这真的是太棒了!请更新那些显示很酷的硬币的图片好么?”没问题!git

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 的眼睛。github

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 /> 特性,咱们但愿能够重用你的代码!”新骰子的功能:后端

  1. 想要“从新掷骰子”的 onClick
  2. 但愿在 APP 和商城网站中都显示。
  3. 有彻底不一样的界面。
  4. 有不一样的随机性。

你如今有两个选项,回复“对不起,咱们不同。”或着你一边向 CoinFlip 中添加 DiceRoll 的复杂功能,一边看着组件没法承受过多职责而崩溃。(是否有一个给忧郁的程序员诗人的市场?我喜欢追求这种技术。)bash

无头组件了解一下

无头用户界面组件将组件的逻辑和行为与其视觉表现分离。当组件的逻辑足够复杂并与它的视觉表现解耦时,这种模式很是有效。实现 <CoinFlip/> 的无头将做为函数子组件或渲染属性,就像这样:app

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 代码看起来应该是这样的:less

<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 来实现。或者是 ViewModelView。这里的重点是将翻转硬币的机制和该机制的“界面”分离。

<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 进行任何更改对状况下,交换 <CoinFlip /> 的实现:

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 工具包那时尚的外观和体验会变,可是操做和组合却不会。

所以,将接口和机制结合在一块儿有两个很差的影响:它使得接口变的生硬,更难响应用户的需求,这意味着试图更改接口具备很强的不稳定性。

另外一方面,经过将这二者分开,咱们能够在没有中断机制的状况下试验新的接口。咱们还能够更容易地为该机制编写好的测试(接口,由于它们太新了,难以证实这样的投资是合理的)。

我喜欢这里的真知灼见!这也让咱们对什么时候使用无头组件模式有了一些了解。

  1. 这个组件会持续多长时间?除了界面外,是否值得刻意保留这个机制?也许在另外一个外观和体验不一样的项目中可使用这种机制?
  2. 咱们的界面改变的频率多快?同一机制会有多个接口么?

当你将“机制”和“策略”分离时,就会产生间接的成本。你须要确保分离的价值大于它的间接成本。我认为这在很大程度上是过去许多 MV* 模式出问题的地方,它们从这样一个公理开始,即全部的东西都应该以这种方式分开;而在现实中,机制和策略每每是紧密耦合的,或分离的成本并无超过度离的好处。

开源无头组件和非平凡引用

要获取一个真正的示例性非平凡无头组件,能够了解一下我朋友 Kent C. Dodds 在 Paypal 上的项目:downshift 的文章。事实上,正是 downshift 给了这篇文章一些灵感。在不提供任何用户界面的状况下,downshift 提供了复杂的自动完成、下拉、选择体验,这些体验都是能够访问的。在这里看看它全部可用的方法。

我但愿随着时间的推移,会出现更多相似的项目。我没法计算有多少次我想使用一个特定的开源 UI 组件,但却没法这样作,由于在知足设计要求的方式上,它并非“主题化的”或“可剥离的”。无头组件彻底经过“自带接口”的要求来解决这个问题。

在一个设计系统和用户界面库都是无头的世界里,你的界面能够有一种高端定制的感受,以及优秀开源库的持久性和可访问性。你仅须要将时间花费在你所须要的部分 —— 一个独特的,外观及体验都只属于你APP的部分。

我能够继续讨论从国际化到 E2E 测试集成的好处,但我建议你最好本身去体验。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索