前端高手进阶 - 前中兴软创主任工程师 - 拉勾教育

上一课时我们从技术细节的角度分析了 CSS 布局的相关内容。这一课时我们提升一下思考维度,从组织管理的角度探讨如何管理好项目中的 CSS 代码。

接下来我们先解决 CSS 原生语法未能很好实现的模块化和作用域的问题,然后再对代码结构进行优化,提升代码的复用率。

如何组织样式文件

尽管 CSS 提供了 import 命令支持文件引用,但由于其存在一些问题(比如影响浏览器并行下载、加载顺序错乱等)导致使用率极低。更常见的做法是通过预处理器或编译工具插件来引入样式文件,因此本课时的讨论将不局限于以 .css 为后缀的样式文件。

管理样式文件的目的就是为了让开发人员更方便地维护代码。

具体来说就是将样式文件进行分类,把相关的文件放在一起。让工程师在修改样式的时候更容易找到对应的样式文件,在创建样式文件的时候更容易找到对应的目录。

下面我们来看看热门的开源项目都是怎么来管理样式文件的。

开源项目中的样式文件

我们先来看看著名的 UI 相关的开源项目是怎么管理样式文件的。

Bootstrap 4.4 为例,下图是项目样式代码结构,可以看出项目使用的是 Sass 预处理器。

第05讲:如何管理你的 CSS 代码? - 图1

该目录包括了 5 个目录、组件样式文件和一些全局样式。再来分析下目录及内容:

  • forms/,表单组件相关样式;
  • helpers/,公共样式,包括定位、清除等;
  • mixins/,可以理解为生成最终样式的函数;
  • utilities/,媒体查询相关样式;
  • vendor/,依赖的外部第三方样式。

根目录存放了组件样式文件和目录,其他样式文件放在不同的目录中。目录中的文件分类清晰,但目录结构相对于大多数实际项目而言过于简单(只有样式文件)。

我们再来看一个更符合大多数情况的开源项目 ant-design 4.2,该项目采用 Less 预处理器,主要源码放在 /components 目录下:

第05讲:如何管理你的 CSS 代码? - 图2

从目录名称上不难猜测,各个组件代码通过文件夹区分,点击其中的 alert 文件夹查看也确实如此,组件相关的代码、测试代码、demo 示例、样式文件、描述文档都在里面。

第05讲:如何管理你的 CSS 代码? - 图3

至于全局样式和公共样式则在 /components/style 目录下:

第05讲:如何管理你的 CSS 代码? - 图4

其中包括 4 个目录:

  • color/,颜色相关的变量与函数;
  • core/,全局样式,根标签样式、字体样式等;
  • mixins/,样式生成函数;
  • themes/,主题相关的样式变量。

将组件代码及相关样式放在一起,开发的时候修改会很方便。 但在组件目录 /comnponents 下设置 style 目录存放全局和公共样式,在逻辑上就有些说不通了,这些 “样式” 文件并不是一个单独的“组件”。再看 style 目录内部结构,相对于设置单独的 color 目录来管理样式中的颜色,更推荐像 Bootstrap 一样设立专门的目录或文件来管理变量。

最后来看看依赖 Vue.js 实现的热门 UI 库 element 2.13.1 的目录结构。项目根路径下的 packages 目录按组件划分目录来存放其源码,但和 ant-design 不同的是,组件样式文件并没有和组件代码放在一起。下图是 /packages 目录下的部分内容。

第05讲:如何管理你的 CSS 代码? - 图5

element 将样式文件统一放入了 /packages/theme-chalk 目录下,目录部分内容如下图所示:

第05讲:如何管理你的 CSS 代码? - 图6

其中包含 4 个目录:

  • common/,一些全局样式和公共变量;
  • date-picker/,日期组件相关样式;
  • fonts/,字体文件;
  • mixins/,样式生成函数及相关变量。

和 antd 有同样的问题,把样式当成 “组件” 看待,组件同级目录设立了 theme-chalk 目录存放样式文件。theme-chalk 目录下的全局样式 reset.scss 与组件样式同级,这也有些欠妥。这种为了将样式打包成模块,在独立项目中直接嵌入另一个独立项目(可以简单理解为一个项目不要有多个 package.json 文件)并不推荐,更符合 Git 使用规范的做法,即是以子模块的方式引用进项目。 而且将组件样式和源码分离这种方式开发的时候也不方便,经常需要跨多层目录查找和修改样式。

样式文件管理模式

除了开源项目之外,Sass Guidelines 曾经提出过一个用来划分样式文件目录结构的 7-1 模式也很有参考意义。这种模式建议将目录结构划分为 7 个目录和 1 个文件,这 1 个文件是样式的入口文件,它会将项目所用到的所有样式都引入进来,一般命名为 main.scss。

剩下的 7 个目录及作用如下:

  • base/,模板代码,比如默认标签样式重置;
  • components/,组件相关样式;
  • layout/,布局相关,包括头部、尾部、导航栏、侧边栏等;
  • pages/,页面相关样式;
  • themes/,主题样式,即使有的项目没有多个主题,也可以进行预留;
  • abstracts/,其他样式文件生成的依赖函数及 mixin,不能直接生成 css 样式;
  • vendors/,第三方样式文件。

点击这里获取示例项目地址,截图如下图所示:

第05讲:如何管理你的 CSS 代码? - 图7

由于这个划分模式是专门针对使用 Sass 项目提出的,从样式文件名称看出还留有 jQuery 时代的影子,为了更加符合单页应用的项目结构,我们可以稍作优化。

  • main.scss 文件存在意义不大,页面样式、组件样式、布局样式都可以在页面和组件中引用,全局样式也可以在根组件中引用。而且每次添加、修改样式文件都需要在 main.scss 文件中同步,这种过度中心化的配置方式也不方便。
  • layout 目录也可以去除,因为像 footer、header 这些布局相关的样式,放入对应的组件中来引用会更好,至于不能被组件化的 “_grid” 样式存在性也不大。因为对于页面布局,既可以通过下面介绍的方法来拆分成全局样式,也可以依赖第三方 UI 库来实现。所以说这个目录可以去除。
  • themes/ 目录也可以去除,毕竟大部分前端项目是不需要设置主题的,即使有主题也可以新建一个样式文件来管理样式变量。
  • vendors/ 目录可以根据需求添加。因为将外部样式复制到项目中的情况比较少,更多的是通过 npm 来安装引入 UI 库或者通过 webpack 插件来写入对应的 cdn 地址。

所以优化后的目录结构如下所示:

  1. src/
  2. |
  3. |– abstracts/
  4. | |– _variables.scss
  5. | |– _functions.scss
  6. | |– _mixins.scss
  7. | |– _placeholders.scss
  8. |
  9. |– base/
  10. | |– _reset.scss
  11. | |– _typography.scss
  12. |
  13. |
  14. |– components/
  15. | |– _buttons.scss
  16. | |– _carousel.scss
  17. | |– _cover.scss
  18. | |– _dropdown.scss
  19. | |- header/
  20. | |- header.tsx
  21. | |- header.sass
  22. | |- footer/
  23. | |- footer.tsx
  24. | |- footer.sass
  25. |
  26. |
  27. |– pages/
  28. | |– _home.scss
  29. | |– _contact.scss
  30. |
  31. |

这只是推荐的一种目录结构,具体使用可以根据实际情况进行调整。比如我在项目的 src 目录下创建了模块目录,按照模块来拆分路由以及页面、组件,所以每个模块目录下都会有 pages/ 目录和 components/ 目录。

如何避免样式冲突

由于 CSS 的规则是全局的,任何一个样式规则,都对整个页面有效,所以如果不对选择器的命名加以管控会很容易产生冲突。

手动命名

最简单有效的命名管理方式就是制定一些命名规则,比如 OOCSSBEMAMCSS,其中推荐比较常用的 BEM。

这里简单补充一下 BEM 相关知识,熟悉 BEM 的可以直接跳过。

BEM 是 Block、Element、Modifier 三个单词的缩写,Block 代表独立的功能组件,Element 代表功能组件的一个组成部分,Modifier 对应状态信息。

下图是官方给出的示例代码:

第05讲:如何管理你的 CSS 代码? - 图8

从命名可以看到 Element 和 Modifier 是可选的,各个单词通过双横线(也可以用双下划线)连接(双横线虽然能和单词的连字符进行区分,但确实有些冗余,可以考虑直接用下划线代替)。BEM 的命名方式具有语义,很容易理解,非常适用于组件样式类。

工具命名

通过命名规范来避免冲突的方式固然是好的,但这种规范约束也不能绝对保证样式名的唯一性,而且也没有有效的校验工具来保证命名正确无冲突。所以,聪明的开发者想到了通过插件将原命名转化成不重复的随机命名,从根本上避免命名冲突。比较著名的解决方案就是 CSS Modules。

下面是一段 css 样式代码:

  1. .className {
  2. color: green;
  3. }

借助 css Modules 插件,可以将 css 以 JSON 对象的形式引用和使用。

  1. import styles from "./style.css";
  2. element.innerHTML = '<div class="' + styles.className + '">';

编译之后的代码,样式类名被转化成了随机名称:

  1. <div class="_3zyde4l1yATCOkgn-DBWEL"></div>
  2. <style>
  3. ._3zyde4l1yATCOkgn-DBWEL {
  4. color: green;
  5. }
  6. </style>

但这种命名方式带来了一个问题,那就是如果想在引用组件的同时,覆盖它的样式会变得困难,因为编译后的样式名是随机。例如,在上面的示例代码中,如果想在另一个组件中覆盖 className 样式就很困难,而在手动命名情况下则可以直接重新定义 className 样式进行覆盖。

如何高效复用样式

如果你有一些项目开发经历,一定发现了某些样式会经常被重复使用,比如:

  • display:inline-block
  • clear:both
  • position:relative
  • ……

这违背了 DRY(Don’t Repeat Yourself)原则,完全可以通过设置为全局公共样式来减少重复定义。

哪些样式规则可以设置为全局公共样式呢?

  • 首先是具有枚举值的属性,除了上面提到的,还包括 cursor:pointer、float:left 等。
  • 其次是那些特定数值的样式属性值,比如 margin: 0、left: 0、height: 100%。
  • 最后是设计规范所使用的属性,比如设计稿中规定的几种颜色。

样式按照小粒度拆分之后命名规范也很重要,合理的命名规范可以避免公共样式重复定义,开发时方便快速引用。

前面提到的语义化命名方式 BEM 显然不太适合。首先全局样式是基于样式属性和值的,是无语义的;其次对于这种复用率很高的样式应该尽量保证命名简短方便记忆,所以推荐使用更简短、更方便记忆的命名规则。比如我们团队所使用的就是 “属性名首字母 + 横线 + 属性值首字母” 的方式进行命名。

举个例子,比如对于 display:inline-block 的样式属性值,它的属性为 “display” 缩写为 “d”,值为“inline-block”,缩写为“ib”,通过短横线连接起来就可以命名成“d-ib”;同样,如果工程师想设置一个 float:left 的样式,也很容易想到使用“f-l” 的样式名。

那会不会出现重复定义呢?这个问题很好解决,按照字母序升序定义样式类就可以了。

延伸:值得关注的 CSS in JavaScript

我们都知道 Web 标准提倡结构、样式、行为分离(分别对应 HTML、CSS、JavaScript 三种语言),但 React.js 的一出现就开始颠覆了这个原则。

先是通过 JSX 将 HTML 代码嵌入进 JavaScript 组件,然后又通过 CSS in JavaScript 的方式将 CSS 代码也嵌入进 JavaScript 组件。这种 “all in JavaScript” 的方式确实有悖 Web 标准。但这种编写方式和日益盛行的组件化概念非常契合,具有 “高内聚” 的特性,所以未来标准有所改变也未尝不可能。这也正是我们需要关注 CSS in JavaScript 技术的原因。

相对于使用预处理语言编写样式,CSS in JavaScript 具有两个不那么明显的优势:

  • 可以通过随机命名解决作用域问题,但命名规则和 CSS Modules 都可以解决这个问题;
  • 样式可以使用 JavaScript 语言特性,比如函数、循环,实现元素不同的样式效果可以通过新建不同样式类,修改元素样式类来实现。

我们以 styled-compoents 为例进行说明,下面是示例代码,第一段是源代码:

  1. const Button = styled.button`
  2. background: transparent;
  3. border-radius: 3px;
  4. border: 2px solid palevioletred;
  5. color: palevioletred;
  6. margin: 0.5em 1em;
  7. padding: 0.25em 1em;
  8. ${props => props.primary && css`
  9. background: palevioletred;
  10. color: white;
  11. `}
  12. `;
  13. const Container = styled.div`
  14. text-align: center;
  15. `
  16. render(
  17. <Container>
  18. <Button>Normal Button</Button>
  19. <Button primary>Primary Button</Button>
  20. </Container>
  21. );

第二段是编译后生成的:

  1. <div class="sc-fzXfNJ ciXJHl">
  2. <button class="sc-fzXfNl hvaMnE">Normal Button</button>
  3. <button class="sc-fzXfNl kiyAbM">Primary Button</button>
  4. </div>
  5. /*CSS 代码*/
  6. .ciXJHl {
  7. text-align: center;
  8. }
  9. .hvaMnE {
  10. color: palevioletred;
  11. background: transparent;
  12. border-radius: 3px;
  13. border-width: 2px;
  14. border-style: solid;
  15. border-color: palevioletred;
  16. border-image: initial;
  17. margin: 0.5em 1em;
  18. padding: 0.25em 1em;
  19. }
  20. .kiyAbM {
  21. color: white;
  22. border-radius: 3px;
  23. border-width: 2px;
  24. border-style: solid;
  25. border-color: palevioletred;
  26. border-image: initial;
  27. margin: 0.5em 1em;
  28. padding: 0.25em 1em;
  29. background: palevioletred;
  30. }

对比以上两段代码很容易发现,在编译后的样式代码中有很多重复的样式规则。这并不友好,不仅增加了编写样式的复杂度和代码量,连编译后也增加了冗余代码。

styled-components 只是 CSS in JavaScript 的一种解决方案,其他解决方案还有很多,有兴趣的同学可以点击这里查阅 GitHub 上的资料学习,上面收录了现有的 CSS in JavaScript 解决方案。

总结

对于样式文件的管理,推荐使用 7-1 模式简化后的目录结构,包括 pages/、components/、abastracts/、base/ 4 个目录。对于样式命名,可以采用 BEM 来命名组件、面向属性的方式来命名公共样式。

最后留一道思考题:说说你在项目中是如何管理样式代码的?