高级

主题

styled-component提供<ThemeProvider>包装组件以支持主题.<ThemeProvider>通过contextAPI 为其后代组件提供主题.在其渲染树中的所有组件都能够访问主题.

下面的示例通过创建一个按钮组件来说明如何传递主题:

  1. // Define our button, but with the use of props.theme this time
  2. const Button = styled.button`
  3. font-size: 1em;
  4. margin: 1em;
  5. padding: 0.25em 1em;
  6. border-radius: 3px;
  7. /* Color the border and text with theme.main */
  8. color: ${props => props.theme.main};
  9. border: 2px solid ${props => props.theme.main};
  10. `;
  11. // We are passing a default theme for Buttons that arent wrapped in the ThemeProvider
  12. Button.defaultProps = {
  13. theme: {
  14. main: "palevioletred"
  15. }
  16. }
  17. // Define what props.theme will look like
  18. const theme = {
  19. main: "mediumseagreen"
  20. };
  21. render(
  22. <div>
  23. <Button>Normal</Button>
  24. <ThemeProvider theme={theme}>
  25. <Button>Themed</Button>
  26. </ThemeProvider>
  27. </div>
  28. );

函数主题

theme prop 也可以传递一个函数.该函数接收渲染树上级<ThemeProvider>所传递的主题. 通过这种方式可以使 themes 形成上下文.

下面的示例说明了如何通过第二个<ThemeProvider>来交换 backgroundforeground的颜色. 函数invertTheme 接收上级 theme 后创建一个新的 theme.

  1. // Define our button, but with the use of props.theme this time
  2. const Button = styled.button`
  3. color: ${props => props.theme.fg};
  4. border: 2px solid ${props => props.theme.fg};
  5. background: ${props => props.theme.bg};
  6. font-size: 1em;
  7. margin: 1em;
  8. padding: 0.25em 1em;
  9. border-radius: 3px;
  10. `;
  11. // Define our `fg` and `bg` on the theme
  12. const theme = {
  13. fg: "palevioletred",
  14. bg: "white"
  15. };
  16. // This theme swaps `fg` and `bg`
  17. const invertTheme = ({ fg, bg }) => ({
  18. fg: bg,
  19. bg: fg
  20. });
  21. render(
  22. <ThemeProvider theme={theme}>
  23. <div>
  24. <Button>Default Theme</Button>
  25. <ThemeProvider theme={invertTheme}>
  26. <Button>Inverted Theme</Button>
  27. </ThemeProvider>
  28. </div>
  29. </ThemeProvider>
  30. );

styled-components外使用主题

如果需要在styled-components外使用主题,可以使用高阶组件withTheme:

  1. import { withTheme } from 'styled-components'
  2. class MyComponent extends React.Component {
  3. render() {
  4. console.log('Current theme: ', this.props.theme)
  5. // ...
  6. }
  7. }
  8. export default withTheme(MyComponent)

theme prop

主题可以通过theme prop传递给组件.通过使用theme prop可以绕过或重写ThemeProvider所提供的主题.

  1. // Define our button
  2. const Button = styled.button`
  3. font-size: 1em;
  4. margin: 1em;
  5. padding: 0.25em 1em;
  6. border-radius: 3px;
  7. /* Color the border and text with theme.main */
  8. color: ${props => props.theme.main};
  9. border: 2px solid ${props => props.theme.main};
  10. `;
  11. // Define what main theme will look like
  12. const theme = {
  13. main: "mediumseagreen"
  14. };
  15. render(
  16. <div>
  17. <Button theme={{ main: "royalblue" }}>Ad hoc theme</Button>
  18. <ThemeProvider theme={theme}>
  19. <div>
  20. <Button>Themed</Button>
  21. <Button theme={{ main: "darkorange" }}>Overidden</Button>
  22. </div>
  23. </ThemeProvider>
  24. </div>
  25. );

Refs

通过传递ref prop给 styled component 将获得:

  • 底层 DOM 节点 (如果 styled 的对象是基本元素如 div)
  • React 组件实例 (如果 styled 的对象是 React Component) ``jsx const Input = styled.input padding: 0.5em; margin: 0.5em; color: palevioletred; background: papayawhip; border: none; border-radius: 3px; `;

class Form extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); }

render() { return ( { this.inputRef.current.focus() }} /> ); } }

render(

);

  1. >注意
  2. >
  3. >v3 或更低的版本请使用 [innerRef prop](https://www.styled-components.com/docs/api#innerref-prop) instead.
  4. ## 安全性
  5. 因为 styled-components 允许使用任意的输入作为插值使用,我们必须谨慎处理输入使其无害.使用用户输入作为样式可能导致用户浏览器中的CSS文件被攻击者替换.
  6. 以下这个例子展示了糟糕的输入导致的 API 被攻击:
  7. ```jsx
  8. // Oh no! The user has given us a bad URL!
  9. const userInput = '/api/withdraw-funds'
  10. const ArbitraryComponent = styled.div`
  11. background: url(${userInput});
  12. /* More styles here... */
  13. `

请一定谨慎处理!这虽然是一个明显的例子,但是CSS注入可能隐式的发生并且产生不良影响.有些旧版本的 IE 甚至会在 url 声明中执行 JavaScript.

There is an upcoming standard to sanitize CSS from JavaScript有一个即将推出的标准,可以用于无害化 JavaScript 中的 CSS, CSS.escape. 这个标准还没有被浏览器很好的支持,因此建议使用 polyfill by Mathias Bynens .

Existing CSS

如果想将 styled-components 和现有的 CSS 共同使用,有很多实现的细节必须注意到.

styled-components 通过类生成实际的样式表,并通过className prop将这些类附加到响应的 DOM 节点. 运行时它会被注入到 document 的 head 末尾.

Styling normal React components

使用styled(MyComponent) 声明, MyComponent 却不接收传入的 className prop, 则样式并不会被呈现. 为避免这个问题,请确保组件接收 className 并传递给 DOM 节点:

  1. class MyComponent extends React.Component {
  2. render() {
  3. // Attach the passed-in className to the DOM node
  4. return <div className={this.props.className} />
  5. }
  6. }

对于已存在类名的组件,可以将其余传入的类合并:

  1. class MyComponent extends React.Component {
  2. render() {
  3. // Attach the passed-in className to the DOM node
  4. return <div className={`some-global-class ${this.props.className}`} />
  5. }
  6. }

Issues with specificity

styled-components类与全局类混用,可能会导致出乎意料的结果.如果一个property在两个类中被定义且两个类的优先级相同,则后者会覆盖前者.

  1. // MyComponent.js
  2. const MyComponent = styled.div`background-color: green;`;
  3. // my-component.css
  4. .red-bg {
  5. background-color: red;
  6. }
  7. // For some reason this component still has a green background,
  8. // even though you're trying to override it with the "red-bg" class!
  9. <MyComponent className="red-bg" />

上述例子中styled-components类的样式覆盖了全局类,因为styled-components在运行时向<head>末尾注入样式.

一种解决方式是提高全局样式的优先级:

  1. /* my-component.css */
  2. .red-bg.red-bg {
  3. background-color: red;
  4. }

避免与第三方样式和脚本的冲突

如果在一个不能完全控制的页面上部署styled-components,可能需要采取措施确保 component styles 不与 host page 上其他样式冲突.

常见的问题是优先级相同,例如 host page 上持有如下样式:

  1. body.my-body button {
  2. padding: 24px;
  3. }

因为其包含一个类名和两个标签名,它的优先级要高于 styled component 生成的一个类名的选择器:

  1. styled.button`
  2. padding: 16px;
  3. `

没有让 styled component 完全不受 host page 样式影响的办法.但是可以通过babel-plugin-styled-components-css-namespace来提高样式的优先级, 通过它可以为 styled components 的类指定一个命名空间. 一个好的命名空间,譬如#my-widget,可以实现styled-components 在 一个 id="my-widget"的容器中渲染, 因为 id 选择器的优先级总是高于类选择器.

一个罕见的问题是同一页面上两个styled-components实例的冲突.通过在 code bundle 中定义 process.env.SC_ATTR 可以避免这个问题. 它将覆盖 <style>标签的data-styled属性, (v3 及以下版本使用 data-styled-components), allowing each styled-components instance to recognize its own tags.

媒体模板

开发响应式 web app 时媒体查询是不可或缺的工具.

以下是一个非常简单的示例,展示了当屏宽小于700px时,组件如何改变背景色:

  1. const Content = styled.div`
  2. background: papayawhip;
  3. height: 3em;
  4. width: 3em;
  5. @media (max-width: 700px) {
  6. background: palevioletred;
  7. }
  8. `;
  9. render(
  10. <Content />
  11. );

由于媒体查询很长,并且常常在应用中重复出现,因此有必要为其创建模板.

由于 JavaScript 的函数式特性,我们可以轻松的定义自己的标记模板字符串用于包装媒体查询中的样式.我们重写一下上个例子来试试:

  1. const sizes = {
  2. desktop: 992,
  3. tablet: 768,
  4. phone: 576,
  5. }
  6. // Iterate through the sizes and create a media template
  7. const media = Object.keys(sizes).reduce((acc, label) => {
  8. acc[label] = (...args) => css`
  9. @media (max-width: ${sizes[label] / 16}em) {
  10. ${css(...args)}
  11. }
  12. `
  13. return acc
  14. }, {})
  15. const Content = styled.div`
  16. height: 3em;
  17. width: 3em;
  18. background: papayawhip;
  19. /* Now we have our methods on media and can use them instead of raw queries */
  20. ${media.desktop`background: dodgerblue;`}
  21. ${media.tablet`background: mediumseagreen;`}
  22. ${media.phone`background: palevioletred;`}
  23. `;
  24. render(
  25. <Content />
  26. );

标记模板字符串

模板字符串是 ES6 的新功能.它允许我们自定义字符串插值规则—styled components 正是基于此功能实现.

如果没有传递插值,则函数接收的一个参数是包含一个字符串的数组:

  1. // These are equivalent:
  2. fn`some string here`
  3. fn(['some string here'])

如果传递了插值,则数组中包含了传递的字符串, split at the positions of the interpolations.其余参数将按顺序进行插值.

  1. const aVar = 'good'
  2. // These are equivalent:
  3. fn`this is a ${aVar} day`
  4. fn(['this is a ', ' day'], aVar)

这用起来有点笨重,但是这意味着我们可以在 styled components 中接收变量,函数或是 mixins ,并且可以将它们转换成纯 CSS.

想了解有关标记模板字符串的更多信息, 请参阅 Max Stoiber 的文章: The magic behind 💅 styled-components

服务端渲染

styled-components 通过样式注水(with stylesheet rehydration)支持并发服务端渲染. 其核心思想是,每当在服务器上渲染应用时, 为 React 树创建一个ServerStyleSheet 和一个 provider ,通过 context API 来接收样式.

这不会影响全局样式,例如 keyframes 或者 createGlobalStyle ,并且允 styled-components 与 React DOM 的 SSR API 共同使用.

设置

为了可靠的执行 SSR,正确的生成客户端 bundle,请使用 babel plugin. 它通过为每个 styled component 添加确定的 ID 来防止校验错误. 更多信息请参考 tooling documentation .

对于 TypeScript 用户, TS 大师 Igor Oleinikov 整合了webpack ts-loader / awesome-typescript-loader 工具链 TypeScript plugin 来完成类似的任务.

If possible, we definitely recommend using the babel plugin though because it is updated the most frequently. It’s now possible to compile TypeScript using Babel, so it may be worth switching off TS loader and onto a pure Babel implementation to reap the ecosystem benefits.

示例

基本 API 的使用如下:

  1. import { renderToString } from 'react-dom/server'
  2. import { ServerStyleSheet } from 'styled-components'
  3. const sheet = new ServerStyleSheet()
  4. const html = renderToString(sheet.collectStyles(<YourApp />))
  5. const styleTags = sheet.getStyleTags() // or sheet.getStyleElement();

collectStyles 方法将元素包装进了 provider.也可以选择直接使用 StyleSheetManager provider.确保不要再客户端使用即可.

  1. import { renderToString } from 'react-dom/server'
  2. import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
  3. const sheet = new ServerStyleSheet()
  4. const html = renderToString(
  5. <StyleSheetManager sheet={sheet.instance}>
  6. <YourApp />
  7. </StyleSheetManager>
  8. )
  9. const styleTags = sheet.getStyleTags() // or sheet.getStyleElement();

sheet.getStyleTags() 方法返回多个字符串的 <style> 标签. 当向 HTML 输出增加 CSS 时需要考虑这一点.

作为另一种选择,ServerStyleSheet 实例也提供 getStyleElement() 方法,返回一个 React 元素的数组.

注意

sheet.getStyleTags()sheet.getStyleElement() 只能在元素渲染和调用. 所以sheet.getStyleElement()中的组件不能与<YourApp />合并为一个更大的组件.

Next.js

首先添加一个自定义的 pages/_document.js. 然后 复制这段逻辑 将服务端你渲染的样式注入 <head>.

参考 our example 中的 Next.js repo .

Streaming Rendering

styled-components 提供一个与ReactDOMServer.renderToNodeStream()搭配使用的流式 API . 一个流式实现需要以下两部分:

在服务器上:

ReactDOMServer.renderToNodeStream 发出一个 styled-components 包装过的”可读”流. 当整个 HTML 块被推到流上时,如果任何相应的样式已经可以渲染,一个样式块会被附加到 React HTML 并发送给客户端浏览器.

  1. import { renderToNodeStream } from 'react-dom/server'
  2. import styled, { ServerStyleSheet } from 'styled-components'
  3. // if you're using express.js, you'd have access to the response object "res"
  4. // typically you'd want to write some preliminary HTML, since React doesn't handle this
  5. res.write('<html><head><title>Test</title></head><body>')
  6. const Heading = styled.h1`
  7. color: red;
  8. `
  9. const sheet = new ServerStyleSheet()
  10. const jsx = sheet.collectStyles(<Heading>Hello SSR!</Heading>)
  11. const stream = sheet.interleaveWithNodeStream(renderToNodeStream(jsx))
  12. // you'd then pipe the stream into the response object until it's done
  13. stream.pipe(
  14. res,
  15. { end: false }
  16. )
  17. // and finalize the response with closing HTML
  18. stream.on('end', () => res.end('</body></html>'))

在客户端里:

  1. import { hydrate } from 'react-dom'
  2. hydrate()
  3. // your client-side react implementation

当客户端注水完毕后, styled-components 将接管重新定位后的流式样式并且注入动态样式.

Referring to other components

有许多方法可以实现覆盖组件样式.话虽如此,很难在不使用广为人知的CSS选择器范式的情况下让使用插值变得轻松.

styled-components 通过”component selector”干净利落的解决了这个问题. 当一个组件由styled()工厂方法创建或是被其包装后,同时也会被分配一个 stable CSS 类用于定位.这实现了非常强力的组合模式而无需在命名和避免选择器冲突上手忙脚乱.

如下例子实现了 Icon 组件对它父组件 Link hover 的响应:

  1. const Link = styled.a`
  2. display: flex;
  3. align-items: center;
  4. padding: 5px 10px;
  5. background: papayawhip;
  6. color: palevioletred;
  7. `;
  8. const Icon = styled.svg`
  9. flex: none;
  10. transition: fill 0.25s;
  11. width: 48px;
  12. height: 48px;
  13. ${Link}:hover & {
  14. fill: rebeccapurple;
  15. }
  16. `;
  17. const Label = styled.span`
  18. display: flex;
  19. align-items: center;
  20. line-height: 1.2;
  21. &::before {
  22. content: '◀';
  23. margin: 0 10px;
  24. }
  25. `;
  26. render(
  27. <Link href="#">
  28. <Icon viewBox="0 0 20 20">
  29. <path d="M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z"/>
  30. </Icon>
  31. <Label>Hovering my parent changes my style!</Label>
  32. </Link>
  33. );

可以在 Link 组件内嵌套样式color-changing, 但是这样就必须要同时考虑这两套规则来理解 Icon 组件的行为.

Caveat

This behaviour is only supported within the context of Styled Components: attempting to mount B in the following example will fail because component A is an instance of React.Component not a Styled Component.

  1. class A extends React.Component {
  2. render() {
  3. return <div />
  4. }
  5. }
  6. const B = styled.div`
  7. ${A} {
  8. }
  9. `

The error thrown - Cannot call a class as a function - occurs because the styled component is attempting to call the component as an interpolation function.

However, wrapping A in a styled() factory makes it eligible for interpolation — just make sure the wrapped component passes along className.

  1. class A extends React.Component {
  2. render() {
  3. return <div className={this.props.className} />
  4. }
  5. }
  6. const StyledA = styled(A)``
  7. const B = styled.div`
  8. ${StyledA} {
  9. }
  10. `

样式对象

styled-components 支持将 CSS 写成 JavaScript 对象.对于已存在的样式对象,可以很轻松的将其迁移到 styled-components.

  1. // Static object
  2. const Box = styled.div({
  3. background: 'palevioletred',
  4. height: '50px',
  5. width: '50px'
  6. });
  7. // Adapting based on props
  8. const PropsBox = styled.div(props => ({
  9. background: props.background,
  10. height: '50px',
  11. width: '50px'
  12. }));
  13. render(
  14. <div>
  15. <Box />
  16. <PropsBox background="blue" />
  17. </div>
  18. );