CSS 代码重构与优化之路

797 阅读11分钟

简短的概括:

1、开局先问大家一个问题,前端三剑客分别是谁?

绝大部分前端开发都会脱口而出:HTML 负责页面结构的搭建,CSS 负责页面的布局与美化,而 JS 负责的是赋予静态页面以动态的逻辑。从这个角度来看,这三者真的是缺一不可。但是随着业务需求的紧逼,我们往往做不到“雨露均沾”,有意无意得忽略了某个角色——CSS。

*重构注重的是提高代码的可理解性与可扩展性,对性能的影响可好可坏。

发现问题:

**项目中css代码痛点:**写CSS的同学们往往会体会到,随着项目规模的增加,项目中的CSS代码也会越来越多,如果没有及时对CSS代码进行维护,CSS代码不断会越来越多。你不知道修改这行代码会有什么影响,所以如果有修改或增加新功能时,开发人员往往不敢去删除旧的冗余的代码,而保险地增加新代码,最终的坏处就是项目中的CSS会越来越多,最终陷入无底洞。

解决问题:

**CSS代码重构:**我们写CSS代码时,不仅仅只是完成页面设计的效果,还应该让CSS代码易于管理,维护。我们对CSS代码重构主要有两个目的:提高代码性能,提高代码的可维护性。

*代码重构与优化:关键词

一、如何提高 CSS 性能

1、CSS是如何工作的?

1)CSS阻止渲染

当一个页面有CSS可用时,无论是内联还是外部样式表,浏览器都会延迟渲染,直到CSS被解析。这是因为没有CSS的页面通常是不可用的。

2)CSS可以阻止HTML的解析

尽管浏览器在完成CSS解析之前不会显示内容,但它会处理HTML的其余部分。然而脚本会阻止解析器,除非它们被标记为defer或async。一个脚本有可能操纵页面和其余代码,所以浏览器必须注意该脚本的执行时间。

屏蔽脚本的解析器:脚本如何屏蔽HTML解析。

因为脚本可以影响应用到页面的样式,如果浏览器仍在处理一些CSS,它就会等到处理完毕再运行脚本。因为在脚本运行之前不会继续解析文档,这意味着CSS不再只是阻止渲染--取决于文档中外部样式表和脚本的顺序,也可能停止HTML解析。

解析器阻塞CSS:CSS如何阻塞HTML解析。

为了避免阻塞解析,请尽快交付CSS,并以最佳顺序安排你的资源。

2、注意CSS的大小

1)压缩和最小化CSS

压缩文件可以显著提高速度,服务器和客户端交互中使用最广泛的压缩格式是Gzip。

最小化是去除空白和任何不必要的代码的过程。输出的是一个更小但完全有效的代码文件,浏览器可以解析,这将为你节省一些字节。

2)删除未使用的CSS

去除未使用的CSS通常是手工操作。主要的挑战在于它有多么复杂。我们必须在所有可能的状态下,在所有可能的设备上仔细审核整个网站(以覆盖媒体查询),并执行所有可能改变样式的JavaScript功能。UnusedCSS和PurifyCSS是流行的工具,可以帮助查明不必要的样式,但我们应该配合仔细的视觉回归测试。

在这里,使用CSS-in-JS的显著优势:每个组件内渲染的样式都是只需要CSS。在CSS-in-JS中加快CSS的秘诀是将CSS内联到页面中,或者将其提取到外部CSS文件中。将CSS发送到一个JavaScript文件中会导致它的解析和缓慢计算。

二、CSS的设计模式/架构

比较流行通用的CSS设计思想:OOCSS、SMACSS、BEMCSS。

1、OOCSS

1)OOCSS定义

OOCSS(Object Oriented CSS)是面向对象css,旨在编写高可复用、低耦合和高扩展的CSS代码。

OOCSS是以面向对象的思想去定义样式,将抽象和实现分离,抽离公共代码。从而使代码 重用性,可维护性和可扩展性更好的书写方法。

2)设计的主要规范

  1. 减少对 HTML 结构的依赖(分离结构和主题)

  2. 增加样式的复用性(分离容器和内容)

在 OOCSS 的观念中,强调重复使用 class,而应该避免使用 id 作为 CSS 的选择器。OOCSS追求元件的复用,其class命名更为抽象,一般不体现具体事物,而注重表现层的抽取。

3)项目实例:

OOCSS鼓励我们应该思考在不同元素中哪些样式是通用的,然后将这些通用的样式从模块、组件、对象等中抽离出来,使其能在任何地方能够复用,而不依赖于某个特定的容器。

.title {
  font-family: Arial, Helvetica, sans-serif;
  font-size: 2px;
  line-height: 1;
  color: #777;
  text-shadow: rgba(0, 0, 0, .3) 3px 3px 6px;
}

.header .title {
  font-size: 1.5px;
  text-shadow: rgba(0, 0, 0, .3) 2px 2px 4px;
}

4)OOCSS的优缺点

优点:

  1. css代码减少,降低工作量。

  2. 样式重复利用,代码简洁,便于维护。

  3. 代码少,加载速度快。

  4. 能轻松构造新的页面布局,或制作新的页面风格

缺点:

  1. 适用于大型网站项目(重复组件,样式多),小型项目优势不明显。

  2. 需要熟练运用,因为特定要求(强调重复使用类选择器,避免使用id选择器)如果运用不得当,反而可能会造成后续维护困难,所以最好写上注释。

2、SMACSS

1)smacss定义

smacss通过一个灵活的思维过程来检查你的设计过程和方式是否符合你的架构。

2)设计的主要规范有三点:

  • Categorizing CSS Rules(为css分类)

  • Naming Rules(命名规范)

  • Minimizing the Depth of Applicability(最小化适配深度)

详细说明:

*Categorizing CSS Rules css分类

这一点是SMACSS的核心。SMACSS认为css有5个类别,分别是:

1 Base

2 Layout

3 Module

4 State

5 Theme or Skin

*Naming Rules 命名规范

按照前面5种的划分:

Base Rules(Pass)

Layout Rules用l-或layout-这样的前缀,例如:.l-header、.l-sidebar。

Module Rules用模块本身的命名,例如图文排列的.media、.media-image。

State Rules用is-前缀,例如:.is-active、.is-hidden。

Theme Rules如果作为单独class,用theme-前缀,例如.theme-a-background、.theme-a-shadow。

*Minimizing the Depth of Applicability 最小适配深度原则,

简单的例子:

/* depth 1 */
.sidebar ul h3 { }

/* depth 2 */
.sub-title { }

两段css的区别在于html和css的耦合度(这一点上和OOCSS的分离容器和内容的原则不谋而合)。可以想到,由于上面的样式规则使用了继承选择符,因此对于html的结构实际是有一定依赖的。如果html发生重构,就有可能不再具有这些样式。对应的,下面的样式规则只有一个选择符,因此不依赖于特定html结构,只要为元素添加class,就可以获得对应样式。

当然,继承选择符是有用的,它可以减少因相同命名引发的样式冲突(常发生于多人协作开发)。但是,我们不应过度使用,在不造成样式冲突的允许范围之内,尽可能使用短的、不限定html结构的选择符。这就是SMACSS的最小化适配深度的意义。

3、BEMCSS

1、BEM简介

BEM的意思就是块(block)、元素(element)、修饰符(modifier),是由Yandex团队提出的一种前端命名方法论。这种巧妙的命名方法让你的CSS类对其他开发者来说更加透明而且更有意义。BEM命名约定更加严格,而且包含更多的信息,它们用于一个团队开发一个耗时的大项目。

2、为什么使用BEM

1)更语意化,可读性更强

通过双下划线(__), 双横杠(--) 等符号代码维护者可以轻松理解每一部分的意义,更强的可读性往往意味着更低的维护成本。

2)模块化,减少层叠带来的样式覆盖的问题

Block是完全独立的存在,其内部的element/modifier的样式都在这个block的命名空间下书写的,所以不会收到其他外部样式的影响,不存在样式覆盖的问题。

3)增强样式的重用性

就像js组合不同的组件得到更复杂的组件一样,我们也可以通过组合不同的block得到更复杂的样式,例如使用.b-btn, .b-input来组合一个简单的form样式,从而提高代码的可复用性,从另一方面讲也是降低了维护成本。

4)更容易做项目迁移

因为block样式是相对独立的,如果在其他项目有需要,我们完全可以讲某个单独的block相关的样式应用到其他项目中。

3、BEM的命名规则

命名约定的模式如下:

.block{}

.block__element{}

.block--modifier{}

说明:

block 代表了更高级别的抽象或组件。

block__element 代表.block的后代,用于形成一个完整的.block的整体。

block--modifier 代表.block的不同状态或不同版本。

之所以使用两个连字符和下划线而不是一个,是为了让你自己的块可以用单个连字符来界定,如:

.site-search{} /* 块 */

.site-search__field{} /* 元素 */

.site-search--full{} /* 修饰符 */

* 举个栗子 🌰

1)常规的css

<div class="person">
    <div class="female">
        <p class="color"></p>
    </div>
    <div class="male">
        <p class="color"></p>
    </div>
</div>

这些CSS类名真是太不精确了,并不能告诉我们足够的信息。尽管我们可以用它们来完成工作,但它们确实非常含糊不清。

2)BEM命名规范

<div class="person">
    <div class="person_female">
        <p class="person_female--color"></p>
    </div>
    <div class="person_male">
        <p class="person_male--color"></p>
    </div>
</div>

4、BEM解决的问题

css的样式应用是全局性的,没有作用域可言。考虑以下场景:

**场景一:**开发一个弹窗组件,在现有页面中测试都没问题,一段时间后,新需求新页面,该页面一打开这个弹窗组件,页面中样式都变样了,一查问题,原来是弹窗组件和该页面的样式相互覆盖了,接下来就是修改覆盖样式的选择器...

**场景二:**承接上文,由于页面和弹窗样式冲突了,所以把页面的冲突样式的选择器加上一些结构逻辑,比如子选择器、标签选择器,借此让选择器独一无二。一段时间后,新同事接手跟进需求,对样式进行修改,由于选择器是一连串的结构逻辑,看不过来,嫌麻烦,就干脆在样式文件最后用另一套选择器,加上了覆盖样式...接下来又有新的需求...最后的结果,一个元素对应多套样式,遍布整个样式文件...

BEM解决问题的思路

由于项目开发中,每个组件都是唯一无二的,其名字也是独一无二的,组件内部元素的名字都加上组件名,并用元素的名字作为选择器,自然组件内的样式就不会与组件外的样式冲突了。

BEM的命名规矩:block-name__element-name--modifier-name,也就是模块名 + 元素名 + 修饰器名。

5、element-ui源码解读 BEM 方法

首先来看一个bem命名示例:

.el-message-box{}
.el-message-box__header{}
.el-message-box__header--active{}

如果使用已经封装好的bem方法的话,那么可以写成:

@include b('message-box') {
    @include e('header') {
        @include m('active');
    }
}

接下来我们来看一下bem方法是如何实现的。

bem方法解析

element/packages/theme-chalk/src/mixins/config.scss 里面定义了如下几个变量

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

然后我们再找到 element/packages/theme-chalk/src/mixins/mixins.scss 文件,找到b,e,m方法。

/* BEM
 -------------------------- */
@mixin b($block) {
  $B: $namespace+'-'+$block !global;
  .#{$B} {
    @content;
  }
}
@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: "";
  @each $unit in $element {
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }
  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}
@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }
  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

**说明:**代码量不多,逻辑也不复杂,语法有点晦涩难懂,解释如下:

  1. !global 变量提升,将局部变量提升为全局变量,在其他函数体内也能访问到此变量。

  2. @at-root将父级选择器直接暴力的改成根选择器。

  3. #{}插值,可以通过 #{} 插值语法在选择器和属性名中使用 SassScript 变量。

总结:

BEM 最难的部分之一是明确作用域是从哪开始和到哪结束的,以及什么时候使用或不使用它。随着不断使用的经验积累,你慢慢就会知道怎么用,这些问题也不再是问题。技术无好坏,合适方最好。

4、设计模式/架构总结

  1. oocss着重可复用,把每一个dom节点当成一个对象,是css返璞归真的思想;

  2. smacss覆盖了所有的细节点;

  3. bemcss着重css的命名和语义化;

三、CSS预处理语言

1、Sass与Less介绍

Sass、Less、Stylus都是动态的样式语言,是css预处理器,css上的一种抽象层。他们是一种特殊的语法/语言而编译成css。

**Sass:**是一种动态样式语言,Sass语法属于缩排语法,比css比多出好些功能(如变量、嵌套、运算,混入(Mixin)、继承、颜色处理,函数等),更容易阅读。

Sass官网地址:www.sass.hk/

**Less:**是一种动态样式语言. 对CSS赋予了动态语言的特性,如变量、继承、运算、函数。Less 既可以在客户端上运行 (支持IE 6+, Webkit, Firefox),也可在服务端运行。

LESS的官网: less.bootcss.com/

2、Sass与Scss是什么关系?

Sass的缩排语法,对于写惯css前端的web开发者来说很不直观,也不能将css代码加入到Sass里面,因此sass语法进行了改良,Sass 3就变成了Scss(sassy css)。与原来的语法兼容,只是用{}取代了原来的缩进。

3、Sass与Less区别

1、实现方式

1)Less是基于JavaScript,是在客户端处理的。

2)Sass是基于Ruby的,是在服务器端处理的。

2、变量

1)sass 是以开头定义的变量,如:开头定义的变量,如:mainColor: #963;

2)less 是以@开头定义的变量,如 @mainColor: #963;

4、Sass与Less相同之处

Less和Sass在语法上有些共性,比如下面这些:

1)混入(Mixins)——class中的class;

2)参数混入——可以传递参数的class,就像函数一样;

3)嵌套规则——Class中嵌套class,从而减少重复的代码;

4)运算——CSS中用上数学;

5)颜色功能——可以编辑颜色;

6)名字空间(namespace)——分组样式,从而可以被调用;

7)作用域——局部修改样式;

8)JavaScript 赋值——在CSS中使用JavaScript表达式赋值。

5、Sass与Less,选择Sass

为什么选择使用Sass而不是Less?

1)Sass在市面上有一些成熟的框架,比如说Compass,而且有很多框架也在使用Sass,比如说Foundation。

2)就国外讨论的热度来说,Sass绝对优于LESS。

3)就学习教程来说,Sass的教程要优于LESS。在国内LESS集中的教程是LESS中文官网,而Sass的中文教程,慢慢在国内也较为普遍。

4)Sass也是成熟的CSS预处理器之一,而且有一个稳定,强大的团队在维护。

5)同时还有Scss对sass语法进行了改良,Sass 3就变成了Scss(sassy css)。与原来的语法兼容,只是用{}取代了原来的缩进。

6)bootstrap(Web框架)最新推出的版本4,使用的就是Sass。

6、sass小工具

为了方便演示,你可以将示例代码直接在线转译:www.sassmeister.com/

sass在线转换: www.sass.hk/css2sass/

7、css预处理器市场分析

**三大预处理器(Sass、Less、Stylus)**中,Sass 满意度最高,其他两者相对较低,其中很大一部分人转投了后处理器 PostCSS 的阵营。 Sass 社区的 LibSass 宣布已弃用,不再支持新的功能的开发,LibSass 和 node-sass 将在最大努力的基础上继续无限期维护,包括修复主要的错误和安全性问题,并保持与最新版本的 Node.js 相兼容;

osc开源社区

调查显示,使用Sass作为CSS预处理工具的受访者最多,占66%,使用Less 的次之,占13.41%,还有一小部分受访者(4.21%)使用Stylus。另外,有13.5%的受访者在编写CSS时不使用任何CSS预处理工具。这反映 出CSS后处理对前端开发人员而言是一个非常新的领域。

Facebook 重构:抛弃 Sass / Less ,迎接原子化 CSS 时代

随着 Facebook 和 Twitter 最近的产品部署,我认为一个新的趋势正在缓慢增长:Atomic CSS-in-JS。

先抛出一个令人开心的结论,新的 CSS 编写和构建方式让 Facebook 的主页减少了 80% 的 CSS 体积。

相关地址:

1)数据来自 The State of CSS 2020

2020.stateofcss.com/zh-Hans/

2)2020 前端技术发展回顾 (分析报告)

ii066.cn/qwtnbC

四、迎接原子化 CSS 时代

Facebook 重构:抛弃 Sass / Less ,迎接原子化 CSS 时代

随着 Facebook 和 Twitter 最近的产品部署,新的趋势正在缓慢增长:Atomic CSS-in-JS(原子 CSS)。

先抛出一个令人开心的结论,新的 CSS 编写和构建方式让 Facebook 的主页减少了 80% 的 CSS 体积。

使用实用工具/原子 CSS,我们可以把结构层和表示层结合起来:当我们需要改变按钮颜色时,我们直接修改 HTML,而不是 CSS!

1、原子 CSS

各种 CSS 方法,如 BEM, OOCSS…

<button class="button button--state-danger">Danger button</button>

现在,人们真的很喜欢 Tailwind CSS 和它的 实用工具优先(utility-first) 的概念。这与 Functional CSS 和 Tachyon 这个库的理念非常接近。

<button
  class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
  Button-text
</button>

用海量的实用工具类(utility classes)组成的样式表,让我们可以在网页里大显身手。

原子 CSS 就像是实用工具优先(utility-first)CSS 的一个极端版本: 所有 CSS 类都有一个唯一的 CSS 规则。原子 CSS 最初是由 Thierry Koblentz (Yahoo!)在 2013 年挑战 CSS 最佳实践时使用的。

/* 原子 CSS */
.bw-2x {
  border-width: 2px;
}
.bss {
  border-style: solid;
}
.sans {
  font-style: sans-serif;
}
.p-1x {
  padding: 10px;
}
/* 不是原子 CSS 因为这个类包含了两个规则 */
.p-1x-sans {
  padding: 10px;
  font-style: sans-serif;
}

CSS 权重也不是什么问题,因为我们使用的是最简单的类选择器。

我们现在通过 html 标签来添加样式,发现了一些有趣的事儿:

  • 我们增加新功能的时候,样式表的增长减缓了。

  • 我们可以到处移动 html 标签,并且能确保样式也同样生效。

  • 我们可以删除新特性,并且确保样式也同时被删掉了。

可以肯定的缺点是,html 有点臃肿。对于服务器渲染的 web 应用程序来说可能是个缺点,但是类名中的高冗余使得 gzip 可以压缩得很好。同时它可以很好地处理之前重复的 css 规则。

2、Tailwind css

tailwindcss 基于比组件更小、更灵活的工具类思想的 CSS 框架。这个思想简单来说就是用 class 保证灵活、便于自定义组件,而不是在组件基础上实现个性化。

Tailwind(顺风) 提供了一种基于实用工具的现代方法来构建响应站点。它有大量的实用工具类,无需编写 CSS 即可构建现代网站。它与其它框架的不同之处在于需要通过开发设置来缩小最终 CSS 的大小,因为如果使用默认值,最终将会得到一个很大的 CSS 文件。

这里有大量的 Tailwind CSS 资源

superdevresources.com/best-tailwi…

官网:tailwindcss.cn/

早期的原子 CSS 并不被认为是一种最佳实践,或者说被认为是一种很差的方案。那个时代提倡所谓的「关注点分离」,HTML 的 class 应该有自己的语义,不应该把样式或者逻辑附在上面。不过随着时代的发展,在组件化流行的今天我们其实已经并不怎么关心 HTML 的语义,语义化的功能已经被组件所取代了。

Tailwind CSS 其实就是把在现代工程化框架里把原子 CSS 做到极致的一个 CSS 框架。其实原子 CSS 很早就出现了,最经典的如 clearfix ,在很多早期的 web 项目里都会有或多或少的原子 CSS。

大家乍一看 tailwindcss 官网肯定会觉得我在 HTML 里写个样式要敲那么多类不好吧。

<figure class="md:flex bg-gray-100 rounded-xl p-8 md:p-0">
  <img class="w-32 h-32 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto" src="/sarah-dayan.jpg" alt="" width="384" height="512">
  <div class="pt-6 md:p-8 text-center md:text-left space-y-4">
    <blockquote>
      <p class="text-lg font-semibold">
        “Tailwind CSS is the only framework that I've seen scale
        on large teams. It’s easy to customize, adapts to any design,
        and the build size is tiny.”
      </p>
    </blockquote>
    <figcaption class="font-medium">
      <div class="text-cyan-600">
        Sarah Dayan
      </div>
      <div class="text-gray-500">
        Staff Engineer, Algolia
      </div>
    </figcaption>
  </div>
</figure>

其实我们是可以利用 Atom CSS 一次只干一件事的特性,将这些类随意组装成我们想要的类,这样就可以提供出来一个更上层的通用样式来复用。

突然看到尤大神在Github上的动态,fork了该项目,看来马上要火的节奏啊!

有什么独特的功能和优势,最重要的是能否给我们开发者带来显而易见的效率的提升。

1、优点:

Tailwind 能够快速将样式添加到 HTML 元素中,并提供了大量的开箱即用的设计样式。

下面这个列表展示了部分类别和对应的例子:

  • 背景 (bg-gray-200, bg-gradient-to-bl)

  • 弹性布局 (flex-1, flex-row)

  • 网格布局 (grid-cols-1, col-span-4)

  • 内边距 (p-0, p-1)

  • 尺寸 (w-1, h-1)

2、缺点:

1)对高度定制化的支持程度不足;

2)使用 Tailwind 是有成本的。花费时间和精力学习 Tailwind 的语法和类名,你会逐渐忘记其背后的语法:也即原生 CSS 的语法。

五、项目实践

一个人走的更快,一群人可以走得更远。

提升团队凝聚力,统一团队代码风格,优化团队协作效率。

确定项目的代码规范:在和多人进行项目的开发时,第一时间需要检查该项目是否有了 CSS 的代码规范。遵循项目的代码规范进行开发,是保持 CSS 整洁的基础,也是不让别人吐槽的护盾。

1、超出省略号

// ****** 超出省略号
@mixin ellipsis($line) {
  display: -webkit-box;
  -webkit-box-orient:vertical;
  overflow: hidden;
  -webkit-line-clamp: $line;
}

.tit-name {
  @include ellipsis(1);
  color: #333;
}

2、继承 @extend

h1{
  border: 4px solid #ff9aa9;
}
.speaker{
  @extend h1;
  border-width: 2px;
}

3、函数 px2rem

@function px2rem ($px) {
  $rem : 37.5px;
  @return ($px / $rem) + rem;
}

.hello {
  width: px2rem (100px);
  height: px2rem (100px);
  &.b {
    width: px2rem ( 50px);
    height: px2rem (50px);
  }
}

4、表单输入框颜色设置

// 输入框设置
input {
  font-size: 14px;
  color: #2c3e50;
}
input::-webkit-input-placeholder {
  color: #c0c4cc;
}
// 多行文本输入框设置
textarea {
  font-size: 14px;
  color: #2c3e50;
}
textarea::-webkit-input-placeholder {
  color: #c0c4cc; 
}

5、自定义滑动条样式

/* 定义整个滑动条的宽度和高度 */
.content-nav::-webkit-scrollbar {
  width: 8px;
}
/* 定义滑块的样式 */
.content-nav::-webkit-scrollbar-thumb {
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  background: #34495e;
}
/* 定义滑块里面的轨道 */
.content-nav::-webkit-scrollbar-track {
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  background: #99a3ae;
}

6、弹出框表单设置

.dialog-from {
  // input 输入框
  .el-input{
    width: 200px;
  }
  // select 下拉菜单
  .el-select{
    width: 200px;
  }
  // textarea 菜单
  .el-textarea{
    width: 200px;
  }
  // radio 菜单
  .el-radio-group{
    width: 200px;
  }
}