zoukankan      html  css  js  c++  java
  • 《深入解析CSS》—模块化CSS

    前言:所有内容与示例源码源于基思·J·格兰特的《深入解析css》,文章用于笔记整理。文章示例源码仓库书籍源码仓库

    引入

    掌握浏览器如何渲染CSS很重要,了解如何在项目中编写和组织CSS也很重要。组织CSS代码使其更易于理解和维护。

    模块化CSS(Modular CSS)是指把页面分割成不同的组成部分,这些组成部分可以在多种上下文中重复使用,并且互相之间没有依赖关系。最终目的是:当我们修改其中一部分CSS时,不会对其他部分产生意料之外的影响。

    我们把样式表的每个组成部分称为模块(module),每个模块独立负责自己的样式,不会影响其他模块内的样式。也就是说,在CSS里引入了软件封装的原则。CSS中没有数据和传统函数的概念,但是有选择器及其命中的页面元素。为了达到封装的目的,这些会成为模块的组成部分,并且每个模块都只负责少量的DOM元素的样式。

    封装(encapsulation)——相关的函数和数据集合在一起组成对象,通常用来隐藏结构化对象内部的状态或值,从而使外部因素不能操作对象内部。

    有了封装的思想,我们就可以为页面上那些彼此分立的组件定义模块了。像导航菜单、对话框、进度条、缩略图,等等。可以通过为DOM元素设置一个独一无二的的类名来识别每个模块。同时,每个模块包含一系列子元素,构建成页面上的组件。模块内部可以嵌套其他模块,最终构成完整的页面

    基础样式

    开始写模块化样式之前,需要先配置好环境。每个样式表的开头都要写一些给整个页面使用的通用规则,这些规则通常被称为基础样式。其他的样式是构建在基础样式之上的。基础样式本身并不是模块化的,但它会为后面编写模块化样式打好基础:

    /*覆盖盒模型*/
    :root {
        box-sizing: border-box;
    }
    *,*::before,*::after {
        box-sizing: inherit;
    }
    
    /*默认字号与字体*/
    body {
        font-family: Helvetica, Arial, sans-serif;
        margin: 0;
    }
    

    其他常用的基础样式还包括链接的颜色、标题的样式、外边距等。<body>标签默认的外边距很小,你可能会考虑将它的外边距去掉。根据项目的实际情况,你也可能想为表单字段、表格和列表等添加一些样式。

    • 基础样式应该是通用的,只添加那些影响页面上大部分或者全部内容的样式
    • 选择器应只用标签类型或者偶尔用用伪类选择器
    • 基础样式提供一些默认的渲染效果,之后可以很方便地根据需要覆盖基础样式
    • 基础样式配置完成以后很少会再修改。会在基础样式之上构建模块化CSS
    • 基础样式后面的内容将主要由各种模块组成

    创建模块

    下面来创建一个短消息通知的模块。每个模块都需要一个独一无二的名称,我们把这个模块叫作“message”:

    image.png

    使用一个类名为message的div标记该模块
    <div class="message">
        Save successful
    </div>
    
    .message{
        padding:0.8em 1.2em;
        border-radius:0.2em;
        border:1px solid #265559;
        color:#265559;
        background-color:#e0f0f2
    }
    

    模块的选择器由单个类名构成

    这很重要。对比一下,如果使用一个类似于#sidebar .message的选择器,就意味着这个模块只能用在#sidebar元素内部。现在没有这些约束,模块就可以在任意上下文中重复使用

    创建模块不但可以精简代码(减少重复),还可以保证视觉一致性。这样看上去更专业,不会给人仓促堆砌的感觉。用户在潜意识里也会更容易相信我们的应用程序

    模块的变体

    保持一致性确实不错,但有时候需要特意避免一致。对于这个短消息模块,比如需要显示一条报错的消息,这时候应该使用红色而不是之前的蓝绿色。这可以通过定义修饰符来实现

    通过定义一个以模块名称开头的新类名来创建一个修饰符。例如,消息模块的error修饰符应该叫作message-error。通过包含模块名称,可以清楚地表明这个类属于消息模块。下面我们为模块创建三个修饰符:成功、警告和错误:

    /*基础消息模块*/
    .message{
        padding:0.8em 1.2em;
        border-radius:0.2em;
        border:1px solid #265559;
        color:#265559;
        background-color:#e0f0f2 
    }
    
    /*1.成功*/
    .message-success{
        color:#2f5926;
        border-color:#2f5926;
        background-color:#cfe8c9
    }
    
    /*2.警告*/
    .message-warning{
        color:#594826;
        border-color:#594826;
        background-color:#e8dec9
    }
    
    /*3.错误*/
    .message-error{
        color:#59262f;
        border-color:#59262f;
        background-color:#e8c9cf
    }
    

    修饰符的样式不需要重新定义整个模块,它覆盖要改变的部分:

    <div class="message message-error">
        密码错误
    </div>
    

    按钮模块及其变体

    下面将实现一个按钮模块,其中包含大小和颜色选项的变体。可以用不同的颜色为按钮添加视觉意义。绿色代表积极的行为,比如保存和提交表单;红色意味着警告,有利于防止用户不小心点击取消按钮:

    image.png

    /*基础按钮模块*/
    .button {
       padding: 0.5em 0.8em;
       border: 1px solid #265559;
       border-radius: 0.2em;
       background-color: transparent;
       font-size: 1rem;
    }
    
    /*1.成功的颜色变体*/
    .button--success {
       border-color: #cfe8c9;
       color: #fff;
       background-color: #2f5926;
    }
    /*2.危险的颜色变体*/
    .button--danger {
       border-color: #e8c9c9;
       color: #fff;
       background-color: #a92323;
    }
    
    /*1.小号字体变体*/
    .button--small {
       font-size: 0.8rem;
    }
    /*2.大号字体变体*/
    .button--large {
       font-size: 1.2rem;
    }
    
    <button class="button button--large">Read more</button>
    <button class="button button--success">Save</button>
    <button class="button button--danger button--small">Cancel</button>
    

    双连字符

    双连字符的写法可能看起来有点儿多余,但当我们开始创建名称很长的模块的时候,比如导航菜单或者文章摘要,好处就显现出来了。

    为这些模块添加修饰符后,类名将如nav-menu--horizontal或者pull-quote--dark。双连字符的写法很容易区分哪部分是模块名称,哪部分是修饰符。nav-menu--horizontal和nav--menu-horizontal分别代表了不同的含义。这样一来,即使项目里有很多名称相似的模块,也很容易分辨它们。

    不要使用依赖语境的选择器

    假设我们正在维护一个网站,里面有浅色调的下拉菜单。有一天老板说,网页头部的下拉菜单需要改成带白色文本的深色调。

    如果没有模块化CSS,我们可能会使用类似于.page-header.dropdown的选择器来覆盖dropdown类提供的默认颜色。

    现在要写模块化CSS,这样的选择器是严格禁用的。虽然上面写法可以解决需求,但接下来可能会带来很多问题。下面我们来分析一下:

    第一,我们必须考虑把这段代码放在哪里,是和网页头部的样式放在一起,还是跟下拉菜单的样式放在一起?如果我们添加太多类似的单一目的的规则,样式之间毫无关联,到最后样式表会变得杂乱无章。并且,如果后面需要修改样式,你还能想起来它们放在哪里吗?

    第二,这种做法提升了选择器优先级。当下次需要修改代码的时候,我们需要满足或者继续提升优先级。

    第三,后面可能需要在其他场景用到深色的下拉列表。刚才创建的这个下拉列表是限定在网页头部使用的。如果侧边栏也需要同样的下拉列表,我们就得为该规则集添加新的选择器来匹配两个场景,或者完整地复制一遍样式

    第四,重复使用这种写法会产生越来越长的选择器,将CSS跟特定的HTML结构绑定在一起。例如,如果有个#products-page.sidebar .social-media div:first-child h3这样的选择器,样式集就会和指定页面的指定位置紧紧耦合。

    这些问题是开发人员处理CSS的时候遭受挫折的根源。使用和维护的样式表越长,情况越糟。新样式需要覆盖旧样式时,选择器优先级会持续提升。到后面不知不觉地就会发现,我们写了一个选择器,其中包含两个ID和五个类名,只是为了匹配一个复选框。

    在样式表中,元素被各种彼此不相关的选择器匹配,这样很难找到它使用的样式。理解整个样式表的组织方式变得越来越困难,你搞不明白它是怎样把页面渲染成这样的。搞不懂代码就意味着bug变得常见,可能很小的改动就会弄乱大片的样式。删除旧代码也不安全,因为你不了解这段代码是干什么的,是否还在用。样式表越长,问题就愈发严重。

    模块化CSS就是要尝试解决这些问题。当模块需要有不同的外观或者表现的时候,就创建一个可以直接应用到指定元素的修饰符类。比如,写.dropdown--dark,而不是写成.page-header .dropdown。通过这种方式,模块本身,并且只能是它本身,可以决定自己的样式表现。其他模块不能进入别的模块内部去修改它。这样一来,深色下拉列表并没有绑定到深层嵌套的HTML结构上,也就可以在页面上需要的地方随意使用。

    千万不要使用基于页面位置的后代选择器来修改模块。坚决遵守这个原则,就可以有效防止样式表变成一堆难以维护的代码。

    多元素模块

    有时模块需要由多个元素组成。比如我们不可能只靠一个元素实现下拉菜单或者模态框。下面来创建一个更复杂的模块。这是一个媒体对象,这个模块由四个元素组成:div容器、容器包含的一张图片、标题、正文:

    image.png

    /*主容器*/
    .media {
      padding: 1.5em;
      background-color: #eee;
      border-radius: 0.5em;
    }
    /*清除浮动*/
    .media::after {
      content: "";
      display: block;
      clear: both;
    }
    /*图片子元素*/
    .media__image {
      float: left;
      margin-right: 1.5em;
    }
    /*正文子元素*/
    .media__body {
      overflow: auto;
      margin-top: 0;
    }
    /*正文内的标题*/
    .media__body > h4 {
      margin-top: 0;
    }
    
    <div class="media">
        <img class="media__image" src="01.jpeg">
        <div class="media__body">
            <h4>Strength</h4>
            <p>
            Strength training is an important part of
            injury prevention. Focus on your core&mdash;
            especially your abs and glutes.
            </p>
        </div>
    </div>
    

    效果如图:

    image.png


    • 创建模块的变体。比如把图片从左浮动改成右浮动

      .media--right>.media__image{
        float: right;
      }
      
      <div class="media media--right">
      	...
      </div>
      
    • 避免在模块选择器中使用通用标签名

      我们在媒体模块中使用了选择器.media__body > h4来匹配标题元素。这么做是允许的,因为<h4>标签就是用来标识一个次要标题的。同样的方式也可以用在带列表的模块上,相比为列表里的每个项目都添加menu__item类名,使用menu > li匹配菜单项简单多了

      我们应该避免使用基于通用标签类型的匹配,类似于.page-header >span的选择器太宽泛了。最初建立模块的时候,可能只是用span标签做一件事,但谁也说不准以后会不会出于其他目的再添加第二个span。后面再为span追加类名就比较麻烦了,因为我们需要在HTML标记中找到所有用到模块的地方,全部改一遍。

    把模块组合成更大的结构

    每个模块应该只做一件事情,用名称简洁明了地概括出它们的目标。

    有的模块是为了版面布局,有的是为了编写体例。当模块想要完成不只一件事的时候,我们应该考虑把它拆分成更小的模块。做一个下拉菜单来演示一下:

    image.png

    当我们需要使用并来描述模块职责的时候,思考一下是不是在描述两种甚至更多的职责。如果是,则需要为每个职责分别定义模块。这是模块封装的一个非常重要的原则,我们把它叫作单一职责原则。尽可能把多种功能分散到不同的模块中,这样每个模块就可以保持精炼、聚焦,并且容易理解。


    拆分不同模块的职责

    • 第一个模块叫作下拉,其中包含一个控制容器可见性的按钮。换句话说,这个模块负责展示和隐藏容器。

      我们也可以描述按钮的外观和代表行为的小三角来阐述模块的细节,这些细节都是从属于首要职责的,因此可以描述

    • 第二个模块叫作菜单,是放置链接的列表。把菜单模块的一个实例放入下拉模块的容器内,就可以构成完整的界面。

    用两个模块构造一个下拉菜单

    <!-- 下拉模块 -->
    <div class="dropdown">
        <!-- 下拉的触发按钮 -->
        <button class="dropdown__toggle">Main Menu</button>
        <!-- 抽屉子元素 -->
        <div class="dropdown__drawer">
            <!-- 菜单模块 -->
            <ul class="menu">
                <li><a href="/">Home</a></li>
                <li><a href="/coffees">Coffees</a></li>
                <li><a href="/brewers">Brewers</a></li>
                <li><a href="/specials">Specials</a></li>
                <li><a href="/about">About us</a></li>
            </ul>
        </div>
    </div>
        
    <script type="text/javascript">
    (function () {
        var toggle = document.querySelector('.dropdown__toggle');
        // 点击按钮触发类
        toggle.addEventListener('click', function (event) {
        event.preventDefault();
        var dropdown = event.target.parentNode;
        dropdown.classList.toggle('is-open');
        });
    }());
    </script>
    

    这里使用了双下划线标记,表示触发器和抽屉是下拉模块的子元素。点击触发器可以显示或者隐藏抽屉元素。JavaScript代码为下拉模块的主元素添加或者移除is-open类,以此来实现这个功能。

    /*下拉模块*/
    .dropdown {
      display: inline-block;
      position: relative;
    }
    .dropdown__toggle {
      padding: 0.5em 2em 0.5em 1.5em;
      border: 1px solid #ccc;
      font-size: 1rem;
      background-color: #eee;
    }
    /* 绘制三角形 */
    .dropdown__toggle::after {
      content: "";
      position: absolute;
      right: 1em;
      top: 1em;
      border: 0.3em solid;
      border-color: black transparent transparent;
    }
    /* 初始时隐藏抽屉 */
    .dropdown__drawer {
      display: none;
      position: absolute;
      left: 0;
      top: 2.1em;
      min- 100%;
      background-color: #eee;
    }
    
    .dropdown.is-open .dropdown__toggle::after {
      top: 0.7em;
      border-color: transparent transparent black;
    }
    /* 触发isopen类显示 */
    .dropdown.is-open .dropdown__drawer {
      display: block;
    }
    
    /* 菜单模块 */
    .menu {
      padding-left: 0;
      margin: 0;
      list-style-type: none;
      border: 1px solid #999;
    }
    .menu > li + li {
      border-top: 1px solid #999;
    }
    .menu > li > a {
      display: block;
      padding: 0.5em 1.5em;
      background-color: #eee;
      color: #369;
      text-decoration: none;
    }
    .menu > li > a:hover {
      background-color: #fff;
    }
    

    在模块里使用定位

    这是我们第一个使用定位的模块,其中创建了模块自己的包含块(主元素的position:relative)。绝对定位的元素(抽屉元素和::after伪元素)就是基于同一个模块内的位置来定位的。应该尽量让需要定位的元素关联到同一个模块内的其他元素。只有这样,我们把模块放在另一个有定位的容器里的时候,才不会弄乱样式。

    状态类

    is-open类在下拉模块中有特定的用途。我们在模块里使用JavaScript动态地添加或移除它。它也是状态类(state class)的一个示例,因为它代表着模块在当前状态下的表现。按照惯例,状态类一般以is-或者has-开头。这样状态类的目的就会比较明显,它们表示模块当前状态下的一些特征或者即将发生的变化。再举一些状态类的示例,比如is-expanded、is-loading或者has-error等。这些状态类具体会表现成什么样子取决于使用它们的模块。

    菜单模块

    每个<li>都是菜单模块的子元素,所以没必要为每个元素添加双下划线类,直接使用后代选择器.menu > li已经足够明确了。菜单模块是完全独立的,不依赖于下拉模块。这使得代码更简单,因为我们不需要理解在这个模块之前先搞懂另一个,也有助于更加灵活地复用模块。

    预处理器和模块化CSS

    所有的预处理器(比如Sass或者LESS)都提供了把分散的CSS文件合并成一个文件的功能。

    我们可以用多个文件和多个目录来组织样式,最后提供一个文件给浏览器。这样可以减少浏览器发起的网络请求数,开发者也可以把代码文件拆分成易于维护的大小。

    如果你正好在使用某种预处理器,强烈建议把CSS里的每个模块都放在各自对应命名的文件里,并按实际需要将这些文件组织到不同目录中。然后创建一个主样式表,引入所有的模块。这样一来,你想修改某个模块时就不必到一个冗长的样式表里面搜索了,因为很清楚去哪儿找它。

    你可以创建一个main.scss文件,里面只包含@import语句,如下所示:

    image.png

    预处理器会从base.scss中引入基础样式,并从每个模块文件引入相应的模块样式,然后输出一个包含所有样式的样式表文件。这样每个模块都单独拥有一个便于维护的文件。

    模块命名

    模块的命名应该有意义,无论使用场景是什么。同时也要避免使用简单地描述视觉效果的名称。把这个模块叫作“带图片的灰盒子”看上去比较通用一些,但是如果之后要改成浅蓝色背景呢?或者重新设计网站呢?这样的名称就不能用了

    我们应该换一种思路,思考模块代表什么含义。“媒体模块”这个名称就很恰当,它代表了一种图文混排的版式。

    模块要适用于各种不同场景,而其名称应该简单易记。当网站有很多页面的时候,我们可能会多次用到某个模块。

    目前,已经实现了消息模块、媒体模块、下拉模块和菜单模块。一些比较好的模块名称包括面板(panel)、警告(alert)、可折叠的部分(collapsible-section)、表单控制项(form-control)等。如果从一开始就对网站的整体设计有全面的了解,会有助于命名。例如,你可能觉得有两个UI元素都可以叫作板块(tile),然而它们毫不相关,这时候就应该更明确地命名它们(比如媒体板块和标题板块)。

    有些人强制使用两个词来命名每个模块,这样就可以避免模块指代不明确。为模块的变体类命名的时候,应该遵守同样的原则。例如,如果已经有按钮模块了,就不应该使用button--red和button--blue命名红色和蓝色变体子类。网站设计在将来有可能会改变,你不知道这些按钮的颜色会不会也跟着变化。应该使用一些更有意义的名称,比如button--danger和button--success。

    工具类

    有时候需要用一个类来对元素做一件简单明确的事,比如让文字居中、让元素左浮动,或者清除浮动。这样的类被称为工具类(utility class)。从某种意义上讲,工具类有点像小号的模块。工具类应该专注于某种功能,一般只声明一次。工具类是唯一应该使用important注释的地方。事实上,工具类应该优先使用它。这样的话,不管在哪里用到工具类,都可以生效。

    /*在容器内实现文字居中*/
    .text-center {
      text-align: center !important;
    }
    
    /*左浮动*/
    .float-left {
      float: left;
    }
    
    /*清除浮动*/
    .clearfix::before,
    .clearfix::after {
        content: " ";
        display: table;
    }
    .clearfix::after {
        clear: both;
    }
    
    /*隐藏某个元素*/
    .hidden {
      display: none !important;
    }
    

    CSS方法论

    目前,在模块化CSS的基础上发展建立了一些新的方法论。这些方法论并不是以任何库或者技术的形式出现的,但确实为开发者组织CSS代码提供了一些引导。

    这些实践对于CSS领域具有里程碑意义。值得花时间研究一下其中比较重大的几个。有的比较简单,只提供了一些编码指导;有的比较严格,硬性规定了样式代码的组织形式。每种方法论都有自己的术语和命名规范,但最终都是为了实现CSS模块化。

    • OOCSS——面向对象的CSS,由Nicole Sullivan创建。
    • SMACSS——可扩展的、模块化CSS架构,由JonathanSnook创建。
    • BEM——块(Block)、元素(Element)和修饰符(Modifier),由Yandex公司提出。
    • ITCSS——倒三角形CSS,由Harry Roberts创建。

    OOCSS仅是基于一些引导原则,ITCSS对类的命名和样式归类有明确的规则,SMACSS和BEM则介于两者之间。

    SMACSS增加了布局样式的部分,用来处理页面主要区域的布局(侧边栏、页脚、网格系统等)。ITCSS则进一步将类别分为七个层。

  • 相关阅读:
    3.2 直线与方程
    3.1.2 两条直线平等与垂直的判定
    GNU Make
    linux 启动后台进程
    go 占位符
    raft 协议
    restTemplate 接收list数据
    JAVA通过实体类生成数据库查询语句(驼峰命名规则)
    flink使用命令开始、停止任务
    SPringBoot 配置类继承WebMvcConfigurationSupport和实现WebMvcConfigurer的使用
  • 原文地址:https://www.cnblogs.com/sanhuamao/p/13691044.html
Copyright © 2011-2022 走看看