CSS 是前端开发的重要组成部分,可是它并不完美,本文主要探讨 React 样式管理方面的一些解决方案,目的是实现样式的高度可定制化, 让大型项目的样式代码更容易维护.javascript
系列目录css
目录html
2014 年vjeux一个 speak 深入揭示的原生 CSS 的一些局限性. 虽然它有些争议, 对于开发者来讲更多的是启发. 至从那以后出现了不少 CSS-in-js
解决方案.前端
CSS 的选择器是没有隔离性的, 无论是使用命名空间仍是 BEM 模式组织, 最终都会污染全局命名空间. 尤为是大型团队合做的项目, 很难肯定某个特定的类或者元素是否已经赋过样式. 因此在大部分状况下咱们都会绞尽脑汁新建立一个类名, 而不是复用已有的类型.java
解决的方向: 生成惟一的类名; shadow dom; 内联样式; Vue-scoped 方案react
因为 CSS 的'全局性', 因此就产生了依赖问题:webpack
一方面咱们须要在组件渲染前就须要先将 CSS 加载完毕, 可是很难清晰地定义某个特定组件依赖于某段特定的 CSS 代码; 另外一方面, 全局性致使你的样式可能被别的组件依赖(某种程度的细节耦合), 你不能随便修改你的样式, 以避免破坏其余页面或组件的样式. 若是团队没有制定合适的 CSS 规范(例如 BEM, 不直接使用标签选择器, 减小选择器嵌套等等), 代码很快就会失控git
解决的方向: 以前文章提到组件是一个内聚单元, 样式应该是和组件绑定的. 最基本的解决办法是使用相似 BEM 命名规范来避免组件之间的命名冲突, 再经过建立优于复用, 组合优于继承的原则, 来避免组件间样式耦合;github
因为上述'依赖'问题, 组件样式之间并无明确的边界, 很难判断哪些样式属于那个组件; 在加上 CSS 的'叠层特性', 更没法肯定删除样式会带来什么影响.web
现代浏览器已支持 CSS 无用代码检查. 但对于无组织的 CSS 效果不会太大
解决的方向: 若是样式的依赖比较明确,则能够安全地移除无用代码
选择器和类名的压缩能够减小文件的体积, 提升加载的性能. 由于原生 CSS 通常有开发者由配置类名(在 html 或 js 动态指定), 因此工具很难对类名进行控制.
压缩类名也会减低代码的可读性, 变得难以调试.
解决的方向: 由工具来转换或建立类名
常规的 CSS 很难作到在样式和 JS 之间共享变量, 例如自定义主题色, 一般经过内联样式来部分实现这种需求
解决的方向: CSS-in-js
CSS 规则的加载顺序是很重要的, 他会影响属性应用的优先级, 若是按需加载 CSS, 则没法确保他们的解析顺序, 进而致使错误的样式应用到元素上. 有些开发者为了解决这个问题, 使用!important 声明属性, 这无疑是进入了另外一个坑.
解决方向:避免使用全局样式,组件样式隔离;样式加载和组件生命周期绑定
组件的样式应该是能够自由定制的, 开发者应该考虑组件的各类使用场景. 因此一个好的组件必须暴露相关的样式定制接口. 至少须要支持为顶层元素配置className
和style
属性:
interface ButtonProps {
className?: string;
style?: React.CSSProperties;
}
复制代码
这两个属性应该是每一个展现型组件应该暴露的 props, 其余嵌套元素也要考虑支持配置样式, 例如 footerClassName, footerStyle.
display: -webkit-flex; display: flex;
)因此 内联 CSS 适合用于设置动态且比较简单的样式属性
社区上有许多 CSS-in-js 方案是基于内联 CSS 的, 例如 Radium, 它使用 JS 添加事件处理器来模拟伪类, 另外也媒体查询和动画. 不过不是全部东西均可以经过 JS 模拟, 好比伪元素. 因此这类解决方案用得比较少
社区有不少 CSS 解决方案, 有个项目(MicheleBertoli/css-in-js)专门罗列和对比了这些方案. 读者也能够读这篇文章(What to use for React styling?)学习对 CSS 相关技术进行选型决策
社区上最流行的, 也是笔者以为使用起来最舒服的是styled-components
, styled-components 有下列特性:
推荐这篇文章: Stop using css-in-javascript for web development, styled-components 能够基本覆盖全部 CSS 的使用场景:
// 定义组件props
const Title = styled.h1<{ active?: boolean }>` color: ${props => (props.active ? 'red' : 'gray')}; `;
// 固定或计算组件props
const Input = styled.input.attrs({
type: 'text',
size: props => (props.small ? 5 : undefined),
})``;
复制代码
const Button = styled.button` color: palevioletred; font-size: 1em; margin: 1em; padding: 0.25em 1em; border: 2px solid palevioletred; border-radius: 3px; `;
// 覆盖和扩展已有的组件, 包含styled生成的组件仍是自定义组件(经过className传入)
const TomatoButton = styled(Button)` color: tomato; border-color: tomato; `;
复制代码
在 SCSS 中, mixin 是重要的 CSS 复用机制, styled-components 也能够实现:
定义:
import { css } from 'styled-components';
// utils/styled-mixins.ts
export function truncate(width) {
return css` width: ${width}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `;
}
复制代码
使用:
import { truncate } from '~/utils/styled-mixins';
const Box = styled.div` // 混入 ${truncate('250px')} background: papayawhip; `;
复制代码
const Example = styled(Component)` // 自动厂商前缀 padding: 2em 1em; background: papayawhip; // 伪类 &:hover { background: palevioletred; } // 提供样式优先级技巧 &&& { color: palevioletred; font-weight: bold; } // 覆盖内联css样式 &[style] { font-size: 12px !important; color: blue !important; } // 支持媒体查询 @media (max-width: 600px) { background: tomato; // 嵌套规则 &:hover { background: yellow; } } > p { /* descendant-selectors work as well, but are more of an escape hatch */ text-decoration: underline; } /* Contextual selectors work as well */ html.test & { display: none; } `;
复制代码
引用其余组件
因为 styled-components 的类名是自动生成的, 因此不能直接在选择器中声明他们, 但能够在模板字符串中引用其余组件:
const Icon = styled.svg` flex: none; transition: fill 0.25s; width: 48px; height: 48px; // 引用其余组件的类名. 这个组件必须是styled-components生成或者包装的组件 ${Link}:hover & { fill: rebeccapurple; } `;
复制代码
媒体查询帮助方法:
// utils/styled.ts
const sizes = {
giant: 1170,
desktop: 992,
tablet: 768,
phone: 376,
};
export const media = Object.keys(sizes).reduce((accumulator, label) => {
const emSize = sizes[label] / 16;
accumulator[label] = (...args) => css` @media (max-width: ${emSize}em) { ${css(...args)} } `;
return accumulator;
}, {});
复制代码
使用:
const Container = styled.div` color: #333; ${media.desktop`padding: 0 20px;`} ${media.tablet`padding: 0 10px;`} ${media.phone`padding: 0 5px;`} `;
复制代码
SCSS 也提供了不少内置工具方法, 好比颜色的处理, 尺寸的计算. styled-components 提供了一个相似的 js 库: polished来知足这部分需求, 另外还集成了经常使用的 mixin, 如 clearfix. 经过 babel 插件能够在编译时转换为静态代码, 不须要运行时.
全局样式
全局样式和组件生命周期绑定, 当组件卸载时也会删除全局样式. 全局样式一般用于覆盖一些第三方组件样式
const GlobalStyle = createGlobalStyle`
body {
color: ${props => (props.whiteColor ? 'white' : 'black')};
}
`
// Test
<React.Fragment>
<GlobalStyle whiteColor />
<Navigation /> {/* example of other top-level stuff */}
</React.Fragment>
复制代码
styled-components 的 ThemeProvider 能够用于取代 SCSS 的变量机制, 只不过它更加灵活, 能够被全部下级组件共享, 并动态变化.
关于 Theme 对象的设计我以为能够参考传统的 UI 框架, 例如Foundation或者Bootstrap, 通过多年的迭代它们代码组织很是好, 很是值得学习. 以 Bootstrap 的项目结构为例:
.
├── _alert.scss
├── ... # 定义各类组件的样式
├── _print.scss # 打印样式适配
├── _root.scss # 🔴根样式, 即全局样式
├── _transitions.scss # 过渡效果
├── _type.scss # 🔴基本排版样式
├── _reboot.scss # 🔴浏览器重置样式, 相似于normalize.css
├── _functions.scss
├── _mixins.scss
├── _utilities.scss
├── _variables.scss # 🔴变量配置, 包含全局配置和全部组件配置
├── bootstrap-grid.scss
├── bootstrap-reboot.scss
├── bootstrap.scss
├── mixins # 各类mixin, 可复用的css代码
├── utilities # 各类工具方法
└── vendor
└── _rfs.scss
复制代码
_variables.scss
包含了如下配置:
颜色系统: 调色盘配置
灰阶颜色: 提供白色到黑色之间多个级别的灰阶颜色. 例如
语义颜色: 根据 UI 上面的语义, 定义各类颜色. 这个也是 CSS 开发的常见模式
尺寸系统: 多个级别的间距, 尺寸大小配置
配置开关: 全局性的配置开关, 例如是否支持圆角, 阴影
连接样式配置: 如颜色, 激活状态, decoration
排版: 字体, 字体大小, font-weight, 行高, 边框, 标题等基本排版配置
网格系统断点配置
bootstrap 将这些配置项有很高的参考意义. 组件能够认为是 UI 设计师 的产出, 若是你的应用有统一和规范的设计语言(参考antd), 这些配置会颇有意义。样式可配置化可让你的代码更灵活, 更稳定, 可复用性和可维护性更高. 无论对于 UI 设计仍是客户端开发, 设计规范能够提升团队工做效率, 减小沟通成本.
styled-components 的 Theme 使用的是React Context
API, 官方文档有详尽的描述, 这里就不展开了. 点击这里了解更多, 另外在这里了解如何在 Typescript 中声明 theme 类型
可使用babel-plugin-styled-components
或babel macro
来支持服务端渲染、 样式压缩和提高 debug 体验. 推荐使用 macro 形式, 无需安装和配置 babel 插件. 在 create-react-app 中已内置支持:
import styled, { createGlobalStyle } from 'styled-components/macro';
const Thing = styled.div` color: red; `;
复制代码
详见Tooling
比较能想到的局限性是性能问题:
下面是基于 v4.0 基准测试对比图, 在众多 CSS-in-js 方案中, styled-components 处于中上水平:
Div
, Span
这类直接照搬元素名的无心义的组件命名在一个文件中定义 styled-components 组件. 对于比较简单的组件, 通常会在同一个文件中定义 styled-components 组件就好了. 下面是典型组件的文件结构:
import React, { FC } from 'react';
import styled from 'styled-components/macro';
// 在顶部定义全部styled-components组件
const Header = styled.header``;
const Title = styled.div``;
const StepName = styled.div``;
const StepBars = styled.div``;
const StepBar = styled.div<{ active?: boolean }>``;
const FormContainer = styled.div``;
// 使用组件
export const Steps: FC<StepsProps> = props => {
return <>...</>;
};
export default Steps;
复制代码
然而对于比较复杂的页面组件来讲, 会让文件变得很臃肿, 扰乱组件的主体, 因此笔者通常会像抽取到单独的styled.tsx
文件中:
import React, { FC } from 'react';
import { Header, Title, StepName, StepBars, StepBar, FormContainer } from './styled';
export const Steps: FC<StepsProps> = props => {
return <>...</>;
};
export default Steps;
复制代码
考虑导出 styled-components 组件, 方便上级组件设置样式
// ---Foo/index.ts---
import * as Styled from './styled';
export { Styled };
// ...
// ---Bar/index.ts----
import { Styled } from '../Foo';
const MyComponent = styled.div` & ${Styled.SomeComponent} { color: red; } `;
复制代码
这里值得一提的是CSS-module, 这也是社区比较流行的解决方案. 严格来讲, 这不是 CSS-in-js. 有兴趣的读者能够看这篇文章CSS Modules 详解及 React 中实践.
特性:
CSS module 一样也有外部样式覆盖问题, 因此须要经过其余手段对关键节点添加其余属性(如 data-name).
若是使用 css-module, 建议使用
*.module.css
来命名 css 文件, 和普通 CSS 区分开来.
扩展:
若是是做为第三方组件库形式开发, 我的以为不该该耦合各类 CSS-in-js/CSS-module. 不能强求你的组件库使用者耦合这些技术栈, 并且部分技术是须要构建工具支持的. 建议使用原生 CSS 或者将 SCSS/Less 这些预处理工具做为加强方案
笔者的项目大部分都是使用styled-components
, 但对于部分极致要求性能的组件, 通常我会回退使用原生 CSS, 再配合 BEM 命名规范. 这种最简单方式, 可以知足大部分需求.
每一个团队的状况和偏好不同, 选择合适本身的才是最好的. 关于 CSS 方面的技术栈搭配也很是多样:
综上所述, CSS-in-js 和 CSS 方案各有适用场景. 好比对于组件库, 如 antd 则选择了 Preprocessor 方案; 对于通常应用笔者建议使用 CSS-in-js 方案, 它学习成本很低, 而且There's Only One Way To Do It
没有太多心智负担, 不须要学习冗杂的方法论, 代码相对比较可控; 另外它还支持跨平台, 在 ReactNative 下, styled-components 是更好的选择. 而 CSS 方案, 对于大型应用要作到有组织有纪律和规划化, 须要花费较大的精力, 尤为是团队成员能力不均状况下, 很容易失控
现在 CSS-Image-Sprite 早已被 SVG-Sprite 取代. 而在 React 生态中使用svgr
更加方便, 它能够将 svg 文件转换为 React 组件, 也就是一个普通的 JS 模块, 它有如下优点:
基本用法:
import starUrl, { ReactComponent as Star } from './star.svg';
const App = () => (
<div>
<img src={starUrl} alt="star" />
<Star />
</div>
);
复制代码
了解更多
antd 3.9 以后使用 svg 图标代替了 font 图标
对比SVG vs Image, SVG vs Iconfont
Bootstrap v4 全面使用 rem 做为基本单位, 这使得全部组件均可以响应浏览器字体的调整:
rem 可让整个文档能够响应 html 字体的变化, 常常用于移动端等比例还原设计稿, 详见Rem 布局的原理解析. 我我的对于以为弹性组件来讲更重要的是 em 单位, 尤为是那些比例固定组件, 例如 Button, Switch, Icon. 好比我会这样定义 svg Icon 的样式:
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
}
复制代码
像 iconfont 同样, 外部只须要设置font-size
就能够配置 icon 到合适的尺寸, 默认则继承当前上下文的字体大小:
<MyIcon style={{ fontSize: 17 }} />
复制代码
em 可让Switch
这类固定比例的组件的样式能够更容易的被配置, 能够配合函数将px转换为em:
扩展:
上文已经阐述了 UI 设计规范的重要性, 有兴趣的读者能够看看这篇文章开发和设计沟通有多难? - 你只差一个设计规范. 简单总结一下:
能够参考如下规范: