webpack5
插件 loader ast ... webpack打包流程
webpack入口
- 执行npm run build --> 找到webpack/bin/webpack.js
- 上述文件就是判断webpack-cli是否暗转,如果安装则运行runCli方法
- 在runCli方法加载了webpack-cli/bin/cli.js
- 执行runCli,在runCli中处理命令行参数,依赖了commander,执行new WebpackCli的时候会触发action回调
- this.program.action(), this.program = program(commander)
- action回调中执行了loadCommandByName--->makeCommand-->runWebpack
- runWebpack的时候执行了createCompiler(),在createCompiler内部调用了webpack方法,
- webpack就是本地安装的webpack,接收配置文件和回调,最终生成一个compiler对象,这个compiler对象会在上述的调用过程中被返回,它就是webpack打包中的第一个核心
- 总结,webpack打包就是使用webpack函数接收config,然后调用run方法
js
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
// Create a new instance of the CLI object
const cli = new WebpackCLI();
try {
await cli.run(args);
}
catch (error) {
cli.logger.error(error);
process.exit(2);
}
};js
// 1.引入原生webpack和配置信息
const webpack = require('webpack')
const config = require('./webpack.config')
// 2.调用webpack方法,传入配置信息获取compiler实例
const compiler = webpack(config)
// 3. 调用compiler身上的run方法,让webpack工作
compiler.run((err, stats) => {
console.log(111);
})源码阅读总结
- webpack内部就是一个函数,有两个参数options和callback,一般只传一个options参数,callback传不传都会调用create方法,返回一个compiler
- compiler是create方法内部调用了createCompiler方法获取的
- 在createCompiler内部执行了new Compiler, compiler的是个单独的模块,在创建compiler之后,后续就是挂载插件,在webpack打包过程中,插件plugins是在compiler声明之后挂载
- 在源码中,webpack内部在挂载插件的时候其实就是将所有配置属性转为插件进行处理,例如entry: './src/index.js',内部转为了new EntryOptionPlugin.apply()
webpack函数
js
// webpack函数
// 两个参数,一个配置参数,一个回调
const webpack = (options, callback) => {
const create = () => {
// ......
let compiler;
if (Array.isArray(options)) {
// ......
} else {
const webpackOptions = /** @type {WebpackOptions} */ (options);
// 编译函数在下边代码块
compiler = createCompiler(webpackOptions);
}
return { compiler, watchOptions };
}
// callback存在
if (callback) {
try {
const { compiler } = create();
compiler.run((err, stats) => {
// ......
});
return compiler;
} catch (err) {
// ......
return null;
}
// 不存在callback时
} else {
const { compiler } = create();
// ......
return compiler;
}
}createCompiler函数
js
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// 下边介绍compiler类功能
const compiler = new Compiler(
/** @type {string} */ (options.context),
options
);
// ......
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 很重要在这个文件const WebpackOptionsApply = require("./WebpackOptionsApply");
// 后续就是挂载插件,在webpack打包过程中,插件plugins是在compiler声明之后挂载,就在这个模块中
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};new Compiler做了什么
- 定义了几个方法,finalCallback onCompiled run
- 内部最终都会执行run方法
- 在run方法内部执行compile方法
js
run(callback) {
//! 最终的回调
const finalCallback = (err, stats) => {
// ......
};
// ! 完成之后的回调
const onCompiled = (err, _compilation) => {
// ......
};
// ! run方法内部调用了个run方法
const run = () => {
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);
});
});
});
};
// ! 无论如何都会调用内部run方法
if (this.idle) {
this.cache.endIdle(err => {
if (err) return finalCallback(err);
this.idle = false;
run();
});
} else {
run();
}
}compile做了什么
- 在它内部包含一个相对完整的打包流程
- 其中具体的打包模块操作是在make环节完成的
- make这里只是触发了之前订阅的一个监听时间,需要找到make这个钩子注册的位置(call -> tap)
js
/**
*! compile 意思就是编译,它里边包含了webpack的打包流程
*! 在这之前已经做了:命令行参数的处理 optios + compiler的声明
*! beforeCompile compile make finishMake afterCompile
*/
compile(callback) {
const params = this.newCompilationParams();
// beforeCompile阶段
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
//! compile阶段 在beforeCompile声明了一个compilation
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
const logger = compilation.getLogger("webpack.Compiler");
logger.time("make hook");
// make阶段
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
// finishMake阶段
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");
// afterCompile阶段
this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
});
});
}make钩子
思考:去哪找 make 钩子的注册
注册时机
- make 是用来实现 webpack 打包的,而打包最初需要的应该是明确入口,入口是在 options 中配置的
- 在 compiler 对象的创建阶段就已经将所有的 options 都转为了插件,便于在整个生命周期中进行使用
- 定位 new Compiler 再次找到 process 方法的调用,定位 entry 的处理, 这里需要注意 make 是由 compiler.run 引起的一个流程,因此配置转plugin 是在 new Compiler 时完成,换句话说 make 钩子触发之前, 相关的钩子注册工作已经完成了。所以可以正常使用
注册位置
- process 负责将 options 插件化,其中就包含了 Entry 的处理
- 在上述的方法中执行了 new EntryOptionPlugin().apply 操作,在apply 内部触发了 applyEntryOption 静态方法
- 在 applyEntryOption 这个静态方法中对 entry 进行了判断和处理, 我们一般传入是非函数
- 对 entry 进行遍历, 然后执行 new EntryPlugin(context, entry, options).apply 方法,在内部执行了 make 的 tap
make 钩子任务
追溯到 webpack 的打包流程, make 钩子被触发时就会执行这里的任务
- 在 make 钩子注册的回调任务当中执行了 addEntry 方法,该方法是属于 compilation 的成员方法
- 在 addEntry 当中执行了 _addEntryItem 私有方法
- 上述的私有方法中调用了 addModuleTree 处理依赖树
- 在 addModuleTree 方法中调用了 handleModuleCreation 方法来处理并创建 module
- 在上述方法中调用了 factorizeModule 来获取 newModule
- 获取 newModule 之后调用 addModule 将 newModule 添加至相应的队列中
- 同时还调用了 buildModule 将 module 添加至相应的队列,队列的身上存在处理模块的 processor
- 通过执行 processor 的 _buildModule 方法来完成最终的 build , 内部执行了 module.build 方法进行最终实现
所以make钩子的注册的任务核心就是完成 模块的 build 操作
build 流程
报错
- 查看 build 具体的实现时发现定位到了 Module 当中的错误提示,这是因为 Module 是一个抽象,专门用于继承,内部并没有真正的实现 build 方法
- 此处真正实现 build 方法的是 NormalModule 模块,它内部有一个 build
runLoaders
- 分析 build 执行发现它的内部又执行了 doBuild 操作
- 在 doBuild 方法内部执行了 runLoaders 方法,且这个方法是一个外部方法,第三方库导入此处不研究
- 执行 runLoaders 的目的最终也是为了读取相应路径的文件内容,然后交由 processResult 处理
- 执行 processResult 时,不论过程如何最终都会执行 callback , 此 callback 就是在执行 doBuild 的时候传入的
- 在 callback 内部有一个核心的 parse 操作,该步是将 loader 读取到的内容交由 ast 语法树进行实现(acorn库)
- 之所以需要 ast 是因为要判断当前被打包模块是否依赖了其它模块,如果依赖了则可以递归进行处理
到此模块的 build 算是结束了,余下的就是对 build 的内容进入处理
seal 流程
在 make 工作完成之后会经历 finishMake 阶段,之后调用了 seal
核心就是将 webpack 打包之后的 modules 数据保存起来
seal 流程
- compilation 在之前已经将本次打包需要处理的模块都放到了 modules 当中
- 遍历处理好的 modules ,然后依据配置生成不同的 chunk
- 不同的 chunk 可能会存在不同的优化处理
- 将优化处理好的 chunk 保存在 compilation 的 assets 属性上
seal 逻辑
- 生成相应的 chunk 图
- 触发各种 optimize 相关的钩子
- 优化操作完成之后,执行 optimizeChunkModules 方法的回调
- 在上述方法的内部执行 codeGeneration 实现最终 code 的生成工作
codeGeneration 方法
- 内部最终调用了 _runCodeGenerationJobs ,传入 callback
- _runCodeGenerationJobs 方法内部使用了asyncLib 库遍历jobs ,同时也传入了 callback
- 不论上述的处理结果如何,最终都执行了调用之处传入的 callback ,而这个 callback 是我们调用 codeGeneration 方法时传入的
- 在 codeGeneration 的 callback 当中我们会执行 createChunkAssets 方法来实现最终 chunkAssets的生成
createChunkAssets 方法
- 定义 manifest 对象,将所有的数据都保存在这个对象上
- 调用 manifest 对象的 render 方法,拿到最终的 source
- 拿到 source 之后执行 emitAsset 方法将 source 写入到相应的 file
emitAsset 方法
- 核心就是将打包之后的资源放到 assets 属性上
- this.assets[file] = source
emitAsset 流程
- 上述的操作执行完成之后就开始执行 finalCallback 操作, 而这个 callback 就是调用 seal 方法时传入
- seal 方法的调用是在 compiler 内完成
- 调用 seal 方法后会执行 afterCompile 钩子, 此时会执行 callback ,这个 callback 就是 compile 调用时传入
- 调用 compile 时传入的就是 onCompiled
- 执行 onCompiled 回调的时候会执行 emitAssets 方法,将打包后资源真正的写入到磁盘操作