原文: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* 模式容易犯错的中央,就是死守这个准则,所有都以这种形式去拆散。回到事实,很多机制和政策都是深度耦合的,拆散的益处可能不足以笼罩所带来的老本。