1. 先把Webpack的本质讲清楚
1.1 它到底解决了什么问题
很多同学接触Webpack,上来就是被各种配置折磨。这个插件装一下,那个loader配一下,跑通了就皆大欢喜,跑不通就在Stack Overflow和GitHub Issues里泡一天。但如果你问我,学Webpack最重要的是什么,我只有一个答案:先搞清楚它到底在解决什么问题。
前端开发走到今天,项目早就不是“一个HTML文件引几个script标签”的形态了。代码要分模块,模块之间有依赖,依赖可能来自npm包,还可能要编译TypeScript、转换JSX、处理Sass、打包图片字体。浏览器凭什么直接运行这些东西?它不认识你写的.vue文件,不认识.ts的语法,更不认识import xxx from 'yyy'这种ES Module的导入方式。你需要在代码运行之前,有一道工序把这些全部处理成浏览器能识别的东西,这道工序就是Webpack这类打包工具存在的意义。
所以Webpack的官方定位是“模块打包器”,它解决三个核心问题:一是模块化,把零散的、按需拆分的文件统一管理起来;二是转换,通过loader把各种各样的源文件转成标准JavaScript;三是优化,把最终产物做压缩、分割、缓存策略,让用户能更高效地加载。
1.2 一张依赖图串起所有核心概念
理解Webpack最关键的一个画面,就是“依赖图”。
Webpack会从你指定的入口文件(entry)出发,顺着import、require这些语句,把项目里所有被引用的文件一个不落地找出来。每找到一个文件,它就会根据配置的规则去转换这个文件,同时把这个文件和相关依赖的关系记录下来。整个过程形成一个巨大的图结构,这个图叫模块依赖图。
后面所有操作都是围绕这张图展开的。代码分割是尝试把这张图切成多块,按需加载;Tree Shaking是在这张图里找“没被引用的死代码”并摇掉;缓存是基于这张图里每个文件的内容生成哈希,内容变了才重新编译。你会慢慢发现,所谓“理解Webpack”,其实就是理解它如何构建这张图、如何利用这张图。
1.3 六个必须记住的名词
- Entry:入口,Webpack构建依赖图的起点,告诉它“从哪个文件开始找”。
- Output:出口,打包后的文件放在哪里、叫什么名字。
- Loader:转换器,把非JS文件(图片、CSS、TS、Vue等)转换成Webpack能处理的模块。它像厨师,负责加工原材料。
- Plugin:插件,从打包优化到环境变量注入,凡是Loader做不了的“额外工作”都归它管。它像工程建设中的监理,贯穿整个流程。
- Module:模块,Webpack里一切皆模块,一个JS文件、一张图片、一段CSS都算。
- Chunk:代码块,依赖图被切分后形成的片段,可能最终对应一个或多个Bundle文件。
面试最常问的loader和plugin区别,本质上就是:loader负责“文件级别的转换”,plugin负责“构建流程级别的干预”。这个后面细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置五要素逐个拆解
2.1 entry和output是打包的起点和终点
entry最简单的写法就是一个字符串:
javascript复制module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
一个入口对应一个输出,这是多数demo的形态。但真实的项目里,多页面应用(MPA)常常需要多入口:
javascript复制module.exports = {
entry: {
home: './src/pages/home.js',
detail: './src/pages/detail.js',
user: './src/pages/user.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js'
}
};
[name]会对应entry里的key,output的filename支持这种模板字符串。我最想提醒的是[hash]、[chunkhash]、[contenthash]这三个的差别,面试也经常问。
hash是整次构建的哈希,只要任何文件变了,所有文件名都会变。chunkhash是某个chunk的哈希,chunk内的变化会影响它。contenthash是根据文件内容生成的哈希,内容不变就不变,最适合做缓存。
实际项目里,JS文件用[contenthash:8],CSS文件用[contenthash:8],图片字体资源也用[contenthash:8]。目标很简单——内容不变的文件,文件名就不变,浏览器缓存就能一直命中。
2.2 loader是整个体系里最容易犯错的地方
loader的配置格式是test加use,test用来匹配文件类型,use指定用哪些loader去处理:
javascript复制module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpe?g|gif|webp)$/,
type: 'asset'
}
]
}
};
很多人第一次写CSS配置都会写错执行顺序。记住一个原则:loader的执行顺序是从右到左,从下到上。['style-loader', 'css-loader']的意思是先用css-loader解析CSS中的@import和url(),然后把解析后的结果交给style-loader,由它把样式插入页面的<style>标签里。顺序反了就报错。
loader本身就是一个函数,接收文件内容作为入参,返回处理后的内容。你可以自己写一个loader试试:
javascript复制// my-loader.js
module.exports = function (source) {
// source就是文件原文
const result = source.replace(/console\.log\([^)]*\)/g, '');
return result;
};
配置里加一行{ test: /\.js$/, use: './my-loader.js' }就能用。理解了这一点,用官方loader时就不会把它当黑盒。
2.3 plugin不是loader的替代品
Loader管文件转换,Plugin管整个构建生命周期
