完整的 webpack loader 执行顺序
2022-07-25 · 1,129 chars · 6 min read
这两天在看 Preact Cli,想了解下这个声称「100 Lighthouse score」的脚手架内部是怎么运行的。刚好我对目前使用的框架也有些不满意的地方,就顺便看看有没有什么亮点值得借鉴的。
Preact is an excellent choice for Progressive Web Apps that wish to load and become interactive quickly. Preact CLI codifies this into an instant build tool that gives you a PWA with a 100 Lighthouse score right out of the box.
其中有个核心功能是拆包,就是按照路由将整个 APP 拆分开来,按需加载。我们之前一直用的是 loadable-components,但是 preact 是通过 @preact/async-loader 实现的,这个 loader 其实是一个 pitch loader,配合 enforce: 'pre'
的 babel-loader 一起处理 js 代码。
由于我以前没写过 pitch loader,便很好奇它和 pre 阶段的 loader 放在一起,执行顺序是怎么样的。网上搜索了一番,大多数文章是把 pre
& post
和 pitch & normal 分开讲的。官方的文档也说的比较简单,于是自己试验了一番,这篇 笔记就简单记录下。
Note
Preact CLI 架构相关的内容,等彻底研究完再看,有价值的话单独写文章
准备代码#
使用版本:
- webpack:5.73.0
- webpack-cli:4.10.0
webpack.config.js
代码如下:
const path = require("path") module.exports = { entry: "./index.js", output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, mode: "development", module: { rules: [ { enforce: "pre", test: /\.md$/i, use: ["pre-a-loader", "pre-b-loader"], }, { test: /\.md$/i, use: ["a-loader", "b-loader", "c-loader"], }, { enforce: "post", test: /\.md$/i, use: ["post-a-loader", "post-b-loader"], }, ], }, resolveLoader: { modules: [path.resolve(__dirname, "node_modules"), path.resolve(__dirname, "loaders")], }, }
为了完整测试,一共添加了 7 个 loader,覆盖除了 inline loader 之前的全部场景(目前很少用 inline)。每个 loader 除了 console.log
的内容外,都长一样的。例如 a-loader.js
:
function loader(content, map, meta) { console.log('a normal loader') return '123' } loader.pitch = () => { console.log('[pitch] a normal loader') } module.exports = loader;
测试#
直接执行 npx webpack
,输出:
[pitch] a post loader [pitch] b post loader [pitch] a normal loader [pitch] b normal loader [pitch] c normal loader [pitch] a pre loader [pitch] b pre loader b pre loader a pre loader c normal loader b normal loader a normal loader b post loader a post loader asset bundle.js 2.38 KiB [compared for emit] (name: main) ./index.js 23 bytes [built] [code generated] ./README.md 3 bytes [built] [code generated] webpack 5.73.0 compiled successfully in 81 ms
在 b-loader.js
的 pitch
方法里 return 点东西
// ...省略 loader.pitch = () => { console.log('[pitch] b normal loader') return '123' // 加上这句 } // ...省略
执行结果是这样的:
[pitch] a post loader [pitch] b post loader [pitch] a normal loader [pitch] b normal loader a normal loader b post loader a post loader asset bundle.js 2.38 KiB [compared for emit] (name: main) ./index.js 23 bytes [built] [code generated] ./README.md 3 bytes [built] [code generated] webpack 5.73.0 compiled successfully in 97 ms
Tip
不清楚 pitch 的朋友看官方文档 pitch loader,还有 Rule.enforce。
结论#
正常情况下的执行顺序是:
- 不同阶段之间:
pre -> normal -> post
- 每个阶段内部:use 数组里,从右到左执行
pitch 是完全反过来的
- 不同阶段之间:
post -> normal -> pre
- 每个阶段内部:use 数组里,从左到右执行
如果某个 pitch 里有返回值,立刻停止,调头回去,之前执行过 pitch 的 loader(不包括自己),继续执行 loader 本身。
其实...这就和 koa 中间件的洋葱模型类似。
Preact CLI#
虽然这篇笔记是讲 webpack loader 执行顺序的,但是是由 preact 引起的,还是稍微讲一下。
Preact CLI 将同步的代码转化为异步代码的核心原理:
- 筛选路径是
routes
等的文件 - 使用只含 pitch 方法的
@preact/async-loader
,返回一段异步加载原文件的代码
webpack 部分的关键代码:
[ { test: /\.[jt]sx?$/, include: [ filter(source('routes') + '/{*,*/index}.{js,jsx,ts,tsx}'), filter(source('components') + '/{routes,async}/{*,*/index}.{js,jsx,ts,tsx}'), ], loader: asyncLoader, // @preact/async-loader options: { name(filename) { filename = normalizePath(filename); let relative = filename.replace(normalizePath(src), ''); if (!relative.includes('/routes/')) return false; return 'route-' + cleanFilename(relative); }, formatName(filename) { filename = normalizePath(filename); let relative = filename.replace(normalizePath(source('.')), ''); return cleanFilename(relative); }, }, } ]
@preact/async-loader
的 pitch 方法:
return ` import Async from ${stringifyRequest( this, path.resolve( __dirname, mode === PREACT_LEGACY_MODE ? 'async-legacy.js' : 'async.js' ) )}; function load(cb) { require.ensure([], function (require) { var result = require(${stringifyRequest(this, '!!' + req)}); typeof cb === 'function' && cb(result); }${name ? ', ' + JSON.stringify(name) : ''}); } export default Async(load); `;
而 async.js
返回一个只负责加载异步组件的空组件
function AsyncComponent() { Component.call(this); if (!component) { this.componentWillMount = () => { load(mod => { component = (mod && mod.default) || mod; this.setState({}); }); }; this.shouldComponentUpdate = () => component != null; } this.render = props => { if (component) { return h(component, props); } const prev = getPreviousSibling(this.__v); const me = (prev && prev.nextSibling) || (this.__P || this._parentDom).firstChild; if (!me) return; if (me.nodeType === 3) return me.data; return h(me.localName, { dangerouslySetInnerHTML: PENDING, }); }; }
很巧妙的 this.setState({})
触发渲染。