如何構建自動化的前端開發流程

劉暘發表於2013-09-03

  如今的前端開發中,已經不再只是一些簡單的靜態檔案了,對於很多Web App來說,前端程式碼甚至比後端程式碼要更加複雜,更加難於管理,例如:

  • 我們有許多的第三方庫的依賴需要管理;
  • 我們有獨立的前端測試需要自動執行;
  • 我們還有很多程式碼需要在釋出時進行打包壓縮;
  • ⋯⋯

  所以構建一個自動化的前端開發流程是非常必要的,但現在前端開發流程的構建是百花齊放,沒有一個統一的標準,還有很多依賴於後端的架構來做前端開發管理。例如在Rails開發中,就有各種前端庫的gem包。但是這種依賴於後端框架的管理方式有許多問題:

  • 許多gem包的維護者並不是前端庫的維護者,所以更新不一定即時;
  • 不利於前端程式碼與後端程式碼做分離;
  • 增加了前端開發者的學習和使用成本;
  • ⋯⋯

  於是現在出現了一些不依賴於後端程式碼(雖然還是要依賴Node.js⋯⋯)的管理工具,對於前端開發者非常友好,例如:YEMANJamvolocomponentBrunch⋯⋯但是這些工具都或多或少有自己的一些問題,所以我決定用一些更輕量的工具(bowergrunt)來搭建自己的前端開發流程。本文的例子來自本人正在開發的一個專案,可以在github上檢視所有的程式碼

  什麼是開發流程?

  在我看來一個完整的開發流程應該包括:

  • 本地開發環境的初始化
  • 第三方依賴的管理
  • 原始檔編譯
  • 自動化測試
  • 釋出到pipeline和各個環境

  而現代的開發流程,就是要使上面的各個部分都可以自動化,一個命令就可以使這些流程都自動走完,並且快速的得到錯誤或通過的反饋,讓我們可以方便快速的修復錯誤和release。

  本地開發環境的初始化

  這裡我使用的工具是Node.jsNPM,它們都基於JavaScript,使用Json來配置,對於前端開發人員非常友好。

  安裝完成Node.js和NPM後,在專案根目錄下建立NPM的配置檔案package.json:

{
    "name": "Project Name",
    "version": "0.0.1",
    "description": "Project Description",
    "repository": {
        "type": "git",
        "url": "git://github.com/path/to/your_project"
    },
    "author": "Author Name",
    "license": "BSD",
    "readmeFilename": "README.md",
    "gitHead": "git head",
    "devDependencies": {
        "grunt": "latest",
        "grunt-contrib-connect": "latest",
        "grunt-contrib-concat": "latest",
        "grunt-contrib-jasmine": "latest",
        "grunt-contrib-watch": "latest",
        "grunt-contrib-compass": "latest"
    }
}

  其中最重要的一個配置項是devDependencies,這是用於開發的依賴,例如:自動化測試、原始檔編譯等等,其中各個依賴的作用和用法將會在後面講到。而前端生產程式碼的依賴會使用另一個工具來管理,也在後面講到。建立完成以後執行npm install,NPM就會將這些依賴都安裝到專案根目錄的node_modules資料夾中。

  第三方依賴的管理

  這裡我使用的工具是bower。其實NPM也可以管理,但是NPM並不是讀取第三方依賴原始的repository,而是讀取自己管理的一個repository,所以更新可能會慢點,並且它使用CommonJS的介面方便Node.js專案的開發,並不是針對純前端開發的專案;而bower是讀取原始的github repository,沒有更新延遲的問題,所有包都是針對純前端開發專案的。

  要使用bower只需要簡單的三步:

  1. 安裝:npm install bower -g
  2. 在專案根目錄中建立配置檔案.bowerrc
  3. 在專案根目錄中建立依賴配置檔案components.json

  我們首先來看看.bowerrc的內容:

 {
    "directory" : "components",
    "json"      : "component.json",
    "endpoint"  : "https://bower.herokuapp.com"
}

  其中directory指定了所有的依賴會被安裝到哪裡;json指定了依賴配置檔案的路徑;endpoint制定了依賴的repository的定址伺服器,你可以替換為自己的定址伺服器。

  然後我們來看看components.json的內容:

 {
    "name": "Project Name",
    "version": "0.0.1",
    "dependencies": {
      "jquery": "latest",
      "underscore": "latest",
      "backbone": "latest",
      "jasmine-jquery": "latest",
      "jasmine-ajax": "git@github.com:pivotal/jasmine-ajax.git"
    }
}

  其中最重要的就是dependencies,它指定了所有前端開發依賴的包。所有bower包含的依賴都可以在這裡查到,對於bower沒有包含的依賴也可以直接指定github的repository,例如:"jasmine-ajax": "git@github.com:pivotal/jasmine-ajax.git"。

  最後執行bower install就可以在components資料夾中看到所有第三方依賴的檔案了。但是bower有一個問題,就是它將所有github repository中的檔案都下載下來了,其中有許多是我們不需要的檔案。下面我們會將我們需要的檔案提取出來打包放到我們指定的目錄中。

  原始檔編譯

  這裡我使用的工具是grunt,他本身主要是基於Node.js的檔案操作包,其中有許多外掛可以讓我們完成js檔案的compile和compress、sass到css的轉換等等操作。要使用它需要先安裝命令列工具:npm install grunt-cli -g,然後在專案根目錄中建立檔案Gruntfile.js,這個檔案用於定義各種task,我們首先定義一個task將從bower下載的第三方依賴都打包到檔案app/js/lib.js中:

module.exports = function(grunt) {
    var dependencies = [
        'components/jquery/jquery.js',
        'components/underscore/underscore.js',
        'components/backbone/backbone.js'];
    grunt.initConfig({
        concat: {
            js: {
                src: dependencies,
                dest: 'app/js/lib.js'
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-concat');
};

  這裡的grunt-contrib-concat就是grunt的一個外掛,用於檔案的合併操作,我們已經在前面的package.json中引入了。js是task name;src指定了合併的原始檔地址;dest指定了合併的目標檔案。這樣當我們執行grunt concat:js後,所有的依賴檔案都會被合併為app/js/lib.js。這樣做的好處是我們可以控制每個依賴的引入順序,但是麻煩的是每次引入新的依賴都需要手動加入到dependencies陣列中。這個暫時沒有更好的解決方案,因為不是所有的包都在自己的components.js中宣告瞭main檔案,很多時候必須自己手動指定。

  JavaScript檔案編譯完成以後就是CSS檔案,在現代的前端開發中,我們已經很少直接寫CSS檔案了,一般都使用SASS或者LESS。grunt也提供了這種支援,這裡我使用的是grunt-contrib-compass

module.exports = function(grunt) {
    var sasses = 'sass';
    grunt.initConfig({
        compass: {
            development: {
                options: {
                    sassDir: sasses,
                    cssDir: 'app/css'
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-compass');
};

  然後執行grunt compass:development就可以完成CSS檔案的編譯了。

  自動化測試

  這裡我使用的自動化測試工具是Jasmine,它grunt中同樣有一個外掛:grunt-contrib-jasmine。下面我們來看看如何在Gruntfile.js中定義測試的task:

module.exports = function(grunt) {
    var sources = 'app/js/**/*.js',
        specs = 'spec/**/*Spec.js';
    grunt.initConfig({
        jasmine: {
            test: {
                src: [sources],
                options: {
                    specs: specs,
                    helpers: ['spec/helper/**/*.js'],
                    vendor: 'app/js/lib.js'
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-jasmine');
};

  配置完成以後就可以執行grunt jasmine:test來跑測試,但問題是每次寫完程式碼都要手動執行一次非常麻煩,最好可以每次程式碼有更改都自動跑一次,讓我們可以更快的得到反饋。grunt的watch外掛就提供了這種支援:

module.exports = function(grunt) {
    var sources = 'app/js/**/*.js',
        specs = 'spec/**/*Spec.js';
    grunt.initConfig({
        jasmine: {
            test: {
                src: [sources],
                options: {
                    specs: specs,
                    helpers: ['spec/helper/**/*.js'],
                    vendor: 'app/js/lib.js'
                }
            }
        },
        watch: {
            test: {
                files: [sources, specs],
                tasks: ['jasmine:test']
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-jasmine');
    grunt.loadNpmTasks('grunt-contrib-watch');
};

  files指定了需要監聽變動的檔案;tasks指定了修改後自動觸發的task。現在只要我們執行grunt watch:test,那麼有任何原始檔、測試檔案的改動,Jasmine測試都會自動執行了。有時候我們也希望測試的結果顯示在網頁上,便於我們做js的除錯。那麼可以將tasks:['jasmine:test']改為tasks: ['jasmine:test:build'],然後開啟根目錄下的_SpecRunner.html檔案,就可以在網頁中看到測試結果了,再加上一些Chrome的Livereload外掛,就可以不用重新整理實時的看到測試結果,效率非常之高。雖然grunt外掛中也有livereload,但是與grunt-contrib-watch無法很好的整合,所以我沒有使用這種方式。

  CI Pipeline

  由於我的專案是host在github上,所以我選擇travis-ci作為我的CI伺服器。要啟用travis-ci需要以下幾步:

  1. travis-ci中註冊一個賬號,獲取一個token;
  2. 在你的github專案的Settings–>Service Hooks中找到Travis,填入token並且啟用;
  3. 回到travis-ci,在Accounts–>Repositories中開啟你的專案的service hook
  4. Push一個.travis.yml到github,觸發第一次build。
  5. 修改package.json的scripts項,指定執行測試的命令

  下面我們來看看如何配置.travis.yml:

language: node_js
node_js:
  - "0.8"
before_script:
  - npm install -g grunt-cli

  由於我們的環境是基於Node.js搭建的,所以在language設定了nodejs;而**nodejs指定了Node.js的版本;before_script**指定了在測試執行前需要執行的命令,由於我們的指令碼都是基於grunt的,所以需要先安裝grunt的命令列包。

  然後再修改package.json:

{
    ⋯⋯
    "scripts": {
        "test": "grunt jasmine:test"
    }
    ⋯⋯
}

  將修改以後的package.jsonpush到github上,再次觸發一個新的build,你可以看到你之前錯誤的build已經綠了。

  這裡還有一個小提示:如何讓build狀態顯示在專案的readme中?很簡單,只需要在README.md中加入以下程式碼就可以了:

[![Build Status](https://travis-ci.org/path/to/your_repository.png?branch=master)](http://travis-ci.org/path/to/your_repository)

  到這裡基本的環境搭建就完成了,當然我們還可以使用grund的registerTask來定義一個任務序列,還可以加入template的編譯⋯⋯這些都可以通過grunt來靈活設定。最重要的是現在別人拿到一個專案的程式碼以後,可以通過一些命令來快速的搭建本地環境,方便的進行測試和開發,而且沒有依賴與後端的開發環境,只要定義好介面,前端開發可以完全獨立開了。雖然這其中還有很多問題沒有解決,例如:

  • 如何讓第三方依賴自申明main檔案
  • package.json與components.json其實有些重複
  • Live Reload還需要Chrome外掛才能完成
  • ⋯⋯

  這正是由於現在前端開發環境還沒有後端開發的那種標準化,也正是挑戰和機遇之所在!

相關文章