精益 React 学习指南 (Lean React)- 2.4 webpack + gulp 构建
2.4 webpack + gulp 构建完整前端工作流
在前面的两个小节中已经完整的讲了 webpack 和 gulp 相关的内容,本小节中将会结合二者构建一个完整的前端工作流,内容目录为:
2.4.1 前端工程结构和目标React 在大多数情况被当做当 SPA (单页面应用)的框架,实际上在真实业务开发过程中,非单页面应用的业务框架居多。所以我们在构建前端工程的时候,以多个页面的方式维护。下面定义前端工程的目标: 基础技术
应用模式多页面应用:以多页面应用方式,能同时构建多个页面 样式结构在样式的架构上的一些基本需求:
样式的设计上,大可归为两种方式:
样式文件在工程中的位置也可以分为两种:
所以在样式的设计上,我们应用如下这些设计:
兼容的第三方库引入方式在第三方库的引入上,可能以 bower_components 的方式,可能是自己公司内部维护的第三方库和基础组件,也可能是 npm 组件,所以为了兼容这些第三方库的引入,确定一下规范:
配置自动化业务代码可能在不断增加,在工程 build 的时候,尽量以 glob 的方式匹配文件,避免增加一个业务文件就需要修改配置 高效的编译代码编译的时间如果太长,会极大的影响开发体验,所以在编译的时候要考虑提高编译的效率:
后端数据 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 来实现。 更多信息可参考:
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 模块,可直接引入, /** * [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
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",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$/,query: { presets: ['es2015','react'] } }],preLoaders:[{ test: /.js$/,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({},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/**/*",// vendor 目录和 bower_components style: "src/style/*/index.less",// style 目录下所有 xx/index.less assets: "assets/**/*" // 图片等应用资源 }; var dist = { root: "dist/",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" } }),stats.toString({ colors: true })); done(); }); } /** * [webpackDevelopment description] * @param {Function} done [description] * @return {[type]} [description] */ var devConfig,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,opt) { return [rest.rester({ context: "/" })] } }); mocks(rest); done(); } /** * [watch description] * @return {[type]} [description] */ function watch() { gulp.watch(src.html,function(file) { gulp.src('dist/') .pipe(connect.reload()); }); } /** * default task */ gulp.task("default",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","single"] } } (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |