JavaScript構建(編繹)系統大比拼:Grunt vs. Gulp vs. NPM
英文原文:A JavaScript Build System Shootout: Grunt vs. Gulp vs. NPM
譯者:kris
決定采用何種技術總是很難的。一旦遇到問題,你不想推翻你之前的選擇。但是你必須選一個,然后讓它按照著你的思路做。實施一套構建(編繹)系也是一樣的,你應該把它看作一個非常重要的選擇,讓我們以 Grunt 為例。
- Grunt 有一個完善的社區,即使是在 Windows 上
- 它不僅僅應用在 Node 社區
- 它簡單易學,你可以隨便安裝插件并配置它們
- 你不需要多先進的理念,也不需要任何經驗 </ul>
這些都是用 Grunt 構建編繹工具的充分理由,但我想澄清一點,我不認為 Grunt 不是唯一最好的選擇。還有一些同樣流行的選擇擺在那里,有些方面可能比 Grunt 做得更好。
我寫這篇文章,以幫助您了解 Grunt,Gulp 和 npm 之間的差異,這是我在前端開發工作中使用最多的三種構建工具。
我們先來討論 Grunt 擅長的方面
Grunt:好的部分
Grunt 最好的一個方面是它的易用性。它能使程序員使用 JavaScript 構建編繹工具時,幾乎不費吹灰之力。你只需要尋找合適的插件,閱讀它們的文檔,然后安裝和配置它。這種易用性意味著大型開發團隊,那些不同技能水平的成 員,也可以沒有任何麻煩的調整編繹流程,以滿足項目的最新需求。而且團隊并不需要精通 Node,他們僅需要配置對象,將不同的任務添加到不同的序列構建編繹流程。
這里有基礎足夠大的插件庫,你會發現自己幾乎不需要開發自己的編譯任務,這能使您和您的團隊能夠快速構建開發工具,如果你要快速完成編繹過程這是至關重要的,你也可以采取小步走,逐步完善編譯流程的策略。
通過 Grunt 管理部署也是可行的,因為有許多包已經可以完成這些任務,如 grunt-git, grunt-rsync, 或 grunt-ec2 等等。
那么,Grunt 有什么缺陷嗎?如果你有一個明顯復雜的編繹過程,它可能會變得過于冗長。當開發一段時間以后,它往往很難將編繹過程作為一個整體。一旦你編繹流程任務到達 兩位數,幾乎可以保證,你會發現自己不得不在多個目標(Targets)中跑同一個 Task,以便你能夠正確地執行任務流。由于任務是需要聲明配置的,你也很難弄清楚任務真正的執行次序。
除此之外,你的團隊應該致力于編寫可維護的代碼,當涉及到你的編繹,比如在使用 Grunt 的情況下,這意味著你需要為每個任務(或者每個編繹流)編寫一份獨立的配置文件,供你的團隊使用。
現在,我們已經了解了 Grunt 好和不好的方面,以及在何種情況下,比較適合作為你項目的編繹工具。我們再來談談 npm,它如何被用作構建工具,以及與 Grunt 有何不同。
將 npm 視為構建工具
為了將 NPM 用作構建工具,你需要一個 package.json 和 npm。制定 NPM 任務就像在腳本中添加屬性一樣簡單。該屬性的名稱將用作任務名和將要執行的命令。下面的這個 build 任務將預先檢查我們的 JavaScript 代碼中有沒有語法錯誤,例子使用 JSHint 命令行接口來。在命令行中你可以運行任何你需要的 shell。
{ "scripts": { "test": "jshint . --exclude node_modules" }, "devDependencies": { "jshint": "^2.5.1" } }
一旦定義完成,就可以通過下面的命令來運行
npm run test
需要注意的是 npm 提供了運行特定任務的快捷方式。比如要運行 test,你可以簡單地使用 npm test 并省略動詞 run。您可以通過一個命令鏈來將一系列 npm run 的任務連在一起,構成你的編繹流程:
{ "scripts": { "lint": "jshint . --exclude node_modules", "unit": "tape test/*", "test": "npm run lint && npm run unit" }, "devDependencies": { "jshint": "^2.5.1", "tape": "~2.10.2" } }
您也可以安排一些后臺完成的任務,然后讓他們同步。假設我們有以下的包文件, 我們將復制出一個目錄用來放 JavaScript 文件,以及將我們用 Stylus 寫的樣式表文件編繹成 CSS。在這種情況下,多個任務一起運行是比較理想的。也可以實現,使用&分隔符即可。
{ "scripts": { "build-js": "cp -r src/js/vendor bin/js", "build-css": "stylus src/css/all.styl -o bin/css", "build": "npm run build-js & npm run build-css" }, "devDependencies": { "stylus": "^0.45.0" } }
要了解關于將 npm 用作構建工具的更多內容,你應該先學學寫一些 Bash 命令。
安裝 NPM 的任務依賴
JSHint CLI 并不一定要包含在你的系統中,這里有兩種安裝它的方式。如果你正在尋找直接從命令行中運行的工具,那么你應該在全局范圍內安裝,使用g標志,如下所示。
npm install -g jshint
不過,如果您使用的是包在 npm run 中使用的,那么你就應該把它加到 devDependency 中,如下所示。這將讓 npm 自動在系統中尋找它所依賴的 JSHint 安裝在了哪里。這方法適用于任何命令行工具中和所有操作系統。
npm install --save-dev jshint
你其實不僅局限使用 CLI 工具。事實上,npm 能夠運行任何 shell 腳本。讓我們來挖一挖!
在 npm 中使用 shell 腳本
下面是一個運行在 node 的腳本,并顯示一個隨機的繪文字符串(emoji-random)。第一行指定運行環境,該腳本基于 Node。
#!/usr/bin/env nodevar emoji = require ('emoji-random'); var emo = emoji.random (); console.log (emo);
如果你將一個名為 emoji 的腳本文件放到你項目的根目錄中,你必須將 emoji-random 申報為依賴關系,并將以下腳本命令添加到包文件中。
{ "scripts": { "emoji": "./emoji" }, "devDependencies": { "emoji-random": "^0.1.2" } }
一旦寫成這樣,你只需要在命令行運行 npm run emoji 即可。
好和壞的方面
使用 NPM 作為構建工具比 Grunt 有幾大優勢。你不會被 Grunt 的插件束縛,你可以利用 NPM 的所有優勢,它有數以萬計的模塊可以選擇。除了 NPM,你不需要任何額外的命令行工具(CLI)或文件,你只需要在 package.json 添加依賴關系。由于 NPM 運行命令行工具(CLI 工具)和 Bash 命令,這比 Grunt 執行的方式更好。
Grunt 的最大缺點之一就是它的I/O限制。這意味著大多數 Grunt 的任務將從磁盤中讀取,再寫入到磁盤。如果你的多個任務需要操作同一個文件,那么該文件很有可能被從磁盤中多次讀取。在 bash 中,命令通過管道直接傳遞給下一個任務,避免 Grunt 額外的I/O開銷。
也許 NPM 的最大的缺點是,在 Windows 環境中的應用可能沒那么好。這意味著使用 NPM 運行的開源項目可能遇到問題。這也意味著 Windows 開發人員嘗試使用 npm 的替代品。這缺點幾乎將 NPM 從 Windows 上排除。
Gulp,另一個構建工具,提出了與 Grunt 和 npm 相似的功能,一會你就會發現。
Gulp 的流式構建工具
與 Grunt 類似,它依賴插件,并且是跨平臺的,它也支持 Windows。Gulp 是一個代碼驅動的構建工具,與 Grunt 的聲明式定義任務相反,它的任務定義更容易閱讀一點。Gulp 也有類似于 npm run 的東西,因為它使用 Node Stream 來轉化輸入輸出。這意味著,Gulp 沒有 Grunt 那種磁盤密集型I/O操作的問題。它也是它比 Grunp 更快的原因,更少的時間花在I/O上面。
在使用 Gulp 的主要缺點是,它在很大程度上依賴于流,管道和異步代碼。不要誤解我的意思:如果你用在 Node 中,這絕對是一個優勢。但是,除非你和你的團隊非常精通 Node,你很有可能會遇到處理流的問題,特別是如果你要建立你自己的 Gulp 任務插件。
在團隊工作的時候,Gulp 不是望而卻步的 npm,因為大多數前端團隊可能都懂 JavaScript,但是他們可能對 Bash 腳本不那么熟練,其中一些可能是使用 Windows 的!這就是為什么我通常建議你在個人項目中運行 NPM 的原因。如果你的團隊很熟悉 Node,你可以使用 Gulp。當然,這是我個人的建議,你應該找到最適合你和你團隊的工具。此外,你應該不會把自己限制在 Grunt,Gulp,或者 npm run 中,對你我來說這些都只是工具。嘗試做一些小小的研究,也許你會發現,你喜歡的甚至比這三個更好的工具。
讓我們通過一些例子來看看 Gulp 中的任務看起來是什么樣子的。
在 Gulp 中運行測試
有一些約定 Gulp 與 Grunt 極為相似。在 Grunt 中有一個定義 Task 的文件 Gruntfile.js,在 Gulp 中叫 Gulpfile.js。另一種微小的差別是,在 Gulp 中,CLI 已經包含在同一個 Gulp 包中,你需要通過 npm 從本地和全局同時安裝。
touch Gulpfile.js npm install -g gulp npm install --save-dev gulp
在開始之前,我將創建一個 Grulp 任務處理一個 JavaScript 文件,就像你已經在 Grunt 和 NPM 中看到的那樣使用 JSHint,你需要先安裝 gulp-jshint,Gulp 的 JSHint 插件。
npm install --save-dev gulp-jshint
現在你已經同時在全局和本地中裝好 CLI 了,本地已經安裝了 gulp 和 gulp-jshint 插件,你可以將這些構建任務合成一個。你可以在 Gulpfile.js 文件中寫出來。
首先,您將使用 gulp.task 定義一個任務和功能。該功能包含了所有必要的代碼來運行這項測試。在這里,你應該使用 gulp.src 創建一個讀取你源文件的流,這個數據流會被管道輸送進 JSHint 插件。然后,所有你需要做的就是管道中的 JSHint 任務打印到終端。下面是 Gulpfile 中展示的結果。
var gulp = require ('gulp'); var jshint = require ('gulp-jshint'); gulp.task ('test', function () { return gulp .src ('./sample.js') .pipe (jshint ()) .pipe (jshint.reporter ('default')); });
點需要提一下,Grulp 流會在一個任務完全結束之后再轉到下一個任務。你可以使用一個 JSHint Reporter 使輸出更加簡潔,從而更易于閱讀。 JSHint Reporter 并不需要 Grulp 插件,例如 jshint-stylish,讓我們在本地直接安裝。
npm install --save-dev jshint-stylish
更新后的 Gulpfile 應如下所示。它會加載 jshint-stylish 模塊,按報表格式輸出。
var gulp = require ('gulp'); var jshint = require ('gulp-jshint'); gulp.task ('test', function () { return gulp .src ('./sample.js') .pipe (jshint ()) .pipe (jshint.reporter ('jshint-stylish')); });
大功告成!這是所有一個命名為 test 的 Gulp 的任務。它可以使用下面的命令運行,只要你安裝了全局的 CLI。
gulp test
這是一個相當簡單的例子。你也可以通過使用 gulp.dest,創建了一個寫數據流到磁盤中。讓我們看看另外一個構建任務。
在 Grulp 中創建一個庫
在開始之前,讓我們明確任務:從磁盤 gulp.src 讀取源文件并通過磁盤管道寫回內容到 gulp.dest,你可以理解成只是將文件復制到另一個目錄。
var gulp = require ('gulp'); gulp.task ('build', function () { return gulp .src ('./sample.js') .pipe (gulp.dest ('./build')); });
復制文件完成了,但是它沒有壓縮這個 JS 文件。要做到這一點,你必須使用一個 Gulp 插件。在這種情況下,你可以使用 gulp-uglify,流行的 UglifyJS 壓縮編繹插件。
var gulp = require ('gulp');var uglify = require ('gulp-uglify'); gulp.task ('build', function () { return gulp .src ('./sample.js') .pipe (uglify ()) .pipe (gulp.dest ('./build')); });
正如你可能意識到的那樣,流使可以讓您添加更多的插件,而只需要讀取和寫入磁盤一次。你也可以指定緩沖器中內容的大小。需要注意的是,如果你在壓縮之前添加它,那么你得到的大小是 unminified。
var gulp = require ('gulp'); var uglify = require ('gulp-uglify'); var size = require ('gulp-size'); gulp.task ('build', function () { return gulp .src ('./sample.js') .pipe (uglify ()) .pipe (size ()) .pipe (gulp.dest ('./build')); });
為了增強這種組合,滿足添加或刪除管道的需要,讓我們添加最后一個插件。這一次,我會用 gulp-header 在頭文件添加一段版權信息的代碼,如名稱,版本和許可證類型。
var gulp = require ('gulp'); var uglify = require ('gulp-uglify'); var size = require ('gulp-size');var header = require ('gulp-header'); var pkg = require ('./package.json');var info = '// <%= pkg.name %>@v<%= pkg.version %>, <%= pkg.license %>\n'; gulp.task ('build', function () { return gulp .src ('./sample.js') .pipe (uglify ()) .pipe (header (info, { pkg : pkg })) .pipe (size ()) .pipe (gulp.dest ('./build')); });
就像 Grunt 一樣,在 Grulp 中你可以通過傳遞一組任務到 gulp.task 來定義流程。在這方面,Grunt 和 Grulp 之間的主要區別在于,Grunt 是同步的,而 Grulp 是異步的。
gulp.task ('build', ['build-js', 'build-css']);
在 Gulp,如果你要讓任務同步運行,你必須聲明一個任務。你的任務開始之前執行。
gulp.task ('build', ['dep'], function () { // 執行 dep 所依辣的任務 });
如果你有任何收獲,先看看這段話。
你使用哪種工具并不重要,只要保證:流程構建(編繹)好用就行了,用起來不要太辛苦。
<span id="shareA4" class="fl">
</span>