loader-runner 是 webpack 官方拆出的 loader 执行引擎,也是 minipack 这类简化实现直接复用的包。它回答的问题是:一批 loader 挂在一个文件上时,以什么顺序、按什么协议执行?
执行流程
- runLoaders 入口:接收
resource(目标文件)、loaders(按配置顺序排列的 loader 绝对路径列表)和options,开始执行。 - 定义 loaderContext:创建所有 loaders 处理该资源时共享的一份数据对象(
loaderContext),包含resource、resourcePath、query、request、callback、async等字段——这是 loader 通过this访问的上下文。 - 初始化 loaderIndex:定义
loaderIndex = 0并挂载到 loaderContext 上。执行过程完全由这个索引驱动。 - iteratePitchingLoaders:根据当前的 loaderIndex 获取对应的 loaderObject:
- 若 loader 存在且定义了 pitch 阶段,先执行 pitch(从左到右);
- pitch 返回非 undefined 时,跳过右侧所有 loader 的处理阶段,直接进入左侧 loader 的处理阶段(类似中间件的短路);
- pitch 返回 undefined 时,索引递增继续下一个 loader。
- 处理阶段:到达最右侧 loader 后反向执行处理阶段(从右到左),每个 loader 接收上一个 loader 的输出(第一个接收原始文件内容),通过
this.callback或返回值把结果传给下一个。 - 最终结果(code + map + meta)通过 runLoaders 的 callback 返回给 webpack 编译流程。
核心协议
- pitch 阶段:loader 的
pitch(remainingRequest, precedingRequest, data)在真正处理文件前执行,可以用于提前拦截、注入代码或设置共享data。pitch 之间从左到右,处理阶段从右到左,形成"洋葱"顺序——与 koa 中间件模型一致。 - loaderContext.data:pitch 阶段写入的
this.data在处理阶段仍可读,是同一个 loader 两个阶段之间共享状态的通道。 - 异步 loader:需要异步操作(读取文件、调 babel 等)时调用
this.async()获得 callback,完成后调用callback(null, code, map);同步 loader 直接 return 结果。 - this.callback:支持同时返回 code、source map 和 metadata,比 return 更完整。
与 webpack 的关系
webpack 编译资源时按 module.rules 生成该资源的 loader 列表,然后调用 loader-runner 执行——因此理解 loader-runner 的索引推进与 pitch 短路规则,就理解了"loader 为什么从左到右配、从右到左执行",以及 style-loader 这类 loader 靠 pitch 短路实现"运行时注入"的原理。
Discussion
留言与讨论
想法、补充和不同意见都欢迎。