不要再在JavaScript中寫 CSS了
本文作者是 react-css-modules 和 babel-plugin-react-css-modules 的作者。并不是對(duì) CSS in JavaScript: The future of component-based styling,或是使用樣式組件的反對(duì),而是一種補(bǔ)充,web 開發(fā)者要了解自己的需求,明白自己使用 styled-components 的真正原因。
9 個(gè)謊言
CSS 不應(yīng)隨意放置。許多項(xiàng)目選擇將樣式寫在 JavaScript 中的理由不對(duì)。本文列出了常見的誤解,以及解決問題的現(xiàn)存 CSS 方案。
本文的任何言論都沒有對(duì)某個(gè)項(xiàng)目或人進(jìn)行人身攻擊的意思。styled-components 是 React 的目前趨勢(shì),所以我將 styled-components 定義為“JavaScript 中的 CSS”。
styled-components 的發(fā)起人(Max Stoiber、Glen Maddern 以及所有的貢獻(xiàn)者)都很聰明、想法獨(dú)特,出發(fā)點(diǎn)也是好的。
為了完全透明,我還要指出我是 react-css-modules 和 babel-plugin-react-css-modules 的作者。
小紅帽
CSS 和 JavaScript 歷史
層疊樣式表(CSS)是為描述標(biāo)記語言文檔的展現(xiàn)樣式而出現(xiàn)的。JavaScript 是為了組合圖片、插件等組件而創(chuàng)造的一種“膠水語言”。隨著發(fā)展,JavaScript 拓展、轉(zhuǎn)變,有了新的應(yīng)用場(chǎng)景。
Ajax 的出現(xiàn)(2005)是一個(gè)重要的里程碑。這時(shí) Prototype、jQuery、MooTools 等庫(kù)已經(jīng)吸引了大量的擁護(hù)者,共同解決后臺(tái)跨瀏覽器數(shù)據(jù)獲取問題。這又引發(fā)了新的問題:如何管理數(shù)據(jù)?
到了 2010 年,Backbone.js 出現(xiàn),成為了應(yīng)用狀態(tài)管理的行業(yè)標(biāo)準(zhǔn)。不久后,Knockout 和 Angular 雙向綁定的特點(diǎn)吸引了所有人。之后,React 和 Flux 出現(xiàn),開啟了單頁應(yīng)用(SPA)的新紀(jì)元,組件構(gòu)造應(yīng)用。
那么 CSS 呢?
借用 styled-components 文檔中的話:
純 CSS 的問題在于它產(chǎn)生的那個(gè)時(shí)代,網(wǎng)站由文檔組成。1993 年,網(wǎng)站產(chǎn)生,主要用于交換科學(xué)文獻(xiàn),CSS 是設(shè)計(jì)文獻(xiàn)樣式的解決方案。但是如今我們構(gòu)建的是豐富的、面向用戶的交互應(yīng)用,而 CSS 并不是為此而生的。
我不這么認(rèn)為 。
CSS 已經(jīng)發(fā)展到可以滿足現(xiàn)代 UI 的需求了。過去十年中出現(xiàn)的新特性數(shù)不勝數(shù)(pseudo-classes、pseudo-elements、CSS variables、media queries、keyframes、combinators、columns、flex、grid、computed values 等等)。
從 UI 的角度看,“組件”是文檔中一個(gè)獨(dú)立的片段(<button /> 就是個(gè)組件)。CSS 被設(shè)計(jì)用來樣式化文檔,包括所有組件。問題在哪?
俗話說:“工欲善其事必先利其器”。
Styled-components
styled-components 可以用標(biāo)記模板字面量在 JavaScript 中寫 CSS。這樣就省去了組件和樣式間的匹配 ——組件由細(xì)粒度的樣式結(jié)構(gòu)組成,比如:
- import React from 'react';
- import styled from 'styled-components';
- // Create a <Title> react component that renders an <h1> which is
- // centered, palevioletred and sized at 1.5em
- const Title = styled.h1`
- font-size: 1.5em;
- text-align: center;
- color: palevioletred;
- `;
- // Create a <Wrapper> react component that renders a <section> with
- // some padding and a papayawhip background
- const Wrapper = styled.section`
- padding: 4em;
- background: papayawhip;
- `;
- // Use them like any other React component – except they're styled!
- <Wrapper>
- <Title>Hello World, this is my first styled component!</Title>
- </Wrapper>
結(jié)果:
Live demo(https://www.webpackbin.com/bins/-KeeZCr0xKfutOfOujxN)
styled-components 目前是 React 的 趨勢(shì) 。
我們要理清一件事情:styled-components 只是 CSS 層面的高度抽象。它只是解析定義在 JavaScript 中的 CSS,然后生成對(duì)應(yīng) CSS 的 JSX 元素。
我不喜歡這個(gè)趨勢(shì),因?yàn)榇嬖诤芏嗾`解。
我在 IRC、Reddit 和 Discord 上調(diào)查了大家使用 styled-components 的原因,整理了一份選擇使用 styled-components 常見原因的列表 。我稱之為 myths。
Myth #1:避免全局命名空間和樣式?jīng)_突
我把這條算作 myth 是因?yàn)樗犉饋砭拖裰斑@些問題沒有得到解決一樣。CSS Modules、Shadow DOM 還有很多命名協(xié)議(比如 BEM)已經(jīng)早就在社區(qū)中解決了這個(gè)問題。
styled-components(就像 CSS modules)只是替人完成了命名的任務(wù)。人總會(huì)犯錯(cuò),計(jì)算機(jī)犯錯(cuò)少點(diǎn)而已。
但就本身而言,這并不是使用 styled-components 的好理由。
Myth 2:styled-components 可以簡(jiǎn)明代碼
通常伴隨著如下的例子:
- <TicketName></TicketName>
- <div className={styles.ticketName}></div>
首先——關(guān)系不大。差異基本可以忽略。
其次,說的也不對(duì)。字符數(shù)量取決于樣式命名。
- <TinyBitLongerStyleName></TinyBitLongerStyleName>
- <div className={styles.longerStyleName}></div>
這同樣適用于本文之后的構(gòu)造樣式(Myth 5:給組件設(shè)置條件樣式更簡(jiǎn)單)。styled-components 只是在多數(shù)基本組件的情況下稍勝一籌。
Myth 3:styled-components 使人更關(guān)注語義化
前提就不對(duì)。樣式和語義化代表著不同的問題,需要不用的應(yīng)對(duì)方案。引用 Adam Morse(mrmrs)的話:
內(nèi)容語義化和視覺樣式 沒有半點(diǎn)關(guān)系。當(dāng)我用樂高建造東西時(shí),我從來不會(huì)想“這是引擎的一部分”,我想著“這是個(gè) 1×4 的藍(lán)色樂高,我用來隨便做什么都行”。不論水下潛水基地還是飛機(jī)——我清晰地知道怎么用這個(gè)樂高塊。
– http://mrmrs.io/writing/2016/03/24/scalable-css/
(強(qiáng)烈建議讀一讀 Adam 關(guān)于 可拓展 CSS 的文章)
我們還可以舉個(gè)例子看看兩者是否相關(guān)。
示例:
- <PersonList>
- <PersonListItem>
- <PersonFirstName>Foo</PersonFirstName>
- <PersonLastName>Bar</PersonLastName>
- </PersonListItem>
- </PersonList>
語義化是要使用正確的標(biāo)簽構(gòu)造標(biāo)記。你能知道這些組件會(huì)渲染成什么 HTML 標(biāo)簽嗎?不,你不知道。
和下面這段代碼比較下:
- <ol>
- <li>
- <span className={styles.firstName}>Foo</span>
- <span className={styles.lastName}>Bar</span>
- </li>
- </ol>
Myth 4:拓展樣式更容易
v1 版本可以用 styled(StyledComponent) 拓展樣式;v2 引進(jìn)了 extend 方法來拓展已存在的樣式,比如:
- const Button = styled.button`
- padding: 10px;
- `;
- const TomatoButton = Button.extend`
- color: #f00;
- `;
這挺好。但是你可以在 CSS 中完成(或者使用 CSS 模塊組合 或 SASS 繼承混合 @extend)。
- button {
- padding: 10px;
- }
- button.tomato-button {
- color: #f00;
- }
難道不比 JavaScript 簡(jiǎn)單?
Myth 5:給組件設(shè)置條件樣式更簡(jiǎn)單
這點(diǎn)是說你可以根據(jù)組件屬性給組件設(shè)置樣式,比如:
- <Button primary />
- <Button secondary />
- <Button primary active={true} />
這在 React 中很有用。畢竟組件行為就是由屬性控制的。給屬性值直接綁定樣式有意義嗎?可能吧。但是來看看組件的實(shí)現(xiàn)代碼:
- styled.Button`
- background: ${props => props.primary ? '#f00' : props.secondary ? '#0f0' : '#00f'};
- color: ${props => props.primary ? '#fff' : props.secondary ? '#fff' : '#000'};
- opacity: ${props => props.active ? 1 : 0};
- `;
利用 JavaScript 按條件創(chuàng)造樣式表是挺強(qiáng)大的,但是這也意味著樣式難以理解,對(duì)比以下 CSS:
- button {
- background: #00f;
- opacity: 0;
- color: #000;
- }
- button.primary,
- button.seconary {
- color: #fff;
- }
- button.primary {
- background: #f00;
- }
- button.secondary {
- background: #0f0;
- }
- button.active {
- opacity: 1;
- }
這樣 CSS 更簡(jiǎn)短(229 VS 222 字符),(個(gè)人認(rèn)為)也更容易理解。此外,還可以用預(yù)處理器使 CSS 分組、更短:
- button {
- background: #00f;
- opacity: 0;
- color: #000;
- &.primary,
- &.seconary {
- color: #fff;
- }
- &.primary {
- background: #f00;
- }
- &.secondary {
- background: #0f0;
- }
- &.active {
- opacity: 1;
- }
- }
Myth 6:有利于代碼組織
有些人告訴我他們喜歡 styled-components,因?yàn)樗梢宰寴邮胶?JavaScript 在一個(gè)文件中。
我理解同一組件有許多文件很煩,但是把樣式和標(biāo)記塞進(jìn)一個(gè)文件的方法很糟糕。這樣不僅版本控制難以回溯,而且所有組件都需要滾動(dòng)很長(zhǎng)一段距離,而不是簡(jiǎn)單地點(diǎn)下按鈕。
如果一定要把 CSS 和 JavaScript 放在一個(gè)文件中, 可以考慮使用 css-literal-loader。它可以在 build 時(shí)用 extract-text-webpack-plugin 提取 CSS,用標(biāo)準(zhǔn) loader 配置處理 CSS。
Myth 7:DX 很方便,這工具太棒了!
很明顯你沒用過 styled-components。
- 一旦樣式寫錯(cuò)了,整個(gè) app 會(huì)崩潰,并輸出長(zhǎng)長(zhǎng)的調(diào)用棧錯(cuò)誤(v2 更奇葩)。相比之下,CSS “style error” 只是元素渲染地不對(duì)而已。
- 元素沒有 className,所以調(diào)試時(shí)不得不去對(duì)比 React 元素樹和 DevTools DOM 樹(v2 可以用 babel-plugin-styled-components 定位)。
- 沒有語法檢查(有一款 樣式檢查插件 正在開發(fā)中)。
- 不合法的樣式會(huì)被忽略(比如:clear: both; float left; color: #f00; 不會(huì)報(bào) error 或 warning,只能祈禱調(diào)試好運(yùn)了,即使看了 styled-components 源碼,還是花了我 15 分鐘查看調(diào)用棧。最后我在聊天中把代碼粘出來尋求幫助,才有人提醒是少了:。你注意到了嗎?)
- 支持語法高亮、代碼補(bǔ)全以及其它 IDE 細(xì)節(jié)的 IDE并不多。如果你在金融或政府機(jī)構(gòu)工作,很可能無法使用 Atom IDE。
Myth 8:性能更好,bundle 更小
- 事實(shí)是,styled-components 無法提取靜態(tài) CSS 文件(比如使用 https://github.com/webpack-contrib/extract-text-webpack-plugin)。這意味著瀏覽器無法開始解釋樣式直到 styled-components 解析、加載到 DOM上。
- 缺少文件分離意味著無法分開緩存 CSS 和 JavaScript。
- 所有樣式化的組件都會(huì)額外包裝一層 HoC。這是不必要的性能損耗。因?yàn)轭愃频慕Y(jié)構(gòu)缺陷,我終止了 https://github.com/gajus/react-css-modules(但創(chuàng)建了 https://github.com/gajus/babel-plugin-react-css-modules)。
- 因?yàn)?HOC,如果在服務(wù)端渲染,會(huì)導(dǎo)致標(biāo)記文檔大很多。
- 有 keyframes, 我也不需要用動(dòng)態(tài)樣式值做動(dòng)畫。
Myth 9:它可以開發(fā)響應(yīng)式組件
這說的是依據(jù)環(huán)境給組件設(shè)置樣式的能力,比如父容器偏移量、子元素?cái)?shù)量等。
首先,styled-components 和響應(yīng)式?jīng)]什么關(guān)系。這已經(jīng)超出了這個(gè)主題的范圍。這種情況最好直接設(shè)置組件的 style,以避免額外的成本。
但是,元素查詢是個(gè)有趣的問題,也逐漸成為 CSS 中的一個(gè)高熱話題,主要是 EQCSS 等類似項(xiàng)目。元素查詢和 @media queries 在語法上很相似,只是元素查詢操作具體某些元素。
- <a href="http://www.jobbole.com/members/feiguohai46">@element</a> {selector} and {condition} [ and {condition} ]* { {css} }
{selector} 是 CSS 選擇器對(duì)應(yīng)著一或多個(gè)元素。例如:#id 或 .class
{condition} 由尺寸和值組成。
{css} 可以包含:任何合法的 CSS 規(guī)則。(例如:#id div { color: red })
元素查詢可以用 min-width、max-width、min-height、max-height、min-characters、max-characters、min-children、max-children、min-lines、max-lines、min-scroll-x、max-scoll-x 等 (詳見 http://elementqueries.com/)條件給元素設(shè)置樣式。
總有一天類似 EQCSS 的內(nèi)容也會(huì)出現(xiàn)在 CSS 標(biāo)準(zhǔn)中的(希望如此)。
等下!
大部分內(nèi)容都長(zhǎng)期有效,無論是社區(qū)、React 變更或 styled-components 本身。但意義何在?CSS 已被廣泛支持,有大量的社區(qū),也確實(shí)行之有效。
本文的目的并不是阻止讀者在 JavaScript 中使用“CSS”或是 styled-components。styled-components 一個(gè)很棒的使用場(chǎng)景是:更好的跨平臺(tái)支持性。不要因?yàn)殄e(cuò)誤的理由使用它。
那么我們應(yīng)該用什么呢?
使用 Shadow DOM v1 還為時(shí)尚早(51% 支持率)。CSS 應(yīng)遵循命名協(xié)議(建議 BEM),如果擔(dān)心類名沖突(或懶得用 BEM),可以用 CSS modules。如果你在開發(fā) React web,考慮用 babel-plugin-react-css-modules。如果在開發(fā) React Native,styled-components 更好。