原文:https://www.merrickchristense...

"策略同机制分离,接口同引擎分离。"— Eric S. Raymond

Headless ui组件是一种新的ui组件开发模式,组件本身不提供ui上的实现,从而让使用者能够自由定制ui样式。“且慢,无ui的ui组件,你知道自己在说什么吗?”

没错,虽然反直觉,但这正是我们所倡导的。

Coin Flip Component

假设你要实现一个抛硬币的功能,需求是这样的:实现一个类似硬币翻转的效果,翻转结束后,硬币正面朝上和反面朝上的概率是对半开。你和产品说,这需求有点复杂,给我半年的时间调研一下,然后你开始写demo

const CoinFlip = () =>
  Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>; ```  
太简单了,然后你拉了个会,拿着ppt就上去了。产品说不错,功能是有了,你把样式优化一下。对你来说问题不大。

const CoinFlip = () =>

Math.random() < 0.5 ? (
    <div>
        <img src="/heads.svg" alt="Heads" />
    </div>
) : (
    <div>
        <img src="/tails.svg" alt="Tails" />
    </div> 
);
没多久,他们希望能在营销页上线你这个功能。他们打算投放到博客推文里,希望你的组件能够对SEO友好。于是你撸起袖子继续开干。

const CoinFlip = (
// 设定默认值为false,以免对之前的应用造成破坏。
// current usage.
{ showLabels = false }
) =>
Math.random() < 0.5 ? (

<div>
  <img src="/heads.svg" alt="Heads" />

  {/* 增加label标签,用于营销页 */}
  {showLabels && <span>Heads</span>}
</div>

) : (

<div>
  <img src="/tails.svg" alt="Tails" />

  {/* 增加label标签,用于营销页 */}
  {showLabels && <span>Tails</span>}
</div>

);

然后又来了一个需求,加一个重来的按钮,并且这个按钮只在应用程序当中添加。组件开始变得丑陋了起来

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>
    )}
  </>
);

}
}

然后有一天你同事找到你:“好兄弟,你的掷硬币功能碉堡了。我们有个新需求叫投骰子,可以复用你的代码么?“你拆分了一下新需求:  
1. 需要重来按钮
1. 需要同时用于应用程序和营销页
1. 和你的组件有着完全不一样的ui
1. 有着不一样的随机概率  
现在你面临两种选择,要不就是拒绝你的同事,要不就是改造你的组件,把投骰子功能赛到掷硬币组件当中,看着这个组件变得臃肿难以维护。
# 使用Headless组件
Headless ui组件将自身的ui和行为分离出来。当一个组件的行为足够复杂,并且逻辑与视觉表现可以解耦时,这种模式非常有效。CoinFlip组件实现Headless的方式可以是让具体的ui实现作为一个子组件或者是renderProp传入,就像下面这样:  

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,
});

}
}

上面这个组件是一个headless ui组件,因为这个组件不渲染任何内容。它完成了逻辑状态的提升,期望消费者去做实际的渲染工作。所以回到我们的应用,代码可能长这样:

<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>


完美,我们对逻辑和状态进行了很好的抽象,剥离了ui,这样我们就可以随意的定制我们的ui了。我知道你可能在想什么...  
> 你是不是傻,不就是一个renderProp么,有必要绕来绕去么?

这个例子当中,恰好我们是利用renderProp去实现它。在react当中,我们当然也可以用HOC来实现。稍微扩散一下思维,我们甚至可以将其实现为MVC当中的View和Controller,或者MVVM中的ViewModel和View。(注:将组件内部的逻辑状态封装,具体的渲染和事件绑定交由渲染框架,单独的开发适配层,组件甚至可以做到跨平台)这里的核心思想是分离组件的机制和表现。
# 回到投骰子组件
这种分离的好处是,我们很容易扩展我们的headless组件,以支持同事的投骰子功能:

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

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

handleClick = () => {

this.setState(run);

};

render() {

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

  // 设置不同的threshold,得到不同的概率
  result: this.state.random < this.props.threshold,
});

}
}


因为是headless组件,我们只需要更新CoinFlip组件的代码,而不需要去修改下级消费者的代码。

const CoinFlip = ({ children }) => (
<Probability threshold={0.5}>

{({ rerun, result }) =>
  children({
    isHeads: result,
    rerun,
  })
}

</Probability>
);


同样的,同事也可以通过复用Probability组件来实现他们的逻辑

const RollDice = ({ children }) => (
// 六面骰子
<Probability threshold={1 / 6}>

{({ rerun, result }) => (
  <div>
    {/* 这里可以实现一些自定义事件 */}
    <span onMouseOver={rerun}>Roll the dice!</span>
    {/* 完全不同的ui实现 */}
    {result ? (
      <div>Big winner!</div>
    ) : (
      <div>You win some, you lose most.</div>
    )}
  </div>
)}

</Probability>
);


优雅,非常优雅。
# 分离原则-Unix设计哲学
这是一个广受业界认可的共识,并且经久不衰。Unix设计哲学基础第四条:
>"策略同机制分离,接口同引擎分离。"— Eric S. Raymond  

我想引用这一部分,并且用界面替代策略这个词。
> 因为界面和机制是按照不同的时间尺度变化的,界面的变化要远远快于机制。GUI工具包的观感时尚来去匆匆,而光栅操作和组合确实永恒的。  
所以,把界面同机制揉成一团有两个负面影响:一来会使界面变得死板,难以适应用户需求的改变,二来也意味着任何界面的改变都极有可能动摇机制。  
相反,将两者剥离,就有可能在探索新界面的时候不足以打破机制。另外,我们也可以更容易为机制写出较好的测试(因为界面太短命,不值得花太多精力在这上面)。

我喜欢这里的深刻见解,这也给我们带来思考,哪些情况下headless ui设计模式是非常有价值的。  
1. 这个组件的寿命有多久?抛开界面表现,背后的机制是否值得我们刻意保留?这个机制我们是否会用在另外一个外观和风格完全不同的项目当中。
2. 我们的界面外观多久更新一次,同样的功能,我们会有多少不同的外观界面?  

将机制和政策分离,是有成本的。我们需要平衡好分离带来的收益和成本。我认为这是过去许多MV*模式容易犯错的地方,就是死守这个原则,一切都以这种方式去分离。回到现实,很多机制和政策都是深度耦合的,分离的好处可能不足以覆盖所带来的成本。

leewhite97
1 声望0 粉丝