Symfony 7 asset mapper 编译后为何部分资源仍使用旧版本哈希?
结论
页面生成资源 URL 时使用的数据,与上传的 importmap.json 不一致。仅凭这两段内容,还无法确定问题出在哪份文件或哪层缓存,也不能认定 AssetMapper 只更新了部分文件的哈希。
先检查完整的编译产物,尤其是 manifest.json、入口元数据,以及生产应用实际读取的目录,再排查模板中写死的 URL 和 HTML 缓存。
如果这两个文件的引用方式与其他文件不同,或者旧页面引用的资源中只有这两个在本次构建里发生了变化,就可能出现只有它们异常、其他文件都正常的情况。
为什么 importmap.json 正确,页面仍可能引用旧哈希?
asset-map:compile 会生成带哈希的资源文件,并在 public/assets/ 下生成 manifest.json、importmap.json 等 JSON 元数据。只核对其中一份文件,无法确认整套部署结果都已更新。Symfony 官方部署说明
页面上的链接可能来自不同位置:
{{ asset('styles/app.css') }}根据逻辑路径解析资源 URL。使用编译产物时,需要检查manifest.json中的映射。{{ importmap('app') }}可以生成 import map、模块预加载链接,以及入口导入的 CSS 链接。因此,题目中的<link rel="stylesheet">不一定来自模板对asset()的直接调用。Symfony 官方入口说明、CSS 处理说明- 如果模板或自定义代码直接写入了带哈希的文件名,编译命令不会替它更新。
- 如果返回的是缓存中的旧 HTML,即使服务器上的资源已全部更新,HTML 中仍会保留旧 URL。
cache:clear 也不会重新编译或同步 public/assets/,或替换模板中写死的文件名。
第一步:在完整编译产物中搜索新旧哈希
在构建目录和生产环境的对应目录分别执行:
rg -n 'suql3DM|UnCEWWx|V_DTn4B|m-YYNT0' public/assets --glob '*.json'
在生产服务器上,将路径替换为应用实际使用的目录。重点检查:
public/assets/manifest.json
public/assets/importmap.json
public/assets/entrypoint.app.json
如果有多个入口,还要检查对应的 entrypoint.*.json。入口元数据的具体结构以项目安装的 symfony/asset-mapper 版本为准,不要假定每份入口文件都直接保存了哈希路径。
如果安装了 jq,可以单独查看清单映射:
jq '{
css: .["styles/app.css"],
controller: .["controllers/modal_controller.js"]
}' public/assets/manifest.json
默认映射下,这两个键是相对于 assets/ 的逻辑路径。如果配置了命名空间,应使用项目实际的逻辑路径。
根据检查结果继续排查:
importmap.json是新哈希,manifest.json仍是旧哈希:先解决编译产物混用或上传不完整的问题。- 构建目录正确,生产目录不一致:检查上传工具是否跳过覆盖、上传是否中断,或是否传到了错误目录。
- 所有 JSON 都已更新:继续检查页面引用来源和应用读取路径。
目录里留有旧哈希资源文件,本身不会让 Symfony 自动选择旧版本。需要查的是哪份映射或哪段代码仍在引用它们。
第二步:查清两个链接的生成位置
在项目根目录搜索旧哈希和相关文件名:
rg -n 'suql3DM|UnCEWWx|styles/app\.css|modal_controller' templates assets config src importmap.php
如果模板中写了这样的链接:
<link rel="stylesheet" href="/order-app/assets/styles/app-suql3DM.css">
应改用逻辑路径:
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
如果 CSS 已经由入口导入,通常保留入口加载即可:
// assets/app.js
import './styles/app.css';
{% block javascripts %}
{{ importmap('app') }}
{% endblock %}
不要为了修复哈希而重复添加同一份样式链接。如果 modal_controller.js 的预加载由 importmap() 生成,也应检查入口依赖和编译元数据,不要手工把旧哈希替换成新哈希。
同时检查实际 HTML 中的 <script type="importmap">。如果其中的模块 URL 与 modulepreload 链接也不一致,可以据此进一步排查手工链接、入口元数据或自定义渲染逻辑。
第三步:确认 PHP 和 Web 服务器使用同一套文件
题目中的上传位置是:
/public_html/order-app/assets
需要确认这个目录确实对应当前 Symfony 应用的 public/assets/。
有些部署会把公开目录单独复制到 public_html,而 PHP 应用保留在另一个目录。即使 Web 服务器能访问新 JS、CSS,Symfony 渲染页面时读取的 JSON 也可能仍是旧的。
检查以下内容:
- 当前域名或子路径实际执行的是哪个
public/index.php。 - Symfony 配置的公开目录、编译输出目录是否与上传位置一致。
- 发布目录或符号链接是否仍指向旧版本。
- 如果有多个实例,各实例是否都部署了同一套代码和资源元数据。
/order-app 是部署子路径前缀,与文件名中的哈希需要分别排查。当前差异集中在哈希部分,应先查映射来源。出现前缀重复、缺失或请求路径错误时,再检查子路径配置。
第四步:明确清理的是哪种缓存
清理生产环境的 Symfony 缓存时,应明确指定环境,并在当前发布版本的项目目录执行:
php bin/console cache:clear --env=prod --no-debug
如果之前只运行了:
php bin/console cache:clear
是否清理了生产缓存,取决于命令实际使用的 APP_ENV,不能只看命令是否执行成功。
接着查看浏览器 Network 面板中主文档请求的响应正文,确认服务器返回的 HTML 是否已经包含旧 URL:
- 如果响应正文中已有旧哈希,继续检查服务端渲染、整页缓存、反向代理或 CDN。
- 如果响应正文中是新哈希,但浏览器仍请求旧 URL,再检查前端动态插入、Service Worker 或其他旧页面来源。
也可以发送要求重新验证缓存的请求,辅助排查:
curl -sS -H 'Cache-Control: no-cache' \
'https://example.com/order-app/' \
| rg 'app-|modal_controller-'
将地址替换为实际页面地址。Cache-Control: no-cache 不能保证绕过所有代理缓存;必要时,应按部署方式直接请求源站,并检查缓存响应头。
确认 PHP 代码或模板更新未生效后,再按服务器配置处理 OPcache 或长期运行的应用进程。旧哈希问题不能一概归因于 OPcache。
如何修复并避免再次发生?
如果确认部署产物不一致,应在新的发布目录中重新构建,再完整发布这次构建的结果。例如:
# 在源码和依赖已准备好的新发布目录中执行
php bin/console importmap:install --env=prod --no-debug
php bin/console cache:clear --env=prod --no-debug
php bin/console asset-map:compile --env=prod --no-debug
importmap:install 用于准备 import map 所需的第三方资源,并非专门刷新哈希的命令。
发布时,带哈希的文件和全部 JSON 元数据应来自同一次构建,并一起发布。尽量先准备好完整的新版本,再切换线上流量,避免逐个覆盖时短暂混用新旧清单和资源。如果只能上传静态资源,也要同步更新 PHP 实际读取的编译元数据。
不要直接清空正在提供服务的生产资源目录。可以在独立构建目录中使用干净的输出目录,并根据缓存页面的有效期,将旧资源保留一段时间。
修复后,应同时满足以下条件:
- 应用实际读取的 JSON 元数据与本次构建一致。
- 新请求返回的 HTML 使用对应的新哈希。
- HTML 引用的完整 URL 能访问到本次发布的资源。
备注:内容仅供参考。
