Grunt 系列(2) 設定
這篇教學將會解釋如何使用 Gruntfile 為你的專案設定任務。如果你還不知道什麼是 Gruntfile 請回到上一篇 Grunt 入門 閱讀。
Grunt 設定
關於任務,你可以把任務當作就是一連串需要被執行的動作,通常這些設定都在 Gruntfile 這個檔案的 grunt.initConfig 這個方法中。
接著各個任務的設定都會在屬性名稱底下,換個說法, initConfig 需要你傳遞一個物件{}
參數進去如下:
grunt.initConfig({property:{ // task configuration here. }});
之後這個物件的每一個屬性通常就對應著一個任務,這只是大部份的狀況,它有可能是任意的資料。只要屬性不衝突即可,一旦衝突就會被忽略。接著讓我們來看看一個比較完整的範例:
grunt.initConfig({
concat: {
// concat task configuration goes here.
// 整合檔案的任務
},
uglify: {
// uglify task configuration goes here.
// 壓縮檔案的任務
},
// Arbitrary non-task-specific properties.
// 任意的資料
my_property: 'whatever',
my_src_files: ['foo/*.js', 'bar/*.js'],
});
# 任務設定和目標(Targets)
當任務開始執行, Grunt 會去尋找設定檔裡面的同名屬性,不同的任務配合不同的屬性,屬性可以設定不同的任務目標。我們用下面的範例來說明: `concat` 任務有 `foo`, `bar` 兩個目標,`uglify` 任務有 `bar` 一個目標。
grunt.initConfig({
concat: {
foo: {
// concat task "foo" target options and files go here.
},
bar: {
// concat task "bar" target options and files go here.
},
},
uglify: {
bar: {
// uglify task "bar" target options and files go here.
},
},
});
上一篇我們說過可以透過 grunt uglify
來執行任務,那當我們有不同的目標時就可以透過 grunt concat:foo
來執行該目標。不過在這邊要再次提醒上面範例只是設定,並無法拿來直接執行。如果直接下 grunt concat
則會把所有目標都執行一遍。這邊還有另一點要注意的是如果使用 grunt.renameTask
修改了任務的名稱,那 grunt 就會自動根據新名稱去找設定。
Options
在每一項任務設定中`options`屬性也許是用來覆寫內建的預設規則,此外每個`target`目標也可能會有一個 `options` 屬性。目標的 `options` 屬性會覆蓋任務的 `options` ,最後就是關於 `options` 這個屬性是可以省略的,他不是必須的。讓我們來看看範例:
grunt.initConfig({
concat: {
options: {
// Task-level options may go here, overriding task defaults.
// 任務等級的 options
},
foo: {
options: {
// "foo" target options may go here, overriding task-level options.
// 『目標』等級的 options 如果設定有重複,它會覆寫任務等級的設定。
},
},
bar: {
// No options specified; this target will use task-level options.
},
},
});
# 檔案
因為大部份的任務都涉及操作檔案, Grunt 對於檔案的定義宣告與操作有非常強及高度的抽象化。它提供了一些方法定義 `src-dest` (來源-目的地)如何對應 ,就是取得來源檔案,然後處理後產生至目的地目錄。提供了不同程度的操作,任何任務都會針對某種格式的檔案來做操作,編譯等等。你可以選擇你需要的格式。
所有的檔案格式都支援 src
和 dest
兩種屬性設定,不過簡潔
格式和檔案陣列
有一些額外的屬性可用。下面會說明什麼是簡潔
格式和檔案陣列
。額外支援的屬性如下:
filter
任何一個fs.States 的方法名稱如:
stats.isFile()你可以用
‘isFile’,或者任何一個 function 輸入
src` 路徑後可以判斷回傳 true/false ,都可以拿來設定,用來過濾。nonull
當一個比對沒有被找到的時候,就會回傳一個包含其 pattern 的列表,否則就會回傳一個空的列表。搭配--verbose
可以用在關於檔案路徑的除錯。
dot
允許檔案名稱的匹配模式用.
開始 ,即使設定沒有在開頭使用.
matchBase
一旦設定這種匹配模式就不會比對/
舉例來說會變成*.js
等於**/*.js
簡單來說就是只比對最後檔名的部分。例如,a?b 會匹配/xyz/123/acb
但不匹配/xyz/acb/123
。expand
處理動態的src
和dest
檔案對應。- 其他屬性都會傳入底層當作匹配的參數
# 簡潔格式
這種格式允許每一個『目標』設定單一屬性的對應,比較常用在讀取檔案,例如 [grunt-contrib-jshint](https://github.com/gruntjs/grunt-contrib-jshint) 只需要 `src` ,而不需要 `dest` 。看下面範例會更清楚。
grunt.initConfig({
jshint: {
foo: {
src: ['src/aa.js', 'src/aaa.js']
},
},
concat: {
bar: {
src: ['src/bb.js', 'src/bbb.js'],
dest: 'dest/b.js',
},
},
});
# 檔案物件格式
這種格式允許每個目標對應多個檔案,前面的屬性名稱會是目的地的檔案名稱。然後來源檔案可以是多個。可以用這種方式設定多組檔案的對應。一旦採用這種設定方式就不能再加入其他屬性了。看看下列程式碼:
grunt.initConfig({
concat: {
foo: {
files: {
'dest/a.js': ['src/aa.js', 'src/aaa.js'],
'dest/a1.js': ['src/aa1.js', 'src/aaa1.js'],
},
},
bar: {
files: {
'dest/b.js': ['src/bb.js', 'src/bbb.js'],
'dest/b1.js': ['src/bb1.js', 'src/bbb1.js'],
},
},
},
});
# 檔案陣列格式
這種方式一樣支援多組 `src-dest` 對應,但可以多加入其他屬性。就跟一開始說的一樣 `簡潔格式` 和 `檔案陣列` 格式可以增加其他的屬性。直接看範例比較清楚。
grunt.initConfig({
concat: {
foo: {
files: [
{src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'},
{src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'},
],
},
bar: {
files: [
{src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true},
{src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'},
],
},
},
});
# 舊版的格式
直接在任務底下,就是沒有 `files` 屬性是舊版本在執行多檔案操作的一種過渡期的方式。直接讓最後目的地的路徑等於`目標`(target) 名稱,很不幸的因為目標名稱就是路徑,所以當你要執行 `grunt task:target` 的時候就顯得很糟糕。而且你也不能在設定 `目標等級` 的 `options` 或加入其他屬性。因此請儘量避免使用這種格式設定。
grunt.initConfig({
concat: {
'dest/a.js': ['src/aa.js', 'src/aaa.js'],
'dest/b.js': ['src/bb.js', 'src/bbb.js'],
},
});
# 自訂過濾 Function
`filter` 這個屬性可以讓你針對檔案設定更多細節,最簡單的方式就是使用 `fs.States` 的 method 名稱來設定,例如下面的程式碼我們就可以輕鬆的確認是否為一個實體檔案,然後把 `tmp` 暫存的檔案都清光。
grunt.initConfig({
clean: {
foo: {
src: ['tmp/**/*'],
filter: 'isFile',
},
},
});
或者是你只要建立一個 function 回傳 true/false 就可以了
grunt.initConfig({
clean: {
foo: {
src: ['tmp/**/*'],
filter: function(filepath) {
return (grunt.file.isDir(filepath) && require('fs').readdirSync(filepath).length === 0);
},
},
},
});
# 模式匹配與符號說明(Global Pattern)
一般來說一個路徑一個路徑設定在實務上來說是不切實際的,非常麻煩。所以 Grunt 支援透過內建的函式庫來做檔名路徑的匹配,或者說透過設定 Pattern 規則來選取檔案。這並不是專門的模式匹配的教學,我們只需要學習常用的符號即可。
*
任意數量的字元,但不包含/
。?
單一字元,不包含/
。**
任何數量的字元,包含/
只要是路徑的一部分就可以。{}
用,
分隔列出清單,只要其中一個符合即可(OR)。!
用在開頭,排除的意思。
大部份的人都知道 /foo/*.js
會選取到所有在 /foo
目錄底下的 js
檔案。但是 foo/**/*.js
則會包含任何在 /foo
目錄底下子目錄的 js
檔案。要注意 /foo/*.js
只有一層,就是在 /foo
底下,至於其它如 /foo/subfolider
就沒有。
此外,為了簡化複雜的匹配, Grunt
也允許你使用檔案陣列的方式如 ['a.js', '/foo/b.js']
,模式匹配是按照順序的,!
會排除匹配的檔案,其結果是唯一的。
// 指定單一檔案
{src: 'foo/this.js', dest: …}
// 使用檔案陣列指定多個檔案
{src: ['foo/this.js', 'foo/that.js', 'foo/the-other.js'], dest: …}
// 使用模式匹配來選取檔案
{src: 'foo/th*.js', dest: …}
// node-glob 模式,對應 foo 目錄底下 a 或 b 開頭的 js
{src: 'foo/{a,b}*.js', dest: …}
// 上面的例子也可以這樣寫。
{src: ['foo/a*.js', 'foo/b*.js'], dest: …}
// foo 目錄下所有的 .js ,但不包含子目錄下的
{src: ['foo/*.js'], dest: …}
{src: ['foo/bar.js', 'foo/*.js'], dest: …}
// 除了 bar.js 以外所有的 .js ,會按照字母排列。
{src: ['foo/*.js', '!foo/bar.js'], dest: …}
// 所有的 .js 檔案, bar.js 先被排除後,又被加入了
{src: ['foo/*.js', '!foo/bar.js', 'foo/bar.js'], dest: …}
// 模版也可以用在檔案路徑或模式匹配中。
{src: ['src/<%= basename %>.js'], dest: 'build/<%= basename %>.min.js'}
// 也可以使用其他設定檔中配置的屬性值。
{src: ['foo/*.js', '<%= jshint.all.src %>'], dest: …}
# 動態建立檔案物件
當你需要處理很多個別的檔案的時候,有些額外的屬性可以協助你動態的建立檔案清單。這些屬性都可以用在簡潔格式和檔案陣列格式中。 * `expand` 設成 `true` 用來啓用後面的屬性設定。 * `cwd` (Current Working Directory) 設定一個目標路徑,注意這個不是檔案路徑或者 Pattern。 * `src` 在根據上面的 `cwd` 做 Pattern 或檔案的選取。 * `dest` 目的地目錄 * `ext` 在 `dest` 目錄設定新的附檔名取代原始附檔名。 * `flatten` 移除 `dest` 設定的路徑,設定值為 true/false * `rename` 對每個符合規則的 `src` 檔案調用這個 function (在執行完 ext 和 flatten 之後)。接著傳遞 `dest` 和 `src` 的值給 function 最後回傳一個新的 `dest` 路徑。
对每个匹配的src文件调用这个函数(在执行ext和flatten之后)。传递dest和匹配的src路径给它,这个函数应该返回一个新的dest值。 如果相同的dest返回不止一次,每个使用它的src来源都将被添加到一个数组中。如果傳回的 dest
重複則會被加入 sources
的陣列。
下面是關於 minify
的例子,我們會看到 static_mappings
和 dynamic_mappings
目標雖然設定不同,卻處理了相同的檔案列表, static_mappings
是一條一條設定,而 dynamic_mappings
則是動態,先切換到一個路徑底下,再根據同樣的規則去執行。
grunt.initConfig({
minify: {
static_mappings: {
// 由於 src-dest 檔案路徑是寫死的, 每次新增或刪除,Gruntfile 都要更新。
files: [
{src: 'lib/a.js', dest: 'build/a.min.js'},
{src: 'lib/b.js', dest: 'build/b.min.js'},
{src: 'lib/subdir/c.js', dest: 'build/subdir/c.min.js'},
{src: 'lib/subdir/d.js', dest: 'build/subdir/d.min.js'}
]
},
dynamic_mappings: {
// 執行任務時 Grunt 會自動在當下的工作目錄 "lib/" 下搜尋 Pettern "**/*.js", 接著建立 src-dest 檔案對應,因此你就不需要在新增或刪除檔案時更新 Gruntfile。
files: [
{
expand: true, // 啓用動態對應
cwd: 'lib/', // 匹配切換成當前的工作目錄 lib
src: '**/*.js', // 根據上面的目錄限制,套用 Pettern
dest: 'build/', // 檔案產生目的地目錄
ext: '.min.js' // 取代原本的附檔名
}
]
}
}
});
# 樣板(Templates)
直接翻譯為樣板可能不太恰當,主要的用法是可以透過 `<% %>` 嵌入變數,而這些變數可以是來自讀取設定檔的資料。模板本身會不斷遞回讀取變數直到裡面不再存在任何模板。 整個設定物件中由外到內的屬性都會被解析,此外 `grunt` 的方法或屬性都可以被嵌入樣板中。例如 `<%= grunt.template.today('yyyy-mm-dd') %>`
<%= prop.subprop %>
取得prop.subprop
設定屬性的值。只要正規的型別他都能使用,不僅是字串,陣列貨,物件也可以。這樣說明你可能會有點模糊,讓我們看下列的範例。module.exports = function(grunt){ grunt.initConfig({ prop: {subprop:"tmp/lib/"}, clean: { src: "<%= prop.subprop %>*.js" } }); }
<% %>
在分隔符號中您可以使用 Javascript。對於做一些基本的邏輯判斷是非常有用的。
讓我們來一些範例觀察,下面使用了 concat
任務來整合檔案,執行 grunt concat:sample
,透過 banner
會在檔案加入 /* abcde */
,而這隻檔案是由 foo/*.js
, bar/*.js
, baz/*.js
這三個規則搜尋來的檔案組合而成,最後結合的檔案則生成 build/abcde.js
。
grunt.initConfig({
concat: {
sample: {
options: {
banner: '/* <%= baz %> */\n' // '/* abcde */\n'
},
src: ['<%= qux %>', 'baz/*.js'], // [['foo/*js', 'bar/*.js'], 'baz/*.js']
dest: 'build/<%= baz %>.js'
}
},
// 同樣在 initConfig 中可以直接存取屬性。
foo: 'c',
bar: 'b<%= foo %>d', //'bcd'
baz: 'a<%= bar %>e', //'abcde'
qux: ['foo/*.js', 'bar/*.js']
});
# 匯入外部資料
在下面的 Gruntfile 中,專案本身的 metadata 可以透過 `package.json` 匯入到設定物件中。接著 `uglify` 任務就可以透過 pkg.name 取得該專案名稱的js。 src/`pkg.name`.js ,然後產生到 `dest` 中並加上 `.min.js`。
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/* <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
dist: {
src: 'src/<%= pkg.name %>.js',
dest: 'dist/<%= pkg.name %>.min.js'
}
}
});
# 實做練習
最後讓我們在透過一個簡單的清除檔案練習,加深對整個 Grunt 運作的觀念吧!
$ mkdir grunt-test
$ cd grunt-test
$ npm init
$ npm install grunt grunt-contrib-clean --save
$ vi Gruntfile.js
Gruntfile 設定
module.exports = function(grunt){
// 設定關於 clean 的設定,清除 tmp/ 底下的 js 檔案
grunt.initConfig({
clean: {
files:[
{src: 'tmp/*.js'}
],
dynamic:{
files:[
{
expand: true,
cwd: '<%= prop.andy %>',
src: ['**/*.js']
}
]
}
}
});
// 載入套件
grunt.loadNpmTasks('grunt-contrib-clean');
// 註冊預設的任務
grunt.registerTask('default', ['clean']);
}
接著執行
$ grunt --help // 查詢可用的指令
$ grunt clean // 執行指令清除
$ grunt clean:dynamic // 執行任務中的 dynamic 目標
# 參考資料
[官方文件](http://gruntjs.com/configuring-tasks) [官方手冊簡體翻譯](http://www.gruntjs.org/article/home.html) [Wiki](https://github.com/gruntjs/grunt/wiki/Configuring-tasks) [其他教學](http://www.cnblogs.com/chyingp/archive/2013/05/11/grunt_getting_started.html)
Grunt 系列(2) 設定