广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Webpack 中 devServer 如何配置静态文件服务目录?

Webpack 的 devServer 在开发环境承担着本地 HTTP 服务器的角色,除了提供打包后的模块资源,它还能直接把磁盘上的某个目录作为静态文件服务暴露给浏览器。有一部分开发者对这部分配置的理解停留在较早版本的 contentBase 选项上,但 webpack-dev-server 从 4.x 开始改用 static 配置项,写法和行为都有明显变化。本文把当前主流的 static 目录配置方式拆开讲清楚,包括单目录、多目录、路径前缀以及排查 404 的常见思路。

Webpack 中 devServer 如何配置静态文件服务目录?

一、静态文件服务与打包产物的关系

在动手配置之前,需要先分清 devServer 提供的两类资源。第一类是 webpack 根据入口文件构建出的 bundle、chunk 和 source map,这些产物并不写入磁盘,而是由 devServer 配合 devMiddleware 直接保存到内存中,请求到达时通过 publicPath 匹配后返回。第二类就是项目里不需要经过构建、也不应该被 webpack 处理的静态文件,例如 favicon.ico、robots.txt、某些字体文件或模拟接口用的 JSON 数据。这类文件通常放在一个独立的 public 目录或者 assets 静态目录里。

devServer 的 static 配置就是为第二类文件服务的。它会把指定目录映射到 HTTP 服务器的根路径或者某个前缀路径下。当浏览器发起请求时,devServer 会先检查内存中的打包产物能否响应这个请求;如果无法匹配,再回到磁盘上的 static 目录中查找对应的文件路径。这个优先级意味着,如果 static 目录下放了一个和打包产物相同路径的文件,实际返回的仍然是内存中的打包产物。因此,static 目录更适合存放那些永远不会与 webpack 输出冲突的资源。

历史上 webpack-dev-server 3.x 使用 contentBase 来指定静态目录,4.x 开始推荐使用 static 并保留 contentBase 作为兼容选项。新的 static 配置支持字符串、对象和数组三种形式,语义比 contentBase 更明确,也能为多个目录分别设置公共前缀。如果你的项目仍在使用 contentBase,建议尽早迁移到 static,一方面避免未来版本移除兼容时的调整成本,另一方面也方便在同一个配置里维护更复杂的目录结构。

二、单目录与多目录配置实例

最基础的用法是把 static 设置为一个对象,并通过 directory 指定磁盘上的绝对路径。假设项目结构如下:根目录下有一个 public 文件夹,里面放着 favicon.ico 和 data 子目录。要让 devServer 把这个目录暴露到服务根路径,可以这样写:

const path = require('path');
module.exports = {
  // 其他配置省略
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
  },
};

上面的配置完成后,启动 devServer,访问 http://localhost:8080/favicon.ico 就会返回 public 目录下的对应文件。这里有两个细节需要留意。第一,directory 必须使用绝对路径,因为 devServer 的工作目录可能和 webpack 配置文件的目录不一致,相对路径容易出现解析偏差。第二,path.join 会自动处理不同操作系统下的路径分隔符,比手写字符串拼接更可靠。

如果项目里同时存在多个静态资源目录,比如 public 用于通用静态资源,docs 用于存放文档预览文件,static 可以写成数组。数组的每一项既可以是一个字符串路径,也可以是一个包含 directory 和 publicPath 的对象。下面的示例为两个目录分配了不同的访问前缀:

const path = require('path');
module.exports = {
  devServer: {
    static: [
      {
        directory: path.join(__dirname, 'public'),
        publicPath: '/assets',
      },
      {
        directory: path.join(__dirname, 'docs'),
        publicPath: '/docs',
        watch: true,
      },
    ],
  },
};

这样一来,/assets/favicon.ico 会映射到 public/favicon.ico,而 /docs/readme.md 会映射到 docs/readme.md。publicPath 相当于给静态文件访问路径加了一层前缀,不会影响磁盘上的真实目录结构。watch 选项设为 true 时,devServer 会监听对应目录的变化,文件新增、删除或修改后无需重启服务就能看到最新内容。对于单个目录,也可以直接在 static 对象里设置 watch,或者在 devServer 顶层设置 watchFiles 来监听更细粒度的文件。

此外,static 还可以直接传入一个字符串路径,这时相当于只配置 directory,其他选项使用默认值。例如 static: path.join(__dirname, 'public') 与前面第一个对象写法效果一致。不过当需要指定 publicPath 或 watch 时,必须使用对象或数组形式。

三、路径匹配规则与 404 排查

理解了基本配置之后,实际开发中经常遇到的一个问题是:配置文件写好了,但访问路径仍然返回 404。排除这类问题首先要明确 devServer 的响应顺序。请求进来后,devServer 会先交给 devMiddleware 处理,devMiddleware 根据 webpack 的 output.publicPath 判断请求是否属于打包产物;如果是,则从内存中返回对应的模块,如果不是,则继续交给 static 配置的静态目录中间件。接着,static 中间件会按照配置的 publicPath 前缀匹配请求,再去对应磁盘目录查找文件。只有两个环节都没有命中,才会触发 historyApiFallback 或者最终返回 404。

常见的一个误区是把 output.publicPath 和 static.publicPath 混为一谈。前者决定打包产物在浏览器中的访问前缀,后者决定静态文件访问前缀,两者可以相同也可以不同。如果 output.publicPath 设置为 /dist/,而 static.publicPath 设置为 /static/,那么 /static/app.css 会查找静态目录,而 /dist/main.js 会从内存中返回 bundle。实际项目里,建议把两者设置成不同的前缀,避免请求路径重叠造成难以追踪的优先级问题。

另一个高频问题是相对路径导致目录不可用。假设 webpack 配置文件位于 config/webpack.dev.js,如果 directory 写的是 public 而不是绝对路径,devServer 可能会基于启动命令的当前工作目录解析,最终指向错误的位置。解决办法是统一使用 path.join(__dirname, 'public') 或 path.resolve(__dirname, 'public') 生成绝对路径。如果确实想用相对路径,也需要写清楚相对于哪个基准目录,但通常不建议这样做。

还有一类情况是静态文件存在,但访问时被 historyApiFallback 拦截并返回了 index.html。比如单页应用配置了 historyApiFallback: true,此时访问一个不存在的静态路径会回退到 index.html,浏览器拿到 HTML 后由前端路由处理。如果希望某些路径不走 history 回退,而是真实地返回 404 页面或静态文件,可以配置 rewrites 规则,把特定前缀排除在回退范围之外。下面是一个示例:

const path = require('path');
module.exports = {
  output: {
    publicPath: '/dist/',
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
      publicPath: '/static',
    },
    devMiddleware: {
      publicPath: '/dist/',
    },
    historyApiFallback: {
      rewrites: [{ from: /^\/static\//, to: '/static/404.html' }],
    },
  },
};

这个配置将 /static/ 开头的请求从 history 回退中排除,如果静态目录下没有对应文件,devServer 会尝试返回 /static/404.html,而不是交给前端路由处理。实际排查时,可以打开浏览器的 Network 面板,观察请求的响应状态码和响应体,同时结合 devServer 启动时输出的静态目录信息,确认目录路径是否被正确加载。

广告位 · 文章底部(后台-模块管理替换此区域)