隨着React、Angular2、Redux等前沿的前端框架越來越流行,使用webpack、gulp等工具構建前端自動化項目也隨之變得越來越重要。鑒於目前業界普遍更流行使用webpack來構建es6(ECMAScript 2015)前端項目,網上的相關教程也比較多;相對來說使用gulp來構建es6項目的中文教程就比較少。
經過一段時間的摸索,我覺得其實使用gulp
也可以很方便地構建es6項目。以下是我感覺gulp
和webpack
主要的不同之處:
gulp
的任務機制和流式管道函數和webpack
的配置參數風格有着顯著區別,它能使開發者更清晰地了解項目的構建流程。- 由於
gulp
是編程式風格的,所以使用起來可定制化的功能也就更靈活一些,可應對一些構建過程較為復雜的情況。
本文特此給大家介紹下如何使用gulp
配合browserify
來構建基於es6的前端項目。
Browserify vs Webpack
browserify
與webpack
都是當下流行的commonjs模塊(或es6模塊)合並打包工具,打包后的js文件可以直接運行在瀏覽器環境中。
很多人都知道,webpack
功能全面,可以對js、css、甚至圖片、字體文件統一進行合並打包,並且插件豐富。而browserify
的特點是職責單一,只負責js模塊合並打包,有些項目也並不需要將css等資源文件和js打包在一起的功能;它的代碼風格也類似管道函數,和gulp
的契合度較高;在github上也可以找到相當多的browserify
插件,如熱替換、代碼分割等等。
有一篇文章對
browserify
和webpack
的對比進行了探討:webpack 跟 browserify 比到底有什么好?
示例項目
本文中使用的示例項目是我為重構過去搞的UI組件庫而建的項目,使用browserify
構建的分支地址請戳這里。這個項目目前已改用gulp
+webpack
構建,但是保留了原先用browserify
構建的分支代碼可供參考。
項目結構目錄
- 項目目錄
- dist (生產代碼目錄,存放生成合並后的各類文件)
- js
- 構建出的項目js文件
- fonts
- ...
- css
- 構建出的項目css文件
- js
- examples (示例目錄)
- src (開發代碼目錄)
- styles (樣式文件目錄)
- base.js (打包入口文件)
- ...
- test (單元測試目錄)
- vendor (第三方依賴庫)
- babelHelpers.js
- ...
- gulpfile.js (gulp配置文件)
- package.json
- LICENSE
- README.md
- dist (生產代碼目錄,存放生成合並后的各類文件)
示例項目目錄大體如上所示,其中使用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-stream
和vinyl-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項需要使用es2015、stage-x、react三個模塊:
es2015
,用於轉換es6代碼stage-x
,用於轉換更新的es7語法,x是指es7不同階段的語法提案,目前有0-3可用react
,用於轉換React的jsx代碼。
- plugins項可引入轉換時需要的插件。一般來說babel-preset-es2015這個包中已經包含了大多數轉換es6代碼的模塊,但也有部分模塊需要在plugins中引入。例如:
transform-object-assign
,用於轉換Object.assign- 如轉換時使用loose模式(設置了loose為true時代碼才可適應IE8,默認為false),則需要單獨引入這些模塊的包。如
transform-es2015-classes
即為轉換es6 class的包,如有需要可設置loose模式為true。 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
包是依賴estraverse
和estraverse-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"參數構建壓縮版
即可執行,具體構建流程如下:
更多細節大家可以查看本文示例的源代碼。
(完)