webpack源码分析-初始化过程

webpack是现代JavaScript应用程序的静态模块打包器。通过递归地构建依赖关系图,将应用程序需要的每个模块打包成一个或多个bundle

基础用法

  1. 使用npm init命令初始化工程

  2. 在src目录创建入口文件index.js

1
2
import $ from 'jquery'; // 引入 jquery 第三方库
$('#app').text('hello webpack!'); // 使用 jquery 输出 hello webpack
  1. 使用npm install jquery --registry https://registry.npm.taobao.org安装jquery

  2. 使用npm install -D webpack@4.44.1 webpack-cli --registry https://registry.npm.taobao.org安装webpack

  3. 使用npx webpack编译打包。可看到目录中生成了dist/main.js文件

  4. 创建浏览器入口index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>webpack基础用法</title>
</head>
<body>
<!-- 创建挂载点 -->
<div id="app"></div>
<!-- 引入入口js文件 -->
<script src="./dist/main.js"></script>
</body>
</html>
  1. 可在编译器浏览index.html文件输出的hello webpack

webpack-cli

在基础用法第5步,使用命令npx webpack编译打包,node会自动执行/node_modules/webpack/bin/webpack.js文件,从133行149行看出webpack是最后交给webpack-cli命令来执行,即执行npx webpacknpx webpack-cli的结果是一样的。

1
2
3
4
5
6
7
8
9
10
11
// 判断是否安装了 webpack-cli,如果没有安装就引导安装,确保存在 webpack-cli
const packageName = 'webpack-cli';
// 执行 webpack-cli
runCommand(packageManager, installOptions.concat(packageName))
.then(() => {
require(packageName); // eslint-disable-line
})
.catch((error) => {
console.error(error);
process.exitCode = 1;
});

分析node_modules/webpack-cli/bin/cli.js文件可发现webpack-cli负责接受用户输入的命令,获取自定义配置文件webpack.config,jswebpackfile.js。创建一个webpack编译器对象compiler并执行compiler对象的run方法。

webpack流程图

webpack流程图

分析webpack初始化过程

node_modules/webpack-cli/bin/cli.js文件中

1
2
3
4
try {
const webpack = require("webpack"); // 引入 webpack
compiler = webpack(options); // 创建 webpack 编译器对象 compiler
} catch (err) {......}

引入webpack,并返回webpack编译器对象compiler

查看文件node_modules/webpack/lib/webpack.js第25行

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
const webpack = (options, callback) => {
// 加载 webpack 默认配置
options = new webpackOptionsDefaulter().process(options);
// 创建 webpack 编译器对象 compiler
compiler = new Compiler(options.context);
compiler.options = options;
// 初始化 webpack 的 log 系统、watch 系统、file 系统
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 {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
//根据 wbepack 的默认配置做 webpack 的初始化操作
compiler.options = new webpackOptionsApply().process(options, compiler);
if (callback) {
...
// 开始编译
compiler.run(callback);
}
// 返回编译器对象
return compiler;
}

加载默认配置webpackOptionsDefaulter

查看node_modules/webpack/lib/webpackOptionsDefaulter.js文件第30行,查看webpackOptionsDefaulter定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class webpackOptionsDefaulter extends OptionsDefaulter {
constructor() {
super();
// 设置默认的入口文件为 “src/index.js” 文件
this.set("entry", "./src");

// 设置默认的开发模式为:测试环境 “eval” 生产环境 “false”
this.set("devtool", "make", options =>
options.mode === "development" ? "eval" : false
);
// 设置默认上下文目录为 process.cwd()
this.set("context", process.cwd());
// 设置 webpack 的默认 target 为 “web”
this.set("target", "web");
...
// 设置 webpack 的默认输出路径为 “dist” 目录
this.set("output.path", path.join(process.cwd(), "dist"));

.........
}
}

由上看出,当没有对webpack做任何配置时,webpack会读取默认配置信息,即默认入口文件src/index.js,默认开发环境、上下文目录、默认target、默认输出路径。

即在基础使用中,没有做任何配置,故webpack默认读取src/index.js文件打包输出到dist目录

创建Compiler对象

对象Compiler类似webpack的司机,控制webpack使用

1
2
3
4
5
6
const webpack = (options, callback) => {
// 创建 webpack 编译器对象 compiler
compiler = new Compiler(options.context);

......
}

它的方法有

  1. run:启动编译

  2. newCompilation:创建编译器

  3. emitAssets:处理编译后结果。

创建node环境

1
2
3
4
5
6
7
8
const webpack = (options, callback) => {
...
// 初始化 webpack 的 log 系统、watch 系统、file 系统
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
...
}

查看node_modules/webpack/lib/node/NodeEnvironmentPlugin.js文件第14行

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
class NodeEnvironmentPlugin {
apply(compiler) {
// 创建 webpack 中的 log 系统
compiler.infrastructureLogger = createConsoleLogger(
Object.assign(
{
level: "info",
debug: false,
console: nodeConsole
},
this.options.infrastructureLogging
)
);
// 创建 webpack 中的 file 读取系统
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
const inputFileSystem = compiler.inputFileSystem;
// 创建 webpack 中的 file 输出系统
compiler.outputFileSystem = new NodeOutputFileSystem();
// 创建 webpack 中的 watch 监听文件系统
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
...
}
}

通过NodeEnvironmentPlugin创建文件系统,使webpack可以读取、编译、输出文件,同时输出一些log文件

加载plugins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const webpack = (options, callback) => {
...
// 开始加载插件
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);
}
}
}
...
}

自定义插件

  1. 提供配置文件webpack.config.jswebpack
1
2
//创建配置文件
touch webpack.config.js
  1. 导入一个对象
1
2
3
module.exports = {
plugins: [],
};
  1. 创建plugins目录
1
mkdir plugins
  1. 在plugins目录中创建文件function-webpack-plugin.js
1
2
3
4
5
6
7
8
9
touch plugins/function-webpack-plugin.js

//文件内容
module.exports = function (compiler) {
// 利用 webpack 的 log 系统输出一句话
compiler.infrastructureLogger('Customer', 'error', [
'我是自定义Function插件',
]);
};
  1. 在plugins目录中创建文件object-webpack-plugin.js
1
2
3
4
5
6
7
8
9
class ObjectwebpackPlugin {
apply(compiler) {
// 同样利用 webpack 的 log 系统输出一句话
compiler.infrastructureLogger('Customer', 'error', [
'我是自定义Object插件',
]);
}
}
module.exports = ObjectwebpackPlugin;
  1. 在webpack.config.js使用4、5中插件
1
2
3
4
5
6
7
8
9
module.exports = {
plugins: [
require('./plugins/function-webpack-plugin'), // 引用 function 方式插件
new (require('./plugins/object-webpack-plugin'))(), // 引用 object 方式插件
],
infrastructureLogging: {
debug: true, // 开启 webpack 的 log 系统
},
};
  1. 执行命令npx webpack

结果

根据默认配置初始化webpack

1
2
3
4
5
6
7
8
const webpackOptionsApply = require("./webpackOptionsApply");
...
const webpack = (options, callback) => {
...
// 根据默认配置初始化 webpack
compiler.options = new webpackOptionsApply().process(options, compiler);
...
}

查看文件node_modules/webpack/lib/webpackOptionsApply.js第55行

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
...
process(options, compiler) {
let ExternalsPlugin;
// 初始化 webpack 的输出路径
compiler.outputPath = options.output.path;
// 初始化 webpack 的 records 路径
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
compiler.recordsOutputPath =
options.recordsOutputPath || options.recordsPath;
// 设置当前编译器的 name
compiler.name = options.name;
switch (options.target) {
case "web":
// 添加浏览器环境的模块生成器模版
JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
NodeSourcePlugin = require("./node/NodeSourcePlugin");
new JsonpTemplatePlugin().apply(compiler);
new FetchCompileWasmTemplatePlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
// 将模块到一个方法中的模版
new FunctionModulePlugin().apply(compiler);
new NodeSourcePlugin(options.node).apply(compiler);
new LoaderTargetPlugin(options.target).apply(compiler);
break;
...
// 入口文件加载插件
new EntryOptionPlugin().apply(compiler);
// 执行 compiler 对象中的 entryOption 钩子函数
compiler.hooks.entryOption.call(options.context, options.entry);
...

webpackOptionsApply的责任为根据webapck的配置信息加载一些插件。

如上代码中入口文件加载插件执行compiler对象中的entryOption钩子函数部分,这里执行了compiler对象中的entryOption钩子函数。

查看文件node_modules/webpack/lib/EntryOptionPlugin.js第20行

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
...
const itemToPlugin = (context, item, name) => {
// 多入口的时候
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name);
}
// 单入口的时候
return new SingleEntryPlugin(context, item, name);
};
module.exports = class EntryOptionPlugin {
apply(compiler) {
// 监听 compiler 对象中的 entryOption 钩子函数
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
// entry 可以为一个 string 或数组,名称默认为 “main”
if (typeof entry === "string" || Array.isArray(entry)) {
itemToPlugin(context, entry, "main").apply(compiler);
} else if (typeof entry === "object") { // entry 可以为一个 object
for (const name of Object.keys(entry)) {
itemToPlugin(context, entry[name], name).apply(compiler);
}
} else if (typeof entry === "function") { // entry 还可以为一个方法
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
};

EntryOptionPlugin监听compiler对象中的entryOption钩子函数的执行

原基础应用使用的是单入口项目,故在此使用的是SIngleEntryPlugin插件,查看文件node_modules/webpack/lib/SingleEntryPlugin.js第40行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context; // 当前 webpack 的上下文目录
this.entry = entry; // 入口文件
this.name = name; // 入口文件的名称
}
apply(compiler) {
...
// 监听 compiler 对象的 make 操作
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
// 把入口文件当成一个 dependency 添加到 webpack 进行编译
compilation.addEntry(context, dep, name, callback);
}
);
}
...
module.exports = SingleEntryPlugin;

到此,webpack的初始化过程结束。webpackSingleEntryPlugin停止运行等待compilermake操作的执行,即webpack编译的开始。

  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020-2024 Aweso Lynn
  • PV: UV: