gulp & webpack整合,魚與熊掌我都要!

來源:http://www.jianshu.com/p/9724c47b406ccss

爲何須要前端工程化?

前端工程化的意義在於讓前端這個行業由野蠻時代進化爲正規軍時代,近年來不少相關的工具和概念誕生。好奇心日報在進行前端工程化的過程當中,主要的挑戰在於解決以下問題:
✦ 如何管理多個項目的前端代碼?
✦ 如何同步修改複用代碼?
✦ 如何讓開發體驗更爽?html

項目實在太多

以前寫過一篇博文 如何管理被多個項目引用的通用項目?,文中提到過好奇心日報的項目偏多(PC/Mobile/App/Pad),要爲這麼多項目開發前端組件並維護是一個繁瑣的工做,而且會有不少冗餘的工做。前端

更好的管理前端代碼

前端代碼要適配後臺目錄的規範,原本能夠很美好的前端目錄結構被拆得四分五裂,前端代碼分散不便於管理,而且開發體驗很不友好。
而有了前端工程化的概念,前端項目和後臺項目能夠完全分離,前端按本身想要的目錄結構組織代碼, 而後按照必定的方式構建輸出到後臺項目中,簡直完美(是否是有種後宮佳麗三千的感受)。node

技術選型

調研了市場主流的構建工具,其中包括gulp、webpack、fis,最後決定圍繞gulp打造前端工程化方案,同時引入webpack來管理模塊化代碼,大體分工以下:
gulp:處理html壓縮/預處理/條件編譯,圖片壓縮,精靈圖自動合併等任務
webpack:管理模塊化,構建js/css。jquery

至於爲何選擇gulp & webpack,主要緣由在於gulp相對來講更靈活,能夠作更多的定製化任務,而webpack在模塊化方案實在太優秀(不由自主的讚美)。webpack

怎麼設計前端項目目錄結構?

抽離出來的前端項目目錄結構以下git


前端項目結構

appfe目錄:appfe就是前面提到的前端項目,這個項目主要包含兩部分:前端代碼、構建任務
appfe > gulp目錄:包含了全部的gulp子任務,每一個子任務包含相關任務的全部邏輯。
appfe > src目錄:包含了全部前端代碼,好比頁面、組件、圖片、字體文件等等。
appfe > package.json:這個不用說了吧。
appfe > gulpfile.js:gulp入口文件,引入了全部的gulp子任務。github

理想很豐滿,現實卻很骨感,這麼美好的願望,在具體實踐過程當中,註定要花很多心思,要踩很多坑。
好奇心日報此次升級改造即將上線,終於也有時間把以前零零碎碎的博文整合在一塊兒,而且結合本身的體會分享給你們,固然將來可能還會有較大的調整,這兒拋磚引玉,你們能夠參考思路。web

gulp 是什麼?

gulp是一個基於流的構建工具,相對其餘構件工具來講,更簡潔更高效。
Tip:以前寫過一篇gulp 入門,能夠參考下,若是對gulp已經有必定的瞭解請直接跳過。json

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任務是最複雜、涵蓋知識最多的一個任務。文章如有紕漏,歡迎你們指正。

相關文章
相關標籤/搜索