Loading...
webpack自定义loader和plugin
4/25/2024, 7:54:05 PM
开发
一个loader就是一个Node.js 模块,这个模块需要导出一个函数,这个导出的函数的工作就是获得处理前的源内容,对源内容进行处理后,返回处理后的内容(类似rollup的构建钩子)。
简而言之,loader就是处理源代码的一个处理函数,很多非js模块通过loader转为js模块(因为webpack识别单位是js模块)。
loader 可以用return
返回处理后的内容; 也可以通过 this.callback(null, content)
返回内容, 此时return
返回值必须为undefined
注意:loader 中导出的函数不能是箭头函数,原因是 webpack 为 loader 提供的 API 都绑定在了 this 对象上;直接和webpack相关的配置、插件、loader推荐使用CMD规范
// test-loader.js module.exports = function(source){ // this.query 和 this.query 一致, 都是用户传入Loader的配置 const options = this.getOptions(); // 将main.js 中的info替换为 info-text console.log(source, options, this.query); return String(source).replace(/-info/ig, options.msg); } //... // webpack.config.js: webpack配置文件及相关插件、loader,推荐使用CMD而非ESM来进行模块化: module.exports = { // 导入多个 loaders: 这样使用loader(test)路径的使用只需要使用文件名就可以了 resolveLoader: { modules: ['node_modules', path.resolve(__dirname, 'loader')], }, module: { rules: [ { test: /.js$/, use: [{ loader: 'test-loader', options: { msg:'哈哈哈' } }, ], } ] }, }
webpack 为 loader 提供的 this.async()
将这个 loader 变成是一个异步 loader, this.callback 用法如下:
this.callback( // 当无法转换源内容时,给 Webpack 返回一个 Error err: Error | null, // 源内容转换后的内容 content: string | Buffer, // 用于把转换后的内容得出原内容的 Source Map,方便调试 sourceMap?: SourceMap, // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回, // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能 abstractSyntaxTree?: AST );
案例:
// test-async-loader.js module.exports = function (source) { // 调用 this.async() API,告诉 webpack本次转换是异步的,loader 会在 callback 中返回结果 const callback = this.async(); // 使用 setTimeout 模拟异步过程 setTimeout(() => { // 将文本中的hello替换为哈哈 const content = source.replace("hello", "哈哈"); // 通过 callback 返回执行异步后的结果 callback(null, content); }, 3000); }; //... 用法同上
webpack插件的自定义难度要远大于loader的自定义,困难点在于你根本不知道某个对象有哪些可用的方法和属性,虽然webpack官网中给出了一些相关钩子,但是具体的各种数据结构,依然缺失的部分还有很多; 还是自己打断点,实地测试,比较有可行性。所以官网如是说:
创建插件比创建 loader 更加高级,因为你需要理解 webpack 底层的特性来处理相应的钩子,所以请做好阅读源码的准备!
如同webpack的loader实际就是一个转换字符串的函数一样;webpack 插件是一个 JavaScript 构造函数或 JavaScript 类,在插件函数的 prototype 上定义一个 apply 方法。
简而言之,webpack插件实际就是一个有定义apply()方法的构造函数,正因为他是构造函数,所以在使用是时候需要 [ new PluginTest(), ]
这样进行new
操作。
Webpack的基本构建流程如下:
在webpack中,Compiler和Compilation是两个核心概念,它们在webpack的构建过程中起着不同的作用。
Compiler是webpack的运行入口,负责编译,它贯穿webpack的整个生命周期。当启动webpack时,Compiler对象会被实例化,且这个对象是全局唯一的。Compiler对象包含了当前运行webpack的配置,包括entry、output、loaders等。Plugin可以通过Compiler对象获取到webpack的配置信息进行处理。
而Compilation则是由Compiler实例化的,Compilation对象可以理解编译对象,每次构建都会产生一个Compilation实例。Compilation对象主要用来存储构建过程中流程使用到的数据,包含了模块、依赖、文件等信息,并控制这些数据的变化。在开发模式下运行Webpack时,每修改一次文件都会产生一个新的Compilation对象,Plugin可以访问到本次编译过程中的模块、依赖、文件内容等信息。
总的来说,Compiler是webpack的构建入口和全局配置对象,而Compilation则是构建过程中产生的具体实例,用来控制和管理构建过程中的数据变化。这两者协同工作,使得webpack能够完成从源代码到最终输出文件的编译和打包过程。
compilation对象的方法(怎么不列出属性?) compiler钩子 compilation钩子
Tapable是Webpack的一个核心工具,Webpack中许多对象扩展自Tapable类,Tapable类暴露了tap、tapAsync和tapPromise方法。各种钩子都拓展此类,包括compiler 钩子(如compiler.hooks.emit.tapAsync()
)和compilation钩子(如compilation.hooks.buildModule.tap()
),可以根据钩子的同步/异步方式来选择一个函数注入逻辑。
案例:
// tap() 设置同步钩子回调: compiler.hooks.compile.tap('MyWebpackPlugin', params => { console.log('我是同步钩子')}); // tapAsync() 设置异步钩子回调,我们可以通过callback告知Webpack异步逻辑执行完毕。下面是一个在emit阶段的示例,在1秒后打印文件列表。 compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => { setTimeout(()=>{ console.log('文件列表', Object.keys(compilation.assets).join(',')); callback(); }, 1000); }); // tapPromise()也是设置异步钩子回调,和tapAsync的区别在于tapPromise是通过返回Promise来告知Webpack异步逻辑执行完毕。 // 下面是一个将生成结果上传到CDN的示例。 compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => { return new Promise((resolve, reject) => { const filelist = Object.keys(compilation.assets); uploadToCDN(filelist, (err) => { if(err) { reject(err); return; } resolve(); }); }); });
Compiler 对象是 webpack 的主要引擎,因此主要列举Compiler对象的钩子,compilation 对象钩子也有很多,但是不咋常见
读取输出资源、模块及依赖: 在emit阶段,我们可以读取最终需要输出的资源、chunk、模块和对应的依赖,如果有需要还可以更改输出资源。
apply(compiler) { compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => { // compilation.chunks存放了代码块列表 compilation.chunks.forEach(chunk => { // chunk包含多个模块,通过chunk.modulesIterable可以遍历模块列表 for(const module of chunk.modulesIterable) { // module包含多个依赖,通过module.dependencies进行遍历 module.dependencies.forEach(dependency => { console.log(dependency); }); } }); callback(); }); }
修改输出资源 通过操作compilation.assets对象,我们可以添加、删除、更改最终输出的资源。
apply(compiler) { compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation) => { // 修改或添加资源 compilation.assets['main.js'] = { source() { return 'modified content'; }, size() { return this.source().length; } }; // 删除资源 delete compilation.assets['main.js']; }); }
通过在emit阶段操作compilation.assets实现。构建完成后会在build目录添加manifest.json
, 文件描述各个静态文件的路径和大小。
class MyWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => { const manifest = {}; for (const name of Object.keys(compilation.assets)) { manifest[name] = compilation.assets[name].size(); // 将生成文件的文件名和大小写入manifest对象 } compilation.assets['manifest.json'] = { source() { return JSON.stringify(manifest); }, size() { return this.source().length; } }; callback(); }); } } module.exports = MyWebpackPlugin;
文章目录