关于CSS的思考

本文总结了之前关于CSS代码维护的思考关于CSS选择器命名的思考关于CSS选择器嵌套的思考这三篇文章的内容,是当时刚写前端时关于编写CSS引发的一些思考。

<!--more-->

1. 关于CSS选择器命名的思考

初学CSS之时,对选择器命名不屑一顾,心想不就是拼个单词吗?实在不行谷歌翻译一下就完事儿了。现在想想,真是可笑至极。选择器命名可以说是样式重用的基础,而最近面对一个几千行CSS代码的项目,大量的时间被耗费在选择器命名上,实在是再也不能忍受了。看了不少博客,也问了数位前辈,一直没能得到满意的答案。 今天这篇博客,就草草总结一下工作两个多月以来被CSS选择器命名折磨所得到的感悟。

1.1. 样式重用

不要使用id选择器

尽管处于同一个页面上的多个元素同时具有相同的id,其样式也会正确显示(在谷歌火狐等浏览器上进行测试),也就是说浏览器实际上并不会检测id的唯一性。然而,尽管如此,那么为什么不直接使用class选择器呢?

  • 在页面上使用多个同名的id选择器,会给Js挖一个很大的坑(JS只会获取到第一个id元素);
  • 在页面上使用唯一的id选择器,由于规范限制,会导致该id选择器的样式无法被其他元素公用,毫无重用性可言。

因此,在不能确保页面上是否会出现类似样式的元素时,不要对已有样式设置id选择器!

慎重使用语义化的选择器名称

语义化选择器带来的好处就是可以直观地了解对应元素在页面中的扮演角色及对应的作用,比如.head,.banner等等,清晰明了。但是,随之而来的问题,却也是我最大的困惑: 总不能在页脚区域写上.header这样的类吧(即使他们的样式十分相似),原本的语义化确限制了样式的重用。没错,的确可以在页脚区域写.header样式,浏览器也会正确显示,JS也没有问题,但是,你真的不觉得别扭吗? 也许,你还会说,页首跟页尾的样式相同的概率没那么大吧?嗯,对的,那么想象一下,页面中有两个或者数个版式相似的区域,他们都有带下划线的标题,红色的边框,放上了数目不等的几张正方形图片,只是图片所代表的内容不同而已,这些区域放置了“旅行”“生活”“工作”balabala之类,难道要给每个区域都给上对应的".travel",".life",".work"么? 也许分组选择器可以解决这个问题,我们还会沾沾自喜,使用一个逗号就完成了样式的复用。相信我,日子长了,你肯定会厌烦为每一个区域都绞尽脑汁地想对应的语义化名称的!

不要使用上下文相关联的标签选择器

后代选择器允许我们根据元素的上下文关系来确定某个标签的样式,而无须指定class或id。首先必须承认,这是一个十分方便且强大的选择器,我们可以轻松加愉快地为目的元素加上样式,选择器命名什么的见鬼去吧。那么,选择器命名的问题就此终结了?怎么可能! 假定有这么一个选择器ul .item,这表示为ul标签下的所有item类指定对应样式,当其他的某个地方也需要同样的样式时,却被其父元素必须为ul给定死了;或者是.items li,只有同为li的元素才能公用.items下的li样式。这意味着,复用这些样式,必须指定同样的DOM结构。并且.items下的所有li,包括li下的同为li的子元素也会获取同样的样式,也许">"是一个办法,然而,这只是使用上下文相关联的选择器所带来的弊端之一。 虽然在部分情况下,样式重用的区域,他们的DOM层次确实相似。但是,当样式与结构互相纠缠在一起的时候,如果需求被更改,意味着不仅样式被更改,页面结构也会发生更改(深受折磨,唯有泪千行)。

小结

也许过分纠结样式重用是一件费力不讨好的事儿,大不了就是多写几个类罢了嘛...然而我对于此却一直耿耿于怀。随着项目经验的增加,模模糊糊感受到了模块化页面的必要性,然而却一直没有完整的概念。

1.2. 便于维护

“改不完的需求”是工作以来感受最深的一件事情,面对页面无休止的更改,到后面的css文件,被我瞎搞的面目全非,在项目开始还特别注重的样式规范,修改过程中被完全抛在脑后:为了解决样式冲突添加了各种页面样式甚至行内样式,还不得不提心吊胆万一更改到了某个重复的样式导致其他页面样式爆炸的情形;或者是为了不影响之前的代码,干脆又重新新建一个类吧,导致的结果就是样式表中多了许多重复的代码,或是DOM元素完全被移除其类名仍然存在于样式表的情况,周而复始,恶性循环。

简洁的类名

都说“一个好的选择器命名,应当清晰明了,让刚接手项目的人也能明白其用途",然而,这句话本身就很难理解,什么是清晰明了? 我所理解的清晰明了,并不是上面所谈论的具体语义化,而是该类名所代表的功能化。比如bootstrap中的.container和.container-fulid这两个名称,表达的是这个元素代表一个容器,至于是哪个具体区块下的容器,完全没有必要知道,如果使用.person-wrap这样的类,哎...

使用单一化功能的类

功能越是单一的类,越容易被复用,且越容易被修改,同样的bootstarap中,比如文本对齐.text-center这样的类,如果页面中大量存在文本居中的情况,不妨将这条属性单独为一个类,这样,如果是某个时刻需要将左对齐更改为右对齐,只需要更改对应元素的类名就可以了。 那么,什么才算是单一化功能呢,比如一个特殊的文本颜色.text-red,一个公用的竖直边距.mb20,都可以很独立为一个类并应用在对应的元素上。 但是,这样做的缺点也很明显,如果滥用,会导致一个元素上挂上N多个类,增加页面大小还是次要的问题,如果需要修改的时候,这种情形会变得十分尴尬:比如需要将所有mb20(margin-bottom:20px)的元素底外边距更改为30px,是再单独新建一个.mb30,然后替换页面上所有的.mb20;还是直接将mb20的属性值改为30px,这样挂羊头卖狗肉的装作什么事都没有发生。 初次在张鑫旭大神的博客中见到这种面向属性的命名方法,觉得十分神奇,然而遇到需要修改的时候,也变得十分蛋疼,原文的建议是:不要对任何任何网站通用的样式进行分离! 所以,转念想想,只要不滥用单一功能化的类,如下定义一个完全居中的类,大概也是可行的吧!毕竟竖直居中这样的功能类,改动的几率会小那么一丁点,即使要修改某个元素,直接去掉这个类就可以了。

.vertical-center {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
}

充分利用继承属性

比如字体的大小,颜色,文本的行高这些属性,完全只对其父元素的类进行设置,并通过继承的方式传递给子元素,这样在修改的时候也可以减轻部分工作量,如果是大段相同的文本样式,设置可以在样式重置时设定,或者是单独剥离出一个文本类。

小结

无休止的修改会带来无穷的噩梦,如果没有良好的规范和习惯,在反复地折腾下,样式表肯定会被我玩坏的。也许提前预知哪些地方将会被修改并做好准备,就可以躲过一劫,然而,提前预知不是超能力吗?

1.3. 命名方法实践

上面吐槽了半天,却一直没有提到解决的办法。搜索了大量的资料和样式库,加上最近一直在使用scss,参考网易的NEC的css命名规范,整理了一下内容:

  • 在类名前加上前缀,作为其命名空间:
    • 布局(grid)(.g-);
    • 模块(module)(.m-);
    • 元件(unit)(.u-);
    • 功能(function)(.f-);
    • 皮肤(skin)(.s-);
    • 状态(.z-)。
  • 关于上面的命名空间,也许并没有必要完全照搬,如果项目多人协作,我自己经常使用的.t-,在选择器前加上姓名缩作为前缀,这样可以有效防止选择器冲突,当然,前提是确定没有使用同一姓名前缀的同事。
  • 作用于页面布局的类,不能是后代选择器,因为后代选择器存在依赖父级元素的情况,将来被污染的可能性比较大
  • 命名应简约而不失语义,这里的语义应当是功能化语义(描述这个选择器的作用),而不是具体到该元素的实际名称,比如应当是(一个文章列表采用m-list而不是article-list)。
  • 使用数字为具有相同语义的不同类命名,比如.m-list-1,.m-list-2(PS:之前就一直使用这种命名,需要及时注释防止过一段时间后就忘记了,此外,最好不要对颗粒化的类采用该条规则,因为颗粒化的类所附带的样式规则本身就很少,采用具体的语义化更合适,比如bg-red而不是bg-color-1)
  • 如果几个类的类型相同且样式相似区别不大时,将他们的公有样式抽离成单独的基类(可作为SCSS选择器继承的父类),比如基类.btn,可拓展.btn-default...这样的形式
  • 如果你的模块或元件可能嵌套或被嵌套于其他模块或元件,那么要慎用标签选择器,必要时采用类选择器
  • 各种常见元素的命名缩写,参考的也是网易NEC的规范,比如使用hd(head),bd(body),sd(side),mn(main),ft(foot)来划分某个具体的区块,使用作为具体元素的后缀修饰等

最后,也是最重要的一点:在动手写页面之前先规划页面整体结构,搭建好布局DOM,抽离公用的结构和样式,使用合适的标签和类名,而不是边写边想,杂乱无章,这是被自己的CSS代码恶心到之后最大的收获。“磨刀不误砍柴工”用在这里,大概是非常合适的。 立帖为证,关于上面的规范,在今后的工作中,一定会谨记的,如果违反了请打死我吧(-_-||)。

2. 关于CSS选择器嵌套的思考

选择器命名对于样式表的作用至关重要;另一方面,选择器的嵌套可能也是比较重要的,选择器的嵌套决定了样式规则的权重值,浏览器的渲染效率等。当然,在《Sass与Compass实战》这本书中指出,“CSS的解析效率在页面加载速度中的影响力是比较低的”,论坛上有位网友指出,CSS 的解析时间跟 js 执行时间相比就像 PHP 代码的运行时间和数据库、I/O运行时间对比一样,不是一个数量级,因此不用担心。所以这里仅仅是关于CSS选择器嵌套的一些思考。

2.1. CSS解析顺序

很早之前就提到(是在张鑫旭大神的博客上看见的),浏览器解析CSS选择器的顺序是从右向左的,而非我们书写时的语义上的从左向右。为什么会做出这样“反人类的”规定呢?这是在stackoverflow上查到的答案。 由于浏览器是根据节点树来匹配对应的选择器,如果是按照从左往右的顺序,则会在匹配失败后回溯到上一层重新匹配下一个节点,当节点树的规模十分庞大时,如此往复会造成巨大的浪费。相反,从右往左进行匹配,最开始就会筛选到目标元素,然后再判断其父元素是否符合条件,这样会大大减少不必要节点的匹配判断。然而需要注意的是,由于是从右往左进行匹配的,因此,浏览器会判断处于最右端的所有元素,如果是一个通配选择符,那么效率可能就不会那么高了(因为这会匹配页面上的全部元素,再判断其父元素是否满足要求)。不单单是通配选择符,如果页面上存在大量的同类选择器,比如a标签,那么浏览器也会对全部的a标签进行判断。解决这个问题,可以使用类选择器代替标签选择器,(解决问题的同时带来的麻烦大概就是如何为这些标签选择一个合适的类名)。

2.2. 嵌套深度

选择器的匹配大概是一个递归的过程,从右往左,筛选出对应的元素并向上判断其父元素是否符合条件,依此递归,如果选择器的修饰过长(即嵌套层次过深),则额外的花费就会更多,因此,应尽量减少选择器的嵌套,这正是这篇文章的主题,自打用上LESS之后就一直在思考这个问题,略有一点总结。 选择器的嵌套取决于页面结构,而又决定了样式的权重值。因此,为了将正确的样式规则应用在正确的元素上,就必须指定正确的选择器,并保证该选择器样式的权重值最为优先才行。 SCSS,LESS这些工具所提供的选择器嵌套,极大地方便了选择器的书写,且能够很直观的看出页面结构。但是,如果一味按照页面结构来嵌套选择器,那么编译出来的CSS文件可能会十分恐怖,在不经意间甚至达到六七层嵌套(我曾经在body上定义了一个类作为该页面的选择器作用域,然后糊里糊涂地在里面进行嵌套,后面在线上需要直接修改CSS文件时,简直是一场灾难)。 此外,选择器嵌套极大程度上限制了样式的重用,所需要的限定越多,能够重用的部分越少,甚至没有任何重用的可能性。 总结一下,关于选择器嵌套所带来的问题:

  • 嵌套越深,浏览器需要的解析时间越长。
  • 嵌套越深,样式的重用性越低。
  • 嵌套越深,CSS文件越难维护。

实际上,后代选择器在标准中其实是一种不那么被推荐的写法。那么,我们在书写CSS时,应在保证样式正确权重值,且能够匹配到正确元素的情况下,尽可能减少选择器的嵌套,剔除不必要的父元素,精简选择器的结构,比如移除多余的标签选择器。此外,选择器嵌套不应当仅仅是书写CSS文件时应当考虑的,正如前面所说,书写正确的HTML文档结构才是决定CSS选择器嵌套的关键。

最后,由于ID选择器是页面唯一的,因此不需要有任何父选择器进行修饰(当然为了样式重用应少使用ID选择器)。

3. 关于CSS代码维护的思考

最近有个项目,已经开发了近两个月,然后客户说之前他们提供的设计图不满意,然后前端页面全部重新修改,个人中心页面维持现状并作相应调整...整个人都不好了,维护CSS代码真是一件可怕的事情,尽管是自己写的。这几天被折磨的死去活来,总结了一点经验,希望以后少给自己以及后面的哥们挖坑。

<!--more-->

社区和论坛上到处都在谈论模块化,组件化,对于这些我只是略有耳闻,未曾深入,总觉得过于高深莫测,不太适合现在的我。就我本人而言,目前得到的关于维护CSS代码的教训有三个方面:

  • 组织合理的文件目录结构
  • 正确的样式命名和代码重用
  • 写文档!写文档!写文档

3.1. 文件目录

之前的项目一直是按照最基本的“根据资源划分目录”,即在根目录下:

- css
- js
- img
- fonts
- ...

好吧,这是刚学切页面时所使用的目录划分。这种结构真的是太脆弱了!临时占位图片,精灵图混在一起;scss,项目CSS,外部CSS文件混在一起;不同页面的html文档也混在一起;JS这里就不说了。改个东西需要在一堆文件里面找半天。 总之,要避免无法维护的风险,一定要划分一个良好的目录结构。论坛上有一种按照模块分目录的做法:将一个模块(比如登陆)相关的html,css,js和其他相关的文件关联在一起,修改的时候可以很轻松的定位到相关文件,但是整个页面就比较分散,(我们后台使用TP框架,用这种结构分目录很难被后端同事接受)。 结合实际生成环境,我尝试采用了下面目录结构来管理页面和样式表,尽管可能还是不合理,但确实比之前要好的多了:

- css
    -  main // 用来存放编译后的CSS文件
    -  plugin // 用来存放外部插件的CSS依赖文件
- img
    - tmp // 临时的占位图片,比如轮播,广告,商品头像啥乱七八糟的
    - sprite // 精灵图
- scss
    - animate // 动画
    - base // 基础
    - utils // 组件
    - layout // 布局
    - page // 不同页面的布局样式
        - page1
        - page2
    - main.scss // 引入下面的各个入口文件并输出单个main.css样式表
    - _animate.scss // animate文件夹样式表的引入入口文件,下同
    - _base.scss
    - _confit.scss // 配置项目相关的变量,如基础边距,基础色等
    - _layout.scss
    - _utils.scss
    - _page.scss
- html
    - page1 // 一组功能相近的页面
    - page2 // 另一组功能相近的页面

用scss写样式表,是一件让人身心愉悦的事情。这里先谈scss在目录划分中的作用。 使用scss的@import将不同模块和功能的样式表分离,管理起来就十分方便,此外,将单个样式表的规模减小,并同意在单个入口文件中进行加载管理维护起来更加轻松,根据相应的文件名可以迅速定位到需要修改的文件(必要情况下可以添加静默注释)。在main.scss中按依赖顺序引入相关的样式表入口文件,编译输出到/css/文件夹下。

分离的样式表实际上也可以按照某些的规律归拢在一起。

基础样式

在之前的工作中,我整理了关于自己的一套基础样式,包括:

  • _reset 样式重置,选择性的重置部分标签(我一般都是表单元素和盒子模型)
  • _css3 主要定义了一些CSS3属性混合器,之后就不用书写浏览器前缀了,实际上在webstrom中安装了prefix之后是可以不用再考虑前缀的问题,看个人习惯
  • _color 定义页面相关页面的变量,以及背景色和字体色颗粒类,防止某些抽疯的需求,其他样式表中不要再出现独立的颜色
  • _font 定义字体相关的变量和属性

上面这些基础样式都保存在scss/base文件夹下,除了_color_font,其余的基础样式表改动可能都不大,所以我们可以使用优秀的样式库来实现。比如,使用Normalize.css来代替_reset

动画样式

如果某些duang的特效需要编写动画,则最好单独放在一个文件夹中,因为动画一般是比较独立的,又需要较多的代码量,还需要一些其他的兼容处理,单独在一个文件中比较容易管理,也预防日后需求变更,或者是积累代码日后重用。

布局样式

根据项目的需求和运行平台,可以大致确定项目的整体布局,是固定布局还是栅格布局,又或者是rem或flex布局。关于后面三种布局方式,我整理了单独的文件,根据需要在项目入口文件_layout灵活引入。

组件样式

实际上我对于组件啊模块啊的概念仍处于一个比较模糊的概念。我对于组件的理解是:同时出现在多个页面的某种特定元素及其子元素所组成的布局和样式。简而言之就是抽取出可复用的代码,比如tabscollapsebtn这些重复出现的HTML结构及样式。 utils文件夹下主要存放的就是各个组件的样式,每个组件都被_utils.scss文件引入,然后在main文件中引入_utils 。在切图之前,应该大致先浏览全部的设计图,明确哪些地方是可以重用的组件,心中有数才开始写代码,比一上来就从首页开始,从上到下,从左到右闷头写要好的多。 另外,随着代码量的增加,甚至可以抽取出多个项目公用的组件样式,通过在入口文件控制是否引入,另外由于单个组件样式位于独立的文件,定位和修改就十分方便了。。

页面样式

尽管每个页面的样式细节可能不太一样,但按照整体布局或者逻辑功能也可以将他们进行分组。比如一个商城:

  • 登陆注册(注册,登陆,验证...) - 功能
  • 首页 - 独立
  • 热门,新品,折扣(统一的侧边栏、主界面宽度,广告...) - 排版
  • 购物车(订单,支付...) - 功能
  • 页眉,页脚 - 排版

根据页面的分组,定义相关页面的容器名称,或是使用%定义相关的父类选择器提供继承继承。

3.2. 样式命名重用

之前花了很多时间思考选择器的命名,最后是参照NEC

  • g-布局
  • m-模块
  • u-元件

呃好吧,这个命名规范我现在仍处于摸索阶段,也遇见了一些坑,比如:布局命名.sec-hd和模块子元素命名.tab-sec .sec-hd这里就冲突了。不过,使用hd,sd,mn这些,确实能大大提高选择器命名的时间,之前一个名字真的要想很久。

至于样式重用,也有一个比较蛋疼的地方:正确的抽取出公共样式,并在页面上很多地方都用上了,用的很爽,然而某一天,需求改了,某个地方的样式需要改动一下,此时,一不小心就会改动到很多地方(这就是错误使用样式重用的一个体现),这种情况下,可能只能小心翼翼地添加类名限制,进行样式覆盖,代码越来越多,越改越难(我现在这个项目就发生了这个情况...)。

3.3. 写文档

CSS有什么文档好写的?一个月前的CSS代码,真的只有上帝知道是怎么回事了。 在拿到设计图和需求文档之后,应当先大致整理整个项目的流程,后端同事是按照页面写逻辑的,在动手写页面之前就应该由我们剔除设计图上的坑(多余的功能,难以实现的分页等),并把项目的逻辑理顺,这不仅仅是为了后端,更是防止日后页面的大改动(逻辑一改页面肯定跟着动的)。而整理项目的逻辑和流程,除了依靠最原始的需求文档,我觉得我们也应该考虑书写前端的开发文档,包括项目的功能模块分类,技术选择等。 另外,浏览设计图时,将公共样式,页面的分组,JS的模块等也记录在文档中,方便日后的维护。此外也可以在样式表中适当添加一些注释,真的,肯定有用到的时候!

3.4. 其他

此外还有两个小小的心得。

  • 前面切好的小图标没有必要就直接做成精灵图,等待项目快上线的时候再弄(使用工具快速生成)可以节省大量的重复修改时间
  • 能定义变量就定义变量,不要再其他的样式表中出现独立的诸如颜色之类的属性值,一个该死的需求会气死人的!(我会说客户把LOGO都改了然后页面全部颜色都替换吗?幸好机智地存了变量!)

4. 小结

本文主要整理了刚开始参加工作时关于CSS的一些思考,现在回头来看,虽然有的想法显得比较幼稚,却一直影响着我写CSS代码的思路和风格,尤其是在学习了BEM之后。总之,庆幸当时初学的时候反思的这些问题。

2018年五月面试发现的一些问题 BFC及其应用