写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址

[TOC] Webpack的Plugin机制让其更加灵活,以适应各种应用场景。在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。

一个最基础的Plugin的代码是这样的:插件参数的获取是通过插件的构造函数进行获取的。

class BasicPlugin {
  // 在构造函数中获取用户为该插件传入的参数
  constructor(options) {
      this.options = options;
  }

  // Webpack会调用BasicPlugin实例的apply方法给插件实例传入compiler对象
  apply(compiler) {
    compiler.plugin('compilation', function(compilation) {})
  }
}

// 导出 Plugin
module.exports = BasicPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在使用这个Plugin时,相关配置代码如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins: [
    new BasicPlugin(options)
  ]
}
1
2
3
4
5
6

Webpack启动后,在读取配置的过程中会先执行new BasicPlugin(options),初始化一个BasicPlugin获得其实例。在初始化compiler对象后,再调用basicPlugin.apply(compiler)为插件实例传入compiler对象。插件实例在获取到compiler对象后,就可以通过compiler.plugin(事件名称, 回调函数)监听到Webpack广播出来的事件。并且可以通过compiler对象去操作Webpack。

通过以上最简单的Plugin,相信我们大概明白了Plugin的工作原理,但在实际开发中还有很多细节需要注意,下面来详细介绍。

插件的错误处理

  • 在参数校验阶段可以直接throw的方式抛出
throw new Error('Error Message');
1
  • 通过Compilation对象的warnings和errors接收
compilation.warnings.push('warnings');
compilation.errors.push('error');
1
2

Compiler和Compilation

在开发Plugin时最常用的两个对象就是Compiler和Compilation,它们是Plugin和Webpack之间的桥梁。Compiler和Compilation的含义如下:

Compiler对象

Compiler对象包含了Webpack环境的所有配置信息,包含options,loaders,plugins等信息,这个对象在Webpack启动时被实例化,它是全局唯一的,可以简单地将它理解为Webpack实例。

Compilation对象

Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当Webpack以开发模式运行时,每当检测到一个文件变化,便有一次新的Compilation被创建。Compilation对象也提供了很多事件回调供插件进行扩展。通过Compilation也能读取到Compiler对象。

Compiler和Compilation的区别在于:Compiler代表了整个Webpack从启动到关闭的生命周期,而Compilation只是代表了一次新的编译

Webpack构建流程

1. 初始化参数

从配置文件package.json和Shell 语句中读取与合并参数,得出最终的参数;

每次在命令行输入webpack后,操作系统都会去调用./node_modules/.bin/webpack这个shell脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js并追加输入的参数,如 -p, -w。

2. Webpack初始化

2.1 构建compiler对象

// /node_modules/webpack-cli/bin/cli.js
const webpack = require("webpack");
let compiler;
compiler = webpack(options);
1
2
3
4
// /node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
	const webpackOptionsValidationErrors = validateSchema(
		webpackOptionsSchema,
		options
	);
	if (webpackOptionsValidationErrors.length) {
		throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
	}
	let compiler;
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options => webpack(options))
		);
	} else if (typeof options === "object") {
		options = new WebpackOptionsDefaulter().process(options);
        // 创建Compiler对象
		compiler = new Compiler(options.context);
        compiler.options = options;
        // 注册NodeEnvironmentPlugin插件
		new NodeEnvironmentPlugin({
			infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);
        // 初始化配置文件中的插件
		if (options.plugins && Array.isArray(options.plugins)) {
			for (const plugin of options.plugins) {
				if (typeof plugin === "function") {
					plugin.call(compiler, compiler);
				} else {
                    // 调用插件的apply方法挂载插件监听
					plugin.apply(compiler);
				}
			}
		}
		compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        // 挂载options中的基础插件,调用WebpackOptionsApply库初始化基础插件。
		compiler.options = new WebpackOptionsApply().process(options, compiler);
	} else {
		throw new Error("Invalid argument: options");
	}
	if (callback) {
		if (typeof callback !== "function") {
			throw new Error("Invalid argument: callback");
		}
		if (
			options.watch === true ||
			(Array.isArray(options) && options.some(o => o.watch))
		) {
			const watchOptions = Array.isArray(options)
				? options.map(o => o.watchOptions || {})
				: options.watchOptions || {};
			return compiler.watch(watchOptions, callback);
		}
		compiler.run(callback);
    }
    // 返回compiler对象
	return compiler;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

3. 调用run方法开始编译

这里分为两种情况:

  • watch:监听文件变化
  • run:执行编译
// /node_modules/webpack-cli/bin/cli.js
if (firstOptions.watch || options.watch) {
    const watchOptions =
        firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
    if (watchOptions.stdin) {
        process.stdin.on("end", function(_) {
            process.exit(); // eslint-disable-line
        });
        process.stdin.resume();
    }
    compiler.watch(watchOptions, compilerCallback);
    if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
} else {
    compiler.run((err, stats) => {
        if (compiler.close) {
            compiler.close(err2 => {
                compilerCallback(err || err2, stats);
            });
        } else {
            compilerCallback(err, stats);
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4. 触发compile

在run()方法中,执行了this.compile()方法。

// /node_modules/webpack/lib/Compiler.js
this.hooks.beforeRun.callAsync(this, err => {
    if (err) return finalCallback(err);
    this.hooks.run.callAsync(this, err => {
        if (err) return finalCallback(err);
        this.readRecords(err => {
            if (err) return finalCallback(err);
            // 调用compile方法开始编译
            this.compile(onCompiled);
        });
    });
});
1
2
3
4
5
6
7
8
9
10
11
12

构建了关键的Compilation对象

this.compile()中创建了compilation对象。

// /node_modules/webpack/lib/Compiler.js
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);

        this.hooks.compile.call(params);
        // 创建Compilation对象
        const compilation = this.newCompilation(params);
        // 在compile方法中触发make事件
        this.hooks.make.callAsync(compilation, err => {
            if (err) return callback(err);

            compilation.finish(err => {
                if (err) return callback(err);

                compilation.seal(err => {
                    if (err) return callback(err);

                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Compilation负责整个编译过程,包含了每个构建环节所对应的方法。对象内部保留了对compiler的引用。当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。

5. 触发make事件并调用addEntry

addEntry() make分析入口文件创建模块对象

// /node_modules/webpack/lib/DllEntryPlugin.js
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
    // 调用addEntry方法将所有的入口模块添加到编译构建队列中,开启编译流程
    compilation.addEntry(
        this.context,
        new DllEntryDependency(
            this.entries.map((e, idx) => {
                const dep = new SingleEntryDependency(e);
                dep.loc = {
                    name: this.name,
                    index: idx
                };
                return dep;
            }),
            this.name
        ),
        this.name,
        callback
    );
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

webpack的make钩子中,tapAsync注册了一个DllEntryPlugin,就是将入口模块通过调用compilation。这一注册在Compiler.compile()方法中被执行。 addEntry方法将所有的入口模块添加到编译构建队列中,开启编译流程。

6. 构建模块

compilation.addEntry中执行_addModuleChain()这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

addEntry(context, entry, name, callback) {
		this.hooks.addEntry.call(entry, name);

		const slot = {
			name: name,
			// TODO webpack 5 remove `request`
			request: null,
			module: null
		};

		if (entry instanceof ModuleDependency) {
			slot.request = entry.request;
		}

		// TODO webpack 5: merge modules instead when multiple entry modules are supported
		const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
		if (idx >= 0) {
			// Overwrite existing entrypoint
			this._preparedEntrypoints[idx] = slot;
		} else {
			this._preparedEntrypoints.push(slot);
		}
		this._addModuleChain(
			context,
			entry,
			module => {
				this.entries.push(module);
			},
			(err, module) => {
				if (err) {
					this.hooks.failedEntry.call(entry, name, err);
					return callback(err);
				}

				if (module) {
					slot.module = module;
				} else {
					const idx = this._preparedEntrypoints.indexOf(slot);
					if (idx >= 0) {
						this._preparedEntrypoints.splice(idx, 1);
					}
				}
				this.hooks.succeedEntry.call(entry, name, module);
				return callback(null, module);
			}
		);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

7. 封装构建结果(seal)

webpack 会监听 seal事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

8. 输出资源(emit)

把Assets输出到output的path中。

小结

webpack是一个插件合集,由 tapable 控制各插件在 webpack 事件流上运行。主要依赖的是compilation的编译模块和封装。

webpack的入口文件其实就实例了Compiler并调用了run方法开启了编译,webpack的主要编译都按照下面的钩子调用顺序执行。

比较关键的 webpack 事件节点:

  • after-plugins 设置完一组初始化插件之后
  • after-resolvers 设置完 resolvers 之后
  • compile 开始编译
  • make 从入口点分析模块及其依赖的模块,创建这些模块对象
  • build-module 构建模块
  • after-compile 完成构建
  • seal 封装构建结果
  • emit 把各个chunk输出到结果文件
  • after-emit 完成输出

一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。

事件流

Webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有在完成当前处理后才能交给下一个流程去处理。插件就像插入生产线中的某个功能,在特定的时机对生产线上的资源进行处理

Webpack通过Tapable来组织这条复杂的生产线。Webpack在运行的过程中会广播事件,插件只需要监听它所关心的事件,就能加入这条生产线中,去改变生产线的运作。Webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack的事件流机制应用了发布订阅模式,和Node.js中的EventEmitter非常相似。Compiler和Compilation都继承自Tapable,可以直接在Compiler和Compilation对象上广播和监听事件,方法如下:

/**
* 广播事件
* event-name为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name', params);

/**
* 监听名称为event-name的事件,当event-name事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name', function(params) {});
1
2
3
4
5
6
7
8
9
10
11
12

同理,compilation.apply和compilation.plugin使用方法和上面一致。

在开发插件时,我们可能会不知道该如何下手,因为不知道该监听哪个事件才能完成任务。

在开发插件时,还需要注意以下两点:只要能拿到Compiler 或 Compilation对象,就能广播出新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。传给每个插件的Compiler 和 Compilation对象都是同一个引用。也就是说:若在一个插件中修改了Compiler 或 Compilation对象上的属性,会影响到后面的插件。有些事件是异步的,这些异步的事件会附带两个参数,第2个参数为回调函数,在插件处理完任务时需要调用回调函数通知Webpack,才会进入下一处理流程。例如:

compiler.plugin('emit', function(compilation, callback) {
  // 支持处理逻辑
  // 处理完毕后执行 callback 以通知 Webpack
  // 如果不执行 callback,运行流程将会一直卡在这里而不往后执行
  callback();
});
1
2
3
4
5
6

常用API

插件可以用来修改输出文件和增加输出文件,甚至可以提升Webpack性能,等等。总之,插件可以通过调用Webpack提供的API完成很多事情。由于Webpack 提供的API非常多,有很多API很少用得上,下面来介绍一些常用的API。

读取输出资源、代码块、模块及其依赖

某些插件可能需要读取Webpack的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。插件的代码如下:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一个代码块
        // 代码块由多个模块组成,通过 chunk.forEachModule 能读取代码块的每个模块
        chunk.forEachModule(function (module) {
          // module 代表一个模块
          // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 会根据Chunk生成输出的文件资源,每个Chunk都对应一个及以上的输出文件
        // 例如在Chunk中包含了CSS模块并且使用了ExtractTextPlugin 时,
        // 该Chunk就会生成.js和.css两个文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放当前即将输出的资源
          // 调用一个输出资源的 source() 方法能获取到输出资源的内容
          let source = compilation.assets[filename].source();
        });
      });

      // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束
      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行
      callback();
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

监听文件变化

Webpack会从配置的入口模块出发,依次找出所有依赖模块,当入口模块或者其依赖的模块发生变化时,就会触发一次新的Compilation

在开发插件时经常需要知道是哪个文件发生变化导致了新的Compilation,为此可以使用如下代码:

// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
    // 获取发生变化的文件列表
    const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
    // changedFiles 格式为键值对,键为发生变化的文件路径。
    if (changedFiles[filePath] !== undefined) {
      // filePath 对应的文件发生了变化
    }
    callback();
});
1
2
3
4
5
6
7
8
9
10

在默认情况下,Webpack只会监视入口和其依赖的模块是否发生了变化,在某些情况下项目可能需要引入新的文件,例如引入一个HTML文件。由于JavaScript 文件不会导入HTML文件,所以Webpack不会监听HTML文件的变化,编辑 HTML文件时就不会重新触发新的Compilation。为了监听HTML文件的变化,我们需要将HTML文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});
1
2
3
4
5

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这一点,则需要监听 emit事件,因为发生emit事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出,因此emit事件是修改Webpack输出资源的最后时机。

所有需要输出的资源都会存放在compilation.assets中,compilation.assets是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置compilation.assets的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

读取compilation.assets的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});
1
2
3
4
5
6
7
8
9

判断Webpack使用了哪些插件

在开发一个插件时,我们可能需要根据当前配置是否使用了其他插件来做下一步决定,因此需要读取Webpack当前的插件配置情况。以判断当前是否使用了 ExtractTextPlugin为例,可以使用如下代码:

// 判断当前配置是否使用了 ExtractTextPlugin,
// compiler 参数为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置使用的所有插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin => plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
1
2
3
4
5
6
7
8

实战

如何写一个插件

参考官方文档 writing-a-plugin 一个webpack插件的编写由以下几个步骤组成:

  1. 一个JavaScript类函数(或者class)。
  2. 在函数原型 (prototype)中定义一个注入compiler对象的apply方法。
  3. apply函数中通过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象。
  4. 使用compilation操纵修改webapack内部实例数据。
  5. 异步插件,数据处理完后使用callback回调。
// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // 注册事件钩子
    compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin', (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);

        // Manipulate the build using the plugin API provided by webpack
        compilation.addModule(/* ... */);
        // 异步钩子需要执行callback
        callback();
      }
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在项目根目录新建plugins目录,用来存放自定义插件;然后新建my-webpack-plugin,即我们的第一个自定义插件。具体内容如下:

index.js:

class HelloWorldPlugin {
    constructor(options) {
        // options中为用户自定义配置参数
        this.options = options;
        console.log(this.options);
    }
    apply(compiler) {
        console.log('This is my first plugin.')
    }
}

module.exports = HelloWorldPlugin;
1
2
3
4
5
6
7
8
9
10
11
12

package.json:

{
  "name": "my-webpack-plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "liujie",
  "license": "ISC"
}
1
2
3
4
5
6
7
8
9
10
11
12

webpack.config.js中配置:

const MyWebpackPlugin = require('./plugins/my-webpack-plugin');

module.exports = {
    plugins: [
        new MyWebpackPlugin('我是自定义的插件')
    ]
};
1
2
3
4
5
6
7

执行npm run build。

获取插件中传递的参数

首先需要知道插件如何使用:一般都是通过实例化来使用。

new ZipPlugin({
    name: 'test'
})
1
2
3

那么,就可以通过插件的构造函数进行插件参数的获取:

CopyrightWebpackPlugin

class CopyrightWebpackPlugin {
    constructor(options) {
        console.log('插件被使用了');
        // console.log(options); // { name: 'haha' }
    }
    apply(compiler) {
        // 同步钩子--不需要传cb参数
        compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => {
            console.log('同步钩子');
        });
        // compiler.hooks钩子函数,就是在某个时刻会执行的函数
        // compiler中存放的是配置的内容以及包括打包的所有内容
        // 而compilation中存放是某一次打包的内容
        // 第一个参数是插件名称
        // emit钩子函数会在将打包后的文件放入到dist目录后触发
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
            // debugger
            compilation.assets['copyright.txt'] = {
                source: function() { // 指定文件内容
                    return 'copyright by liujie';
                },
                size: function() { // 指定文件大小
                    return 21;
                }
            };
            cb(); // 异步钩子都需要执行cb
        });
    }
}

module.exports = CopyrightWebpackPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

FileListPlugin

class FileListPlugin {
    constructor(options) {
        console.log(options);
        this.options = options;
    }
    apply(compiler) {
        // emit是异步hook,使用tapAsync触及它,还可以使用tapPromise(异步)/tap(同步)
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
            // 在生成文件中,创建一个头部字符串
            let fileList = 'In this build:\n\n';
            // 遍历所有编译过的资源文件,
            // 对于每个文件名称,都添加一行内容。
            for (let filename in compilation.assets) {
                console.log(compilation.assets);
                fileList += `- ${filename}\n`;
            }
            // 将这个列表作为一个新的文件资源,插入到webpack构建中:
            compilation.assets['fileList.md'] = {
                source: function() {
                    return fileList;
                },
                size: function() {
                    return fileList.length;
                }
            };
            callback();
        });
    }
}

module.exports = FileListPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

在上述例子中,compilation.assets对象如下:

{ 'bundle.js':
   CachedSource {
     _source: ConcatSource { children: [Array] },
     _cachedSource: undefined,
     _cachedSize: undefined,
     _cachedMaps: {},
     node: [Function],
     listMap: [Function] },
  'copyright.txt': { source: [Function: source], size: [Function: size] } }
1
2
3
4
5
6
7
8
9

zip-plugin

class ZipPlugin {
    // 需要接收插件传递的参数,这里需要写options
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        console.log('插件参数为:', this.options);
    }
}
1
2
3
4
5
6
7
8
9

原理总结

Webpack是一个庞大的Node.js应用,如果你阅读过它的源码,你会发现实现一个完整的Webpack需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对Webpack的使用者来说,它是一个简单强大的工具;对Webpack的开发者来说,它是一个扩展性的高系统。

Webpack之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让Webpack几乎能胜任任何场景。

参考文档

  1. 如何编写一个WebPack的插件原理及实践
  2. 干货!撸一个webpack插件(内含tapable详解+webpack流程)
  3. 细说 webpack 之流程篇
  4. 如何编写一个WebPack的插件原理及实践