devServer.static.directory 是 Webpack 开发服务器中一个容易被忽视但非常关键的配置项。它决定了当你在浏览器中直接请求一个静态文件(比如图片、字体、某个 HTML 页面)时,devServer 会去磁盘的哪个目录下查找这个文件。很多人在配置完之后发现资源一直返回 404,或者明明文件存在却访问不到,十有八九问题就出在这个配置上。本文将从基本用法入手,逐步深入到多目录配置、优先级关系以及常见报错的排查方法。

一、devServer.static.directory 的基本作用
在 Webpack 4 及更早的版本中,这个功能通过 contentBase 来配置,到了 webpack-dev-server 4.x 之后统一迁移到了 static 配置对象下,写法变成了 static.directory。如果你项目里还在使用 contentBase,升级 dev-server 版本时会收到废弃警告,甚至直接报错。
这个配置的核心作用只有一个:告诉 devServer,当请求的 URL 没有命中编译产物、也没有命中代理规则时,应该去磁盘上的哪个目录尝试读取文件。最典型的例子就是你的 index.html 放在项目根目录而不是打包输出目录里,这时候就需要配置它。
下面是一个基础的配置示例:
// webpack.config.js
const path = require('path');
module.exports = {
// ...其他配置
devServer: {
static: {
directory: path.join(__dirname, 'public'), // 静态文件根目录
publicPath: '/', // 浏览器访问静态文件时的 URL 前缀
watch: true, // 监听文件变化,改动后自动刷新页面
},
port: 8080,
open: true,
},
};在这个例子中,所有放在 public 目录下的文件都可以通过根路径直接访问。比如 public/logo.png 对应的访问地址就是 http://localhost:8080/logo.png。
需要特别说明的一点是,directory 的值必须是绝对路径或者相对于配置文件所在目录会被正确解析的路径。如果直接写一个相对路径字符串,不同版本的 dev-server 解析行为可能不一致,最稳妥的写法就是像上面那样用 path.join(__dirname, '目录名')。
二、多目录配置与访问优先级
实际项目中,静态资源可能分散在多个目录,比如一部分在 public,另一部分在 assets。webpack-dev-server 支持把 static 配置成数组形式,同时指定多个目录:
module.exports = {
devServer: {
static: [
{
directory: path.join(__dirname, 'public'),
publicPath: '/',
},
{
directory: path.join(__dirname, 'docs'),
publicPath: '/docs/', // docs 目录下的文件挂载到 /docs/ 路径下
},
],
},
};这种写法的好处是可以把不同来源的资源挂载到不同的 URL 前缀下,结构更清晰。例如 docs/readme.md 的访问地址就是 http://localhost:8080/docs/readme.md。
当多个配置项的 publicPath 发生重叠时,比如两个目录都挂载在根路径下,请求会按照数组顺序依次查找,先匹配到的目录优先返回。所以如果两个目录下存在同名文件,数组中排在前面的目录会胜出。
另外还有一个容易混淆的问题:static.publicPath 和外层的 output.publicPath 是两回事。前者控制静态文件目录在浏览器中的访问前缀,后者控制 Webpack 打包产物(JS、CSS 等 bundle)的引用前缀。两者可以完全不同,如果你发现 bundle 加载正常但某个图片 404,大概率是这两个前缀没有区分清楚导致的。
三、常见问题排查与进阶技巧
配置了 static.directory 之后还是 404,通常有下面几种原因。第一,路径拼接错误,比如把目录写成了 path.join(__dirname, '../public'),但实际文件在别处,可以用 path.resolve 配合打印确认。第二,请求被 devServer 的代理规则拦截了,代理配置 proxy 的优先级高于静态文件服务,如果代理规则的匹配范围太宽(比如直接代理了所有路径),静态文件请求根本走不到 static 这一层。第三,使用了 HTML5 History 路由但没有开启 historyApiFallback,导致刷新页面时请求被当成静态文件路径去查找。
排查时可以开启日志观察请求去向:
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
// 打印请求日志,方便确认请求是否到达静态文件服务
onListening: function (server) {
const port = server.server.address().port;
console.log('Listening on port:', port);
},
},
};还有一个进阶用法是通过 static.serveIndex 控制是否显示目录索引页,通过 static.watch(对应旧版的 watchContentBase)控制文件变更是否触发页面刷新。如果你的静态资源是编译生成的,watch 监听可能会触发不必要的重复刷新,此时可以把它设为 false,改由 Webpack 自身的 watch 机制处理。
最后提醒一点:如果项目是通过 devServer.static: false 完全禁用了静态文件服务(比如纯前后端分离、页面由后端模板渲染的场景),那么所有请求都会走代理或编译产物,这时候就不会再有静态目录的概念,遇到资源 404 需要从代理配置入手排查,而不是 static.directory。
总结
devServer.static.directory 的本质是为开发服务器指定一个磁盘上的静态资源查找根目录,配合 publicPath 决定访问前缀,配合多目录数组实现分路径挂载。配置时的关键点是:使用绝对路径、区分好 static.publicPath 与 output.publicPath、注意 proxy 的优先级高于静态文件服务。掌握这三点,静态资源 404 的问题基本都能快速定位解决。