Webpack Workflow

一、概述

开发语言: HTML + [email protected] + [email protected]
依赖项: 第三方组件flv.js + 第三方开源插件
构建工具: [email protected]

目的: 编译混淆打包源码及素材,并打包合并第三方文件成单个文件。

__ 问题:__
1、 如何对源码文件混淆打包,第三方文件仅打包,最终再将这两部分打包成单个文件?
2、如何对源码部分进行深度混淆?
3、 如何设计多种工作流适应多种开发调试环境?

__ 设想:__
1、定制webpack工作流,大致如下图:

① => ③ => ④ (打包编译源码, 生成 player.js)
==================== } => ③ => ⑥ (打包合并)
② => ③ => ⑤ (打包合并第三方代码, 生成library.js)

2、可以考虑webpack自带的插件UglifyJS 或者 第三方Google Closure Compiler。
3、可以使用在package.json 中配置npm scripts来实现多命令工作流。

二、定制工作流

2.1 webpack 工作流

在项目架构设计开始,还没有正式发布 webpack2,所以一直使用的是 webpack。webpack2与webpack配置等部分会有些差异,短期不考虑迁移。后期可以考虑迁移到webapck2。如有兴趣,也可以直接查看 webpack2 官方文档。

下图简要说明了Webpack的构建作用。

webpack工作流的例如下图:

上图这样的项目结构如下图(源文件app.js, cats.js 打包生成app.bundle.js):

配置package.json:

const webpack = require('webpack');

module.exports = {
    entry: './src/app.js',
    output: {
        path: './bin',
        filename: 'app.bundle.js',
    },
    module: {
        loaders: [{
            test: /\.js?$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
        }]
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false,
            },
            output: {
                comments: false,
            },
        }),
    ]
}

2.2 定制项目工作流

__ 目标工作流: __
1、编译混淆打包源码及资源,包括TypeScript, Less, images等,生成文件player.js。
2、打包合并第三方组件和第三方插件,仅压缩,不做其他任何处理,生成文件library.js
3、最后将player.js和library合并打包成player.min.js, 不做任何处理。

__ 初步想法:__
尝试通过webpack plugins来分情况来实现工作流。

__ 遇到问题:__
打包合并是通过webpack原生来实现的,并没有插件来实现编译打包之后能再次自定义打包的功能。

__ 解决方案:__
那么我们可能就需要自己定制一个插件。

Webpack提供了API可以让开发者自定义开发Webpack工作流中的Plugins。地址为: http://webpack.github.io/docs/plugins.html

webpack的插件工作流中总共分成这么几个步骤:
图片地址:http://7xoebg.com1.z0.glb.clouddn.com/170221174303.jpeg?imageMogr2/strip

具体可以查看Taobao FED的这篇总结

可以将自定义的事件异步绑定在某个工作流上。

__ 自定义webpack plugin __

新建webpack.plugin.concatenate.js文件:

 // webpack.plugin.concatenate.js
function Concatenate (options) {
    this.options = options;
}

Concatenate.prototype.apply = function (compiler) {

    compiler.plugin('emit', function (compilation, callback) {
        var _output = '',
            _filename = '';

        compilation.chunks.forEach(function(chunk) {
            // Explore each module within the chunk (built inputs):
            chunk.modules.forEach(function(module) {
                // Explore each source file path that was included into the module:
            });

            // Explore each asset filename generated by the chunk:
            chunk.files.forEach(function(filename) {
                // Get the asset source for each file generated by the chunk:
                var source = compilation.assets[filename].source();

                _filename = filename;
                _output += source + '\n';

            });
        });

        var filenameArr = _filename.split('.');
        filenameArr.splice(filenameArr.length - 1, 0, 'min');
        _filename = filenameArr.join('.');

        if (this.options['name']) {
            _filename = this.options['name'];
        }

        if (this.options['banner']) {
            var _banner = this.options['banner'],
                _bannerArr =  _banner.split('\n'),
                _comments = '/*!';

            _bannerArr.forEach(function (_b) {
                _comments += '\n* ' + _b;
            });

            _comments += '\n*/\n\n';

            _output = _comments + _output;
        }

        compilation.assets[_filename] = {
            source: function() {
                return _output;
            },
            size: function() {
                return _output.length;
            }
        };


        callback();

    }.bind(this));

};

module.exports = Concatenate;

使用:

 // webpack.config.js
 var Concatenate = require('./webpack.plugin.concatenate');
 var library = './src/library.js',
     player = './src/index.js';

 var config = {
    entry: {
        library: library,
        player: player
    },

    output: {
        path: path.resolve(__dirname, 'dist'),
        publishPath: '/',
        filename: '[name].js'
    },

    // module: ...

   plugins: [
     // ...,
     new Concatenate({
        name: 'player.min.js',
        banner: 'Copyright Tuzk1ss 2016 - 2017' 
     }) 
   ] 
}

输出文件例如:
生成dist文件夹内文件格式如下:

其中,player.min.js类似格式如下:

三、深度混淆

深度混淆是指将属性名等各种可以被压缩混淆的命名都压缩。有两种解决方案:Webpack自带Webpack.Plugin.UglifyJS 或 Google Closure Complier

简单使用之后发现,使用UglifyJS简单方便,但是当前版本(@2.7.5)无法默认识别各种原生JavaScript方法,以及定制化比较麻烦。
使用Google Closure Complier虽然耗时相对UglifyJS多一些,但是对原生JavaScript方法,和自定义不压缩混淆属性比较友好…

所以考虑之后,采用Google Closure Complier。

需要新建一个自定义未压缩命名文件,例如, webpack.externs.js。 可以在这个链接 https://github.com/google/closure-compiler/tree/master/contrib/externs 找到 你在项目中使用到的第三方JavaScript类库等… 例如jQuery

// webpack.externs.js

// customize proterties
var set, get, value, ...

// jQuery proterties
/**
 * @typedef {(Window|Document|Element|Array<Element>|string|jQuery|
 *     NodeList)}
 */
var jQuerySelector;

/** @typedef {function(...)|Array<function(...)>} */
var jQueryCallback;
...

在webpack.config.js中配置改插件:

// webpack.config.js

config.plugins.push(new ClosureCompilerPlugin({
        exclude: /library\.js/i,
        compiler: {
            compilation_level: 'ADVANCED',
            externs: ['./webpack.externs.js']
        }
    }));

四、使用NPM Script多任务处理

4.1 Development Static Mode

本地开发静态模式, 需要每次手动刷新页面。在package.json中”scripts”字段添加:

// package.json scripts
"webpack": "webpack "

// run
npm run webpack

4.2 Development Hot Load Mode

本地开发热加载模式,自动更新页面。在package.json中”scripts”字段添加:

// package.json scripts
"dev": "webpack-dev-server",

// run
npm run dev

4.3 Release Static Mode

远程代理混淆静态模式,需要每次手动刷新页面。在package.json中”scripts”字段添加:
__使用兼容类Unix和windows node环境npm插件: cross_env __

// package.json scripts
"release": "cross-env NODE_ENV=release webpack",

// run
npm run release

___在webpack.config.js能使用 process.env.NODE_ENV 获取到npm scripts中的变量, 例如: __

// webpack.config.js
... 
if ('release' === process.env.NODE_ENV) {  
   // 混淆模式
} else if ('publish' === process.env.Node_ENV) { 
   // 发布模式
} else { 
   // 普通模式
} 

4.4 Release Hot Load Mode

远程代理混淆热加载模式。在package.json中”scripts”字段添加:
__使用兼容类Unix和windows node环境npm插件: cross_env __

// package.json scripts
 "dev-release": "cross-env NODE_ENV=release webpack-dev-server",

 // run
 npm run dev-release

在webpack.config.js同上, #4.5

4.5 Publish Mode

发布模式。具体过程可以自己定制,需要依次运行多条npm脚本的话可以使用 && 符号链接。例如:

// copy 暂未定义
npm run release && npm run copy

具体过程比较复杂,大致为混淆打包 => 复制 => zip => 输出过程时间。 如下图: (gif 大约30s, 请耐心等待)

四、优化及改进

  • 输出换行及颜色兼容window平台
  • 优化源码压缩混淆时间