一些老项目可能还依赖了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 \
webpack@1.15.0 \
babel-loader@6.4.1 \
babel-core@6.26.3 \
babel-preset-env@1.7.0
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个重要函数,loadPitch
,onLoadPitchDone
和runSyncOrAsync
。
(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/_babel-loader@6.4.1@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 uglify-js@2.7.5
新建 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 <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:1547:18)
at js_error (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:1555:11)
at croak (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2094:9)
at token_error (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2102:9)
at unexpected (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2108:9)
at semicolon (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2128:56)
at simple_statement (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2319:73)
at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2188:19)
at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2141:24)
at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>: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
> debug-webpack@1.0.0 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压缩也会报错。