這篇文章將帶領你用Grunt來提速和優化網站開發的流程。首先我們會簡短介紹Grunt的功能,然後我們直接上手,介紹如何用Grunt的不同外掛來替你完成網站專案開發中的很多繁冗工作。
接著我們會建立一個簡單的input校驗器,用 Sass 來完成CSS的預處理,我們會學習如何用grunt-cssc 和CssMin來合併和壓縮CSS,如何用 HTMLHint 來保證我們的HTML正確無誤,以及如何實現在執行時部署和壓縮我們的檔案。最後,我們會學習如何用UglifyJS 來極簡化我們的JavaScript以儘可能地節約頻寬。
Grunt.js 是一個JavaScript 任務執行工具,它能替你完成重複性的任務,如極簡化、 編譯、單元測試和linting。
Grunt入門
過去幾年JavaScript的發展速度令人震驚,不管是像Backbone.js和Ember.js這樣的開發框架,還是JS Bin這樣的開發社群,這個語言的發展都不僅改變了我們作為使用者對網站的體驗,還改變了我們作為開發者對網站的開發方式。
使用JavaScript,你往往需要定期去執行一系列的任務, 在大部分專案里人們可能對此已習以為常了,但它們仍然是重複的、浪費時間的活計。身處一個如此活躍的開發社群,你大概已經猜到,有現成的工具可以幫你自動化和加速完成這些任務了——這就是Grunt的用武之地。
Grunt 是什麼?
Grunt基於Node.js之上,是一個以任務處理為基礎的命令列工具,可以減少優化開發版本為釋出版本所需的人力和時間,從而加速開發流程。它的工作原理是把這些工作整合為不同的任務,在你開發時自動執行。基本上,你可以讓Grunt完成任何讓你厭煩的任務:那些你需要重複進行的手工設定和執行釋出的任務。
早期版本的Grunt自帶JSHint和Uglify等plugin,最新的版本(version 0.4)則完全依賴使用者指定plugin來執行任務。到底有哪些任務可以執行呢?這個單子可是長得很,可以說,Grunt能幹任何你扔給它的活,從極簡化(minifying) 到合併JavaScript (concatenating)。它還可以完成一些跟JavaScript無關的任務,比如從LESS和Sass編譯CSS。我們甚至還用過它跟 blink(1) 來做編譯失敗的提醒。
為什麼要用Grunt ?
Grunt最大的優勢之一是給團隊帶來一致性。如果你和別人一起工作過,你肯定知道程式碼風格的不一樣有多讓人傷神。Grunt能讓團隊使用一套統一的命令,從而保證每個人寫的程式碼符合統一標準。說到底,如果因為團隊中幾個人程式碼風格的微小不同而導致編譯失敗,那可是最煩人的事了。
Grunt還有一個極其活躍的開發者社群,定期釋出新的plugin。使用Grunt的門檻也相對較低,因為很多工具和自動化任務都是直接可用的。
設定安裝
要使用Grunt,第一件事是安裝Node.js。(即使你沒用過Node.js也不用擔心——你只需安裝它讓Grunt能執行。)
安裝了Node.js之後,用命令列工具執行以下命令:
1 |
$ npm install -g grunt-cli |
要確認Grunt是否正確安裝,可以使用以下命令:
1 |
$ grunt --version |
下一步是在你的專案的根目錄下建立package.json和gruntfile.js兩個檔案。
建立package.json檔案
這個JSON檔案讓我們指定我們的開發環境所依賴的必須模組。有了它,專案的所有開發者都能保證安裝上一致的必須模組,從而保證所有人擁有一樣的開發環境。
在專案根目錄下的pacakge.json檔案中寫上:
1 2 3 4 5 6 7 8 9 10 |
{ "name" : "SampleGrunt", "version" : "0.1.0", "author" : "Brandon Random", "private" : true, "devDependencies" : { "grunt" : "~0.4.0" } } |
然後在命令列工具執行:
1 |
$ npm install |
該命令告訴npm 需要安裝的必須模組,npm會安裝它們,自動儲存在專案根目錄下一個叫做 node_modules 的資料夾裡。
建立gruntfile.js檔案
gruntfile.js 檔案本質上就是一個wrapper函式,接受grunt作為引數:
1 2 3 4 5 6 7 8 9 |
module.exports = function(grunt){ grunt.initConfig({ pkg: grunt.file.readJSON('package.json') }); grunt.registerTask('default', []); }; |
現在你已經可以在專案根目錄下執行grunt命令列工具了。
1 2 |
$ grunt > Task "default" not found. Use --force to continue. |
這裡我們只指定了Grunt作為必須模組,還沒定義任何任務。接下來我們就要指定任務和必須模組。首先來看如何擴充package.json檔案。
擴充package.json檔案
使用Node.js最好的一點,就是它可以根據package.json檔案的內容,一次性查詢和安裝多個package。要安裝我們專案的所有必須任務,只須在package.json檔案中加上以下內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "name" : "SampleGrunt", "version" : "0.1.0", "author" : "Mike Cunsolo", "private" : true, "devDependencies" : { "grunt" : "~0.4.0", "grunt-contrib-cssmin": "*", "grunt-contrib-sass": "*", "grunt-contrib-uglify": "*", "grunt-contrib-watch": "*", "grunt-cssc": "*", "grunt-htmlhint": "*", "matchdep": "*" } } |
那麼如何實現安裝?你肯定已經猜到了:
1 |
$ npm install |
使用Grunt載入任務
package安裝好後,還必須被Grunt載入才能為我們所用。使用 matchdep,我們用一行程式碼就可以自動載入所有任務。這是開發流程的一大優化,因為現在我們只須把必須任務列表寫在package.json一個檔案裡,便於管理。
在gruntfile.js裡,grunt.initConfig之上,寫上以下代碼:
1 |
require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks); |
要是沒有matchdep,我們就必須為每一個必須任務寫一次grunt.loadNpmTasks(“grunt-task-name”); ,隨著我們使用的任務的增加,這些載入任務的程式碼很快就會變得相當繁冗。在Grunt載入這些任務前,我們還可以指定設定選項。
現在我們需要建立我們的HTML檔案(index.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 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;"> <title>Enter your first name</title> <link rel="stylesheet" href="build/css/master.css"> </head> <body> <label for="firstname">Enter your first name</label> <input id="firstname" name="firstname" type="text"> <p id="namevalidation" class="validation"></p> <script type="text/javascript" src="build/js/base.min.js"></script> </body> </html> |
用HTMLHint驗證HTML
在grunt.initConfig里加入下列設定程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
htmlhint: { build: { options: { 'tag-pair': true, 'tagname-lowercase': true, 'attr-lowercase': true, 'attr-value-double-quotes': true, 'doctype-first': true, 'spec-char-escape': true, 'id-unique': true, 'head-script-disabled': true, 'style-disabled': true }, src: ['index.html'] } } |
一般來說,一個plugin的設定方法如下:plugin的名稱(去掉grunt-contrib-或grunt-前綴),選擇使用此plugin的一個或多個物件(在這裡可以給不同檔案設定此plugin 的不同選項),一個選項object,以及plugin要作用的物件。現在,如果我們用命令列工具執行grunt htmlhint,該plugin就會檢查我們在src裡指定的HTML檔案,驗證其中有沒有錯誤!但是每個小時都要手動執行幾次這個任務,很快就讓人覺得很繁瑣了。
自動化任務執行
watch是一個特殊的任務,它可以在目標檔案儲存時自動觸發一系列任務的執行。在grunt.initConfig里加入以下設定:
1 2 3 4 5 6 |
watch: { html: { files: ['index.html'], tasks: ['htmlhint'] } } |
然後,在命令列工具中執行grunt watch命令。現在,你可以試試在index.html里加一行注釋,儲存檔案。你會注意到,儲存檔案時會自動觸發HTML的驗證!這是對開發流程的一大優化:在你寫程式碼時,watch任務就會默默同時為你驗證程式碼,如果驗證失敗任務就會報告失敗(它還會告訴你問題在哪)。
注意grunt watch任務會一直執行,直到命令列工具關閉,或手動停止(control+c在Mac中)。
保持JavaScript極簡
讓我們來寫一個驗證使用者輸入的名字的JavaScript檔案。簡便起見,我們這裡只檢查其中是否含有非字母的字元。我們的JavaScript會使用strict模式,這可以防止我們寫可用但低質量的JavaScript。建立assets/js/base.js檔案並在其中寫上:
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 |
function Validator() { "use strict"; } Validator.prototype.checkName = function(name) { "use strict"; return (/[^a-z]/i.test(name) === false); }; window.addEventListener('load', function(){ "use strict"; document.getElementById('firstname').addEventListener('blur', function(){ var _this = this; var validator = new Validator(); var validation = document.getElementById('namevalidation'); if (validator.checkName(_this.value) === true) { validation.innerHTML = 'Looks good! :)'; validation.className = "validation yep"; _this.className = "yep"; } else { validation.innerHTML = 'Looks bad! :('; validation.className = "validation nope"; _this.className = "nope"; } }); }); |
讓我們用UglifyJS來極簡化這個原始碼,在grunt.initConfig中加上以下設定:
1 2 3 4 5 6 7 |
uglify: { build: { files: { 'build/js/base.min.js': ['assets/js/base.js'] } } } |
UglifyJS會替換所有的變數和函式名,剔除所有空白和註釋,從而生成佔據最小空間的JavaScript檔案,對釋出非常有用。同樣地,我們需要設定一個watch任務來使用它,在watch的設定里加入以下程式碼:
1 2 3 4 5 6 |
watch: { js: { files: ['assets/js/base.js'], tasks: ['uglify'] } } |
從Sass原始檔生成CSS
Sass對CSS相關工作非常有用,特別是在團隊中。使用Sass可以大量減少代碼量,因為Sass可通過變數、mixin函式生成CSS代碼。Sass的具體使用方法並不在本教程探討的範圍內,所以如果你還不想使用Sass這樣的CSS前處理器,可以跳過這一段。但我們這裡會介紹一個很簡單的用例,使用變數、一個mixin函式和Sass式CSS語法(SCSS)。
Grunt的Sass plugin需要使用Sass gem,為此你需要安裝Ruby(OS X中已經預裝了Ruby)。用以下命令你可以測試系統中是否已安裝Ruby:
1 |
ruby -v |
使用以下命令安裝Sass gem:
1 |
gem install sass |
根據系統設定的不同,你可能需要用sudo來執行此命令——即sudo gem install sass——這裡你會被要求輸入管理者密碼。安裝好Sass,在assets檔案夾裡建立sass檔案夾,並在其中建立檔案master.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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
@mixin prefix($property, $value, $prefixes: webkit moz ms o spec) { @each $p in $prefixes { @if $p == spec { #{$property}: $value; } @else { -#{$p}-#{$property}: $value; } } } $input_field: #999; $input_focus: #559ab9; $validation_passed: #8aba56; $validation_failed: #ba5656; $bg_colour: #f4f4f4; $box_colour: #fff; $border_style: 1px solid; $border_radius: 4px; html { background: $bg_colour; } body { width: 720px; padding: 40px; margin: 80px auto; background: $box_colour; box-shadow: 0 1px 3px rgba(0, 0, 0, .1); border-radius: $border_radius; font-family: sans-serif; } input[type="text"] { @include prefix(appearance, none, webkit moz); @include prefix(transition, border .3s ease); border-radius: $border_radius; border: $border_style $input_field; width: 220px; } input[type="text"]:focus { border-color: $input_focus; outline: 0; } label, input[type="text"], .validation { line-height: 1; font-size: 1em; padding: 10px; display: inline; margin-right: 20px; } input.yep { border-color: $validation_passed; } input.nope { border-color: $validation_failed; } p.yep { color: $validation_passed; } p.nope { color: $validation_failed; } |
你會注意到SCSS比起普通的Sass更像CSS。這個樣式表使用了Sass的兩個特性:mixin和變數。一個mixin根據給它的引數生成CSS代碼塊,很像函式。而一個變數可以用來定義一段CSS代碼片段,然後在很多地方重用。變數對定義Hex顏色尤其有用,我們可以用它建立一個色表,然後在嘗試不同設計時,只須修改一處程式碼,從而大大提高了效率。這裡的mixin則用來給CSS3的apperance和transition等屬性生成前綴,減少了冗餘程式碼。編寫一個很長的樣式表檔案時,任何減少程式碼量的方法,都會讓團隊中日後更新此樣式表的人受益。
在Sass之外,grunt-cssc任務可以整合CSS檔案中定義的樣式規則,最大限度削減所生成的CSS檔案中的重復內容。在中到大型專案中經常出現重複的樣式規則,使用這個任務就很有益處。但是,由此生成的CSS檔案也不一定就是最簡的,所以我們還需要使用cssmin任務,它既能剔除所有空白,還能把顏色值替換為可能的最簡形式(比如white會被替換為#fff)。在gruntfile.js中加入如下內容:
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 |
cssc: { build: { options: { consolidateViaDeclarations: true, consolidateViaSelectors: true, consolidateMediaQueries: true }, files: { 'build/css/master.css': 'build/css/master.css' } } }, cssmin: { build: { src: 'build/css/master.css', dest: 'build/css/master.css' } }, sass: { build: { files: { 'build/css/master.css': 'assets/sass/master.scss' } } } |
現在我們設定好了處理樣式表的任務,還要讓它們自動執行。Grunt自動建立了build檔案夾來存放所有的釋出用script、CSS和(如果這是一個完整的網站專案的話)壓縮後的圖片檔案。這意味著assets檔案夾裡可以包含為開發而做的詳細的註釋檔案甚至說明文件,而build檔案夾裡則只會包含極簡化程式碼和優化壓縮過的影像檔案。
我們給CSS相關的工作定義一套新的任務,在gruntfile.js裡的default task下面加上以下內容:
1 |
grunt.registerTask('buildcss', ['sass', 'cssc', 'cssmin']); |
現在,執行grunt buildcss任務就會按順序執行所有CSS相關的任務,比起分別執行grunt sass、grunt cssc然後grunt cssmin來,這樣簡潔多了。最後我們需要做的就是更新watch任務的設定,讓這些CSS相關任務也能自動執行:
1 2 3 4 5 6 |
watch: { css: { files: ['assets/sass/**/*.scss'], tasks: ['buildcss'] } } |
這個路徑可能看起來有點奇怪,它的用途是遞迴地遍歷我們assets/sass檔案夾裡的所有檔案和子資料夾,來查詢.scss檔案。如此一來,我們就可以建立任意多的.scss檔案,而不需要在gruntfile.js裡新增它們的路徑。現在,你的gruntfile.js應該是下面這樣:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
module.exports = function(grunt){ "use strict"; require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), cssc: { build: { options: { consolidateViaDeclarations: true, consolidateViaSelectors: true, consolidateMediaQueries: true }, files: { 'build/css/master.css': 'build/css/master.css' } } }, cssmin: { build: { src: 'build/css/master.css', dest: 'build/css/master.css' } }, sass: { build: { files: { 'build/css/master.css': 'assets/sass/master.scss' } } }, watch: { html: { files: ['index.html'], tasks: ['htmlhint'] }, js: { files: ['assets/js/base.js'], tasks: ['uglify'] }, css: { files: ['assets/sass/**/*.scss'], tasks: ['buildcss'] } }, htmlhint: { build: { options: { 'tag-pair': true, // Force tags to have a closing pair 'tagname-lowercase': true, // Force tags to be lowercase 'attr-lowercase': true, // Force attribute names to be lowercase e.g. <div id="header"> is invalid 'attr-value-double-quotes': true, // Force attributes to have double quotes rather than single 'doctype-first': true, // Force the DOCTYPE declaration to come first in the document 'spec-char-escape': true, // Force special characters to be escaped 'id-unique': true, // Prevent using the same ID multiple times in a document 'head-script-disabled': true, // Prevent script tags being loaded in the for performance reasons 'style-disabled': true // Prevent style tags. CSS should be loaded through }, src: ['index.html'] } }, uglify: { build: { files: { 'build/js/base.min.js': ['assets/js/base.js'] } } } }); grunt.registerTask('default', []); grunt.registerTask('buildcss', ['sass', 'cssc', 'cssmin']); }; |
現在我們有了一個靜態HTML頁面,一個存放Sass和JavaScript原始檔的assets檔案夾,一個存放優化後的CSS和JavaScript的build檔案夾,以及package.json檔案和gruntfile.js檔案。
至此你已經有了一個不錯的基礎來進一步探索Grunt。像之前提到的,一個非常活躍的開發者社群在為Grunt開發前端plugin。我建議你現在就到plugin library 去看看那300個以上的plugin。