前端高手进阶第05讲:如何管理你的 CSS 代码?

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

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

如何组织样式文件

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

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

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

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

开源项目中的样式文件

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

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

image (9).png

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

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

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

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

image (10).png

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

image (11).png

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

image (12).png

其中包括 4 个目录:

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

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

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

image (13).png

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

image (14).png

其中包含 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/,第三方样式文件。

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

image (15).png

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

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

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

src/
|
|– abstracts/
|   |– _variables.scss    
|   |– _functions.scss    
|   |– _mixins.scss       
|   |– _placeholders.scss 
|
|– base/
|   |– _reset.scss        
|   |– _typography.scss   
|   …                     
|
|– components/
|   |– _buttons.scss      
|   |– _carousel.scss     
|   |– _cover.scss        
|   |– _dropdown.scss     
|   |- header/
|      |- header.tsx
|      |- header.sass
|   |- footer/
|      |- footer.tsx
|      |- footer.sass
|   …                     
|
|– pages/
|   |– _home.scss         
|   |– _contact.scss      
|   …                     
|

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

如何避免样式冲突

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

手动命名

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

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

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

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

image (16).png

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

工具命名

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

下面是一段 css 样式代码:

/* style.css */
.className {
  color: green;
}

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

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

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

<div class="_3zyde4l1yATCOkgn-DBWEL"></div>
<style>
._3zyde4l1yATCOkgn-DBWEL {
  color: green;
}
</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 为例进行说明,下面是示例代码,第一段是源代码:

// 源代码
const Button = styled.button`
  background: transparent;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: palevioletred;
  margin: 0.5em 1em;
  padding: 0.25em 1em;
  ${props => props.primary && css`
    background: palevioletred;
    color: white;
  `}
`;
const Container = styled.div`
  text-align: center;
`
render(
  <Container>
    <Button>Normal Button</Button>
    <Button primary>Primary Button</Button>
  </Container>
);

第二段是编译后生成的:

<!--HTML 代码-->
<div class="sc-fzXfNJ ciXJHl">
  <button class="sc-fzXfNl hvaMnE">Normal Button</button>
  <button class="sc-fzXfNl kiyAbM">Primary Button</button>
</div>
/*CSS 代码*/
.ciXJHl {
  text-align: center;
}
.hvaMnE {
  color: palevioletred;
  background: transparent;
  border-radius: 3px;
  border-width: 2px;
  border-style: solid;
  border-color: palevioletred;
  border-image: initial;
  margin: 0.5em 1em;
  padding: 0.25em 1em;
}
.kiyAbM {
  color: white;
  border-radius: 3px;
  border-width: 2px;
  border-style: solid;
  border-color: palevioletred;
  border-image: initial;
  margin: 0.5em 1em;
  padding: 0.25em 1em;
  background: palevioletred;
}

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

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

总结

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

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


精选评论

Mr.W:

在评价element的样式文件管理时说element的样式文件与组件文件分离这种方式开发的时候也不方便,经常需要跨多层目录查找和修改样式。但是,根据Sass Guidelines优化后得到的样式文件组织结构不也是与组件文件分离的吗?也会造成开发不方便的问题吧。期望能够得到回复与解答。

    讲师回复:

    是样式与组件分离,同一个组件的样式与模板逻辑都会放在一起、不会出现跨目录层级查找的情况。
你说的开发方便是指“All in js“吗?
这种编写方式比较适用于小组件,一般情况下还是推荐按照 W3C 的规范,将组件的模板、逻辑和样式都分离开来。
一方面降低代码耦合度,另一方面也方便复用。

**鑫:

我个人是使用boostrap的css库,直接才用多class并行 比如:最后一个放的是当前div的私有样式,个人认为,用这样写很快,也好维护,写div就把涉及重排的样式都写上,最后剩下的定制css就很少了 。缺点是很class很长,丑

Warn:

个人项目中样式还是偏向BEM命名多一些

**威:

公共样式应放在哪个目录?

    讲师回复:

    全局的公共样式可以放在 src/base 目录哦~

**其:

打卡!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值