首页
/ create-react-app 的 public 文件夹深度解析:PUBLIC_URL 静态资源逃生通道的用法与实现机制

create-react-app 的 public 文件夹深度解析:PUBLIC_URL 静态资源逃生通道的用法与实现机制

2026-09-04 12:35:22作者:仰钰奇

public 文件夹是 Create React App(CRA)项目中最容易“用错”的目录:它既是唯一可以直接修改的 index.html 的所在地,也是绕过 webpack 模块系统投递静态资源的“逃生通道(escape hatch)”。本篇以官方文档 Using the Public Folder 为主体,逐条还原其中的规则与示例,并结合 react-scripts 源码,讲清楚 %PUBLIC_URL%process.env.PUBLIC_URL 在构建时究竟是如何被计算和替换的,以及开发服务器与生产构建分别如何处理 public 目录下的文件。读完本文,你将能够正确决定哪些文件该放 public、哪些文件该改用 import,并理解 CRA 在任意部署路径(非根 URL、客户端路由)下资源地址依然正确的底层原因。

public 文件夹的角色:可改的 HTML 与不经过 webpack 的静态文件

官方文档(using-the-public-folder.md)指出,该功能自 react-scripts@0.5.0 起可用。public 文件夹承担两类职责:

  1. 存放可自定义的 HTML 文件。你可以直接编辑 public/index.html,例如设置页面标题和 meta 标签(参见 title-and-meta-tags.md)。文档强调:编译产物对应的 <script> 标签会在构建过程中自动注入到 HTML 中,你不需要也不应该手动写。
  2. 存放不走模块系统的其他静态资源。把文件放进 public,它不会被 webpack 处理,而是原样复制进 build 文件夹。要引用这些资源,必须使用 PUBLIC_URL 这个环境变量。

从源码可以印证“原样复制”这一行为:生产构建脚本在 build.js 中直接调用 fs.copySync(paths.appPublic, paths.appBuild, {...})public 整体拷入 build;而 public/index.html 本身则作为 webpack 的模板参与构建,见 webpack.config.jstemplate: paths.appHtml 的配置,其中 appHtmlpaths.js 中被解析为 resolveApp('public/index.html')。也就是说,public 目录在 CRA 中是“双通道”的:index.html 走 HtmlWebpackPlugin 模板通道(因此会被注入 script 标签、被变量插值、在 production 下被压缩),而目录里的其他文件走文件系统直接拷贝通道(因此不会被处理、不会压缩、文件名不带内容哈希)。

为什么不推荐把资源放 public:import 通道的三大好处

文档明确指出,通常情况下推荐在 JavaScript 中 import 资源(参见 添加样式表添加图片和字体),因为模块系统提供以下好处:

  • 脚本和样式表会被压缩、打包到一起,避免额外的网络请求;
  • 文件缺失会在编译期报错,而不是让用户遭遇 404;
  • 产物文件名包含内容哈希(content hash),无需担心浏览器缓存旧版本。

而放进 public 的资源则恰好是这三点的反面,文档列出了使用这种逃生通道必须接受的代价:

  • public 里的文件不会被后处理或压缩;
  • 文件缺失在编译期不会被发现,用户访问时会直接 404;
  • 产物文件名不包含内容哈希,每次文件变更后你需要自行加查询参数或改文件名来破除缓存。

因此 public 的定位是 workaround,而不是默认路径。

在 index.html 中使用 %PUBLIC_URL%

index.html 中,public 下的资源通过 %PUBLIC_URL% 前缀引用,官方示例:

<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />

有两条硬性规则需要注意:

  • 只有 public 文件夹内的文件才能通过 %PUBLIC_URL% 前缀访问。如果你想引用 srcnode_modules 里的文件,必须先把它复制到 public——文档把这视为一种“显式声明该文件属于构建产物”的意图表达;
  • 运行 npm run build 时,CRA 会把 %PUBLIC_URL% 替换为正确的绝对路径,这样即使项目使用了客户端路由(client-side routing)或部署在非根 URL 下,资源引用依然有效。

%PUBLIC_URL% 的替换发生在构建期,由 InterpolateHtmlPlugin 完成。这个插件挂在 HtmlWebpackPlugin 的 afterTemplateExecution 钩子上,对 HTML 做全局正则字符串替换:

data.html = data.html.replace(
  new RegExp('%' + escapeStringRegexp(key) + '%', 'g'),
  value
);

它在 webpack.config.js 中以 new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw) 的形式注册。也就是说,凡是 env.raw 中的键(NODE_ENVPUBLIC_URLWDS_SOCKET_*FAST_REFRESH 以及所有 REACT_APP_* 变量)都可以通过 %键名% 的形式写进 index.html%PUBLIC_URL% 只是其中最常用的一个。

在 JavaScript 中使用 process.env.PUBLIC_URL

在 JS 代码里,等价能力是 process.env.PUBLIC_URL,官方示例:

render() {
  // Note: this is an escape hatch and should be used sparingly!
  // Normally we recommend using `import` for getting asset URLs
  // as described in “Adding Images and Fonts” above this section.
  return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />;
}

文档特意标注这应当“sparingly”(节制地)使用。其注入机制在 env.jsgetClientEnvironment(publicUrl) 函数中:PUBLIC_URL: publicUrl 被放入环境对象,随后连同 NODE_ENV 和所有 REACT_APP_* 变量一起被 JSON.stringify,最终通过 webpack 的 DefinePluginnew webpack.DefinePlugin(env.stringified))在编译期做文本替换,把 process.env.PUBLIC_URL 换成具体的字符串字面量。因此它是构建时注入而非运行时读取——修改部署路径后必须重新构建才能生效。

PUBLIC_URL 的取值来源:从源码看计算优先级

%PUBLIC_URL%process.env.PUBLIC_URL 拿到的是同一个值,其计算逻辑集中在 paths.js

const publicUrlOrPath = getPublicUrlOrPath(
  process.env.NODE_ENV === 'development',
  require(resolveApp('package.json')).homepage,
  process.env.PUBLIC_URL
);

核心解析函数是 getPublicUrlOrPath.js,取值优先级为:

  1. 环境变量 PUBLIC_URL.env 文件或 shell 中设置):若以 . 开头(相对路径,如 .),development 下会规范为 /,production 下则按原样保留以启用相对资源路径(服务于不使用 pushState 客户端路由的应用);若是带域名的完整 URL,development 下取 pathname,production 下原样使用;
  2. package.jsonhomepage 字段:规则类似,取 URL 的 pathname 部分;
  3. 默认值 /:前两者都未设置时,public URL 就是根路径。

另外注意 webpack.config.js 中有一处细节:传给 getClientEnvironment 的值是 paths.publicUrlOrPath.slice(0, -1),即刻意去掉了末尾斜杠(源码注释解释:%PUBLIC_URL%/xyz%PUBLIC_URL%xyz 更好看),所以在 HTML 里书写时仍需手动补上 /

开发服务器的行为与生产构建一致地“以 public 为静态根”:webpackDevServer.config.js 配置了 static: { directory: paths.appPublic },源码注释也明确写道——“在 index.html 中,你可以用 %PUBLIC_URL% 获取 public 文件夹的 URL;在 JavaScript 代码中,你可以用 process.env.PUBLIC_URL 访问它”。因此开发期与构建期的行为是统一的,资源引用无需区分环境。

什么时候该用 public 文件夹

文档给出的推荐立场是:样式表、图片、字体仍应通过 JavaScript import 引入public 文件夹适用于以下较少见的情形:

  • 你需要在构建产物中得到指定文件名的文件,例如 PWA 的 manifest.webmanifest
  • 你有成千上万张图片,需要动态拼接路径来引用;
  • 你想在打包代码之外引入一个类似 pace.js小型独立脚本
  • 某些库与 webpack 不兼容,你只能以 <script> 标签方式引入。

文档同时提醒:如果你在 index.html 中加入了声明全局变量<script>,需要继续了解如何安全地引用这些变量,参见 Using Global Variables

小结:一张决策速查表

场景 正确做法
修改页面标题、meta 标签 编辑 public/index.html(参见 title-and-meta-tags.md
引入样式表、图片、字体 在 JS 中 import,让 webpack 处理(参见 adding-a-stylesheet.mdadding-images-fonts-and-files.md
需要构建产物中的固定文件名(如 manifest) 放入 public,用 %PUBLIC_URL% / process.env.PUBLIC_URL 引用
需要动态路径引用大量图片、引入 webpack 不兼容的脚本 放入 public
调整部署根路径 设置 PUBLIC_URL 环境变量或 homepage 字段,重新构建

一句话总结:public 是“HTML 可编辑 + 资源直通构建目录”的逃生通道,%PUBLIC_URL% 由 InterpolateHtmlPlugin 在构建期替换、process.env.PUBLIC_URL 由 DefinePlugin 注入,两者同源于 getPublicUrlOrPathPUBLIC_URL 环境变量、homepage 字段和默认值 / 的优先级解析——理解这条链路,就能在任意部署路径下正确地组织 CRA 的静态资源。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384