styled-components 通过导出一个 <ThemeProvider>
包装组件来提供完整的主题支持。此组件通过上下文 API 为其下方所有 React 组件提供主题。在渲染树中,所有样式化组件都可以访问提供的主题,即使它们嵌套在多个级别中。
为了说明这一点,让我们创建我们的 Button 组件,但这次我们将一些变量作为主题传递下去。
您也可以为主题道具传递一个函数。此函数将接收父主题,即来自树中更高层级的另一个 <ThemeProvider>
。这样,主题本身就可以变得上下文化。
此示例渲染了我们上面提到的主题化的 Button 和第二个使用第二个 ThemeProvider 反转背景和前景颜色的 Button。函数 invertTheme
接收上层主题并创建一个新主题。
withTheme
高阶组件如果您需要在样式化组件之外使用当前主题(例如,在大型组件内),可以使用 withTheme
高阶组件。
import { withTheme } from 'styled-components' class MyComponent extends React.Component { render() { console.log('Current theme: ', this.props.theme) // ... } } export default withTheme(MyComponent)
useContext
React Hook在使用 React Hooks 时,您也可以使用 useContext
访问样式化组件之外的当前主题。
import { useContext } from 'react' import { ThemeContext } from 'styled-components' const MyComponent = () => { const themeContext = useContext(ThemeContext) console.log('Current theme: ', themeContext) // ... }
useTheme
自定义 Hook在使用 React Hooks 时,您也可以使用 useTheme
访问样式化组件之外的当前主题。
import { useTheme } from 'styled-components' const MyComponent = () => { const theme = useTheme() console.log('Current theme: ', theme) // ... }
您也可以使用 theme
道具将主题传递给组件。
这在绕过缺失的 ThemeProvider
或覆盖它时很有用。
将 ref
道具传递给样式化组件将根据样式化目标为您提供两种结果之一
styled.div
)React.Component
扩展)使用旧版本的 styled-components(低于 4.0.0)或 React?请改用 innerRef
道具。
由于 styled-components 允许您使用任意输入作为插值,因此您必须小心地对该输入进行消毒。使用用户输入作为样式会导致任何 CSS 在用户的浏览器中进行评估,攻击者可以将其放置在您的应用程序中。
此示例展示了不良用户输入甚至会导致代表用户调用 API 端点。
// Oh no! The user has given us a bad URL! const userInput = '/api/withdraw-funds' const ArbitraryComponent = styled.div` background: url(${userInput}); /* More styles here... */ `
请务必小心!这显然是一个虚构的示例,但 CSS 注入可能并不明显,并且会产生不良影响。某些 IE 版本甚至在 url 声明中执行任意的 JavaScript。
有一个即将推出的标准用于从 JavaScript 对 CSS 进行消毒,CSS.escape
。它在不同浏览器中的支持情况并不理想,因此我们建议在您的应用程序中使用 Mathias Bynens 的 polyfill。
如果您选择将 styled-components 与现有 CSS 一起使用,那么您应该注意一些实现细节。
styled-components 使用类生成一个实际的样式表,并通过 className
道具将这些类附加到样式化组件的 DOM 节点。它在运行时将生成的样式表注入到文档头部末尾。
如果您使用 styled(MyComponent)
符号,并且 MyComponent
未渲染传入的 className
道具,则不会应用任何样式。为了避免此问题,请确保您的组件将传入的 className
附加到 DOM 节点
class MyComponent extends React.Component { render() { // Attach the passed-in className to the DOM node return <div className={this.props.className} /> } }
如果您有带有类的预先存在的样式,则可以将全局类与传入的类结合起来
class MyComponent extends React.Component { render() { // Attach the passed-in className to the DOM node return <div className={`some-global-class ${this.props.className}`} /> } }
如果您将全局类与样式化组件类一起应用,结果可能与您的预期不符。如果在一个属性中定义了具有相同特异性的两个类,则最后一个类将获胜。
// MyComponent.js const MyComponent = styled.div`background-color: green;`; // my-component.css .red-bg { background-color: red; } // For some reason this component still has a green background, // even though you're trying to override it with the "red-bg" class! <MyComponent className="red-bg" />
在上面的示例中,样式化组件类优先于全局类,因为 styled-components 默认情况下在运行时将样式注入到 <head>
的末尾。因此,它的样式优先于其他单个类名选择器。
一种解决方案是提高样式表中选择器的特异性
/* my-component.css */ .red-bg.red-bg { background-color: red; }
如果您在不完全控制的页面上部署 styled-components,则可能需要采取预防措施以确保您的组件样式不会与宿主页面的样式发生冲突。
最常见的问题是特异性不足。例如,考虑一个包含以下样式规则的宿主页面
body.my-body button { padding: 24px; }
由于该规则包含一个类名和两个标签名,因此它的特异性高于此样式化组件生成的单个类名选择器
styled.button` padding: 16px; `
虽然没有办法让组件完全不受宿主页面样式的影响,但您可以通过 babel-plugin-styled-components-css-namespace 提高样式规则的特殊性,该插件允许您为所有样式组件指定 CSS 命名空间。如果所有样式组件都渲染在一个具有 #my-widget
的容器中,一个好的命名空间应该是类似于 id="my-widget"
,因为 ID 选择器比任何数量的类名都具有更高的特殊性。
一个不太常见的问题是页面上两个 styled-components 实例之间的冲突。您可以通过在包含 styled-components 实例的代码包中定义 process.env.SC_ATTR
来避免这种情况。该值会覆盖默认的 <style>
标签属性 data-styled
(在 v3 及更低版本中为 data-styled-components
),允许每个 styled-components 实例识别自己的标签。
标记模板字面量是 ES6 中的一项新功能。它们允许您定义自定义字符串插值规则,这就是我们能够创建样式组件的方式。
如果您不传递任何插值,则函数收到的第一个参数是一个包含字符串的数组。
// These are equivalent: fn`some string here`; fn(['some string here']);
一旦您传递插值,该数组将包含传递的字符串,在插值位置处被拆分。其余参数将按顺序为插值。
const aVar = 'good'; // These are equivalent: fn`this is a ${aVar} day`; fn(['this is a ', ' day'], aVar);
这有点难以处理,但意味着我们可以接收变量、函数或 mixin(css
助手)在样式组件中,并可以将其展平为纯 CSS。
说到这里,在展平过程中,styled-components 会忽略求值为 undefined
、null
、false
或空字符串(""
)的插值,这意味着您可以随意使用 短路求值 来有条件地添加 CSS 规则。
const Title = styled.h1<{ $upsideDown?: boolean; }>` /* Text centering won't break if props.$upsideDown is falsy */ ${props => props.$upsideDown && 'transform: rotate(180deg);'} text-align: center; `;
如果您想了解更多关于标记模板字面量的信息,请查看 Max Stoiber 的文章:The magic behind 💅🏾 styled-components
styled-components 支持并发服务器端渲染,并带有样式表重水化功能。基本思想是,每次您在服务器上渲染应用程序时,都可以创建一个 ServerStyleSheet
并向您的 React 树添加一个提供程序,该提供程序通过上下文 API 接收样式。
这不会干扰全局样式,例如 keyframes
或 createGlobalStyle
,并允许您将 styled-components 与 React DOM 的各种 SSR API 配合使用。
为了可靠地执行服务器端渲染并让客户端包无缝接入,您需要使用我们的 babel 插件。它通过向每个样式组件添加确定性 ID 来防止校验和不匹配。有关更多信息,请参考 工具文档。
对于 TypeScript 用户,我们团队中的 TS 大师 Igor Oleinikov 为 webpack ts-loader / awesome-typescript-loader 工具链创建了一个 TypeScript 插件,它完成了一些类似的任务。
如果可能,我们绝对建议使用 babel 插件,因为它更新频率最高。现在可以使用 Babel 编译 TypeScript,因此可能值得关闭 TS 加载器并转到纯 Babel 实现,以利用生态系统优势。
基本 API 如下所示
import { renderToString } from 'react-dom/server'; import { ServerStyleSheet } from 'styled-components'; const sheet = new ServerStyleSheet(); try { const html = renderToString(sheet.collectStyles(<YourApp />)); const styleTags = sheet.getStyleTags(); // or sheet.getStyleElement(); } catch (error) { // handle error console.error(error); } finally { sheet.seal(); }
collectStyles
方法将您的元素包装在一个提供程序中。您可以选择直接使用 StyleSheetManager
提供程序,而不是此方法。只要确保不要在客户端使用它。
import { renderToString } from 'react-dom/server'; import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; const sheet = new ServerStyleSheet(); try { const html = renderToString( <StyleSheetManager sheet={sheet.instance}> <YourApp /> </StyleSheetManager> ); const styleTags = sheet.getStyleTags(); // or sheet.getStyleElement(); } catch (error) { // handle error console.error(error); } finally { sheet.seal(); }
sheet.getStyleTags()
返回多个 <style>
标签的字符串。您需要在将 CSS 字符串添加到 HTML 输出时考虑这一点。
或者,ServerStyleSheet
实例还有一个 getStyleElement()
方法,它返回一个 React 元素数组。
如果渲染因任何原因失败,最好使用 try...catch...finally
来确保 sheet
对象始终可用于垃圾回收。确保 sheet.seal()
仅在 sheet.getStyleTags()
或 sheet.getStyleElement()
被调用之后调用,否则会抛出不同的错误。
sheet.getStyleTags()
和 sheet.getStyleElement()
只能在您的元素被渲染之后调用。因此,来自 sheet.getStyleElement()
的组件不能与 <YourApp />
组合成一个更大的组件。
基本上,您需要添加一个自定义的 pages/_document.js
(如果您还没有)。然后 复制逻辑 以便 styled-components 将服务器端渲染的样式注入到 <head>
中。
请参考 Next.js 仓库中的 示例,以获取最新的使用示例。
从 12 版本开始,Next.js 使用一个名为 SWC 的 Rust 编译器。如果您没有使用任何 babel 插件,您应该参考 这个示例。
在这个版本中,您 只需要添加 styledComponents: true,
在 next.config.js
文件中的编译器选项中,并在 _document
文件中使用 getInitialProps
,如 这个示例 中那样,以支持 SSR。
对于在 Next.js v13+ 中定义的 app/
目录中的路由,您需要在其中一个布局文件中放置一个 styled-components 注册表,如 Next.js 文档中所述。请注意,这取决于 styled-components v6+。还要注意 'use client'
指令的使用 - 因此虽然您的页面将进行服务器端渲染,但 styled-components 仍将出现在您的客户端包中。
Gatsby 有一个官方插件,可以为 styled-components 启用服务器端渲染。
请参考 插件页面,以获取设置和使用说明。
styled-components 提供了一个流式 API,可用于 ReactDOMServer.renderToNodeStream()。流式实现分为两个部分
在服务器上
ReactDOMServer.renderToNodeStream
发出一个 styled-components 包裹的“可读”流。当 HTML 的完整块被推送到流中时,如果任何相应的样式已准备好进行渲染,则样式块将被预先添加到 React 的 HTML 中,并转发到客户端浏览器。
import { renderToNodeStream } from 'react-dom/server'; import styled, { ServerStyleSheet } from 'styled-components'; // if you're using express.js, you'd have access to the response object "res" // typically you'd want to write some preliminary HTML, since React doesn't handle this res.write('<html><head><title>Test</title></head><body>'); const Heading = styled.h1` color: red; `; const sheet = new ServerStyleSheet(); const jsx = sheet.collectStyles(<Heading>Hello SSR!</Heading>); const stream = sheet.interleaveWithNodeStream(renderToNodeStream(jsx)); // you'd then pipe the stream into the response object until it's done stream.pipe(res, { end: false }); // and finalize the response with closing HTML stream.on('end', () => res.end('</body></html>'));
在客户端上
import { hydrate } from 'react-dom'; hydrate(); // your client-side react implementation
客户端重水化完成后,styled-components 将按预期接管,并在重新定位的流式样式之后注入任何进一步的动态样式。
这是一个 **特定于 Web 的** API,你 **无法** 在 react-native 中使用它。
有许多方法可以对组件的样式应用上下文覆盖。也就是说,如果没有使用一个众所周知的目标 CSS 选择器范式并使它们能够在插值中使用,这很少容易。
styled-components 通过“组件选择器”模式干净地解决了此用例。每当通过 styled()
工厂函数创建或包装组件时,还会为其分配一个稳定的 CSS 类以用于目标定位。这允许非常强大的组合模式,而无需担心命名并避免选择器冲突。
一个实际示例:这里,我们的 Icon 组件定义了它对父级 Link 被悬停的响应
我们可以在 Link 组件中嵌套颜色改变规则,但那样的话,我们就必须考虑两组规则,才能理解 Icon 为何表现得这样。
此行为仅在 Styled 组件的上下文中受支持:尝试在以下示例中挂载 B
将失败,因为组件 A
是 React.Component
的实例,而不是 Styled 组件。
class A extends React.Component { render() { return <div /> } } const B = styled.div` ${A} { } `
抛出的错误 - Cannot call a class as a function
- 发生是因为 styled 组件试图将组件作为插值函数调用。
但是,在 styled()
工厂中包装 A
使其能够进行插值 - 只要确保包装的组件传递 className
。
class A extends React.Component { render() { return <div className={this.props.className} /> } } const StyledA = styled(A)`` const B = styled.div` ${StyledA} { } `
styled-components 可选地支持将 CSS 编写为 JavaScript 对象而不是字符串。当您拥有现有的样式对象并希望逐步迁移到 styled-components 时,这特别有用。