單元素元件模式簡介
使用 React 或其它基於元件的庫建立可靠構建模組的規則和最佳實踐。
在 2002 年 — 當我開始構建網頁的時候 — 包括我在內的大多數開發者都使用 <table>
標籤來構建網頁佈局。
直到 2005 年,我才開始遵循網頁標準。
如果有網站或網頁宣稱遵循網頁標準,通常就表示他們的網頁符合 HTML、CSS、JavaScript 等標準。HTML 的部分也要滿足無障礙性以及 HTML 語義的要求。
我瞭解了語義化和無障礙性,然後開始使用正確的 HTML 標籤和外部 CSS。我很自豪地將 W3C 認證徽章新增到我製作的每個網站。
我們編寫的 HTML 程式碼和輸出到瀏覽器中的真實程式碼非常相似。這意味著使用 W3C 驗證器 和其它工具來驗證輸出程式碼的規範性也可以告訴我們如何寫出更好的程式碼。
時光流逝。為了分離前端中可重用部分,我使用過 PHP、模版系統、jQuery、Polymer、Angular 和 React。尤其是後幾個,最近三年我一直在使用它們。
隨著時間的推移,我們編寫的程式碼和使用者實際使用的程式碼越來越不同了。現在,我們使用不同方式(例如 Babel 和 TypeScript)來編譯程式碼。我們使用 ES2015+ 和 JSX 規範編寫,但最終的輸出程式碼就只是 HTML 和 JavaScript。
如今,雖然我們還會使用 W3C 的工具來驗證我們的網站,但對於編寫程式碼沒有太大幫助。我們仍在追求程式碼穩定和可維護的最佳實踐。而且,如果你正在讀這篇文章,我想你也有同樣的訴求。
我為你準備了一些東西。
單元素元件模式(Singel 原始碼)
我已經不知道寫過多少個元件了。但如果把 Polymer、Angular 和 React 的專案都加起來,我敢說這個數字肯定超過一千了。
除公司專案外,我還維護了一個包含 40 多個示例元件的 React 模版庫。另外,我正在和 Raphael Thomazella 維護一套互動式 UI 元件庫,他為這個專案貢獻了很多。
許多開發者都有一個誤解:如果以一個完美的檔案結構來開始一個專案,那麼他們就不會遇到任何問題。事實上,檔案結構的一致性沒那麼重要。如果你的元件沒有遵循明確定義的規則,這最終會使你的專案很難維護。
在建立和維護了那麼多元件之後,我發現了一些使它們更加一致和可靠的特性,這樣用起來會更加愉快。一個元件越像一個 HTML 元素,它就會變得越可靠。
沒有什麼比一個
<div>
標籤更可靠了。
使用元件時,你可以問問自己下面的問題:
- 問題 #1:如果我需要將 props 傳遞給巢狀元素會怎麼樣?
- 問題 #2:由於某種原因,這個元件會使應用中斷嗎?
- 問題 #3:如果我想傳遞
id
或其它 HTML 屬性會怎麼樣? - 問題 #4:我可以通過傳遞
className
或style
屬性來自定義元件樣式嗎? - 問題 #5:事件是如何處理的呢?
可靠性意味著,在這種情況下,不需要開啟檔案檢視原始碼來了解它的工作原理。如果你在使用一個 <div>
,你馬上就會知道答案,如下:
我把這一組規則稱為 Singel。
重構驅動開發
先讓它工作,然後再去優化。
當然,不可能讓所有元件都遵循 Singel 全部規則。在某情況下 — 實際上很多情況下 — 你不得不至少打破第一條規則。
應該遵循這些規則的元件是應用中最重要的部分:原子、原始、構建塊、元素或任何稱為基礎的元件。這篇文章中,我將統稱它們為單個元素。
其中一些很容易抽象出來,比如:Button
、Image
和 Input
。也可以說是那些和 HTML 元素有直接關係的元件。在其它情況下,只有在重複程式碼時才會識別出它們。那也沒關係。
通常,無論何時你需要更改某個元件時,不管是新增新功能,還是修復問題,你可能會看到 — 或者開始編寫重複的樣式和行為。這就是需要將它抽象為一個新的單元素訊號。
單元素元件與其它元件的比值越高,應用就會越穩定、越方便維護。
將它們放到單獨的資料夾中 — 比如:elements
, atoms
, primitives
,因此,無論何時你需要匯入這些元件時,你都會確信它們遵循了規則。
一個例項
在本文中,我重點放在 React 上。同樣的規則也適用於其它任何基於元件的庫。
這就是說,我們有一個 Card
元件。它由 Card.js
和 Card.css
組成,在 Card.css
檔案中我們為 .card
、.top-bar
、.avatar
和其它類選擇器配置了樣式規則。
const Card = ({ profile, imageUrl, imageAlt, title, description }) => (
<div className="card">
<div className="top-bar">
<img className="avatar" src={profile.photoUrl} alt={profile.photoAlt} />
<div className="username">{profile.username}</div>
</div>
<img className="image" src={imageUrl} alt={imageAlt} />
<div className="content">
<h2 className="title">{title}</h2>
<p className="description">{description}</p>
</div>
</div>
);
複製程式碼
在某些時候,應用中的其它位置也有可能使用頭像。為了不重複 HTML 和 CSS 程式碼,我們要建立一個新的 Avatar
單元素元件,然後就能複用它了。
規則 #1:每次只渲染一個元素
它由 Avatar.js
和 Avatar.css
組成,後者配置了我們從 Card.css
中取出用於 .avatar
的樣式,最終返回一個 <img>
元素:
const Avatar = ({ profile, ...props }) => (
<img
className="avatar"
src={profile.photoSrc}
alt={profile.photoAlt}
{...props}
/>
);
複製程式碼
下面是我們如何在 Card
和應用中其它位置使用它:
<Avatar profile={profile} />
複製程式碼
規則 #2:從不中斷應用
一個 <img>
元素,雖然 src
屬性是必須的,如果你不傳遞它,也不會中斷應用。但是,對於我們的應用,如果不傳遞 profile
,那麼這個元件就會中斷應用。
React 16 版本中提供了一個名為 componentDidCatch
的新的生命週期方法,可以用來優雅地處理元件內部錯誤。雖然在應用中實現邊界錯誤處理是一種很好的做法,但這也會掩蓋單元素元件中的錯誤。
我們必須確保 Avatar
元件本身是可靠的,並考慮到所需要的屬性父元件可能不會傳遞的情況。在這種情況下,除了在使用 profile
之前檢查它是否存在之外,還要使用 Flow
、TypeScript
或 PropTypes
對這種情況給出警告,如下:
const Avatar = ({ profile, ...props }) => (
<img
className="avatar"
src={profile && profile.photoUrl}
alt={profile && profile.photoAlt}
{...props}
/>
);
Avatar.propTypes = {
profile: PropTypes.shape({
photoUrl: PropTypes.string.isRequired,
photoAlt: PropTypes.string.isRequired
}).isRequired
};
複製程式碼
現在我們不傳遞任何屬性來使用 <Avatar />
元件,來看看控制檯會給出什麼警告:
通常,我們會忽略這些警告並在控制檯中累積幾個。因為當新警告出現時,我們永遠不會在意,所以 PropTypes
無法發揮作用。因此,在這些警告累積之前,請務必解決。
規則 #3:應用所有作為屬性傳遞的 HTML 屬性
目前為止,我們的單元素元件使用了名為 profile
的自定義屬性。我們應該避免使用自定義屬性,特別是當它們直接對映為 HTML 屬性時。檢視下面的建議 #1: 避免使用自定義屬性瞭解更多。
通過將所有屬性傳遞給底層元素,就可以在單元素元件中輕鬆實現應用所有 HTML 屬性。我們可以通過傳遞相應的 HTML 屬性來解決自定義屬性問題:
const Avatar = props => <img className="avatar" {...props} />;
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired
};
複製程式碼
現在 Avatar
使用起來更像一個 HTML 元素了:
<Avatar src={profile.photoUrl} alt={profile.photoAlt} />
複製程式碼
如果底層 HTML 元素接受 children
屬性,這條規則也同樣適用。
規則 #4:應用作為屬性傳遞的樣式規則
在應用中的某個地方,你可能希望單元素元件有一個稍微不同的樣式。你應該可以通過 className
或 style
屬性來自定義它。
單元素元件內部樣式等同於瀏覽器應用到原生 HTML 元素的樣式。也就是說,當我們的 Avatar
元件收到一個 className
屬性時,不應該用來替換內部值 — 而是追加進去。
const Avatar = ({ className, ...props }) => (
<img className={`avatar ${className}`} {...props} />
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string
};
複製程式碼
如果我們將 style
屬性應用於 Avatar
元件,可以使用物件擴充套件 輕鬆完成應用:
const Avatar = ({ className, style, ...props }) => (
<img
className={`avatar ${className}`}
style={{ borderRadius: "50%", ...style }}
{...props}
/>
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object
};
複製程式碼
現在我們就可以像下面這樣將新樣式應用到單元素元件:
<Avatar
className="my-avatar"
style={{ borderWidth: 1 }}
/>
複製程式碼
如果你發現自己需要複製新樣式,請毫不猶豫地建立另一個組成 Avatar
的單元素元件。建立一個包含另一個單元素元件沒問題 — 通常也是必須的。
規則 #5:應用所有作為屬性傳遞的事件處理方法
由於我們將所有屬性向下傳遞,單元素元件已經準備好接收任何事件處理屬性。但是,如果元件內部已經應用了這個事件的處理,我們該怎麼辦?
這種情況下,我們有兩個選擇:使用傳遞的處理方法替換掉內部處理方法,或者兩個都呼叫。這取決於你。只要確保始終應用來自屬性傳遞的事件處理方法。
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));
const internalOnLoad = () => console.log("loaded");
const Avatar = ({ className, style, onLoad, ...props }) => (
<img
className={`avatar ${className}`}
style={{ borderRadius: "50%", ...style }}
onLoad={callAll(internalOnLoad, onLoad)}
{...props}
/>
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object,
onLoad: PropTypes.func
};
複製程式碼
建議
建議 #1: 避免使用自定義屬性
在建立單元素元件 — 特別是在應用中開發新功能時 — 很容易去新增自定義屬性來滿足不同的使用。
使用 Avatar
元件舉個例子,設計師建議有些地方頭像是方形的,而其它地方應該是圓形。你也許認為給元件新增一個 rounded
屬性是一個好主意。
除非你正在建立一個文件良好的開源庫,否則,千萬不要那樣。除了文件需要,那樣還會導致不可擴充套件和程式碼的不可維護。總是建立一個新的單元素元件 — 比如 AvatarRounded
— 它會渲染 Avatar
並做一些修改,而不是去新增自定義屬性。
如果你堅持使用獨特的描述性命名、建立可靠的元件,你將會建立成百上千個元件。它們依然是高度可維護的。元件名就可以作為文件。
建議 #2:接收作為屬性傳遞的 HTML 元素
並不是每個自定義屬性都是不好的。有時你想要改變單元素元件中包裹的 HTML 元素。通過新增一個自定義屬性來達到這個目的可能是唯一方法。
const Button = ({ as: T, ...props }) => <T {...props} />;
Button.propTypes = {
as: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
Button.defaultProps = {
as: "button"
};
複製程式碼
一個常見的例子是將 Button
元件渲染為 <a>
元素,如下:
<Button as="a" href="https://google.com">
Go To Google
</Button>
複製程式碼
或者作為另一個元素的使用:
<Button as={Link} to="/posts">
Posts
</Button>
複製程式碼
如果你對這個功能感興趣,我建議你看一下 Reas 專案,這是一個使用 Singel 理念構建的 React UI 工具包。
使用 Singel CLI 來驗證你的單元素元件
最後,在閱讀完所有內容之後,你可能想知道是否有工具可以根據此模式自動驗證元素。我開發了這樣一個工具,叫做 Singel CLI。
如果你想在正在進行的專案中使用它,我建議你建立一個新的資料夾並把你的單元素元件放在裡面。
如果你正在使用 React,你可以通過 npm 安裝 singel
並執行它,如下:
$ npm install --global singel
$ singel components/*.js
複製程式碼
輸出結果類似於下面這樣:
另一種方法是在專案中作為開發依賴安裝,並在 package.json
檔案中新增指令碼:
$ npm install --dev singel
{
"scripts": {
"singel": "singel components/*.js"
}
}
複製程式碼
然後,執行 npm 指令碼吧:
$ npm run singel
複製程式碼
感謝閱讀!
如果你喜歡這篇文章並發現它很有用,你可以通過以下方式來表達你的支援:
- 點選 ❤️ 按鈕喜歡這篇文章
- Star ⭐️ 我的 GitHub 專案:github.com/diegohaz/si…
- 在 GitHub 上關注我:github.com/diegohaz
- 在 Twitter 上關注我:twitter.com/diegohaz
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。