@open-wxa/wxa-runner-core 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1018,9 +1018,9 @@ ${frames}` : "");
1018
1018
  (this.replies.get(query.id) ?? Promise.resolve(null)).then((reply) => port.postMessage(reply));
1019
1019
  }
1020
1020
  };
1021
- function installMockMirror() {
1022
- if (navigator.webdriver || !navigator.serviceWorker) return null;
1023
- navigator.serviceWorker.register(SERVICE_WORKER_URL, { scope: "/" }).catch((error) => console.warn("[wxa-runner] Service Worker \u6CE8\u518C\u5931\u8D25\uFF0C\u88AB\u62E6\u622A\u7684\u8BF7\u6C42\u4E0D\u4F1A\u663E\u793A\u5728 Network \u9762\u677F\u91CC", error));
1021
+ function installMockMirror(externalServiceWorker = false) {
1022
+ if (!externalServiceWorker && navigator.webdriver || !navigator.serviceWorker) return null;
1023
+ if (!externalServiceWorker) navigator.serviceWorker.register(SERVICE_WORKER_URL, { scope: "/" }).catch((error) => console.warn("[wxa-runner] Service Worker \u6CE8\u518C\u5931\u8D25\uFF0C\u88AB\u62E6\u622A\u7684\u8BF7\u6C42\u4E0D\u4F1A\u663E\u793A\u5728 Network \u9762\u677F\u91CC", error));
1024
1024
  return new MockMirror(navigator.serviceWorker);
1025
1025
  }
1026
1026
 
@@ -1697,16 +1697,11 @@ ${frames}` : "");
1697
1697
  }
1698
1698
 
1699
1699
  // src/runtime/host/api/webView.ts
1700
- function createWebViewApis({ documentOf, emitToWebview }) {
1700
+ function createWebViewApis({ documentOf, emitToWebview, windows }) {
1701
+ if (windows) return createWindowApis(documentOf, windows);
1701
1702
  const find = (webviewId, htmlId) => documentOf(webviewId)?.querySelector(`iframe[data-wxa-html-id="${Number(htmlId)}"]`) ?? null;
1702
1703
  const reportStart = (webviewId, htmlId, src) => emitToWebview(webviewId, "onWebviewStartLoad", { htmlId, src, isMainFrame: true });
1703
1704
  const reportFinish = (webviewId, htmlId, src) => emitToWebview(webviewId, "onWebviewFinishLoad", { htmlId, src });
1704
- const place = (frame, position) => {
1705
- if (!position) return;
1706
- for (const side of ["left", "top", "width", "height"]) {
1707
- if (typeof position[side] === "number") frame.style[side] = `${position[side]}px`;
1708
- }
1709
- };
1710
1705
  return {
1711
1706
  insertHTMLWebView: (args, { api, webviewId }) => {
1712
1707
  const doc = documentOf(webviewId);
@@ -1737,7 +1732,9 @@ ${frames}` : "");
1737
1732
  place(frame, args.position);
1738
1733
  if (args.src) {
1739
1734
  const src = String(args.src);
1735
+ warnCrossSite(src);
1740
1736
  frame.dataset.wxaLoading = src;
1737
+ frame.dataset.wxaSrc = src;
1741
1738
  reportStart(webviewId, Number(args.htmlId), src);
1742
1739
  frame.src = src;
1743
1740
  }
@@ -1749,6 +1746,161 @@ ${frames}` : "");
1749
1746
  }
1750
1747
  };
1751
1748
  }
1749
+ function createWindowApis(documentOf, windows) {
1750
+ return {
1751
+ insertHTMLWebView: (args, { api, webviewId }) => {
1752
+ const doc = documentOf(webviewId);
1753
+ if (!doc) return fail(api, "page not found");
1754
+ windows.insert(webviewId, Number(args.htmlId), doc, args.position);
1755
+ return ok(api);
1756
+ },
1757
+ updateHTMLWebView: (args, { api, webviewId }) => {
1758
+ if (!windows.update(webviewId, Number(args.htmlId), args.position, args.src ? String(args.src) : void 0)) {
1759
+ return fail(api, "web-view not found");
1760
+ }
1761
+ return ok(api);
1762
+ },
1763
+ removeHTMLWebView: (args, { api, webviewId }) => {
1764
+ windows.remove(webviewId, Number(args.htmlId));
1765
+ return ok(api);
1766
+ }
1767
+ };
1768
+ }
1769
+ function iframeWebViews(doc) {
1770
+ return Array.from(doc?.querySelectorAll("iframe[data-wxa-src]") ?? []).map((frame) => ({ src: frame.dataset.wxaSrc, windowName: null }));
1771
+ }
1772
+ var WebViewWindows = class {
1773
+ /** @param isShown 页面当前是否显示:隐藏的页面先记下地址,显示时再打开窗口 */
1774
+ constructor(device, isShown) {
1775
+ this.device = device;
1776
+ this.isShown = isShown;
1777
+ this.records = /* @__PURE__ */ new Map();
1778
+ }
1779
+ insert(webviewId, htmlId, doc, position) {
1780
+ const previous = this.records.get(windowName(webviewId, htmlId));
1781
+ previous?.placeholder.remove();
1782
+ const placeholder = doc.createElement("div");
1783
+ placeholder.dataset.wxaHtmlId = String(htmlId);
1784
+ placeholder.style.cssText = "position:fixed;z-index:2147483647;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:24px;background:#f7f7f7;color:#666;font:14px/1.6 sans-serif;text-align:center;word-break:break-all";
1785
+ const status = doc.createElement("div");
1786
+ const button = doc.createElement("button");
1787
+ button.textContent = "\u5728\u65B0\u7A97\u53E3\u6253\u5F00 H5";
1788
+ button.style.cssText = "padding:8px 16px;border:0;border-radius:4px;background:#07c160;color:#fff;font-size:14px";
1789
+ placeholder.append(status, button);
1790
+ place(placeholder, position);
1791
+ doc.body.appendChild(placeholder);
1792
+ const record = { webviewId, name: windowName(webviewId, htmlId), placeholder, status, src: previous?.src, win: previous?.win ?? null };
1793
+ this.records.set(record.name, record);
1794
+ button.addEventListener("click", () => {
1795
+ if (record.win && !record.win.closed) record.win.focus();
1796
+ else this.open(record);
1797
+ });
1798
+ this.render(record);
1799
+ }
1800
+ /** @returns 找不到这个 web-view 时为 false */
1801
+ update(webviewId, htmlId, position, src) {
1802
+ const record = this.records.get(windowName(webviewId, htmlId));
1803
+ if (!record) return false;
1804
+ place(record.placeholder, position);
1805
+ if (src && src !== record.src) {
1806
+ record.src = src;
1807
+ if (this.isShown(webviewId)) this.open(record);
1808
+ else this.render(record);
1809
+ }
1810
+ return true;
1811
+ }
1812
+ remove(webviewId, htmlId) {
1813
+ const record = this.records.get(windowName(webviewId, htmlId));
1814
+ if (!record) return;
1815
+ this.records.delete(record.name);
1816
+ close(record);
1817
+ record.placeholder.remove();
1818
+ }
1819
+ pageShown(webviewId) {
1820
+ for (const record of this.recordsOf(webviewId)) {
1821
+ if (record.src && !(record.win && !record.win.closed)) this.open(record);
1822
+ }
1823
+ }
1824
+ pageHidden(webviewId) {
1825
+ for (const record of this.recordsOf(webviewId)) {
1826
+ close(record);
1827
+ this.render(record);
1828
+ }
1829
+ }
1830
+ /** 页面关闭:渲染层已经或即将被删除,不必再动占位 */
1831
+ pageDestroyed(webviewId) {
1832
+ for (const record of this.recordsOf(webviewId)) {
1833
+ this.records.delete(record.name);
1834
+ close(record);
1835
+ }
1836
+ }
1837
+ closeAll() {
1838
+ for (const record of this.records.values()) close(record);
1839
+ }
1840
+ /** 页面里的 web-view,按创建顺序;还没设置 src 的不算 */
1841
+ list(webviewId) {
1842
+ return this.recordsOf(webviewId).flatMap((record) => record.src ? [{ src: record.src, windowName: record.name }] : []);
1843
+ }
1844
+ recordsOf(webviewId) {
1845
+ return [...this.records.values()].filter((record) => record.webviewId === webviewId);
1846
+ }
1847
+ open(record) {
1848
+ if (!record.src) return;
1849
+ if (record.win && !record.win.closed) {
1850
+ record.win.location.href = record.src;
1851
+ } else {
1852
+ const features = `popup,width=${this.device.screenWidth},height=${this.device.screenHeight}`;
1853
+ record.win = window.open(record.src, record.name, features);
1854
+ if (record.win) notify("open", record.name, record.src);
1855
+ }
1856
+ this.render(record);
1857
+ }
1858
+ render(record) {
1859
+ const opened = record.win && !record.win.closed;
1860
+ const [title, detail] = !record.src ? ["web-view \u8FD8\u6CA1\u6709\u8BBE\u7F6E src", ""] : opened ? ["H5 \u5DF2\u5728\u65B0\u7A97\u53E3\u6253\u5F00", record.src] : record.win === null && this.isShown(record.webviewId) ? ["\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u5F39\u51FA\u7A97\u53E3\uFF0C\u70B9\u4E0B\u9762\u7684\u6309\u94AE\u6253\u5F00 H5", record.src] : ["H5 \u5728\u65B0\u7A97\u53E3\u6253\u5F00", record.src];
1861
+ record.status.textContent = "";
1862
+ const heading = record.status.ownerDocument.createElement("div");
1863
+ heading.textContent = title;
1864
+ heading.style.cssText = "color:#333;font-size:16px";
1865
+ record.status.append(heading);
1866
+ if (detail) record.status.append(detail);
1867
+ }
1868
+ };
1869
+ function windowName(webviewId, htmlId) {
1870
+ return `wxa-h5-${webviewId}-${htmlId}`;
1871
+ }
1872
+ function close(record) {
1873
+ if (!record.win) return;
1874
+ record.win.close();
1875
+ record.win = null;
1876
+ notify("close", record.name);
1877
+ }
1878
+ function notify(type, name, src) {
1879
+ const listener = window.__wxa_webview_window_event__;
1880
+ listener?.(type, name, src)?.catch(() => void 0);
1881
+ }
1882
+ function place(element2, position) {
1883
+ if (!position) return;
1884
+ for (const side of ["left", "top", "width", "height"]) {
1885
+ if (typeof position[side] === "number") element2.style[side] = `${position[side]}px`;
1886
+ }
1887
+ }
1888
+ var warnedOrigins = /* @__PURE__ */ new Set();
1889
+ function warnCrossSite(src) {
1890
+ let url;
1891
+ try {
1892
+ url = new URL(src, location.href);
1893
+ } catch {
1894
+ return;
1895
+ }
1896
+ if (!["http:", "https:"].includes(url.protocol)) return;
1897
+ if (url.hostname === location.hostname && url.protocol === location.protocol) return;
1898
+ if (warnedOrigins.has(url.origin)) return;
1899
+ warnedOrigins.add(url.origin);
1900
+ console.warn(
1901
+ `[wxa-runner] web-view \u52A0\u8F7D\u7684 ${url.origin} \u4E0E\u6A21\u62DF\u5668\uFF08${location.origin}\uFF09\u4E0D\u662F\u540C\u4E00\u4E2A\u7AD9\u70B9\uFF0C\u6D4F\u89C8\u5668\u6309\u8DE8\u7AD9\u5D4C\u5165\u5904\u7406\uFF0C\u7F51\u9875\u7684 Cookie \u53EF\u80FD\u4E0D\u751F\u6548\uFF08\u5FAE\u4FE1\u91CC\u7684\u5C0F\u7A0B\u5E8F\u6CA1\u6709\u8FD9\u4E2A\u95EE\u9898\uFF09\u3002\u9700\u8981 Cookie \u65F6\u8BA9\u7F51\u9875\u5728\u65B0\u7A97\u53E3\u6253\u5F00\uFF1A\u547D\u4EE4\u884C\u52A0 --webview-window\uFF0CPlaywright \u7528 test.use({ wxaWebView: 'window' })`
1902
+ );
1903
+ }
1752
1904
  function readableUrl(frame) {
1753
1905
  try {
1754
1906
  return frame.contentWindow?.location.href ?? null;
@@ -2089,15 +2241,15 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2089
2241
  layer.append(mask, sheet);
2090
2242
  container.appendChild(layer);
2091
2243
  columns.forEach((column, index) => column.setItems(options.columns[index], options.current[index] ?? 0));
2092
- const close = (value) => {
2244
+ const close2 = (value) => {
2093
2245
  layer.remove();
2094
2246
  resolve(value);
2095
2247
  };
2096
- cancel.addEventListener("click", () => close(null));
2097
- mask.addEventListener("click", () => close(null));
2248
+ cancel.addEventListener("click", () => close2(null));
2249
+ mask.addEventListener("click", () => close2(null));
2098
2250
  confirm.addEventListener("click", () => {
2099
2251
  columns.forEach((column) => column.settle());
2100
- close(columns.map((column) => column.index));
2252
+ close2(columns.map((column) => column.index));
2101
2253
  });
2102
2254
  return {
2103
2255
  result,
@@ -2435,19 +2587,19 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2435
2587
  modal.appendChild(input);
2436
2588
  }
2437
2589
  const actions = element("div", "wxa-modal-actions");
2438
- const close = (confirm2) => {
2590
+ const close2 = (confirm2) => {
2439
2591
  wrapper.remove();
2440
2592
  resolve({ confirm: confirm2, cancel: !confirm2, ...options.editable ? { content: input.value } : {} });
2441
2593
  };
2442
2594
  if (options.showCancel !== false) {
2443
2595
  const cancel = element("button", "wxa-modal-cancel", options.cancelText || "\u53D6\u6D88");
2444
2596
  cancel.style.color = options.cancelColor || "#000";
2445
- cancel.addEventListener("click", () => close(false));
2597
+ cancel.addEventListener("click", () => close2(false));
2446
2598
  actions.appendChild(cancel);
2447
2599
  }
2448
2600
  const confirm = element("button", "wxa-modal-confirm", options.confirmText || "\u786E\u5B9A");
2449
2601
  confirm.style.color = options.confirmColor || "#576b95";
2450
- confirm.addEventListener("click", () => close(true));
2602
+ confirm.addEventListener("click", () => close2(true));
2451
2603
  actions.appendChild(confirm);
2452
2604
  modal.appendChild(actions);
2453
2605
  wrapper.appendChild(modal);
@@ -2459,7 +2611,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2459
2611
  const wrapper = document.createElement("div");
2460
2612
  const mask = maskElement(true);
2461
2613
  const sheet = element("div", "wxa-actionsheet");
2462
- const close = (index) => {
2614
+ const close2 = (index) => {
2463
2615
  wrapper.remove();
2464
2616
  resolve(index);
2465
2617
  };
@@ -2467,13 +2619,13 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2467
2619
  options.itemList.forEach((text, index) => {
2468
2620
  const button = element("button", "wxa-actionsheet-item", text);
2469
2621
  button.style.color = options.itemColor || "#000";
2470
- button.addEventListener("click", () => close(index));
2622
+ button.addEventListener("click", () => close2(index));
2471
2623
  sheet.appendChild(button);
2472
2624
  });
2473
2625
  const cancel = element("button", "wxa-actionsheet-cancel", "\u53D6\u6D88");
2474
- cancel.addEventListener("click", () => close(null));
2626
+ cancel.addEventListener("click", () => close2(null));
2475
2627
  sheet.appendChild(cancel);
2476
- mask.addEventListener("click", () => close(null));
2628
+ mask.addEventListener("click", () => close2(null));
2477
2629
  wrapper.append(mask, sheet);
2478
2630
  this.overlay.appendChild(wrapper);
2479
2631
  });
@@ -2494,10 +2646,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2494
2646
  notice.setAttribute("role", "alert");
2495
2647
  const text = element("div", "wxa-notice-text");
2496
2648
  text.append(element("div", "wxa-notice-title", title), element("div", "wxa-notice-message", message));
2497
- const close = element("button", "wxa-notice-close", "\xD7");
2498
- close.setAttribute("aria-label", "\u5173\u95ED");
2499
- close.addEventListener("click", () => this.hideNotice());
2500
- notice.append(element("span", "wxa-notice-icon", "!"), text, close);
2649
+ const close2 = element("button", "wxa-notice-close", "\xD7");
2650
+ close2.setAttribute("aria-label", "\u5173\u95ED");
2651
+ close2.addEventListener("click", () => this.hideNotice());
2652
+ notice.append(element("span", "wxa-notice-icon", "!"), text, close2);
2501
2653
  this.stage.appendChild(notice);
2502
2654
  this.noticeEl = notice;
2503
2655
  this.noticeTimer = setTimeout(() => this.hideNotice(), NOTICE_DURATION);
@@ -2553,8 +2705,6 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2553
2705
  this.api = new ApiRegistry((context) => this.reportUnimplemented(context));
2554
2706
  /** setup 和测试 API 声明的请求拦截 */
2555
2707
  this.routes = new RequestRoutes();
2556
- /** 把被拦截的请求显示在 Network 面板里 */
2557
- this.mockMirror = installMockMirror();
2558
2708
  this.mapOperations = new MapOperations((webviewId, event, data) => this.publishToPages(event, data, [webviewId]));
2559
2709
  this.service = null;
2560
2710
  /** 新版基础库的业务子上下文;旧版基础库业务代码直接在逻辑层 iframe 里 */
@@ -2570,6 +2720,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2570
2720
  this.routeListeners = [];
2571
2721
  /** 渲染层报来的 2d、webgl 画布,按编号记下所在页面和最近的位置,供 placeCanvas 认出逻辑层的画布元素 */
2572
2722
  this.canvases = /* @__PURE__ */ new Map();
2723
+ this.mockMirror = installMockMirror(info.externalServiceWorker);
2573
2724
  this.device = info.device;
2574
2725
  this.forwarder = new ConsoleForwarder(
2575
2726
  info.forwardConsole ?? false,
@@ -2580,6 +2731,11 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2580
2731
  this.shell = new PhoneShell(root, info.device);
2581
2732
  this.tabPaths = new Set((info.wxConfig.tabBar?.list || []).map((item) => stripHtml(item.pagePath)));
2582
2733
  this.storage = new MiniProgramStorage(window.localStorage, info.appid);
2734
+ this.webViewWindows = info.webViewWindow ? new WebViewWindows(info.device, (webviewId) => this.currentPage()?.id === webviewId) : null;
2735
+ if (this.webViewWindows) {
2736
+ const windows = this.webViewWindows;
2737
+ window.addEventListener("pagehide", () => windows.closeAll());
2738
+ }
2583
2739
  const tabBar = info.wxConfig.tabBar ? { ...info.wxConfig.tabBar, list: info.wxConfig.tabBar.list.map((item) => ({ ...item, pagePath: stripHtml(item.pagePath) })) } : void 0;
2584
2740
  this.shell.setTabBar(tabBar, (pagePath) => this.enqueueRoute(() => this.switchTab(pagePath)));
2585
2741
  this.shell.onBack(() => this.enqueueRoute(() => this.navigateBack(1)));
@@ -2849,11 +3005,12 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2849
3005
  }
2850
3006
  closePage(page, options = { keepTab: true }) {
2851
3007
  if (options.keepTab && this.tabPaths.has(page.path) && this.tabPages.get(page.path) === page) {
2852
- page.hide();
3008
+ this.hidePage(page);
2853
3009
  return;
2854
3010
  }
2855
3011
  if (this.tabPages.get(page.path) === page) this.tabPages.delete(page.path);
2856
3012
  this.pages.delete(page.id);
3013
+ this.webViewWindows?.pageDestroyed(page.id);
2857
3014
  page.destroy();
2858
3015
  for (const [canvasNumber, record] of this.canvases) {
2859
3016
  if (record.webviewId === page.id) this.canvases.delete(canvasNumber);
@@ -2861,9 +3018,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2861
3018
  }
2862
3019
  activate(page, openType) {
2863
3020
  for (const other of this.pages.values()) {
2864
- if (other !== page) other.hide();
3021
+ if (other !== page) this.hidePage(other);
2865
3022
  }
2866
3023
  page.show();
3024
+ this.webViewWindows?.pageShown(page.id);
2867
3025
  page.navigation = { ...page.navigation, showBack: this.stack.size > 1 };
2868
3026
  this.shell.setNavigation(page.navigation);
2869
3027
  this.shell.renderTabBar(this.tabPaths.has(page.path) ? page.path : null);
@@ -2876,6 +3034,14 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2876
3034
  }, ROUTE_DONE_DELAY);
2877
3035
  this.routeListeners.forEach((listener) => listener(page, openType));
2878
3036
  }
3037
+ hidePage(page) {
3038
+ page.hide();
3039
+ this.webViewWindows?.pageHidden(page.id);
3040
+ }
3041
+ /** 页面里设置过 src 的 web-view,按创建顺序 */
3042
+ webViewsOf(page) {
3043
+ return this.webViewWindows ? this.webViewWindows.list(page.id) : iframeWebViews(page.win?.document);
3044
+ }
2879
3045
  /** 宿主地址栏同步为当前页面,刷新后可回到同一页面 */
2880
3046
  updateLocation(page) {
2881
3047
  const params = new URLSearchParams(location.search);
@@ -2996,7 +3162,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2996
3162
  }),
2997
3163
  ...createWebViewApis({
2998
3164
  documentOf: (webviewId) => this.pages.get(webviewId)?.win?.document,
2999
- emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data)
3165
+ emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data),
3166
+ windows: this.webViewWindows
3000
3167
  }),
3001
3168
  ...createMiscApis(this.info.appid, {
3002
3169
  devtoolsLogin: this.info.devtoolsLogin,
@@ -3355,6 +3522,10 @@ ${script}
3355
3522
  ready,
3356
3523
  currentPage: () => snapshot(host.currentPage()),
3357
3524
  pageStack: () => host.pageStack().map((page) => snapshot(page)),
3525
+ webViews: () => {
3526
+ const page = host.currentPage();
3527
+ return page ? host.webViewsOf(page) : [];
3528
+ },
3358
3529
  waitForPage(path, timeout = 1e4) {
3359
3530
  const target = path.replace(/^\//, "");
3360
3531
  return new Promise((resolve, reject) => {
@@ -3418,13 +3589,14 @@ ${script}
3418
3589
  const root = document.getElementById("wxa-root");
3419
3590
  const info = await (await fetch("/__wxa__/app-info.json")).json();
3420
3591
  const restore = new PageStackRestore(info.appid);
3421
- const sendReport = connectDevServer(restore);
3592
+ const sendReport = info.liveReload === false ? () => void 0 : connectDevServer(restore);
3422
3593
  if (info.buildError) {
3423
3594
  new PhoneShell(root, info.device).showError(`\u7F16\u8BD1\u5931\u8D25
3424
3595
 
3425
3596
  ${info.buildError}`);
3426
3597
  return;
3427
3598
  }
3599
+ if (window.__wxa_webview_window__ === true) info.webViewWindow = true;
3428
3600
  const host = new Host(info, root, sendReport);
3429
3601
  window.__wxaHost = host;
3430
3602
  let markReady;