愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

styled-components学习记录

一、styled-components有什么用?

我们常常会思考,如何以一种更优雅、更灵活的方式来为React组件编写样式?因而styled-components应运而生。

使用styled-components,它可以带来如下这些好处:

  • 自动提取关键的CSS: styled-components会自动追踪页面上哪些组件得到了渲染,只有那些得到了渲染的组件的样式才会被应用,这样子带来的好处就是结合code splitting的情况下,只有必要的代码才会进行加载
  • 没有类命名的问题: styled-components会为样式创建独一无二的类名,因此不必担心重名、覆盖以及拼写错误的问题
  • 方便移除CSS: 有时候我们很难以追踪一个CSS类在代码仓中的哪些地方被用到了,而styled-components能够使得这个追踪过程变得更容易,我们可以清楚地知道样式在哪里被使用了,如果组件不再使用且被删除的时候,其相关的CSS也很容易被找到移除
  • 简单的动态样式: 我们有时候会基于组件的props以及全局的主题设置来调整组件的样式,那么styled-components使得这件事变得容易得多,避免了我们需要为了实现类似于的效果多去维护大量的类名
  • 无痛维护: 我们无需再去翻遍不同的文件来理清楚组件的样式受到哪些CSS类影响,因此即便是维护一个大的代码仓,样式的维护工作也变得轻松了许多
  • 自动添加厂商前缀: 有了styled-components,编写CSS我们就只需要根据标准语法来写,再也不用管要不要加厂商前缀了(形如-webkit-之类的),因为styled-components会帮我们做完剩下的事情


二、安装

安装styled-components很简单,只需要用包管理工具把styled-components添加作为运行时依赖就好了,如用npm可以:

npm install -S styled-components

如果是使用类似于yarn这种支持在package.json里添加一个resolutions字段的包管理工具,那么比较推荐加入一个主版本范围,从而避免在项目里被安装了不同版本的styled-components

{
  "resolutions": {
    "styled-components": "^5"
  }
}


三、快速入门

styled-components使用了模板字符串标签字面量(tagged template literals)语法来为组件增添样式,它不再使用映射的方式来关联样式与组件(如写一个类名,然后通过styles.someName的方式作为组件className属性的值),因此这意味着当你定义样式的时候,其实就已经创建了一个正常的React组件,并且这个React组件会被自动带上样式,例子如下:

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

// 接下来就可以直接像使用普通组件一样,使用 Title 跟 Wrapper 两个组件了
render(
  <Wrapper>
    <Title>
      Hello, world!
    </Title>
  </Wrapper>
)

其实就类似于原先的下列这种写法产生的效果:

/* style.css */
.title {
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
}
.wrapper {
  padding: 4em;
  background: papayawhip;
}
import styles from './style.css';

render(
  <section className={styles.wrapper}>
    <h1 className={styles.title}>Hello, world</h1>
  </section>
);


四、根据Props调整

有时候,我们希望根据不同的组件属性,来呈现不同的组件样式,比如按钮,分为主按钮、次按钮……这种情况下,可以通过传入一个函数插值styled-components的模板字面量来实现。例子如下:

const Button = styled.button`
  margin: 1em;
  padding: .25em 1em;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
  background: ${props => props.primary ? 'palevioletred' : 'white'};
  color: ${props => props.primary ? 'white' : 'palevioletred'};
`

render(
  <div>
    <Button>Normal</Button>
    <Button primary>Primary</Button>
  </div>
);


五、继承样式

有许多场景下,你可能会想要使用一个组件,但是仅仅对它的样式作轻微的变更。你可以通过传入一个函数插值来根据组件属性做到这点,但是这有点儿麻烦了。
更方便的创建一个组件来继承另一个组件样式的方式是,仅仅使用styled()包裹起来创建,这里我们使用前面写过的Button来创建另一个样式的Button,其写法如下:

const TomatoButton = styled(Button)`
  color: tomato;
  border-color: tomato;
`;

render(
  <TomatoButton>Click Me</TomatoButton>
);

运行过后,我们会发现<TomatoButton>组件长得跟<Button>组件差不多,因为我们也仅仅添加了两条样式。
此外,有时候我们在一些场景下可以复用相同的样式,但是标签不同,尤其是在创建导航的时候,这种情况下,我们可以使用as属性,比如我们希望最终渲染出来的<TomatoButton>是带有链接的<a>标签,那么可以这么做:

render(
  <TomatoButton as="a" href="/tomato">Click Me</TomatoButton>
);

甚至,连自定义组件也可以使用as语法,如:

const ReversedButton = props => (
  <Button {...props} children={props.children.split('').reverse()} />
);

render(
  <div>
    <Button>Normal</Button>
    <Button as={ReversedButton}>Reversed</Button>
  </div>
);

那么,最终第二个Button呈现出的文本内容会是:desreveR


六、为任何组件添加样式

styled其实还可以很好地作用于你自己编写的组件或者任何的第三方组件,只要这些组件能够传递好className属性到最终的DOM元素上,如:

const Link = ({ className, children }) => (
  <a className={className}>{children}</a>
);

const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;

render(
  <div>
    <Link>Normal Link</Link>
    <StyledLink>Styled Link</StyledLink>
  </div>
);

注意: 我们也可以直接给styled()工厂函数传递标签名字,如:styled('div'),而实际上,styled.tagName就是styled(tagName)的别名


七、传递属性

如果被添加样式的目标是一个简单的元素(如:styled.div),那么styled-components会将所有的已知HTML属性传递到DOM上;而如果这是一个自定义的React组件(如:styled(MyComponent)),那么styled-components就会透传所有的props

下面这个例子展示了Input组件的全部属性是如何传递到已挂载的DOM节点上的:

const Input = styled.input`
  padding: .5em;
  margin: .5em;
  color: ${props => props.inputColor || 'palevioletred'};
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

render(
  <div>
    <Input defaultValue="@probablyup" type="text" />
    <Input defaultValue="@geelen" type="text" inputColor="rebeccapurple" />
  </div>
);

可以注意到,inputColor作为属性没有传递给DOM,但是typedefaultValue传递了,所以styled-components会聪明地帮你筛选掉非标准属性从而作为props传递


八、一些原则

1、styled-components是怎样在一个组件内工作的

如果你对导入CSS到组件中熟悉的话(类似于CSS Modules),那么你就会对如下的写法司空见惯:

import React from 'react'
import styles from './styles.css'

export default class Counter extends React.Component {
  state = { count: 0 }

  increment = () => this.setState({ count: this.state.count + 1 })
  decrement = () => this.setState({ count: this.state.count - 1 })

  render() {
    return (
      <div className={styles.counter}>
        <p className={styles.paragraph}>{this.state.count}</p>
        <button className={styles.button} onClick={this.increment}>+</button>
        <button className={styles.button} onClick={this.decrement}>-</button>
      </div>
    )
  }
}

而一个样式化的组件,就是元素与样式规则的结合,我们可以将上面的Counter改写为:

import React from 'react'
import styled from 'styled-components'

const StyledCounter = styled.div`
  /* ... */
`
const Paragraph = styled.p`
  /* ... */
`
const Button = styled.p`
  /* ... */
`

export default class Counter extends React.Component {
  state = { count: 0 }

  increment = () => this.setState({ count: this.state.count + 1 })
  decrement = () => this.setState({ count: this.state.count - 1 })

  render() {
    return (
      <StyledCounter>
        <Paragraph>{this.state.count}</Paragraph>
        <Button onClick={this.increment}>+</Button>
        <Button onClick={this.decrement}>-</Button>
      </StyledCounter>
    )
  }
}

2、应该在render方法之外定义样式化组件

render方法之外定义样式化组件非常重要,否则每一次的渲染都会进行重复创建的过程,在render方法内部定义样式化组件会阻碍缓存且降低渲染速度,这应该被避免,即要写成如下这种方式:

const StyledWrapper = styled.div`
  /* ... */
`;

而非这种方式:

const Wrapper = ({ message }) => {
  const StyledWrapper = styled.div`
    /* ... */
  `

  return <StyledWrapper>{message}</StyledWrapper>
}

3、伪元素、伪类与嵌套

styled-components使用了stylis这种预处理器来实现类scss风格的嵌套语法,且&可以用来指向主组件,下面是用法的示例:

const Thing = styled.div.attrs((/* props */) => ({ tabIndex: 0 }))`
  color: blue;
  &:hover {
    color: red; // <Thing>组件被hover的时候
  }
  & ~ & {
    background: tomato;
  }
  & + & {
    background: lime;
  }
`

如果不使用&,则指向的是子元素:

const Thing = styled.div`
  color: blue;
  
  .something {
    border: 1px solid;
    display: block;
  }
`

render(
  <Thing>
    <label htmlFor="foo-button" className="something">Mystery button</label>
    <button id="foo-button">What do I do?</button>
  </Thing>
)

最后,&可以被用来增强组件规则的CSS特殊性,这可以解决样式冲突时的样式问题,如:

const Thing = styled.div`
  && {
    color: blue;
  }
`
const GlobalStyle = createGlobalStyle`
  div${Thing} {
    color: red;
  }
`

render(
  <>
    <GlobalStyle />
    <Thing>I am blue</Thing>
  </>
)

最终I am blue显示为蓝色


九、自动添加属性

.attrs可以自动给组件带上一些属性,如:

const Input = styled.input.attrs(props => ({
  type: 'password',
  size: props.size || '1em'
}))`
  color: palevioletred;
  font-size: 1em;
  border: 12px solid palevioletred;
  border-radius: 3px;

  margin: ${props => props.size};
  padding: ${props => props.size};
`

render(
  <div>
    <Input placeholder="A small text input" />
    <br />
    <Input placeholder="A bigger text input" size="2em" />
  </div>
)

十、动画

使用@keyframes的CSS动画一般不局限在单一的组件中,但我们仍然希望这些CSS动画能够不被置于全局语境从而带来命名冲突。在这种场景下,我们可以使用keyframes这个tag,如:

const rotate = keyframes`
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
`;

const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;

render(
  <Rotate>Hello</Rotate>
);

Keyframes只在被使用时懒注入,这也是为什么能够被code-splitted的原因,所以需要使用css标签来包裹共享片段:

const rotate = keyframes``;
// 以下这么写会报错
const styles = `
  animation: ${rotate} 2s linear infinite;
`

// 这样子写才对
const styles = css`
  animation: ${rotate} 2s linear infinite;
`;