如何在 React 中使用樣式化組件
已發表: 2022-03-10樣式化組件是一種 CSS-in-JS 工具,它彌合了組件和样式之間的差距,提供了許多功能,讓您以功能性和可重用的方式啟動和運行樣式化組件。 在本文中,您將學習樣式化組件的基礎知識以及如何將它們正確地應用到您的 React 應用程序中。 在閱讀本教程之前,您應該以前使用過 React。 如果您正在尋找各種樣式的 React 組件選項,您可以查看我們之前關於該主題的帖子。
CSS 的核心是能夠以全局方式定位任何 HTML 元素,無論其在 DOM 樹中的位置如何。 當與組件一起使用時,這可能是一個障礙,因為組件在合理範圍內要求託管(即保持狀態和样式等資產)更接近它們的使用位置(稱為本地化)。
用 React 自己的話來說,樣式化的組件是“組件的視覺原語”,它們的目標是為我們提供一種靈活的方式來設置組件的樣式。 結果是組件及其樣式之間的緊密耦合。
注意:樣式組件可用於 React 和 React Native,雖然您絕對應該查看 React Native 指南,但我們這裡的重點將放在 React 的樣式組件上。
為什麼選擇樣式化組件?
除了幫助您確定樣式範圍之外,樣式化組件還包括以下功能:
- 自動供應商前綴
您可以使用標準 CSS 屬性,並且樣式化的組件將在需要時添加供應商前綴。 - 唯一的類名
樣式化的組件彼此獨立,您不必擔心它們的名稱,因為庫會為您處理。 - 消除死樣式
樣式化組件會刪除未使用的樣式,即使它們已在您的代碼中聲明。 - 還有很多。
安裝
安裝樣式組件很容易。 您可以通過 CDN 或包管理器(例如 Yarn...
yarn add styled-components
… 或 npm:
npm i styled-components
我們的演示使用 create-react-app。
開始
關於樣式化組件,您可能首先會注意到它們的語法,如果您不了解樣式化組件背後的魔力,這可能會令人生畏。 簡而言之,樣式化組件使用 JavaScript 的模板文字來彌合組件和样式之間的差距。 所以,當你創建一個樣式化的組件時,你實際上創建的是一個帶有樣式的 React 組件。 它看起來像這樣:
import styled from "styled-components"; // Styled component named StyledButton const StyledButton = styled.button` background-color: black; font-size: 32px; color: white; `; function Component() { // Use it like any other component. return <StyledButton> Login </StyledButton>; }
在這裡, StyledButton
是樣式化組件,它將被渲染為包含樣式的 HTML 按鈕。 styled
是一種內部實用方法,可將樣式從 JavaScript 轉換為實際的 CSS。
在原始 HTML 和 CSS 中,我們會這樣:
button { background-color: black; font-size: 32px; color: white; } <button> Login </button>
如果 styled 組件是 React 組件,我們可以使用 props 嗎? 我們可以。
基於道具適配
樣式化的組件是函數式的,因此我們可以輕鬆地動態地為元素設置樣式。 假設我們的頁面上有兩種類型的按鈕,一種是黑色背景,另一種是藍色。 我們不必為它們創建兩個樣式化的組件; 我們可以根據他們的道具調整他們的樣式。
import styled from "styled-components"; const StyledButton = styled.button` min-width: 200px; border: none; font-size: 18px; padding: 7px 10px; /* The resulting background color will be based on the bg props. */ background-color: ${props => props.bg === "black" ? "black" : "blue"; `; function Profile() { return ( <div> <StyledButton bg="black">Button A</StyledButton> <StyledButton bg="blue">Button B</StyledButton> </div> ) }
因為StyledButton
是一個接受 props 的 React 組件,所以我們可以根據bg
prop 的存在或值來分配不同的背景顏色。
不過,你會注意到,我們沒有給按鈕一個type
。 讓我們這樣做:
function Profile() { return ( <> <StyledButton bg="black" type="button"> Button A </StyledButton> <StyledButton bg="blue" type="submit" onClick={() => alert("clicked")}> Button B </StyledButton> </> ); }
樣式化的組件可以區分它們接收的道具類型。 他們知道type
是一個 HTML 屬性,所以他們實際渲染<button type="button">Button A</button>
,同時在他們自己的處理中使用bg
屬性。 請注意我們是如何附加事件處理程序的?
說到屬性,擴展語法讓我們可以使用attrs
構造函數來管理 props。 看一下這個:
const StyledContainer = styled.section.attrs((props) => ({ width: props.width || "100%", hasPadding: props.hasPadding || false, }))` --container-padding: 20px; width: ${(props) => props.width}; // Falls back to 100% padding: ${(props) => (props.hasPadding && "var(--container-padding)") || "none"}; `;
注意我們在設置寬度時不需要三元組嗎? 那是因為我們已經為它設置了默認width: props.width || "100%",
width: props.width || "100%",
此外,我們使用 CSS 自定義屬性,因為我們可以!
注意:如果 styled 組件是 React 組件,並且我們可以傳遞 props,那麼我們也可以使用 state 嗎? 圖書館的 GitHub 帳戶有一個問題來解決這個問題。
擴展樣式
假設您正在處理登錄頁面,並且您已將容器設置為某個最大寬度以保持居中。 你有一個StyledContainer
:
const StyledContainer = styled.section` max-width: 1024px; padding: 0 20px; margin: 0 auto; `;
然後,您發現您需要一個較小的容器,兩邊的填充為 10 像素,而不是 20 像素。 您的第一個想法可能是創建另一個樣式組件,您是對的,但您很快就會意識到您正在復製樣式。
const StyledContainer = styled.section` max-width: 1024px; padding: 0 20px; margin: 0 auto; `; const StyledSmallContainer = styled.section` max-width: 1024px; padding: 0 10px; margin: 0 auto; `;
在繼續創建StyledSmallContainer
,就像上面的代碼片段一樣,讓我們學習重用和繼承樣式的方法。 它或多或少類似於spread
運算符的工作方式:
const StyledContainer = styled.section` max-width: 1024px; padding: 0 20px; margin: 0 auto; `; // Inherit StyledContainer in StyledSmallConatiner const StyledSmallContainer = styled(StyledContainer)` padding: 0 10px; `; function Home() { return ( <StyledContainer> <h1>The secret is to be happy</h1> </StyledContainer> ); } function Contact() { return ( <StyledSmallContainer> <h1>The road goes on and on</h1> </StyledSmallContainer> ); }
在您的StyledSmallContainer
中,您將從StyledContainer
獲得所有樣式,但填充將被覆蓋。 請記住,通常情況下,您將獲得為StyledSmallContainer
呈現的部分元素,因為這是StyledContainer
呈現的。 但這並不意味著它是刻在石頭上的或一成不變的。
“as”多態道具
使用as
多態屬性,您可以交換渲染的結束元素。 一個用例是當您繼承樣式時(如上一個示例所示)。 例如,如果您更喜歡div
而不是StyledSmallContainer
的section
,則可以將as
屬性傳遞給帶有您喜歡的元素的值的樣式化組件,如下所示:
function Home() { return ( <StyledContainer> <h1>It's business, not personal</h1> </StyledContainer> ); } function Contact() { return ( <StyledSmallContainer as="div"> <h1>Never dribble when you can pass</h1> </StyledSmallContainer> ); }
現在, StyledSmallContainer
將呈現為div
。 您甚至可以將自定義組件作為您的值:
function Home() { return ( <StyledContainer> <h1>It's business, not personal</h1> </StyledContainer> ); } function Contact() { return ( <StyledSmallContainer as={StyledContainer}> <h1>Never dribble when you can pass</h1> </StyledSmallContainer> ); }
不要認為這是理所當然的。
類 SCSS 語法
CSS 預處理器 Stylis 使樣式化組件能夠支持類似 SCSS 的語法,例如嵌套:
const StyledProfileCard = styled.div` border: 1px solid black; > .username { font-size: 20px; color: black; transition: 0.2s; &:hover { color: red; } + .dob { color: grey; } } `; function ProfileCard() { return ( <StyledProfileCard> <h1 className="username">John Doe</h1> <p className="dob"> Date: <span>12th October, 2013</span> </p> <p className="gender">Male</p> </StyledProfileCard> ); }
動畫
樣式化組件有一個keyframes
助手,可幫助構建(可重用)動畫關鍵幀。 這裡的優點是關鍵幀將從樣式組件中分離出來,並且可以在需要的地方導出和重用。
import styled, {keyframes} from "styled-components"; const slideIn = keyframes` from { opacity: 0; } to { opacity: 1; } `; const Toast = styled.div` animation: ${slideIn} 0.5s cubic-bezier(0.4, 0, 0.2, 1) both; border-radius: 5px; padding: 20px; position: fixed; `;
全局樣式
雖然 CSS-in-JS 以及樣式化組件的最初目標是樣式範圍,但我們也可以利用樣式化組件的全局樣式。 因為我們主要使用範圍樣式,你可能認為這是一個不變的工廠設置,但你錯了。 想一想:什麼是范圍界定? 從技術上講,以全局樣式的名義,我們可以做類似的事情:
ReactDOM.render( <StyledApp> <App /> </StyledApp>, document.getElementById("root") );
但是我們已經有了一個輔助函數createGlobalStyle
它存在的唯一原因就是全局樣式。 那麼,為什麼要否認它的責任呢?
我們可以使用createGlobalStyle
的一件事是規範化 CSS:
import {createGlobalStyle} from "styled-components"; const GlobalStyle = createGlobalStyle` /* Your css reset here */ `; // Use your GlobalStyle function App() { return ( <div> <GlobalStyle /> <Routes /> </div> ); }
注意:使用createGlobalStyle
創建的樣式不接受任何子樣式。 在文檔中了解更多信息。
此時,您可能想知道我們為什麼要費心使用createGlobalStlye
。 這裡有幾個原因:
- 如果沒有它,我們就無法定位根渲染之外的任何內容(例如
html
、body
等)。 -
createGlobalStyle
注入樣式但不渲染任何實際元素。 如果您仔細查看最後一個示例,您會注意到我們沒有指定要呈現的任何 HTML 元素。 這很酷,因為我們實際上可能不需要該元素。 畢竟,我們關心的是全局樣式。 我們的目標是選擇器,而不是特定的元素。 -
createGlobalStyle
沒有作用域,可以在我們的應用程序的任何地方呈現,只要它在 DOM 中就可以應用。 考慮概念,而不是結構。
import {createGlobalStyle} from "styled-components"; const GlobalStyle = createGlobalStyle` /* Your css reset here */ .app-title { font-size: 40px; } `; const StyledNav = styled.nav` /* Your styles here */ `; function Nav({children}) { return ( <StyledNav> <GlobalStyle /> {children} </StyledNav> ); } function App() { return ( <div> <Nav> <h1 className="app-title">STYLED COMPONENTS</h1> </Nav> <Main /> <Footer /> </div> ); }
如果您考慮結構,則app-title
不應像GlobalStyle
中設置的那樣設置樣式。 但它不是那樣工作的。 無論您選擇在何處渲染GlobalStyle
,它都會在您的組件被渲染時被注入。
注意: createGlobalStyles
只有在 DOM 中時才會被渲染。
CSS 助手
我們已經看到瞭如何根據道具調整樣式。 如果我們想更進一步怎麼辦? CSS 輔助函數有助於實現這一點。 假設我們有兩個帶有狀態的文本輸入字段:空的和活動的,每個都有不同的顏色。 我們做得到:
const StyledTextField = styled.input` color: ${(props) => (props.isEmpty ? "none" : "black")}; `;
一切都好。 隨後,如果我們需要添加另一個填充狀態,我們必須修改我們的樣式:
const StyledTextField = styled.input` color: ${(props) => props.isEmpty ? "none" : props.active ? "purple" : "blue"}; `;
現在三元運算的複雜性越來越高。 如果我們稍後在我們的文本輸入字段中添加另一個狀態怎麼辦? 或者,如果我們想為每個州提供除顏色以外的其他樣式怎麼辦? 你能想像將樣式限制在三元運算中嗎? css
助手派上用場了。
const StyledTextField = styled.input` width: 100%; height: 40px; ${(props) => (props.empty && css` color: none; backgroundcolor: white; `) || (props.active && css` color: black; backgroundcolor: whitesmoke; `)} `;
我們所做的是擴展了我們的三元語法以適應更多樣式,並具有更易於理解和組織的語法。 如果前面的陳述看起來不對,那是因為代碼試圖做的太多了。 所以,讓我們退後一步,完善一下:
const StyledTextField = styled.input` width: 100%; height: 40px; // 1. Empty state ${(props) => props.empty && css` color: none; backgroundcolor: white; `} // 2. Active state ${(props) => props.active && css` color: black; backgroundcolor: whitesmoke; `} // 3. Filled state ${(props) => props.filled && css` color: black; backgroundcolor: white; border: 1px solid green; `} `;
我們的改進將樣式分為三個不同的易於管理且易於理解的塊。 這是一場胜利。
樣式表管理器
與 CSS 幫助器一樣, StyleSheetManager
是用於修改樣式處理方式的幫助器方法。 它需要某些道具——比如disableVendorPrefixes
(你可以查看完整列表)——幫助你從其子樹中選擇退出供應商前綴。
import styled, {StyleSheetManager} from "styled-components"; const StyledCard = styled.div` width: 200px; backgroundcolor: white; `; const StyledNav = styled.div` width: calc(100% - var(--side-nav-width)); `; function Profile() { return ( <div> <StyledNav /> <StyleSheetManager disableVendorPrefixes> <StyledCard> This is a card </StyledCard> </StyleSheetManager> </div> ); }
disableVendorPrefixes
作為道具傳遞給<StyleSheetManager>
。 因此,由<StyleSheetManager>
包裝的樣式組件將被禁用,但不是<StyledNav>
中的組件。
更容易調試
在向我的一位同事介紹樣式化組件時,他們的抱怨之一是很難在 DOM 或 React 開發人員工具中找到渲染的元素。 這是樣式化組件的缺點之一:在嘗試提供唯一的類名時,它為元素分配唯一的哈希值,這恰好是神秘的,但它使displayName
可讀,以便於調試。
import React from "react"; import styled from "styled-components"; import "./App.css"; const LoginButton = styled.button` background-color: white; color: black; border: 1px solid red; `; function App() { return ( <div className="App"> <LoginButton>Login</LoginButton> </div> ); }
默認情況下,樣式化的組件在 DOM 中將LoginButton
呈現為<button class="LoginButton-xxxx xxxx">Login</button>
,在 React Developer Tools 中呈現為LoginButton
,這使得調試更容易。 如果我們不想要這種行為,我們可以切換displayName
布爾值。 這需要一個 Babel 配置。
注意:在文檔中,指定了包babel-plugin-styled-components
,以及.babelrc
配置文件。 這樣做的問題是,因為我們使用的是create-react-app
,所以除非我們彈出,否則我們無法配置很多東西。 這就是 Babel 宏的用武之地。
我們需要使用 npm 或 Yarn 安裝babel-plugin-macros
,然後在我們的應用程序的根目錄下創建一個babel-plugin-macros.config.js
,其內容為:
module.exports = { styledComponents: { displayName: true, fileName: false, }, };
反轉fileName
值後, displayName
將以文件名作為前綴,以獲得更獨特的精度。
我們現在還需要從macro
中導入:
// Before import styled from "styled-components"; // After import styled from "styled-components/macro";
結論
既然您可以以編程方式編寫 CSS,請不要濫用自由。 對於它的價值,盡你最大的努力保持你的樣式組件的理智。 不要試圖編寫繁重的條件,也不要認為每一件事都應該是一個樣式化的組件。 此外,不要通過為您只是猜測即將出現的用例創建新生樣式的組件來過度抽象。
更多資源
- 文檔,樣式化的組件
- “使用 React.js 和 styled-components 構建可重用的組件系統”,Lukas Gisder-Dube
- 與 Next.js 一起使用
- 與蓋茨比一起使用