diff --git a/assets/js/app.js b/assets/js/app.js index d70c144..58163d7 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -90,14 +90,21 @@ document.head.appendChild(l); }); } + /* ⚠️ 与 Semi 段那个 `loadJs` 用**同一个全局登记表** —— 两处都加载 React + (Semi 无条件、antd 有条件),不共享去重就会执行两次 ⇒ React #321。 + 详见 Semi 段 `loadJs` 上方的完整说明。 */ function loadJs(src) { - return new Promise(function (res, rej) { + var reg = window.__fbJsLoaded; + if (!reg) { reg = window.__fbJsLoaded = {}; } + if (reg[src]) { return reg[src]; } + reg[src] = new Promise(function (res, rej) { var el = document.createElement('script'); el.src = src; el.onload = function () { res(); }; el.onerror = function () { rej(new Error('加载失败: ' + src)); }; document.head.appendChild(el); }); + return reg[src]; } function ensureCropper() { if (window.Cropper) { return Promise.resolve(); } @@ -1597,14 +1604,47 @@ document.head.appendChild(l); }); } + /** + * 加载一个 JS 文件。 + * + * 🔴🔴 2026-11-05 修 React #321(Invalid hook call): + * + * 症状:首页点任务 → 浮层点「编辑」→ 控制台的 `AntD 日期控件已挂上 1 处` + * 紧接着抛 `Minified React error #321`,日期控件崩掉。 + * + * 根因:**React 被加载了两遍**。 + * · Semi 段的 `boot()` 用 `requestIdleCallback` 延迟跑, + * 里面**无条件** `loadJs(conf.react)` + `loadJs(conf.dom)`; + * · antd 段的 `ensure()` 只在 `!window.React` 时才加载 —— 看着更"谨慎", + * 但两边谁先谁后取决于 idle 回调的时机。 + * ⇒ antd 先加载、Semi 的 idle 后到 ⇒ Semi **又插了一遍**同一个 + * `react.min.js` ⇒ 那段 UMD **执行第二次** ⇒ `window.React` 被覆盖。 + * 而 antd 已经在内部捕获了**第一次**的 React 引用 ⇒ 两套 React 并存 + * ⇒ antd 的 hook(useContext)在"第二套"的 dispatcher 上调用 ⇒ #321。 + * + * 堆栈里 `VM303 react.min.js` / `VM320 react-dom.min.js` 这种 **VM 前缀** + * 就是"同一个文件被 eval 执行了不止一次"的铁证。 + * + * ⇒ 修法:按 `src` **全局去重**,同一个地址永远只执行一次。 + * 缓存在 `window.__fbJsLoaded`(跨 IIFE 共享 —— 本文件里有两个独立的 + * `loadJs` 定义,Semi 一个、Semi 外层一个,各自闭包内的变量互相看不见)。 + * + * ⚠️ 不能只给 Semi 那边加判断(`if (!window.React)`):那种写法依赖"现有全局 + * 是否已经有 React",而 UMD 执行完才赋值 —— 两个请求几乎同时发出时, + * 两边都还看不到 `window.React`,照样会各插一个。**按 URL 去重才是确定的。** + */ function loadJs(src) { - return new Promise(function (res, rej) { + var reg = window.__fbJsLoaded; + if (!reg) { reg = window.__fbJsLoaded = {}; } + if (reg[src]) { return reg[src]; } + reg[src] = new Promise(function (res, rej) { var s = document.createElement('script'); s.src = src; s.onload = function () { res(); }; s.onerror = function () { rej(new Error('加载失败: ' + src)); }; document.head.appendChild(s); }); + return reg[src]; } function pad(n) { return (n < 10 ? '0' : '') + n; }