Qwik City 构建后 Godot 游戏 index.html 被删除的根因分析

2026-06-20
16086 分钟
...

背景

在 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.htmlq-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
)

这样部署逻辑保持干净:

  1. 清理旧产物。
  2. 执行标准构建。
  3. 校验关键游戏入口存在。
  4. 启动 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 路由产物发生语义冲突,也能让部署脚本只做校验,不承担构建产物修补职责。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
qwik2026-06-24
Qwik 中本地图片为什么推荐用 ESM 导入
在 Qwik 项目里,如果我们直接这样写本地图片: 代码本身是可以运行的,但 ESLint 可能会提示 。这个提示不是错误,而是 Qwik 在提醒我们: 本地图片可以通过 ESM 导入的方式进行优化 。 问题来源 Qwik 推荐把本地图片放到 目录下,然后通过 导入,并在路径后面加上 。 例如: 这样导入之后,图片会变成一个可以直接使用的组件。 为什么要这样做 直接写 时,浏览器只会加载这一张原图。图片多大,用户就下载多大。 而使用 Q...
学习
qwik2026-02-28
理解 Qwik 的 routeLoader$:执行时机与 SSR/SSG/CSR 全景解析
🔑 一句话定义 是 Qwik City 专为“路由级数据加载”设计的声明式 API 。 它将数据获取逻辑与组件解耦,通过 序列化 + 状态恢复 实现“零 hydration”体验——这正是 Qwik “可恢复性”(Resumability)架构的灵魂所在。 📊 执行时机全景表(建议收藏!) | 场景 | 执行位置 | 触发时机 | 数据来源 | 客户端是否重执行? | | | | | | | | SSR | 服务端 | 用户请求页面...
学习
qwik2026-02-24
Qwik 技术深度回顾:从入门到实战
Qwik 是一个以 Resumability(可恢复性) 为核心的现代前端框架,它的目标是实现 O(1) 的 JavaScript 加载量,即无论应用多大,首屏加载的 JS 量都几乎为零。 本文将带你回顾项目中实际使用到的 Qwik 核心技术,帮助你快速重拾对 Qwik 的记忆。 1\. 核心概念: 后缀与懒加载 在 Qwik 中,你会发现大量的 API 以 结尾(如 , , )。 含义 : 标志着代码的 序列化边界 。编译器会将 包裹...
学习
qwik2026-02-24
Qwik博客性能优化实战
1\. 回归原生:用标准能力替代冗余脚本 曾几何时,为实现图片懒加载与资源预取,开发者常需手写复杂的 Intersection Observer 逻辑。但随着浏览器能力的演进,这类“黑科技”反而可能成为性能负担。 问题所在 : 自定义懒加载脚本不仅增加首屏 JS 体积,还会频繁触发 DOM 监听与计算,占用主线程资源,推高 Total Blocking Time(TBT)。 优化方案 : 图片加载 :直接采用 。浏览器内核级实现更高效、...
学习
qwik2026-01-15
Qwik 服务端能力深度解析
1\. Qwik 服务端架构概览 Qwik 采用独特的 可恢复性 (resumability)架构,使其服务端渲染(SSR)能力与传统框架有本质区别。在 Qwik 中,服务端不仅负责初始 HTML 生成,还负责: 组件序列化与反序列化 事件监听器的注册与恢复 数据流的管理(从服务端到客户端) 优化资源加载策略 Qwik 的服务端处理是 细粒度 的,允许开发者精确控制哪些代码在服务端执行,哪些在客户端执行,同时保持无缝协作。 2\. Qw...
学习
qwik2026-01-09
qwik api介绍
| 类别 | 名称 | 功能描述 | 适用场景 | | | | | | | 生命周期 | onMount | 组件挂载时执行 | 初始化DOM操作、设置事件监听 | | | onUnmount | 组件卸载时执行 | 清理资源、移除事件监听 | | | onVisible | 组件在视口可见时执行 | 懒加载内容、分析追踪 | | | onResume | 从序列化状态恢复时执行 | 恢复应用状态 | | 核心API | compone...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录