原文: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*模式容易犯错的地方,就是死守这个原则,一切都以这种方式去分离。回到现实,很多机制和政策都是深度耦合的,分离的好处可能不足以覆盖所带来的成本。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。