Grunt 是一個以 Node.js 為基礎所開發的命令列工具,在經過適當的設定之後,它可以幫助程式開發者將一些重複性的工作自動化,減輕開發者與開發團隊的負擔。

Grunt 可以處理的事情很多,例如精簡 CSS 程式或網頁的大小、編譯 CoffeeScript、unit test、linting 等,舉凡一般性的重複動作多半都可以使用這個工具來處理。

Grunt 背後有一個很大的生態系統(ecosystem),包含了大量的 plugins,使用者可以藉由這些 plugins 將自己的工作自動化,而使用者也可以很容易的把自己開發的 plugin 上傳到 npm 上面分享給其他人使用,也因為這樣的分享機制,這個生態系統目前也持續在成長。

Grunt 與 Makefile

Grunt 的角色類似傳統上的 Makefile,但是它主要是用於一些網頁應用程式的開發,而根據 Grunt 作者 Ben Alman 的解釋,如果使用傳統的 Makefile 處理網頁應用程式開發問題,通常使用者必須自己安裝相關的開發程式(像 CoffeeScriptMocha 等),如果是在 Windows 環境下,要安裝的東西可能更多,包含 make 本身與基本的 shell 環境等。

而如果使用 Grunt 的話,你只需要安裝 grunt-cli 這個程式,然後使用 npm install 這樣的指令就可以自動安裝大部分開發所需的程式與環境,也幫你處理許多套件相依性的問題,簡化了開發前期準備工作的複雜度,而且由於 Grunt 是建立在 Node.js 架構之下,所以只要是 Node.js 有支援的平台,都可以直接套用這樣的方式,達到跨平台的效果,讓每個平台的操作方式都一模一樣。

Grunt 除了將一般性的工作自動化之外,它還可以處理外部的 JSON 或 YAML 檔案動態提供的資料、檔案系統的監控(filesystem watching)或 livereload 等,因為它是特別針對網頁應用程式開發的需求所設計的,所以在這方面的使用上會比傳統的 Makefile 方便一些。

當然 Grunt 做的這些事情若是硬要使用傳統的 Makefile 來做,應該也是可以做到的,只不過稍微麻煩一些。

安裝 Node.js

由於 Grunt 建立在 Node.js 架構之下,所以在安裝 Grunt 之前要先把 Node.js 的環境安裝好,安裝過程可以參考在 Windows、Mac OS X 與 Linux 中安裝 Node.js 網頁應用程式開發環境

安裝 Grunt 的 Command Line Interface(CLI)

當 Node.js 的環境安裝好之後,接著再使用 npm 指令來安裝 Grunt 的命令列工具(CLI):

npm install -g grunt-cli

因為這個 grunt 工具要安裝在系統的目錄中,所以如果是在 Mac OS X 或 Linux 等系統中,安裝時要使用 sudo 或是切換成 root 帳號來安裝。

這裡要注意一點,安裝 Grunt CLI(grunt-cli)這個套件並不會安裝 Grunt task runner,Grunt CLI 做的事情只是負責執行 Gruntfile 所設定的 Grunt 版本而已,而這樣的機制可以讓一個系統中同時安裝多個 Grunt 版本。

Grunt CLI 是如何運作的?

每當 grunt 指令執行時,它會使用 Node.js 的 require() 尋找在本地的目錄中所安裝的 Grunt,而你也可以在任何專案的子目錄中執行 grunt

當找到本地目錄中所安裝的 Grunt 函式庫之後,CLI 就會將其載入,然後依照專案中 Gruntfile 的設定執行指定的工作流程。

如果你想要完全了解整個流程,其實可以直接查看 grunt 指令的原始碼,它的程式碼內容其實不長。

既有的 Grunt 專案

如果你已經有一個設定好 package.jsonGruntfile 的 Grunt 專案,那麼在 Grunt CLI 安裝好之後,接下來使用 Grunt task 就很輕鬆了:

  1. 進入專案的根目錄(root directory)。
  2. 執行 npm install 自動安裝相依性的套件。
  3. 執行 grunt 指令呼叫本地目錄中的 Grunt 進行各項 task。

這些就是全部的動作,而安裝的 Grunt tasks 可以藉由 grunt --help 指令列出來,但是通常還是建議先閱讀專案的說明文件。

新的 Grunt 專案

一般的 Grunt 專案中最重要的就是 package.jsonGruntfile 這兩個設定檔:

  • package.json:這個檔案是 npm 儲存一些中繼資料(metadata)的地方,你必須把 Grunt 與專案需要的 Grunt plugins 列在這個檔案中的 devDependencies
  • Gruntfile:這個檔案會被命名為 Gruntfile.js 或是 Gruntfile.coffee,用來設定要載入的 Grunt plugins 與要執行的 task。

package.json

package.json 這個檔案與 Gruntfile 一樣都是放在專案的根目錄底下,而且最好也納入專案的原始碼一起納入管理(committed),而在 package.json 所在的目錄中執行 npm install 可以自動處理相依性的問題,依照 package.json 中指定的相依性套件版本來安裝各個套件。

若要在專案中建立 package.json 這個檔案,有幾種方式:

  • 大多數的 grunt-init 範本可以自動建立專案所需要的 package.json 檔案。
  • 執行 npm init 這個指令可以產生基本的 package.json 檔案。
  • 使用下面這個範例,參考 package.json 的使用規範,加入自己需要的設定。

以下是一個簡單的 package.json 範例:

{
  "name": "my-project-name",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.2",
    "grunt-contrib-jshint": "~0.6.3",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-uglify": "~0.2.2"
  }
}

安裝 Grunt 與 Grunt Plugins

要安裝 Grunt 與 Grunt Plugins 最簡單的方式就是使用 npm install <module> --save-dev 這個指令,這樣除了將 <module> 這個套件安裝在本地的目錄之外,也會自動以 tilde(~)的版本指定方式將該套件加入 devDependencies

舉例來說,下面這個指令就會在專案的目錄中安裝 Grunt,並且將其加入 devDependencies

npm install grunt --save-dev

至於其他的 Grunt plugins 與 Node.js 模組也都一樣可以使用這樣的方式安裝,而安裝完之後記得要將更新後的 package.json 納入一起納入專案原始碼的管理(commit),不要把這個檔案漏掉了。

Gruntfile

Gruntfile.jsGruntfile.coffee 這兩個檔案是一般的 JavaScript 與 CoffeeScript 檔,也跟 package.json 一樣存放在專案的根目錄之中,並且也要跟專案原始碼一起管理。

一個 Gruntfile 檔案通常包含下面幾個部份:

  • wrapper 函數。
  • 專案與 task 的設定。
  • 載入 Grunt plugins 與 tasks。
  • 自訂 tasks。

Gruntfile 範例

在下面這個範例中會把 package.json 中的專案中繼資料匯入 Grunt 的設定中,並且設定使用這些中繼資料與 grunt-contrib-uglify 這個 plugin 所提供的 uglify task 來處理程式碼最小化與加入 banner 註解的動作,當使用者執行 grunt 指令時,預設就會執行 uglify 這個 task。

module.exports = function(grunt) {

  // 專案設定
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/<%= pkg.name %>.js',
        dest: 'build/<%= pkg.name %>.min.js'
      }
    }
  });

  // 載入可以提供 uglify task 的 plugin
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // 預設的 task
  grunt.registerTask('default', ['uglify']);

};

以下我們將這個 Gruntfile 分為幾個部份來解說。

wrapper 函數

基本上每一個 Gruntfile(與 Grunt plugin)都會有一個這樣的 wrapper 函數,而所有的 Grunt 程式碼都會放在這個函數裡面。

module.exports = function(grunt) {
  // 這裡放置 Grunt 相關的程式碼
};

專案與 task 的設定

大部分的 Grunt task 都會使用 grunt.initConfig 這個函數來設定,設定的方式是將所有的設定資料包裝成一個物件,然後再傳入這個函數中。

以這個例子來說,grunt.file.readJSON('package.json') 會將儲存在 package.json 中 JSON 格式的中繼資料匯入至 Grunt 的設定檔中,而在 Gruntfile 中還可以使用 <% %> 這種語法取用任何設定檔中的設定值,所以像檔案的路徑與檔案列表等都可以透過這樣的方式避免不必要的重複設定。

在這個設定檔物件中,你也可以在不會發生變數名稱衝突的情況下,加入自己定義的資料,另外因為這個檔案本身是一個 JavaScript 檔案,所以你不見得一定要使用標準的 JSON 格式,一般的 JavaScript 程式碼也可以放在這裡執行,所以你也可以在這裡撰寫一些 JavaScript 程式產生你要的設定檔。

grunt-contrib-uglify 這個 plugin 的 uglify task 需要一個相同名稱的設定物件(大部分的 plugin 也都是這樣),而這個設定物件中在 options 部分用 banner 指定註解內容,而在 build 的部分則用 srcdest 指定原始碼與目的檔的位置。

// 專案設定
grunt.initConfig({
  pkg: grunt.file.readJSON('package.json'),
  uglify: {
    options: {
      banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
    },
    build: {
      src: 'src/<%= pkg.name %>.js',
      dest: 'build/<%= pkg.name %>.min.js'
    }
  }
});

載入 Grunt plugins 與 tasks

一般專案常會使用的 tasks(例如 concatenationminificationlinting 等)在 grunt plugins 中都有提供,只要在 package.json 中設定好相依性,並且使用 npm install 這樣的指令安裝好之後,就可以在 Gruntfile 中使用這樣的設定方式啟用它了:

// 載入可以提供 uglify task 的 plugin
grunt.loadNpmTasks('grunt-contrib-uglify');

如果要查詢所有可用的 tasks,可以使用 grunt --help 這個指令。

預設的 tasks

Gruntfile 中可以使用 grunt.registerTask() 這個函數設定預設要執行的 task(default),在這個例子中,我們將預設的 task 設定為 uglify,所以如果執行 grunt 指令而沒有加上任何參數的時候,它會執行這個預設的 task,而這個狀況就跟執行 grunt uglifygrunt default 相同。而在這個陣列中也可以加入多個預設的 tasks,讓 grunt 預設一次執行多個 tasks。

// 預設的 task
grunt.registerTask('default', ['uglify']);

自定 tasks

如果你的專案所需要的 task 在 Grunt plugin 中沒有提供,你也可以在 Gruntfile 中加入自定的 task。

下面這個 Gruntfile 是一個自定 task 的範例,這裡完全自行定義函數的方式來處理,沒有使用到 Grunt plugin 所提供的 task。

module.exports = function(grunt) {

  // A very basic default task.
  grunt.registerTask('default', 'Log some stuff.', function() {
    grunt.log.write('Logging some stuff...').ok();
  });

};

如果你自行定義的函數比較複雜,也可以將它儲存成另外一個 .js 檔,把它跟 Gruntfile 分開,再使用 grunt.loadTasks() 載入,這樣會比較好管理。