為 Gruntfile 帶來超值

如何充分利用建構設定

Paul Bakaus
Paul Bakaus

簡介

對於剛接觸 Grunt 世界的您來說,不妨先從 Chris Coyier 撰寫的極佳文章「Grunt for People Think Like Grunt is Weird and Hard」就是很好的入門篇。Chris 開始介紹後,您應該自行規劃 Grunt 專案,並試吃了 Grunt 的強力方案。

在本文中,我們不會著重介紹許多 Grunt 外掛程式實際對專案程式碼執行的作業,而是 Grunt 的建構程序。我們將提供您以下方面的實用建議:

  • 如何讓 Gruntfile 保持簡潔有序
  • 如何大幅縮短建構時間
  • 以及如何在建構發生時接收通知。

迅速取得免責事項的時間:Grunt 只是用來完成任務的眾多工具之一。如果 Gulp 更是你的風格,那就太棒了!看過上述選項後,如果您仍然想打造自己的工具鍊,那也沒關係!這篇文章擁有龐大的生態系統,並擁有長期的使用者族群,因此選擇將重心放在《Grunt》。

整理 Gruntfile

無論是納入大量 Grunt 外掛程式,還是必須在 Gruntfile 中編寫大量手動工作,都可能很快就會變得十分不便且難以維護。幸好有幾種外掛程式能聚焦於這項問題:讓 Gruntfile 變得簡單明瞭。

The Gruntfile,在最佳化前

以下是在我們對 Gruntfile 進行任何最佳化之前的樣子:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

當你說出「嘿!我本來以為太糟了!這其實是可以維護的!」為了方便起見,我們只納入三個沒有自訂項目的外掛程式。如要使用實際執行的 Gruntfile 建構大小適當的專案,就需要在本文中無限捲動。讓我們來看看能做些什麼!

自動載入 Grunt 外掛程式

在新增要用於專案的新 Grunt 外掛程式時,必須將其以 npm 依附元件新增至 package.json 檔案,然後在 Gruntfile 中載入。針對「grunt-contrib-concat」外掛程式,外掛程式可能如下所示:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

如果你現在透過 npm 解除安裝外掛程式,並更新 package.json,但忘記更新 Gruntfile,建構作業就會中斷。這就是酷炫的 load-grunt-tasks 外掛程式可以派上用場的地方。

在此之前,我們必須手動載入 Grunt 外掛程式,如下所示:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

透過載入衝擊任務,您可以將其收合為下列單行程式碼:

require('load-grunt-tasks')(grunt);

要求這個外掛程式後,它會分析 package.json 檔案,判斷哪些依附元件是 Grunt 外掛程式,並自動全部載入。

將 Grunt 設定分割成不同檔案

load-grunt-tasks 會將您的 Gruntfile 縮減至程式碼大小且複雜一點,但當您設定大型應用程式時,它仍然會成為非常大型的檔案。這時 load-grunt-config 就能派上用場。load-grunt-config 可讓您依工作細分 Gruntfile 設定。此外,它也會封裝 load-grunt-tasks 及其功能!

重要事項:分割 Gruntfile 不一定適用於所有情況。如果您的工作之間有許多共用設定 (例如使用大量的 Grunt 範本),請您多加留意。

使用 load-grunt-config 時,Guntfile.js 會如下所示:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

沒錯,這就是整個檔案!工作設定在哪裡?

在 Gruntfile 的目錄中建立名為 grunt/ 的資料夾。根據預設,外掛程式中該資料夾中的檔案,與要使用的工作名稱相符的檔案。目錄結構應如下所示:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

現在,讓我們直接將每項工作的工作設定放入個別檔案中 (您會看到這些範例大多只是從原本的 Gruntfile 複製而來,並貼至新結構):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

如果 JavaScript 設定區塊不合您意,不妨使用 load-grunt-tasks 語法,這樣就能改用 YAML 或 CoffeeScript 語法。現在以 YAML 編寫最終的必要檔案,也就是「aliases」檔案。這是用來註冊工作別名的特殊檔案,我們必須在 Gruntfile 中透過 registerTask 函式進行這項工作。具體做法如下:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

就是這麼簡單!在終端機中執行下列指令:

$ grunt

如果一切都順利,這項作業現在會查看「預設」工作,並依序執行所有內容。我們現在將主要的 Gruntfile 刪除為三行程式碼,而且不需要處理每項工作設定,也不必將各種設定外流,可以在這裡完成所有步驟。但確定萬事俱備後,還需要花上不少工夫。我們來看看該如何改善。

盡可能縮短建構時間

雖然網頁應用程式的執行階段和載入時間效能比執行建構所需的時間要重要,但緩慢的建構仍然有問題。使用 grunt-contrib-watch 等外掛程式,或在 Git 修訂版本速度夠快後,這會導致執行自動建構作業的難度難以實現,還會產生「懲罰」來實際執行建構作業,因為建構時間越快,工作流程越靈活。如果實際工作環境的執行時間超過 10 分鐘,則只會在絕對需要的情況下執行該版本,而且您會在跑步時喝杯咖啡。這是效率提升工具。我們有進步空間。

只建構實際變更過的版本檔案:grunt-newer

初次建構網站後,您可能在重新建構網站時只碰到幾個檔案。假設在我們的範例中,您變更了 src/img/ 目錄中的圖片 (執行 imagemin 來重新最佳化圖片很合理,但僅對該圖片而言很合理),而且重新執行 concatuglify 只是在浪費寶貴的 CPU 循環。

當然,您也可以一律從終端機執行 $ grunt imagemin,而非 $ grunt,以便僅選擇性執行手中的工作,但也有更聰明的方法。是新興客戶

Grunt-newer 具有本機快取,會在其儲存實際發生變更的檔案資訊,並且只針對實際上發生變化的檔案執行工作。接著來看看啟用方式。

還記得 aliases.yaml 檔案嗎?變更方式:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

改為:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

只要預先在工作前面加上「newer:」,即可先透過新的外掛程式管道前往來源和目的地檔案,接著判定哪些檔案 (如果有的話) 應執行

同時執行多項工作:主動並行

grunt-concurrent 外掛程式是一種在需要相互獨立、花費大量時間的工作時非常實用的外掛程式。這種方法會使用裝置的 CPU 數量,並平行執行多項任務。

最棒的是,其設定也非常簡單。假設您使用 load-grunt-config,建立下列新檔案:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

我們剛剛設定了一個名為「first」和「second」的平行執行測試群組。concat 工作需要先執行,在此期間就沒有其他可以執行的項目。在第二個音軌中,我們同時放入 uglifyimagemin,因為這兩種方式各自獨立,且所需時間也很長。

這本身尚未產生任何作用。我們必須變更 default 工作別名,使其指向並行工作,而非直接工作。以下是 grunt/aliases.yaml 的新內容:

default:
  - 'concurrent:first'
  - 'concurrent:second'

如果您現在重新執行 Grunt 版本,並行外掛程式將先執行 concat 工作,然後在兩個不同的 CPU 核心上產生兩個執行緒,以同時執行 imagemin 和 Uglify。!

但有一個建議:在我們的基本範例中,捕手並行無法大幅加快建構速度。這是因為在不同的執行緒中產生不同的 Grunt 執行個體,造成的負擔是產生至少 300 毫秒的負荷。

花了多少時間?

現在,我們已經將各項任務最佳化,瞭解執行每項任務需要多久時間會很有幫助。幸運的是,您也可以使用 time-grunt 外掛程式。

time-grunt 不是會載入為 npm 工作的傳統外掛程式,而是您直接納入的外掛程式,類似於 load-grunt-config。我們要在 Gruntfile 中新增需要時間碼,就像處理 load-grunt- 設定一樣。Gruntfile 現在看起來會像這樣:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

很抱歉造成你的不便,不過請放心,你可以試著透過終端機重新執行 Grunt,而且每項任務 (以及總版本) 中,你應該可以在執行時間看到格式正確的資訊面板:

收集時間

自動系統通知

現在,您已擁有大幅最佳化的 Grunt 版本,而且以某種方式自動建構 (例如透過觀看檔案,或確認使用修訂版本後),如果系統能在您建構新版本準備好開始使用,或發生任何不良情況時通知您,那就再好不過了?認識 grunt-notify

根據預設,grunt-notify 使用 OS X 或 Windows 的 Growl 通知系統,針對所有 Grunt 錯誤和警告提供自動通知:OS X 或 Windows、Mountain Lion 和 Mavericks’s 通知中心,以及 Notify-send。想取得這項功能的話,你只需要從 npm 安裝外掛程式,並載入 Gruntfile (別忘了,如果您在上述步驟中使用 grunt-load-config,這個步驟就會自動執行!)。

視作業系統而定,其外觀可能有所不同:

Notify

除了錯誤和警告之外,讓我們進行設定,使其在上次工作執行完畢後才執行。假設您使用 grunt-load-config 來分割不同檔案的任務,以下是您需要的檔案:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

在 config 物件的第一層,金鑰必須符合我們要連結任務的名稱。這個範例會在執行 imagemin 工作 (也就是建構鏈中的最後一個工作) 後,立即顯示訊息。

總結

如果您從頂層開始,現在您就是自己最自豪的建構程序。除了具備平行處理和選擇性處理功能,您更是建構程序最為自豪的一員,並在出現問題時通知您。

如果你發現其他能改善 Grunt 功能與其外掛程式的寶石,請告訴我們!在此之前,祝你一切順心!

更新 (2014 年 2 月 14 日):如要取得 Grunt 專案的完整範例副本,請按這裡