create-react-app 的 public 文件夹深度解析:PUBLIC_URL 静态资源逃生通道的用法与实现机制
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 文件夹承担两类职责:
- 存放可自定义的 HTML 文件。你可以直接编辑
public/index.html,例如设置页面标题和 meta 标签(参见 title-and-meta-tags.md)。文档强调:编译产物对应的<script>标签会在构建过程中自动注入到 HTML 中,你不需要也不应该手动写。 - 存放不走模块系统的其他静态资源。把文件放进
public,它不会被 webpack 处理,而是原样复制进build文件夹。要引用这些资源,必须使用PUBLIC_URL这个环境变量。
从源码可以印证“原样复制”这一行为:生产构建脚本在 build.js 中直接调用 fs.copySync(paths.appPublic, paths.appBuild, {...}) 把 public 整体拷入 build;而 public/index.html 本身则作为 webpack 的模板参与构建,见 webpack.config.js 中 template: paths.appHtml 的配置,其中 appHtml 在 paths.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%前缀访问。如果你想引用src或node_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_ENV、PUBLIC_URL、WDS_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.js 的 getClientEnvironment(publicUrl) 函数中:PUBLIC_URL: publicUrl 被放入环境对象,随后连同 NODE_ENV 和所有 REACT_APP_* 变量一起被 JSON.stringify,最终通过 webpack 的 DefinePlugin(new 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,取值优先级为:
- 环境变量
PUBLIC_URL(.env文件或 shell 中设置):若以.开头(相对路径,如.),development 下会规范为/,production 下则按原样保留以启用相对资源路径(服务于不使用 pushState 客户端路由的应用);若是带域名的完整 URL,development 下取pathname,production 下原样使用; package.json的homepage字段:规则类似,取 URL 的pathname部分;- 默认值
/:前两者都未设置时,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.md、adding-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 注入,两者同源于 getPublicUrlOrPath 对 PUBLIC_URL 环境变量、homepage 字段和默认值 / 的优先级解析——理解这条链路,就能在任意部署路径下正确地组织 CRA 的静态资源。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00