編程學習網 > WEB開發 > CSS+DIV > 從css談模塊化
2016
12-14

從css談模塊化

模塊化是現今我們隨處都可以聽到的一個名詞,什么是模塊化?為什么我們需要模塊化?這是本系列文章我們要弄明白的一個問題。我們也借這部分內容,順帶回顧一下前端的發展歷程。

 

說實話,模塊化這個主題有點大,我一時也不知道從哪里講起比較合適,通常來說,前端的工作內容主要涉及三個方面:html、css、js(javascript),其他的像as(actionscript,flash的腳本語言)、jsp、smarty等等模版類的語法標記我們在此就先略去了,因為不是特別重要。那我們所說的模塊化也可以分別當成這三條線去看,如html的模塊化、css的模塊化,以及js的模塊化,這三者我們稱為(web)前端模塊化,搞清楚這幾者的關系,我們接下來要了解的事情已經很清晰了。

我們單獨來講,我想先從css,因為這是我認為最容易入手且非常鮮明的一塊內容。

背景

起初的.css是長什么樣的?

/* index.css */

body {
  margin: 0;
  padding: 0;
  font-size: 18px;
}

.box {
  background: #333;
  color: #fff;
}

.box .list {
  margin-left: 10px;
}

.box .list .item {
  border-bottom: 1px solid #ccc;
}

.box .list .item:last-child {
  border-bottom: 0;
}

.box .list .item a {
  text-decoration: none;
  color: #fff;
}

.box .list .item span {
  color: red;
}

.box .list .item a ... {
  ...
}

是不是很熟悉?一個簡單的手打的列表模塊。

這里存在的問題是:

  • 按照這個順序寫下去,選擇器會越寫越長,造成書寫累贅;
  • 越來越長的選擇器容易使我們混淆dom的空間順序,想象如果有好幾個平級的選擇器(如.box .list .item a與.box .list .item span),我們可能一時看不出這兩者的關系,是父子還是兄弟元素?
  • 維護困難,假設我們需要重構這個box,在.box和.list之間加入一層.wrap,在.item與a和span之間加入一層.block,那簡直就是個災難,我們要謹慎地找到確切的位置,然后再找到所有匹配的、長長的選擇器,在合適的位置全部做修改;
  • 我們很難從做到復用,假設我們在另外一個頁面也需要這個box,那我們就需要把所有跟box相關的部分復制粘貼一份,而當這個box需要修改的時候,我們可能要重新找出所有用到這個box的地方,然后又是復制粘貼一份——當然,有人說這個問題是可以通過一定的方法解決的,我們后面再來討論這個問題;

……

(歡迎補充css的開發痛點)

事實上我們手打css遇到的問題可以大致歸納為以下幾點:

  • 選擇器繁瑣冗長;
  • 命名沖突;
  • 層級結構不清晰;
  • 代碼難以復用;

問題很多,怎么去解決?由于css的發展很緩慢,在工具上也一直沒有進展,導致這些問題,比如上述的第一點,幾乎無解。所以問題往往要依賴“規范”來解決。我們先來看代碼復用。

復用

要實現代碼復用很簡單,我們只需要提供一個公共css庫,來存放我們的公共樣式以及公共模塊即可:

/* common.css */

body {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.box ... {
  background: #333;
  color: #fff;
  ...
}

.another-box ... {
  ...
}

然后我們在其他的css文件中引用這個common.css,這樣就實現了代碼的復用,只要是想全局共享的樣式和模塊,只要在這里添加進去就可以了。

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
  <link rel="stylesheet" type="text/css" href="./style/common.css">
  <link rel="stylesheet" type="text/css" href="./style/index.css">
</head>
<body>
  <div class="box">
    ...
  </div>
  <div class="another-box">
    ...
  </div>
</body>
</html>

很完美,

——至少目前為止是這樣沒錯。

這里我們來探究幾種情況:

  1. 假設我們這個項目非常大,大概有20個頁面這么多,那么我們每做一個頁面就會往common里面補充3~4個公共樣式/模塊,那么在這個項目開發完成以后,common的體積可能要比其他css的體積都大;
  2. 假設有幾個這樣的頁面,他們本身內容非常少,比如404頁面,可能只需要用到少量的公共樣式,但是由于考慮到維護問題,我們還是要引入common(單獨寫樣式會使得該頁面在common更新的時候無法同步得到更新),這就使得一個頁面變得很“重”;
  3. 由于common越寫越大,它所占用的命名就越多,那么我們在引入common的時候,即使我們頁面還什么都沒有,但已經默認被占用了很多的命名,使得我們在某個頁面的可用命名變少,而且是越來越少;
  4. 我們在common中書寫公共模塊,在具體頁面的私有css里書寫私有模塊,假設現在我們需要全局添加一個公共模塊.nice-box,我們發現,這個模塊名已經在index.css中被占用了,于是我們試著把名字改成.handsome-box,卻又發現這個名字在about.css中被占用了,哦買噶的!

……

瞬間整個人都不好了,內心充滿了絕望,心想還是轉行吧,垃圾語言毀我一生。

我們分析一下,上面這些情況其實重點只有兩個:一、冗余;二、污染。冗余是難免的,為了維護犧牲一部分靈活性也是可以接受的,不過我們需要用一些方式來減少這樣的冗余,避免讓它成為負擔;而污染,卻是亟待解決的問題,這顆定時炸彈隨著項目的增大最終會變成一場無法挽回的災難!這時候就體現出了命名規范的重要性了。

這就要求我們用一套合理的規范來約束和組織我們的代碼。

規范

編程規范使得我們的項目在一定程度上是可維護的。比如針對類名污染制定了命名規范,針對選擇器指定了書寫選擇器所要遵循的規范,等等。這些規范都在一定程度上約束了css的書寫,使得項目不至于混亂。網易的NEC是其中一種比較完整的解決方案。有興趣的童鞋可以搜索了解,筆者本身受nec影響較大,所以以下內容有一定程度的雷同。

現在我們來試著制定一套css編程規范,來解決以上提到的問題。

我們規定頁面由且只由幾種基本結構體構成:框架、模塊,以及元件。其他零散的元素,除了是作為模塊的輔助類,否則不能獨立于這三者存在。

框架

框架是指構成頁面的基礎結構,它是一個頁面的筋骨。我們假設有個頁面index.html,它的整體最外圍表現為一個class為.g-index的div,然后它由頁頭(.g-hd)、主體(.g-bd)、頁腳(.g-ft)三個部分組成:

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-hd"></div>
    <div class="g-bd"></div>
    <div class="g-ft"></div>
  </div>
</body>
</html>

這樣我們就大概能描繪出一個頁面的基本輪廓了。緊接著我們來給它補充一些模塊。

模塊

模塊是頁面上數量最多,同時也是最重要的部分,它是代碼復用的主體部分,是一個個按照功能劃分的區域,如導航欄、輪播圖、登錄窗口、信息列表等等,模塊之間相互獨立,分布在頁面上,嵌在框架的各個位置上,組成一個豐富多彩的頁面。

還是以index.html為例,我們假設頁頭有個導航欄模塊(.m-nav),主體有個新聞列表模塊(.m-news),頁腳有個版權聲明模塊(.m-copy_right):

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-hd">
      <div class="m-nav">
        nav
      </div>
    </div>
    <div class="g-bd">
      <div class="m-news">
        news
      </div>
    </div>
    <div class="g-ft">
      <div class="m-copy_right">
        copy_right
      </div>
    </div>
  </div>
</body>
</html>

元件

元件是獨立的、可重復使用的,并且在某些情況下可以作為模塊的組成部分的一種細顆粒。比如一個按鈕,一個logo等等。某種意義上說,它其實可以等同于模塊,因為它們兩者的區別只是規模不同而已。模塊更強調一個功能完整的整體,而元件則更強調獨立性。

我們假設這個頁面還需要在頁頭放個logo(.u-logo),在導航欄中放置一個登錄按鈕(.u-login_btn):

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-hd">
      <img class="u-logo" alt="logo">
      <div class="m-nav">
        nav
        <a href="/logoin" class="u-login_btn">登錄</a>
      </div>
    </div>
    <div class="g-bd">
      <div class="m-news">
        news
      </div>
    </div>
    <div class="g-ft">
      <div class="m-copy_right">
        copy_right
      </div>
    </div>
  </div>
</body>
</html>

三種基本結構體介紹完,我們回來總結一下。你一定發現了一個現象,在搭建框架的時候,我給框架元素命名用g-開頭,給模塊命名使用m-,給元件命名使用u-。這是命名規范的一部分,我們使用這三個前綴給相應結構體命名,就是為了更好地標志一個結構體,更好地展示它的功用,這也是我們常說的 語義化 ,同時也能實現隔離作用,起到類似命名空間的效果。

  1. 框架的命名以g-開頭,一般與頁面同名,比如index.html,那框架就是最外層就是.g-index,about.html就是.g-about,以此類推,其他常用的內部結構有.g-hd(header)、.g-bd(body)、.g-ft(footer)、.g-sd(side)、.g-mn(main)等等;
  2. 模塊命名以m-開頭,一般以相對應的用途來命名,比如導航欄m-nav、新聞m-news、版權m-copy_right等等,一般來說模塊名是唯一的,而且模塊本身應該是可移植、可復用的;
  3. 元件命名以u-開頭,一般以自身含義來命名,比如u-logo表示一個logo,u-btn表示一個按鈕。

那么除卻框架、模塊、元件的相關命名內容之外,命名規范還有以下幾點內容:

  1. 命名盡量以縮寫的方式,言簡意賅地表達,比如用bd表達body,用nav表達navigator等,使用長長的單詞顯得多余又臃腫;
  2. 前綴與名稱之間用-連接,而名稱之間的若干單詞以_連接,組合單詞除外,如side-menu;
  3. z-開頭表示狀態,如z-active、z-succ、z-disabled等等;
  4. 可以根據需要定制其他開頭,但是請盡量將分類控制在少數,因為太多的分類反而造成困惑和不必要的分類開銷,其實gmuz就已經可以滿足日常開發了。

重構common

有了命名規范,我們可以對common進行一次改寫:

/* common.css */

body {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.m-nav { ... }

.m-news { ... }

.m-copy_right { ... }

ok,現在我們一定程度上緩解了“污染”的問題,至少按照命名規范,我們的common構成由原來籠統的一類,變成了現在gmuz四類,變得更加可管理且“沒那么容易沖突”了,但是這還遠沒有解決“污染”。

以下為了方便表述,我們把common.css稱為“common”,把對應頁面的css,比如index.html -> index.css、about.html -> about.css,稱為“頁面css”。

這里有個問題需要細致思考一下:模塊的屬性。理論上講,一個模塊應該是公有或者私有的,假設一個模塊它基本只可能在某個頁面用,或者我們不打算在其他頁面用到它,我們可以說這個模塊是這個頁面的私有模塊,比如文章頁里的文章列表模塊(m-article_list),以及組成這個模塊的列表單元元件(u-article_item),我們基本可以確定這兩者不會在其他頁面被復用到了,那么它們其實是已經默認私有的屬性,沒必要放在common里,直接放在article.css就可以了。這樣也可以人為地減少common的體積。那么問題來了,如果模塊既可以存放在common,又可以存放在頁面css,那么我們后續在common中添加公共模塊的時候,如何避免模塊名已經在頁面css中被占用的情況?(即上文對common的設計提問的第4點)

我曾經跟一位同事針對“后續添加公共模塊可能與其他頁面的私有模塊命名沖突”的問題進行探討,最后我們得出兩種解決方案:

  1. 默認由common管理所有模塊,所有模塊默認為公共模塊,不允許私有模塊;
  2. 為公共模塊單獨使用一種前綴cm-來做區分,所有m-前綴的模塊都是私有模塊。

第一種方案會使得common體積非常大,而且會一直增大,不可取;第二種方案顯式地聲明模塊屬性,以此來避免沖突,可取。

于是乎又變成了:

/* common.css */

body {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.cm-nav { ... }

.cm-news { ... }

.cm-copy_right { ... }

而我們的私有模塊是這樣的:

/* index.css */

.g-index {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.m-nav { ... }

.m-news { ... }

.m-copy_right { ... }

這樣子處理之后,我們的公共模塊和私有模塊之間的命名沖突就解決了,而且也不會出現“一個還什么都沒有頁面引用了common之后,許多的類名就被占用了”的情況,因為common絕大部分內容都是cm模塊,而頁面自己的css里只能擁有私有的m模塊。

顯然這種方案是可行的,但是我們會多一種前綴,而且還略丑。但就當時的手打css技術來說,我們沒有其他更好的解決方案,這個問題就到這里暫算了結。我們解決了問題,但是方法還不夠好,后續等我們提及css預處理語言的時候,我們會提出一些更好的解決方案。

好,接下來我們思考這樣一個問題:假設我們已經寫完了index頁面,接著寫about頁面,這時候我們發現,原本在index中的一個模塊m-news,我們將它歸為私有模塊,而現在在about中居然也需要用到這一個模塊,于是乎,我們重新回到index頁面,把m-news模塊從index.css轉移到了common.css當中,并改名為cm-news,然后回到index頁面,把與m-news相關的內容(html、js)都修改成cm-news。這還是在我們能夠意識到的情況下做的,如果頁面多了起來,我們根本沒有印象哪個頁面是不是也有這樣一個模塊,要不要把它提升為公共模塊。一個月之后,這個項目一個星期前已經搞定了,現在需要進行后續的開發,加多一個contact頁面,然后我們又發現,這頁面里用到了一個原本我們在about頁面里把它劃為私有模塊的m-loc,于是乎,我們又走了一遍提升公共模塊的流程。。。

為什么會出現這樣的問題?根本原因在于,我們無法事先規劃好所有的模塊,無法在一開始就對一個模塊的屬性清晰地劃分。這個問題也基本算是無解。矛盾在于,我們對模塊進行了私有和公有的屬性劃分,卻無法事先掌握所有的模塊屬性,只能走一步算一步,錯了就回來再改改。

解決這問題的辦法是,取消對模塊的屬性劃分,所有模塊都默認為公共模塊,可以隨時取用。但是這樣就倒退回了我們之前的那種情況,所有的模塊都是m-*,且都扎堆在common里,導致common的體積過大,所以這個問題只能到這里為止了。

模塊

如何界定一個模塊?或者說,怎么樣才能把一部分代碼劃分為一個模塊?劃分的依據是什么?這是我們接下去要探討的問題。

設計原則

我們說模塊是一個功能相對獨立且完整的結構體,其實這應該是 組件 的概念,我們這里只從css的范圍內來探討模塊化,那么模塊的定義就可以縮窄到:一個(組)樣式相對獨立且完整的類。比如:

/* copy_right */
.m-copy_right {
  color: #ccc;
  background: #666;
  font-size: 14px;
  text-align: center;
  padding: 20px 0;
  line-height: 1.8;
}

/* nav */
.m-nav {
  color: #ccc;
  background: #666;
  font-size: 14px;
}

.m-nav .u-logo { ... }

.m-nav .list { ... }

.m-nav .list .item { ... }

原則上來講,一個css模塊應該遵循以下幾點要求:

  1. 只對外暴露一個類名;
/**
 * 正確示范,所有模塊相關的代碼都掛在模塊的選擇器名下
 */
.m-nav { ... }
.m-nav .list { ... }
.m-nav .list .item { ... }

/**
 * 錯誤示范,暴露了.m-nav和.list兩個類名,污染了空間
 */
.m-nav { ... }
.list { ... }
.list .item { ... }
  1. 不影響周圍布局:一般情況下,盡量不要使用一個脫離文檔流的布局(既使用了float:left/right,position:absolute/fixed的布局),盡量不要使用外邊距(margin)。這是為了使得模塊更加穩定、具備更高的可塑性;
/**
 * 正確示范,在common中定義一個模塊,在頁面css中對模塊進行定位和偏移
 */

/* common */
.u-logo {
  width: 100px;
  height: 100px;
}

.cm-news {
  width: 200px;
  height: 100px;
}

/* index */
.u-logo {
  position: absolute;
  left: 20px;
  top: 20px;
}

.cm-news {
  margin-top: 50px;
}
/**
 * 錯誤示范,在common中定義一個模塊并固定它的位置
 */

/* common */
.u-logo {
  width: 100px;
  height: 100px;
  position: absolute;
  left: 20px;
  top: 20px;
}

.cm-news {
  width: 200px;
  height: 100px;
  margin-top: 50px;
}
  1. 模塊盡量設計為方便復用的量級,避免大而全,求精巧;
<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-bd">
      <!-- 正確的示范 -->
      <!-- 創建一個大的內容塊article_box,而不是一個大模塊 -->
      <div class="article_box">
        <div class="hd">
          最新文章
        </div>
        <div class="bd">
          <div class="list">
            <!-- 這里我們把每一個項作為可復用的私有模塊 -->
            <div class="m-list_item">
              <img class="cover" />
              <div class="info">
                <div class="title">
                  <a href="#">文章標題</a>
                </div>
                <div class="desc">文章簡介</div>
              </div>
            </div>
          </div>
        </div>
        <div class="ft">
          <!-- 這里我們直接引入了一個公共分頁模塊 -->
          <div class="cm-page">
            <a href="#" class="pg">1</a>
            <a href="#" class="pg">2</a>
            <a href="#" class="pg">3</a>
            <a href="#" class="pg">4</a>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>
<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-bd">
      <!-- 錯誤的示范 -->
      <!-- 創建一個龐大且不可復用的私有模塊m-article_box -->
      <div class="m-article_box">
        <div class="hd">
          最新文章
        </div>
        <div class="bd">
          <div class="list">
            <div class="item">
              <img class="cover" />
              <div class="info">
                <div class="title">
                  <a href="#">文章標題</a>
                </div>
                <div class="desc">文章簡介</div>
              </div>
            </div>
          </div>
        </div>
        <div class="ft">
          <div class="page">
            <a href="#" class="pg">1</a>
            <a href="#" class="pg">2</a>
            <a href="#" class="pg">3</a>
            <a href="#" class="pg">4</a>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>

值得注意的是,這里的原則第三點,并不只是出于css模塊化的考慮,事實上這更適用于 組件化 的設計思路,這在后面講組件化的時候我們會提到。

繼承

css的繼承也是很簡單的,一般來說是有這么幾種方式:

  1. 在css中并寫兩個類,如.cm-nav, .m-nav,我們知道,這相當于讓兩個(組)類共享一套樣式,然后我們再單獨對.m-nav進行補充,實現繼承和定制;
  2. 在class屬性里并寫兩個類,如<img class="u-logo logo">,這樣我們只需要在頁面css中單獨對.logo類進行補充,就可以實現定制;
  3. 在頁面css中直接對類進行引用
    ,然后補充樣式,實現定制,如.cm-nav { margin-bottom: 20px; };

……

(還有許多黑魔法,歡迎補充)

第一種在我們這套模式里是不可取的,因為我們的公共模塊都是放在common里,不可能每繼承一次就上去補一個類;
第二種可取,但是需要多一個近似的類名,不提倡;
第三種又簡單又靠譜。

/* common.css */

body {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.cm-nav {
  width: 100%;
  height: 50px;
  color: #fff;
  background: #333;
}

我們在頁面css可以這樣用:

/* index.css */

.g-index {
  background: #fff;
  color: #333;
  font-size: 16px;
}

.cm-nav {
  width: 1000px;  /* 樣式覆蓋 */
  margin: auto;  /* 樣式增加 */
}

狀態

我們在上面講前綴的時候,提到過一個前綴z-,我們說它可以用來表示狀態。一個模塊是可以有 狀態 的,當然,這里說的不是狀態好狀態差的意思(模塊還成精了~),這里指的是有多種表現形式,我們舉例來說,一個彈窗模塊m-dialog,它應該至少具備兩種狀態:顯示和隱藏(關閉)。我們用關鍵字 active 來表示這兩種狀態,添加z-active類表示顯示,不加表示隱藏。如下:

/* index.css */

.m-dialog {
    display: none;
}

.m-dialog.z-active {
    display: block;
}
<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
  <title>index</title>
</head>
<body>
  <div class="g-index">
    <div class="g-bd">
      <div class="m-dialog">
        這是一個未激活的彈窗,你看不到!
      </div>
      <div class="m-dialog z-active">
        這是一個已激活的彈窗,你看得到!
      </div>
    </div>
  </div>
</body>
</html>

彈窗一個比較有代表性的例子,另一個典型的例子是按鈕,用過bootstrap的人都知道,按鈕btn只需要相對應添加幾個狀態類,就可以有不同的配色方案,應付不同的場景需要,這其實就是我們的z-的含義。z-是很常用的,我們應該把我們的模塊設計得盡量滿足多種可預見的需求,而不是每次都在頁面去定制和覆蓋基本樣式。

總結

到目前為止,我們已經定制了一套基于規范的css模塊化生產方式,雖然并不完美,但這卻也是一套簡單的、零工具、零成本的解決方案,適用于任何項目。

可以看出,我們這里所謂的模塊化,其實是規范化的子集,通過制定了一套規范,才產生了模塊。所以css的模塊化過程其實是css規范化的過程。

事實上,由于css本身并不是一門語言,不具備語言的特性,我們只有借助其他方式和工具,才能達到模塊化的目的。而目前為止,我們還只是停留在規范的約束方式上,內容看起來比較low :),沒關系,下一節我們會開始介紹【css預處理語言的模塊化實踐】。要知道,對于早已習慣sass編程的我,也是悶著一大口氣好不容易才寫到了這里的。。。

想必你也留意到了文中多處提到“手打css”這一說法,其實這只是對傳統css編程方式的一種戲稱,說實話有哪種編程不是手打的,難不成用腳么?哈哈。但是說實話,有了css預處理,模塊化才能算得上真正意義的模塊化,模塊化的意義才凸顯出來,因為我們所有的思考與努力,機關算盡,最終的目的都只有一個——提高工作效率。

預告

css預處理語言的出現是一個十分重要的階段,它直接推進了css的發展,改變了長久以來css的編程方式。

我們來瞥一眼sass:

// index.scss

@import './common/normalize';
@import './common/common';
@import './common/mixin';

.g-index {
  @include m-nav;
  @include m-news;
  @include m-copy_right;

  .g-hd {
    .m-nav {
      width: 100px;
      margin: auto;
    }
  }

  .g-bd {
    .m-news {
      margin-top: 20px;
    }
  }

  .g-ft {
    .m-copy_right {
      width: 100px;
      margin: auto;
    }
  }
}

sass的選擇器嵌套寫法是不是亮瞎雙眼?文件直接導入是不是很方便?還有變量?還能寫函數???

下一節,【css預處理語言的模塊化實踐】我們來看css預處理語言如何使模塊化更加美好。

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取