Qwik City 构建后 Godot 游戏 index.html 被删除的根因分析
背景
在 Qwik 博客项目中,我把 Godot Web 导出的游戏资源放在:
public/games/godot/ember-survivors/
Qwik 页面通过 iframe 嵌入游戏:
<iframe src="/games/godot/ember-survivors/index.html" />
本地开发和本地静态资源检查时,游戏资源是存在的;但服务器执行完整构建后,发现:
dist/games/godot/ember-survivors/index.html
消失了,最终导致 iframe 加载失败。
一开始容易误判为服务器部署脚本删文件、Git 没拉到资源、Godot 导出目录不对,或者 public 静态资源没有复制进 dist。但逐步排查后发现,根因不是这些。
现象复现
Qwik 项目的构建脚本是分两段执行的:
{
"build": "pnpm -s sync:articles && NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm -s build.client && pnpm -s build.server",
"build.client": "vite build",
"build.server": "qwik check-client src dist && vite build -c adapters/node-server/vite.config.ts"
}
分别检查两段构建后的文件状态:
rm -rf dist server
pnpm -s build.client
test -f dist/games/godot/ember-survivors/index.html && echo exists || echo missing
pnpm -s build.server
test -f dist/games/godot/ember-survivors/index.html && echo exists || echo missing
结果是:
after client: exists
after server: missing
这说明 vite build 客户端构建阶段会把 public/games/.../index.html 正常复制到 dist,但是 build.server 阶段结束后它被删掉了。
所以问题不在 Git,也不在服务器文件同步,而是在 Qwik City 的 SSR/SSG 构建后处理阶段。
根因
项目使用的是 Qwik City 的 Node adapter:
nodeServerAdapter({
name: 'node-server',
ssg: {
include: ['/', '/posts/*', '/tags', '/tags/*'],
origin: env.VITE_SITE_URL ?? 'https://dropkit.tech',
},
})
这个 adapter 内部会启用类似下面的行为:
cleanStaticGenerated: true
在 build.server 完成 SSR bundle 和 SSG 生成后,Qwik City 会扫描客户端输出目录,也就是 dist,并清理掉不属于 Qwik 静态生成结果的 index.html 和 q-data.json。
清理逻辑的核心可以理解为:
if (fsName === 'index.html' || fsName === 'q-data.json') {
if (!staticPaths.has(pathname) && cleanStatic) {
await fs.promises.unlink(fsPath);
}
return;
}
这套逻辑对普通 Qwik 路由是合理的。Qwik City 需要保证 dist 中的 HTML 页面和它的路由、SSG 结果一致,避免留下旧路由页面或错误的静态页面。
但 Godot Web 导出的入口文件也叫:
index.html
并且路径是:
/games/godot/ember-survivors/index.html
它不是 Qwik City 的路由页面,也不在 SSG 的 staticPaths 中,所以在 build.server 的后处理阶段被当作“非 Qwik 生成的 index.html”清理掉。
这就是根因:Godot Web 导出的入口文件名 index.html 与 Qwik City SSG 的静态 HTML 清理规则冲突。
为什么不是部署脚本的问题
部署脚本里确实有清理:
rm -rf dist server || true
pnpm build
但这只是清理旧构建产物,然后重新执行完整构建。真正删除游戏入口的是 pnpm build 内部的 build.server 阶段,而不是部署脚本额外删了游戏目录。
服务器上也能确认:
public/games/godot/ember-survivors/index.html 存在
dist/games/godot/ember-survivors/index.html 缺失
这说明源码静态资源没有丢,丢的是构建产物中的 HTML 入口。
为什么补拷贝不是好方案
一种临时方案是在 pnpm build 之后把 public/games/**/*.html 再复制回 dist/games。
这种方案能恢复页面,但它是补丁式修复:
- 它依赖构建后额外脚本,容易被部署流程遗漏。
- 它没有改变 Qwik City 清理
index.html的事实。 - 后续如果 Godot 游戏增多,每个
index.html都可能继续踩同一个规则。 - 问题看起来被修复了,但构建模型仍然不清晰。
更合理的做法是让 Godot 的独立 HTML 入口不要叫 index.html。
最终修复
把 Godot Web 导出的入口改名为:
game.html
目录结构变为:
public/games/godot/ember-survivors/game.html
public/games/godot/ember-survivors/index.js
public/games/godot/ember-survivors/index.wasm
public/games/godot/ember-survivors/index.pck
iframe 改为:
<iframe src="/games/godot/ember-survivors/game.html" />
Godot 的 export_presets.cfg 也同步改为:
export_path="../../myProject/qwik-blog-serve/public/games/godot/ember-survivors/game.html"
这样 game.html 仍然会在 build.client 阶段从 public 复制到 dist,但不会命中 Qwik City 对 index.html 的特殊清理逻辑。
验证结果:
after client game: exists
after server game: exists
after server index: missing
这个结果很关键:index.html 仍然会被清理,说明根因判断成立;而 game.html 能保留下来,说明新方案避开了 Qwik City 的清理规则。
部署脚本里的校验
部署脚本不再做补拷贝,只保留构建后的强校验:
test -f dist/games/godot/ember-survivors/game.html || (
echo 'game entry missing after build: dist/games/godot/ember-survivors/game.html'
exit 2
)
这样部署逻辑保持干净:
- 清理旧产物。
- 执行标准构建。
- 校验关键游戏入口存在。
- 启动 PM2。
如果未来构建规则变化,部署会直接失败,而不是静默上线一个无法加载游戏的版本。
总结
这次问题的关键不是服务器环境差异,而是 Qwik City Node adapter 的 SSG 后处理会清理非路由生成的 index.html。
Godot Web 默认导出的入口也叫 index.html,当它被放进 Qwik 的 public 并参与完整构建后,会在 build.client 阶段出现,又在 build.server 阶段被 Qwik City 清掉。
最终解决思路是:不要让嵌入式独立应用的 HTML 入口使用 Qwik City 会特殊处理的 index.html 文件名。
对这类资源,推荐使用更明确的入口名:
game.html
app.html
player.html
embed.html
这样可以避免和 Qwik 路由产物发生语义冲突,也能让部署脚本只做校验,不承担构建产物修补职责。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录