Files
Jyotisha/frontend/src/lib/first-paint-fallback.ts
T

130 lines
5.4 KiB
TypeScript

/**
* Classic inline script for the home loading screen.
*
* It has to run when the client bundle never runs, so the generated string is
* ES5: no modules, no React, no reload until the button is clicked.
* The timer is longer than the 20s in-bundle bootstrap slow threshold, so a
* normal hydrate does not flash this copy.
*/
import {
HOME_BOOTSTRAP_FAILURE,
sanitizeBootstrapBuildId,
} from "./home-bootstrap-failure.ts";
export const FIRST_PAINT_FALLBACK_TIMEOUT_MS = 25_000;
export const FIRST_PAINT_FALLBACK_TITLE = "这个页面没能加载完";
export const FIRST_PAINT_FALLBACK_BODY =
"网络中断或浏览器版本过旧都会这样。可以重新加载试一次;如果反复出现,请在 Safari 设置里清除本站数据,或把系统升级到 iOS 16.4 以上。";
export const FIRST_PAINT_FALLBACK_ACTION = "重新加载";
function jsDoubleQuoted(value: string): string {
let escaped = "";
for (let index = 0; index < value.length; index += 1) {
const char = value.charAt(index);
const code = value.charCodeAt(index);
if (char === "\\") escaped += "\\\\";
else if (char === "\"") escaped += "\\\"";
else if (char === "\n") escaped += "\\n";
else if (char === "\r") escaped += "\\r";
else if (char === "<") escaped += "\\u003c";
else if (code === 0x2028 || code === 0x2029) escaped += `\\u${code.toString(16)}`;
else escaped += char;
}
return `"${escaped}"`;
}
export function firstPaintFallbackBootScript(buildId?: string): string {
const safeBuild = sanitizeBootstrapBuildId(buildId ?? process.env.NEXT_PUBLIC_GIT_COMMIT);
const timeout = String(FIRST_PAINT_FALLBACK_TIMEOUT_MS);
const hydrateTimeout = jsDoubleQuoted(HOME_BOOTSTRAP_FAILURE.hydrateTimeout);
const chunk404 = jsDoubleQuoted(HOME_BOOTSTRAP_FAILURE.chunk404);
const chunkParse = jsDoubleQuoted(HOME_BOOTSTRAP_FAILURE.chunkParse);
const staleCache = jsDoubleQuoted(HOME_BOOTSTRAP_FAILURE.staleCache);
const weakNetwork = jsDoubleQuoted(HOME_BOOTSTRAP_FAILURE.weakNetwork);
return `(function () {
var started = Date.now();
var buildId = ${jsDoubleQuoted(safeBuild)};
var category = ${hydrateTimeout};
function fromCache(source) {
try {
if (!source || !window.performance || !window.performance.getEntriesByName) return false;
var entries = window.performance.getEntriesByName(source);
var i;
for (i = 0; i < entries.length; i++) {
var entry = entries[i];
if (entry && entry.decodedBodySize > 0 && entry.transferSize === 0) return true;
}
} catch (ignore) {}
return false;
}
function classify(message, source) {
var text = String(message || "");
var from = String(source || "");
var combined = text + " " + from;
if (/SyntaxError|Unexpected token|Invalid regular expression/i.test(text)) {
return fromCache(from) ? ${staleCache} : ${chunkParse};
}
if (/ChunkLoadError|Loading chunk|Failed to fetch dynamically imported module|Failed to load/i.test(combined) || /\\/_next\\/static\\//.test(from)) {
if (fromCache(from)) return ${staleCache};
if (navigator && navigator.onLine === false) return ${weakNetwork};
return ${chunk404};
}
return "";
}
window.addEventListener("error", function (event) {
var message = "";
var source = "";
if (event) {
message = event.message || "";
source = event.filename || "";
if (!source && event.target && event.target.src) source = event.target.src;
}
var next = classify(message, source);
if (next) category = next;
}, true);
window.setTimeout(function () {
var root = document.documentElement;
if (!root || root.getAttribute("data-hydrated") === "1") return;
var loading = document.querySelector(".app-loading");
if (!loading) return;
if ((category === ${hydrateTimeout} || category === ${chunk404}) && navigator && navigator.onLine === false) {
category = ${weakNetwork};
}
var slot = loading.querySelector(".app-loading-content");
if (!slot) return;
if (loading.getAttribute("data-bootstrap-fallback") === "1") return;
loading.setAttribute("data-bootstrap-fallback", "1");
loading.setAttribute("aria-busy", "false");
if (loading.className.indexOf("app-loading-error") === -1) {
loading.className += " app-loading-error";
}
slot.setAttribute("role", "alert");
slot.innerHTML = "";
var title = document.createElement("strong");
title.appendChild(document.createTextNode(${jsDoubleQuoted(FIRST_PAINT_FALLBACK_TITLE)}));
var body = document.createElement("span");
body.appendChild(document.createTextNode(${jsDoubleQuoted(FIRST_PAINT_FALLBACK_BODY)}));
var actions = document.createElement("div");
actions.className = "app-loading-actions";
var button = document.createElement("button");
button.className = "button-primary";
button.type = "button";
button.appendChild(document.createTextNode(${jsDoubleQuoted(FIRST_PAINT_FALLBACK_ACTION)}));
button.onclick = function () { window.location.reload(); };
actions.appendChild(button);
slot.appendChild(title);
slot.appendChild(body);
slot.appendChild(actions);
var duration = Date.now() - started;
root.setAttribute("data-bootstrap-failure", category);
root.setAttribute("data-bootstrap-build", buildId);
root.setAttribute("data-bootstrap-duration", String(duration));
window.__jyotishaBootstrapObservation = { category: category, buildId: buildId, durationMs: duration };
}, ${timeout});
})();`;
}