之所以想寫有關前端自動化工具的文章出于以下幾個原因:
- 自動化構建工具對于前端開發的重要性:高效、減少重復性操作、各種強大插件的支撐。
- 構建工具的上手使用有一定的成本,其中也有不少坑踩,前端在掌握html/js/css三劍客的同時,還需要了解node.js、npm包管理器、構建工具的配置、語法糖以及插件的使用,也要學會當構建工具的使用日趨復雜龐大的時候如何優雅有效的組織代碼,減少在使用工具的時候出現bug的概率。
- 工作中遇到一些grunt相關的常用實例與奇技淫巧可以拿來品玩、解讀,有助于更快速上手并定制一套強大的自動化工作方式。
- 同類的構建工具例如gulp、webpack(
嚴格意義上它應該是模塊管理工具,但它依舊可以做一些構建的工作
),甚至是揚言可以擯棄grunt與gulp的npm scripts,它們各有各的可取之處,刷新了我對構建工具的認識。而在我看來,與其爭論個孰好孰壞,還不如用上一個自己覺得順手的、更貼合項目需求的工具庫。
自動化構建工具 --- grunt
先說下在沒有誕生這些工具之前寫前端代碼的一些痛點:
- “css寫得好費勁啊,那些可復用的樣式能不能存在一個變量或函數里直接調用啊”
- “樣式里還要記得寫上兼容不同瀏覽器的前綴,ctrl+C/V手好累”
- “更改代碼后每次都要按F5來刷新瀏覽器,如果要進行多臺設備的調試,每臺設備都要手動刷新下,想想都覺得心累~”
- “代碼寫完后要借用工具手動合并、壓縮最后還要自己再拷貝到產品目錄下,每次發布都要進行著重復的操作...”
- “太好了,代碼合并后現在頁面只有一個script標簽了,大幅度減少了請求數,但是卻引入了其他頁面才會使用到的代碼,能不能拆分到它們各自需要的page view里啊...”
- etc...
痛點實在太多,不勝枚舉,小點的項目這么手動折騰下無傷大雅,但是到了大中型的程度依舊這么徒手操作,實在不敢想象。為了讓前端的工作不那么枯燥,各路好漢紛紛支招,在node的光環照耀下,js的構建工具應運而生,逐漸成為前端生態下必不可少的一環。自動化的構建工具就是要讓你在編寫前端代碼的時候對反復重復
枯燥無聊
的工作 say no。
About Grunt
前面扯了那么多閑話,趕緊介紹今天的主角吧。Grunt,(說實話第一眼看到這個單詞我竟然想到的是魔獸爭霸里我獸族的大G~
) 為什么要選擇用grunt來作為首選的構建工具呢,首先還是因為個人比較熟悉吧,也是用到的第一個構建框架,其次借用下官方說的推薦緣由:
Grunt生態系統非常龐大,并且一直在增長。由于擁有數量龐大的插件可供選擇,因此,你可以利用Grunt自動完成任何事,并且花費最少的代價。如果找不到你所需要的插件,那就自己動手創造一個Grunt插件,然后將其發布到npm上吧。
---- from grunt 官網介紹
是的,截止到目前為止grunt的插件數目已經達到5,500多個,擁有了這些插件就好比擁有了一把瑞士軍刀,正所謂工欲善其事必先利其器
,有關grunt的基本安裝、配置、注冊任務、etc..就不在此多做介紹,詳情可以參照官網的快速入門指南,讓我們看下插件TOP100里,grunt是如何讓我們的武器更加鋒利無比的。
Grunt的基本套裝
grunt自家利器:(grunt官方維護的插件)
包名稱 | 說明 |
---|---|
contrib-watch | 監視文件的變化,可以指定發生變化時執行的任務 |
contrib-clean | 清楚指定目錄下的文件 |
contrib-jshint | js語法規范提示,可以將規范寫入配置文件,對不符合規范的代碼予以提示 |
contrib-copy | 拷貝文件到指定目錄 |
contrib-uglify | 壓縮指定的js代碼 |
contrib-concat | 合并指定的js or css代碼 |
contrib-cssmin | 壓縮指定的css代碼 |
contrib-less | 將less文件編譯為css |
contrib-htmlmin | 壓縮指定的html代碼 |
contrib-imagemin | 壓縮指定的圖片 |
家常必備神器:(常用的第三方插件,配合官方插件效果更佳)
包名稱 | 說明 |
---|---|
postcss | css預處理工具,可以實現less or scss or stylus的css預處理器效果,也可以借助其強大的auto-prefix插件來為css代碼自動添加兼容性瀏覽器廠商前綴 |
babel | ES6語法轉為ES5 js轉換器 |
sync | 類似contrib-copy,但只是拷貝那些被更改過的文件 |
webpack | 強大的模塊管理工具,其極具特色的loader功能可以讓你在js代碼里引入幾乎任何類型文件 |
jsdoc | 通過寫遵循約定好的語法格式的注釋而自動生成文檔的grunt插件 |
sails-linker | 將css or js(一個或多個)文件自動插入到頁面的指定位置 |
assets-linker | 類似sails-linker,但其配置語法更為簡潔 |
browser-sync | 一個支持在多個設備間同步測試與調試的輕量版http開發服務器 |
time-grunt | 可以直觀的看到每個grunt task的耗時,可以有效的優化構建工具 |
grunt-cdn | 指定cdn路徑,為css、js資源添加cdn路徑 |
load-grunt-configs | 可以將注冊好的各個grunt task拆分到單獨的文件里,在tasks數目比較大的時候能更方便組織與管理 |
load-grunt-tasks | 自動將各個task載入到grunt.loadNpmTasks中,節省代碼量 |
grunt全家桶的運用場景
在此,假定你已經掌握如何安裝grunt、配置package.json文件、使用grunt插件以及注冊grunt task等一系列基本操作,如果還是不太清楚請猛戳 官方介紹。緊接上面介紹的十幾款常用的grunt插件,我想從項目的兩種模式(開發與產品)里詳細的列出它們的使用場景,但在此之前,有必要從一個基礎的項目例子講起,它的目錄架構大體長這樣:
├── your project
│ ├── Gruntfile.js
│ ├── package.json
│ ├── grunt
│ │ ├── watch.js
│ │ ├── clean.js
│ │ ├── ...
│ ├── assets
│ │ ├── js
│ │ │ ├── index.js
│ │ │ ├── ...
│ │ ├── less
│ │ │ ├── index.less
│ │ │ ├── ...
│ ├── www
│ │ ├── js
│ │ │ ├── index.js
│ │ │ ├── ...
│ │ ├── css
│ │ │ ├── index.css
│ │ │ ├── ...
│ ├── build
| | ├── min
│ │ | ├── js
│ │ │ | ├── index.js
│ │ │ | ├── ...
│ │ | ├── css
│ │ │ | ├── index.css
│ │ │ | ├── ...
其中想特別說明的是,在官網介紹的 Gruntfile.js 文件中,grunt 個插件的配置以及task的載入都是類似下面的方式書寫的:
module.exports = function(grunt) {
// Project configuration.
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" 任務的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
// 默認被執行的任務列表。
grunt.registerTask('default', ['uglify']);
};
這其中只是引入了一個任務(uglify)的插件。想象一下,如果有幾十個插件寫入,Gruntfile.js 可就沒那么好看咯。為了能夠單獨拆分每個插件到不同文件,分開管理,這里就需要引入 load-grunt-configs
與 load-grunt-tasks
插件,它們分別實現grunt任務拆分到單獨文件與自動加載包含對應的grunt任務。在它們的幫助下代碼量將極大的減少,并且極大的提高grunt各任務的可維護性。若對比官方的寫法,現在的代碼可以是類似這般的優雅:
module.exports = fucntion(grunt){
var options = {
config : {
src: "grunt/*.js"
}
};
var configs = require('load-grunt-configs')(grunt, options);
grunt.initConfig(configs);
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
}
將各自的grunt任務寫到單獨的js文件里,以 watch task 為例,像這樣:
module.exports.tasks = {
watch: {
js: {
files: [
'assets/js/**/*.js',
'routes/**/*.js'
],
tasks: ['copy:dev'],
options: {
livereload: true
}
},
less: {
files: ['assets/styles/**/*.less'],
tasks: ['less:dev', 'postcss'],
options: {
livereload: true
}
},
view: {
files: ['templates/**/*'],
options: {
livereload: true
}
}
}
};
把這些文件都放在 grunt 目錄下,再在 load-grunt-configs 的 options 配置里指定好grunt目錄位置,就可以輕松實現grunt任務寫入單獨文件。而通過 load-grunt-tasks,我們只需要一行代碼:
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
就可以代替如下 n 行!
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-recess');
grunt.loadNpmTasks('grunt-sizediff');
grunt.loadNpmTasks('grunt-svgmin');
grunt.loadNpmTasks('grunt-styl');
grunt.loadNpmTasks('grunt-php');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-bower-requirejs');
...
而另一個可以在 Gruntfile.js 中配合使用的插件 --- time-grunt,它可以非常直觀的輸出每個grunt task的耗時以便你可以針對某項task做好構建時間的優化,如下所示:
在開發模式下使用grunt
開發模式下的grunt任務主要包括源碼預編譯、代碼修飾、代碼規范檢查、代碼tag的自動注入等,這些如同為你配備了一把全能的瑞士軍刀般的體驗完全可以解決之前提到的諸多痛點,結合grunt全家桶,下面一一介紹如何配置好一套適用于開發環境下的自動化流程:
首先回到之前的項目目錄,可以看到分別有assets、www、build三個包含了類似文件的目錄,
- assets 用于存放項目前端代碼的源碼
- www 里包含了編譯、修飾過的、可供本地調試服務器上的網頁直接訪問的代碼與靜態資源
- build 則是包含了產品模式下的所有打包過的代碼與資源,用于放在cdn服務下
之所以這么劃分是為了讓grunt的職責與分工更加明確,也方便兩種模式下的輕松切換與管理。
進入正題,下面列出的是一套開發模式下常用到的任務列表:
[
'clean',
'less',
'postcss',
'jshint',
'copy',
'asset-linker',
'browserSync',
'watch'
]
以上任務轉換為自然語言就是:
- 首先清空目標目錄,確保下次再執行grunt任務時清空上次任務生成的文件,寫入一個干凈的目錄下
- 將less(css預編譯語言,此處也可以是scss、stylus)編譯成css
- 修飾編譯好的css(例如簡化后的css、通過auto-prefix添加過兼容性前綴的css)
- 檢測js代碼的規范性,是否有書寫有誤,是否足夠規范
- 將處理好的代碼拷貝到目標目錄(例如www、build)
- 自動添加link、script標簽到html或模板文件下
- 檢測指定目錄下的文件,如有任何修改,則自動刷新瀏覽器,修改效果所見即所得
接下來,我們只需要把這一些列任務注冊到grunt dev
這個指令下,每個任務按照排列的先后順序依次執行:
grunt.registerTask('dev', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev',
'browserSync',
'watch'
]);
PS:大部分grunt任務都是支持多線程的,即每個grunt任務下可以同時運行多個子任務,也可以單獨只運行某個子任務,像'clean:dev',就運行了clean下的dev子任務。因此這里可以根據環境來分為dev與build
為了更直觀的了解grunt任務的子任務,舉個栗子就好啦:
module.exports.tasks = {
clean: {
dev: ['www'],
build: ['build-res']
}
};
注:當我們在terminal輸入grunt clean
時,默認會執行clean下的所有子任務:dev與build
在上面的例子里通過registerTask注冊過的任務集群,我們只要在終端輸入grunt dev
,剩下的事就交給工具自行處理即可
在產品模式下使用grunt
在我看來,產品模式較之開發模式顯得更為嚴謹與精簡。開發模式講究的是開發者可以快速的調試與追蹤自己的代碼以及代碼變更產生的所見即所得的效果,為的是更高效、更便捷的完成功能點的開發與測試。而產品模式則要求原來在開發模式下的代碼更少出現錯誤、更小的體積(文件大小)更適于網絡傳播,不僅如此,產品模式還需要考慮到每次發布版本的時候,通過加入代碼的版本號,來保證版本更新的平滑過渡,而接下來,就一步步來介紹如何讓grunt為我們處理好這一切:
先獻出一份產品模式下的tasks list:
grunt.registerTask('build', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev',
'cdn',
'concat',
'uglify',
'cssmin',
'asset-linker:linkCssProd',
'asset-linker:linkJsProd',
'clean:build',
'copy:build'
]);
可以發現這份列表基本囊括了開發模式下的任務,為此我們可以把這部分共有的task單獨注冊到一個叫做compileAssets里:
grunt.registerTask('compileAssets', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev'
]);
grunt.registerTask('dev', [
'compileAssets',
'browserSync',
'watch'
]);
grunt.registerTask('build', [
'compileAssets',
'cdn',
'concat',
'uglify',
'cssmin',
'asset-linker:linkCssProd',
'asset-linker:linkJsProd',
'clean:build',
'copy:build'
]);
添加版本號
眾所周知,每個項目中的package.json都有一個version的字段來表明項目的版本號,而我們要做的就是把這個版本號添加到相關的任務中:
相關任務
- cdn
- asset-linker
- copy
關于添加版本號的位置,我們可以把版本號添加到文件的末尾處,例如index.1.0.0.js
,但是仔細想下,發布版本時,為了能保證新舊版本的文件可以同時保留到線上,一定會出現一個文件夾下有好多個帶版本號的文件(當你保留的版本號比較多的時候),這樣很顯然不方便整理,為此最明智的選擇是把版本號放到根目錄下,例如http://your-web-site/1.0.1/index.js
,如此一來一個版本就是一個目錄,既美觀又方便版本管理,想刪掉其中一個版本,只要把整個目錄除去掉即可。
gulp 與 npm scripts
本來這篇文章只想介紹grunt的內容,但既然大家都是自動化構建工具,也就不得不把這倆貨搬出來聊聊。又因為前一陣子讀到一篇《我為何放棄Gulp與Grunt,轉投npm scripts》的譯文,可謂大開眼界,茅塞頓開,醍醐灌頂,心鄰神會,如沐春風,不明覺厲... 既然都寫到這了就簡單介紹下兩者吧
gulp
gulp給我最大的感受就是:
- 配置代碼更簡潔、更直觀
- 基于node.js的streams流工作方式,使其處理任務速度更快
gulp允許你把源文件灌入到管道內,期間可以配置一系列插件對管道內的文件逐一處理,最后輸出到目標位置。像是工廠里的流水線一樣,gulp直接把上一個流水線任務完成的output作為下一個流水線任務的input,這就意味著相比grunt而言,我們不需要在每個grunt任務里指定這個任務的input與output,這樣就節省很多代碼,說再啰嗦也敵不過一個赤裸裸的例子擺在你的面前:
Grunt
sass: {
dist: {
options: {
style: 'expanded'
},
files: {
'dist/assets/css/main.css': 'src/styles/main.scss',
}
}
},
autoprefixer: {
dist: {
options: {
browsers: [
'last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'
]
},
src: 'dist/assets/css/main.css',
dest: 'dist/assets/css/main.css'
}
},
grunt.registerTask('styles', ['sass', 'autoprefixer']);
讓我們看下同樣的配置在Gulp下是怎么實現的:
Gulp
gulp.task('sass', function() {
return sass('src/styles/main.scss', { style: 'expanded' })
.pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
.pipe(gulp.dest('dist/assets/css'))
});
有木有一種眼前一亮的感覺!這確實會讓不少grunt的老玩家會毅然決定跳到gulp圈里。有關Gulp的配置與入門教程,可以參考這篇非常棒的入門文章,以上的代碼例子也是引用這篇好文(好學生要注明摘要出處,尊重版權)
--- Getting started with gulp
npm scripts
說實話,看完那篇《我為何放棄Gulp與Grunt,轉投npm scripts》,給我最最最形象的感受是,就像聽到一個大神說,編輯器我只用Vim,容我拜三下。當然啦,總的來說npm scripts大法很好很強大,也需要一定的成本才能練就,就像文中所說的要使用npm scipts可能還需要學會一些命令行的指令與操作,這更像是高級玩家玩的游戲,一下post出一些文中提到的其強大之處:
npm scripts本身其實是非常強大的。它提供了基于約定的pre與post鉤子:
{
name: "npm-scripts-example",
version: "1.0.0",
description: "npm scripts example",
scripts: {
prebuild: "echo I run before the build script",
build: "cross-env NODE_ENV=production webpack",
postbuild: "echo I run after the build script"
}
}
此外,還可以通過在一個腳本中調用另一個腳本來對大的問題進行分解:
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"clean": "rimraf ./dist && mkdir dist",
"prebuild": "npm run clean",
"build": "cross-env NODE_ENV=production webpack"
}
}
如果一個命令很復雜,那還可以調用一個單獨的文件:
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"build": "node build.js"
}
}
總結
學會使用恰當的工具來解決問題一定會是一件大快人心的事,也會讓工作變得更有趣、更具可玩性。文中提到的三種自動化構建工具基本是前端工程化工作中必不可少的需要掌握的除js、css、html外的工作技巧。grunt有其龐大的插件在背后支持,可以通過大量組合來支撐更為復雜的構建工作。gulp更符合小而美,快而精,less is more的準則,github上獲得不少的點贊(比grunt多好多!),算是后起之秀。而npm scripts則脫離了一層不必要的抽象,且不需要像grunt和gulp要依賴與其插件作者的維護,直接通過npm的指令即可完成大部分構建工作,為自動化構建流程提供了一種新的思路,有一種返璞歸真的意思。所以,具體真的要選擇哪一種作為工作主打的工具,還是那句話,就用你覺得順手的那個好啦~