React组件设计实践总结03 - 样式的管理

CSS 是前端开发的重要组成部分,可是它并不完美,本文主要探讨 React 样式管理方面的一些解决方案,目的是实现样式的高度可定制化, 让大型项目的样式代码更容易维护.javascript

系列目录css


目录html




1. 认识 CSS 的局限性

vjeux-speak

2014 年vjeux一个 speak 深入揭示的原生 CSS 的一些局限性. 虽然它有些争议, 对于开发者来讲更多的是启发. 至从那以后出现了不少 CSS-in-js 解决方案.前端

1️⃣ 全局性

CSS 的选择器是没有隔离性的, 无论是使用命名空间仍是 BEM 模式组织, 最终都会污染全局命名空间. 尤为是大型团队合做的项目, 很难肯定某个特定的类或者元素是否已经赋过样式. 因此在大部分状况下咱们都会绞尽脑汁新建立一个类名, 而不是复用已有的类型.java

解决的方向: 生成惟一的类名; shadow dom; 内联样式; Vue-scoped 方案react


2️⃣ 依赖

因为 CSS 的'全局性', 因此就产生了依赖问题:webpack

一方面咱们须要在组件渲染前就须要先将 CSS 加载完毕, 可是很难清晰地定义某个特定组件依赖于某段特定的 CSS 代码; 另外一方面, 全局性致使你的样式可能被别的组件依赖(某种程度的细节耦合), 你不能随便修改你的样式, 以避免破坏其余页面或组件的样式. 若是团队没有制定合适的 CSS 规范(例如 BEM, 不直接使用标签选择器, 减小选择器嵌套等等), 代码很快就会失控git

解决的方向: 以前文章提到组件是一个内聚单元, 样式应该是和组件绑定的. 最基本的解决办法是使用相似 BEM 命名规范来避免组件之间的命名冲突, 再经过建立优于复用, 组合优于继承的原则, 来避免组件间样式耦合;github


3️⃣ 无用代码的移除

因为上述'依赖'问题, 组件样式之间并无明确的边界, 很难判断哪些样式属于那个组件; 在加上 CSS 的'叠层特性', 更没法肯定删除样式会带来什么影响.web

现代浏览器已支持 CSS 无用代码检查. 但对于无组织的 CSS 效果不会太大

解决的方向: 若是样式的依赖比较明确,则能够安全地移除无用代码


4️⃣ 压缩

选择器和类名的压缩能够减小文件的体积, 提升加载的性能. 由于原生 CSS 通常有开发者由配置类名(在 html 或 js 动态指定), 因此工具很难对类名进行控制.

压缩类名也会减低代码的可读性, 变得难以调试.

解决的方向: 由工具来转换或建立类名


5️⃣ 常量共享

常规的 CSS 很难作到在样式和 JS 之间共享变量, 例如自定义主题色, 一般经过内联样式来部分实现这种需求

解决的方向: CSS-in-js


6️⃣ CSS 解析方式的不肯定性

CSS 规则的加载顺序是很重要的, 他会影响属性应用的优先级, 若是按需加载 CSS, 则没法确保他们的解析顺序, 进而致使错误的样式应用到元素上. 有些开发者为了解决这个问题, 使用!important 声明属性, 这无疑是进入了另外一个坑.

解决方向:避免使用全局样式,组件样式隔离;样式加载和组件生命周期绑定




2. 组件的样式管理

1️⃣ 组件的样式应该高度可定制化

组件的样式应该是能够自由定制的, 开发者应该考虑组件的各类使用场景. 因此一个好的组件必须暴露相关的样式定制接口. 至少须要支持为顶层元素配置classNamestyle属性:

interface ButtonProps {
  className?: string;
  style?: React.CSSProperties;
}
复制代码

这两个属性应该是每一个展现型组件应该暴露的 props, 其余嵌套元素也要考虑支持配置样式, 例如 footerClassName, footerStyle.




2️⃣ 避免使用内联 CSS

  1. style props 添加的属性不能自动增长厂商前缀, 这可能会致使兼容性问题. 若是添加厂商前缀又会让代码变得啰嗦.
  2. 内联 CSS 不支持复杂的样式配置, 例如伪元素, 伪类, 动画定义, 媒体查询和媒体回退(对象不容许同名属性, 例如display: -webkit-flex; display: flex;)
  3. 内联样式经过 object 传入组件, 内联的 object 每次渲染会从新生成, 会致使组件从新渲染. 固然经过某些工具能够将静态的 object 提取出去
  4. 不方便调试和阅读 ...

因此 内联 CSS 适合用于设置动态且比较简单的样式属性

社区上有许多 CSS-in-js 方案是基于内联 CSS 的, 例如 Radium, 它使用 JS 添加事件处理器来模拟伪类, 另外也媒体查询和动画. 不过不是全部东西均可以经过 JS 模拟, 好比伪元素. 因此这类解决方案用得比较少




3️⃣ 使用 CSS-in-js

社区有不少 CSS 解决方案, 有个项目(MicheleBertoli/css-in-js)专门罗列和对比了这些方案. 读者也能够读这篇文章(What to use for React styling?)学习对 CSS 相关技术进行选型决策

社区上最流行的, 也是笔者以为使用起来最舒服的是styled-components, styled-components 有下列特性:

  • 自动生成类名, 解决 CSS 的全局性和样式冲突. 经过组件名来标志样式, 自动生成惟一的类名, 开发者不须要为元素定义类名.
  • 绑定组件. 隔离了 CSS 的依赖问题, 让组件 JSX 更加简洁, 反过来开发者须要考虑更多组件的语义
  • 天生支持'关键 CSS'. 样式和组件绑定, 能够和组件一块儿进行代码分割和异步加载
  • 自动添加厂商前缀
  • 灵活的动态样式. 经过 props 和全局 theme 来动态控制样式
  • 提供了一些 CSS 预处理器的语法
  • 主题机制
  • 支持 react-native. 这个用起来比较爽
  • 支持 stylint, 编辑器高亮和智能提示
  • 支持服务端渲染
  • 符合分离展现组件和行为组件原则

推荐这篇文章: Stop using css-in-javascript for web development, styled-components 能够基本覆盖全部 CSS 的使用场景:


0. 基本用法

// 定义组件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),
})``;
复制代码

1. 样式扩展

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; `;
复制代码

2. mixin 机制

在 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; `;
复制代码

3. 类 SCSS 的语法

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; } `;
复制代码

5. JS 带来的动态性

媒体查询帮助方法:

// 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 插件能够在编译时转换为静态代码, 不须要运行时.


6. 绑定组件的全局样式

全局样式和组件生命周期绑定, 当组件卸载时也会删除全局样式. 全局样式一般用于覆盖一些第三方组件样式

const GlobalStyle = createGlobalStyle`
  body {
    color: ${props => (props.whiteColor ? 'white' : 'black')};
  }
`

// Test
<React.Fragment>
  <GlobalStyle whiteColor />
  <Navigation /> {/* example of other top-level stuff */}
</React.Fragment>
复制代码

7. Theme 机制及 Theme 对象的设计

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 类型


8. 提高开发体验

可使用babel-plugin-styled-componentsbabel macro来支持服务端渲染、 样式压缩和提高 debug 体验. 推荐使用 macro 形式, 无需安装和配置 babel 插件. 在 create-react-app 中已内置支持:

import styled, { createGlobalStyle } from 'styled-components/macro';

const Thing = styled.div` color: red; `;
复制代码

详见Tooling


9. 了解 styled-components 的局限性

比较能想到的局限性是性能问题:

  1. css-in-js: 须要一个 JS 运行时, 会增长 js 包体积(大约 15KB)
  2. 相比原生 CSS 会有更多节点嵌套(例如 ThemeConsumer)和计算消耗. 这个对于复杂的组件树的渲染影响尤其明显
  3. 不能抽取为 CSS 文件, 这一般不算问题

官方benchmark

下面是基于 v4.0 基准测试对比图, 在众多 CSS-in-js 方案中, styled-components 处于中上水平:

styled-components benchmark


10. 一些开发规范

  • 避免无心义的组件名. 避免相似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; } `;
    复制代码

11. 其余 CSS-in-js 方案

  • CSS-module
  • JSS
  • emotion
  • glamorous

这里值得一提的是CSS-module, 这也是社区比较流行的解决方案. 严格来讲, 这不是 CSS-in-js. 有兴趣的读者能够看这篇文章CSS Modules 详解及 React 中实践.

特性:

  • 比较轻量, 不须要 JS 运行时, 由于他在编译阶段进行计算
  • 全部样式默认都是 local, 经过导入模块方式能够导入这些生成的类名
  • 能够和 CSS proprocessor 配合
  • 采用非标准的语法, 例如:global, :local, :export, compose:

CSS module 一样也有外部样式覆盖问题, 因此须要经过其余手段对关键节点添加其余属性(如 data-name).

若是使用 css-module, 建议使用*.module.css来命名 css 文件, 和普通 CSS 区分开来.

扩展:




4️⃣ 通用的组件库不该该耦合 CSS-in-js/CSS-module 的方案

若是是做为第三方组件库形式开发, 我的以为不该该耦合各类 CSS-in-js/CSS-module. 不能强求你的组件库使用者耦合这些技术栈, 并且部分技术是须要构建工具支持的. 建议使用原生 CSS 或者将 SCSS/Less 这些预处理工具做为加强方案




5️⃣ 优先使用原生 CSS

笔者的项目大部分都是使用styled-components, 但对于部分极致要求性能的组件, 通常我会回退使用原生 CSS, 再配合 BEM 命名规范. 这种最简单方式, 可以知足大部分需求.




6️⃣ 选择合适本身团队的技术栈

每一个团队的状况和偏好不同, 选择合适本身的才是最好的. 关于 CSS 方面的技术栈搭配也很是多样:

css determination

  • 选择 CSS-in-js 方案: 优势: 这个方案解决了大部分 CSS 的缺陷, 灵活, 动态性强, 学习成本比较低, 很是适合组件化的场景. 缺点: 性能相比静态 CSS 要弱, 不过这点已经慢慢在改善. 能够考虑在部分组件使用原生 CSS
  • 选择 CSS 方案:
    • 选择原生 CSS 方案: 这种方案最简单
    • 选择 Preprocessor: 添加 CSS 预处理器, 能够加强 CSS 的可编程性: 例如模块化, 变量, 函数, mixin. 优势: 预处理器能够减小代码重复, 让 CSS 更好维护. 适合组织性要求很高的大型项目. 缺点: 就是须要学习成本, 因此这里笔者建议使用标准的 cssnext 来代替 SCSS/Less 这些方案
    • 方法论: CSS 的各类方法论旨在提升 CSS 的组织性, 提供一些架构建议, 让 CSS 更好维护.
    • postcss: 对 CSS 进行优化加强, 例如添加厂商前缀
    • css-module: 隔离 CSS, 支持暴露变量给 JS, 解决 CSS 的一些缺陷, 让 CSS 适合组件化场景. 可选, 经过合适的命名和组织实际上是能够规避 CSS 的缺陷

综上所述, CSS-in-js 和 CSS 方案各有适用场景. 好比对于组件库, 如 antd 则选择了 Preprocessor 方案; 对于通常应用笔者建议使用 CSS-in-js 方案, 它学习成本很低, 而且There's Only One Way To Do It 没有太多心智负担, 不须要学习冗杂的方法论, 代码相对比较可控; 另外它还支持跨平台, 在 ReactNative 下, styled-components 是更好的选择. 而 CSS 方案, 对于大型应用要作到有组织有纪律和规划化, 须要花费较大的精力, 尤为是团队成员能力不均状况下, 很容易失控




7️⃣ 使用 svgr 转换 svg 图标

现在 CSS-Image-Sprite 早已被 SVG-Sprite 取代. 而在 React 生态中使用svgr更加方便, 它能够将 svg 文件转换为 React 组件, 也就是一个普通的 JS 模块, 它有如下优点:

  • 转换为普通 JS 文件, 方便代码分割和异步加载
  • 相比 svg-sprite 和 iconfont 方案更容易管理
  • svg 能够经过 CSS/JS 配置, 可操做性更强; 相比 iconfont 支持多色
  • 支持 svgo 压缩

基本用法:

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




8️⃣ 结合使用 rem 和 em 等相对单位, 建立更有弹性的组件

Bootstrap v4 全面使用 rem 做为基本单位, 这使得全部组件均可以响应浏览器字体的调整:

bootstrap

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:

Edit redux hooks

扩展:




3. 规范

1️⃣ 促进创建统一的 UI 设计规范

上文已经阐述了 UI 设计规范的重要性, 有兴趣的读者能够看看这篇文章开发和设计沟通有多难? - 你只差一个设计规范. 简单总结一下:

  • 提供团队协做效率
  • 提升组件的复用率. 统一的组件规范可让组件更好管理
  • 保持产品迭代过程当中品牌一致性

2️⃣ CSS 编写规范

能够参考如下规范:

3️⃣ 使用stylint进行样式规范检查




扩展

相关文章
相关标签/搜索