你配置过Webpack吗?是不是照着文档写了几行
rules和plugins,然后它就神奇地把代码打包好了?今天我们不背配置,直接钻进Webpack的肚子里,看看Loader和Plugin到底在干什么。看完你就能自己写一个Loader和一个Plugin,再也不用怕面试官问“Webpack原理”了。
前言
把Webpack想象成一家汽车工厂。原料是各种文件(JS、CSS、图片、字体……),产品是打包后的bundle。
- Loader:流水线上的工人。每个工人只干一件事:把某种原料加工成下一个工人能处理的形式。比如把Sass转成CSS,把ES6转成ES5。
- Plugin:包工头。包工头不管具体加工,而是监听整个生产流程——开工前、完成某个环节后、打包结束——然后在合适的时机做全局性的事,比如抽离CSS、生成HTML、压缩代码。
今天我们就来认识这两位“功臣”,顺便自己动手写一个。
一、Loader:干啥啥都行,专精第一名
Loader是一个函数,它接收源文件内容,返回处理后的内容。一个文件可以经过多个Loader串联(从右到左,从下到上)。
// 一个最简单的Loader:把内容里的“Hello”换成“Hi”
module.exports = function(source) {
const result = source.replace(/Hello/g, 'Hi');
return result;
};
配置里Loader的执行顺序
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
// 执行顺序:sass-loader → css-loader → style-loader
}
]
}
};
像流水线:sass-loader把SCSS转成CSS → css-loader处理CSS中的依赖(@import等)→ style-loader把CSS注入到DOM的<style>标签。
常用Loader举例
babel-loader:把ES6+转成ES5css-loader:解析@import和url()style-loader:把CSS插入DOMfile-loader:把文件输出到目录,返回路径url-loader:小文件转成base64,大文件走file-loadersass-loader:编译Sass/SCSS
动手写一个“清除console”的Loader
// clean-console-loader.js
module.exports = function(source) {
// 移除console.log、console.warn等
const cleaned = source.replace(/console\.(log|warn|error)\([^)]*\);?/g, '');
return cleaned;
};
在webpack.config.js里使用:
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, 'clean-console-loader.js')
}
]
}
二、Plugin:包工头,管全局
Plugin是一个类(或者带有apply方法的对象)。它通过监听Webpack生命周期里的钩子(hooks),在特定时机干预打包过程。
class MyPlugin {
apply(compiler) {
// 在打包结束后执行
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('🎉 打包完成了!');
});
}
}
Webpack的钩子有同步和异步之分。比如emit(生成资源到输出目录之前)是异步的,要用tapAsync。
常用Plugin举例
HtmlWebpackPlugin:自动生成HTML,并注入打包后的JS/CSSMiniCssExtractPlugin:把CSS抽成单独文件DefinePlugin:定义全局常量(比如环境变量)CleanWebpackPlugin:打包前清理输出目录
动手写一个“打包完成发通知”的Plugin
class NotifyPlugin {
apply(compiler) {
compiler.hooks.done.tap('NotifyPlugin', (stats) => {
const time = stats.endTime - stats.startTime;
console.log(`✅ 打包成功,耗时 ${time}ms`);
// 这里可以调系统通知API(需要额外库)
});
}
}
module.exports = NotifyPlugin;
使用:
const NotifyPlugin = require('./notify-plugin');
plugins: [new NotifyPlugin()]
三、Loader和Plugin的核心区别
| 维度 | Loader | Plugin |
|---|---|---|
| 职责 | 转换单个文件 | 影响整个构建流程 |
| 作用范围 | 匹配test正则的文件 | 全局 |
| 实现形式 | 函数 | 类(带apply方法) |
| 运行时机 | 模块加载过程中 | 生命周期钩子 |
| 常见例子 | babel-loader, css-loader | HtmlWebpackPlugin, CleanPlugin |
形象比喻:
- Loader:工人,只会加工原料。
- Plugin:包工头,指挥全局,监听事件。
四、编写Loader的进阶技巧
1. 获取Loader选项
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用options...
return source;
};
2. 异步Loader
如果Loader里要异步操作(比如网络请求),用this.async()。
module.exports = function(source) {
const callback = this.async();
setTimeout(() => {
const result = source.toUpperCase();
callback(null, result);
}, 1000);
};
3. 生成多个文件
可以用this.emitFile。
const { RawSource } = require('webpack-sources');
module.exports = function(source) {
this.emitFile('new-file.txt', new RawSource('hello'));
return source;
};
五、编写Plugin的进阶技巧
1. 常用钩子
compiler.hooks.entryOption:读取入口配置后compiler.hooks.beforeRun:开始编译前compiler.hooks.compile:编译前compiler.hooks.emit:生成资源到输出目录前(可以修改文件内容)compiler.hooks.done:打包完成
2. 修改输出内容
class ModifyFilePlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('ModifyFilePlugin', (compilation, callback) => {
// compilation.assets 包含所有待输出文件
const content = compilation.assets['bundle.js'].source();
const modified = content.replace('old', 'new');
compilation.assets['bundle.js'] = {
source: () => modified,
size: () => modified.length
};
callback();
});
}
}
六、总结:从配置使用者到原理掌握者
- Loader:文件转换器,函数式,串联处理。
- Plugin:构建流程干预者,事件监听式,做全局工作。
- 掌握原理后,你就能:
- 写自定义Loader处理特殊文件(比如把XML转成JS对象)
- 写自定义Plugin做自动上传CDN、生成资源清单等
- 更从容地调试Webpack配置错误
下次面试官问“Webpack的Loader和Plugin区别”,你可以自信地画出流水线和包工头的比喻,顺便掏出自己写的Loader和Plugin代码。
如果你觉得今天的“工厂之旅”够形象,点个赞让更多人看到。明天我们将深入Webpack优化——如何让打包速度飞起,让产物体积瘦成闪电。我们明天见!