CSS in JS

作者: 左冬的博客 | 来源:发表于2021-04-26 21:04 被阅读0次

    啥是CSS in JS?

    以前,网页开发有一个原则,叫做"关注点分离"(separation of concerns)

    各个技术各司其职,只负责自己的领域,不要混合在一起,对于我们日常开发来说,主要是三种技术分离:

    • HTML:负责网页的结构,又称语义层
    • CSS:负责网页样式,又称视觉层
    • JS:负责网页的逻辑和交互,又称逻辑层或交互层

    简而言之,不要写"行内样式"(inline style)和"行内脚本"(inline script)

    // bad
    <h1 style="color:red;font-size:46px;"  onclick="alert('Hi')">
      Hello World
    </h1>
    

    但是随着React为首的现代前端开发框架兴起,这个原则不再适用了。因为React是以组件为最小颗粒度划分的,强制我们将HTMLCSSJS写在一起。在JS中维护CSS(css in js)的方案成为了当代前端社区的新趋势

    先看一段不使用CSS in JS方案的React代码:

    const style = {
      'color': 'red',
      'fontSize': '46px'
    };
    
    const clickHandler = () => alert('hi'); 
    
    ReactDOM.render(
      <h1 style={style} onclick={clickHandler}>
         Hello, world!
      </h1>,
      document.getElementById('example')
    );
    

    上面这段代码在一个文件(组件)里,封装了结构、样式和逻辑。完全背离了“关注点分离”的原则

    但是不得不说,确实也有一定的优点,比如,组件的隔离,每个组件包含了所有需要的代码,不依赖外部,组件之间无耦合,方便复用。

    关注点混合

    React在JS里实现了对HTML和CSS的封装,通过方法去操作网页样式和结构。

    比如,React对HTML的封装是JSX。
    而对CSS的封装非常简单,它沿用了DOM的style属性对象

    const style = {
      'color': 'red',
      'fontSize': '46px'
    };
    

    主要使用驼峰写法,这是JS操作CSS的约定。
    显而易见的,很难写,虽然说在一定程度上实现了样式的组件化封装,但是由于内联样式缺少CSS所能提供的许多特性,比如伪选择器、动画与渐变、媒体选择器等,下面具体来看。

    传统CSS缺陷

    缺乏模块组织

    传统CSS和JS都没有模块的概念,后来在JS界陆续有了CommonJS和ES Module,CSS in JS可以用模块化的方式组织CSS,依托于JS的模块化方案:

    // button1.ts
    import styled from '@emotion/styled'
    // ES module组织方式
    export const Button = styled.button`
      font-size: 16px;
    `
    
    // button2.ts
    import styled from '@emotion/styled'
    
    export const Button = styled.button`
      font-size: 16px;
    `
    

    上面代码都是Button,但是可以用于不同的文件,不同模块,不会互相冲突,解决了模块组织问题

    缺乏作用域

    传统CSS只有一个全局作用域,比如说一个class可以匹配全局的任意元素。随着项目越来越大,CSS会变得难以组织,容易失控。CSS in JS可以通过生成独特的选择符,来实现作用域效果:

    /* css in js自动生成的classname */
    .css-1c4ktv6 >* {
        margin-top: 20px;
    }
    

    这样保证了整个rule不会被应用到全局,只会应用在我们指定的作用域

    隐式依赖,让样式难以追踪

    .target .name h1 {
        color: red;
    }
    body #container h1 {
        color: blue;
    }
    
    <!doctype html>
    <html lang="en">
    <body>
      <div id='container'>
       <div class='target'>
         <div class='name'>
           <h1>我是啥颜色?</h1>
         </div>
       </div>
      </div>
    </body>
    </html>
    

    好,即使通过选择器优先级可以判断,但还是没有很直观,因为h1上面没有附带任何样式,如果想要追踪,需要通过全局搜索或着挨个检查才能找到影响h1样式的代码片段

    export const Title = styled.h1`
        color: blue;
    `
    // 直接把样式通过普通组件来使用
    <Title>
        我是啥颜色?
    </Title>
    

    么有变量

    传统CSS规则里面没有变量,但是在CSS in JS中可以方便的控制变量,可以进行条件判断、变量的计算都会非常方便

    const Container = styled.div(props => {
        display: 'flex';
        flexDirection: props.column && 'column'
    })
    

    CSS选择器与HTML元素耦合

    .target .name h1 {
        color: red;
    }
    body #container h1 {
        color: blue;
    }
    
    <!doctype html>
    <html lang="en">
    <body>
      <div id='container'>
       <div class='target'>
         <div class='name'>
           <h1>我是啥颜色?</h1>
         </div>
       </div>
      </div>
    </body>
    </html>
    

    如果想改标签的名字,比如h1改成h2,就必须要同时改动CSS和HTML。

    可以成为React好搭档的,CSS替代方案

    对于Angular和Vue来说,这两个都有框架原生提供的CSS封装方案,比如Vue的<style scoped>标签和Angular组件的viewEncapsukation属性。React本身的设计原则决定了其不会提供原生CSS封装方案,或者说CSS封装并不是React框架本身的关注点。

    由于CSS的封装非常弱,React社区从很早的时候就开始寻找相关替代办法,一系列的第三方库,用来加强React对CSS的操作。统称为CSS in JS

    CSS in JS在2014年由Facebook的员工Vjeux在《NationJS会议》上提出。可以借用JS解决许多CSS本身的一些缺陷,比如全局作用域、生效顺序依赖于样式加载顺序、常量共享等等问题。

    前端社区也在各个方向上探索着CSS in JS。甚至,Chrome v85为CSS in JS的需求修复了一个问题,这也可以从侧面看出CSS in JS已经得到了浏览器厂商的重视


    image.png

    使用JS语言写CSS
    根据不完全统计,各种CSS in JS的库至少有47种

    这么多库里面代表库有styled-component和emotion,通过几年间的竞争,为了满足开发者的需求,同时结合社区的使用反馈,在不断的更新过程中,几乎在接口上使用同样的接口设计,以Emotion为例:

    • CSS prop
      可以算是内联样式的升级版,用户定义的内联样式以 JSX 标签属性的方式与组件紧密结合
    • 样式组件
      更像是 CSS 的组件化封装,将样式抽象为语义化的标签,把样式从组件实现中分离出来,让 JSX 结构更“干净整洁”,复用性更高

    这两种方案在内部实现中都会享受当代前端工程化的福利,如语法检查、自动增加浏览器属性前缀、帮助开发者增强样式的浏览器兼容性等等

    同时利用vscode-styled-components等代码编辑器插件,我们可以在 JS 代码中增加对于 CSS 的语法高亮支持

    这次分享重点介绍emotion,它对React做了很好的适应,在github中有12.9k的star,官方文档

    Emotion

    Emotion支持的两种样式定义方法

    • String Style
    • Object Style

    String Style

    需结合css函数使用,该函数返回一个对象(包含样式名,样式字符串)给Emotion底层使用

    import { css, jsx } from '@emotion/react'
    
    const color = 'darkgreen'
    
    render(
      <div
        css={css`
          background-color: hotpink;
          &:hover {
            color: ${color};
          }
        `}
      >
        This has a hotpink background.
      </div>
    )
    

    另外,关于css函数的写法

    // 标签模板字符串
    const style = css`
        color: "black";
        &:hover {
          color: "white";
        }
    `;
    
    // 等同于
    const style = css(`
        color: "black";
        &:hover {
          color: "white";
        }
    `);
    

    Object Styles

    一个JS对象,使用驼峰式命名,可用在css propStyled Componentscss函数

    import { css, cx } from '@emotion/css'
    
    const objectStyle = 'white'
    
    render(
      <div
        css={{
          backgroundColor: 'hotpink',
          '&:hover': {
            color: ${objectStyle};
          }
        }}
      >
        Hover to change color.
      </div>
    )
    

    这种写法比起 React 自带的 style 的写法功能更强大,比如可以处理级联、伪类等 style 处理的不了的情况

    Style Precedance

    props 对象中的 css 属性优先级⾼于组件内部的 css 属性

    /** @jsx jsx */
    import { jsx } from '@emotion/react'
    
    const P = props => (
      <p
        css={{
          margin: 0,
          fontSize: 12
        }}
        {...props} // <- props contains the `className` prop
      />
    )
    
    const ArticleText = props => (
      <P
        css={{
          fontSize: 14,
          fontFamily: 'Georgia, serif'
        }}
        {...props} // <- props contains the `className` prop
      />
    )
    
    <ArticleText style={{fontSize: 16px}}>
    

    结果

    .css-result {
        - font-size: 12px;
        - font-size: 14px;
        + font-size: 16px;
    }
    

    Style Components

    启发于另一个CSS-in-JS库styled-components,能够样式化任何接收className的组件

    静态样式

    // String Style
    const Button = styled.button`
      color: turquoise;
    `
    // Object Style
    const Button = styled.button({color: turquoise;})
    
    // <button>This my button component.</button>
    render(<Button>This my button component.</Button>)
    

    动态样式

    // 动态定义某个属性
    const Button = styled.button`
      color: ${props =>
        props.primary ? 'hotpink' : 'turquoise'};
    `
    render(<Button primary>This my button component.</Button>)
    
    // 动态定义Object Style
    const H1 = styled.h1(
      {
        fontSize: 20
      },
      props => ({ color: props.color })
    )
    render(<H1 color="lightgreen">This is lightgreen.</H1>)
    
    // 动态多个属性
    const dynamicStyle = props =>
      css`
        color: ${props.color};
      `
    
    const Container = styled.div`
      ${dynamicStyle};
    `
    render(
      <Container color="lightgreen">
        This is lightgreen.
      </Container>
    )
    

    修改标签

    使用withComponent生成新的自定义组件

    const Section = styled.section`
      background: #333;
      color: #fff;
    `
    
    const Aside = Section.withComponent('aside')
    
    render(
      <div>
        <Section>This is a section</Section>
        <Aside>This is an aside</Aside>
      </div>
    )
    

    将样式组件当作选择器使用

    import styled from '@emotion/styled'
    
    const Child = styled.div`
      color: red;
    `
    
    const Parent = styled.div`
      ${Child} {
        color: green;
      }
    `
    
    render(
      <div>
        <Parent>
          <Child>Green because I am inside a Parent</Child>
        </Parent>
        <Child>Red because I am not inside a Parent</Child>
      </div>
    )
    

    组件选择器也可以使用Object Style的写法

    import styled from '@emotion/styled'
    
    const Child = styled.div({
      color: 'red'
    })
    
    const Parent = styled.div({
        [Child]: {
                color: 'green'
        }
    })
    
    render(
      <div>
        <Parent>
          <Child>green</Child>
        </Parent>
        <Child>red</Child>
      </div>
    )
    

    依然可以使用&

    /* @jsx jsx */
    import { jsx } from '@emotion/react'
    
    render(
      <div
        css={{
          color: 'darkorchid',
          '& .name': {
            color: 'orange'
          }
          '& :hover': {
              color: 'red'
          }
        }}
      >
        This is darkorchid.
        <div className="name">This is orange</div>
      </div>
    )
    

    scss怎么写,Emotion就能怎么写

    /** @jsx jsx */
    import { jsx, css } from '@emotion/react'
    
    const paragraph = css`
      color: turquoise;
    
      a {
        border-bottom: 1px solid currentColor;
        cursor: pointer;
      }
    `
    render(
        // 可以自定义样式名,后加上自定义字符串,比如下面这行paragraph
      <p css={paragraph}>
        Some text.
        <a>A link with a bottom border.</a>
      </p>
    )
    

    样式组合

    /** @jsx jsx */
    import { jsx, css } from '@emotion/core'
    
    const base = css`
      color: hotpink;
    `
    
    render(
      <div
        css={css`
          ${base};
          background-color: #eee;
        `}
      >
        This is hotpink.
      </div>
    )
    

    在传统css中,两个className组合的优先级由样式表中定义的先后决定。在应用时修改优先级需要使用!important。而Emotion则可以根据应用时的顺序决定:

    /** @jsx jsx */
    import { css, jsx } from '@emotion/core'
    
    const danger = css`
      color: red;
    `
    
    const base = css`
      color: blue;
    `
    
    render(
      <div>
        <div css={base}>This will be blue</div>
        <div css={[danger, base]}>
          This will be also be blue since the base styles
          overwrite the danger styles.
        </div>
        <div css={[base, danger]}>This will be red</div>
      </div>
    

    附加props

    /** @jsx jsx */
    import { jsx, css } from '@emotion/core'
    
    const PasswordInput = ({size, ...restProps}) => (
      <input
        type="password"
        css={css`
          color: palevioletred;
          font-size: 1em;
          border: 2px solid palevioletred;
          border-radius: 3px;
    
          /* here we use the dynamically computed prop */
          margin: ${props => props.size};
          padding: ${props => props.size};
        `}
        size={props.size || "1em"}
        {...restProps}
      />
    )
    
    render(
      <div>
        <PasswordInput placeholder="red" />
        <PasswordInput placeholder="pink" css={pinkInput} />
      </div>
    );
    

    媒体查询

    /** @jsx jsx */
    import { jsx } from '@emotion/react'
    
    render(
      <div
        css={{
          color: 'darkorchid',
          '@media(min-width: 420px)': {
            color: 'orange'
          }
        }}
      >
        This is orange on a big screen and darkorchid on a small
        screen.
      </div>
    )
    

    参考:

    相关文章

      网友评论

          本文标题:CSS in JS

          本文链接:https://www.haomeiwen.com/subject/dlqyrltx.html