聊聊 React 組件庫的技術選型與設計

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近在業務中開發了一套定製化的 C 端組件庫,在這個過程中遇到了一些組件庫技術選型和設計的問題,在參考公司內外的多個組件庫後確定了最終的方案。本文希望通過向讀者介紹技術選型的過程中的方案比較和組件庫設計中的考量,讓讀者在組件庫的技術選型和設計上有所啓發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f4\/f4fa8d8b2431f9a8d5d4e606e87a4c06.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個完整的組件庫方案的思路"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"組件庫的技術選型"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"樣式方案選擇"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/24\/248c15374c57d407c3d87cfdab05e576.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,這三種樣式方案可以並存,但實際開發以其中一種爲主。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Sass\/Less"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是大家最熟悉的方式,它的優點是足夠靈活、開發成本低(絕大多數工程師都熟悉它們)、 完全支持外部覆蓋組件的樣式,缺點是難以調試(需要到 runtime 才能知道命中的規則),以及難以實現靜態分析。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Atomic CSS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 UI 足夠標準化的情況下,使用 Atomic CSS 能實現更小的包體積大小,對於單個組件,除了極少數無法抽象的樣式以及自定義動畫,不再需要聲明其他樣式。當然它的缺點是代碼可讀性稍稍降低。同時開發者需要先熟悉項目的原子樣式,增加了一定的開發成本。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"CSS-in-JS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CSS-in-JS 指包括 styled-component、Emotion、JSS 等在內的,在運行時通過 js 生成 css 樣式的第三方庫。CSS-in-JS 這種方案的優點在於能有效解決“組件樣式隨着數據變化”的問題。但是,它的缺點在於爲了支持從外部覆蓋內部元素的樣式,需要給內部元素加上 className,同時不支持 postcss,取而代之的是特定 CSS-in-JS 庫自己的 plugin 生態,少部分庫(如 emotion)沒有支持 rem 的工具庫。另外在做 SSR 和流式渲染時,都需要在 node 層增加提取樣式邏輯,增加了開發成本和額外的開銷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"小結:在有成熟的 UI 規範的情況下,Atomic CSS 是一個不錯的選擇,其次,使用傳統的 sass\/less 來編寫樣式也利於維護(大部分前端開發者都熟悉它),在選用 CSS-in-JS 方案時則要考慮團隊的開發習慣和上手成本。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"icon 方案選擇"}]},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/d1\/92\/d1c58f2b3244e0d37bb1acaa3bafa492.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在選擇 icon 方案的時候,除了關注渲染質量,我們還應該關注它的靈活性,以便具有更好的適配能力。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"iconfont"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iconfont 這種方案的優點在於兼容性最好,支持 IE6 及以上版本。但是,由於 iconfont 方案是將 icon 作爲文本來使用,"},{"type":"text","marks":[{"type":"strong"}],"text":"在 webkit 內核的瀏覽器下由於對文字有抗鋸齒,導致渲染失真"},{"type":"text","text":"。另外,由於將所有的 icon 打包成一個字體文件,不支持按需加載,包體積偏大。這樣很容易導致"},{"type":"text","marks":[{"type":"strong"}],"text":"在加載完成 icon font 後頁面的重刷新"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/d5\/3f\/d50b341260106b9ebee6d1982670ca3f.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"base64 引入"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"base64 也是一種常用的方法,但是由於將 svg 作爲背景圖引入,只能控制它的大小,不能覆蓋它的顏色,也更不能修改 svg 內部的元素,不夠靈活。對於常常採用 MPA 結構的端內 h5,不利於 icon 在不同 SPA 之間複用。同時 base64 字符串的長度是 svg 文件(優化後)的 1.3 倍左右。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"React Component、SVGUseElement 和直接寫入 svg 元素"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這三種方式本質上都是將 svg 作爲 html 元素進行渲染,但具體的使用方式不同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"svg 的基本能力的兼容性除了在 IE11 以下不支持動畫和縮放,基本沒問題,而 svg effect(主要是使用 transform、filter 等屬性)在 android4.4 以上的支持良好。svg 的動畫性能有瓶頸,幸運的是我們可以使用 css 動畫來替代它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直接寫入 SVG 元素的方式缺點在於"},{"type":"text","marks":[{"type":"strong"}],"text":"完全無法複用同一個 icon"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 SVGUseElement 的具體實現方式有使用元素、 元素和 SVG fragment identifiers 等方式,但總的來說,都是在頂部聲明 svg 元素,在需要使用的地方使用元素引入。具體可以參考使用 SVGUseElement 插入 icon 的"},{"type":"text","marks":[{"type":"strong"}],"text":"例子"},{"type":"text","text":"[1]。它的缺點在於"},{"type":"text","marks":[{"type":"strong"}],"text":"不夠靈活,icon 難以在不同頁面複用,同時支持 SSR 也比較困難"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前調研的結果,最好的方式是使用 "},{"type":"text","marks":[{"type":"strong"}],"text":"svgr"},{"type":"text","text":"[2] 將 svg 轉換爲 React Component 來使用,它支持按需加載、完全的樣式覆蓋能力。同時,它支持自定義 AST 模板,可以在轉換時給 svg 元素加入自定義的 className 等,容易實現 icon 自動適配 RTL、Dark Mode(這部分下文會詳細介紹)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"svgr 集成了 svgo 對 svg 文件進行優化,它可以抹去 svg 中無用的屬性、隱藏元素等,具體的配置可以參考 "},{"type":"text","marks":[{"type":"strong"}],"text":"svgo-github"},{"type":"text","text":"[3]。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"小結:目前看來使用 svgr 將 svg 轉換生成 React Component 來構建 icon 是最佳的方式,能很方便地按需加載、複用,適配能力也最強。我們可以*"},{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"將 icon 專門做成一個 npm 包*"},{"type":"text","marks":[{"type":"strong"}],"text":",供組件庫使用,也可以在業務倉庫中直接使用。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"組件庫的核心設計"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"深色模式(Dark Mode)適配"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,本小節討論的是業務上使用組件庫的 Dark Mode 能力時會遇到的兼容性問題和實際業務場景。但組件庫本身就是服務於業務的,從這個角度講本小節的內容也屬於組件庫相關的一部分,它指導組件庫如何去提供更好的 Dark Mode 適配能力。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"多主題能力"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"深色模式本質上是一種運行時的多主題問題,主要是在運行時支持切換不同的主題色。我們可以使用 CSS 變量來定義顏色,然後在 Sass\/Less\/Css 中約定使用它:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":":root{\n --bg-default: #fff;\n}\n:root[theme=\"dark\"]{\n --bg-default: #000;\n}\n.button{\n background-color: var(--bg-default);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,只要我們在元素中設置自定義屬性 theme 的值爲 dark,顏色就會自動切換。且我們只要定義好顏色變量,並約定使用它,則開發組件的時候只寫一次就可以支持多個主題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可惜的是 CSS 變量在 android4、IE11 及以下等有兼容性問題。我們有如下三種方案:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/6d\/a4\/6d7e694055408d073ff3be8419da74a4.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以實現一個 postcss plugin 來生成兜底屬性,做法類似於:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ 處理前\n.button{\n background-color: var(--bg-default);\n}\n\n\/\/ 處理後\n.button{\n background-color: #fff; \/\/ 對於不支持css變量的瀏覽器這行會生效\n background-color: var(--bg-default); \/\/ 對於支持css變量的瀏覽器這行會覆蓋上一行屬性\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它最大的優點在於增大的包大小几乎可以忽略不計,缺點在於對於不支持 CSS 變量的顏色實際上變成了強制展示一套兜底主題色。對於移動端內頁面來說,不支持 css 變量的環境可以等同於沒有深色模式的環境,可以使用淺色模式的主題色兜底。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還有另一種方式來實現兼容,比如下面這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":".button{\n background-color: #fff;\n}\n.theme-a .button{\n background-color: #000;\n}\n.thema-b .button{\n background-color: #ccc;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後在某個根元素上(例如 html)增加 theme-a 這個 class 即可,這樣的優點在於完全不會有兼容性問題,缺點在於增加了開發成本,幸運的是,我們可以使用"},{"type":"text","marks":[{"type":"strong"}],"text":"postcss-css-variables"},{"type":"text","text":"[4]來很方便地從 css 變量的寫法生成這種聲明。它的另一個缺點是隨着主題色的增多,會成倍地產生額外的 CSS 包大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"css-vars-ponyfill 能完美支持多主題色,缺點是會產生固定的額外包大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"小結:支持運行時多主題色主要使用 css 變量,而業務倉庫的解決兼容性問題,可以根據具體情況選擇。如果是端內 h5 且只需要深淺色模式,可以考慮使用 postcss plugin 生成兜底屬性,否則可以使用 css-vars-ponyfill 或者 postcss-css-variables。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"判斷 Dark Mode"}]},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/49\/f9\/49173e91c1dd6a3e8f8cce7553e39ff9.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"媒體查詢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以很容易的利用 prefers-color-scheme 這個媒體特性來檢測 Dark Mode,結合我們 css 變量的使用,就像這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":":root{\n --bg-default: #fff;\n}\n@media (prefers-color-scheme: dark) {\n :root{\n --bg-default: #000;\n }\n}\n\/\/ 支持白名單逃逸,再寫一次:root下的屬性\n:root[theme=\"light\"]{\n --bg-default: #fff;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白名單逃逸是指在我們的業務中,可能有一部分頁面,如活動頁、抽獎頁等不支持 Dark Mode,我們可以通過在 html 上增加一個 theme 屬性來強制爲淺色模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"媒體查詢的優點是"},{"type":"text","marks":[{"type":"strong"}],"text":"使用方便"},{"type":"text","text":",媒體查詢會自動監聽系統設置的變化(是否開啓深色模式)"},{"type":"text","marks":[{"type":"strong"}],"text":"不用在 html 中增加額外代碼"},{"type":"text","text":"。缺點在於"},{"type":"text","marks":[{"type":"strong"}],"text":"對需要逃逸的情況,書寫比較繁瑣"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"JS API 監聽媒體查詢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 JS API 的例子如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章