阅读 6857

styled-components:一本通

  • 初体验
  • styled方法:将React组件包装成Styled组件
    • ()的三种情况
    • tagged template literal
      • interpolations 插值表达式
      • mixin
  • StyledComponent相关特性
    • extend 继承
    • withComponent
    • component-selector
    • innerRef
    • isStyledComponent
  • attr方法:给Styled组件添加默认属性
  • 主题组件
    • defaultProps
    • 关于theme对象
    • ThemeProvider嵌套与Function theme
    • 在React组件中获取theme
  • injectGlobal方法:插入全局样式
  • keyframes方法:使用帧动画
  • 其它

pre-notify

previously:

在越来越组件化开发的今天,我们通过JSX已经将js好html/xml很好的柔和在了一起,那么css呢?

虽然在vue这样的框架里我们能在.vue文件里将css、js、html写在一起,但实际上它们的联系很弱,特别是js和css,它们完全无法沟通。

styled-components很好的解决了这个问题,通过它,我们能让整个css架构跟着组件走,而不再仅仅是貌合神离的被放置在一个文件中。可以这么说,styled-components让一个组件变得更加得完整,更加得像一个组件!

初体验

styled-compnents,正如其名,就是有样式的react-component,是对react组件的再封装,它不仅可以往<Component/>添加了固定的css样式,还可以通过组件的属性让css和一个组件紧密的联系起来。

除此之外它支持几乎所有sass/less等css预处理器具有的功能,嵌套、&、变量、插值,甚至更加强大!

我们先来看一个基本使用栗子

// 把一个React-Component包装成Styled-Component

import React,{Component}from 'react';
import styled from 'styled-components';

class Xxx extends React.Component{
  render(){
    return (
      <div className={this.props.className}>
        container
        <h2>title</h2>
        <div>body</div>
      </div>
    )
  }
}

const StyledComponent = styled(Xxx)`
  &{
    color:red;
    h2{
      color:blue;
    }
    div{
      font-size:${props=>props.fontSize};
      color:pink;
    }
  }
`;

export default StyledComponent;
复制代码

styled()style-components中最重要的方法,它能将一个React组件包装成一个具有样式的<StyleComponent/>,并且它还会往原本的React组件中传递一个className属性,这个属性的值是一串hash值(防止命名冲突),我们需要将它放置到它应该被放置的元素身上

通过styled(),我们已经将一个React组件包装成了一个Styled组件并导出,接下来我们去渲染这个导出的组件

import React from 'react';
import ReactDOM from 'react-dom';
import StyledComponent from './test.js';

ReactDOM.render(
  <StyledComponent fontSize='30px'/>
  ,window.root
)
复制代码

渲染结果长这样:

可以发现,在使用上,一个StyledComponent和ReactComponent完全木有区别,emmm,应该说还是有一点的,我们能过给一个组件传递属性来控制该组件的css样式,So,StyledComponent其实是ReactComponent的超集。

嗯,是不是有那么一点兴趣了耶,接下来让我们一起更加深入的学习style-components吧!

styled方法:将React组件包装成Styled组件

上栗中我们已经知道了styled能干什么,这一回让我们来完整的分析下这个API。

首先它的格式是这样的

const StyledCompoent = styled()``
复制代码

它接收两次传参(这其实是es6中的标签函数的写法,这里不再展开),并最终返回一个包装后的React组件,即具有样式的React组件。

()的三种情况

()可以接收一个React-Component也可以接收一个tagName

上栗子中我们演示了第一种情况,So,其实它还能接收一个tagName,比如div

const StyledCompoent = styled('div')``
复制代码

其实就相当于

let ReactComponent = (props,context)=><div className={props.className}></div>; //上栗中我们说过当我们调用styed()时,react组件中会自动传入一个由hash组成的className属性

const StyledCompoent = styled(ReactComponent)``
复制代码

除此之外它还有一种快捷写法

const StyledCompoent = styled.div``
复制代码

嗯,除了上面两种大情况,还有一种情况就是()中也可以接收一个StyledComponent,这种情况大多出现在一个StyledComponent的样式需要继承自另外一个StyledComponent时

const StyledCompoent2 = styled(StyledCompoent1)`
    color:'orange'
`
复制代码

tagged template literal

emmm...这货怎么翻译?标签模板字符串?带有标签的模板字面量?

不管啦~反正就是指括号(())后的 `` 里的内容。

在经过styled()后,我们已经确保将一个有效的react组件初始化为了styled组件,接下来我们只需要往这个组件中添加样式。

const StyledCompoent = styled.div`
  /* all declarations will be prefixed */
  //所有css样式会自动添加兼容性前缀
  padding: 2em 1em;
  background: papayawhip;

  /* pseudo selectors work as well */
  //支持伪类选择器
  &:hover {
    background: palevioletred;
  }

  /* media queries are no problem */
  //支持媒体查询
  @media (max-width: 600px) {
    background: tomato;

    /* nested rules work as expected */
    //支持嵌套
    &:hover {
      background: yellow;
    }
  }

  > p {
    /* descendant-selectors work as well, but are more of an escape hatch */
    //支持后代选择器
    text-decoration: underline;
  }

  /* Contextual selectors work as well */
  //支持环境选择器
  html.test & {
    display: none;
  }
`;
复制代码

以上示例出自官方文档,可见它无鸭梨支持:嵌套、前缀自动补全、各类选择器、媒体查询...

interpolations 插值表达式

除此之外,Of Course,它也支持变量,并且有两种可选

let color1 = 'orange';

const StyledCompoent = styled.div`
    color:${color1} //支持接收js变量作为css属性值
    ,fontSize:${props=>props.fontSize}; //支持接收组件的props中的某个值来作为css属性值
`

//--- --- --- 

// somewhere
...
<StyledComponent fontSize='30px'/>
...
复制代码

其中的${}被称之为interpolations ,嗯,插值表达式,应该叫这名?

需要注意的是${}中可以放一个js变量,也可以放一个函数,如果是函数,它会接受一个props属性(即React组件初始化时包装而成的props对象)作为参数。

哎嘿,还有种可能,${}也能接收一个css对象,like this

...
${{
    position:'absolute'
    ,left:'100px'
    ,top:'100px'
}}
...
复制代码

mixin

styled-components中也允许我们使用像sass中@mixin一样的东东

import React,{Component}from 'react';
import styled,{css} from 'styled-components';

class Xxx extends React.Component{
  render(){
    return (
      <div className={this.props.className}>
        container
        <h2 className='title'>title</h2>
        <div className='content'>body</div>
      </div>
    )
  }
}

let mixin = css`
  &{
    color:red;
    ${{
      position:'absolute'
      ,left:'100px'
      ,top:'100px'
    }}
    .title{
      color:blue;
    }
    .content{
      font-size:${props=>props.someCondition.fontSize};
      color:pink;
    }
  }
`

const StyledComponent = styled(Xxx)`
  ${props=>props.someCondition?mixin:null}
`;

export default StyledComponent;


// --- --- ---

ReactDOM.render(
  <StyledComponent someCondition={{fontSize:'30px'}}/>
  ,window.root
)
复制代码

其中我们用到了styled-components中的另外一个方法css,这个方法其实就是创建一个mixin,使我们可以在任何<StyledComponent>中复用这份样式。

需要注意的是, props属性可以透传mixin使其在内部使用(要不我们怎么说这货是一个mixin呢)

最终的渲染结果长这样

StyledComponent相关特性

通过上节中的styled()方法能将一个react组件包装成一个具有样式的react组件,我们将它称之为StyledComponent,它除了在样式上和组件强耦合外,还具有一些它独有的特性。

extend 继承

前面我们说过,我们能通过styled(StyledCompoent1)一个StyleComponent来创建一个继承自StyledCompoent1的StyledComponent2组件。

let StyledCompoent2 = styled(StyledCompoent1)`
    color:xxx
    ...
`
复制代码

但这样继承内部其实是一个工厂模式,StyledComponent2其实是一个全新的class。

如果我们想要做到真正的继承,需要使用style-components提供的extend方法,它是StyleComponent下的一个属性方法。

let StyledCompoent2 =StyledCompoent1.extend`
    color:xxx
    ...
`
复制代码

withComponent

withComponent同样是StyleComponent下的一个属性方法,它能帮助我们将原本的Styled组件中的标签给替换成另外一种标签

//会将原本的<button>替换成<a>
const Link = Button.withComponent('a');
复制代码

[danger] 注意: 若原本的Styled组件是一个具有复合标签的组件,那么它的整个DOM都会被替换掉,这可能并不是你所期望的结果。

component-selector

styled-components允许我们在tagged template literal 中使用一个StyledComponent变量作为css选择器,我们将它称之为component-selector

注意: 依然需要手动定位className的起始位置

let ReactComponent = (props,context)=>{
    <div className={props.className}>
    	<h2>hello</h2>
    </div>
}
let StyledComponent1 = styled(ReactComponent)``
let StyledComponent2 = styled.div`
    ${StyledComponent1}{
        background:orange;
        h2{
          color:red;
        }
        &:after{
          content:'';
          display:block;
          width:10px;
          height:10px;
          border:1px solid black;
        }
    }
`

//--- --- ---
...
ReactDOM.render(
  <StyledComponent2>
    <StyledComponent1/>
  </StyledComponent2>
  ,window.root
)
复制代码

innerRef

在styled-components中,我们要想获取到一个StyledComponent的真实入口DOM,需要使用innerRef而不是ref(作用和用法都是一样的)。

const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: palevioletred;
  background: papayawhip;
  border: none;
  border-radius: 3px;
  ${{color:'red'}}
`;

export default class Form extends React.Component {
  render() {
    return (
      <Input
        placeholder="Hover here..."
        innerRef={x => { this.input = x }}
        onMouseEnter={() => this.input.focus()}
      />
    );
  }
}
复制代码

点击查看官方示例

上栗中使用的是styled.input这种快捷创建styledComponent的方式,

如果我们改成使用styled(原生React组件)的方式,那么像上面那样我们是无法获取到dom的,获取的是styled()括号中传入的原生React组件对象

class _B extends React.Component{
  render(){
    return <div></div>
  }
}

const B = styled(_B)``;

export default class A extends React.Component{
  componentDidMount(){
    console.log('this.dom', this.dom);
  }
  render(){
    return <B innerRef={x => this.dom = x}></B>;
  }
}
复制代码

(获取到的不是dom,而是styled包裹之前的组件对象)

解决办法是在_B内再使用原生的ref挂载一次,把dom挂载在_B上,这样我们就可以通过往下再深一层访问的方式拿到dom。

isStyledComponent

有些时候我们需要判断一个组件是不是StyledComponent,我们才好运用只有StyledComponent才具有的特性,比如component-selector

import React from 'react';
import styled, { isStyledComponent } from 'styled-components';
import MaybeStyledComponent from './somewhere-else';

let TargetedComponent =
  isStyledComponent(MaybeStyledComponent)
    ? MaybeStyledComponent
    : styled(MaybeStyledComponent)``;

const ParentComponent = styled.div`
  color: cornflowerblue;

  ${TargetedComponent} {
    color: tomato;
  }
`
复制代码

注意: isStyledComponent方法需要从styled-components中额外导入

attr方法:给Styled组件添加默认属性

attr方法接收一个对象,它允许我们为一个StyledComponent添加默认属性和默认样式值

此方法也是私认为是styled-components中最为重要的方法之一。

const Input = styled.input.attrs({
  // 定义一些静态属性
  type: 'password',

  // 给css属性动态赋予初始值
  margin: props => props.size || '1em',
  padding: props => props.size || '1em'
})`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* here we use the dynamically computed props */
  margin: ${props => props.margin};
  padding: ${props => props.padding};
`;

export default class xxx extends React.Component{
  render(){
    return (
      <div>
        <Input placholder='A small text input' size='1em'/>
        <br/>
        <Input placholder='A bigger text input' size='2em'/>
      </div>
    )
  }
}
复制代码

最终的渲染结果长这样

主题组件

通过styled-components为我们提供的ThemeProvider组件(没错,是一个React组件),我们能为我们的StyledComponent订制主题。

import React from 'react';
import styled,{ThemeProvider} from 'styled-components';

// 定制主题
const theme = {
  main:'mediumseagreen'
}

const Button = styled.button`
  font-size:1em;
  margin:1em;
  padding:0.25em 1em;
  border-radius:3px;
  /*color the border and text with theme.main*/
  color:${props=>props.theme.main}; //——》这里使用主题提供的属性
  border:2px solid ${props=>props.theme.main};
`

export default class xxx extends React.Component{
  render(){
    return(
      <div>
        <Button>Normal</Button>

        <ThemeProvider theme={theme}>
          <Button>Themed</Button>
        </ThemeProvider>
      </div>
    )
  }
}
复制代码

点击查看官方示例

上栗中,我们定制了一个theme主题对象,并将这个对象传递给<ThemeProvider>组件,这样在被这个组件包裹的任何子组件中我们就能获取到这个theme对象(无论嵌套多少层)。

defaultProps

在上栗中其实有一个bug,那就是没有被<ThemeProvider>包裹住的<Button/>其实是没有props.theme属性对象的,那么它就会报错。

So,这个时候我们需要给这个Button组件设置一个默认值

...
// 设置默认属性,
Button.defaultProps = {
  theme:{
    main:'palevioletred'
  }
}

const theme = {
  main:'mediumseagreen'
}
...
复制代码

关于theme对象

其实我们除了在组件外部定义一个theme对象,并通过<ThemeProvider theme={theme}>来传递外,我们也可以直接在一个StyledComponent上定义theme对象

...
const theme = {
  main: 'mediumseagreen'
};
...
<ThemeProvider theme={theme}>
  <div>
    <Button>Themed</Button>
    <Button theme={{ main: 'darkorange' }}>Overidden</Button>
  </div>
</ThemeProvider>
...
复制代码

ThemeProvider嵌套与Function theme

ThemeProvider嵌套时,被嵌套的当ThemeProvider的theme属性此时不仅可以接收一个对象也可以接收一个函数,如果是个函数,那么这个函数会接受到一个参数,这个参数则是上一级ThemeProvide接收到的theme对象。

...
const theme = {
  fg:'palevioletred'
  ,bg:'white'
};
const invertTheme = ({fg,bg})=>({
  fg:bg
  ,bg:fg
})
...
<ThemeProvider theme={theme}>
    <div>         
      <ThemeProvider theme={invertTheme}>
        <Button>Themed</Button>
      </ThemeProvider>
    </div>
</ThemeProvider>
...
复制代码

点击查看官方示例

在React组件中获取theme

如果你想要在React组件中获取theme,styled-compnents也为我们提供了一个withTheme的方法,经过它包装后,我们就能在一个React组件中获取到props.theme

import { withTheme } from 'styled-components'

class MyComponent extends React.Component {
  render() {
    console.log('Current theme: ', this.props.theme);
    // ...
  }
}
export default withTheme(MyComponent)
复制代码

injectGlobal方法:插入全局样式

首先它是styled-components额外提供的一个的方法。

import { injectGlobal } from 'styled-components';

injectGlobal`
  @font-face {
    font-family: 'Operator Mono';
    src: url('../fonts/Operator-Mono.ttf');
  }

  body {
    margin: 0;
  }
`;
复制代码

嗯,官方推荐你最好只在font-face和body方面使用它。

keyframes方法:使用帧动画

往往和interpolation一起使用

import styled, { keyframes } from 'styled-components';

const fadeIn = keyframes`
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
`;

const FadeInButton = styled.button`
  animation: 1s ${fadeIn} ease-out;
`;
复制代码

点击查看官方示例

其它

关于服务端渲染

服务端渲染

关于TypeScript

如何在TypeScript中使用styled-components

关于ReactNative

在ReactNative中使用styled-components需要注意的事情

关于styledComponent的更新

如果有一个新的状态传入导致需要添加新的cssText,那么会往style标签中追加cssText,

注意是往里追加,并不会删除style里之前的cssText。(即使当前的props已经不满足之前css文本的生成条件也不会删除)


参考