如何进行webpack调试,可以参考这里。
//webpack.config.js
const path = require("path");
module.exports = {
entry:"./src/index.js",
output:{
filename:"bundle.js",
path:path.resolve(__dirname,"dist")
}
}
1、node_modules/webpack/bin/webpack.js
来看看node_modules/webpack/bin/webpack.js
的核心代码。
const path = require("path");
//"d:\workspace\webpack\demo\node_modules\webpack-cli\package.json"
const pkgPath = require.resolve("webpack-cli/package.json");
const pkg = require(pkgPath);
//"d:\workspace\webpack\demo\node_modules\webpack-cli\bin\cli.js"
require(path.resolve(path.dirname(pkgPath),pkg.bin["webpack-cli"]));
require.resolve
和path.resolve
功能一样,解析后都会得到一个绝对路径。
加载node_modules/webpack-cli/package.json
,从中获取bin
属性值,进而加载node_modules/webpack-cli/bin/cli.js
2、node_modules/webpack-cli/bin/cli.js
继续探探node_modules/webpack-cli/bin/cli.js
主要做了些什么。
let options;
try{
options = require("./utils/convert-argv")(argv);
}catch(err){
}
function processOptions(options){
const webpack = require("webpack");
try {
compiler = webpack(options);
} catch (err) {
}
function compilerCallback(err, stats) {
}
compiler.run((err, stats) => {
compilerCallback(err, stats);
});
}
processOptions(options);
在上面这部分代码中,大家很容易发现这样一种写法:在一个函数A中定义另一个函数B,并在A中调用B。
function A(){
function B(){
}
B();
}
A();
这也是大家常说的闭包,用来延长作用域链的。读webpack源码的过程中,会发现很多模块在其实现过程中都是这样做的。
options = require("./utils/convert-argv")(argv)
require("./utils/convert-argv")
,require了convert-argv这么一个模块,后面紧跟着(argv)
,直接传参调用了。可见,convert-argv这个模块对外提供的接口是一个函数。
一句话说清楚这个函数的功能:找到webpack配置文件>加载配置文件,获取配置参数>处理各项配置参数,最后返回了options
。
下面代码是这个函数的一个大概实现,其中省略了"找"的过程,require(path.resolve(process.cwd(),configPath))
即加载配置文件,processConfiguredOptions(options)
即处理各项配置参数。
//convert-argv.js
const path = require("path");
module.exports = function(){
let configFiles = [];
let configFileLoaded = false;
const options = [];
const configFileName = "webpack.config.js";
//D:\workspace\webpack\demo\webpack.config.js
const resolvedPath = path.resolve(configFileName);
configFiles.push({
path:resolvedPath
});
const requireConfig = function requireConfig(configPath){
let options = (function WEBPACK_OPTIONS(){
return require(path.resolve(process.cwd(),configPath));
})();
return options;
}
if(configFiles.length>0){
configFiles.forEach(function(file){
options.push(requireConfig(file.path));
})
}
configFileLoaded = true;
if(!configFileLoaded){
return processConfiguredOptions(options);
}else if(options.length==1){
return processConfiguredOptions(options[0]);
}else {
return processConfiguredOptions(options);
}
function processConfiguredOptions(options){
if(Array.isArray(options)){
options.forEach(processOptions);
}else {
processOptions(options);
}
if(!options.context){
options.context = process.cwd();
}
return options;
}
function processOptions(options){
}
}
const webpack = require(“webpack”)
加载的模块是node_modules\webpack\lib\webpack.js
,该模块提供的对外接口是函数webpack
,接受两个参数:options
和callback
。
compiler = webpack(options)
webpack(options)
,调用webpack函数并传入参数options
时,便进入了node_modules\webpack\lib\webpack.js
模块。
3、node_modules\webpack\lib\webpack.js
这个模块中最重要的就是webpack
函数了,输入options
,输出compiler
对象。
webpack
函数首先会检查options
语法,有错则抛出;语法OK则往下执行。
然后判断options
是一个数组(其中的每个元素都是一个普通对象),还是一个普通对象。如果是数组,就会递归webpack(options)
。
本例中,options
是一个普通对象,所以这里就分析普通对象这种情况。
const webpack = (options,callback) => {
let compiler;
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
compiler.options = options;
if(options.plugins && Array.isArray(options.plugins)){
for(const plugin of options.plugins){
if(typeof plugin === 'function'){
plugin.call(compiler,compiler);
}else {
plugin.apply(compiler);
}
}
}
compiler.options = new WebpackOptionsApply().process(options,compiler);
}
options = new WebpackOptionsDefaulter().process(options)
虽然我们在webpack.config.js
中简简单单地配置了entry
和output
这两个参数。
经options = require("./utils/convert-argv")(argv)
处理后也只多了context
。
但在new WebpackOptionsDefaulter().process(options)
的这个过程,webpack默默地给我们添加了很多参数,真的很多。
const OptionsDefaulter = require("./OptionsDefaulter");
class WebpackOptionsDefaulter extends OptionsDefaulter {
constructor(){
super();
this.set("entry", "./src");
this.set("devtool", "make", options =>
options.mode === "development" ? "eval" : false
);
this.set("context", process.cwd());
this.set("module", "call", value => Object.assign({}, value));
this.set("module.rules", []);
this.set("output", "call", (value, options) => {
if (typeof value === "string") {
return {
filename: value
};
} else if (typeof value !== "object") {
return {};
} else {
return Object.assign({}, value);
}
});
}
}
entry
,devtool
,context
,module
,module.rules
等,这里只是罗列了一小部分。
呃,等等,我们自己不是在webpack.config.js
中配置了entry
吗?怎么还有个默认的entry
呀?
这就要说到webpack的参数合并策略了,这个策略其实就是WebpackOptionsDefaulter
的父类OptionsDefaulter
的process
方法,该方法会根据配置类型(undefined|make||call|append
)来决定属性是使用用户自己配置的,还是使用默认值,还是调用方法经计算得到。
呃,那entry
是啥配置类型,它最终采用的是我们自己配置的吗?
我们再回到WebpackOptionsDefaulter
,这个类只有一个构造函数,而且这个构造函数里整齐划一地调用着this.set()
,有的传入两个参数,有的传了三个参数。
只传入两个参数时,
第一个参数:属性名
第二个参数:属性值
结果是:
this.default[属性名] = 属性值 //默认属性
this.config[属性名] = undefined //配置类型
合并策略:
配置类型为undefined:options自己有,则使用自有属性;没有,则使用默认属性;
传入三个参数时
第一个参数:属性名
第二个参数:配置类型,"make" | "call" | "append"
第三个参数:计算方式,是一个函数
结果是:
this.defaults[属性名] = 计算方式
this.config[属性名] = 配置类型 "make" | "call" | "append"
合并策略:
配置类型为make:options自己有,则使用自有属性;没有,则调用默认方法,根据其他属性计算得到该属性值;
配置类型为call:不论options有没有,都会调用默认方法,重新计算该属性值。
配置类型为append:不论options有没有,都会追加默认值。
const getProperty = (obj,path) => {
let name = path.split(".");
for(let i=0;i<name.length-1;i++){
obj = obj[name[i]];
if(typeof obj!== 'object' || !obj || Array.isArray(obj)) return;
}
return obj[name.pop()];
}
const setProperty = (obj,path,value) => {
let name = path.split(".");
for(let i=0;i<name.length-1;i++){
if(typeof obj[name[i]] !== 'object' && obj[name[i]]!==undefined) return
if(Array.isArray(obj[name[i]])) return;
if(!obj[name[i]]) obj[name[i]] = {};
obj = obj[name[i]];
}
obj[name.pop()] = value;
}
class OptionsDefaulter{
constructor(){
this.defaults = {};
this.config = {}
}
process(options){
options = Object.assign({},options);
for(let name in this.defaults){
switch(this.config[name]){
case undefined:
if(getProperty(options,name) === undefined){
setProperty(options,name,this.defaults[name]);
}
break;
case "call":
setProperty(options,name,this.defaults[name].call(this,getProperty(options,name),options));
break;
case "make":
if(getProperty(options,name) === undefined){
setProperty(options,name,this.defaults[name].call(this,options));
}
break;
case "append":
let oldValue = getProperty(options,name);
if(!Array.isArray(oldValue)){
oldValue = [];
}
oldValue.push(...this.defaults[name]);
setProperty(options,name,oldValue);
break;
default:
throw new Error("Defaulter cannot process " + this.config[name]);
}
}
return options;
}
set(name,config,def){
if(def !== undefined){
this.defaults[name] = def;
this.config[name] = config;
}else{
this.defaults[name] = config;
delete this.config[name];
}
}
}
compiler = new Compiler(options.context)
这里新建了一个Compiler实例
,compiler
。截取了部分代码,来看看Compiler实例有啥。
嗯,Compiler
继承自父类Tapable
,定义了有很多属性和方法,其中的一个属性hooks
很特别,特别长。不着急哈,一步一步来看。
//node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable{
constructor(context){
super();
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
assetEmitted: new AsyncSeriesHook(["file", "content"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
contextModuleFactory: new SyncHook(["contextModulefactory"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
failed: new SyncHook(["error"]),
invalid: new SyncHook(["filename", "changeTime"]),
watchClose: new SyncHook([]),
infrastructureLog: new SyncBailHook(["origin", "type", "args"])
//省略不写了
}
this._pluginCompat.tap("Compiler", options => {
switch (options.name) {
case "additional-pass":
case "before-run":
case "run":
case "emit":
case "after-emit":
case "before-compile":
case "make":
case "after-compile":
case "watch-run":
options.async = true;
break;
}
});
//部分属性
this.outputFileSystem = null;
this.inputFileSystem = null;
this.recordsInpuPath = null;
this.recordsOutputPath = null;
this.options = {};
this.context = context;
this.running = false;
this.watchMode = false;
}
//部分方法
run(callback) {}
createCompilation() {}
newCompilation(params) {}
createNormalModuleFactory() {}
newCompilationParams(){}
compile(callback){}
}
首先,super()
,即调用父类Tapable
的构造函数,Tapable
的实现如下,它的第一行
new SyncBailHook(['options'])
和上面hooks
的第一个new SyncBailHook(["compilation"])
几乎一样,仅有的差别是传入的参数不同。
//Tapable
function Tapable(){
this._pluginCompat = new SyncBailHook(['options']);
this._pluginCompat.tap(
{
name: "Tapable camelCase",
stage: 100
},
options => {
//省略了
}
);
this._pluginCompat.tap(
{
name: "Tapable this.hooks",
stage: 200
},
options => {
//省略了
}
);
}
啊哈,到这儿,就到重点了webpack的事件系统
,它本质上是订阅者发布者
模式,很有必要好好了解下,那就从SyncBailHook
开始吧。
可以看到SyncBailHook
继承自Hook
。
Hook
实现了tapAsync
、tapPromise
和tap
方法,而compile
方法却是抽象的,必须由子类自己去实现。
作为子类,SyncBailHook
实现了compile
方法,而tapAsync
、tapPromise
和tap
三个方法中,它抛弃了tapAsync
和tapPromise
,实际上只继承了tap
。
顾名思义,SyncBailHook
中Sync
是同步
的意思,tapAsync
的Async
、tapPromise
中的promise
都是异步
的意思,明显不搭嘛。
另外,在这里我们能较明显地感受到javascript这门语言的弱点。
//SyncBailHook
class SyncBailHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncBailHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncBailHook");
}
compile(){
}
}
//Hook
class Hook{
constructor(args){
if(!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
}
tapAsync(options, fn) {}
tapPromise(options, fn){}
compile(options){
throw new Error("Abstract: should be overriden");
}
//options:可以是一个字符串,但更期望是一个对象。传入回调函数fn的参数
//fn:回调函数
tap(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tap(options: Object, fn: function)"
);
options = Object.assign({ type: "sync", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
//订阅事件,注册监听事件
this._insert(options);
}
//stage小,往前排;stage大,往后排
_insert(item) {
let stage = 0;
if (typeof item.stage === "number") stage = item.stage;
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}
new NodeEnvironmentPlugin().apply(compiler)
NodeEnvironmentPlugin
是webpack的内置插件,这里初始化了这个插件,调用apply
方法并传入Compiler
对象作为参数。
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
插件NodeEnvironmentPlugin
的定义大体是这样的:
class NodeEnvironmentPlugin{
constructor(options){
this.options = options || {};
}
apply(compiler){
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin",compiler => {
});
}
}
module.exports = NodeEnvironmentPlugin;
主要作用是在compiler.hooks.beforeRun
钩子上注册了一个监听事件,回调函数compiler => {}
就是事件处理函数,至于这个事件什么时候会被触发,我们拭目以待吧。
这里,我们也可以趁机学习下如何开发自己的插件,可以参考NodeEnvironmentPlugin用类
来定义:
class MyPlugin{
constructor(){
}
apply(compiler){
compiler.hooks.xxx.yyy("MyPlugin",compiler => {
})
}
}
module.exports = MyPlugin;
其中,xxx
是Compiler
实例的hooks
中的任一种,yyy
是与xxx
配套的tap
或tapAsync
或tapPromise
。
if (options.plugins && Array.isArray(options.plugins)) {}
如果我们在webpack.config.js
中配置了plugins
(是个数组,其中元素要么是通过new xxxPlugin()
创建的插件实例,要么是一个函数),webpack就会在这里调用插件并传入参数compiler
对象。事实上,不论是内置插件,第三方插件还是自己开发的插件,它的基本原理都是通过webpack提供的hooks
(钩子),注册一个监听事件和事件回调函数, 来希望webpack在对的时间里做对的事情
,从而顺利完成编译打包。
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}