書籍完整目錄
2.4 webpack + gulp 構建完整前端工作流
在前面的兩個小節中已經完整的講了 webpack 和 gulp 相關的內容,本小節中將會結合二者構建一個完整的前端工作流,內容目錄為:
-
前端工程結構和目標
-
前端工程目錄結構
-
gulp clean
-
gulp copy
-
gulp less
-
gulp autoprefixer
-
gulp webpack
-
gulp eslint
-
gulp watch
-
gulp connect 和 livereload
-
gulp mock server
-
gulp test
2.4.1 前端工程結構和目標
React 在大多數情況被當做當 SPA (單頁面應用)的框架,實際上在真實業務開發過程中,非單頁面應用的業務框架居多。所以我們在構建前端工程的時候,以多個頁面的方式維護。下面定義前端工程的目標:
基礎技術
-
react(es6 + jsx)
-
less
-
gulp + webpack
應用模式
多頁面應用:以多頁面應用方式,能同時構建多個頁面
樣式結構
在樣式的架構上的一些基本需求:
-
基於 less 或者 sass 或者其他樣式語言
-
基礎庫
-
共享變量和工具類
樣式的設計上,大可歸為兩種方式:
-
獨立樣式:樣式的開發和其他代碼儘量分離獨立;
-
Inline Style:樣式通過 javascript 變量維護,或者通過工具將 css 轉化為 javascript,再應用到 React 的 style 中;
樣式文件在工程中的位置也可以分為兩種:
-
邏輯樣式隔離:javascript 文件和樣式文件在不同的工程目錄,這是傳統的樣式邏輯分離設計,符合大多數的業務場景;
-
component pod:也就是一個組件的目錄結構包括樣式,邏輯和模板,在 React 中模板和邏輯是在一起的,也就是一個組件包括一個
component.js和component.css。 這種模式的目的是出於組件的 獨立性,所以基於 pod 的組件好處是能夠更好的共享,但壞處是和不方便共享變量和工具類(共享就會產生耦合,也就違背了 pod 的目的)
所以在樣式的設計上,我們應用如下這些設計:
-
基於 Less
-
Less 相關的基礎庫和公共變量獨立出來,變量主要是用於主題設置
-
樣式統一放在 style 目錄下面,業務組件需要共享變量,以非 pod 的方式設計樣式,放置在 style 目錄獨立文件中,文件名稱和組件名相同
-
需要獨立的組件以 npm 的方式維護,樣式以 pod 和 inline style 的方式設計
兼容的第三方庫引入方式
在第三方庫的引入上,可能以 bower_components 的方式,可能是自己公司內部維護的第三方庫和基礎組件,也可能是 npm 組件,所以為了兼容這些第三方庫的引入,確定一下規範:
-
vendor 目錄下面放在第三方庫,包括樣式和邏輯,為了優化編譯速度,這些目錄的文件在編譯的時候只做合併,避免和業務代碼的編譯做過多的耦合(vendor 庫文件通常比較大)
-
bower_components 中的庫同 vendor
-
npm 中的第三方庫做代碼分割,統一打包到 vendor.bundle.js 中
配置自動化
業務代碼可能在不斷增加,在工程 build 的時候,儘量以 glob 的方式匹配文件,避免增加一個業務文件就需要修改配置
高效的編譯
代碼編譯的時間如果太長,會極大的影響開發體驗,所以在編譯的時候要考慮提高編譯的效率:
-
避免全局編譯
-
增量編譯:利用上一節介紹的 gulp-cached 和 gulp-remember
-
只在 prodution 的時候才做代碼壓縮優化
後端數據 mock 和代理
能夠支持數據 mock 和代理功能
2.4.2 前端工程目錄結構
基於這些目標定義如下工程結構:
.
├── package.json
├── README.md
├── gulpfile.js // gulp 配置文件
├── webpack.config.js // webpack 配置文件
├── doc // doc 目錄:放置應用文檔
├── test // test 目錄:測試文件
├── dist // dist 目錄:放置開發時候的臨時打包文件
├── bin // bin 目錄:放置 prodcution 打包文件
├── mocks // 數據 mock 相關
├── src // 源文件目錄
│ ├── html // html 目錄
│ │ ├── index.html
│ │ └── page2.html
│ ├── js // js 目錄
│ │ ├── common // 所有頁面的共享區域,可能包含共享組件,共享工具類
│ │ ├── home // home 頁面 js 目錄
│ │ │ ├── components
│ │ │ │ ├── App.js
│ │ │ ├── index.js // 每個頁面會有一個入口,統一為 index.js
│ │ ├── page2 // page2 頁面 js 目錄
│ │ │ ├── components
│ │ │ │ ├── App.js
│ │ │ └── index.js
│ └── style // style 目錄
│ ├── common // 公共樣式區域
│ │ ├── varables.less // 公共共享變量
│ │ ├── index.less // 公共樣式入口
│ ├── home // home 頁面樣式目錄
│ │ ├── components // home 頁面組件樣式目錄
│ │ │ ├── App.less
│ │ ├── index.less // home 頁面樣式入口
│ ├── page2 // page2 頁面樣式目錄
│ │ ├── components
│ │ │ ├── App.less
│ │ └── index.less
├── vendor
│ └── bootstrap
└── └── jquery
2.4.3 安裝基礎依賴
目錄創建好過後,進入項目目錄,安裝 webpack ,gulp,react 相關的基礎依賴
// react 相關
$ cd project
$ npm install react react-dom --save
// webpack 相關
$ npm install webpack-dev-server webpack --save-dev
$ npm install babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-polyfill --save-dev
// gulp 相關
$ npm install gulpjs/gulp-cli -g
$ npm install gulpjs/gulp.git#4.0 --save-dev
$ npm install gulp-util del gulp-rename gulp-less gulp-connect connect-rest@1.9.5 --save-dev
2.4.4 創建 gulpfile.js
創建 gulpfile.js 並全局定義打包源文件和打包目標相關的配置
// gulpfile.js
var gulp = require("gulp");
var gutil = require("gulp-util");
var src = {
// html 文件
html: "src/html/*.html",
// vendor 目錄和 bower_components
vendor: ["vendor/**/*", "bower_components/**/*"],
// style 目錄下所有 xx/index.less
style: "src/style/*/index.less",
// 圖片等應用資源
assets: "assets/**/*"
};
var dist = {
root: "dist/",
html: "dist/",
style: "dist/style",
vendor: "dist/vendor",
assets: "dist/assets"
};
var bin = {
root: "bin/",
html: "bin/",
style: "bin/style",
vendor: "bin/vendor",
assets: "bin/assets"
};
2.4.5 清理和拷貝任務
在每次啓動 build 的時候,需要先清除之間的 build 結果,然後將最新的文件如 html, assets, vendor 這些文件拷貝到 dist 目錄中
var del = require("del");
/**
* clean build dir
*/
function clean(done) {
del.sync(dist.root);
done();
}
/**
* [cleanBin description]
* @return {[type]} [description]
*/
function cleanBin(done) {
del.sync(bin.root);
done();
}
/**
* [copyVendor description]
* @return {[type]} [description]
*/
function copyVendor() {
return gulp.src(src.vendor)
.pipe(gulp.dest(dist.vendor));
}
/**
* [copyAssets description]
* @return {[type]} [description]
*/
function copyAssets() {
return gulp.src(src.assets)
.pipe(gulp.dest(dist.assets));
}
/**
* [copyDist description]
* @return {[type]} [description]
*/
function copyDist() {
return gulp.src(dist.root + '**/*')
.pipe(gulp.dest(bin.root));
}
/**
* [html description]
* @return {[type]} [description]
*/
function html() {
return gulp.src(src.html)
.pipe(gulp.dest(dist.html))
}
2.4.6 樣式轉換
樣式使用了 gulp-less 插件,其中需要注意的一點是,less 文件如果出錯可能會將整個 build 進程結束,需要添加 error 時候的處理函數,同時也能自定義的輸出 error 相關的信息,樣式的轉換使用了 autoprefixer 代碼補全
var less = require('gulp-less');
var autoprefixer = require('gulp-autoprefixer');
/**
* [style description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
function style() {
return gulp.src(src.style)
.pipe(cached('style'))
.pipe(less())
.on('error', handleError)
.pipe(autoprefixer({
browsers: ['last 3 version']
}))
.pipe(gulp.dest(dist.style))
}
exports.style = style;
/**
* [handleError description]
* @param {[type]} err [description]
* @return {[type]} [description]
*/
function handleError(err) {
if (err.message) {
console.log(err.message)
} else {
console.log(err)
}
this.emit('end')
}
執行 gulp style 測試
$ gulp style
[21:00:35] Starting 'style'...
[21:00:36] Finished 'style' after 87 ms
home/index.less
/**
* before
*/
body {
background: white;
color: #333;
transform: rotate(45deg);
display: flex;
}
/**
* after
*/
body {
background: white;
color: #333;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
如果需要做樣式的 lint 可以通過 ,gulp-stylelint 來實現。
更多信息可參考:
gulp-autoprefixer: https://www.npmjs.com/package/gulp-autoprefixer
gulp-stylelint: https://github.com/stylelint/stylelint
2.4.7 webpack 配置
首先創建 webpack.config.js,這裏使用的一個技巧是使用 glob 動態添加 entry,讓配置做到自動化。
// webpack.config.js
var webpack = require("webpack");
const glob = require('glob');
var config = {
entry: {
vendor: [
'react',
'react-dom'
]
},
output: {
path: __dirname + '/dist/js/',
filename: '[name].js'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: { presets: [ 'es2015', 'stage-0', 'react' ] }
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js')
]
};
/**
* find entries
*/
var files = glob.sync('./src/js/*/index.js');
var newEntries = files.reduce(function(memo, file) {
var name = /.*\/(.*?)\/index\.js/.exec(file)[1];
memo[name] = entry(name);
return memo;
}, {});
config.entry = Object.assign({}, config.entry, newEntries);
/**
* [entry description]
* @param {[type]} name [description]
* @return {[type]} [description]
*/
function entry(name) {
return './src/js/' + name + '/index.js';
}
module.exports = config;
在 gulpfile 中定義 webpack 任務,因為 webpack.config.js 為一個 node 模塊,可直接引入,
又因為在 production 環境和 development 環境的模式是不同的,可以定義兩個不同的任務:
/**
* [webpack 相關的依賴]
* @type {[type]}
*/
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");
/**
* [webpackDevelopment description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
var devConfig, devCompiler;
devConfig = Object.create(webpackConfig);
devConfig.devtool = "sourcemap";
devConfig.debug = true;
devCompiler = webpack(devConfig);
function webpackDevelopment(done) {
devCompiler.run(function(err, stats) {
if (err) {
throw new gutil.PluginError("webpack:build-dev", err);
return;
}
gutil.log("[webpack:build-dev]", stats.toString({
colors: true
}));
done();
});
}
/**
* [webpackProduction description]
* production 任務中添加了壓縮和打包優化組件,且沒有 sourcemap
* @param {Function} done [description]
* @return {[type]} [description]
*/
function webpackProduction(done) {
var config = Object.create(webpackConfig);
config.plugins = config.plugins.concat(
new webpack.DefinePlugin({
"process.env": {
"NODE_ENV": "production"
}
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin()
);
webpack(config, function(err, stats) {
if(err) throw new gutil.PluginError("webpack:build", err);
gutil.log("[webpack:production]", stats.toString({
colors: true
}));
done();
});
}
2.4.8 javascript lint
為了能夠 lint Es6 和 jsx 的 javascript ,可以基於 Eslint 來實現,Eslint 的基本配置:
安裝相關依賴
$ npm install eslint eslint-loader eslint-plugin-react --save-dev
添加 .eslintrc 配置文件
{
// Extend existing configuration
// from ESlint and eslint-plugin-react defaults.
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
// Enable ES6 support. If you want to use custom Babel
// features, you will need to enable a custom parser
// as described in a section below.
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
// Enable custom plugin known as eslint-plugin-react
"plugins": [
"react"
],
"rules": {
// Disable `no-console` rule
"no-console": 0,
// Give a warning if identifiers contain underscores
"no-underscore-dangle": 1,
// Default to single quotes and raise an error if something
// else is used
"quotes": [2, "single"]
}
}
修改 webpack.config.js 配置
在 webpack.config.js 中添加 preloaders (preloader 會在其他 loader 前應用)
module: {
preLoaders:[{
test: /\.js$/,
loader: "eslint-loader",
exclude: /node_modules/
}]
},
eslint: {
configFile: './.eslintrc'
}
測試
修改 index.js 添加如下的代碼塊:
const d = a ? b : c;
運行 webpack 測試
$ webpack
ERROR in ./src/js/home/index.js
......./src/js/home/index.js
1:7 error 'd' is defined but never used no-unused-vars
1:11 error 'a' is not defined no-undef
1:15 error 'b' is not defined no-undef
1:19 error 'c' is not defined no-undef
✖ 4 problems (4 errors, 0 warnings)
更多的 eslint 的定義可以參考官網:http://eslint.org
2.4.9 自動刷新和數據 mock
代碼的自動刷新用到了 gulp-connect 插件,並通過 connect-rest 模塊實現 rest 接口的數據 mock。
/**
* [connectServer description]
* @return {[type]} [description]
*/
function connectServer(done) {
connect.server({
root: dist.root,
port: 8080,
livereload: true,
middleware: function(connect, opt) {
return [rest.rester({
context: "/"
})]
}
});
mocks(rest);
done();
}
mocks 目錄下面定義了一個 index.js 如下:
/**
* [mocks]
* @param {[type]} app [description]
* @return {[type]} [description]
*/
module.exports = function(app) {
app.get("rest", function(req, content, callback) {
setTimeout(function() {
callback(null, {
a: 1,
b: 2
});
}, 500)
})
}
connect-rest 不僅可以做數據 mock 的 rest 接口,同時也能實現 proxy 轉發。更多可參見 https://github.com/imrefazekas/connect-rest/tree/v2
需要注意的是 connect-rest 用到的版本為 1.9.5, 高版本不兼容。
2.4.10 代碼監控
為了能夠監控文件改變能實現自動刷新,還需要通過定義 watch 任務,監控文件的改變。 這裏使用到了一個 trick,只監控 dist 目錄的文件,如果該目錄文件改變了,使用 pipe 的方式調用 connect.reload(), 直接調用不會自動刷新
/**
* [watch description]
* @return {[type]} [description]
*/
function watch() {
gulp.watch(src.html, html);
gulp.watch("src/**/*.js", webpackDevelopment);
gulp.watch("src/**/*.less", style);
gulp.watch("dist/**/*").on('change', function(file) {
gulp.src('dist/')
.pipe(connect.reload());
});
}
2.4.11 任務編排
最後將之前定義的任務通過 parallel 和 series 方法進行編排, 默認任務為開發任務,build 任務為 production 任務
/**
* default task
*/
gulp.task("default", gulp.series(
clean,
gulp.parallel(copyAssets, copyVendor, html, style, webpackDevelopment),
connectServer,
watch
));
/**
* production build task
*/
gulp.task("build", gulp.series(
clean,
gulp.parallel(copyAssets, copyVendor, html, style, webpackProduction),
cleanBin,
copyDist,
function(done) {
console.log('build success');
done();
}
));
2.4.12 webpack-dev-server vs gulp-connect
在 webpack 小節也介紹過 webpack-dev-server 可以實現自動刷新並能實現局部的熱加載 ,那為什麼不使用 webpack-dev-server 而是使用 gulp-connect?
我的觀點是對於一般的項目來説兩者都可以使用,甚至可以只使用 webpack 就能完整工程構建任務,但是引入了 gulp 過後,能夠更加清晰可控的編排任務,通過使用 gulp-connect 能夠很方便的通過中間件的方式實現數據 mock,並且也能和 gulp.watch 整合。
附件:完整代碼
// webpack.config.js
var webpack = require("webpack");
const glob = require('glob');
var config = {
entry: {
vendor: ['react', 'react-dom']
},
output: {
path: __dirname + '/dist/js/',
filename: '[name].js'
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'stage-0', 'react']
}
}],
preLoaders:[{
test: /\.js$/,
loader: "eslint-loader",
exclude: /node_modules/
}],
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js')
],
eslint: {
configFile: './.eslintrc'
}
};
/**
* find entries
*/
var files = glob.sync('./src/js/*/index.js');
var newEntries = files.reduce(function(memo, file) {
var name = /.*\/(.*?)\/index\.js/.exec(file)[1];
memo[name] = entry(name);
return memo;
}, {});
config.entry = Object.assign({}, config.entry, newEntries);
/**
* [entry description]
* @param {[type]} name [description]
* @return {[type]} [description]
*/
function entry(name) {
return './src/js/' + name + '/index.js';
}
module.exports = config;
// gulpfile.js
/**
* [gulp description]
* @type {[type]}
*/
var gulp = require("gulp");
var gutil = require("gulp-util");
var del = require("del");
var rename = require('gulp-rename');
var less = require('gulp-less');
var autoprefixer = require('gulp-autoprefixer');
var cached = require('gulp-cached');
var remember = require('gulp-remember');
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");
var connect = require('gulp-connect');
var rest = require('connect-rest');
var mocks = require('./mocks');
/**
* ----------------------------------------------------
* source configuration
* ----------------------------------------------------
*/
var src = {
html: "src/html/*.html", // html 文件
vendor: ["vendor/**/*", "bower_components/**/*"], // vendor 目錄和 bower_components
style: "src/style/*/index.less", // style 目錄下所有 xx/index.less
assets: "assets/**/*" // 圖片等應用資源
};
var dist = {
root: "dist/",
html: "dist/",
style: "dist/style",
vendor: "dist/vendor",
assets: "dist/assets"
};
var bin = {
root: "bin/",
html: "bin/",
style: "bin/style",
vendor: "bin/vendor",
assets: "bin/assets"
};
/**
* ----------------------------------------------------
* tasks
* ----------------------------------------------------
*/
/**
* clean build dir
*/
function clean(done) {
del.sync(dist.root);
done();
}
/**
* [cleanBin description]
* @return {[type]} [description]
*/
function cleanBin(done) {
del.sync(bin.root);
done();
}
/**
* [copyVendor description]
* @return {[type]} [description]
*/
function copyVendor() {
return gulp.src(src.vendor)
.pipe(gulp.dest(dist.vendor));
}
/**
* [copyAssets description]
* @return {[type]} [description]
*/
function copyAssets() {
return gulp.src(src.assets)
.pipe(gulp.dest(dist.assets));
}
/**
* [copyDist description]
* @return {[type]} [description]
*/
function copyDist() {
return gulp.src(dist.root + '**/*')
.pipe(gulp.dest(bin.root));
}
/**
* [html description]
* @return {[type]} [description]
*/
function html() {
return gulp.src(src.html)
.pipe(gulp.dest(dist.html))
}
/**
* [style description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
function style() {
return gulp.src(src.style)
.pipe(cached('style'))
.pipe(less())
.on('error', handleError)
.pipe(autoprefixer({
browsers: ['last 3 version']
}))
.pipe(gulp.dest(dist.style))
}
exports.style = style;
/**
* [webpackProduction description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
function webpackProduction(done) {
var config = Object.create(webpackConfig);
config.plugins = config.plugins.concat(
new webpack.DefinePlugin({
"process.env": {
"NODE_ENV": "production"
}
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin()
);
webpack(config, function(err, stats) {
if(err) throw new gutil.PluginError("webpack:build", err);
gutil.log("[webpack:production]", stats.toString({
colors: true
}));
done();
});
}
/**
* [webpackDevelopment description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
var devConfig, devCompiler;
devConfig = Object.create(webpackConfig);
devConfig.devtool = "sourcemap";
devConfig.debug = true;
devCompiler = webpack(devConfig);
function webpackDevelopment(done) {
devCompiler.run(function(err, stats) {
if (err) {
throw new gutil.PluginError("webpack:build-dev", err);
return;
}
gutil.log("[webpack:build-dev]", stats.toString({
colors: true
}));
done();
});
}
/**
* webpack develop server
*/
// devConfig.plugins = devConfig.plugins || []
// devConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
// function webpackDevelopmentServer(done) {
// new WebpackDevServer(devCompiler, {
// contentBase: dist.root,
// lazy: false,
// hot: true
// }).listen(8080, 'localhost', function (err) {
// if (err) throw new gutil.PluginError('webpack-dev-server', err)
// gutil.log('[webpack-dev-server]', 'http://localhost:8080/')
// reload();
// done();
// });
// }
/**
* [connectServer description]
* @return {[type]} [description]
*/
function connectServer(done) {
connect.server({
root: dist.root,
port: 8080,
livereload: true,
middleware: function(connect, opt) {
return [rest.rester({
context: "/"
})]
}
});
mocks(rest);
done();
}
/**
* [watch description]
* @return {[type]} [description]
*/
function watch() {
gulp.watch(src.html, html);
gulp.watch("src/**/*.js", webpackDevelopment);
gulp.watch("src/**/*.less", style);
gulp.watch("dist/**/*").on('change', function(file) {
gulp.src('dist/')
.pipe(connect.reload());
});
}
/**
* default task
*/
gulp.task("default", gulp.series(
clean,
gulp.parallel(copyAssets, copyVendor, html, style, webpackDevelopment),
connectServer,
watch
));
/**
* production build task
*/
gulp.task("build", gulp.series(
clean,
gulp.parallel(copyAssets, copyVendor, html, style, webpackProduction),
cleanBin,
copyDist,
function(done) {
console.log('build success');
done();
}
));
/**
* [handleError description]
* @param {[type]} err [description]
* @return {[type]} [description]
*/
function handleError(err) {
if (err.message) {
console.log(err.message)
} else {
console.log(err)
}
this.emit('end')
}
/**
* [reload description]
* @return {[type]} [description]
*/
function reload() {
connect.reload();
}
// .eslintrc
{
// Extend existing configuration
// from ESlint and eslint-plugin-react defaults.
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
// Enable ES6 support. If you want to use custom Babel
// features, you will need to enable a custom parser
// as described in a section below.
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
// Enable custom plugin known as eslint-plugin-react
"plugins": [
"react"
],
"rules": {
// Disable `no-console` rule
"no-console": 0,
// Give a warning if identifiers contain underscores
"no-underscore-dangle": 1,
// Default to single quotes and raise an error if something
// else is used
"quotes": [2, "single"]
}
}