前端工具鏈課(二)—— 模塊化工具及組件化思想
第二個問題,我想從這個最簡單的 HTML 頁面開始。
<!DOCTYPE html>n<html>n<head>n <title>Test</title>n</head>n<body>n ...n</body>n
當我們想寫一些樣式的時候,我們通常會引入一個外部的 CSS 文件,就像這樣:
<link rel="stylesheet" href="style.css">n
有時我們可能會想用一個比如說 Bootstrap 這種的 UI 框架,當然我們也是通過一個 <link> 標籤去引入:
<link rel="stylesheet" href="bootstrap.css">n
JavaScript 代碼也是類似的,我們可能會有 jQuery、Bootstrap、一些插件以及一些業務代碼。
<script src="jquery.js"></script>n<script src="bootstrap.js"></script>n<script src="plugin-A.js"></script>n<script src="plugin-B.js"></script>n<script src="app.js"></script>n
當我們在多個 JavaScript 文件之間進行通訊時,我們可能會把一個變數掛到 window 上,變成一個全局的變數。當項目變得越來越複雜,這些全局變數也會變得越來越多,在我剛入職計蒜客的時候,我甚至看到一個頁面的全局變數多達 40 多個。當我嘗試去維護的時候,我發現這些文件的耦合度非常高。並且由於全局變數非常多,很容易就出現 命名衝突 的情況。
而且,在很多時候,我們多個 JS 文件之間是有依賴關係的,比如說我們圖中的 plugin-B。js 如果依賴了 plugin-A.js,當別人想使用 plugin-B.js 但是沒有引入 plugin-A.js 的時候,那麼 plugin-B.js 就不能正常運行了。所以我們遇到了一個比較繁瑣的 文件依賴 問題。
所以,我們第二個問題就是:
如何解決命名衝突和依賴混亂問題?
首先,之所以我們會有命名衝突問題,是因為那些 .js 文件都是共享作用域的,而且它們還定義了一些全局的變數。我們要做的,就是要 限制作用域,並且 移除全局變數。還有依賴混亂問題,我們可以通過規定一些特殊的語法,來在代碼中聲明依賴關係,再開發一個工具來自動化處理文件之間的依賴,就可以解決依賴混亂的問題了。
模塊化
這種做法我們稱為 模塊化。
我們把每一個 .js 文件都視為一個 模塊,模塊內部有自己的作用域,不會影響到全局。並且,我們 約定一些關鍵詞來進行依賴聲明和 API 暴露。而這些約定的關鍵詞就是通過制定一些 規範 去進行規範的。
模塊化規範
比較有名模塊化規範的是 CMD、AMD、CommonJS 和 ES6 Module,它們都是為了實現在瀏覽器端模塊化開發的目的。前面兩個規範分別來自 SeaJS 及 RequireJS,這兩個規範現在基本已經很少人用了;CommonJS 由於是被 NodeJS 所採用的,所以很多人用;而 ES6 Module 自然是來自去年正式發布的 ECMAScript 2015 所採用的了,以後會逐漸成為最主要的模塊化規範。
因為我們這個系列文章中使用的工具都是基於 NodeJS 寫的,而且後面的工具還會用到,所以我們就介紹一下 CommonJS 的語法吧。
比如我們想在一個文件名為 foo.js 的模塊中把 { bar: 123 } 這個對象暴露出去,讓別人能使用,我們可以用 module.exports 這個關鍵詞:
module.exports = {n bar: 123n}n
比如我們某個模塊依賴了 foo.js 這個模塊,那麼我們可以使用 require 這個關鍵詞來聲明我們對 foo.js 的依賴:
require(foo.js) // 返回 { bar: 123 }n
CommonJS 規範的語法就這麼簡單。
了解完規範之後呢,我們還需要一個工具來自動處理它們的這些依賴:
Webpack
Webpack 可以來幫我們解決這個問題。
它的安裝方法很簡單,用我們上篇文章中學習到的 NPM 就可以了:執行 npm install -g webpack,這樣就可以把 webpack 安裝到全局下了。
我們往第一個我們文件名為 foo.js 的模塊里填入上面的那段代碼:
module.exports = { bar: 123 }n
然後我們再來寫第二個文件去引用它,比如我們叫它 entry.js,我們在裡面填入:
var foo = require(./foo.js)nconsole.log(foo.bar)n
接下來,我們就可以開始用 Webpack 了,我們打開 Terminal,進入到當前目錄,然後執行:
webpack entry.js --output-filename build/output.jsn
這條命令的意思是指定 entry.js 為入口文件,最終打包後的文件路徑為 build/output.js 。
沒有意外的話,我們就會看到這樣的輸出:
Hash: 08ed99b71325392159ffnVersion: webpack 1.13.0nTime: 68msn Asset Size Chunks Chunk Namesn./build/output.js 1.69 kB 0 [emitted] mainn [0] multi main 40 bytes {0} [built]n [1] ./entry.js 47 bytes {0} [built]n [2] ./foo.js 32 bytes {0} [built]n
這表示我們打包成功了,並且依照我們的配置生成路徑為 ./build/output.js(有興趣的同學可以打開我們打開剛剛生成的 output.js 看看,結構並不複雜),而這個output.js` 是可以直接在瀏覽器里使用的。
使用 Webpack 配置文件
剛剛那種方法是我們直接在命令行里選擇入口文件和輸出文件,但大家有沒有覺得每次都這麼敲命令太麻煩了?顯然的,除了這種方法之外,我們還可以通過配置文件來實現。我們創建一個叫 webpack.config.js 的文件,在裡面這樣寫:
module.exports = { // 這裡就是我們剛剛說的 CommonJS 的關鍵詞n entry: ./entry.js, // 入口文件n output: {n path: ./build,n filename: output.js // 輸出文件路徑及文件名n }n}n
這樣寫完之後呢,我們現在只需要執行一下 webpack 就可以完成跟剛才一樣的編譯了。並且,我們還可以加一個 -w(watch)來監聽文件的變化,這樣我們之後修改文件之後就不用再手動去執行 webpack 了,它自動就會重新編譯。
除了 JavaScript 之外,還有 CSS、圖片之類的,怎麼辦呢?
要實現非 JS 文件的模塊化,我們需要使用 Webpack 的 loader。Loader 可以幫我們把一些非 JS 的資源變成可以在 JS 中使用。
比如我們想 require 一個 CSS 資源,那我們就會需要 css-loader,它可以把 CSS 文件變成 JS 的語法,然後我們可以再通過 style-loader 把已經被轉換成 JS 語法的 CSS 再插入到 DOM 中,讓我們的樣式生效。我們來試試:
// entry.jsnrequire(./style.css)nn// style.cssnp { color: red; }nn// webpack.config.jsnmodule.exports = {n entry: ./entry.js, // 入口文件n output: {n path: ./build,n filename: output.js // 輸出文件路徑及文件名n },n module: {n loaders: [n { test: /.css$/, loader: style!css } // `!` 是管道,loader 會從後往前依次執行n ]n }n}n
我們同樣執行 webpack,就可以打包完成了,之後我們直接把生成出來的 output.js 放到頁面里用就可以了。
組件化與目錄結構
很多人習慣性地會按照傳統把項目設計成這樣:
我們可以看到,比如說這個 Button,它把自己的 JS 抽離了出來變成一個模塊,自己的樣式和模板也分別抽離了出來變成一個模塊。
但是我們想,這種按照傳統的目錄結構有個很大的問題,那就是每個組件的模板、樣式和腳本都分別在不同的目錄當中,當我們在創建、修改和刪除一個組件的時候需要在不同的目錄之間來回切換,這樣會很麻煩。於是,很自然地我們就會想把每個組件都放到同一個目錄當中去:
左邊是原來的目錄結構,右邊是我們重新設計後的目錄結構。我們會發現,當我們把目錄結構設計成以組件的形式來分割,而不是以傳統的 JS、CSS、HTML 這樣去分割的時候,會給我們帶來很多好處。
組件化
我們把這種將模板、樣式和邏輯都抽象出來獨立出來的做法稱之為 組件化。
比如說,我們在開發 button 組件的時候,不再需要分別在幾個文件夾之間跳來跳去,去修改它們的模板、樣式和邏輯。我們只需要在 button 組件的文件夾里修改就好了。
這種設計其實也是跟我們在軟體工程中的「關注度分離」原則是非常吻合的,當我們需要對某個組件進行開發的時候,我們只需要關注這個組件的本身,當我們關注的東西越少,我們出錯的可能性就越小,代碼的內聚性就更好,耦合性也更低。
以計蒜客的課程列表頁為例,我把組件的劃分用框框把它們給突顯出來:
比如上面的導航條是一個組件,大的課程列表是一個組件,課程列表裡的每個課的面板也是一個組件,列表右上角也引用了一個按鈕組件。這麼劃分完之後呢,我們就可以對劃分出來的組件進行分工了。
通常我會在文檔中畫一個類似這樣的圖,上面就是各個組件的依賴關係,以及通過不同的顏色來表示不同的開發同學所要去開發的組件。拿到組件分配任務的同學,就可以對自己負責的組件進行開發了。
組件化庫
實際上,除了我們手動去做這麼一套組件化的工作之外,業界其實已經有比較火的組件化的庫和框架了,比如 Facebook 的 React 和我們國內有名的 @尤雨溪 開發的 Vue。我個人比較喜歡 Vue,因為它可以以非常平滑和優雅的方式融入到一個已經存在的項目裡面去,用作者尤雨溪的話來說,那就是小而美。我們在新的項目中幾乎完全拋棄 jQuery 而去用 Vue 了,老的項目也在重構中慢慢地引入 Vue。
由於時間的關係,這一塊我就不再擴展開去討論了,大家感覺興趣的話可以自己在網上多了解了解。
關於《模塊化工具和一些組件化的思想》咱們大概就聊到這裡,下一篇將會關於自動化構建工具,敬請期待。
擴展閱讀
這是前端工具鏈課系列分享第二篇,如果想看第一篇可以點擊:《包管理工具》。
知乎 Live
最後的最後,還是要給我的 Live 打個廣告哈!如果你想了解關於前端工程師的自我修養應該是怎樣的、如何成為一個優雅的前端工程師,歡迎點擊參加:《前端工程師的自我修養》。
推薦閱讀:
※使用Nuxt.js改善現有項目
※【譯】如何只用CSS製作一個漂亮的載入動畫
※APP圖標設計小技巧:在iOS上快速獲得APP圖標的真實預覽圖
※探究Babel生態