基於 Gulp + Browserify 構建 ES6 環境下的自動化前端項目

隨着ReactAngular2Redux等前沿的前端框架愈來愈流行,使用webpackgulp等工具構建前端自動化項目也隨之變得愈來愈重要。鑑於目前業界廣泛更流行使用webpack來構建es6(ECMAScript 2015)前端項目,網上的相關教程也比較多;相對來講使用gulp來構建es6項目的中文教程就比較少。css

通過一段時間的摸索,我以爲其實使用gulp也能夠很方便地構建es6項目。如下是我感受gulpwebpack主要的不一樣之處:html

  • gulp的任務機制和流式管道函數和webpack的配置參數風格有着顯著區別,它能使開發者更清晰地瞭解項目的構建流程。前端

  • 因爲gulp是編程式風格的,因此使用起來可定製化的功能也就更靈活一些,可應對一些構建過程較爲複雜的狀況。react

本文特此給你們介紹下如何使用gulp配合browserify來構建基於es6的前端項目。webpack

Browserify vs Webpack

browserifywebpack都是當下流行的commonjs模塊(或es6模塊)合併打包工具,打包後的js文件能夠直接運行在瀏覽器環境中。git

不少人都知道,webpack功能全面,能夠對js、css、甚至圖片、字體文件統一進行合併打包,而且插件豐富。而browserify的特色是職責單一,只負責js模塊合併打包,有些項目也並不須要將css等資源文件和js打包在一塊兒的功能;它的代碼風格也相似管道函數,和gulp的契合度較高;在github上也能夠找到至關多的browserify插件,如熱替換代碼分割等等。es6

有一篇文章對browserifywebpack的對比進行了探討:webpack 跟 browserify 比到底有什麼好?github

示例項目

本文中使用的示例項目是我爲重構過去搞的UI組件庫而建的項目,使用browserify構建的分支地址請戳這裏。這個項目目前已改用gulp+webpack構建,可是保留了原先用browserify構建的分支代碼可供參考。web

項目結構目錄

項目目錄npm

  • dist (生產代碼目錄,存放生成合並後的各種文件)

    • js

      • 構建出的項目js文件

    • fonts

      • ...

    • css

      • 構建出的項目css文件

  • examples (示例目錄)

  • src (開發代碼目錄)

    • styles (樣式文件目錄)

    • base.js (打包入口文件)

    • ...

  • test (單元測試目錄)

  • vendor (第三方依賴庫)

    • babelHelpers.js

    • ...

  • gulpfile.js (gulp配置文件)

  • package.json

  • LICENSE

  • README.md

示例項目目錄大致如上所示,其中使用babel進行es6至es5轉換,並使用eslint進行js代碼檢驗。你們看到這裏可能有疑問,爲何項目中沒有babel及eslint的配置文件.babelrc.eslintrc呢?緣由就是這些配置文件裏的內容實際上是能夠直接配置在gulpfile.js中的相關插件內的。

配置package.json

在這裏只列出項目依賴的各類包,大體分爲以下幾類:

{
  ...
  "devDependencies": {
    /*browserify包及相關插件*/
    "browserify": "^13.0.0",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "standalonify": "^0.1.3",
    /*babel相關插件*/
    "babelify": "^7.2.0",
    "babel-plugin-external-helpers": "^6.4.0",
    "babel-plugin-transform-es2015-classes": "^6.5.2",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.5.2",
    "babel-plugin-transform-object-assign": "^6.3.13",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babel-preset-stage-0": "^6.3.13",
    /*eslint相關插件*/
    "babel-eslint": "^5.0.0",
    "estraverse": "^4.2.0",
    "estraverse-fb": "^1.3.1",
    /*gulp包及相關插件*/
    "gulp": "^3.9.0",
    "gulp-clean": "^0.3.1",
    "gulp-concat": "^2.6.0",
    "gulp-cssnano": "^2.1.1",
    "gulp-eslint": "^2.0.0",
    "gulp-if": "^2.0.0",
    "gulp-jasmine": "^2.2.1",
    "gulp-less": "^3.0.5",
    "gulp-rename": "^1.2.2",
    "gulp-sequence": "^0.4.4",
    "gulp-uglify": "^1.5.1",
    /*postcss相關插件*/
    "gulp-postcss": "^6.1.0",
    "autoprefixer": "^6.3.4",
    /*外部依賴包*/
    "nornj": "^0.3.0",
    "react": "^0.14.8",
    "react-dom": "^0.14.8",
    /*其餘依賴包*/
    "jsdom": "^8.1.0",
    "yargs": "^4.2.0",
    ...
  },
  ...
}

編寫gulpfile.js

gulpfile.js即爲gulp的配置文件,其做用相似於webpack的webpack.config.js文件。在代碼風格方面,與webpack.config.js的配置參數風格不一樣的是,gulpfile.js更偏向編程風格。gulpfile.js總體結構以下所示:

//引入依賴的各類包:
var gulp = require('gulp'),
  browserify = require('browserify'),
  ...

//定義一些全局函數及變量
function getJsLibName() {
  ...
}
...

//定義各類任務
gulp.task('build-all-js', ...);

gulp.task('build-all-css', ...);

gulp.task('build', ['build-all-js', 'build-all-css', ...]);
...

//定義默認任務
gulp.task('default', ['build']);

使用gulp須要定義各類任務來處理各種文件的構建生成。如例中所示,定義build-all-js任務來構建js文件,執行任務時須輸入命令:

gulp build-all-js

能夠定義一個默認任務,通常在這個任務裏依次執行所有子任務,執行時輸入命令:

gulp

關於gulp基礎使用方法的更多細節你們能夠參考這篇文章:前端構建工具gulpjs的使用介紹及技巧

使用Browserify進行js模塊合併

配合gulp使用browserify須要引入的包:

var browserify = require('browserify'),
  source = require('vinyl-source-stream'),
  buffer = require('vinyl-buffer'),
  standalonify = require('standalonify'),
  argv = require('yargs').argv;

建立gulp任務build-js:

gulp.task('build-js', function () {
  return browserify({
    entries: './src/base.js'  //指定打包入口文件
  })
    .plugin(standalonify, {  //使打包後的js文件符合UMD規範並指定外部依賴包
      name: 'FlareJ',
      deps: {
        'nornj': 'nj',
        'react': 'React',
        'react-dom': 'ReactDOM'
      }
    })
    .transform(babelify, ...)  //使用babel轉換es6代碼
    .bundle()  //合併打包
    .pipe(source(getJsLibName()))  //將常規流轉換爲包含Stream的vinyl對象,而且重命名
    .pipe(buffer())  //將vinyl對象內容中的Stream轉換爲Buffer
    .pipe(gulp.dest('./dist/js'));  //輸出打包後的文件
});

function getJsLibName() {
  var libName = 'flarej.js';
  if (argv.min) {  //按命令參數"--min"判斷是否爲壓縮版
    libName = 'flarej.min.js';
  }

  return libName;
}
  • webpack相似,browserify也須要指定打包的入口文件。在本例中只有一個入口文件,browserify會自動分析文件內依賴的各js模塊,最終生成一個完整的打包文件。

  • 使用standalonify插件使打包後的js文符合UMD規範,並能夠指定不將一些外部依賴包打進包內。一開始我使用了dependify,以後發現它生成的包有bug且做者又不維護,因而就參考它重發了一個更完善的standalonify。使用這個插件打出來的包能夠更好地生成依賴包的信息,此功能就相似於webpack中的externals參數。例如UMD中的AMD部分會這樣生成:

...
else if (typeof define === 'function' && define.amd) { define(["nornj","react","react-dom"], ...)
...

其實使用browserify自帶的standalone屬性也能夠打出UMD包,並配合browserify-shim插件也能夠排除外部依賴包,可是打包後依賴包的信息只能定義爲全局的。

  • 而後使用bundle方法進行js模塊合併打包,如代碼爲es6環境則需在此以前執行transform方法進行es6轉es5。

  • browserify在打包後需要進行Stream轉換纔可和gulp配合,在這裏須要使用vinyl-source-streamvinyl-buffer這兩個包。

  • 在使用vinyl-source-stream時能夠將打包文件重命名,此時可用yargs包提供的獲取命令參數功能來決定是否使用壓縮版命名。例如命名爲壓縮版需輸入命令:

    gulp build-js --min
  • 最後使用gulp.dest方法指定打包後文件保存的目錄。

關於browserify更詳細的技術資料你們能夠參考這篇文章:browserify使用手冊

使用Babel將es6代碼轉換爲es5

因爲es6代碼目前大部分瀏覽器還未能徹底支持,所以通常都須要轉換爲es5後執行。本示例中使用babel配合browserify在打包的過程當中進行轉換,babel的版本爲6.0+。須要引入babelify,這個包是browserify的一個transform插件。使用方法以下:

gulp.task('build-js', function () {
  return browserify({
    entries: './src/base.js'
  })
    .plugin(standalonify, ...)
    .transform(babelify, {  //此處babel的各配置項格式與.babelrc文件相同
      presets: [
        'es2015',  //轉換es6代碼
        'stage-0',  //指定轉換es7代碼的語法提案階段
        'react'  //轉換React的jsx
      ],
      plugins: [
        'transform-object-assign',  //轉換es6 Object.assign插件
        'external-helpers',  //將es6代碼轉換後使用的公用函數單獨抽出來保存爲babelHelpers
        ['transform-es2015-classes', { "loose": false }],  //轉換es6 class插件
        ['transform-es2015-modules-commonjs', { "loose": false }]  //轉換es6 module插件
        ...
      ]
    })
    .bundle()
    ...
});
  • babelify插件的配置項格式與.babelrc文件徹底相同。在babel升級6.0+後與以前的5.x差異較大,它拆分爲了不少個模塊須要分別引入。這些模塊都須要單獨安裝各自的npm包,具體請查看package.json文件。

  • presets項須要使用es201五、stage-x、react三個模塊:

    1. es2015,用於轉換es6代碼

    2. stage-x,用於轉換更新的es7語法,x是指es7不一樣階段的語法提案,目前有0-3可用

    3. react,用於轉換React的jsx代碼。

  • plugins項可引入轉換時須要的插件。通常來講babel-preset-es2015這個包中已經包含了大多數轉換es6代碼的模塊,但也有部分模塊須要在plugins中引入。例如:

    1. transform-object-assign,用於轉換Object.assign

    2. 如轉換時使用loose模式(設置了loose爲true時代碼纔可適應IE8,默認爲false),則須要單獨引入這些模塊的包。如transform-es2015-classes即爲轉換es6 class的包,若有須要可設置loose模式爲true。

    3. external-helpers,這個模塊的做用是將babel轉換後的一些公用函數單獨抽出來,這樣就能夠減小轉換後的冗餘代碼量。具體使用方法爲先全局安裝babel:

      npm install babel-cli -g

      而後執行命令:

      babel-external-helpers #可加-t參數按不一樣方式生成,值爲global|umd|var,默認爲global

    這樣就能夠在命令行中生成babelHelpers的代碼,而後將之保存爲babelHelpers.js,在本例中放在vendor目錄內。

生成最終的js代碼

因爲本例中使用external-helpers方式進行es6轉換,故須要將babelHelpers.js與browserify打包後的項目js文件進行鏈接合併:

var concat = require('gulp-concat'),
  sequence = require('gulp-sequence'),
  gulpif = require('gulp-if'),
  uglify = require('gulp-uglify');

//定義鏈接js任務
gulp.task('concat-js', function () {
  var jsLibName = getJsLibName();
  return gulp.src(['./vendor/babelHelpers.js', './dist/js/' + jsLibName])
    .pipe(concat(jsLibName))
    .pipe(gulpif(argv.min, uglify()))
    .pipe(gulp.dest('./dist/js'));
});

//將兩個任務串聯起來
gulp.task('build-all-js', sequence('build-js', 'concat-js'));
  • 先使用gulp-concat插件將babelHelpers.js和項目js文件進行鏈接合併。

  • 而後使用gulp-if插件判斷當前執行命令是否輸入了--min參數,若是是則使用gulp-uglify插件進行js代碼壓縮。

  • 定義build-all-js任務來將build-js和concat-js任務串聯起來,可是須要使用gulp-sequence插件才能保證這兩個任務是按順序執行的。

  • 最後,在/dist/js目錄下會生成最終的項目js文件。

執行單元測試

本例中使用jasmine進行單元測試。代碼比較簡單,執行全部test目錄內以"Spec"結尾的文件:

var jasmine = require('gulp-jasmine');

gulp.task("test", function () {
  return gulp.src(["./test/**/**Spec.js"])
    .pipe(jasmine());
});

執行命令:

gulp test

便可在命令行中查看測試結果。

執行js代碼檢驗

本例中使用eslint進行js代碼檢驗,需引入gulp-eslint插件:

var eslint = require('gulp-eslint');

gulp.task('eslint', function () {
  return gulp.src(['./src/**/*.js'])  //獲取src目錄內所有js文件
    .pipe(eslint({  //此處eslint的各配置項格式與.eslintrc文件相同
      "rules": {
        "camelcase": [2, { "properties": "always" }],
        "comma-dangle": [2, "never"],
        "semi": [2, "always"],
        "quotes": [2, "single"],
        "strict": [2, "global"]
      },
      "parser": "babel-eslint"
    }))
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
});

執行命令:

gulp eslint

便可在命令行中查看js代碼檢測結果。

另外若是是在es6環境下使用gulp-eslint,那麼還須要安裝babel-eslint這個包。此處有個小坑,就是babel-eslint包是依賴estraverseestraverse-fb包的,但這兩個包其實卻須要單獨安裝。

生成css及字體文件

例中的css及字體文件也須要合併構建,這裏只簡單介紹一下構建css的流程:

var less = require('gulp-less'),
  cssnano = require('gulp-cssnano'),
  postcss = require('gulp-postcss'),
  autoprefixer = require('autoprefixer');

function getCssLibName() {
  var libName = 'flarej.css';
  if (argv.min) {
    libName = 'flarej.min.css';
  }

  return libName;
}

//構建項目css文件
gulp.task('build-css', function () {
  return gulp.src('./src/styles/base.less')
    .pipe(less())  //轉換less
    .pipe(rename(getCssLibName()))  //重命名轉換後的css文件
    .pipe(gulp.dest('./dist/css'));
});

//將normalize.css與項目css進行合併
gulp.task('concat-css', function () {
  var cssLibName = getCssLibName();
  return gulp.src(['./vendor/normalize.css', './dist/css/' + cssLibName])
    .pipe(concat(cssLibName))  //鏈接合併
    .pipe(gulpif(argv.min, cssnano()))  //執行css壓縮
    .pipe(postcss([autoprefixer({ browsers: ['last 50 versions'] })]))  //自動補廠商前綴
    .pipe(gulp.dest('./dist/css'));
});

//將兩個任務串聯起來
gulp.task('build-all-css', sequence('build-css', 'concat-css'));

構建所有代碼

本例中的gulp默認任務即爲構建所有代碼,輸入命令:

gulp #可加"--min"參數構建壓縮版

便可執行,具體構建流程以下:

圖片描述

更多細節你們能夠查看本文示例的源代碼

(完)

相關文章
相關標籤/搜索