原生离线页面(WebViewGold)
当应用无法加载您的 URL 时显示。在 iOS 上,对话框和页面文字取自 offlinetitle、offlinemsg、screen1、screen2 和 offlinebuttontext,而 offlinescreenautoretry 会每秒自动重试一次,无需点按。在 Android 上,OFFLINE_SCREEN_BACKGROUND_COLOR 可以让它与您的品牌配色一致。不涉及任何 Web 代码。
ChatGPT & Codex
有些审核人员会打开飞行模式,而每位审核人员迟早都会遇到一个很慢的请求。封装的 Web 应用有两层离线处理:WebViewGold 负责页面根本无法加载的那一刻,而您的 Web 应用必须处理页面加载之后每一个失败的请求。Codex 非常适合第二层,因为它能列出代码库中的每一个数据调用,并用切断网络的测试验证每一种错误状态。
每个平台一次性 $109 起,或由我们的团队全程代劳,$698 起。
离线问题最终都会落到准则 2.1(应用完整性)上:永远转个不停的加载图标、请求失败后的白屏、卡片上原样显示的“TypeError: Failed to fetch”、一个会丢掉您输入的所有内容的表单(准则 2.1 拒审详解)。Google Play 则把同样的问题归为功能故障。这些问题都不需要飞行模式就会发生:闲置后会休眠的免费托管方案、冷启动的 serverless 函数,或者酒店里信号很差的网络,都会导致同样的结果。
目标是:应用永远不会显示空白或卡死的页面。在没有网络的情况下启动时,会出现一个带重试按钮的原生离线页面。在使用过程中,每个页面要么显示一个会结束的加载状态,要么显示一个带“重试”按钮的错误状态,要么显示缓存数据并提示内容可能不是最新的。
离线模式专题对比了各种原生方案,ChatGPT 和 Codex 应用的离线模式介绍了 Codex 项目的设置方法。
当应用无法加载您的 URL 时显示。在 iOS 上,对话框和页面文字取自 offlinetitle、offlinemsg、screen1、screen2 和 offlinebuttontext,而 offlinescreenautoretry 会每秒自动重试一次,无需点按。在 Android 上,OFFLINE_SCREEN_BACKGROUND_COLOR 可以让它与您的品牌配色一致。不涉及任何 Web 代码。
每个请求都有超时、带重试的错误状态和一条通俗易懂的提示信息。这是审核人员最常碰到的一层,也是本指南要自动化的一层。
开启 offlinelocalhtmlswitch(iOS)或 FALLBACK_USE_LOCAL_HTML_FOLDER_IF_OFFLINE(Android)后,应用在离线时会显示项目 local-html 文件夹中的一个页面,而一个跳转到 reconnect:// 的按钮会重新加载最后一个在线 URL(Reconnect API)。在 iOS 上,zipfiledownloadfromserver 和 zipfileremoteurl 可以从您的服务器更新这个备用页面。
Firestore 的持久化本地缓存会在离线时提供最后已知的文档,因此列表依然能显示,并附上“最后更新时间”提示;写入操作会排队,直到网络恢复。
把规则作为不变的约定,一次性写进 Web 应用的 AGENTS.md。之后的每个任务(“添加一个报表页面”)都会自动带上错误状态,而 Review guidelines 部分能让 Codex 代码审查在 pull request 中发现回归问题。
## Network and error states (every screen)
- All HTTP calls go through src/lib/request.ts: request(path, { timeoutMs = 10000 }).
It uses AbortSignal.timeout, retries idempotent GETs once with backoff, and throws
typed errors (OfflineError, TimeoutError, HttpError with status) that carry a
user-facing message. Components never call fetch directly.
- Every data view renders exactly one of: loading (skeleton, ends after 10 s at the
latest), content, empty (helpful text and a next action), error (message and a
"Try again" button).
- No raw error text in the UI: no "TypeError", no stack traces, no JSON.
- Offline banner: listen to the online and offline events and render
<div role="status">You are offline. Changes will sync when you are back.</div>
Do not rely on navigator.onLine alone; a failed request also counts as offline.
- Forms keep their input when a submit fails. Submit buttons show progress and
re-enable on error. Never send the same mutation twice without an idempotency key.
- Firestore: initializeFirestore with persistentLocalCache. Show "Last updated"
when snapshot.metadata.fromCache is true.
- Express: error middleware returns JSON { error, message } with the right status,
never an HTML error page. GET /healthz answers 200 without touching the database.
- No service worker for offline inside the native app. WebViewGold's offline screen
and the bundled fallback page handle "no network at launch".
## Review guidelines
- P1: fetch() or axios outside src/lib/request.ts, or any request without a timeout.
- P1: a catch block that swallows an error without showing an error state.
- P1: a loading state with no exit (no timeout, no error branch).
只要有一个优化良好的Web应用版本,几分钟内您就能拥有一个出色的移动应用。除此之外,客户服务非常友好且乐于助人!我只能说:推荐WebViewGold :-)
这个任务很适合作为云端任务运行,因为结果是一个规模大但很机械的 diff,您会希望以 pull request 的形式阅读它。在云端环境中,把 npx playwright install --with-deps chromium 加到设置脚本里,让浏览器在代理开始工作前就准备好;在 CLI 中,Codex 需要联网安装时会请求审批。
请阅读 AGENTS.md,尤其是“Network and error states”部分。这个 Web 应用运行在用 WebViewGold 构建的原生 iOS 和 Android 应用中,App 审核会在糟糕的网络条件下测试它。 1. 盘点:找出每一个网络调用(fetch、axios、Firestore 的 getDoc、getDocs 和 onSnapshot、server-sent events 或 WebSocket、文件上传)。编写 docs/network-inventory.md,每个调用一行:文件、页面、调用、当前的加载、空状态和错误处理方式。 2. 按照 AGENTS.md 中的规定创建 src/lib/request.ts,并把所有 HTTP 调用迁移到它上面。 3. 为清单中的每个页面实现四种状态:最迟 10 秒后结束的加载状态、内容、空状态,以及带“重试”的错误状态。复用同一个 ErrorState 组件和同一个 EmptyState 组件。 4. 添加带 role="status" 的离线横幅,由 online 和 offline 事件以及 request() 抛出的 OfflineError 驱动。 5. 表单:失败时保留用户的输入,在提交按钮旁显示错误,并重新启用按钮。 6. Firestore:切换到 persistentLocalCache,当 snapshot.metadata.fromCache 为 true 时显示“最后更新时间”。 7. Express:一个返回 JSON 的错误中间件、一个针对 /api 返回 JSON 的 404 处理器,以及 GET /healthz。 8. 创建 native-fallback/index.html:一个自包含的文件(内联 CSS、以内联 SVG 或 data URI 形式嵌入的标志、不发起任何外部请求),包含应用名称、文字“您已离线”,以及一个跳转到 reconnect:// 的“重试”按钮。我会把它复制到原生项目的 local-html 文件夹中。 9. Playwright 测试:API 请求被中止、API 请求永不响应,以及浏览器上下文切换到离线再切回在线。每种情况都必须在 12 秒内进入错误或离线状态,且不显示任何原始错误文本。如果缺少 Playwright 就安装它(先询问)。 最后给出盘点清单、改动过的页面列表,以及所有您无法覆盖的内容。
三种网络故障涵盖了审核人员可能遇到的大部分情况。它们可以在 Codex 沙盒和 CI 中运行,因为只需要本地开发服务器。
// tests/offline.spec.js (Playwright)
import { test, expect } from "@playwright/test";
test("failed API shows a retry, not a white screen", async ({ page }) => {
await page.route("**/api/**", (route) => route.abort("internetdisconnected"));
await page.goto("/dashboard");
await expect(page.getByRole("button", { name: /try again/i })).toBeVisible({ timeout: 12000 });
await expect(page.getByText(/TypeError|Failed to fetch/)).toHaveCount(0);
});
test("hanging API never leaves an endless spinner", async ({ page }) => {
await page.route("**/api/**", () => { /* never answer: a server that hangs */ });
await page.goto("/dashboard");
await expect(page.getByRole("button", { name: /try again/i })).toBeVisible({ timeout: 12000 });
});
test("offline banner appears and clears", async ({ page, context }) => {
await page.goto("/dashboard");
await context.setOffline(true);
await expect(page.getByRole("status")).toContainText(/offline/i);
await context.setOffline(false);
await expect(page.getByRole("status")).toHaveCount(0);
});| 目标 | iOS(Config.swift) | Android(Config.java) |
|---|---|---|
| 离线文字和按钮标签 | offlinetitle、offlinemsg、screen1、screen2、offlinebuttontext(可通过 alternatelanguage1 和 alternatelanguage2 翻译) | Config.java 中没有文档记载的文字配置项;背景色通过 OFFLINE_SCREEN_BACKGROUND_COLOR 设置 |
| 无需点按即可重试 | offlinescreenautoretry | 没有文档记载的选项;请使用带 reconnect:// 的备用页面 |
| 内置备用页面 | offlinelocalhtmlswitch 设为 true,页面放在 local-html 文件夹中 | FALLBACK_USE_LOCAL_HTML_FOLDER_IF_OFFLINE 设为 true,页面放在 local-html 文件夹中 |
| 从备用页面重试 | reconnect:// | reconnect:// |
| 远程更新备用页面 | zipfiledownloadfromserver 和 zipfileremoteurl | 随构建版本一起发布 |
| 应用从后台返回时刷新 | autoRefreshEnabled | AUTO_REFRESH_ENABLED |
粘贴上面的段落,并根据您的项目调整路径和超时时间。在任务运行之前提交,这样云端任务才能看到它。
作为云端任务,您会得到一个 diff 和一个 pull request;在 CLI 中则使用 codex --sandbox workspace-write --ask-for-approval on-request。无论哪种方式,Playwright 测试都必须通过。
docs/network-inventory.md 中的每一行都应对应一个具备四种状态的页面。在 pull request 中评论 @codex review,再应用一次审查规则。
用符合您应用风格和语言的措辞编写离线文字,决定是否开启 offlinescreenautoretry,设置 Android 背景色;如果需要备用页面,就把 native-fallback/index.html 复制为 local-html/index.html,并开启 offlinelocalhtmlswitch 或 FALLBACK_USE_LOCAL_HTML_FOLDER_IF_OFFLINE(Android Reconnect 文档)。
在真机上分别在启动时和使用过程中打开飞行模式。在 iPhone 上,开发者设置中的 Network Link Conditioner(设备连接过 Xcode 后可用)可以模拟极差的网络和完全断网;在 Android 上,使用飞行模式或模拟器的网络设置。
如果应用需要网络连接,请在审核备注中说明,同时说明应用离线时会显示什么。提交的其余步骤见 Codex 发布工作流程。
reconnect:// 会返回到最后一个页面。autoRefreshEnabled 或 AUTO_REFRESH_ENABLED 时页面会刷新;不开启时,过时的页面依然表现正常。navigator.onLine。在 Wi-Fi 认证门户之后或连接已断开时,它仍然是 true;请把失败和超时的请求也视为离线。不必。需要服务器的应用可以要求联网,只要在没有网络时能妥善处理:清晰的离线页面、带重试的错误状态、没有卡死或空白的页面。请在审核备注中说明应用需要网络连接。
离线页面是原生的,通过几段文字和选项来配置。备用页面是您自己放在项目 local-html 文件夹中的 HTML,在开启 offlinelocalhtmlswitch(iOS)或 FALLBACK_USE_LOCAL_HTML_FOLDER_IF_OFFLINE(Android)时代替离线页面显示,并且可以提供一个 reconnect:// 按钮。
可以,针对 Web 层。Playwright 可以中止请求、让请求一直挂起,以及把浏览器上下文切换到离线,这些都能在 Codex 沙盒中完成。原生离线页面和备用页面仍然需要在真机上用飞行模式测试。
对于以读取为主的应用,应该开启:列表从缓存中渲染,写入稍后同步。请标明数据何时来自缓存,并在缓存敏感数据之前考虑共用设备的情况。
在当前的系统 WebView 中支持;Safari 从 16 版开始支持。如果您需要支持更旧的 iOS 版本,请让 Codex 在 request.ts 中用 AbortController 加 setTimeout 作为回退方案。
结果相同,只是您需要投入的工作量不同。三种方式都附带审核通过保证和 7×24 小时人工支持。
$109 每个平台,一次性付费
WebViewGold 许可证:一个现成的 Xcode 或 Android Studio 项目。粘贴您的 URL,设置名称和图标,开启所需功能,构建并提交。永久免费更新。
在主页查看方案$349 每个许可证,一次性附加购买
无需 Mac、Xcode 或 Android Studio:在浏览器中完成配置,我们在云端构建两个应用,并将其交付到 App Store Connect 和 Play Console。
Cloud Builder 如何运作 试用免费演示$698 每个平台 $798
把您的 URL 和图标发给我们。我们的团队在 72 小时内完成配置、构建并交付您的应用,白标,附源码项目。两个平台共 $1,158。
Full Service 详情 服务提供方:冬季特惠价格有效期至 2026年12月31日。
App Store & Google Play 审核通过保证 – 否则 14 天内退款
想直接交给我们?Full Service:我们在 72 小时内完成配置、构建并交付,每个平台 $698 起。有疑问?通过 7×24 小时在线聊天联系我们。
退款依据我们 FAQ 中列出的条件:应用因非内容原因被 Apple 和/或 Google 拒审,并在购买后 14 天内提出申请。