为什么要用 Css In JavaScript ?

1,631 阅读6分钟

背景

以前,网页开发有一个原则,叫做"关注点分离"(separation of concerns)。 它的意思是,各种技术只负责自己的领域,不要混合在一起,形成耦合。对于网页开发来说,主要是三种技术分离。 现在在 vue ,angular 仍然是这种形式,关注点分离,三部分的代码分开来写,html css js 单独来写,html 中能取到哪些 js 变量,是由框架来控制的。

<template>
  <div id="app">
    <h2>Css</h2>
    <h3>{{ count }}</h3>
    <button @click="count++">+1</button>
    <h4>{{ dayjs() }}</h4>
  </div>
</template>

<script>
import dayjs from "dayjs";
export default {
  name: "App",
  data() {
    return {
      count: 0,
      // dayjs
    };
  },
};
</script>

<style>
</style>

而在 React 中,推荐了 jsx 的语法,在 js 中写 html ,利用 js 的变量作用域特性,程序员可以知道能够拿到哪些变量,只需要了解 js ,而不需要过多的去了解框架的写法。 这样的话, html 和 js 就组合起来了,可以说,页面的结构(html) 就具有了 动态(js) 的特性。如果说,能将 css 和 js 组合起来,页面的样式(css) 就具有了 动态(js) 的特性。

什么是 css in js

根据上面的背景,这个就很好解释了,将 css 和 js 写在一起,css 中直接可以取到 js 的变量,使 css 具有动态性。 codesandbox.io/s/small-pap…

import React, { useState } from "react";

export default function App() {
  const [fontSize, setFontSize] = useState(12);

  return (
    <div>
      <span
        style={{
          fontSize
        }}
        >
        css in js
      </span>
      <button onClick={() => setFontSize(fontSize + 1)}>+1</button>
    </div>
  );
}

如上面的例子,span 的 css 会根据 fontSize 这个 state 的改变而改变。

css 在 react 中的使用方式

react 的 jsx 仅仅是 js 和 html 组合到一起使用,官方并没有提供 css 的方案,绝大部分都是社区方案;css modules ,styled-components ,emotion 等。

jsx 文件中直接引入 css 文件

import './Child.scss';

export default function Child() {
  return (
    <div>
      <h1 className='title'>
        Child
      </h1>
    </div>
  );
}
.title {
  color: red;
}
import React, { useState } from "react";
import Child from '../../components/Child'

export default () => {
  const [showChild, setShowChild] = useState(false);

  return (
    <div>
      <h1 className="title">View</h1>

      <button onClick={() => setShowChild(!showChild)}>showChild</button>

      {showChild && <Child />}
    </div>
  );
}

副作用

一个组件,通常会分一个单独的 jsx 文件来写,在顶部像引入其他 js 变量一样,引入一个 scss 文件,但实际上这种做法是存在副作用的,按照 esmodules 规范,它是会被摇树摇掉的,因为 Child 并不依赖于它。 但默认是允许副作用的存在的,所以这里的 scss 仍然是生效的,在 package.json 中配置 sideEffects: false 会关掉副作用,摇掉这个 scss 文件。

"sideEffects": false,

按照规范来说,不推荐大家写具有副作用的部分的。

css 未隔离,样式影响

需要通过命名来隔离样式,如果出现选择器名字相同,会导致组件内样式冲突。

import css.png

命名困难

通常来讲,大家会使用类选择器来指定标签内容,在不冲突其他的场景时,语义化命名却成一个比较困难的点,常见命名:title, content, desc 等。实际上这样的命名很容易冲突,而且这样的 css 很难进行复用,偶尔遇到 css 冲突却很难解决。

css 只敢加,不敢删

由于没有像 js 变量的依赖引用关系;css 的影响范围,受影响的组件是不好定位的,所以 css 基本上是不敢删除的。

无法直接使用 js 变量

css modules

import React from 'react';
import style from './App.css';

export default () => {
  return (
    <h1 className={style.title}>
      Hello World
    </h1>
  );
};
.title {
  color: red;
}

无副作用

很显然,这种引入 css 的方式,类似于 esm 的 default 默认导出,当组件内不依赖 style 这个变量时,这个 css 就会被摇树摇掉。

css 隔离

构建工具会编译生成新的类名。

命名困难

无法直接使用 js 变量

style 内联样式

例子见前文。

无副作用

css 只对该元素生效

style 写在 dom 的标签上,只对该 dom 生效。

无命名问题

可直接使用 js 变量,实现 css in js

部分 css 无法实现

例如:伪类等。

inline style.png

styled-components

示例

import styled from "styled-components";
import React, { useState } from "react";

const Title = styled.h1`
  &.title-d {
    color: blue;
    font-size: ${props => props.size}px;
  }
`;

export default () => {
  const [fontSize, setFontSize] = useState(14);

  return <div>
    <h1 className="title-d">StyledComponents</h1>
    <Title className="title-d" size={fontSize}>123</Title>
    <button onClick={() => setFontSize(fontSize + 1)}>+1</button>
  </div>
}

由上面的例子可知,title-d 这个不会出现 css 样式影响,做到了样式的隔离。 fontSize 这个变量得知, Title 支持 prop 的传递,实现 css in js ,使用了 js 变量。

闭包用法,骚操作有性能问题

import styled from "styled-components";
import React, { useEffect, useState } from "react";

function Child() {
  useEffect(() => {
    console.log('Child mounted')
  }, []);
  return <div>Child</div>
}

// const Title = styled.h1`
//   color: blue;
//   font-size: ${props => props.size}px;
// `;

export default () => {
  const [fontSize, setFontSize] = useState(14);

  const Title = styled.h1`
    color: blue;
    font-size: ${fontSize}px;
  `;

  return <div>
    <Title size={fontSize}>
      123
      <Child />
    </Title>
    <button onClick={() => setFontSize(fontSize + 1)}>+1</button>
  </div>
}

由例子可知,Title 这个 styled-components 创建的组件,直接利用闭包的特性保存了变量 fontSize ,从而实现了 font-size 的动态性。 但是它有一个问题,这种写法也不是 styled-components 所推荐的,Title 在每次 re-render 时,都创建了一个新的组件,新的引用,既然是一个新的引用,那么 Title 组件直接会被销毁然后重新创建,Title 内的子组件 Child 也是会重新创建,useEffect 已经证明了这一点。 styled-components.com/docs/faqs#w… 同理可得,一个组件的声明不要放在函数组件或渲染方法中,不然也会进行频繁的销毁创建,带来性能问题。

写法原理

developer.mozilla.org/zh-CN/docs/… styled-components 的写法并不是编译时的行为,是 es6 所支持的 标签函数 语法,`` 字符的内容即调用函数的参数。 标签函数接受的第一个参数是 字符串数组,后面的参数是一个 ...rest 参数,字符串数组的长度正好是 rest 参数的个数 + 1 ,即后面的参数是截断的 ${} 变量。

const person = "Mike";
const age = 28;

function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = ageExp > 99 ? "centenarian" : "youngster";

  // 我们甚至可以返回使用模板字面量构建的字符串
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.

同理可得, 可以猜测,styled-components 的实现方法可能如下。

const styled = {
  div: (strings, ...values) => {
    console.log('strings', strings);

    console.log('values', values);
  }
};

const color = 'red';

styled.div`
  color: ${color};
  font-size: 10px;
`;

styled-components原理.png

由上例可知,所以我们在调用 styled 创建一个样式组件时,传参数并不一定需要传入一个模板字符串,可以按照上面的介绍进行传参。不推荐这种写法,不易读,且不完全等价(缺少 raw)

// const Title = styled.h1`
//   color: blue;
//   font-size: ${props => props.size}px;
// `;

// `` 调用函数的等价写法 (不完全等价)
const Title = styled.h1([`
color: blue;
font-size: `, `px;`], props => props.size);

styled 传入的参数组件,一定需要支持 className prop

codesandbox.io/s/epic-brat…

styled 文档:styled-components.com/docs/basics…

问题

前面的大部分问题,在 styled-components 下都可以支持,所以比较推荐的就是 styled-components ,但它仍然没有很好的解决命名问题,styled-components 创建的组件,很多时候既不能复用,语义化也差,无法代表一个样式单元,例如:仅仅是加了一个 fontSize 等。

emotion

我个人认为 emotion 才是一个当下最好使用的 css in js 方案,它像是将 styled-components 和 styled-system 的组合,即能像 styled-components 创建组件,又能直接传入 css 。

render(
  <div
    css={{
      backgroundColor: 'hotpink',
      '&:hover': {
        color: 'lightgreen'
      }
    }}
  >
    This has a hotpink background.
  </div>
)

上面这段代码,本质上是使用 babel 做了一个编译时的代码转换,因为 div 是没有 css 这个 prop 的,利用编译时的转换来实现。

Css 的存在真的有必要吗?(讨论,勿喷)

通常来说,想要复用 css 是很困难的,组件化开发的流行也证明了这一点,可复用的应该是 组件 ,而非 css 。 css 的复杂度更是徒增了开发者的负担,定位它成了一个困难的事情,大部分的场景下,使用 props 来直接传递样式,甚至可以说,样式本就应该是 组件 的一个 prop 。 在 native android 和 flutter 中,都没有 css 的概念。 原生 android 通过 xml + java 来实现,样式直接在 xml 中写就可以了。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:orientation="vertical" >
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/to" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/subject" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="top"
        android:hint="@string/message" />
    <Button
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="@string/send" />
</LinearLayout>

flutter 更是只使用 dart 一门语言来实现即可。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      body: Center(
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text(
                  'Flutter Demo',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 24.0,
                  ),
                ),
                SizedBox(height: 16.0),
                Text(
                  'This is a demo of how to add styles to text in Flutter. You can use the TextStyle class to customize the font, size, color, and other properties of your text.',
                  style: TextStyle(
                    fontSize: 16.0,
                    color: Colors.grey[700],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

由上面例子可知,样式的写法类似于 内联样式;主题这种公用的值,抽取成一个变量(编程语言的)来共用即可。 antd5 已经完全使用了 css in js ;继续像旧的方式,通过选择器来定位元素的方式,是否过时?内联样式(直接传 prop ),是否才是对开发者更小的心智负担?