- 原文地址:Headless User Interface Components
- 原文作者:Merrick Christensen
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Starrier
- 校對者:xunge0613、Moonliujk
無頭使用者介面元件是一種不提供任何介面而提供最大視覺靈活性的元件。“等等,你是在提倡沒有使用者介面的使用者介面模式麼?”
是的,這正是我所提倡的。
擲硬幣元件
假設你現在需要實現一個擲硬幣的功能,當元件渲染時模擬一次擲硬幣!一半的時間元件應該渲染“正面”,一半的時間應該渲染“反面”。你對你的產品經理說“這需要多年的研究!”然後你繼續工作。
const CoinFlip = () =>
Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;
複製程式碼
事實證明,模仿擲硬幣比你想象的要容易得多,所以你可以自豪地分享成果。你得到了回覆,“這真的是太棒了!請更新那些顯示很酷的硬幣的圖片好麼?”沒問題!
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 的眼睛。
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
。 - 希望在 APP 和商城網站中都顯示。
- 有完全不同的介面。
- 有不同的隨機性。
你現在有兩個選項,回覆“對不起,我們不一樣。”或著你一邊向 CoinFlip
中新增 DiceRoll
的複雜功能,一邊看著元件無法承受過多職責而崩潰。(是否有一個給憂鬱的程式設計師詩人的市場?我喜歡追求這種技術。)
無頭元件瞭解一下
無頭使用者介面元件將元件的邏輯和行為與其視覺表現分離。當元件的邏輯足夠複雜並與它的視覺表現解耦時,這種模式非常有效。實現 <CoinFlip/>
的無頭將作為函式子元件或渲染屬性,就像這樣:
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 程式碼看起來應該是這樣的:
<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 哲學
這表達了一個存在很長時間對普遍基本原則,“Unix 基礎哲學第四條”:
分離原則:將策略與機制分離,將介面和引擎分離 —— Eric S. Raymond。
我想借用書中的部分,並且用“介面”來替換“策略”一詞。
介面和機制都傾向於在不同時間範圍內變化,但介面的變化比機制要快得多。GUI 工具包那時尚的外觀和體驗會變,但是操作和組合卻不會。
因此,將介面和機制結合在一起有兩個不好的影響:它使得介面變的生硬,更難響應使用者的需求,這意味著試圖更改介面具有很強的不穩定性。
另一方面,通過將這兩者分開,我們可以在沒有中斷機制的情況下試驗新的介面。我們還可以更容易地為該機制編寫好的測試(介面,因為它們太新了,難以證明這樣的投資是合理的)。
我喜歡這裡的真知灼見!這也讓我們對何時使用無頭元件模式有了一些瞭解。
- 這個元件會持續多長時間?除了介面外,是否值得刻意保留這個機制?也許在另一個外觀和體驗不同的專案中可以使用這種機制?
- 我們的介面改變的頻率多快?同一機制會有多個介面麼?
當你將“機制”和“策略”分離時,就會產生間接的成本。你需要確保分離的價值大於它的間接成本。我認為這在很大程度上是過去許多 MV* 模式出問題的地方,它們從這樣一個公理開始,即所有的東西都應該以這種方式分開;而在現實中,機制和策略往往是緊密耦合的,或分離的成本並沒有超過分離的好處。
開源無頭元件和非平凡引用
要獲取一個真正的示例性非平凡無頭元件,可以瞭解一下我朋友 Kent C. Dodds 在 Paypal 上的專案:downshift 的文章。事實上,正是 downshift 給了這篇文章一些靈感。在不提供任何使用者介面的情況下,downshift 提供了複雜的自動完成、下拉、選擇體驗,這些體驗都是可以訪問的。在這裡看看它所有可用的方法。
我希望隨著時間的推移,會出現更多類似的專案。我無法計算有多少次我想使用一個特定的開源 UI 元件,但卻無法這樣做,因為在滿足設計要求的方式上,它並不是“主題化的”或“可剝離的”。無頭元件完全通過“自帶介面”的要求來解決這個問題。
在一個設計系統和使用者介面庫都是無頭的世界裡,你的介面可以有一種高階定製的感覺,以及優秀開源庫的永續性和可訪問性。你僅需要將時間花費在你所需要的部分 —— 一個獨特的,外觀及體驗都只屬於你APP的部分。
我可以繼續討論從國際化到 E2E 測試整合的好處,但我建議你最好自己去體驗。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。