webpack群侠传(十一):webpack 1.x

一些老项目可能还依赖了webpack 1.x,
虽然我们对webpack 4.x有一些了解了,但是1.x的旧逻辑还是有些不同的。

本文记录一下webpack 1.15.0的调试过程。

1. webpack 1.15.0

webpack 1.x最新的版本是 1.15.0,
为了进行调试,我们得先新建一个webpack 1.x项目,并指定babel的历史版本。

1.1 新建项目

$ mkdir debug-webpack
$ cd debug-webpack
$ npm init -f

1.2 安装依赖

$ npm i -D \
[email protected] \
[email protected] \
[email protected] \
[email protected]

1.3 webpack.config.js

const path = require('path');

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'src/index.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js',
    },
    module: {
        loaders: [
            { test: /\.js$/, loader: 'babel-loader' },
        ]
    },
};

注意这里的loader配置和webpack 4.x不同。

(1)webpack 4.x
配置webpack.config.js,module.rules

...,
module.exports = {
    ...,
    module: {
        rules: [
            { test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
        ]
    },
};

(2)webpack 1.x
配置webpack.config.js,module.loaders

...
module.exports = {
    ...,
    module: {
        loaders: [
            { test: /\.js$/, loader: 'babel-loader' },
        ]
    },
};

1.4 .babelrc

webpack 1.x 除了配置webpack.config.js之外,还需要配置babel。
在项目根目录中新建.babelrc文件。

{
    "presets": [
        "env"
    ]
}

1.5 npm scripts

打开pakcage.json,添加npm scripts,

{
  ...,
  "scripts": {
    ...,
    "build": "webpack",
  },
  ...,
}

1.6 新建src/index.js文件

alert();

1.7 查看编译结果

$ npm run build
/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};

/******/    // The require function
/******/    function __webpack_require__(moduleId) {

/******/        // Check if module is in cache
/******/        if(installedModules[moduleId])
/******/            return installedModules[moduleId].exports;

/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            exports: {},
/******/            id: moduleId,
/******/            loaded: false
/******/        };

/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/        // Flag the module as loaded
/******/        module.loaded = true;

/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }


/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;

/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;

/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";

/******/    // Load entry module and return exports
/******/    return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

    "use strict";

    alert();

/***/ })
/******/ ]);

1.8 代码压缩

webpack 1.x默认对目标代码是不压缩的,
要想得到压缩后的代码,我们需要添加UglifyJsPlugin插件。

...,
const webpack = require('webpack');

module.exports = {
    ...,
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: true,
        }),
    ],
};
$ npm run build
!function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){"use strict";alert()}]);

2. debug webpack

2.1 新建debug.js文件

首先我们要有能力对webpack进行调试,在项目根目录下我们新建一个debug.js文件,
这个文件和webpack 4.x的写法一样。

const webpack = require('webpack');
const options = require('./webpack.config');

const compiler = webpack(options);
compiler.run(true, (...args) => { });

然后在最后一行compiler.run位置打个断点,保持焦点在本文件中,按F5进行调试。

2.2 Compiler & Compilation

点击单步调试后,代码进入到了Compiler.js中,第166行,

Compiler.prototype.run = function (callback) {
  ...
};

这是ES5的写法。

轻车熟路,我们来看看资源是怎么加载的,
在webpack 4.x中,compiler通过make这个hooks调用了compilation.addEntry,这里也是如此。
于是,我们打开Compilation.js找到addEntry方法,位于Compilation.js 第423行,

Compilation.prototype.addEntry = function process(context, entry, name, callback) {
  this._addModuleChain(context, entry, function (module) {
    ...
  }.bind(this), function (err, module) {
    ...
  }.bind(this));
};

同样是调了compilation._addModuleChain,位于Compilation.js 第336行。
后面业务逻辑和webpack 4.x都大体相似,我们就不详细说明了。

在资源载入阶段,比较重要的两件事,载入loader,以及使用loader载入源码。
载入loader的过程大同小异,下面我们直接看loader载入源码的过程。

2.3 载入资源

我们知道,compilation._addModuleChain会调用moduleFactory创建一个module,
然后调用compilation.buildModule载入资源,位于Compilation.js 第398行,

Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
    ...,
    moduleFactory.create(context, dependency, function(err, module) {
        ...,
        this.buildModule(module, function(err) {
            ...,
        }.bind(this));
        ...,
    }.bind(this));
};

然后compilation.buildModule又调用了,module.build方法,位于NormalModule.js 第81行,

NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
    ...,
    return this.doBuild(options, compilation, resolver, fs, function(err) {
        ...,
    }.bind(this));
};

然而doBuild的实现却不在webpack中,在weboack-core v0.6.9里,位于NormalModuleMixin.js 第49行,

NormalModuleMixin.prototype.doBuild = function doBuild(options, moduleContext, resolver, fs, callback) {
    ...,
    function runSyncOrAsync(fn, context, args, callback) {
        ...,
        try {
            var result = (function WEBPACK_CORE_LOADER_EXECUTION() { return fn.apply(context, args) }());
            ...,
        } catch(e) {
            ...,
        }
    }

    (function loadPitch() {
        ...,
        runSyncOrAsync(l.module.pitch, privateLoaderContext, [remaining.join("!"), pitchedLoaders.join("!"), l.data = {}], function(err) {
            ...,
        }.bind(this));
    }.call(this));

    function onLoadPitchDone() {
        ...,
        if(resourcePath) {
            ...,
            fs.readFile(resourcePath, function(err, buffer) {
                ...,
                nextLoader(null, buffer);
            });
        } else
            nextLoader(null, null);
    }

    function nextLoader(err/*, paramBuffer1, param2, ...*/) {
        ...,
        runSyncOrAsync(l.module, privateLoaderContext, args, function() {
            ...,
        });
    }
};

这个函数非常长,有269行,
总共涉及以下3个重要函数,loadPitchonLoadPitchDonerunSyncOrAsync

(1)loadPitch
首先会执行loadPitch代码逻辑,它会通过require载入loader,

(function loadPitch() {
  ...,
  if(typeof __webpack_modules__ === "undefined") {
    if(require.supportQuery) {
      ...,
    } else {
      try {
        l.module = require(l.path);
      } catch (e) {
        ...,
      }
    }
  } else if(typeof __webpack_require_loader__ === "function") {
    ...,
  } else {
    ...,
  }
  ...,
  if(typeof l.module.pitch !== "function") return loadPitch.call(this);
  ...,
  runSyncOrAsync(l.module.pitch, privateLoaderContext, [remaining.join("!"), pitchedLoaders.join("!"), l.data = {}], function(err) {
    ...,
  }.bind(this));
}.call(this));

其中,l.path的值为,

~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js

注意到,loadPitch其实是一个递归函数。
载入loader之后,又递归调用了自己两次,最终调用了onLoadPitchDone

(2)onLoadPitchDone
这个是doBuild的内部函数,位于第244行,
它会读取源代码文件,然后调用nextLoader去载入它,位于第259行,

function onLoadPitchDone() {
  ...,
  if(resourcePath) {
    ...,
    fs.readFile(resourcePath, function(err, buffer) {
      ...,
      nextLoader(null, buffer);
    });
  } else
    ...,
}

其中,resourcePath的值为,

~/Test/debug-webpack/src/index.js

这就是我们项目中的源文件。

(3)runSyncOrAsync
nextLoader也位于doBuild函数内部,位于第265行,

function nextLoader(err/*, paramBuffer1, param2, ...*/) {
  ...,
  runSyncOrAsync(l.module, privateLoaderContext, args, function() {
    loaderContext.inputValue = privateLoaderContext.value;
    nextLoader.apply(null, arguments);
  });
}

最终它调用了runSyncOrAsync,这个函数名在webpack 4.x中也遇到过,
就是这个函数调用了loader,用loader载入源码,位于第155行,

function runSyncOrAsync(fn, context, args, callback) {
  ...,
  try {
    var result = (function WEBPACK_CORE_LOADER_EXECUTION() { return fn.apply(context, args) }());
    ...,
  } catch(e) {
    ...,
  }
}

在这里,调用loader的函数名叫做WEBPACK_CORE_LOADER_EXECUTION
还记得么,在webpack 4.x中,这个函数名是LOADER_EXECUTION

它会跳入到 babel-loader v6.4.1 index.js中,位于第99行,

module.exports = function(source, inputSourceMap) {

这里我们就略过不表了。

2.4 代码压缩

在webpack 4.x中,代码压缩是使用uglifyjs-webpack-plugin来完成的,
webpack首先在make阶段载入资源,之后调用了compilation.seal
在optimize-chunk-asssets这个hooks中实现了代码压缩。

其中,uglifyjs-webpack-plugin是webpack 4.x的一个内置插件,它实现了optimize-chunk-asssets,
内部调用了uglify-es进行了代码压缩。

而在webpack 1.x中,这件事略有不同。

在webpack 1.x中,默认是不压缩目标代码的,除非我们手动配置UglifyJsPlugin插件,
这在上文第1.8节我们已经介绍过了,

...,
const webpack = require('webpack');

module.exports = {
    ...,
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: true,
        }),
    ],
};

UglifyJsPlugin也是一个webpack的内置插件,位于lib/optimize/UglifyJsPlugin.js中,
我们看到在第33行,它实现了optimize-chunk-asssets,

compilation.plugin("optimize-chunk-assets", function(chunks, callback) {

它先在第82行调用uligfy-js v2.7.5的uglify.parse,将未压缩的代码转换成ast

var ast = uglify.parse(input, {
    filename: file
});

注: uglify-js和uglify-es是不同的,是两个npm包。

然后对ast进行操作,例如压缩和混淆,

if(options.compress !== false) {
  ast.figure_out_scope();
  var compress = uglify.Compressor(options.compress); // eslint-disable-line new-cap
  ast = ast.transform(compress);
}
if(options.mangle !== false) {
  ast.figure_out_scope(options.mangle || {});
  ast.compute_char_frequency(options.mangle || {});
  ast.mangle_names(options.mangle || {});
  if(options.mangle && options.mangle.props) {
    uglify.mangle_properties(ast, options.mangle.props);
  }
}

还有生成source map相关的逻辑,
JavaScript
if(options.sourceMap !== false) {
var map = uglify.SourceMap({ // eslint-disable-line new-cap
file: file,
root: ""
});
output.source_map = map; // eslint-disable-line camelcase
}

最终将压缩后的代码通过stream写入到compilation.assets变量,

var stream = uglify.OutputStream(output); // eslint-disable-line new-cap
ast.print(stream);
...,
asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
  new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :
  new RawSource(stream));

其中,file是,

index.js

compilation.assets['index.js']._value的值是,

!function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){"use strict";alert()}]);

就是压缩后的目标代码了。

注: 如果webpack.config.js中不配置UglifyJsPlugin插件,这个插件就不会被加载了,
optimize-chunk-asssets hooks触发的时候,就不会调用UglifyJsPlugin插件中的代码逻辑了。

3. uglify-js

上文中,我们看到webpack 1.x使用了uglify-js v2.7.5进行代码压缩,
然而,uglify-js v2.7.5却不能用来压缩ES6。

3.1 let引发的错误

我们新建一个项目来试试,并指定版本安装uglify-js,

$ mkdir debug-uglify-js
$ cd debug-uglify-js
$ npm init -f
$ npm i -S [email protected] 

新建 index.js,

const uglify = require('uglify-js');

const source = 'let a = 1;';
const ast = uglify.parse(source, {
    filename: 'index.js',
});

最后执行它,就报错了,

$ node index.js

undefined:1555
    throw new JS_Parse_Error(message, filename, line, col, pos);
    ^
Error
    at new JS_Parse_Error (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :1547:18)
    at js_error (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :1555:11)
    at croak (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2094:9)
    at token_error (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2102:9)
    at unexpected (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2108:9)
    at semicolon (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2128:56)
    at simple_statement (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2319:73)
    at eval (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2188:19)
    at eval (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2141:24)
    at eval (eval at  (~/Test/debug-uglify-js/node_modules/[email protected]@uglify-js/tools/node.js:28:1), :2909:23)

这是因为let a = 1;,包含了let,它是ES6代码,uglify-js v2.7.5不能识别它。
不过令人奇怪的是,const a = 1;,却可以被识别。

3.2 babel-loader

上文我们没有强调的一点是,babel-loader加载后的代码并不是源代码,而是转换过后的ES5代码。
因此,上文我们配置了babel-loader之后,源代码甚至源代码依赖的node_modules模块,都会被转为ES5,
再进行uglify-js v2.7.5进行压缩就不会报错了。

如果我们把babel-loader删掉,uglify-js v2.7.5同样也会报错。
这需要同时满足以下几个条件,

(1)让babel-loader失效。(只需要删除.babelrc即可)
(2)配置UglifyJsPlugin到webpack.config.js中,并启用compress,参考本文第1.8节。
(3)源码或者它依赖的node_modules模块中包含ES6代码。

例如,我们修改src/index.js文件内容为,

let a = 1;

然后执行一下构建,

$ npm run build

> [email protected] build ~/Test/debug-webpack
> webpack

Hash: 99cc00b93a9d989cb93e
Version: webpack 1.15.0
Time: 425ms
   Asset     Size  Chunks             Chunk Names
index.js  1.49 kB       0  [emitted]  index
    + 1 hidden modules

ERROR in index.js from UglifyJs
SyntaxError: Unexpected token: name (a) [./src/index.js:1,4]

3.3 exclude

很多项目中为了提高编译速度,会配置babel-loader的exclude属性,

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'src/index.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js',
    },
    module: {
        loaders: [
            { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
        ]
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: true,
        }),
    ],
};

以上webpack.config.js中,我们配置了babel-loader的exclude属性,
对于node_moduels目录中的文件,就会直接加载,不通过loader转换。

值得注意是的,如果node_modules中包含ES6代码,
载入后再通过uglify-js v2.7.5压缩也会报错。


参考

webpack v1.15.0
webpack-core v0.6.9
uglify-js v2.7.5

你可能感兴趣的:(webpack群侠传(十一):webpack 1.x)