likes
comments
collection
share

CSS设计模式与架构一次性了解到位

作者站长头像
站长
· 阅读数 20

在开始之前,我想问大家一个问题,你们有吐槽过别人的代码吗?

为什么会吐槽?我猜原因大概是他们的代码冗余且重复,难以维护和重构。

作为前端开发工程师,这样的代码只会出现在JS中吗?不,也会出现在CSS中。如果开发者从来没有CSS架构意识,那么他的项目CSS代码将会一团糟。比如:class命名毫无章法,css代码这也是,那里也是,不知道它们放什么位置等等。

那么如果解决这样一个问题呢?那就要引出我们接下来要说的各种CSS设计模式与架构方案了。

OOCSS

OOCSS(Object-Oriented CSS)即采用面向对象的思想来组织和构建CSS代码。面向对象有三大特性:封装、继承、多态。但这毕竟是属于面向对象语言的特性,例如Java,CSS也有这些特性吗?

答案是CSS只应用了其中的封装继承这两个特性。如何理解这两个特性在CSS中的应用?我们来看个案例:

假设我们有一个项目需要创建不同类型的卡片组件(card),每个卡片都有相同的基本样式,但也有一些独特的变化。使用OOCSS的封装和继承特性,我们可以实现卡片组件的样式复用和可配置性。

首先,定义一个基本的卡片对象(object):

/* 卡片对象 */
.card {
  /* 基本的卡片样式 */
}

该卡片对象 .card 包含了卡片的基本样式,如背景色、边框、内边距等。这就是封装特性的体现,把基本的样式统一封装在了card类中。

接下来,我们可以创建多个带有不同特性的卡片,即面向对象概念的子类

.featured-card {
  /* 特定的卡片样式 1*/
}

.related-card {
  /* 特定的卡片样式 2*/
}

最后,我们用 .featured-card.related-card 来创建具有不同样式的卡片组件。例如:

<div class="card featured-card">
  <!-- 卡片内容 -->
</div>

<div class="card related-card">
  <!-- 卡片内容 -->
</div>

通过这种方式,我们实现了卡片样式的复用和可维护性。如果想要修改所有卡片的基本样式,只需修改卡片对象 .card;如果想要修改特定类型卡片的样式,只需修改对应的子类样式(例如 .featured-card.related-card)。

了解了什么是OOCSS?我们接下来了解下在OOCSS设计模式中,有两条重要的原则需要遵循:

  • 容器与内容分离
  • 结构与皮肤分离

容器与内容分离

这个原则指的是将容器的样式与内容的样式分开,使得容器具有通用性,可以容纳不同类型的内容。

举个例子,假设我们有一个简单的导航菜单,其中包含多个菜单项。按照容器与内容分离的原则,我们可以定义以下样式:

/* 容器样式 */
.nav {
  /* 导航菜单的基本样式 */
}

/* 菜单项样式 */
.nav-item {
  /* 菜单项的基本样式 */
}

在这个例子中,我们将导航菜单的样式定义为 .nav,并将菜单项的样式定义为 .nav-item。通过这种分离,我们可以在保持导航菜单的通用样式不变的情况下,对菜单项进行特定的样式修改,例如背景色、字体大小等。

 在实际使用中,我们可以像下面这样使用HTML来组织导航菜单:

<nav class="nav">
  <ul>
    <li class="nav-item">菜单项1</li>
    <li class="nav-item">菜单项2</li>
    <li class="nav-item">菜单项3</li>
  </ul>
</nav>

通过添加适当的类名,我们可以在每个菜单项中应用通用的样式,并在需要的时候修改特定的样式。这种分离使得导航菜单的样式与具体的菜单项内容解耦,可以灵活地进行样式修改,同时实现了代码的重用和可维护性。

结构与皮肤分离

 这个原则指的是将样式与HTML结构分离,使得样式可以独立于具体的HTML标签,从而保持HTML的结构清晰和语义化。

 举个例子,假设我们有一个带图标的按钮组件。按照结构与皮肤分离的原则,我们可以定义以下样式:

/* 结构样式 */
.button {
  /* 按钮的基本结构样式 */
}

/* 皮肤样式 */
.button-primary {
  /* 主要样式 */
}

.button-secondary {
  /* 次要样式 */
}

在这个例子中,我们将按钮的结构样式定义为 .button,并将不同皮肤样式定义为 .button-primary.button-secondary。通过这种分离,我们可以在保持按钮的基本结构不变的情况下,通过添加不同的类名来应用不同的皮肤样式。 

 在实际使用中,我们可以使用以下HTML来创建带图标的按钮:

<button class="button button-primary">
  <span class="icon">图标</span>
  按钮文本
</button>
<button class="button button-secondary">
  <span class="icon">图标</span>
  按钮文本
</button>

通过添加适当的类名,我们可以在按钮上应用不同的皮肤样式。这种分离使得按钮的结构保持清晰和语义化,同时允许我们轻松地变换不同的皮肤样式,提高了样式的复用性和可维护性。

BEM

BEM(Block Element Modifier)是一种常用的CSS命名约定,用于帮助开发者更好地组织和管理CSS代码,其本质就是进阶版的OOCSS。

BEM将页面的视图分为块(Block)、元素(Element)、修饰符(Modifier),如何理解它们之间的关系呢? 如下图所示:

CSS设计模式与架构一次性了解到位

  1. 块(Block):它代表功能独立的整体,是一系列结构、表现和行为的封装。块的类名应该使用简短而具有描述性的名字(如:.tabs、.card)
  2.  元素(Element):顾名思义,块内部的内容就是元素,它代表块中的组成部分。元素的类名由块名和元素名以双下划线(__)进行连接(例如:.card__title、.tabs__item)。 
  3.  修饰符(Modifier):如上图所示第三个元素,它拥有个性化样式,我们需要单独为它添加样式。通过添加修饰符类,我们可以轻松修改块或元素的外观、状态。修饰符的类名由块或元素名后面加上连字符(--),然后加上修饰符名称(例如:.card--highlighted、.button--disabled)。

下面列举出和上图对应的代码,来说明BEM的应用:

<div class="card">
  <p class="card__element">这是一段文本内容。</p>
  <p class="card__element">这是一段文本内容。</p>
  <p class="card__element card__element--primary">这是一段文本内容。</p>
</div>

在这个例子中,我们有一个卡片card块,它包含三个文本内容card__element元素。根据BEM的命名规则,我们使用双下划线(__)连接块名和元素名,使得类名可以清晰地表示它们之间的关系。 

 此外,我们还使用了修饰符类名card__element--primary,用于修改第三个元素的样式。通过添加修饰符类名,我们可以轻松地为特定的块或元素应用不同的样式。

对BEM感兴趣的话,可以访问BEM的官网 

SMACSS

SMACSS(Scalable and Modular Architecture for CSS)是一种CSS设计模式,旨在帮助前端开发工程师更好地组织和管理大型项目中的CSS代码。

SMACSS的核心思想是将项目样式代码划分为不同的模块,并根据其功能和作用进行分类。这种分类使得样式的结构更清晰,对于新的需求或变化的需求可以更容易地进行修改。 

下面是SMACSS的五个主要原则: 

  1.  基础(Base):基础样式主要是对浏览器默认样式的重置。
  2.  布局(Layout):布局样式定义了页面的整体结构和布局,如头部、侧边栏、页脚等。
  3.  模块(Module):模块样式定义了可重复使用的独立组件,如导航栏、卡片、按钮等。每个模块应该具有自己的封装和独立性,以便于在不同的页面上进行复用。 
  4.  状态(State):状态样式定义了元素在特定状态下的样式,如鼠标悬停、选中、禁用等。状态样式可以通过添加类名或伪类来应用,并且应该与模块样式分开。 
  5.  主题(Theme):主题样式定义了页面的整体风格和视觉差异,如颜色、字体等。主题样式可以根据具体的项目需求进行定制。

注意:SMACSS设计模式核心思想是分类。至于在实际工作中CSS代码是否完全按照这5个原则进行划分则无关紧要,比如说项目中没有主题要求,则完全可以不考虑主题层级。

ITCSS

ITCSS(Inverted Triangle CSS)设计模式的起源可以追溯到2013年,由Harry Roberts提出。Harry Roberts是一位著名的前端开发专家和顾问,他在实践中发现,许多项目在处理CSS时都存在类似的问题:样式表无序、样式冲突、复杂度高、维护困难等。

为了解决这些问题,Harry Roberts提出了ITCSS设计模式,其灵感来自于CSS的层叠特性。通过将样式表按照特定的层次结构组织起来,使得样式规则更具层次性和可维护性。那他具体是如何处理的呢?

ITCSS的核心思想是分层,目前普遍意义上把项目划分成了七层,如下:

1、Settings(设置):在这个层次中,我们定义项目中的变量、配置和全局选项等。这些设置可以是颜色、字体、间距等通用的样式变量,供其他层次使用。

2、Tools(工具):在这个层次中,我们定义一些辅助函数、混合器(mixins)或者其他工具类。例如 SCSS 中的 @mixin 、@function。

3、Generic(通用):在这个层次中,主要是重置浏览器默认样式(normalize.css),或者标准化样式(reset.css)。

4、Elements(元素):在这个层次中,主要是根据自身项目需要  对一些元素进行定制化的设置,例如重新覆盖A 标签默认样式等。

5、Objects(对象):在这个层次中,我们定义可复用的、独立的样式模块。相当于SMACSS中的Layout。

6、Components(组件):在这个层次中,我们定义特定的页面组件,和Element UI这种组件库单独为每个组件定义样式相似。

7、Trumps 层: 根据实际需要设置 important! 的地方。

但这七层也不都是必须的,而是根据实际的项目需要去划分的。下面我们以Element-plus为例,来看下在实际的项目中,ITCSS设计模式是如何运用的。

CSS设计模式与架构一次性了解到位

如图所示,展示的是Element-plus的CSS代码结构,从代码结构中,我们可以分析出:

1、Settings(设置):项目中使用了这一层,如图所示common目录下的各个文件里面都定义了变量,供项目其它文件使用。

2、Tools(工具):项目中使用了这一层,如图所示mixins目录下的各个文件里面都定义了各种工具函数,各种mixin,供项目其它文件使用。

3、Generic(通用):项目中没有定义该层级,因为这是一个组件库,不是直接面向客户的实际项目,没有必要重置浏览器样式。

4、Elements(元素):项目中使用了这一层,虽然截图看不到,但是实际上有一个reset.scss的样式文件,重置了诸如a、h1等标签的样式。

5、Objects(对象):项目中没有定义该层级,因为这是一个组件库,不是直接面向客户的实际项目,但是项目中提供了布局组件。

6、Components(组件):项目中使用了这一层,可以说就是按照一个个组件进行文件划分的。

7、Trumps 层:项目中没有定义该层级,实际项目中一般用不到该层级。

由此可以看出,ITCSS核心是提出了分层思想,而不是说项目一定要按照这些层级去划分,多一个层级(例如添加一个主题层:theme),少一个层级(例如上述案例中的Generic通用层)没有关系的,可灵活调整。

CSS Modules

在CSS设计模式中本来是不包含这个模式的,不过我个人认为它和BEM等其它模式目标一致,都是为了项目能更好的运作和维护,并且近些年用的比较多,所以我把它以及接下来要讲的CSS-in-JS都归纳在一起进行比较。

CSS Modules核心思想是保证某个组件的样式,不会影响到其他组件

如何理解呢?我们来看几个案例:

假如我现在有个组件,写了如下一行CSS代码:

.button{  position: relative;
}

我想问一下,你怎么保证这行样式不会影响到现有项目中其它组件?怎么知道引入的组件库中有没有一个名叫button的class类?项目小还好,但通常企业级大项目有多人协同参与的时候,我们无法知道其它同事会如何命名。

如果把上面组件CSS代码改成BEM设计模式的格式+命名空间呢?如下:

.el-button{
  position: relative;
}

.el-button--primary {
  cursor: pointer;
}

改成这样就一定能避免类名重复吗?基本能避免,但也不能完全避免,毕竟你命名和别人完全相同的情况下,是可以覆盖样式的。

那有没有一种办法能完全隔离类名重复呢?答案就是CSS Modules。

它的实现方式思路是给每个样式取一个独一无二的命名。以Vue3为例,我们来看下案例:

<template>
  <p :class="$style.red">This should be red</p>
</template>

<style module>
.red {
  color: red;
}
</style>

这是Vue3的组件代码,至于为什么要用$style.red这样的方式去绑定类名可以参考官方文档,最终渲染的效果如图所示:

CSS设计模式与架构一次性了解到位

我们在style里面写的类名是red,可最终渲染出来的类名是_red_1xn05_3,也就是类名经过hash算法后变成了独一无二的,既然所有的类名都是独一无二的,自然也就避免了因为命名冲突导致的代码样式覆盖问题。

CSS-in-JS

再说什么是CSS-in-JS之前,我们需要了解一个历史概念,叫关注点分离

假如我们现在有如下一段代码:

<p style="color:red;"  onclick="alert('hello')">
  Hello World
</p>

如果放在以前,这段代码是不被接受的。为什么?因为我们的意识里认为HTML代码负责语义,CSS代码负责样式,JS代码负责交互,他们应该各司其职,分开维护,也就是关注点分离。

但是React出现以后,这个原则不再适用了。因为React 是组件结构,把 HTML、CSS、JavaScript 写在一起,能进行更好更方便地开发维护。把上面这段代码改成React代码如下所示:

const style = {
  'color': 'red',
};

const clickHandler = () => alert('hello'); 

ReactDOM.render(
  <p style={style} onclick={clickHandler}>
     Hello, world!
  </p>,
  document.getElementById('app')
);

上面这段代码在一个文件里面,封装了结构、样式和逻辑,完全违背了"关注点分离"的原则。 但是每个组件内部包含了所有需要用到的代码,不依赖外部,组件之间没有耦合,很方便复用。所以随着React 的兴起,这种关注点混合的新写法逐渐成为主流。

可是上面这些和CSS-in-JS有什么关系呢?

那是因为React代码里写CSS样式代码其实是很不方便的,为了能够在项目中更方便的使用CSS样式,于是乎衍生出了诸如styled-components这样的第三方库,用来加强CSS的操作,而这些第三方库统称为CSS-in-JS。

总结

本文总共列举出了常见的设计模式,并对其设计理念进行了剖析。

OOCSS模式是借助了面向对象思想里面的封装和继承,提高代码的可重用性和可维护性。

OOCSS的进一步发展是BEM模式,这种设计模式对于组件或模块化样式的制定,命名有很高的灵活性和可扩展性。

接着是简单介绍了SMACSS模式,通过对样式分类,使得项目结构清晰、易于维护。

再来是大型项目中使用更广泛的ITCSS模式,其核心思想是分层,通过分层使得项目可维护性高。

CSS Modules和CSS-in-JS虽然不属于市面上说的设计模式,但这些概念近些年比较活跃,我们有必要了解一下。

最后其实还有一些设计模式我这里没有列举出来,诸如ACSS,因为用的不是太广泛,没有进行说明,感兴趣的朋友可以进行了解一下。