以极客时间《玩转Webpack》课程学习为主的记录笔记。
源码解读
- webpack的命令跟踪,从node_modules/webpack/bin/ 可以看到命令内容,webpack会查看是否下载安装了webpack-cli / webpack-command。
- 使用webpack-cli 解析命令行信息、安装使用到的依赖,启动webpack构建。
Hook和Tapable
- 从webpack的文件(/node_modules/webpack/lib/webpack.js)中可见:根绝传入的options,将生成多个不同的compiler实例。
- Compiler(/node_modules/webpack/lib/Compiler.js) / Complization
Compiler、Complization继承了Tapable对象,内部实现了很多的hooks。
常见Compiler Hooks:
entryOption
:在entry配置项处理过之后,执行一个插件。emit
:在生成资源到输出目录之前执行。done
:在编译完成时执行。
常见Compilation Hooks:
buildModule
:在模块构建之前执行。optimize
:在优化阶段开始时执行。optimizeChunks
:在块优化阶段时才执行。afterSeal
:完成封装之后执行。
Compiler
Compiler
对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中执行构建时,Compiler
对象负责处理所有事务。
当你运行 webpack 命令时,一个Compiler
实例会被创建,并且在其生命周期中,它会对每个构建创建出一个新的Compilation
对象。
Compiler
对象可以访问所有的 webpack 环境特定的功能,包括起始选项,构建的所有配置,以及注册的所有插件等。
Compilation
Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当 webpack 以开发模式运行时,每当检测到一个文件变化,就会创建一个新的Compilation
,意味着Compilation
对象描述的是一次单一的版本构建和生成资源的过程。
在构建过程中,Compilation
实例会被用来收集从入口开始的所有依赖,对这些依赖进行处理、优化、分割等操作,并最终输出编译后的资源文件到文件系统。
总结一下,Compiler
是一个 webpack 构建过程中的全局上下文,而Compilation
是对于单次构建的上下文抽象。开发者可以通过 hooks 挂钩到Compiler
和Compilation
的生命周期事件中,以扩展和控制构建过程。例如,plugin 通过监听特定的事件,可以在某个特定的构建时刻介入构建流程,添加模块、改变输出资源等。
// 源码
class Compiler {/*** @param {string} context the compilation path* @param {WebpackOptions} options options*/constructor(context, options = /** @type {WebpackOptions} */ ({})) {this.hooks = Object.freeze({/** @type {SyncHook<[]>} */initialize: new SyncHook([]),/** @type {SyncBailHook<[Compilation], boolean | undefined>} */shouldEmit: new SyncBailHook(["compilation"]),/** @type {AsyncSeriesHook<[Stats]>} */done: new AsyncSeriesHook(["stats"]),/** @type {SyncHook<[Stats]>} */afterDone: new SyncHook(["stats"]),/** @type {AsyncSeriesHook<[]>} */additionalPass: new AsyncSeriesHook([]),/** @type {AsyncSeriesHook<[Compiler]>} */beforeRun: new AsyncSeriesHook(["compiler"]),/** @type {AsyncSeriesHook<[Compiler]>} */run: new AsyncSeriesHook(["compiler"]),/** @type {AsyncSeriesHook<[Compilation]>} */emit: new AsyncSeriesHook(["compilation"]),/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */assetEmitted: new AsyncSeriesHook(["file", "info"]),/** @type {AsyncSeriesHook<[Compilation]>} */afterEmit: new AsyncSeriesHook(["compilation"]),/** @type {SyncHook<[Compilation, CompilationParams]>} */thisCompilation: new SyncHook(["compilation", "params"]),/** @type {SyncHook<[Compilation, CompilationParams]>} */compilation: new SyncHook(["compilation", "params"]),/** @type {SyncHook<[NormalModuleFactory]>} */normalModuleFactory: new SyncHook(["normalModuleFactory"]),/** @type {SyncHook<[ContextModuleFactory]>} */contextModuleFactory: new SyncHook(["contextModuleFactory"]),/** @type {AsyncSeriesHook<[CompilationParams]>} */beforeCompile: new AsyncSeriesHook(["params"]),/** @type {SyncHook<[CompilationParams]>} */compile: new SyncHook(["params"]),/** @type {AsyncParallelHook<[Compilation]>} */make: new AsyncParallelHook(["compilation"]),/** @type {AsyncParallelHook<[Compilation]>} */finishMake: new AsyncSeriesHook(["compilation"]),/** @type {AsyncSeriesHook<[Compilation]>} */afterCompile: new AsyncSeriesHook(["compilation"]),/** @type {AsyncSeriesHook<[]>} */readRecords: new AsyncSeriesHook([]),/** @type {AsyncSeriesHook<[]>} */emitRecords: new AsyncSeriesHook([]),/** @type {AsyncSeriesHook<[Compiler]>} */watchRun: new AsyncSeriesHook(["compiler"]),/** @type {SyncHook<[Error]>} */failed: new SyncHook(["error"]),/** @type {SyncHook<[string | null, number]>} */invalid: new SyncHook(["filename", "changeTime"]),/** @type {SyncHook<[]>} */watchClose: new SyncHook([]),/** @type {AsyncSeriesHook<[]>} */shutdown: new AsyncSeriesHook([]),/** @type {SyncBailHook<[string, string, any[]], true>} */infrastructureLog: new SyncBailHook(["origin", "type", "args"]),// TODO the following hooks are weirdly located here// TODO move them for webpack 5/** @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"])});this.webpack = webpack;// ......}// ....
}
webpack可以理解为一种基于事件流的编程范例,一系列的插件运行。内部实现订阅和通知功能的模块就是Tapable。
Tapable
Tapable是一个类似于Nodejs EventEmiter的库,用于控制hooks的发布订阅功能的一个模块。
Tapable暴露了很多Hook,为插件提供挂载的钩子
const{
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子 当函数有任何返回值,就会在当前执行函数停止(遇到return将直接返回)
SyncWaterfallHook, //同步流水钩子 运行结果可以传递给下一个插件
SyncLoopHok, //同步循环钩子 监听函数返回true表示继续循环,返回undefine表示结束循环
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require(“tapable”);使用:
const hook1 = new SyncHook(['argu1','argu2',])
使用方式:
Tabpack提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对
应的方法。
Async | Sync |
---|---|
绑定:tapAsync/tapPromise/tap | 绑定:tap |
执行:callAsync/promise | 执行:call |
简单用法:
const hook1 = new SyncHook(["arg1","arg2","arg3"]); //绑定事件流到webapck事件流
hook1.tap('hook1',(arg1,arg2,arg3)=>console.log(arg1,arg2,arg3))//1,2,3 //执行绑定的事件
hook1.call(1,2,3)
自测书写一个Plugin订阅构建实例(Compiler)中提供的hook,使用Compilation
Plugin的固定写法为:
- 暴露一个类。
- 类中存在webpack的调用入口:
apply
,webpack在初始化时,将主动执行Myplugin.apply,将compiler构建实例传入插件中,供挂载订阅hook节点使用。 - 看下方的实现代码,所有的发布hook都在
compiler.hooks
中,compiler.hooks.xxx就是发布的事件名称,订阅流程节点信息就是使用tap(同步)
/tapAsync(异步)
绑定上事件名和方法。
// eg1:
// const Compiler = require('./Compiler')class MyPlugin{constructor() {}apply(compiler){compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) => {return new Promise((resolve,reject)=>{setTimeout(()=>{console.log(`tapPromise to ${source} ${target} ${routesList}`)resolve();},1000)});});}
}const myPlugin = new MyPlugin();
// eg2:使用`emit`钩子在每次构建完成后输出一个文本文件
class MyExampleWebpackPlugin {// 插件必须具有apply方法apply(compiler) {// 确定在哪个阶段插入该插件compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin', (compilation, callback) => {// 创建一份新的资源const mySource = '这是我的插件添加的资源内容';// 向Webpack输出列表中添加资源compilation.assets['my-plugin-output.txt'] = {source: function() {return mySource;},size: function() {return mySource.length;}};// 执行回调,表示插件处理完成callback();});}
}module.exports = MyExampleWebpackPlugin;
webpack流程
Chunk 生成算法
- webpack 先将 entry 中对应的 module 都生成一个新的chunk
- 遍历 module 的依赖列表,将依赖的 module 也加入到chunk中
- 如果一个依赖 module 是动态引入的模块,那么就会根据这个module创建一个新的 chunk,继续遍历依赖
- 重复上面的过程,直至得到所有的 chunks
AST
抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntaxtree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
在线demo: https://esprima.org/demo/parse.html
loader开发
一个最简单的loader栗子,loader主要用于"翻译代码变成JS引擎可识别的代码“
module.exports = function(source) {console.log ('loader a is executed');return source;
};
建议使用loader-runner来简化开发流程。
loader-runner可以提供一个独立的loader运行环境,不用下载webpack,有利于调试。
// 使用举例
// 新建一个命令文件 run-loader.js:const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");runLoaders({resource: "./demo.txt", // 要翻译的对象文件loaders: [path.resolve(__dirname, "./loaders/rawloader")], // 当前loader在的地方readResource: fs.readFile.bind(fs), // 读取文件的方式},(err, result) => (err ? console.error(err) : console.log(result))
);
运行查看结果:
node run-loader.js