先简单了解下概念
entry:指定一个入口起点(或多个入口起点)。默认值为 ./src
webpack.config.js
ini
module.exports = {
entry: './path/to/my/entry/file.js'
};
多入口
css
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
**output:**告诉 webpack 在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为 ./dist
lua
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
loader:
loader 有两个目标:
-
test
属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。 -
use
属性,表示进行转换时,应该使用哪个 loader。const path = require('path');module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /.txt$/, use: 'raw-loader' } ] }};
配置的loader,是告诉webpack 编译器,当你碰到「在 require()
/import
语句中被解析为 '.txt' 的路径」时,在对它打包之前,先使用 raw-loader
转换一下
plugins:插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量
javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'}) ]
};
模块解析(module resolution)
resolver 是一个库(library),用于帮助找到模块的绝对路径。一个模块可以作为另一个模块的依赖模块,然后被后者引用
webpack 中的解析规则
使用 enhanced-resolve
,webpack 能够解析三种文件路径:
绝对路径
arduino
import "/home/me/file";
import "C:\\Users\\me\\file";
由于我们已经取得文件的绝对路径,因此不需要进一步再做解析。
相对路径
arduino
import "../src/file1";
import "./file2";
在这种情况下,使用 import
或 require
的资源文件(resource file)所在的目录被认为是上下文目录(context directory)。在 import/require
中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。
模块路径
arduino
import "module";
import "module/lib/file";
模块将在 resolve.modules
中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias
配置选项来创建一个别名。
一旦根据上述规则解析路径后,解析器(resolver)将检查路径是否指向文件或目录。如果路径指向一个文件:
- 如果路径具有文件扩展名,则被直接将文件打包。
- 否则,将使用 [
resolve.extensions
] 选项作为文件扩展名来解析,此选项告诉解析器在解析中能够接受哪些扩展名(例如.js
,.jsx
)。
如果路径指向一个文件夹,则采取以下步骤找到具有正确扩展名的正确文件:
-
如果文件夹中包含
package.json
文件,则按照顺序查找resolve.mainFields
配置选项中指定的字段。并且package.json
中的第一个这样的字段确定文件路径。 -
如果
package.json
文件不存在或者package.json
文件中的 main 字段没有返回一个有效路径,则按照顺序查找resolve.mainFiles
配置选项中指定的文件名,看是否能在 import/require 目录下匹配到一个存在的文件名。 -
文件扩展名通过
resolve.extensions
选项采用类似的方法进行解析。const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装 const webpack = require('webpack'); // 用于访问内置插件
module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /.txt$/, use: 'raw-loader' } ] }, plugins: [ new webpack.optimize.UglifyJsPlugin(), new HtmlWebpackPlugin({template: './src/index.html'})
], resolve: { // 路径别名, 懒癌福音 alias: { shared: path.resolve(__dirname, './packages/shared'), events: path.resolve(__dirname, './packages/events') }, extensions: ['.web.js', '.js', '.jsx'] //后缀名自动补全 } };
接下来通过调试源代码,深入理解
Webpack =内容转换+资源合并
- 初始化阶段:
*** 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数**
swift
{
"name": "ltq-webpack-core",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development"
},
"devDependencies": {
"acorn-walk": "^6.1.1",
"markdown-loader": "^4.0.0",
"tapable": "^1.1.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
},
"dependencies": {
"acorn": "^6.0.4"
}
}
使用node 调试工具 ndb,执行
arduino
ndb npm run dev
第一步先执行webpack 脚本,判断webpack-cli 有没有安装

如果安装了webpack-cli ,require(cli),脚本执行

执行 runCli(process.argv);
ini
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
const cli = new WebpackCLI();
try {
await cli.run(args);
}
catch (error) {
cli.logger.error(error);
process.exit(2);
}
};
先创建WebpackCLI,调用run,在run中调用createCompiler 创建,下面是run 方法中主要代码:
typescript
async run(args, parseOptions) {
// ...
this.program.action(async (options, program) => {
const { operands, unknown } = this.program.parseOptions(program.args);
const defaultCommandToRun = getCommandName(buildCommandOptions.name);
let commandToRun = operand;
let commandOperands = operands.slice(1);
if (isKnownCommand(commandToRun)) {
await loadCommandByName(commandToRun, true);
}
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
from: "user",
});
});
await this.program.parseAsync(args, parseOptions);
}
其中this.program.action的回调函数,存放在this._actionHandler,后面会调用
kotlin
action(fn) {
const listener = (args) => {
const actionArgs = args.slice(0, expectedArgsCount);
if (this._storeOptionsAsProperties) {
actionArgs[expectedArgsCount] = this; // backwards compatible "options"
} else {
actionArgs[expectedArgsCount] = this.opts();
}
actionArgs.push(this);
return fn.apply(this, actionArgs);
};
this._actionHandler = listener;
return this;
}
重点看:await this.program.parseAsync(args, parseOptions);
// 准备用户参数,解析执行命令
kotlin
// command.js
async parseAsync(argv, parseOptions) {
const userArgs = this._prepareUserArgs(argv, parseOptions);
await this._parseCommand([], userArgs);
return this;
}
_parseCommand(operands, unknown) {
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs));
if (this.parent) {
actionResult = this._chainOrCall(actionResult, () => {
this.parent.emit(commandEvent, operands, unknown); // legacy
});
}
actionResult = this._chainOrCallHooks(actionResult, 'postAction');
return actionResult;
}
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs));
该方法就是调用上面存储的this._actionHandler回调,在回调中 await loadCommandByName(commandToRun, true);
dart
const loadCommandByName = async (commandName, allowToInstall = false) => {
const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
const isWatchCommandUsed = isCommand(commandName, watchCommandOptions);
if (isBuildCommandUsed || isWatchCommandUsed) {
await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
this.webpack = await this.loadWebpack();
return this.getBuiltInOptions();
}, async (entries, options) => {
if (entries.length > 0) {
options.entry = [...entries, ...(options.entry || [])];
}
await this.runWebpack(options, isWatchCommandUsed);
});
}
//...
};
在loadCommandByName 方法中调用 makeCommand方法,该方法先loadWebpack加载webpack
scss
async makeCommand(commandOptions, options, action) {
if (options) {
options = await options();
for (const option of options) {
this.makeOption(command, option);
}
}
command.action(action);
return command;
}
options 为函数
kotlin
async () => {
this.webpack = await this.loadWebpack();
return this.getBuiltInOptions();
}
command.action 方法执行后,将该方法存储在this._actionHandler,等待后面执行:
ini
async (entries, options) => {
if (entries.length > 0) {
options.entry = [...entries, ...(options.entry || [])];
}
await this.runWebpack(options, isWatchCommandUsed);
});
执行完成后,回到主函数,接着执行
csharp
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
from: "user",
});
该方法会调用runWebpack方法,
javascript
async runWebpack(options, isWatchCommand) {
const callback = (error, stats) => {
// ...
};
compiler = await this.createCompiler(options, callback); //创建compiler
if (!compiler) {
return;
}
const isWatch = (compiler) => Boolean(this.isMultipleCompiler(compiler)
? compiler.compilers.some((compiler) => compiler.options.watch)
: compiler.options.watch);
if (isWatch(compiler) && this.needWatchStdin(compiler)) {
process.stdin.on("end", () => {
process.exit(0);
});
process.stdin.resume();
}
}
在 compiler = await this.createCompiler(options, callback);
创建compiler,加载webpack.config.js 打包配置项,构建配置项
javascript
//webpack-cli.js
async createCompiler(options, callback) {
let config = await this.loadConfig(options);
config = await this.buildConfig(config, options);
let compiler;
compiler = this.webpack(config.options, callback);
return compiler;
}
- 创建编译器对象 :用上一步得到的参数创建
Compiler
对象
ini
this.loadConfig(options);加载项目定义的webpack.config.js 配置文件
const webpack = ((
options,
callback
) => {
const create = () => {
let compiler;
let watch = false;
let watchOptions;
if (Array.isArray(options)) {
compiler = createMultiCompiler(options);
watch = options.some(options => options.watch);
watchOptions = options.map(options => options.watchOptions || {});
} else {
compiler = createCompiler(options);
watch = options.watch;
watchOptions = options.watchOptions || {};
}
return { compiler, watch, watchOptions };
};
const { compiler, watch, watchOptions } = create();
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
});
class Compiler {
/**
* @param {string} context the compilation path
*/
constructor(context) {
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
...
/** @type {SyncHook<[CompilationParams]>} */
compile: new SyncHook(["params"]),
/** @type {SyncBailHook<[string, string, any[]], true>} */
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
/** @type {SyncHook<[]>} */
environment: new SyncHook([]),
/** @type {SyncHook<[]>} */
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<[Compiler]>} */
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<[Compiler]>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<[string, Entry], boolean>} */
entryOption: new SyncBailHook(["context", "entry"])
});
/** @type {string=} */
this.name = undefined;
/** @type {Compilation=} */
this.parentCompilation = undefined;
/** @type {Compiler} */
this.root = this;
...
}
}
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
-
将
process.args + webpack.config.js
合并成用户配置 -
调用
validateSchema
校验配置 -
调用
getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
合并出最终配置 -
创建
compiler
对象 -
遍历用户定义的
plugins
集合,执行插件的apply
方法 -
调用
new WebpackOptionsApply().process
方法,加载各种内置插件
详细看下new WebpackOptionsApply().process(options, compiler)添加内置plugins

主要逻辑集中在 WebpackOptionsApply
类,webpack 内置了数百个插件,这些插件并不需要我们手动配置,WebpackOptionsApply
会在初始化阶段根据配置内容动态注入对应的插件,包括:
-
注入
EntryOptionPlugin
插件,处理entry
配置 -
根据
devtool
值判断后续用那个插件处理sourcemap
,可选值:EvalSourceMapDevToolPlugin
、SourceMapDevToolPlugin
、EvalDevToolModulePlugin
-
注入
RuntimePlugin
,用于根据代码内容动态注入 webpack 运行时
执行createCompiler先创建Compiler ,创建compiler的hooks,初始化plugins ,完成创建compiler后调用,执行完成后接下来调用
ini
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
调用钩子函数
kotlin
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
this.compile(onCompiled);
});
});
});
调用run 钩子函数,在run中执行compile方法
ini
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params); //创建compilation
const logger = compilation.getLogger("webpack.Compiler");
logger.time("make hook");
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
this.hooks.finishMake.callAsync(compilation, err => {
logger.timeEnd("finish make hook");
if (err) return callback(err);
process.nextTick(() => {
logger.time("finish compilation");
compilation.finish(err => {
logger.timeEnd("finish compilation");
if (err) return callback(err);
logger.time("seal compilation");
compilation.seal(err => {
logger.timeEnd("seal compilation");
if (err) return callback(err);
logger.time("afterCompile hook");
this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
});
});
}
在this.hooks.beforeCompile中创建compilation对象,Compilation
:单次编辑过程的管理器,比如 watch = true
时,运行过程中只有一个 compiler
但每次文件变更触发重新编译时,都会创建一个新的 compilation
对象
在 webpack 构建过程(make 阶段)中逐步收集模块间的依赖关系,到 seal 阶段的 chunk 生成
从网上找了张图,可以看到
webpack 的主体框架:

构建阶段
进入make 阶段,可以看到make 阶段和seal阶段,make阶段通过hooks发布事件完成

make阶段第一个执行的plugin是EntryPlugin
Entry
:编译入口,webpack 编译的起点

构建阶段从 entry
开始递归解析资源与资源的依赖,在 compilation
对象内逐步构建出 module
集合以及 module
之间的依赖关系,核心流程:

进入handleModuleCreation

ini
// Compilation 文件
addModuleChain(context, dependency, callback) {
this.handleModuleCreation(
{
factory: moduleFactory,
dependencies: [dependency],
originModule: null,
context
},
err => {
if (err && this.bail) {
callback(err);
this.buildQueue.stop();
this.rebuildQueue.stop();
this.processDependenciesQueue.stop();
this.factorizeQueue.stop();
} else {
callback();
}
}
);
}
handleModuleCreation(
{ factory, dependencies, originModule, context, recursive = true },
callback
) {
const moduleGraph = this.moduleGraph;
this.factorizeModule(
{ currentProfile, factory, dependencies, originModule, context },
(err, newModule) => {
this.addModule(newModule, (err, module) => {
if (err) {
if (!err.module) {
err.module = module;
}
this.errors.push(err);
return callback(err);
}
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
moduleGraph.setResolvedModule(originModule, dependency, module);
}
if (module !== newModule) {
if (currentProfile !== undefined) {
const otherProfile = moduleGraph.getProfile(module);
if (otherProfile !== undefined) {
currentProfile.mergeInto(otherProfile);
} else {
moduleGraph.setProfile(module, currentProfile);
}
}
}
this.buildModule(module, err => {
this.processModuleDependencies(module, err => {
if (err) {
return callback(err);
}
callback(null, module);
});
});
});
}
);
}
this.buildModule构建模块,执行NormalModule 的build函数

调用 loader-runner 仓库的 runLoaders
转译 module
内容,通常是从各类资源类型转译为 JavaScript 文本

接着调用JavascriptParser模块中的parse方法解析模块,根据解析的ast语法树分析依赖

遍历 AST,触发各种钩子
-
在
HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖 -
调用
module
对象的addDependency
将依赖对象加入到module
依赖列表中

执行this.blockPreWalkStatements 中处理依赖


调用 module
对象的 addDependency
将依赖对象加入到 module
依赖列表中

执行完成解析后,回到handleParseResult(result);

在handleParseResult 调用handleBuildDone,完成build函数的回调callback;

回到buildModule的回调

processModuleDependencies 处理模块的依赖,触发handleModuleCreation整个循环就跑起来了
总结:
-
调用
handleModuleCreate
,根据文件类型构建module
子类 -
调用 loader-runner 仓库的
runLoaders
转译module
内容,通常是从各类资源类型转译为 JavaScript 文本 -
调用 acorn 将 JS 文本解析为AST
-
遍历 AST,触发各种钩子
-
在
HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖 -
调用
module
对象的addDependency
将依赖对象加入到module
依赖列表中 -
AST 遍历完毕后,调用
module.handleParseResult
处理模块依赖 -
对于
module
新增的依赖,调用handleModuleCreate
,控制流回到第一步 -
所有依赖都解析完毕后,构建阶段结束
生成阶段
构建阶段围绕 module
展开,生成阶段则围绕 chunks
make 完成后进入seal阶段


seal 方法比较复杂,大体流程如下:
-
构建本次编译的
ChunkGraph
对象; -
遍历
compilation.modules
集合,将module
按entry/动态引入
的规则分配给不同的Chunk
对象; -
compilation.modules
集合遍历完毕后,得到完整的chunks
集合对象 -
createXxxAssets
遍历module/chunk
,调用compilation.emitAssets
方法将assets
信息记录到compilation.assets
对象中 -
触发
seal
回调,控制流回到compiler
对象
写入文件系统

seal
结束之后,执行回调函数,回到onCompiled方法

紧接着调用 compiler.emitAssets
函数,函数内部调用 compiler.outputFileSystem.writeFile
方法将 assets
集合写入文件系统

最后总结:
webpack核心完成了 内容转换 + 资源合并 两种功能,实现上包含三个阶段:
-
初始化阶段:
-
初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
-
创建编译器对象 :用上一步得到的参数创建
Compiler
对象 -
初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
-
开始编译 :执行
compiler
对象的run
方法 -
确定入口 :根据配置中的
entry
找出所有的入口文件,调用compilition.addEntry
将入口文件转换为dependence
对象 -
构建阶段:
-
编译模块(make) :根据
entry
对应的dependence
创建module
对象,调用loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理 -
完成模块编译 :上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
-
生成阶段:
-
输出资源(seal) :根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会 -
写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统