Skip to main content

编写样式

在上一章编写组件中我们了解到可以快速地通过JavaScript或TypeScript编写一个组件,也了解了@/这一特殊的别名。本章节将介绍如何编写样式文件,以及由CSS Module产生的样式使用的特殊性。

支持语言

reSKRipt支持CSS和LESS作为样式语言,但不支持如SASS、Stylus等其它语言。

考虑到LESS本身的编译性能不太理想,也考虑到React推荐组件粒度足够小,不会让一个组件有太复杂的样式,因此我们比较推荐你尽量使用纯CSS来编写样式。

CSS Module

CSS Modules是由社区提出的CSS的一种编译方法,它会将CSS中的class、id等编译成一个带有哈希的全局唯一的标识,来使各个样式相互隔离,不会产生全局名称上的冲突。

reSKRipt在全局启用了CSS Modules,所有的.css.less文件都会经过Modules处理,除以下2种情况外:

  • 后缀名为.global.css.global.less
  • 属于第三方的样式,即在node_modules下。

让TypeScript支持样式

我们推荐用TypeScript来编写你的应用,但TypeScript并不支持.less.svg.png这些文件的类型定义,所以我们需要先进行手动的声明。

建立一个src/interface/static.d.ts文件,我们把所有非脚本类型的文件的类型定义都放在里面:

/* eslint-disable */
declare module '*.css' {
type Primitive = string | number | boolean | null | undefined;
const content: {
[className: string]: string;
(...names: Array<Primitive | Record<string, Primitive>>): string;
};
export default content;
}

declare module '*.less' {
import style from '*.less';
export default style;
}

declare module '*.png' {
const url: string;
export default url;
}

declare module '*.svg' {
declare const url: string;
export default url;
}

declare module '*.svg?react' {
import {SVGAttributes, ComponentType, RefAttributes} from 'react';

const Component: ComponentType<SVGAttributes<SVGElement> & RefAttributes<SVGElement>>;
export default Component;
}

如果你需要.gif.jpg.ttf等其它二进制格式,也可以参考上文的*.png来定义。

你在上面的定义中,应该也已经窥视到了.css.less.svg与其它类型的文件有所不同,我们在本章会重点说明样式的使用方式,在后续章节会说明.svg的使用。

增加全局样式

在一个应用中,我们往往需要一些全局的样式,它与具体组件无关,更多的是对font-familybodymargin等的处理。

我们建议这些全局的样式被放在src/styles目录下,且统一用一个index.ts文件来引入,所以你的目录结构应该如下:

/src
/styles
app.global.css # 放全局样式
index.ts

app.global.css中,我们来覆盖一些全局样式,再增加几个CSS变量:

html,
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}

:root {
--color-primary: #389af2;
--color-warning: #f69f57;
--color-error: #f5443d;
}

再从index.ts中引入它:

import './app.global.css';

需要特别注意的是,对于.global.css并没有CSS Modules的编译,所以引入它们不需要指定一个变量名

最后,我们在src/entries/index.ts中再把styles/index.ts也引入:

import {render} from 'react-dom';
import '@/styles';

// 初始化代码

截止此刻,我们已经让应用中有了一个全局的样式覆盖。如果你需要使用reset-css等其它的全局样式,同样可以在src/styles/index.ts中引入。

定义全局样式变量

如果你使用LESS来编写样式,那么经常会用到它的变量功能。有一些变量它可能是全局的,你希望尽可能在任何.less文件中都能用到,也并不想每次用的时候要加一个@import (reference) ...的语句。

reSKRipt中,你只需要在src/styles下放置一个后缀为.var.less的文件就可以声明全局全用的变量。

我们试着创建一个src/styles/theme.var.less文件,再写入一些变量:

@header-height: 50px;
@font-size-base: 14px;

注意,这个文件不需要被任何地方import引入,只需要放在那边,它默认会在全局生效。

编写组件样式

引入样式

最后,我们回到最常见的场景:为组件编写样式。

我们先尝试建立一个src/components/Header目录,在这个目录下,我们计划实现一个顶栏的组件,它是一个固定在顶部的带有深色背景的条,并且包含了LOGO和一个全局搜索框。

我们先把对应的样式写到Header/index.less中:

.root {
position: fixed;
top: 0;
left: 0;
right: 0;
height: @header-height;
display: flex;
align-items: center;
padding: 0 20px;
background-color: #1e1e1e;
}

.dark {
background-color: #4f4f4f;
}

.logo {
margin-right: 20px;
}

.logo-image {
width: 40px;
height: 40px;
}

.search {
width: 240px;
}

可以观察到上面的.root类中使用了@header-height这一LESS变量,它是在上文的src/styles/theme.var.less中定义的,因此可以直接引用。

我们再回到组件的实现上,先建立Headers/index.tsx文件,把index.less给引入进来:

import c from './index.less';

了解样式函数

你应该注意到,上文在index.less被引入时,绑定了一个叫做c的变量。

这正是reSKRipt内置的一个功能。因为启用了CSS Modules,所以正常来说样式引入后是一个对象,你可以这样使用:

import styles from './index.less';

<div className={styles.root} />

reSKRipt更进一步,在此基础上使用classnames对样式对象做了一次封装,此处绑定的c变量事实上是一个函数,它的作用与classNames函数完全一致,并且还具备原本对象类型的效果,因此以下使用方法都是合理的:

import c from './index.less';

// 保持与对象相同的用法
c.root;
// 有横线连接的类名会被转为camelCase,下面的调用对应.size-small
c.sizeSmall;
// 字符串作为参数,会返回编译后的串
c('root');
// 传多个字符串,会做拼接
c('root', 'dark');
// 使用对象来根据条件选择性地使用某些类名
c('root', {'size-small': props.small});
// 如果字符串不在LESS中定义,会保持原样,不会加上哈希转义
c('external-class');

还有其它更多的用法,请参考classnames的文档来了解,不在此一一赘述。

为组件增加样式

在了解完reSKRipt对样式的处理后,我们简单地为Header组件增加一些样式:

import {Input} from 'antd';
import logo from './logo.png'; // 图片依然是标准的引入后变成字符串
import c from './index.less';

interface Props {
mode: 'light' | 'dark';
}

function Header({mode}: Props) => {
return (
<header className={c('root', {dark: mode === 'dark'})}>
<a className={c.logo} href="/">
<img className={c.logoImage} src={logo} alt="回到首页" />
</a>
<Input.Search className={c.search} />
</header>
);
}

在将Header组件加入App并成功运行应用后,观察开发者工具中各个HTML元素的class属性,你会发现类似header-root-8bff2这样的串。出于调试和问题排查方便,reSKRipt将组件的名称、LESS中的类名都放到了这个class属性上,并增加了一个哈希值来防止冲突,最终产生了一个同时具备可读性并全局唯一的标识。

总结

在本章中,我们重点讲解了样式的编写与管理,reSKRipt在这一块的处理有以下特点:

  • 全局使用CSS Modules,除了.global.{css,less}和第三方样式外。
  • 全局样式推荐使用src/styles/*.global.css进行管理,使用src/styles/indes.ts统一引入。
  • 可创建src/styles/*.var.less来声明全局可用的LESS变量和mixin。
  • 样式文件被import引入后会变成一个与classnames功能相同的函数,也具备对象访问具体类名的功能。
  • 组件可以使用import c from './index.less'的形式引入样式后与className绑定使用。