如何在 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 一起使用
- 与盖茨比一起使用