《架构师》2021年9月
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

卷首语

我对项目中CSS架构的理解和使用

作者 | 孟笑晨

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

在我负责重构公司项目的过程中,越来越意识到“架构思维”的重要性,以及它对于后续维护甚至是新功能迭代的帮助!

ITCSS

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

它可以帮助你

•管理CSS代码的书写顺序

•通过分层来明确每层CSS的作用

•更好地使用CSS cascade(权重)

•安全的使用继承

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

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

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

我是怎么做的

首先,上面说的第一层——Settings是很重要的。我们可以在其中放一些公共css变量。比如负责更改主题色的变量。常见的有:

•颜色

•边框

•字体大小

•阴影

•层级

•排版

• ...

    /* 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合并为一层);而element UI是在Base层下又加了一层Theme(这也是为什么选择ITCSS方案的原因:随意扩展和缩减)。

然后是tools层,也是不可或缺的。这里面经常被用来放一些“工具样式”:比如当你使用了scss后的一些需要全局处理的mixin函数、比如水平垂直居中、比如溢出省略、清除浮动等样式类或function ...

关于这一层,网上有好多人推荐_sass Magic.scss库。据说挺好用的!

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

这里说一句题外话:其实原生css越想写简单(提高复用)就越会发现,如果css中能引用css就好了(非@import形式)——这样css的东西就可以在css内部解决,完成一次复用。到html中只需引入一个类/属性名即可!scss的mixin就达到了这个效果。

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

(css的自定义变量也可以达到这样的效果——不过你要写在:root中。而且scss中的自定义变量更加强大!)

对这一层感兴趣的可以研究element UI库源码,它的mixin写的非常之精妙。

注意:这两层的主要代码(涉及创建了变量、mixin和function的)是要在vue.config.js中引入的——这样就能在其它css和页面中使用到:

    module.exports = {
      css: {
        loader Options: {
          scss: {
          prepend Data:
          @import "@/style/settings/var.scss";
          @import "@/style/theme/scss/index.scss";
          @import "@/style/tools/_sass Magic.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:底部固定,且有一个高z Index */
    @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, 1 {
      [bga#{$i * 10}] {
        background-color: rgba(0, 0, 0, $i);
      }
    }

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

最后一层Trumps,其实就是在你的业务(组件)中进一步“描述”功能的差异性。结合之前层次样式的复用展现想要的效果。

为什么架构

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

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

•“层级越靠前,优先级越低、复用性越强”

•“下一层永远继承上面(所有)层”

意犹未尽,说点其他的

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

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

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

还比如溢出省略这些效果、可能某些公司业务场景还有css特效。这些不是妥妥的Tools层吗?

最后,我和某人聊起的时候他说会不会到后面文件(夹)太多。我觉得不会影响大局,它带来的收益是大于损耗的。而且像我司自研脚手架是确保文件按需加载的,就完全没有这个担心了,哈哈。