模組化是現今我們隨處都可以聽到的一個名詞,什麼是模組化?為什麼我們需要模組化?這是本系列文章我們要弄明白的一個問題。我們也借這部分內容,順帶回顧一下前端的發展歷程。
說實話,模組化這個主題有點大,我一時也不知道從哪裡講起比較合適,通常來說,前端的工作內容主要涉及三個方面:html、css、js(javascript),其他的像as(actionscript,flash的指令碼語言)、jsp、smarty等等模版類的語法標記我們在此就先略去了,因為不是特別重要。那我們所說的模組化也可以分別當成這三條線去看,如html的模組化、css的模組化,以及js的模組化,這三者我們稱為(web)前端模組化,搞清楚這幾者的關係,我們接下來要了解的事情已經很清晰了。
我們單獨來講,我想先從css,因為這是我認為最容易入手且非常鮮明的一塊內容。
背景
起初的.css是長什麼樣的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/* 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庫,來存放我們的公共樣式以及公共模組即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* common.css */ body { background: #fff; color: #333; font-size: 16px; } .box ... { background: #333; color: #fff; ... } .another-box ... { ... } |
然後我們在其他的css檔案中引用這個common.css,這樣就實現了程式碼的複用,只要是想全域性共享的樣式和模組,只要在這裡新增進去就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- 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> |
很完美,
——至少目前為止是這樣沒錯。
這裡我們來探究幾種情況:
- 假設我們這個專案非常大,大概有20個頁面這麼多,那麼我們每做一個頁面就會往common裡面補充3~4個公共樣式/模組,那麼在這個專案開發完成以後,common的體積可能要比其他css的體積都大;
- 假設有幾個這樣的頁面,他們本身內容非常少,比如404頁面,可能只需要用到少量的公共樣式,但是由於考慮到維護問題,我們還是要引入common(單獨寫樣式會使得該頁面在common更新的時候無法同步得到更新),這就使得一個頁面變得很“重”;
- 由於common越寫越大,它所佔用的命名就越多,那麼我們在引入common的時候,即使我們頁面還什麼都沒有,但已經預設被佔用了很多的命名,使得我們在某個頁面的可用命名變少,而且是越來越少;
- 我們在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)三個部分組成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<!-- 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<!-- 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-
。這是命名規範的一部分,我們使用這三個字首給相應結構體命名,就是為了更好地標誌一個結構體,更好地展示它的功用,這也是我們常說的 語義化 ,同時也能實現隔離作用,起到類似名稱空間的效果。
- 框架的命名以
g-
開頭,一般與頁面同名,比如index.html,那框架就是最外層就是.g-index,about.html就是.g-about,以此類推,其他常用的內部結構有.g-hd(header)、.g-bd(body)、.g-ft(footer)、.g-sd(side)、.g-mn(main)等等; - 模組命名以
m-
開頭,一般以相對應的用途來命名,比如導航欄m-nav
、新聞m-news
、版權m-copy_right
等等,一般來說模組名是唯一的,而且模組本身應該是可移植、可複用的; - 元件命名以
u-
開頭,一般以自身含義來命名,比如u-logo
表示一個logo,u-btn
表示一個按鈕。
那麼除卻框架、模組、元件的相關命名內容之外,命名規範還有以下幾點內容:
- 命名儘量以縮寫的方式,言簡意賅地表達,比如用
bd
表達body
,用nav
表達navigator
等,使用長長的單詞顯得多餘又臃腫; - 字首與名稱之間用
-
連線,而名稱之間的若干單詞以_
連線,組合單詞除外,如side-menu
; z-
開頭表示狀態,如z-active
、z-succ
、z-disabled
等等;- 可以根據需要定製其他開頭,但是請儘量將分類控制在少數,因為太多的分類反而造成困惑和不必要的分類開銷,其實gmuz就已經可以滿足日常開發了。
重構common
有了命名規範,我們可以對common進行一次改寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* 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點)
我曾經跟一位同事針對“後續新增公共模組可能與其他頁面的私有模組命名衝突”的問題進行探討,最後我們得出兩種解決方案:
- 預設由common管理所有模組,所有模組預設為公共模組,不允許私有模組;
- 為公共模組單獨使用一種字首
cm-
來做區分,所有m-
字首的模組都是私有模組。
第一種方案會使得common體積非常大,而且會一直增大,不可取;第二種方案顯式地宣告模組屬性,以此來避免衝突,可取。
於是乎又變成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* common.css */ body { background: #fff; color: #333; font-size: 16px; } .cm-nav { ... } .cm-news { ... } .cm-copy_right { ... } |
而我們的私有模組是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* 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的範圍內來探討模組化,那麼模組的定義就可以縮窄到:一個(組)樣式相對獨立且完整的類。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* 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 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * 正確示範,所有模組相關的程式碼都掛在模組的選擇器名下 */ .m-nav { ... } .m-nav .list { ... } .m-nav .list .item { ... } /** * 錯誤示範,暴露了.m-nav和.list兩個類名,汙染了空間 */ .m-nav { ... } .list { ... } .list .item { ... } |
- 不影響周圍佈局:一般情況下,儘量不要使用一個脫離文件流的佈局(既使用了float:left/right,position:absolute/fixed的佈局),儘量不要使用外邊距(margin)。這是為了使得模組更加穩定、具備更高的可塑性;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * 正確示範,在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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * 錯誤示範,在common中定義一個模組並固定它的位置 */ /* common */ .u-logo { width: 100px; height: 100px; position: absolute; left: 20px; top: 20px; } .cm-news { width: 200px; height: 100px; margin-top: 50px; } |
- 模組儘量設計為方便複用的量級,避免大而全,求精巧;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<!-- 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> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<!-- 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的繼承也是很簡單的,一般來說是有這麼幾種方式:
- 在css中並寫兩個類,如
.cm-nav, .m-nav
,我們知道,這相當於讓兩個(組)類共享一套樣式,然後我們再單獨對.m-nav進行補充,實現繼承和定製; - 在class屬性裡並寫兩個類,如
,這樣我們只需要在頁面css中單獨對.logo類進行補充,就可以實現定製;
- 在頁面css中直接對類進行引用
,然後補充樣式,實現定製,如.cm-nav { margin-bottom: 20px; }
;
……
(還有許多黑魔法,歡迎補充)
第一種在我們這套模式裡是不可取的,因為我們的公共模組都是放在common裡,不可能每繼承一次就上去補一個類;
第二種可取,但是需要多一個近似的類名,不提倡;
第三種又簡單又靠譜。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* common.css */ body { background: #fff; color: #333; font-size: 16px; } .cm-nav { width: 100%; height: 50px; color: #fff; background: #333; } |
我們在頁面css可以這樣用:
1 2 3 4 5 6 7 8 9 10 11 12 |
/* index.css */ .g-index { background: #fff; color: #333; font-size: 16px; } .cm-nav { width: 1000px; /* 樣式覆蓋 */ margin: auto; /* 樣式增加 */ } |
狀態
我們在上面講字首的時候,提到過一個字首z-
,我們說它可以用來表示狀態。一個模組是可以有 狀態 的,當然,這裡說的不是狀態好狀態差的意思(模組還成精了~),這裡指的是有多種表現形式,我們舉例來說,一個彈窗模組m-dialog
,它應該至少具備兩種狀態:顯示和隱藏(關閉)。我們用關鍵字 active 來表示這兩種狀態,新增z-active
類表示顯示,不加表示隱藏。如下:
1 2 3 4 5 6 7 8 9 |
/* index.css */ .m-dialog { display: none; } .m-dialog.z-active { display: block; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 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預處理語言如何使模組化更加美好。