在前面的两个小节中已经完整的讲了 webpack 和 gulp 相关的内容,本小节中将会结合两者构建一个完整的前端工做流,内容目录为:
前端工程结构和目标javascript
前端工程目录结构css
gulp cleanhtml
gulp copy前端
gulp lessjava
gulp autoprefixernode
gulp webpackreact
gulp eslintjquery
gulp watchwebpack
gulp connect 和 livereloadgit
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.jsvar 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.jsvar 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-undef1:15 error 'b' is not defined no-undef1: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"] } }