【轉】有關 Grunt - 自動化構建工具的奇技淫巧

原文:https://segmentfault.com/a/1190000005029360?utm_source=tuicool&utm_medium=referralphp

之因此想寫有關前端自動化工具的文章出於如下幾個緣由:css

  1. 自動化構建工具對於前端開發的重要性:高效、減小重複性操做、各類強大插件的支撐。html

  2. 構建工具的上手使用有必定的成本,其中也有很多坑踩,前端在掌握html/js/css三劍客的同時,還須要瞭解node.js、npm包管理器、構建工具的配置、語法糖以及插件的使用,也要學會當構建工具的使用日趨複雜龐大的時候如何優雅有效的組織代碼,減小在使用工具的時候出現bug的機率。前端

  3. 工做中遇到一些grunt相關的經常使用實例與奇技淫巧能夠拿來品玩、解讀,有助於更快速上手並定製一套強大的自動化工做方式。node

  4. 同類的構建工具例如gulp、webpack(嚴格意義上它應該是模塊管理工具,但它依舊能夠作一些構建的工做),甚至是揚言能夠擯棄grunt與gulp的npm scripts,它們各有各的可取之處,刷新了我對構建工具的認識。而在我看來,與其爭論個孰好孰壞,還不如用上一個本身以爲順手的、更貼合項目需求的工具庫。android

自動化構建工具 - Grunt

先說下在沒有誕生這些工具以前寫前端代碼的一些痛點:webpack

  • 「css寫得好費勁啊,那些可複用的樣式能不能存在一個變量或函數裏直接調用啊」ios

  • 「樣式裏還要記得寫上兼容不一樣瀏覽器的前綴,ctrl+C/V手好累」git

  • 「更改代碼後每次都要按F5來刷新瀏覽器,若是要進行多臺設備的調試,每臺設備都要手動刷新下,想一想都以爲心累~」github

  • 「代碼寫完後要借用工具手動合併、壓縮最後還要本身再拷貝到產品目錄下,每次發佈都要進行着重複的操做...」

  • 「太好了,代碼合併後如今頁面只有一個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的指令便可完成大部分構建工做,爲自動化構建流程提供了一種新的思路,有一種返璞歸真的意思。因此,具體真的要選擇哪種做爲工做主打的工具,仍是那句話,就用你以爲順手的那個好啦~

相關文章
相關標籤/搜索