美文网首页代码工具资源webpack
gulp & webpack整合,鱼与熊掌我都要!

gulp & webpack整合,鱼与熊掌我都要!

作者: 齐修_qixiuss | 来源:发表于2016-06-30 17:47 被阅读31733次

    为什么需要前端工程化?

    前端工程化的意义在于让前端这个行业由野蛮时代进化为正规军时代,近年来很多相关的工具和概念诞生。好奇心日报在进行前端工程化的过程中,主要的挑战在于解决如下问题:
    ✦ 如何管理多个项目的前端代码?
    ✦ 如何同步修改复用代码?
    ✦ 如何让开发体验更爽?

    项目实在太多

    之前写过一篇博文 如何管理被多个项目引用的通用项目?,文中提到过好奇心日报的项目偏多(PC/Mobile/App/Pad),要为这么多项目开发前端组件并维护是一个繁琐的工作,并且会有很多冗余的工作。

    更好的管理前端代码

    前端代码要适配后台目录的规范,本来可以很美好的前端目录结构被拆得四分五裂,前端代码分散不便于管理,并且开发体验很不友好。
    而有了前端工程化的概念,前端项目和后台项目可以彻底分离,前端按自己想要的目录结构组织代码, 然后按照一定的方式构建输出到后台项目中,简直完美(是不是有种后宫佳丽三千的感觉)。

    技术选型

    调研了市场主流的构建工具,其中包括gulp、webpack、fis,最后决定围绕gulp打造前端工程化方案,同时引入webpack来管理模块化代码,大致分工如下:
    gulp:处理html压缩/预处理/条件编译,图片压缩,精灵图自动合并等任务
    webpack:管理模块化,构建js/css。

    至于为什么选择gulp & webpack,主要原因在于gulp相对来说更灵活,可以做更多的定制化任务,而webpack在模块化方案实在太优秀(情不自禁的赞美)。

    怎么设计前端项目目录结构?

    抽离出来的前端项目目录结构如下


    前端项目结构

    appfe目录:appfe就是前面提到的前端项目,这个项目主要包含两部分:前端代码、构建任务
    appfe > gulp目录:包含了所有的gulp子任务,每个子任务包含相关任务的所有逻辑。
    appfe > src目录:包含了所有前端代码,比如页面、组件、图片、字体文件等等。
    appfe > package.json:这个不用说了吧。
    appfe > gulpfile.js:gulp入口文件,引入了所有的gulp子任务。

    理想很丰满,现实却很骨感,这么美好的愿望,在具体实践过程中,注定要花不少心思,要踩不少坑。
    好奇心日报这次升级改造即将上线,终于也有时间把之前零零碎碎的博文整合在一起,并且结合自己的体会分享给大家,当然未来可能还会有较大的调整,这儿抛砖引玉,大家可以参考思路。

    gulp 是什么?

    gulp是一个基于流的构建工具,相对其他构件工具来说,更简洁更高效。
    Tip:之前写过一篇gulp 入门,可以参考下,如果对gulp已经有一定的了解请直接跳过。

    webpack 是什么?

    webpack是模块化管理的工具,使用webpack可实现模块按需加载,模块预处理,模块打包等功能。
    Tip:之前写过一篇webpack 入门,可以参考下,如果对webpack已经有一定的了解请直接跳过。

    如何整合gulp & webpack

    webpack是众多gulp子任务中比较复杂的部分,主要对JS/CSS进行相关处理。
    包括:模块分析、按需加载、JS代码压缩合并、抽离公共模块、SourceMap、PostCSS、CSS代码压缩等等...

    webpack-stream方案[不推荐]

    使用webpack-stream虽然可以很方便的将webpack整合到gulp中,但是有致命的问题存在:
    如果关闭webpack的监听模式,那么每次文件变动就会全量编译JS/CSS文件,非常耗时。
    如果打开webpack的监听模式,那么会阻塞其他gulp任务,导致其他gulp任务的监听失效。
    所以这种方案几乎不可用!

    webpack原生方案

    直接使用webpack原生方案,相对来说更灵活。
    Tip:代码较复杂,里面涉及的知识点也很多,建议看看形状就好,如果真有兴趣,可以好好研究研究,毕竟花了很长时间去思考这些方案。

    // webpack.config.js 关键地方都有大致注释
    var _ = require('lodash');
    var path = require('path');
    var webpack = require('webpack');
    var ExtractTextPlugin = require("extract-text-webpack-plugin");
    
    var autoprefixer = require('autoprefixer');
    var flexibility = require('postcss-flexibility');
    var sorting = require('postcss-sorting');
    var color_rgba_fallback = require('postcss-color-rgba-fallback');
    var opacity = require('postcss-opacity');
    var pseudoelements = require('postcss-pseudoelements');
    var will_change = require('postcss-will-change');
    var cssnano = require('cssnano');
    
    var project = require('./lib/project')();
    var config = require('./config.' + project).webpack;
    
    
    // loaders配置
    var getLoaders = function(env) {
        return [{
            test: /\.jsx?$/,
            exclude: /(node_modules|bower_components|vendor)/,
            loader: 'babel?presets[]=es2015&cacheDirectory=true!preprocess?PROJECT=' + project
        }, {
            test: /\.css$/,
            loader: ExtractTextPlugin.extract("style-loader", "css-loader!postcss-loader")
        }, {
            test: /\.less$/,
            loader: ExtractTextPlugin.extract("style-loader", "css-loader!postcss-loader!less-loader")
        }, {
            test: /\/jquery\.js$/,
            loader: 'expose?$!expose?jQuery!expose?jquery'
        }, {
            test: /\.xtpl$/,
            loader: 'xtpl'
        }, {
            test: /\.modernizrrc$/,
            loader: "modernizr"
        }];
    };
    
    // 别名配置
    var getAlias = function(env) {
        return {
            // 特殊
            'jquery': path.resolve(__dirname, '../src/vendor/jquery2/jquery.js'),
    
            // 正常第三方库
            'jquery.js': path.resolve(__dirname, '../src/vendor/jquery2/jquery.js'),
        };
    };
    
    // 插件配置
    var getPlugins = function(env) {
        var defaultPlugins = [
            // 这个不仅是别名,还可以在遇到别名的时候自动引入模块
            new webpack.ProvidePlugin({
                '$': 'jquery.js',
                'jquery': 'jquery.js',
                'jQuery': 'jquery.js',
            }),
            // 抽离公共模块
            new webpack.optimize.CommonsChunkPlugin('common', 'common.js'),
            new ExtractTextPlugin(
                path.join('../../stylesheets', project, '/[name].css'), {
                    allChunks: true
                }
            )
        ];
    
        if (env == 'production') {
            // 线上模式的配置,去除依赖中重复的插件/压缩js/排除报错的插件
            plugins = _.union(defaultPlugins, [
                new webpack.optimize.DedupePlugin(),
                new webpack.optimize.UglifyJsPlugin({
                    sourceMap: false,
                    mangle: {
                        except: ['$', 'jQuery']
                    }
                }),
                new webpack.NoErrorsPlugin()
            ]);
        } else {
            plugins = _.union(defaultPlugins, []);
        }
    
        return plugins;
    };
    
    // postcss配置
    var getPostcss = function(env) {
        var postcss = [
            autoprefixer({ browers: ['last 2 versions', 'ie >= 9', '> 5% in CN'] }),
            flexibility,
            will_change,
            color_rgba_fallback,
            opacity,
            pseudoelements,
            sorting
        ];
    
        if (env == 'production') {
            // 线上模式的配置,css压缩
            return function() {
                return _.union([
                    cssnano({
                        // 关闭cssnano的autoprefixer选项,不然会和前面的autoprefixer冲突
                        autoprefixer: false, 
                        reduceIdents: false,
                        zindex: false,
                        discardUnused: false,
                        mergeIdents: false
                    })
                ], postcss);
            };
        } else {
            return function() {
                return _.union([], postcss);
            }
        }
    };
    
    // 作为函数导出配置,代码更简洁
    module.exports = function(env) {
        return {
            context: config.context,
            entry: config.src,
            output: {
                path: path.join(config.jsDest, project),
                filename: '[name].js',
                chunkFilename: '[name].[chunkhash:8].js',
                publicPath: '/assets/' + project + '/'
            },
            devtool: "eval",
            watch: false,
            profile: true,
            cache: true,
            module: {
                loaders: getLoaders(env)
            },
            resolve: {
                alias: getAlias(env)
            },
            plugins: getPlugins(env),
            postcss: getPostcss(env)
        };
    }
    
    // webpack任务
    var _ = require('lodash');
    var del = require('del');
    var webpack = require('webpack');
    var gulp = require('gulp');
    var plumber = require('gulp-plumber');
    var newer = require('gulp-newer');
    var logger = require('gulp-logger');
    
    var project = require('../lib/project')();
    var config = require('../config.' + project).webpack;
    var compileLogger = require('../lib/compileLogger');
    var handleErrors = require('../lib/handleErrors');
    
    
    // 生成js/css
    gulp.task('webpack', ['clean:webpack'], function(callback) {
        webpack(require('../webpack.config.js')(), function(err, stats) {
            compileLogger(err, stats);
            callback();
        });
    });
    
    // 生成js/css-监听模式
    gulp.task('watch:webpack', ['clean:webpack'], function() {
        webpack(_.merge(require('../webpack.config.js')(), {
            watch: true
        })).watch(200, function(err, stats) {
            compileLogger(err, stats);
        });
    });
    
    // 生成js/css-build模式
    gulp.task('build:webpack', ['clean:webpack'], function(callback) {
        webpack(_.merge(require('../webpack.config.js')('production'), {
            devtool: null
        }), function(err, stats) {
            compileLogger(err, stats);
            callback();
        });
    });
    
    // 清理js/css
    gulp.task('clean:webpack', function() {
        return del([
            config.jsDest,
            config.cssDest
        ], { force: true });
    });
    

    实践中遇到那些坑?

    如何组织gulp任务?

    由于gulp任务较多,并且每个核心任务都有关联任务,比如webpack的关联任务就有webpack/watch:webpack/build:webpack/clean:webpack,如何组织这些子任务是一个需要很小心的事情,出于一直以来的习惯:把关联的逻辑放在一起,所以我的方案是webpack相关的任务放到一个文件,然后定义了default/clean/watch/build四个入口任务来引用对应的子任务。

    webpack任务结构
    gulp怎么实现错误自启动

    使用watch模式可以更高效的开发,监听到改动就自动执行任务,但是如果过程中遇到错误,gulp就会报错并终止watch模式,必须重新启动gulp,简直神烦!
    利用gulp-plumber可以实现错误自启动,这样就能开心的在watch模式下开发且不用担心报错了。
    进一步结合gulp-notify,在报错时可以得到通知,便于发现问题。

    // 错误处理
    var notify = require("gulp-notify")
    
    module.exports = function(errorObject, callback) {
        // 错误通知
        notify.onError(errorObject.toString().split(': ').join(':\n'))
            .apply(this, arguments);
        
        // Keep gulp from hanging on this task
        if (typeof this.emit === 'function') {
            this.emit('end');
        }
    }
    
    // 任务
    var gulp = require('gulp');
    var plumber = require('gulp-plumber');
    
    var project = require('../lib/project')(); // 得到当前的后台项目
    var config = require('../config.' + project).views; // 读取配置文件
    var handleErrors = require('../lib/handleErrors');
    
    
    gulp.task('views', function() {
        return gulp.src(config.src)
            .pipe(plumber(handleErrors)) // 错误自启动
            .pipe(gulp.dest(config.dest));
    });
    
    gulp怎么处理同步任务和异步任务

    同步任务:gulp通过return stream的方式来结束当前任务并且把stream传递到下一个任务,大多数gulp任务都是同步模式。
    异步任务:实际项目中,有些任务的逻辑是异步函数执行的,这种任务的return时机并不能准确把控,通常需要在异步函数中调用callback()来告知gulp该任务结束,而这个callback什么都不是,就是传到该任务中的一个参数,没有实际意义。

    // 同步任务
    gulp.task('views', function() {
        return gulp.src(config.src)
            .pipe(plumber(handleErrors))
            .pipe(gulp.dest(config.dest));
    });
    
    // 异步任务
    gulp.task('webpack', function(callback) {
        webpack(config, function(err, stats) {
            compileLogger(err, stats);
    
            callback(); //异步任务的关键之处,如果没有这行,任务会一直阻塞
        });
    });
    
    webpack怎么抽出独立的css文件

    webpack默认是将css直接注入到html中,这种方法并不具有通用性,不推荐使用。
    结合使用extract-text-webpack-plugin,可以生成一个独立的css文件,extract-text-webpack-plugin会解析每一个require('*.css')然后处理输出一个独立的css文件。

    // webpack.config.js
    var ExtractTextPlugin = require("extract-text-webpack-plugin");
    
    module.exports = {
        entry: {
            'homes/index': 'pages/homes/index.js'
        },
        output: {
            filename: "[name].js"
        },
        module: {
            loaders: [{
                test: /\.css$/,
                loader: ExtractTextPlugin.extract("style-loader", "css-loader")
            }]
        },
        plugins: [
            new ExtractTextPlugin("[name].css")
        ]
    }
    
    webpack怎么抽出通用逻辑和样式

    没有webpack之前,想要抽离出公共模块完全需要手动维护,因为js是动态语言,所有依赖都是运行时才能确定,webpack可以做静态解析,分析文件之间的依赖关系,使用CommonsChunkPlugin就可以自动抽离出公共模块。

    // webpack.config.js
    var webpack = require('webpack');
    var ExtractTextPlugin = require("extract-text-webpack-plugin");
    
    module.exports = {
        entry: {
            'homes/index': 'pages/homes/index.js'
        },
        output: {
            filename: "[name].js"
        },
        module: {
            loaders: [{
                test: /\.css$/,
                loader: ExtractTextPlugin.extract("style-loader", "css-loader")
            }]
        },
        plugins: [
            //抽离公共模块,包含js和css
            new webpack.optimize.CommonsChunkPlugin("commons", "commons.js"), 
            new ExtractTextPlugin("[name].css")
        ]
    }
    
    webpack的watch模式

    webpack相对来说比较耗时,尤其是项目较复杂时,需要解析的文件较多。好奇心日报web项目首次全量执行webpack任务大概需要10s,所以必须引入增量构建。增量构建只需要简单的给webpack配置添加watch参数即可。


    webpack任务输出日志

    但是问题在于,如果给webpack-stream添加watch参数,webpack-stream的任务会阻塞其他的watch任务,最后导致其他任务的增量构建失效。
    所以如果要使用webpack的增量构建,需要使用原生的webpack方案!

    灵活的webpack入口文件

    webpack入口文件接收三种格式:字符串,数组,对象,对于多页应用场景,只有对象能够满足条件,所以我们把所有的入口文件全部列出来即可。
    但这种方案极不灵活,借鉴gulp的方案,是否可以读取某个文件下的所有入口文件呢?为了解决这个问题,自定义了一个函数来实现该功能。

    //获取文件夹下面的所有的文件(包括子文件夹)
    var path = require('path'),
        glob = require('glob');
    
    module.exports = function(dir, ext) {
        var files = glob.sync(dir + '/**/*.' + ext),
            res = {};
    
        files.forEach(function(file) {
            var relativePath = path.relative(dir, file),
                relativeName = relativePath.slice(0, relativePath.lastIndexOf('.'));
    
            res[relativeName] = './' + relativePath;
        });
    
        return res;
    };
    
    webpack的development/production配置合并

    webpack任务的development配置和production配置差异巨大,并且各自拥有专属的配置。
    由于webpack.config.js默认写法是返回一个对象,对象并不能根据不同条件有不同的输出,所以将webpack.config.js改成函数,通过传入参数来实现不同的输出。

    // 其中定义了getLoaders,getAlias,getPlugins,getPostcss函数
    // 都是为了解决development配置和production配置的差异问题
    // 既最大程度的复用配置,又允许差异的存在
    module.exports = function(env) {
        return {
            context: config.context,
            entry: config.src,
            output: {
                path: path.join(config.jsDest, project),
                filename: '[name].js',
                chunkFilename: '[name].[chunkhash:8].js',
                publicPath: '/assets/' + project + '/'
            },
            devtool: "eval",
            watch: false,
            profile: true,
            cache: true,
            module: {
                loaders: getLoaders(env)
            },
            resolve: {
                alias: getAlias(env)
            },
            plugins: getPlugins(env),
            postcss: getPostcss(env)
        };
    }
    
    webpack怎么线上模式异步加载js文件

    webpack可以将js代码分片,把入口文件依赖的所有模块打包成一个文件,但是有些场景下的js代码并不需要打包到入口文件中,更适合异步延迟加载,这样能最大程度的提升首屏加载速度。
    比如好奇心日报的登录浮层,这里面包含了复杂的图片上传,图片裁剪,弹框的逻辑,但是它没必要打包在入口文件中,反倒很适合异步延迟加载,只有当需要登录/注册的时候才去请求。


    图片上传裁剪

    我们可以通过webpack提供的requirerequire.ensure来实现异步加载,值得一提的是,除了指定的异步加载文件列表,webpack还会自动解析回调函数的依赖及指定列表的深层次依赖,并打包成一个文件。

    但是实际项目中还得解决浏览器缓存的问题,因为这些异步JS文件的时间戳是rails生产的,对于webpack是不可知的,也就是说请求这个异步JS文件并不会命中。
    为了解决这个问题,我们在rails4中自定义了一个rake任务:生产没有时间戳版本的异步JS文件。


    rake任务

    上图中还有一个小细节就是,这些异步JS文件有两个时间戳,前者为webpack时间戳,后者为rails时间戳,之所以有两个时间戳,是为了解决浏览器缓存的问题。

    简而言之就是:
    通过require/require.ensure,来生成异步JS文件,解决异步加载的问题。
    通过自定义rake任务,来生成没有rails时间戳的异步JS文件,解决webpack不识别rails时间戳的问题。
    通过webpack的chunkFileName配置,给异步JS文件加上webpack时间戳,解决浏览器缓存的问题。

    总结说点啥?

    前端工程化可以自动化处理一些繁复的工作,提高开发效率,减少低级错误。
    更重要的是,还是文章开头的说的,前端工程化最大的意义在于给我们新的视角去看待前端开发,让前端开发可以做更复杂、更有挑战的事情!

    这是前端工程化实践的第一篇博文,后续还有对之前零零散散的博文的总结。不过整体来说,webpack任务是最复杂、涵盖知识最多的一个任务。
    文章若有纰漏,欢迎大家指正。

    相关文章

      网友评论

      • 一轮明月随潮涌:我也正在尝试整合,就是在生产环境时,webpack打包和gulp其他任务是异步的,这里不知如何做成同步?我是把webpack的出口,当成gulp的入口,所以需要知道webpack何时打包完成。试过在gulp task里面return、回调都不行
      • 0c45406e8da8:看到你的文章,觉得写得很不错,也很有分享精神。我们侠课岛正好在找远程录制课程视频或图文教程的朋友,我们会给到课程的需求大纲,每一节课程需要你来详细展开写一些代码举例和讲解清楚,对经验积累和创新能力有一定的要求。有兴趣联系我。微信:zhimadt
      • 1ae826a4b8fd:你写过一篇webpack入门,又写过一篇gulp入门;
        估计你两个工具都只是刚刚入门;
        原则上两个工具不需要整合,才能满足开发;
        1ae826a4b8fd:@齐修_qixiuss 前朝楚水: @前朝楚水 gulp能实现的,webpack都能实现,原则上不需要过多的工具一起使用。各种整合,那还可以把grunt,parcel这些工具整进来 ,只是把项目整一堆工具而已
        齐修_qixiuss:@前朝楚水 是不是入门水平,请看下一篇文章。至于为什么要整合,也多去了解下webpack & gulp再说。
        另外不要轻易给别人下结论。
      • ishowman:gulp和webpack的区别到底是什么?还是搞不懂
      • 37f1904d59fa:非常感谢您的文章,看着看着发现了孙燕姿,我偶像哈哈哈哈,忍不住评论。
      • e5ce253ba0b3:var project = require('../lib/project')();
        var config = require('../config.' + project).webpack;
        我很像知道 这个 project.js 里面写了什么 我对于这一点不太理解。
        能否解答一下。
        齐修_qixiuss:@一心相上 恩,差不多就是这么个意思,由于好奇心的appfe是多项目共用的。如果appfe只有一个项目使用,一个配置文件就OK啦~
        e5ce253ba0b3:@齐修_qixiuss
        取得后台的参数 我模拟了一下 例如 cd appfe && gulp build --web 取得web
        我的project代码如下
        module.exports=function(){
        var argv;
        argv = process.argv[process.argv.length-1].replace("--","");
        return argv;
        }

        然后取出所有config.web.js下的配置参数是吗? 原理上。
        齐修_qixiuss:@一心相上 project很简单,就是根据命令行的参数判断是哪个项目
      • MMoooooon:很好的文章
      • 實現承諾:要想看看完整的demo 还是不太懂:sob:
      • NARUTO_86::+1:
        cloud_32a0:你的整合方案,貌似被说得很好,能否插件开源
      • LucyWoo:正需要,作者辛苦啦
      • 1263cdbd0f8f:回去研究
      • 5e90eed52b7c:可以发现demo吗 :stuck_out_tongue_closed_eyes:
      • 守得云开:好文章,lz能否把这个放到github上 :smile:
        _良丨道丨知_:看了,很激动,奈何能力有限,跪求demo
        lemonleo:@齐修_qixiuss LZ能否开源下code~ 看完了两篇完整的文章 本想自己写下 奈何能力有限啊~ :sob:
        齐修_qixiuss:@守得云开 最近比较忙,还没时间将这部分抽离出来,推荐阅读我最新的文章http://www.jianshu.com/p/2cc6a22c9ecc,两篇综合下来看,比较全面细致了,应该对整体会有新的认识。
      • 4da6815ff534:请教一下,楼主是怎么用gulp+webpack进行图片合并的呢?不想用base64格式
        齐修_qixiuss:@土豆丝Pro 类似的思路即可,核心就是appfe是前端开发目录,将构建出来的JS/CSS/Image等资源输出到Rails/Node等指定的目录即可
        4da6815ff534:@齐修_qixiuss 那node+webpack+gulp应该去架构呢
        齐修_qixiuss:@土豆丝Pro 可以参考我最新的博客里的sprites任务,可以完成自动合并,然后生成sprites图和css文件,http://www.jianshu.com/p/2cc6a22c9ecc。当然,对于常规图标的处理,推荐用iconfonts。
      • d128f7984695:请教一下,楼主既然用了less+autoprefixer,postcss剩下的插件引用的用途是什么呢
      • 茅己硍:谢谢分享
      • 8e2707c6a81b:挺好的,学习
      • e262c6a96c27:请问生产环境下代码生成需要多少时间,我们的项目gulp + vuejs 打包很慢,要20、30、40秒、一分钟这样子啊~
        蓝胖子又叫叮当猫: @齐修_qixiuss 楼主 如果两分钟呢?≥﹏≤
        e262c6a96c27:@齐修_qixiuss 好的~谢谢啦~
        齐修_qixiuss:@您所访问的用户因言论不当被禁言 这个应该的,我的也要40s左右,还是优化之后的,也是可以接受的范围。
      • 天外来人帅:这粉丝有点多啊 膜拜
      • 前端黑板报:感谢,很有帮助:relieved:
      • e4aa4b047ea6:现在学H5前景怎么样啊
        齐修_qixiuss:@爱的美丽传奇 前端的人才需求还是蛮大的(学得好有前景,学得不好就尴尬,任何行业都这样
      • 傅简书:前辈,可以分享下demo代码吗?
        小银杏:compileLogger代码可否分享下
        8e2707c6a81b:@Michael_Fu 已经很详细了,你这还要人家code...

      本文标题:gulp & webpack整合,鱼与熊掌我都要!

      本文链接:https://www.haomeiwen.com/subject/msrxjttx.html