
直接上手打包之前我建议先弄明白手里的工具到底在替你干什么活。用WebPack官方拼写是webpack但大家都习惯了叫它WebPack之前我接触过很长时间传统的页面开发引几个script标签按顺序依赖全局变量样式靠手写CSS文件一个个link进来。小项目还好一旦页面多了、公共代码抽出来了依赖顺序错一个控制台能给你刷一整屏报错。后来我转到webpack工作流最大的感受就是“原来资源还能这样管”。这篇文章我会从零开始把webpack的安装、核心概念、以及打包js/css/img三类资源的过程完整走一遍。不管你是刚接触前端工程化的新人还是从传统开发方式转过来的老手只要跟着把每一步做了就能建立起一套可运行的本地构建环境后面再去看vue-cli或create-react-app之类的封装配置你也能看懂底层到底发生了什么。1. 项目初始化与安装细节1.1 环境准备Node.js版本与npm源webpack本身是用Node.js写的所以第一步是确保电脑上有可用的Node环境。我建议装LTS版本没必要追最新版。这里有一个容易踩的坑webpack 5对Node.js的版本要求是10.13.0以上但如果要用到一些较新的语法特性Node 14会更稳妥。你可以用一个简单命令检查node -v npm -v两个命令都有输出版本号就说明基础环境没问题。npm源在国内访问官方源经常卡在网络请求上安装依赖时进度条半天不动。我用的是淘宝镜像的registry设置方式很简单npm config set registry https://registry.npmmirror.com注意设置镜像地址属于日常开发习惯具体网络策略请按你所在环境的规范来。如果你的网络访问官方源没问题跳过这一步完全不影响后面的操作。1.2 初始化package.json在项目目录下打开终端执行npm init -y这条命令会自动生成一个基础的package.json文件里面记录了项目名称、版本、入口文件、脚本命令等元信息。-y参数的意思是跳过交互式提问直接用默认值生成。如果你愿意手动填去掉-y它会逐个问你name、version、description、entry point这些字段。这里我额外多说一句package.json是项目的“身份证”后面安装的依赖会记录在dependencies或devDependencies字段里别人拿到你的项目后执行npm install就能复原整个依赖环境。这也是webpack工作流相比传统“拷贝一个jquery.js到项目里”最大的优势——依赖可管理、可追溯。1.3 本地安装webpack与webpack-cli安装命令看起来简单但有几个细节值得注意。我推荐本地安装而不是全局安装npm install webpack webpack-cli --save-dev为什么不推荐全局安装我在本地和全局两种方式都试过。全局安装的webpack在命令行里能直接调用但它会把构建工具版本锁死在某一个版本上当两个项目一个需要webpack 4、一个需要webpack 5时全局的版本无法同时满足冲突起来非常麻烦。而本地安装会把webpack装进当前项目的node_modules里每个项目都可以有自己的版本互不干扰。命令中的--save-dev含义是写入devDependencies因为webpack属于构建时工具最终线上运行并不需要它。安装完成后node_modules目录会非常庞大这是正常的。真正需要关注的验证方式是npx webpack -v能输出版本号说明安装成功。npx的作用是“临时调用项目node_modules里的可执行文件”它不会污染全局环境比直接用webpack命令更规范。提示如果长时间卡在安装过程先不要反复删node_modules重试先检查npm源是否可达再看网络代理配置90%的安装超时问题都出在这两处。2. 核心概念Entry、Output、Loader与Plugin2.1 四大概念的分工逻辑如果你以前没用过构建工具第一次看webpack文档容易被概念淹没。我用自己的话总结一下这四者之间的关系Entry决定起点Output决定终点Loader负责“翻译”Plugin负责“干活”。Entry入口告诉webpack“从哪个文件开始找依赖关系”。整个应用是一个依赖图入口是这张图的起点。Output输出告诉webpack“打包出来的文件放到哪里、叫什么名字”。Loader加载器webpack本身只能识别JS文件遇到CSS、图片、字体这些格式时需要Loader把各种类型转成webpack能处理的模块。你可以把Loader看作一个翻译官CSS翻译成JS图片翻译成base64字符串或文件路径。Plugin插件比Loader更强大能做范围更广的事情比如打包前自动清理目录、把打包结果中的CSS抽取成单独文件、压缩代码等。我遇到不少初学者把Loader和Plugin混为一谈这里我用一个类比帮你区分Loader是“逐文件处理”它针对的是单个文件的格式转换Plugin是“全局任务调度”它可以在打包的不同生命周期阶段做事情。比如你要把一堆散落各地的JS文件合并压缩这个“合并压缩”是整体行为靠Loader做不到必须写一个Plugin或者用现成的插件。2.2 默认配置与零配置体验webpack 4和webpack 5的一个贴心之处是支持零配置打包。所谓零配置意思是如果你不写webpack.config.jswebpack会采用一套默认约定入口默认是./src/index.js输出默认是./dist/main.js模式默认是production所以理论上你只要建一个src目录里面放一个index.js执行npx webpack就能打包。但这套默认约定仅限于“最最简单的JS场景”一旦涉及CSS、图片、HTML模板零配置就不够用了。我的建议是零配置可以用来快速验证安装是否成功但正式项目第一步就应该创建配置文件后面所有自定义逻辑都从这里出发。2.3 配置文件的骨架结构一个最基础的webpack.config.js长这样const path require(path); module.exports { entry: ./src/index.js, output: { path: path.resolve(__dirname, dist), filename: bundle.js }, mode: development };这里面有四个信息值得解释一下入口这里用了字符串写法多页面项目的时候entry可以改成一个对象每个页面一个入口。output.path必须用绝对路径所以需要path.resolve和__dirname配合。output.filename决定最终打包出的文件名。dev环境下为了调试方便我习惯叫bundle.js生产环境则可以用contenthash命名后面再说。mode的值有development、production和none三档。development模式会启用很多帮助调试的特性比如命名函数、更快构建速度但不压缩代码production模式会做tree shaking和代码压缩构建速度反而慢一些。3. 打包JavaScript从小Demo到多入口配置3.1 一个起手式的模块化示例我在src目录下建了两个文件模拟最常见的模块依赖// src/hello.js export function sayHello(name) { console.log(Hello, ${name}!); }// src/index.js import { sayHello } from ./hello.js; sayHello(WebPack);然后执行npx webpack --modedevelopment打开dist目录下生成的bundle.js你会发现webpack把我们自己写的两个模块代码都包含进来了而且还给每个模块套了一层函数包裹。这就是webpack最核心的机制依赖分析。它从入口文件开始挨个解析import语句将整个文件之间的依赖关系网建出来最后统一打包成一个或几个文件。我把这个过程比作“搬家”你不需要自己把客厅的沙发、卧室的床头柜挨个搬下楼你只需要告诉搬家公司“所有房间的东西都搬走”搬家公司自己会进屋盘点、分类、装箱、运输。webpack的依赖分析就是“盘点”的过程你做的是在入口处告诉它“从这儿开始搬”。3.2 多入口与代码分割的初步认识很多传统网站并不是单页应用它们有首页、列表页、详情页每个页面各自加载各自的JS文件。这种情况下单入口配置就不够了需要改成多入口module.exports { mode: development, entry: { home: ./src/home.js, list: ./src/list.js, detail: ./src/detail.js }, output: { path: path.resolve(__dirname, dist), filename: [name].bundle.js } };这里的[name]是占位符构建时会被替换成entry对象里的键名最终生成home.bundle.js、list.bundle.js和detail.bundle.js。顺带一提这三个页面如果共用同一个公共模块比如公共的工具函数库webpack还能借助SplitChunksPlugin把公共代码抽取成单独的一个chunk避免每个页面都重复加载同一段代码。这是优化加载性能的重要手段不过对于入门阶段先跑通多入口就已经足够了。3.3 配置npm scripts替代长命令每次都敲npx webpack --modedevelopment确实不优雅一个更规范的替代方案是把构建命令写进package.json的scripts字段{ scripts: { build: webpack --modeproduction, dev: webpack --modedevelopment --watch } }有了scripts配置后执行npm run build就行。这种方式让其他同事接手项目时不用翻阅文档就能明白该敲什么命令。里面我加了--watch参数它的作用是监听文件变化一旦源文件变动就自动重新打包。不过我更推荐后面讲的webpack-dev-server体验会好得多。4. 打包CSSLoader的实战应用4.1 为什么webpack不能直接“看懂”CSS如果你在入口文件里写一句import ./style.css;然后直接打包webpack大概率会报错You may need an appropriate loader to handle this file type。原因就是我在前面说的webpack原生只认识JS模块。CSS文件在它眼里就是一段未知格式的文本不知道该怎么处理。解决办法是安装对应的Loader让“翻译官”上场npm install style-loader css-loader --save-devcss-loader和style-loader这对组合总被放在一起说但它们的职责完全不同css-loader负责解析CSS文件里的import、url()这些语句把CSS内容转化成JS能用的模块style-loader负责把css-loader解析出来的样式通过动态创建style标签的方式注入到HTML页面中。可以这样理解css-loader是把外文的书逐字翻译成中文style-loader是把翻译好的中文书念给你听。4.2 配置loader的三种写法在webpack.config.js中loader配置写在module.rules数组里module.exports { // ... module: { rules: [ { test: /\.css$/i, use: [style-loader, css-loader] } ] } };这里有两点我在实际使用中踩过坑重点说明一下test字段的值是一个正则表达式/\.css$/i表示“匹配以.css结尾的路径字符串”后面的i表示忽略大小写。新手容易把test写成字符串.css这样WebPack无法正确匹配——正则表达式才是它预期的格式。use数组的执行顺序是从后往前这一点几乎每个新手都会搞反。也就是说webpack先调用css-loader解析CSS内容把结果交给style-loader注入页面。如果你不小心写成[css-loader, style-loader]打包会报错因为style-loader收到的输入并不是它期望的格式。实际写的时候我还喜欢在use数组里为每个loader追加options配置比如{ test: /\.css$/i, use: [ style-loader, { loader: css-loader, options: { modules: true } } ] }options里的modules字段是CSS Modules开关。开启之后你在JS里import styles from ./style.css拿到的不是普通的样式插入结果而是一个映射对象可以像访问对象属性一样用类名div className{styles.container}。这种方式能有效避免样式类名污染问题在多团队协作的场景下尤其好用。不过这是进阶玩法入门阶段先掌握前两种写法就够了。4.3 单独的入口样式文件与extract实践前面那种style-loader方案把样式直接注入到JS里对于纯JS应用来说没问题但如果样式文件很多全部打进bundle.js里会导致JS体积迅速膨胀还会带来“样式加载依赖JS执行”的问题JS还没跑起来页面就是裸样式。生产环境下我们通常会希望把CSS抽取成独立的.css文件通过link标签并行加载。这个需求靠webpack内置能力做不了要用到插件npm install mini-css-extract-plugin --save-dev对应配置如下const MiniCssExtractPlugin require(mini-css-extract-plugin); module.exports { // ... module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, css-loader] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: css/[name].[contenthash].css }) ] };dev环境用style-loader就够了因为它支持热更新的体验更好生产环境则换成MiniCssExtractPlugin.loader让样式独立成文件。这是我在实际项目中一直坚持的区分方式。5. 打包图片Asset Modules详解5.1 从file-loader到内置asset module的变迁如果你在网上搜“webpack打包图片”会看到很多教程还在提file-loader和url-loader。这两个loader在webpack 4时代确实是标准方案但webpack 5已经在内置功能里取代了它们新增了asset modules这一套资源模块类型。也就是说webpack 5项目里不需要额外安装file-loader和url-loader直接用内置的asset/resource、asset/inline、asset等类型配置即可。我建议统一使用webpack 5的内置方式好处是少装两个依赖包、配置更简洁、长期维护也不用担心旧loader的兼容问题。5.2 三种资源处理策略的选择逻辑webpack 5的asset modules有四种type但日常处理图片主要用三种类型行为适用场景asset/resource文件直接被拷贝到输出目录返回URL大图片、字体文件asset/inline文件被转成base64字符串内嵌到JS里小图标、小体积图片asset自动在resource和inline之间切换默认8KB为阈值通用场景第三种asset类型是我最常用的因为它在体积小的时候自动内联、体积大的时候自动输出为文件不用我手动判断一张图应该走哪种方案。这个默认阈值8KB可以通过parser.dataUrlCondition.maxSize调整module.exports { // ... module: { rules: [ { test: /\.(png|jpe?g|gif|webp|svg)$/i, type: asset, parser: { dataUrlCondition: { maxSize: 10 * 1024 } } } ] } };看到10 * 1024你可能想问为什么图片大小阈值这么重要这里牵扯到HTTP请求的性能模型。如果一张图片只有几百字节你却让它独立成一个文件发起一个网络请求那为了加载这几百字节要额外承担完整的请求响应链路开销非常不划算。把它转成base64字符串塞到JS或CSS里就省掉了一个HTTP请求。但反过来如果图片有几MB还强行转base64编码后的字符串体积比原文件大三分之一左右同时会撑大CSS或JS文件体积反而拖慢加载。所以8KB左右的阈值是一个合理的经验值。5.3 图片打包的实操验证在src目录下放一张图片比如logo.png然后在CSS里引入它/* src/style.css */ .logo { background-image: url(./logo.png); width: 100px; height: 100px; }再在index.js里把CSS引入import ./style.css;执行打包后dist目录下会多出一个文件这个文件名是webpack根据内容哈希自动生成的。如果logo.png小于阈值你会发现dist下不会有这个图片文件因为它已经被转成了base64字符串嵌入在打包结果中。两种结果可以通过直接查看dist目录和打开页面确认。有一个很隐蔽的坑必须提醒如果你在CSS里写的图片路径不对比如写成url(./images/logo.png)但实际图片在src/assets下webpack打包时不会帮你纠正路径——它会直接报错“Module not found”。这个报错反而是好事因为它是构建期就能发现的错误而不是运行时才白屏。所以请一定保持路径书写和实际文件位置严格一致。5.4 输出文件名与缓存策略图片输出文件名默认会带上内容哈希这既是好事也需要理解其原因。webpack默认生成的名称类似1a2b3c4d5e6f.png这段字符串是根据文件内容计算出来的。只要文件内容不变哈希值不变生成的文件名也不变。浏览器对静态文件的缓存策略通常是“文件名没变就用缓存”这样同一张图片再次访问时直接被缓存命中下一次打包如果图片没改文件名也不会变缓存依然可用如果图片改了文件名就变浏览器自然加载新文件。这就是内容哈希contenthash的缓存友好逻辑。如果你希望图片放在单独的images目录下可以通过output.assetModuleFilename来配置output: { // ... assetModuleFilename: images/[hash][ext][query] }这里的[hash]是内容哈希[ext]保留原扩展名[query]保留查询参数。个人习惯上图片和字体统一放到images目录CSS在css目录JS在js目录dist一眼扫过去结构就非常清爽。6. 开发服务器与完整的本地调试体验6.1 webpack-dev-server带来的开发效率提升前面提到过--watch模式可以自动监测文件变化重新打包但它的体验还不够好每次重新打包后你还得手动刷新浏览器而且watch模式会把打包结果写到dist目录频繁读写磁盘对性能也不友好。更理想的开发方案是webpack-dev-server。它的特点是启动一个本地HTTP服务并基于WebSocket与浏览器建立实时连接。当源代码变化时dev server在内存中完成打包通过WebSocket通知浏览器自动刷新页面或局部更新样式整个过程不需要你手动刷新。样式修改时的“热更新”体验尤其好页面不动、只是样式变了这种反馈速度对前端开发效率的提升非常明显。安装命令npm install webpack-dev-server --save-dev6.2 配置devServer与npm脚本在webpack.config.js中加入module.exports { // ... devServer: { static: { directory: path.resolve(__dirname, dist) }, port: 8080, hot: true, open: true } };static字段指定了dev server托管的静态文件目录。这个其实也有讲究如果你还没有任何HTML文件dev server会以目录列表形式展示若要用真实的HTML模板我建议配合html-webpack-plugin来使用让打包结果自动生成带正确资源引用的HTML文件。port指定端口hot开启热更新open设置启动后自动打开浏览器。这几个配置都是字面意思但有一个坑我见过很多次有人把static目录配置成了src虽然页面能打开但引用的JS文件路径会对不上——因为dev server内存中的打包结果并不会真实写入磁盘它默认服务的静态文件目录应该指向你的打包输出目录。6.3 一个完整的HTML页面配合前面所有打包都停留在“产生了文件”层面但你实际打开页面还需要一个HTML。手动去dist目录写一个HTML当然可以但里面引用的带哈希的文件名每次都变手动维护不现实。这里推荐一个插件html-webpack-plugin。npm install html-webpack-plugin --save-dev配置const HtmlWebpackPlugin require(html-webpack-plugin); module.exports { // ... plugins: [ new HtmlWebpackPlugin({ template: ./src/index.html }) ] };然后在src目录下创建index.html模板不需要手动写script标签!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWebPack Demo/title /head body div idapp/div /body /htmlhtml-webpack-plugin的作用就是在每次打包时读取模板自动把打包好的JS和CSS文件以script和link标签的形式注入到HTML中。文件哈希变了插件生成的HTML引用也跟着变彻底解决了“手动维护文件名”的烦恼。7. 构建性能优化与常见问题排查实录7.1 大项目构建慢的3个优化方向随着项目膨胀构建速度变慢几乎是必然的。我整理了几个自己实践过的优化方向配置resolve.alias减少模块解析范围比如将react直接指向node_modules里的具体文件路径webpack就不用一层层地向上查找依赖。使用module.noParse告诉webpack某些文件不需要解析比如一些大型库的压缩版本它们已经是可直接运行的代码不需要再做依赖分析。前提是你确认该库确实没有依赖别的模块。合理使用cachewebpack 5内置了持久化缓存功能配置cache: { type: filesystem }可以让增量构建大幅提速。原理是把构建过程产生的中间产物缓存到文件系统下次构建直接复用省去重复处理时间。还有一点容易被忽略生产构建时应该关闭source map或只保留简化的map因为生成source map本身也消耗不少时间。开发环境开启cheap-module-source-map生产环境可以用hidden-source-map或直接关掉按需取舍即可。7.2 打包后的代码体积分析代码写完后怎么看打包结果到底包含了什么、有没有冗余我用过最好的工具是webpack-bundle-analyzer。这个插件会在构建完成后打开一个交互气泡图把每个包的大小、模块的占比可视化展示出来。安装方式npm install webpack-bundle-analyzer --save-dev在webpack.config.js中const BundleAnalyzerPlugin require(webpack-bundle-analyzer).BundleAnalyzerPlugin; module.exports { // ... plugins: [ new BundleAnalyzerPlugin() ] };执行打包后会自动打开分析报告页面。通过这个界面能直观看到哪个第三方库才是体积大头再决定是否换用更精简的库、是否按需引入、是否拆分chunk。我在很多项目里靠这个工具发现“体积大爆点”其实是某个工具库的完整版本而不是业务代码本身。7.3 常见报错速查表我自己踩过的那些坑报错信息原因解决方案Module not found: Cant resolve xxx依赖没有安装或路径写错检查node_modules是否存在该包检查import路径大小写与文件实际路径一致You may need an appropriate loader文件类型与loader不对应确认rules中test正则匹配了该文件类型且loader已正确安装Conflicting order between chunksCSS在多个chunk中引入顺序不一致从源头上统一模块的导入顺序或调整runtimeChunk配置Invalid configuration object配置文件格式错误检查devServer等字段是否拼写正确webpack的配置校验信息会明确指出哪个字段出错Asset size limit warning产物体积超过webpack默认244KB限额按业务实际需要在performance.maxAssetSize上调阈值或拆分资源7.4 调试source map配置选择开发阶段报错信息就像天书一样看不出对应源码位置这种感觉你体验过就懂。source map的作用就是让浏览器工具能把你写的源代码和打包后的代码对应起来定位到真实的行号和变量名。开发环境和生产环境对source map策略的选择是不同的。我的习惯是module.exports { mode: development, devtool: cheap-module-source-map };这里说一下选项的含义cheap表示只追踪到行级别不做列级别映射因为列级别的映射开销很大而行级别足以定位绝大多数问题module表示对loader转换后的源码也生成映射否则你看到的会是经过loader转译后的代码而不是原始业务代码source-map表示生成独立的.map文件。开发环境下这个组合兼顾了定位精度和构建速度。生产环境我通常直接关闭source map或使用hidden-source-map后者会生成.map文件但在打包产物中不写入注释连接避免暴露源代码的同时保留错误排查能力。如果你不需要线上排错直接production模式下不配devtool即可。8. 从Demo走向真实项目我的经验总结走到这一步你已经能完成webpack的安装、打包js/css/img三类资源、配置开发服务器、优化构建速度和排查报错已经完全具备了接手真实项目构建配置的能力。不过我想额外补充几点真实项目里比Demo复杂得多的地方第一处理路径时要统一标准。项目里有人用/作为别名指向src目录有人又用相对路径容易引起混乱和“Module not found”。建议在初期就约定好路径别名并在eslint或代码审查上加一道关卡。配置别名的方式很简单在resolve.alias中声明resolve: { alias: { : path.resolve(__dirname, src) } }第二环境变量区分。每个项目迟早都要区分开发环境、测试环境和生产环境的接口地址。webpack内置的DefinePlugin或webpack-define-plugin可以实现环境变量注入这个在你后续对接后端接口时会经常用到。第三不要把webpack配置当成一次性玩具。我见过太多人从网上复制一份配置能用就再也不管可是webpack的配置维护是一个持续演进的过程新版本升级、loader更新、项目结构变化都可能让旧配置悄悄失配。建议在每次升级webpack主版本后系统性地跑一遍完整流程确认所有loader和插件兼容不要等上线前夕才临时抱佛脚。我个人在实际操作中还有一个习惯每新建一个项目先不管业务代码先用webpack把“空壳工程”完整跑通再开始写功能。一个能稳定构建的骨架永远是你后面写代码最扎实的地基。