1. 从散落的静态文件到可交付产物:这次打包到底要解决什么
手里有一套品优购的静态商城页面,纯HTML、CSS加一点图片和字体图标,没有框架、没有模块化、没有构建工具。这种项目在本地双击打开index.html就能跑,看起来岁月静好,但真要交付或者上线,问题就全冒出来了:CSS里引用的背景图路径是相对路径,换一层目录就全挂;多个页面重复引用了同一套reset样式,改一处要改十几个文件;浏览器缓存不刷新,用户看到的还是旧样式;图片没压缩,首屏加载慢得让人想关页面。
这次要干的事情,就是用webpack把这套纯静态站点打包成一份可以直接丢到服务器上的产物。注意,这里说的"打包"不是把HTML塞进JS里那种SPA玩法,而是让webpack接管资源处理流程:压缩CSS、压缩HTML、处理图片、加hash指纹、自动注入引用。最终产物还是HTML+CSS+图片,只是变得更小、更规范、更适合部署。
适合谁看?如果你手头有类似的传统静态站,想引入工程化但不想大动干戈重构,这篇就是给你写的。如果你已经用惯了Vite或者Parcel,也可以看看webpack处理纯静态资源时的那些细节,很多坑是通用的。我默认你装好了Node环境,知道npm install是怎么回事,至于webpack配置,咱们从零开始搭。
先说清楚一个前提:纯静态站引入webpack,最大的价值不在于"模块化",而在于"资源管道"。HTML、CSS、图片、字体,全部经过一条流水线处理,输出统一规格的产物。这个定位想明白了,后面的配置取舍就顺了。
2. 动手前的环境盘点和目录规划
2.1 原始项目结构长什么样
品优购这类静态商城,典型结构是这样的:根目录下平铺着index.html、list.html、detail.html、cart.html等页面,一个css文件夹放着base.css、index.css、list.css,一个images文件夹塞满商品图和图标,可能还有个fonts文件夹放图标字体。页面之间靠a标签跳转,CSS靠link标签引入,图片靠相对路径引用。
这种结构的特点是"扁平且耦合":所有页面共享同一层级的资源路径,任何一个文件挪位置,引用它的地方都得跟着改。webpack要做的第一件事,就是把这些散落的引用关系收拢到配置里统一管理。
2.2 为什么源码和产物必须分开放
我见过不少人直接在原目录上跑webpack,输出也丢在原目录,结果第二次构建时webpack把上次的产物又当成源码处理了一遍,越滚越大。正确做法是明确区分src和dist:
project/ src/ index.html list.html css/ base.css index.css images/ logo.png banner.jpg fonts/ iconfont.woff2 dist/ package.json webpack.config.jssrc放原始文件,dist放构建产物,每次构建前清空dist。这个习惯看着简单,但能省掉后面无数"为什么产物里有重复文件"的困惑。目录规划这一步没有技术含量,但它是后面所有配置的地基,地基歪了楼就盖不高。
2.3 依赖清单和版本选择的考量
纯静态站打包需要的依赖不多,但每个都有讲究:
| 依赖包 | 作用 | 选型理由 |
|---|---|---|
| webpack | 核心构建工具 | 5.x版本,长期维护稳定 |
| webpack-cli | 命令行接口 | 配合webpack 5使用 |
| html-webpack-plugin | 处理HTML模板 | 支持多页面配置 |
| mini-css-extract-plugin | 抽离CSS | 比style-loader更适合生产环境 |
| css-minimizer-webpack-plugin | 压缩CSS | webpack 5推荐方案 |
| css-loader | 解析CSS中的引用 | 处理url()和@import |
| html-loader | 处理HTML中的资源引用 | 让img src能被webpack识别 |
| copy-webpack-plugin | 复制静态资源 | 处理不需要处理的文件 |
| clean-webpack-plugin | 清理dist | 或直接用output.clean |
这里有个容易踩的坑:webpack 5内置了asset modules,图片和字体其实可以不装file-loader/url-loader,直接用type: 'asset'就行。很多人还在照搬webpack 4的教程装一堆loader,结果配置冲突。我的建议是能用内置就用内置,少一个依赖少一个坑。
版本上,webpack 5对Node版本有要求,至少Node 10.13以上,实际用建议16以上。装依赖时别用太老的版本组合,html-webpack-plugin 5.x配webpack 5.x是经过验证的稳定搭配。
3. webpack配置的骨架搭建与多页面入口设计
3.1 配置文件的基本形态
webpack.config.js用CommonJS写法最省心,不需要额外配置babel转译配置文件本身。骨架大概长这样:
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); module.exports = { mode: 'production', entry: { index: './src/index.html', list: './src/list.html' }, output: { path: path.resolve(__dirname, 'dist'), filename: 'js/[name].[contenthash:8].js', clean: true }, module: { rules: [] }, plugins: [], optimization: { minimizer: ['...', new CssMinimizerPlugin()] } };注意optimization.minimizer里的'...',这是webpack 5的语法,表示保留默认的JS压缩器,同时追加CSS压缩器。如果不写这个省略号,默认的TerserPlugin会被覆盖掉,JS就不压缩了。这个细节很多教程不讲,但实际项目里一旦漏了,产物体积会大一圈。
3.2 多页面入口为什么不能直接写HTML
上面entry里我写了HTML文件作为入口,这其实是个取巧做法,需要配合html-loader才能跑通。更规范的做法是每个页面配一个JS入口,哪怕这个JS是空的:
entry: { index: './src/js/index.js', list: './src/js/list.js' }然后在index.js里import '../css/index.css',让CSS通过JS被引入。这样做的好处是依赖关系清晰,webpack能准确追踪每个页面用到哪些资源。坏处是要为每个页面建一个JS文件,哪怕里面只有一行import。
对于纯静态站,我倾向于用HTML作为入口配合html-loader,因为改动最小,不用为每个页面造JS文件。但要注意html-loader的版本,2.x和3.x的配置写法有差异,3.x默认不处理<img src>,需要显式开启。
3.3 HtmlWebpackPlugin的多页面配置
每个HTML页面都要配一个HtmlWebpackPlugin实例:
plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html', chunks: ['index'] }), new HtmlWebpackPlugin({ template: './src/list.html', filename: 'list.html', chunks: ['list'] }) ]chunks字段很关键,它决定这个HTML注入哪些JS和CSS。如果不写,所有chunk都会被注入,页面会加载一堆用不上的资源。多页面项目里这个字段必须逐个配清楚。
还有个细节:如果多个页面共享同一套CSS,比如base.css,可以把它单独抽成一个chunk,然后在每个页面的chunks里都加上。这样base.css只会被打包一次,多个页面共用,而不是每个页面各打一份。
4. CSS、图片、字体的资源处理链路
4.1 CSS抽离与压缩的完整配置
CSS处理是纯静态站打包的重头戏。先看loader配置:
{ test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] }这里用MiniCssExtractPlugin.loader而不是style-loader,因为生产环境需要把CSS抽成独立文件,而不是通过JS动态插入style标签。style-loader适合开发环境的热更新,生产环境用它会导致页面闪一下才出样式,体验很差。
MiniCssExtractPlugin的实例配置:
new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' })contenthash是根据文件内容算出来的,内容不变hash就不变,这样浏览器缓存能最大化利用。改了一个CSS文件,只有那个文件的hash变,其他文件的缓存不受影响。这是生产环境必做的优化,比query string那种?v=1.0的方式靠谱得多。
压缩交给CssMinimizerPlugin,它在optimization.minimizer里配置,前面已经提过。压缩后的CSS会去掉注释、空格、合并重复规则,体积通常能减30%以上。
4.2 图片处理的三种策略
webpack 5的asset modules把图片处理简化成了三种类型:
asset/resource:总是输出为独立文件,适合大图asset/inline:转成base64内联,适合小图标asset:自动判断,小于阈值内联,大于阈值输出文件
配置示例:
{ test: /\.(png|jpe?g|gif|webp)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024 } }, generator: { filename: 'images/[name].[contenthash:8][ext]' } }阈值设8KB是个经验值。小于8KB的图标内联进CSS,减少HTTP请求;大于8KB的商品图输出独立文件,避免CSS文件过大。这个阈值不是死的,如果你的页面图标特别多,可以调到10KB;如果商品图普遍很大,调到4KB也行。
这里有个坑:base64内联会让CSS文件体积膨胀约33%,因为base64编码本身有开销。所以阈值不能设太大,否则CSS文件反而拖慢首屏。我一般控制在8KB以内,实测下来比较平衡。
4.3 字体图标文件的处理
字体图标文件(woff、woff2、ttf、eot)用asset/resource处理:
{ test: /\.(woff2?|eot|ttf|otf)$/i, type: 'asset/resource', generator: { filename: 'fonts/[name].[contenthash:8][ext]' } }字体文件不需要内联,因为体积通常不小,而且浏览器对字体文件的缓存策略和图片不同。输出到独立的fonts目录,配合contenthash,缓存管理很清晰。
要注意的是,CSS里引用字体用的是url(),css-loader会解析这个引用并交给asset module处理。如果字体路径写错了,构建时不会报错,但运行时字体加载失败,图标显示成方块。所以构建完一定要打开页面检查图标是否正常。
5. HTML压缩、资源注入与路径修正
5.1 html-loader处理HTML中的资源引用
纯静态站的HTML里有大量<img src="...">,webpack默认不认识这些引用。需要html-loader来解析:
{ test: /\.html$/, use: ['html-loader'] }html-loader会把HTML里的src、href等属性中的资源路径提取出来,交给对应的loader处理。处理完后,路径会被替换成构建后的路径。
但html-loader有个行为要注意:它默认会把HTML压缩成一行,如果你在模板里写了注释或者特定格式,可能会被破坏。如果不想压缩,可以配置sources: false只处理资源不压缩,或者用minimize选项控制。
5.2 HtmlWebpackPlugin的压缩选项
HtmlWebpackPlugin自带压缩功能,通过minify选项配置:
new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html', chunks: ['index'], minify: { collapseWhitespace: true, removeComments: true, removeAttributeQuotes: true, minifyCSS: true, minifyJS: true } })collapseWhitespace去掉多余空格,removeComments删注释,removeAttributeQuotes去掉属性引号(HTML5允许无引号属性),minifyCSS和minifyJS压缩内联的样式和脚本。这几个选项开下来,HTML体积能减20%到40%。
但要注意,removeAttributeQuotes在某些边缘情况下会出问题,比如属性值包含空格时。如果你的HTML里有class="a b c"这种多类名,去掉引号后变成class=a b c,浏览器解析会出错。所以这个选项要谨慎,我一般不开,或者开了之后仔细检查产物。
5.3 路径问题的排查思路
打包后最常见的报错就是资源404。排查路径问题有个固定套路:
- 打开dist目录,看文件实际输出到了哪里
- 打开产物的HTML,看引用的路径是什么
- 对比两者是否一致
常见原因有几个:output.publicPath没配导致路径是相对的,但页面在子目录里;asset module的generator.filename写错了目录;CSS里的url()路径和HTML里的路径基准不同。
publicPath这个配置值得单独说。如果产物部署在域名根目录,publicPath设'/';如果部署在子目录,比如/shop/,publicPath要设'/shop/'。设错了所有资源都会404。纯静态站如果不确定部署路径,可以先设'auto',webpack 5会根据HTML的位置自动推断,但多页面情况下auto不一定准,还是手动配最稳。
6. 构建产物验证与常见报错排查
6.1 构建后必须检查的几件事
构建成功不等于产物可用。每次构建完,我习惯做这几项检查:
- 打开dist里的HTML,确认CSS和JS引用路径正确
- 在浏览器里打开页面,看控制台有没有404
- 检查图标字体是否正常显示
- 检查图片是否都加载出来了
- 对比构建前后的文件体积
这几项检查花不了几分钟,但能拦住大部分低级错误。尤其是图标字体,构建时不报错,运行时才显示方块,不检查根本发现不了。
6.2 几个高频报错的定位方法
报错一:Module not found,找不到某个CSS或图片
这种通常是路径写错了。webpack的路径解析基准是配置文件所在目录,不是HTML所在目录。HTML里写../images/logo.png,在webpack看来可能是从项目根目录开始找。解决办法是用相对配置文件的路径,或者配resolve.alias简化路径。
报错二:CSS里的背景图不显示
css-loader处理url()时,如果路径是绝对路径(以/开头),webpack默认不会处理,直接原样输出。但构建后这个绝对路径可能指向错误的位置。解决办法是把CSS里的路径改成相对路径,或者配resolve.alias。
报错三:产物里有多余的JS文件
如果entry里配了HTML作为入口,webpack会为每个HTML生成一个JS文件,哪怕这个JS是空的。这些空JS文件虽然小,但没必要存在。解决办法是用html-loader配合HtmlWebpackPlugin,不把HTML作为entry,而是通过plugin处理。
报错四:contenthash每次构建都变
如果contenthash每次都变,说明有内容在变。常见原因是CSS里引用了带hash的图片,图片hash变了CSS的hash也跟着变。这是正常行为,但如果所有文件hash都变,可能是某个公共依赖在变。检查一下有没有引入时间戳之类的动态内容。
6.3 产物体积的优化空间
纯静态站打包后,体积优化主要在这几个方向:
- 图片压缩:webpack本身不做图片压缩,需要额外的image-minimizer-webpack-plugin,或者构建前用工具批量压缩
- CSS去重:多个页面共享的样式抽成公共chunk,避免重复打包
- 按需加载:如果某些页面的JS很大,可以配splitChunks做代码分割
- 移除未使用代码:纯静态站一般没有这个问题,但如果引入了第三方库,tree shaking能去掉没用到的部分
图片压缩是收益最大的。品优购这类商城,商品图往往占总体积的70%以上。构建前用工具把图片压一遍,比任何webpack配置优化都管用。
7. 从开发到部署的完整工作流
7.1 开发阶段的配置差异
生产配置和开发配置应该分开。开发阶段不需要压缩,需要source map方便调试,需要热更新提升效率。可以用webpack-merge把公共配置抽出来,开发和生产各写一份差异配置。
开发配置的关键点:mode设为development,devtool设为eval-source-map,CSS用style-loader而不是MiniCssExtractPlugin,配devServer支持热更新和代理。
但纯静态站其实不一定需要devServer,因为页面本身就是静态的,直接开个本地服务器指向src目录就能预览。webpack只在构建产物时跑一次。这种情况下,开发配置可以简化很多,甚至只保留生产配置,构建完用http-server之类的工具预览dist。
7.2 构建脚本的封装
package.json里配好脚本:
{ "scripts": { "build": "webpack --config webpack.config.js", "dev": "webpack serve --config webpack.dev.js" } }构建时跑npm run build,产物输出到dist。如果要做CI/CD,构建命令直接写进流水线配置就行。
有个细节:webpack 5的CLI命令是webpack,不是webpack-cli。装了webpack-cli之后,npx webpack就能跑。如果报"command not found",检查一下webpack-cli有没有装,或者用npx webpack确保用的是本地版本。
7.3 部署时的注意事项
产物部署到服务器,有几个点要注意:
- 服务器要配好MIME类型,尤其是woff2字体,有些老服务器不认这个类型
- 开启gzip压缩,CSS和JS压缩后体积能再减60%以上
- 配好缓存策略,带hash的文件设长期缓存,HTML设不缓存或短缓存
- 如果用了publicPath的子目录,确保服务器路径和publicPath一致
gzip这块特别值得做。webpack产物里的CSS和JS都是文本,gzip压缩率很高。服务器开gzip通常是一行配置的事,但收益巨大。图片和字体已经是压缩格式,gzip对它们效果不大,但也不会有坏处。
8. 这套方案跑下来的一些真实体会
纯静态站引入webpack,最大的阻力不是技术,是心态。很多人觉得"就几个HTML文件,至于上构建工具吗"。但真跑一遍就会发现,构建工具带来的不只是压缩和hash,更重要的是资源关系的显式化。以前CSS里引用图片,路径对不对全靠肉眼检查;现在webpack会在构建时告诉你哪个引用找不到。这种确定性,在项目文件多起来之后价值巨大。
另一个体会是配置的"最小可用"原则。网上很多webpack配置教程一上来就配几十个loader和plugin,看着很全,但大部分用不上。纯静态站真正需要的就那几个:处理HTML、处理CSS、处理图片字体、压缩、加hash。把这几个配好,剩下的按需加。配置越少,出问题时排查范围越小。
最后说个实际踩过的坑:html-loader和HtmlWebpackPlugin同时用时,如果html-loader的版本和配置不对,会出现HTML里的资源被处理了两次,路径变成嵌套的奇怪形式。解决办法是明确分工,html-loader只负责解析资源引用,HtmlWebpackPlugin只负责注入和压缩,不要让两者的职责重叠。这个坑我调了大半天才定位到,写出来给后来人省点时间。