Last updated on

React 组件库 CSS 样式方案

背景

最近考虑构建一个自己的组件库,需要考虑 CSS 样式方案

分析

当我们构建组件库时,考虑问题的角度和普通项目可能会不太一样,不但需要考虑开发体验,同时也要照顾到使用者的感受。

CSS 与 JS 的关联关系

CSS 的方案分为以下三种类型

1. 样式和逻辑分离。

组件的 CSS 和 JS 在代码层面分离,JS 里不引入样式文件,在组件库打包时分别生成独立的逻辑和样式文件。对于组件库的使用者来说,添加一个组件,需要分别引入组件代码和 CSS 文件。 假设做一个 Foo 的组件 index.tsx

import React, { type FC } from "react";

const Foo: FC<{ title: string }> = (props) => (
  <h4 className="foo">{props.title}</h4>
);

export default Foo;

index.less

.foo {
  color: red;
}

如果要使用这个组件的话 要同时引入 index.tsx 和 index.less

使用案例的话 element-plus

// main.ts
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";

antd@4

import React, { useState } from "react";
import { render } from "react-dom";
import { ConfigProvider, DatePicker, message } from "antd";
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from "antd/es/locale/zh_CN";
import moment from "moment";
import "moment/locale/zh-cn";
import "antd/dist/antd.css";
import "./index.css";

此外 还可以借助打包工具进行按需样式自动引入,如

module.exports = {
  plugins: [
    ["import", { libraryName: "antd", style: true }], // `style: true` 会加载 less 文件
  ],
};

2. 样式和逻辑结合

将组件的 JS 和 CSS 打包在一起,最终只输出 JS 文件。使用时只需要引入组件就可以直接使用。

import 样式文件

import React, { type FC } from 'react';

+ import './style.less'

const Foo: FC<{ title: string }> = (props) => <h4 className='foo'>{props.title}</h4>;

export default Foo;

生成的产物是

import React from "react";
import "./style.less";
var Foo = function Foo(props) {
  return /*#__PURE__*/ React.createElement(
    "h4",
    {
      className: "foo",
    },
    props.title
  );
};
export default Foo;
.foo {
  color: red;
}

上面这种写法对我们使用组件的项目的打包工具是有要求的,通过打包工具将 CSS 打进 JS 里。例如使用 webpack 配合 style-loader 或 rollup 配合 rollup-plugin-styles。

举 rollup-plugin-styles 的例子

rollup.config.js

import styles from "rollup-plugin-styles";

export default {
  input: "src/main.js",
  output: {
    file: "dist/index.js",
    format: "cjs",
  },
  plugins: [
    styles({
      mode: "inject",
      modules: true,
    }),
  ],
};

main.js

import "./index.css";
import style from "./index.module.css";

console.log(style);

index.css

body {
  height: 100vh;
  background-color: pink;
}

@import url("./style.css");

index.module.css

.foo {
  color: blue;
}

@import url(./style.css);

style.css

.bar {
  color: red;
}
PicGo20230423203707

CSS in JS

在这个方案下,不存在 CSS 文件,一切都是 JS,对打包工具是没要求了,但是有运行时的性能消耗,而且作为组件库不好样式覆盖 「但也有解,见下一节」

import styled from "styled-components";

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

  .foo {
    color: red;

    &:hover {
      background: blue;
    }
  }
`;

function Demo1() {
  return (
    <div>
      Demo1
      <Title>
        I am Happy
        <div className="foo">foo</div>
      </Title>
    </div>
  );
}
PicGo20230423205651

再谈 CSS in JS

CSS in JS 最大的优势便是灵活,注意到 antd 5 和 mui 都选用了 CSS in JS 的方案

https://mui.com/material-ui/customization/how-to-customize/

PicGo20230423210544

antd5 做的很棒,在样式方面借助 :where() 结合 cssinjs 的 hash 选择器,我们可以让组件的样式始终处于在 hash 的范围之下,这可以保证组件样式不会对全局样式造成任何污染,又方便覆盖 https://ant-design.github.io/antd-style/guide/components-usage

选择

最终选择用 antd-style 来做基于 antd 的二次开发,很舒服

PicGo20230423212450

下面是写的一个小 demo PicGo20230424102456

Foo/index.tsx

import React from "react";
import { createStyles } from "../utils";

const compCls = `foo`;

const useStyles = createStyles(({ token, css, prefixCls, cx }) => {
  const prefix = [prefixCls, compCls].join("-");

  return {
    container: cx(
      prefix,
      css`
        &.${prefix} {
          background-color: ${token.colorPrimaryBg};
        }
      `
    ),
    card: cx(
      `${prefix}-card`,
      css`
        &.${prefix}-card {
          color: ${token.colorPrimary};
        }
      `
    ),
  };
});

const Foo = (props: { title: string }) => {
  const { styles } = useStyles();
  return (
    <div className={styles.container}>
      <div className={styles.card}>{props.title}</div>
    </div>
  );
};

export default Foo;

utils.ts

import { createInstance } from "antd-style";

export const { createStyles, ThemeProvider } = createInstance({
  hashPriority: "low",
  prefixCls: "sedationh",
});

参考

https://juejin.cn/post/7097100515535765534 https://blog.pig1024.me/posts/62fa1f4e8631e51c9414da21