自定义样式

定义样式

在前面的章节中您可能已经了解了如何引入样式,我们这里再温习一遍。

首先让我们制作一个最简单的显示 Hello World 的页面,并附加一些样式。关于怎样用 umi 创建一个最简单的页面,可以参考 快速上手 | UmiJS。关于目录结构,可以参考 目录约定 | UmiJS

image.png | left | 683x530

我们的目标是:添加一些样式定义,使得 Hello World 文字更粗更大,变为绿色。

第一,在 src/pages 目录下创建页面 index.jsx 和样式文件 styles.css

.
└── src
    └── pages
        ├── index.jsx
        └── styles.css

写入以下内容到文件

/* index.jsx */
export default () => {
  return (
    <div className={myStyles.hello}>Hello World</div>
  );
};
/* styles.css */
.hello {
  font-size: 32px;
  font-weight: bold;
  color: #30b767; /* 绿色 */
}

注意: 对于 css 文件,

  • 不要在值上使用引号;
  • .号要紧跟 hello,定义一个 class 选择器。

下面展示了错误的写法。

/* 错误!1. 引号是无效的;2. hello 前面的点漏掉了 */
hello {
  font-size: '32px';
  font-weight: 'bold';
  color: "#30b767";
}

第二,要把样式文件在页面文件中引用,它才能真正生效。首先添加一行 import 语句;其次在 div 的 className 属性中使用 hello。

import myStyles from './styles.css';
export default () => {
  return (
    <div className={myStyles.hello}>Hello World</div>
  );
};

注意:

  • 在 React 的语境下,我们使用 className 保留字来定义一个 html 元素的 class,而非 w3c 标准中的 class 保留字。
  • 在 umi 中我们默认开启了 CSS modules 特性,这使得 class 名需要通过变量属性去引用。

在终端中运行 umi dev 启动我们的应用。看到了期待中 Hello World 渲染出的样子。

image.png | left | 683x512

注意:

  • 如果没有预期效果,请检查是否在终端有报错,请打开浏览器的控制台看是否有相关报错。
  • 如果依然有问题,请检查是否正在使用某些网络工具,或者曾经不慎修改了 /etc/hosts 文件。

因为默认开启了 CSS modules 特性,这份样式定义只会对 index.html 文件起作用,所以我们不用再担心不同样式文件之间的干扰。在下一节中,我们将详细地介绍 CSS modules。

理解 CSS modules

让我们打开浏览器的调试工具(这里以 Chrome DevTool 为例子)。切到 Elements 面板,找到 Hello World。

image.png | left | 683x663

我们看到它实际的 class 是 style__hello__<hash数值>,并非在源文件中声明的 hello。这就是 CSS modules 起了作用。这个 hash 值是全局唯一的,比如通过文件路径来获得,这样 class 名称就做到了全局唯一。通过全局唯一的 CSS 命名,我们变相地获得了局部作用域的 CSS(scoped CSS)。如果一个 CSS 文件仅仅是作用在某个局部的话,我们称这样一个 CSS 文件为 CSS module

CSS modules 不是一个可以安装的 npm 包,也不是 w3c 中的某个标准,只是一项流行的社区规范(an opinionated proposal)。webpack browserify 等打包工具(module bundler)的能力让工具生成局部 CSS 成为可能,CSS modules 规范也应运而生。

CSS 的局部作用域解决了大问题。在w3c 规范中,CSS 始终是「全局的」。在传统的 web 开发中,最为头痛的莫过于处理 CSS 问题。因为全局性,明明定义了样式,但就是不生效,原因可能是被其他样式定义所强制覆盖。接手老项目更是噩梦,改对了一个地方的样式,却把另外许多地方的样式打乱。这一切的罪魁祸首就是 CSS 的「全局型」。为了得到局部作用域,社区曾经流行过叫做 BEM 的方案,它 约定 class 应该写成:

.block-name__element-name--modifier-name {

}

但这终究只是一种书写 class 的命名规范(convention),可以因为疏忽被打破,每个团队之间的规范也不一定互通。CSS modules 是从工具层面给出的一套生成局部 CSS 的规范,本质还是生成全局唯一的 CSS 定义。webpack 实现了这套规范。umi 依赖 webpack,默认开启了 CSS modules 特性。

webpack 实现 CSS module 的原理

在现代 web 开发中,服务器并不能直接使用我们写的 JS CSS HMTL 文件。事实上,我们按照规范写出代码,输入给编译工具 (transpiler) ,它最终把代码转换/打包,输出成浏览器可以使用的 JS CSS HTML。在多年的社区沉淀后,脱颖而出的是诸如 webpack 这样的工具,这类编译工具又称为 module bundler。webpack 允许我们用 import/export (ES6) 或者 require/module.exports (CommonJs) 这样的语法来书写我们的 JS 代码,它甚至允许我们在 js 里面 import 一个 CSS 文件。注意:如果脱离了 webpack 的语境,这么写当然是会引起语法错误的。

在现代 web 开发中,我们的运行时代码强耦合了编译时工具,强耦合换来的是传统 web 开发所不可企及的新能力。对于 webpack,当我们每次写了 import A from B 的时候,我们其实是声明了一个A 对于 B 的依赖。当在 a.js 中写入 import styles from a.css 后,webpack 就可以把这个依赖翻译成:每当 a.js 被使用时,保证生成一个 style 标签,里面嵌入 a.css 的内容。同时 webpack 给予我们另一个能力:不同类型文件间可以信息传递。webpack 把 a.css 中的类名根据规则编译成为全局唯一的字符串,传递给 a.js 使用,于是手工维持的命名规则就可以自动生成。

注意:很多 CSS 选择器是不会被 CSS Modules 处理的,比如 body、div 、a 这样的 HTML 标签名就不会。我们推荐如果要定义局部 css 样式/动画, 只使用 class 或 @keyframe。

CSS modules 与 Less 语法一起使用

虽然被称作 CSS modules,但是它完全可以和 Less 一起无缝使用,与使用普通 CSS 没有什么区别,只需要使用 Less 的语法写样式就可以了。鉴于 antd 的样式也使用了 Less 作为开发语言。所以我们的实战教程强烈推荐 CSS module 和 Less 文件一起使用。

Less 介绍

Less 是一个 CSS 的超集,Less 允许我们定义变量,使用嵌套式声明,定义函数等。严格说 Less 包含两部分:1. Less 的语法。2. Less 预处理器(Less preprocessor)。浏览器终究只认识 CSS,所以 Less 文件需要经过 Less 预处理器编译成为 CSS。

在工具的支持下,一个 Less 文件首先会经过 CSS modules 的编译,把类名全局唯一化,然后才被 Less preprocessor 编译成为 CSS 文件。正因此,Less 文件可以和 CSS modules 无缝联合使用。

第一,我们建立一个新页面 css-modules-with-less,并且使用 less 文件作为样式文件。

.
└── src
    └── pages
        ├── css-modules-with-less
        │   ├── index.jsx
        │   └── styles.less
        ├── index.jsx
        └── styles.css

第二,我们定义 less 文件,故意添加一些非 css 语法,比如嵌套、变量定义。

@grey-color: rgba(0, 0, 0, .25);

.hello {
  font-size: 32px;
  font-weight: bold;
  color: #30b767;
  .deleted {
    text-decoration: line-through;
    background-color: @grey-color;
  }
}

第三,我们创建页面,并使用这份样式。

import styles from './styles.less';

export default () => {
  return (
    <div className={styles.hello}>
      <span className={styles.deleted}>Hello World</span>
    </div>
  );
};

页面效果如下:

image.png | left | 640x480

DevTools 里可以看到 less 文件被正确地编译了,并且也经过 CSS modules 处理。

image.png | left | 555x643

在 CSS modules 中覆盖 antd 样式

全局唯一带来的特性就是样式不会被覆盖,但是我们有时就是想做样式覆盖。比如我们引用了 antd 的 Button 组件,我们想要覆盖它的一些样式属性定义。通过观察,我们知道 Button 组件中声明了 .ant-btn 类用来做样式定义,我们就从它入手。

image.png | left | 683x714

小技巧: 使用 DevTools 的元素选择器再配合 styles 面板,可以用来了解 antd 中使用的样式名称。

比如我们想要覆盖掉 .ant-btn 中声明的 border-radius 属性,来绘制圆角按钮。直观的想法是:

<!-- 在 css/less 文件中定义覆盖 -->
.override-ant-btn .ant-btn {
    border-radius: 16px;
}

<!-- 在 html 中使用对应的 class 名称 -->
<span className={styles['override-ant-btn']}>
  <Button>加入圆角的按钮</Button>
</span>

但其实是没效果的。由于 CSS modules 的使用, ant-btn 会被改名:

image.png | left | 531x472

从而无法引用到 ant-btn 原名。

此时 CSS modules 的 global 语法 派上了用场。它允许我们声明一个 class 名称不可被改写。语法很简单:

:global(.ant-btn) {
  // ...
}

于是 .ant-btn 就不会被改写了。这里我们举一个实用的例子,来具体展示使用 global 覆盖 antd Button 组件的样式。

第一,我们建立一个新页面 css-modules-with-antd。

.
└── src
    └── pages
        ├── css-modules-with-antd
        │   ├── index.jsx
        │   └── styles.less
        ├── css-modules-with-less
        │   ├── index.jsx
        │   └── styles.less
        ├── index.jsx
        └── styles.css

第二,我们建立页面文件,包含两个 antd Button,其中一个需要被样式覆盖,一个不需要。

import styles from './styles.less';
import { Button } from 'antd';

export default () => {
  return (
    <div>
      <p>
        <span className={styles['override-ant-btn']}>
          <Button>圆角样式按妞</Button>
        </span>
      </p>
      <p>
        <Button>antd 原始按钮</Button>
      </p>
    </div>
  );
};

第三,我们建立样式文件覆盖 antd 原生样式,用 global 来声明不修改 class 名。

.override-ant-btn {
  :global(.ant-btn) {
    border-radius: 16px;
  }
}

于是看到覆盖的效果。

image.png | left | 640x480

最后强调,global 不应该被滥用,特别地我们建议:若想在某个文件中覆盖 antd 样式,请加上一个类似 .override-ant-btn 的类包裹住 global 修饰的名称,以避免全局样式声明分散在项目各处。

更换 antd 主题

前面章节我们讲过如何覆盖某个页面中 antd 的样式,有时候我们想要「批量修改」 antd 的样式,这就需要利用 less 提供的一个能力:modifyVars。简单地讲,antd 在使用 less 定义样式时,使用了大量的变量声明。这些变量的定义在编译期是可以被工具识别并修改的。

如果使用的是 umi ,这个过程相当简单,只需要简单地修改配置文件即可。

第一,找到 umi 的配置文件,如果不存在则创建一个。注意配置文件放置的位置。我们这里使用 .umirc.js

├── .gitignore
├── .umirc.js
└── src
    ├── global.less
    └── pages
        ├── .umi
        │   ├── router.js
        │   └── umi.js
        ├── css-modules-with-antd
        │   ├── index.jsx
        │   └── styles.less
        ├── css-modules-with-less
        │   ├── index.jsx
        │   └── styles.less
        ├── index.jsx
        └── styles.css

第二,把之前创建的 css-modules-with-antd 页面中的 Button 加上 type=“primary” 定义。当 type 值为 “primary” 时,Button 应该显示为蓝色按钮。

<div>

  <p>
    <span className={styles['override-ant-btn']}>
      <Button type="primary">圆角样式按妞</Button>
    </span>
  </p>

  <p>
    <Button type="primary">antd 原始按钮</Button>
  </p>

</div>

image.png | left | 640x480

第三,配置 umi 主题(实质是 modifyVars 机制)。如果是创建新文件,则写入:

export default {
  theme: {
    "@primary-color": "#30b767",
  }
}

如果是已存在配置文件,则把 theme 这一段嵌入到 export default 内部。

export default {
  // 若已有配置
  outputPath: "./build",

  // 加入 theme 定义
  theme: {
    "@primary-color": "#30b767", // 绿色
  }
}

在 antd Button 定义时,颜色并不是写死的,而是使用了 Less 变量。我们看到修改变量后,按钮的颜色变为了绿色。

image.png | left | 640x480

想要知道 antd 都定义了哪些变量,可以参考这里

更改全局样式

如果使用 umi 的话,有一个专门的文件 global.less 来让我们书写全局样式。这个文件并不会被 CSS modules 处理。

.
└── src
    ├── global.less
    └── pages
        ├── css-modules-with-antd
        │   ├── index.jsx
        │   └── styles.less
        ├── css-modules-with-less
        │   ├── index.jsx
        │   └── styles.less
        ├── index.jsx
        └── styles.css

一个用途是全局性地定义 HTML 标签的样式,比如写入:

p {
  margin: 0;
}

如果此时去往 css-modules-with-antd 会发现上下两个按钮贴在一起了。

image.png | left | 640x480

原因是在大多数浏览器中,默认 p 元素是有 1em 高度的 margin-bottom。而我们的定义覆盖了默认的样式。

另外一个用途是全局性地覆盖第三方库的样式,比如 antd 中的样式 。

我们全局覆盖 ant-btn 的样式,增加下面的定义:

.ant-btn {
  box-shadow: 0 3px 7px rgba(0, 0, 0, .5);
}

可以看到我们成功地添加了按钮阴影

image.png | left | 640x480