From b5113ba3e10fadc004cb2efa1f6ecfaf30013d01 Mon Sep 17 00:00:00 2001 From: hongweiwhw <131351148+hongweiwhw@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:36:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(reader):=20=E4=BF=AE=E5=A4=8D=E6=89=93?= =?UTF-8?q?=E5=BC=80=E4=BB=BB=E6=84=8F=E4=B9=A6=E7=B1=8D=E6=8A=A5=20makeBo?= =?UTF-8?q?ok=20is=20not=20defined=EF=BC=88v1.3.5/v1.3.6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:打开任意 EPUB 报 ReferenceError: makeBook is not defined,与设备无关,任何设备均复现(Closes #703)。 根因: 1. reader.template.html 调用的是裸 makeBook(...),而打包后的 foliate bundle 仅暴露 window.makeBook,并不存在全局裸 makeBook,故抛出 ReferenceError。 2. build-reader.js 入口同时静态导入 zip.js / EPUB / pdf.js,esbuild 打包后 bundle 结尾 先急切初始化全部格式引擎、最后才赋值 window.makeBook;任一引擎(尤其 pdf.js, 含 Node 环境检测 / CDN worker 等逻辑)在旧 WebView 初始化失败会导致整个 bundle 中断,window.makeBook 永远不会被赋值,阅读器内核完全不可用。 修复: - reader.template.html:两处裸 makeBook 改为 window.makeBook,并在打开书籍前增加 if (!window.makeBook) 守卫,内核缺失时给出明确提示。 - build-reader.js:window.makeBook 改为同步最先赋值;zip/EPUB/PDF 引擎改为 Promise 异步懒加载,失败被 catch 隔离,不再阻塞阅读器内核(仅失去懒加载/PDF 特性)。 - build-reader.js:esbuild target es2020 降为 es2017;注入旧 WebView 运行时垫片 (String.replaceAll / Array.prototype.at / Object.fromEntries / Promise.allSettled / structuredClone / Intl.Locale)。 - reader.html:使用修复后的模板重新生成。 测试:在海信 A7(Android,Chromium 85+ 老 WebView)实测,修复后 EPUB 可正常打开阅读; 本地以浏览器 API 桩环境运行 bundle,确认 window.makeBook 在引擎初始化前即就绪, 引擎加载失败被安全隔离。 --- packages/app-expo/assets/reader/reader.html | 226 ++++++++++++------ .../assets/reader/reader.template.html | 24 +- packages/app-expo/scripts/build-reader.js | 110 ++++++++- 3 files changed, 277 insertions(+), 83 deletions(-) diff --git a/packages/app-expo/assets/reader/reader.html b/packages/app-expo/assets/reader/reader.html index 6967e9048..a2a3ec915 100644 --- a/packages/app-expo/assets/reader/reader.html +++ b/packages/app-expo/assets/reader/reader.html @@ -221,6 +221,20 @@ if (RN) RN.postMessage(JSON.stringify({ type, ...data })); } window.__READANY_READER_BUILD_ID = 'android-local-server-cors'; + + // ── init error capture (diagnostics for bundle load failures) ── + window.__readerInitErrors = []; + window.addEventListener('error', function (e) { + try { + window.__readerInitErrors.push(String(e && e.message ? e.message : (e.type || 'unknown')) + ' @' + (e.lineno || 0) + ':' + (e.colno || 0)); + } catch (err) {} + }); + window.addEventListener('unhandledrejection', function (e) { + try { + window.__readerInitErrors.push('Promise rejection: ' + String((e && e.reason && (e.reason.message || e.reason)) || (e && e.reason) || 'unknown')); + } catch (err) {} + }); + postToRN('debug', { message: '[ReaderBuild] ' + window.__READANY_READER_BUILD_ID }); Promise.withResolvers ??= function () { let resolve; @@ -1457,6 +1471,12 @@ } }; + if (!window.makeBook) { + var _initErrors = (window.__readerInitErrors || []).join(' | ') || '未捕获到脚本错误'; + var _bundleRan = window.__bundleStart ? 'bundle已启动' : 'bundle未启动'; + throw new Error('阅读器内核未加载:foliate bundle 未注入或执行失败。[' + _bundleRan + '] ' + _initErrors); + } + let file; if (msg.base64) { const binary = atob(msg.base64); @@ -1561,7 +1581,7 @@ throw new Error('No book data provided'); } - const book = (file && file.sections) ? file : await makeBook(file); + const book = (file && file.sections) ? file : await window.makeBook(file); currentBook = book; attachBookTransformHandler(book); @@ -4357,7 +4377,7 @@ return; } - currentBook = await makeBook(await createBookFileFromMessage(msg)); + currentBook = await window.makeBook(await createBookFileFromMessage(msg)); await handleExtractChapters(); } catch (err) { console.error('[WebView] Error extracting book chapters:', err); @@ -5025,8 +5045,72 @@ diff --git a/packages/app-expo/assets/reader/reader.template.html b/packages/app-expo/assets/reader/reader.template.html index d3658a0b3..5fc3f9e7b 100644 --- a/packages/app-expo/assets/reader/reader.template.html +++ b/packages/app-expo/assets/reader/reader.template.html @@ -174,6 +174,20 @@ if (RN) RN.postMessage(JSON.stringify({ type, ...data })); } window.__READANY_READER_BUILD_ID = 'android-local-server-cors'; + + // ── init error capture (diagnostics for bundle load failures) ── + window.__readerInitErrors = []; + window.addEventListener('error', function (e) { + try { + window.__readerInitErrors.push(String(e && e.message ? e.message : (e.type || 'unknown')) + ' @' + (e.lineno || 0) + ':' + (e.colno || 0)); + } catch (err) {} + }); + window.addEventListener('unhandledrejection', function (e) { + try { + window.__readerInitErrors.push('Promise rejection: ' + String((e && e.reason && (e.reason.message || e.reason)) || (e && e.reason) || 'unknown')); + } catch (err) {} + }); + postToRN('debug', { message: '[ReaderBuild] ' + window.__READANY_READER_BUILD_ID }); Promise.withResolvers ??= function () { let resolve; @@ -1410,6 +1424,12 @@ } }; + if (!window.makeBook) { + var _initErrors = (window.__readerInitErrors || []).join(' | ') || '未捕获到脚本错误'; + var _bundleRan = window.__bundleStart ? 'bundle已启动' : 'bundle未启动'; + throw new Error('阅读器内核未加载:foliate bundle 未注入或执行失败。[' + _bundleRan + '] ' + _initErrors); + } + let file; if (msg.base64) { const binary = atob(msg.base64); @@ -1514,7 +1534,7 @@ throw new Error('No book data provided'); } - const book = (file && file.sections) ? file : await makeBook(file); + const book = (file && file.sections) ? file : await window.makeBook(file); currentBook = book; attachBookTransformHandler(book); @@ -4310,7 +4330,7 @@ return; } - currentBook = await makeBook(await createBookFileFromMessage(msg)); + currentBook = await window.makeBook(await createBookFileFromMessage(msg)); await handleExtractChapters(); } catch (err) { console.error('[WebView] Error extracting book chapters:', err); diff --git a/packages/app-expo/scripts/build-reader.js b/packages/app-expo/scripts/build-reader.js index ec5c53b56..6b3a30fa6 100644 --- a/packages/app-expo/scripts/build-reader.js +++ b/packages/app-expo/scripts/build-reader.js @@ -14,25 +14,93 @@ const TEMPLATE = path.resolve(ASSETS_DIR, "reader.template.html"); const OUTPUT = path.resolve(ASSETS_DIR, "reader.html"); const JUSTIFIED_TEXT = path.resolve(ASSETS_DIR, "justified-text.js"); +// ES2020+ runtime polyfills for older Android WebViews. Must be ES5-compatible +// (runs before the bundle on devices that may lack modern APIs). +const POLYFILLS = `/* ReadAny WebView runtime polyfills */ +(function () { + if (typeof String.prototype.replaceAll !== "function") { + String.prototype.replaceAll = function (search, replace) { + if (search instanceof RegExp) { + if (!search.global) throw new TypeError("String.prototype.replaceAll called with a non-global RegExp"); + return this.replace(search, replace); + } + return this.split(String(search)).join(String(replace)); + }; + } + if (typeof Array.prototype.at !== "function") { + Array.prototype.at = function (index) { + var n = Math.trunc(index) || 0; + if (n < 0) n += this.length; + return n < 0 || n >= this.length ? undefined : this[n]; + }; + } + if (typeof String.prototype.at !== "function") { + String.prototype.at = function (index) { + var n = Math.trunc(index) || 0; + if (n < 0) n += this.length; + return n < 0 || n >= this.length ? undefined : this[n]; + }; + } + if (typeof Object.fromEntries !== "function") { + Object.fromEntries = function (entries) { + var obj = {}; + for (var i = 0; i < entries.length; i++) { + var kv = entries[i]; + obj[kv[0]] = kv[1]; + } + return obj; + }; + } + if (typeof Promise.allSettled !== "function") { + Promise.allSettled = function (promises) { + return Promise.all(Array.prototype.slice.call(promises).map(function (p) { + return Promise.resolve(p).then( + function (value) { return { status: "fulfilled", value: value }; }, + function (reason) { return { status: "rejected", reason: reason }; } + ); + })); + }; + } + if (typeof structuredClone !== "function") { + (typeof globalThis !== "undefined" ? globalThis : window).structuredClone = function (value) { + return JSON.parse(JSON.stringify(value)); + }; + } + if (typeof Intl !== "undefined" && typeof Intl.Locale !== "function") { + var ShimLocale = function (tag) { + var parts = String(tag).split("-"); + this.language = parts[0] || ""; + var dir = "ltr"; + if (/^(zh|ja|ko|ar|he|ur|fa)$/i.test(this.language)) dir = "rtl"; + this.textInfo = function () { return { direction: dir }; }; + this.getTextInfo = this.textInfo; + }; + Intl.Locale = ShimLocale; + } +})(); +window.__bundleStart = 1; +`; + async function buildReader() { // Create a temporary entry point + // IMPORTANT: window.makeBook is assigned FIRST and synchronously. Heavy format + // engines (zip/EPUB/PDF) are loaded lazily in a non-blocking promise chain so a + // failure in one of them (e.g. pdf.js on an old WebView) can never prevent the + // reader kernel from becoming available. const entryContent = ` import { makeBook, View } from "${FOLIATE_DIR.replace(/\\/g, "/")}/view.js"; import { Overlayer } from "${FOLIATE_DIR.replace(/\\/g, "/")}/overlayer.js"; import * as CFI from "${FOLIATE_DIR.replace(/\\/g, "/")}/epubcfi.js"; - import { configure, ZipReader, BlobReader, TextWriter, BlobWriter } from "${FOLIATE_DIR.replace(/\\/g, "/")}/vendor/zip.js"; - import { EPUB } from "${FOLIATE_DIR.replace(/\\/g, "/")}/epub.js"; - import { extractPDFChapters, makePDFFromURL } from "${FOLIATE_DIR.replace(/\\/g, "/")}/pdf.js"; window.makeBook = makeBook; window.Overlayer = Overlayer; window.CFI = CFI; - // Expose zip.js and EPUB for lazy Range-based loading in reader template - window._zipJs = { configure, ZipReader, BlobReader, TextWriter, BlobWriter }; - window._EPUB = EPUB; - window._makePDFFromURL = makePDFFromURL; - window._extractPDFChapters = extractPDFChapters; + // Placeholders — filled in by the async engine loader below + window._zipJs = null; + window._EPUB = null; + window._makePDFFromURL = null; + window._extractPDFChapters = null; if (!customElements.get('foliate-view')) { customElements.define('foliate-view', View); @@ -41,6 +109,28 @@ async function buildReader() { if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'foliate-loaded' })); } + + // Lazy-load zip.js / EPUB / PDF engines without blocking the reader kernel. + // Each import is converted by esbuild to a Promise-based lazy require, so a + // failing engine (e.g. pdf.js on an old WebView) is contained here and the + // reader still works — it only loses lazy Range loading / PDF features. + Promise.resolve() + .then(() => import("${FOLIATE_DIR.replace(/\\/g, "/")}/vendor/zip.js")) + .then((m) => { + window._zipJs = { configure: m.configure, ZipReader: m.ZipReader, BlobReader: m.BlobReader, TextWriter: m.TextWriter, BlobWriter: m.BlobWriter }; + return import("${FOLIATE_DIR.replace(/\\/g, "/")}/epub.js"); + }) + .then((m) => { + window._EPUB = m.EPUB; + return import("${FOLIATE_DIR.replace(/\\/g, "/")}/pdf.js"); + }) + .then((m) => { + window._makePDFFromURL = m.makePDFFromURL; + window._extractPDFChapters = m.extractPDFChapters; + }) + .catch((err) => { + try { console.warn('[Reader] Lazy engine init failed:', err); } catch (_) {} + }); `; const entryFile = path.resolve(__dirname, "../.foliate-entry.mjs"); @@ -51,13 +141,13 @@ async function buildReader() { entryPoints: [entryFile], bundle: true, format: "iife", - target: "es2020", + target: "es2017", minify: true, write: false, resolveExtensions: [".js", ".mjs"], }); - const bundledJS = result.outputFiles[0].text; + const bundledJS = POLYFILLS + "\n" + result.outputFiles[0].text; // Read the template HTML and reader-side helper sources (never modified) const template = fs.readFileSync(TEMPLATE, "utf-8");