- 原文地址:Headless User Interface Components
- 原文做者:Merrick Christensen
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Starrier
- 校對者:xunge0613、Moonliujk
無頭用戶界面組件是一種不提供任何接口而提供最大視覺靈活性的組件。「等等,你是在提倡沒有用戶界面的用戶界面模式麼?」前端
是的,這正是我所提倡的。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 />
特性,咱們但願能夠重用你的代碼!」新骰子的功能:後端
onClick
。你如今有兩個選項,回覆「對不起,咱們不同。」或着你一邊向 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>
複製代碼
這很好不是麼!咱們把邏輯與視覺表現徹底解耦!這給咱們視覺上帶來了很大的靈活性!我知道你正在思考什麼......
你這小笨蛋,這不就是一個渲染屬性麼?
這個無頭組件剛好是做爲渲染工具實現的,是的!它也能夠做爲一個高階組件來實現。**即便是簡單的實現,也能夠到達咱們的要求。**它甚至能夠做爲 View
和 Controller
來實現。或者是 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 進行任何更改對狀況下,交換 <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 基礎哲學第四條」:
分離原則:將策略與機制分離,將接口和引擎分離 —— Eric S. Raymond。
我想借用書中的部分,而且用「接口」來替換「策略」一詞。
接口和機制都傾向於在不一樣時間範圍內變化,但接口的變化比機制要快得多。GUI 工具包那時尚的外觀和體驗會變,可是操做和組合卻不會。
所以,將接口和機制結合在一塊兒有兩個很差的影響:它使得接口變的生硬,更難響應用戶的需求,這意味着試圖更改接口具備很強的不穩定性。
另外一方面,經過將這二者分開,咱們能夠在沒有中斷機制的狀況下試驗新的接口。咱們還能夠更容易地爲該機制編寫好的測試(接口,由於它們太新了,難以證實這樣的投資是合理的)。
我喜歡這裏的真知灼見!這也讓咱們對什麼時候使用無頭組件模式有了一些瞭解。
當你將「機制」和「策略」分離時,就會產生間接的成本。你須要確保分離的價值大於它的間接成本。我認爲這在很大程度上是過去許多 MV* 模式出問題的地方,它們從這樣一個公理開始,即全部的東西都應該以這種方式分開;而在現實中,機制和策略每每是緊密耦合的,或分離的成本並無超過度離的好處。
要獲取一個真正的示例性非平凡無頭組件,能夠了解一下我朋友 Kent C. Dodds 在 Paypal 上的項目:downshift 的文章。事實上,正是 downshift 給了這篇文章一些靈感。在不提供任何用戶界面的狀況下,downshift 提供了複雜的自動完成、下拉、選擇體驗,這些體驗都是能夠訪問的。在這裏看看它全部可用的方法。
我但願隨着時間的推移,會出現更多相似的項目。我沒法計算有多少次我想使用一個特定的開源 UI 組件,但卻沒法這樣作,由於在知足設計要求的方式上,它並非「主題化的」或「可剝離的」。無頭組件徹底經過「自帶接口」的要求來解決這個問題。
在一個設計系統和用戶界面庫都是無頭的世界裏,你的界面能夠有一種高端定製的感受,以及優秀開源庫的持久性和可訪問性。你僅須要將時間花費在你所須要的部分 —— 一個獨特的,外觀及體驗都只屬於你APP的部分。
我能夠繼續討論從國際化到 E2E 測試集成的好處,但我建議你最好本身去體驗。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。