天天看点

说说我对项目中css架构的浅显理解

在实现业务的过程中,我们难免会发现之前由于各种原因存在的代码中正在产生大量的冗余。这时候就需要优化代码,如果有功能的迭代,就是进行重构的好时机了!

最近在搞某一块业务的重构,恰好时间不紧。组内大佬说“放手去干吧”。于是我将目光投向了自认为比较了解的CSS方面。

ITCSS

这是由csswizardry提倡的一个 CSS 设计方法论,他可以让你更好的管理、维护你的项目的 CSS。

它可以帮助你

  • 管理 CSS 代码的书写顺序
  • 通过分层来明确每层 CSS 的作用
  • 更好地使用 CSS cascade(权重)
  • 安全的使用继承

ITCSS 把 CSS 分成了以下的几层:

Layer 作用
Settings 项目使用的全局变量
Tools mixin & function
Generic 最基本的设定 normalize.css,reset
Base type selector
Objects 不经过装饰 (Cosmetic-free) 的设计模式
Components UI 组件
Trumps helper 唯一可以使用 ​

​important!​

​ 的地方

也就是常说的“七层架构”。

但在实际项目中,我们只需借鉴其思路,达到维护一套完善利于阅读、扩展、复用的css代码即可。

我是怎么做的

首先,上面说的第一层 —— Settings

  • 颜色
  • 边框
  • 字体大小
  • 阴影
  • 层级
  • 排版
/* Color
----------------------- */
$color-primary: #FF5777;
$color-white: #FFFFFF;
$color-black: #000000;

$color-text-primary: #333333;
$color-text-secondary: #666666;
$color-text-tertiary: $color-white;

$background-color-primary: #F1F1F1;
$background-color-secondary: $color-white;
$background-color-tertiary: $color-primary;

/* Border
----------------------- */
$border-width-base: 1Px;
$border-style-base: solid;
$border-base: $border-width-base $border-style-base $border-color-base;


/* z-index
-------------------------- */
$index-normal: 1;
$index-top: 1000;
$index-popper: 2000;      

注意:这里必须是抽取的全局的、多个地方会使用到的公共的样式变量。更加细节的可以放在后面层级中单独写。

但通常,主题色不是这么容易实现的。它甚至需要大量的函数计算以及js的介入 —— 目前流行​

​Ant Design​

​​采用了“三套主题色变量” 的方式(将Settings和Theme合并为一层);而​

​elementUI​

​是在 Base 层下又加了一层Theme(这也是为什么选择ITCSS方案的原因:随意扩展和缩减)。

然后是 tools 层,也是不可或缺的。这里面经常被用来放一些“工具样式”:比如当你使用了​

​scss​

​​后的一些需要全局处理的​

​mixin​

​​函数、比如水平垂直居中、比如溢出省略、清除浮动等样式类或​

​function​

​​ …

关于这一层,网上有好多人推荐​​

​_sassMagic.scss​

​库。据说挺好用的!

值得注意的是:上面两层都是 全局层面 的。一般笔者是这样安排的:(在我司的大部分项目中,这两层都属于自研脚手架中内置的)

说说我对项目中css架构的浅显理解

这里说一句题外话:其实原生 css 越想写简单(提高复用)就越会发现,如果 css 中能引用 css 就好了(非​

​@import​

​​形式) —— 这样 css 的东西就可以在 css 内部解决,完成一次复用。到 html 中只需引入一个类/属性名即可!​

​scss​

​​的​

​mixin​

​就达到了这个效果。

@mixin {
  color: #737373;
}
.line-title {
  @include large-text;
  padding: 4px 0;
}
.code-title {
  @include large-text;
  padding: 2px;
}      

(css的自定义变量也可以达到这样的效果 —— 不过你要写在​

​:root​

​中。而且 scss 中的自定义变量更加强大!)

对这一层感兴趣的可以研究​

​elementUI​

​​ 库源码,它的​

​mixin​

​写的非常之精妙。

注意⚠️:这两层的主要代码(涉及创建了 变量、​

​mixin​

​​和​

​function​

​​的)是要在​

​vue.config.js​

​中引入的 —— 这样就能在其它css和页面中使用到:

module.exports = {
  css: {
    loaderOptions: {
      scss: {
        prependData: `
          @import "@/style/settings/var.scss";
          @import "@/style/theme/scss/index.scss";
          @import "@/style/tools/_sassMagic.scss";
          `
      },
  }
}      

第三层 Generic。这一层就是专门放置一些css样式初始化等功能。你可以选择normalize.css这样成型的第三方css;也可以根据项目中用到的标签做针对性初始化处理。

这一层没什么说的。

第四层 Base 层:这一层可以用来放定制化css样式 —— 它是对基础样式的补充。比如你的网站中 ​

​a​

​​ 链接点击后是什么样、或者 ​

​li​

​ 的前面几个点是什么样的。

这两层是 组件级别 的。

一般来说,“全局级别”的样式主要负责供应“全局”、“其它低级别样式文件”以及“极少量独立样式代码”;而“组件级别”主要负责供应所有构成页面的组件中需要的样式、制定本项目样式规范以及特殊情况。

第五层 Object 和第六层 Components 其实可以合并为一层:component。它其实就是写组件。

这一层首先在结构上不再维护在和其它目录同级的目录下(如上面的style),而是放在组件存放的 components 目录下。

在这一层你要做的就是:自行/利用第三方库封装一个具有“基本架子(结构)”的组件。考虑到复用性,所以这里使用最多的就是​​

​slot​

​了。比如:

<!-- src/components/layout/footer.vue -->
<template>
  <footer class="c-footer">
    <slot></slot>
  </footer>
</template>
<script>export default {
  name: 'VFooter'
}</script>
<style lang="scss" scoped>/** 使用到tools层的mixin:底部固定,且有一个高zIndex */
@include b(c-footer) {
  position: fixed;
  bottom: 0px;
  width: 100%;
}</style>      

由于这一层的“特殊性”,再加上根据 css 中的就近原则来说他们对html的影响是最大的,也是最小的(只负责一个文件的样式,一般一个文件就是一个部分的功能)。所以推荐OOCSS(面向对象css)的进阶写法:BEM。

BEM规范

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

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

以往开发组件,我们都用“重名概率小”或者干脆起个“当时认为是独一无二的名字”来保证样式不冲突,这是不可靠的。

理想的状态下,我们开发一套组件的过程中,我们应该可以随意的为其中元素进行命名,而不必担心它是否与组件以外的样式发生冲突。

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

这是通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。

这也可以看作是一种“硬性约束”,因为一般来说,我们的组件会放置在同一目录下,那么操作系统中,同一目录下文件名必须唯一,这一点也就确保了组件之间不会冲突。

BEM的命名规矩很容易记:​

​block-name__element-name--modifier-name​

​,也就是模块名 + 元素名 + 修饰器名。

这里面还涉及到一个问题:要不要用​

​scope​

​?这个问题值得深思,比如vue中的scoped会形成一个样式隔离。如果需要样式复用还需要样式穿透的介入,非常麻烦。但是一味的遵循“开放”反而会引来“无妄之灾”。

OOCSS中,最重要的便是“结构与皮肤分离”。结构就是指“基础对象”,也就是我们说的“搭好一个架子”。

<div class="media">
  <div class="m-img"></div>
  <span class="m-content"></span>
</div>      

遇到上面的HTML,一般会先给一个“固定的样子”:

.media {
  .m-img {
  }
  .m-content {
  }
}      

这时候如果有新的样式或者颜色之类的改动。就需要另写一个类名:

<div class="media m-color">
  <div class="m-img"></div>
  <span class="m-content"></span>
</div>      
.m-color {
  color: red;
}      

但是在其它组件中,就完全不受影响:

<div class="media">
  <div class="m-img"></div>
  <p class="m-content"></p>
</div>      

OOCSS的复用就是体现在“架子的重复使用”上。这一点和“组件复用”有异曲同工之妙!也是这一层的基本思想。

ACSS规范

有时候我们还会在 component 上(样式的优先级比component低)再加一层:ACSS。

ACSS是原子类样式。通俗的讲就是“一个类只写一个样式”。这样的好处是可以达到对css的极限复用。而不好的地方就是让css失去了语义化。

可以用属性选择器解决无语义化的痛点。

在 scss 强大的函数加持下,比如你写不同透明度的​

​background​

​可以这样:

/** 背景颜色
[bgaxxx] {
  background-color: rgba(0, 0, 0, 0.xxx);
}
*/
@each $i in 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, {
  [bga#{$i * 10}] { 
    background-color: rgba(0, 0, 0, $i);
  }
}      

像我司这种电商平台,会有“今日必抢”、“限时抢购”这样的为了抓住用户眼球而区别于一般字体的艺术字存在。而这种字体并不是所有地方都要用到的,而且它只作用于字体样式。我们就可以在 ACSS 层下新建 font 文件夹实现。

最后一层 Trumps

为什么架构

css架构的目的不是“为了在页面不写css代码”,而是“为了更好的复用,更简单的维护,和更清晰的结构”!

在上面的内容中,很明显看到:

  • “层级越靠前,优先级越低、复用性越强”
  • “下一层永远继承上面(所有)层”

意犹未尽,说点其他的

在只有少量功能迭代的场景下,如果碰上周期不那么长其实是没法“推翻重来”的。这时候我们只能做一点点的优化(当然,本文说的都是结构上的,这里也不例外)。

上面的文字翻来覆去的看,其实核心也就两个字 —— 复用 。尽量减少重复代码的编写、甚至是文件的数量。而这一点在大多数项目中都可以优化:

比如提升层级:有时候刚开始写就是为了功能的实现,但是有可能这个 div 下包裹的文字和 div 同级的某个地方的文字是一样的,不管大小还是family,简直就是一个 family 的!其实可以将文字抽离出来,作为单独的一个样式类(或者属性选择器)。甚至是按照上面的ACSS层规范来,因为字体这玩意不可能只有一个页面有。花点时间嘛,哪怕花一点呢!