Fix iOS WebKit crash by shipping external app.js instead of inlined HTML.
Stop embedding the 1.4MB bundle in index.html to avoid std::overflow_error on launch, validate the iOS asset build, and surface load failures in the native shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,8 +36,11 @@ window.__WORDLOOP_RUNTIME__ = {
|
||||
const withoutModule = html
|
||||
.replace(/\s+crossorigin(="[^"]*")?/g, '')
|
||||
.replace('<script type="module"', '<script')
|
||||
.replace(/\.\/assets\//g, 'wordloop://app/assets/')
|
||||
return withoutModule.replace('<script src=', `${bootstrap}\n <script src=`)
|
||||
const withBootstrap = withoutModule.replace('<script src=', `${bootstrap}\n <script src=`)
|
||||
const scriptTag = withBootstrap.match(/<script src="\.\/assets\/[^"]+"><\/script>/)
|
||||
if (!scriptTag) return withBootstrap
|
||||
const withoutHeadScript = withBootstrap.replace(scriptTag[0], '')
|
||||
return withoutHeadScript.replace('</body>', ` ${scriptTag[0]}\n </body>`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,6 +47,6 @@ open ios/WordLoop/WordLoop.xcodeproj
|
||||
- **界面层**:内嵌 `frontend` 构建产物,经 `wordloop://` 本地协议加载(避免 `file://` 白屏)
|
||||
- **数据层**:请求发往所选服务器的 `/api`
|
||||
|
||||
内嵌页通过 `loadHTMLString` 加载,`app.js` 构建时会内联到 `</body>` 前(避免在 `<head>` 执行时 `#app` 尚未存在导致白屏)。
|
||||
内嵌页通过 `loadFileURL` 加载 Bundle 内 `www/index.html`,`app.js` / `app.css` 为独立外链文件(**不要**内联进 HTML,否则 WebKit 解析超大 `<script>` 会触发 `std::overflow_error` 崩溃)。构建时脚本放在 `</body>` 前,确保 `#app` 已存在。
|
||||
|
||||
若出现白屏,请先执行 `bash ios/scripts/build-web-assets.sh`,再在 Xcode **Product → Clean Build Folder** 后重新 Run。
|
||||
|
||||
@@ -6,6 +6,7 @@ final class WebViewStore: ObservableObject {
|
||||
@Published var isLoading = true
|
||||
@Published var canGoBack = false
|
||||
@Published var pageTitle = "WordLoop"
|
||||
@Published var loadError: String?
|
||||
weak var webView: WKWebView?
|
||||
var schemeHandler: BundleWebSchemeHandler?
|
||||
}
|
||||
@@ -92,26 +93,23 @@ struct WordLoopWebView: UIViewRepresentable {
|
||||
loadedEntryURL = entryURL
|
||||
loadedAPIBaseURL = apiBaseURL
|
||||
pendingAPIBaseURL = apiBaseURL
|
||||
store.isLoading = true
|
||||
Task { @MainActor in
|
||||
store.isLoading = true
|
||||
store.loadError = nil
|
||||
}
|
||||
|
||||
if usesBundledFrontend, let bundleEntry = Self.bundledHTMLPayload() {
|
||||
// loadHTMLString 比 wordloop:// 自定义协议更可靠(WKWebView 对内联/外链脚本执行更稳定)
|
||||
webView.loadHTMLString(bundleEntry.html, baseURL: bundleEntry.baseURL)
|
||||
if usesBundledFrontend,
|
||||
let wwwRoot = ServerSettings.bundledWWWDirectory,
|
||||
let indexURL = ServerSettings.bundledHomeURL {
|
||||
let readAccess = ServerSettings.bundledReadAccessURL() ?? wwwRoot
|
||||
webView.loadFileURL(indexURL, allowingReadAccessTo: readAccess)
|
||||
} else if let remoteURL = ServerSettings.homeURL(for: apiBaseURL) {
|
||||
webView.load(URLRequest(url: remoteURL, cachePolicy: .reloadIgnoringLocalCacheData))
|
||||
} else {
|
||||
webView.load(URLRequest(url: entryURL, cachePolicy: .reloadIgnoringLocalCacheData))
|
||||
}
|
||||
}
|
||||
|
||||
private static func bundledHTMLPayload() -> (html: String, baseURL: URL)? {
|
||||
guard let wwwRoot = ServerSettings.bundledWWWDirectory,
|
||||
let indexURL = ServerSettings.bundledHomeURL,
|
||||
let html = try? String(contentsOf: indexURL, encoding: .utf8),
|
||||
!html.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return (html, wwwRoot)
|
||||
}
|
||||
|
||||
func updateAPIBase(_ apiBaseURL: String, in webView: WKWebView) {
|
||||
guard loadedAPIBaseURL != apiBaseURL else { return }
|
||||
loadedAPIBaseURL = apiBaseURL
|
||||
@@ -126,26 +124,43 @@ struct WordLoopWebView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
store.isLoading = true
|
||||
store.canGoBack = webView.canGoBack
|
||||
Task { @MainActor in
|
||||
store.isLoading = true
|
||||
store.canGoBack = webView.canGoBack
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
Task { @MainActor in
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
store.loadError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
Task { @MainActor in
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
store.loadError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
store.pageTitle = webView.title ?? "WordLoop"
|
||||
if let apiBaseURL = pendingAPIBaseURL {
|
||||
updateAPIBase(apiBaseURL, in: webView)
|
||||
Task { @MainActor in
|
||||
store.isLoading = false
|
||||
store.canGoBack = webView.canGoBack
|
||||
store.pageTitle = webView.title ?? "WordLoop"
|
||||
if let apiBaseURL = pendingAPIBaseURL {
|
||||
updateAPIBase(apiBaseURL, in: webView)
|
||||
}
|
||||
}
|
||||
webView.evaluateJavaScript("document.getElementById('app')?.innerHTML?.length ?? 0") { [weak self] result, _ in
|
||||
let appLen = (result as? NSNumber)?.intValue ?? 0
|
||||
guard appLen <= 0 else { return }
|
||||
Task { @MainActor in
|
||||
self?.store.loadError = "页面未渲染,请删除 App 后重新安装"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +212,24 @@ struct WordLoopWebContainer: View {
|
||||
)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
|
||||
if store.isLoading {
|
||||
if let loadError = store.loadError {
|
||||
VStack(spacing: 12) {
|
||||
Text("页面加载失败")
|
||||
.font(.headline)
|
||||
Text(loadError)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
Button("重新加载") {
|
||||
store.loadError = nil
|
||||
store.webView?.reload()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding(24)
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
} else if store.isLoading {
|
||||
ProgressView("加载中…")
|
||||
.padding(20)
|
||||
.background(.ultraThinMaterial)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,29 +18,33 @@ cp -R dist/* "$OUT/"
|
||||
|
||||
python3 - <<PY
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
out = Path("$OUT")
|
||||
html_path = out / "index.html"
|
||||
js_path = out / "assets" / "app.js"
|
||||
css_path = out / "assets" / "app.css"
|
||||
html = html_path.read_text(encoding="utf-8")
|
||||
js = js_path.read_text(encoding="utf-8")
|
||||
css = css_path.read_text(encoding="utf-8")
|
||||
|
||||
# 单文件内联,避免 wordloop:// 二次请求脚本失败
|
||||
html = html.replace('<link rel="stylesheet" href="wordloop://app/assets/app.css">', f"<style>{css}</style>")
|
||||
html = html.replace(
|
||||
'<script src="wordloop://app/assets/app.js"></script>',
|
||||
"",
|
||||
)
|
||||
# 脚本放在 </body> 前,确保 #app 已存在(head 内联会导致 Vue mount 失败、白屏)
|
||||
html = html.replace(
|
||||
"</body>",
|
||||
f" <script>{js}</script>\\n </body>",
|
||||
)
|
||||
# 保持外链资源,避免把 ~1.4MB JS 内联进 HTML 导致 WebKit std::overflow_error 崩溃
|
||||
html = html.replace("wordloop://app/assets/", "./assets/")
|
||||
|
||||
script = re.search(r'<script src="\./assets/[^"]+"></script>', html)
|
||||
if script:
|
||||
tag = script.group(0)
|
||||
if tag not in html.split("</body>", 1)[-1]:
|
||||
html = html.replace(tag, "", 1)
|
||||
html = html.replace("</body>", f" {tag}\n </body>", 1)
|
||||
|
||||
html_path.write_text(html, encoding="utf-8")
|
||||
print(">>> 已内联 app.js / app.css 到 index.html")
|
||||
|
||||
if len(html) > 20_000:
|
||||
raise SystemExit("index.html 过大,可能仍含内联 JS,会导致 WebKit overflow 崩溃")
|
||||
for block in re.findall(r"<script>(.*?)</script>", html, re.DOTALL):
|
||||
if len(block) > 1000:
|
||||
raise SystemExit("index.html 检测到内联业务脚本,请保持 app.js 外链")
|
||||
if "./assets/app.js" not in html:
|
||||
raise SystemExit("index.html 缺少 ./assets/app.js 外链")
|
||||
|
||||
print(">>> 已生成外链版 index.html(app.js / app.css 独立文件)")
|
||||
PY
|
||||
|
||||
echo ">>> 已复制到 $OUT"
|
||||
|
||||
Reference in New Issue
Block a user