@open-wxa/wxa-runner-core 0.11.0 → 0.12.1

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.
@@ -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,194 @@ ${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 COMMIT_WAIT_MS = 5e3;
1773
+ var WebViewWindows = class {
1774
+ /** @param isShown 页面当前是否显示:隐藏的页面先记下地址,显示时再打开窗口 */
1775
+ constructor(device, isShown) {
1776
+ this.device = device;
1777
+ this.isShown = isShown;
1778
+ this.records = /* @__PURE__ */ new Map();
1779
+ }
1780
+ insert(webviewId, htmlId, doc, position) {
1781
+ const previous = this.records.get(windowName(webviewId, htmlId));
1782
+ previous?.placeholder.remove();
1783
+ const placeholder = doc.createElement("div");
1784
+ placeholder.dataset.wxaHtmlId = String(htmlId);
1785
+ 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";
1786
+ const status = doc.createElement("div");
1787
+ const button = doc.createElement("button");
1788
+ button.textContent = "\u5728\u65B0\u7A97\u53E3\u6253\u5F00 H5";
1789
+ button.style.cssText = "padding:8px 16px;border:0;border-radius:4px;background:#07c160;color:#fff;font-size:14px";
1790
+ placeholder.append(status, button);
1791
+ place(placeholder, position);
1792
+ doc.body.appendChild(placeholder);
1793
+ const record = { webviewId, name: windowName(webviewId, htmlId), placeholder, status, src: previous?.src, win: previous?.win ?? null };
1794
+ this.records.set(record.name, record);
1795
+ button.addEventListener("click", () => {
1796
+ if (record.win && !record.win.closed) record.win.focus();
1797
+ else this.open(record);
1798
+ });
1799
+ this.render(record);
1800
+ }
1801
+ /** @returns 找不到这个 web-view 时为 false */
1802
+ update(webviewId, htmlId, position, src) {
1803
+ const record = this.records.get(windowName(webviewId, htmlId));
1804
+ if (!record) return false;
1805
+ place(record.placeholder, position);
1806
+ if (src && src !== record.src) {
1807
+ record.src = src;
1808
+ if (this.isShown(webviewId)) this.open(record);
1809
+ else this.render(record);
1810
+ }
1811
+ return true;
1812
+ }
1813
+ remove(webviewId, htmlId) {
1814
+ const record = this.records.get(windowName(webviewId, htmlId));
1815
+ if (!record) return;
1816
+ this.records.delete(record.name);
1817
+ close(record);
1818
+ record.placeholder.remove();
1819
+ }
1820
+ pageShown(webviewId) {
1821
+ for (const record of this.recordsOf(webviewId)) {
1822
+ if (record.src && !(record.win && !record.win.closed)) this.open(record);
1823
+ }
1824
+ }
1825
+ pageHidden(webviewId) {
1826
+ for (const record of this.recordsOf(webviewId)) {
1827
+ close(record);
1828
+ this.render(record);
1829
+ }
1830
+ }
1831
+ /** 页面关闭:渲染层已经或即将被删除,不必再动占位 */
1832
+ pageDestroyed(webviewId) {
1833
+ for (const record of this.recordsOf(webviewId)) {
1834
+ this.records.delete(record.name);
1835
+ close(record);
1836
+ }
1837
+ }
1838
+ closeAll() {
1839
+ for (const record of this.records.values()) close(record);
1840
+ }
1841
+ /** 页面里的 web-view,按创建顺序;还没设置 src 的不算 */
1842
+ list(webviewId) {
1843
+ return this.recordsOf(webviewId).flatMap((record) => record.src ? [{ src: record.src, windowName: record.name }] : []);
1844
+ }
1845
+ recordsOf(webviewId) {
1846
+ return [...this.records.values()].filter((record) => record.webviewId === webviewId);
1847
+ }
1848
+ open(record) {
1849
+ if (!record.src) return;
1850
+ if (record.win && !record.win.closed) {
1851
+ this.navigate(record);
1852
+ } else {
1853
+ const features = `popup,width=${this.device.screenWidth},height=${this.device.screenHeight}`;
1854
+ record.win = window.open(record.src, record.name, features);
1855
+ if (record.win) {
1856
+ record.opening = { src: record.src, at: Date.now() };
1857
+ notify("open", record.name, record.src);
1858
+ }
1859
+ }
1860
+ this.render(record);
1861
+ }
1862
+ /**
1863
+ * 已打开的窗口跳到最新的 src。用已有的引用跳转,不靠窗口名:网页可能改了自己的 window.name,按名字找不到时会多开一个窗口。
1864
+ *
1865
+ * Chrome 里新窗口的初始页面还没提交时改它的地址,这次跳转偶尔会被丢掉,窗口停在原来的网页上(不经过 CDP 也一样)。
1866
+ * 窗口刚打开就改 src 会碰上,所以等初始页面提交后再跳
1867
+ */
1868
+ navigate(record) {
1869
+ const win = record.win;
1870
+ if (!win || win.closed || !record.src || record.retry !== void 0) return;
1871
+ if (record.opening && !committed(win, record.opening.src) && Date.now() - record.opening.at < COMMIT_WAIT_MS) {
1872
+ record.retry = window.setTimeout(() => {
1873
+ record.retry = void 0;
1874
+ this.navigate(record);
1875
+ }, 20);
1876
+ return;
1877
+ }
1878
+ record.opening = void 0;
1879
+ win.location.href = record.src;
1880
+ }
1881
+ render(record) {
1882
+ const opened = record.win && !record.win.closed;
1883
+ 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];
1884
+ record.status.textContent = "";
1885
+ const heading = record.status.ownerDocument.createElement("div");
1886
+ heading.textContent = title;
1887
+ heading.style.cssText = "color:#333;font-size:16px";
1888
+ record.status.append(heading);
1889
+ if (detail) record.status.append(detail);
1890
+ }
1891
+ };
1892
+ function windowName(webviewId, htmlId) {
1893
+ return `wxa-h5-${webviewId}-${htmlId}`;
1894
+ }
1895
+ function committed(win, src) {
1896
+ try {
1897
+ return win.location.href !== "about:blank" || src === "about:blank";
1898
+ } catch {
1899
+ return true;
1900
+ }
1901
+ }
1902
+ function close(record) {
1903
+ clearTimeout(record.retry);
1904
+ record.retry = void 0;
1905
+ record.opening = void 0;
1906
+ if (!record.win) return;
1907
+ record.win.close();
1908
+ record.win = null;
1909
+ notify("close", record.name);
1910
+ }
1911
+ function notify(type, name, src) {
1912
+ const listener = window.__wxa_webview_window_event__;
1913
+ listener?.(type, name, src)?.catch(() => void 0);
1914
+ }
1915
+ function place(element2, position) {
1916
+ if (!position) return;
1917
+ for (const side of ["left", "top", "width", "height"]) {
1918
+ if (typeof position[side] === "number") element2.style[side] = `${position[side]}px`;
1919
+ }
1920
+ }
1921
+ var warnedOrigins = /* @__PURE__ */ new Set();
1922
+ function warnCrossSite(src) {
1923
+ let url;
1924
+ try {
1925
+ url = new URL(src, location.href);
1926
+ } catch {
1927
+ return;
1928
+ }
1929
+ if (!["http:", "https:"].includes(url.protocol)) return;
1930
+ if (url.hostname === location.hostname && url.protocol === location.protocol) return;
1931
+ if (warnedOrigins.has(url.origin)) return;
1932
+ warnedOrigins.add(url.origin);
1933
+ console.warn(
1934
+ `[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' })`
1935
+ );
1936
+ }
1752
1937
  function readableUrl(frame) {
1753
1938
  try {
1754
1939
  return frame.contentWindow?.location.href ?? null;
@@ -2089,15 +2274,15 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2089
2274
  layer.append(mask, sheet);
2090
2275
  container.appendChild(layer);
2091
2276
  columns.forEach((column, index) => column.setItems(options.columns[index], options.current[index] ?? 0));
2092
- const close = (value) => {
2277
+ const close2 = (value) => {
2093
2278
  layer.remove();
2094
2279
  resolve(value);
2095
2280
  };
2096
- cancel.addEventListener("click", () => close(null));
2097
- mask.addEventListener("click", () => close(null));
2281
+ cancel.addEventListener("click", () => close2(null));
2282
+ mask.addEventListener("click", () => close2(null));
2098
2283
  confirm.addEventListener("click", () => {
2099
2284
  columns.forEach((column) => column.settle());
2100
- close(columns.map((column) => column.index));
2285
+ close2(columns.map((column) => column.index));
2101
2286
  });
2102
2287
  return {
2103
2288
  result,
@@ -2435,19 +2620,19 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2435
2620
  modal.appendChild(input);
2436
2621
  }
2437
2622
  const actions = element("div", "wxa-modal-actions");
2438
- const close = (confirm2) => {
2623
+ const close2 = (confirm2) => {
2439
2624
  wrapper.remove();
2440
2625
  resolve({ confirm: confirm2, cancel: !confirm2, ...options.editable ? { content: input.value } : {} });
2441
2626
  };
2442
2627
  if (options.showCancel !== false) {
2443
2628
  const cancel = element("button", "wxa-modal-cancel", options.cancelText || "\u53D6\u6D88");
2444
2629
  cancel.style.color = options.cancelColor || "#000";
2445
- cancel.addEventListener("click", () => close(false));
2630
+ cancel.addEventListener("click", () => close2(false));
2446
2631
  actions.appendChild(cancel);
2447
2632
  }
2448
2633
  const confirm = element("button", "wxa-modal-confirm", options.confirmText || "\u786E\u5B9A");
2449
2634
  confirm.style.color = options.confirmColor || "#576b95";
2450
- confirm.addEventListener("click", () => close(true));
2635
+ confirm.addEventListener("click", () => close2(true));
2451
2636
  actions.appendChild(confirm);
2452
2637
  modal.appendChild(actions);
2453
2638
  wrapper.appendChild(modal);
@@ -2459,7 +2644,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2459
2644
  const wrapper = document.createElement("div");
2460
2645
  const mask = maskElement(true);
2461
2646
  const sheet = element("div", "wxa-actionsheet");
2462
- const close = (index) => {
2647
+ const close2 = (index) => {
2463
2648
  wrapper.remove();
2464
2649
  resolve(index);
2465
2650
  };
@@ -2467,13 +2652,13 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2467
2652
  options.itemList.forEach((text, index) => {
2468
2653
  const button = element("button", "wxa-actionsheet-item", text);
2469
2654
  button.style.color = options.itemColor || "#000";
2470
- button.addEventListener("click", () => close(index));
2655
+ button.addEventListener("click", () => close2(index));
2471
2656
  sheet.appendChild(button);
2472
2657
  });
2473
2658
  const cancel = element("button", "wxa-actionsheet-cancel", "\u53D6\u6D88");
2474
- cancel.addEventListener("click", () => close(null));
2659
+ cancel.addEventListener("click", () => close2(null));
2475
2660
  sheet.appendChild(cancel);
2476
- mask.addEventListener("click", () => close(null));
2661
+ mask.addEventListener("click", () => close2(null));
2477
2662
  wrapper.append(mask, sheet);
2478
2663
  this.overlay.appendChild(wrapper);
2479
2664
  });
@@ -2494,10 +2679,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2494
2679
  notice.setAttribute("role", "alert");
2495
2680
  const text = element("div", "wxa-notice-text");
2496
2681
  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);
2682
+ const close2 = element("button", "wxa-notice-close", "\xD7");
2683
+ close2.setAttribute("aria-label", "\u5173\u95ED");
2684
+ close2.addEventListener("click", () => this.hideNotice());
2685
+ notice.append(element("span", "wxa-notice-icon", "!"), text, close2);
2501
2686
  this.stage.appendChild(notice);
2502
2687
  this.noticeEl = notice;
2503
2688
  this.noticeTimer = setTimeout(() => this.hideNotice(), NOTICE_DURATION);
@@ -2579,6 +2764,11 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2579
2764
  this.shell = new PhoneShell(root, info.device);
2580
2765
  this.tabPaths = new Set((info.wxConfig.tabBar?.list || []).map((item) => stripHtml(item.pagePath)));
2581
2766
  this.storage = new MiniProgramStorage(window.localStorage, info.appid);
2767
+ this.webViewWindows = info.webViewWindow ? new WebViewWindows(info.device, (webviewId) => this.currentPage()?.id === webviewId) : null;
2768
+ if (this.webViewWindows) {
2769
+ const windows = this.webViewWindows;
2770
+ window.addEventListener("pagehide", () => windows.closeAll());
2771
+ }
2582
2772
  const tabBar = info.wxConfig.tabBar ? { ...info.wxConfig.tabBar, list: info.wxConfig.tabBar.list.map((item) => ({ ...item, pagePath: stripHtml(item.pagePath) })) } : void 0;
2583
2773
  this.shell.setTabBar(tabBar, (pagePath) => this.enqueueRoute(() => this.switchTab(pagePath)));
2584
2774
  this.shell.onBack(() => this.enqueueRoute(() => this.navigateBack(1)));
@@ -2848,11 +3038,12 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2848
3038
  }
2849
3039
  closePage(page, options = { keepTab: true }) {
2850
3040
  if (options.keepTab && this.tabPaths.has(page.path) && this.tabPages.get(page.path) === page) {
2851
- page.hide();
3041
+ this.hidePage(page);
2852
3042
  return;
2853
3043
  }
2854
3044
  if (this.tabPages.get(page.path) === page) this.tabPages.delete(page.path);
2855
3045
  this.pages.delete(page.id);
3046
+ this.webViewWindows?.pageDestroyed(page.id);
2856
3047
  page.destroy();
2857
3048
  for (const [canvasNumber, record] of this.canvases) {
2858
3049
  if (record.webviewId === page.id) this.canvases.delete(canvasNumber);
@@ -2860,9 +3051,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2860
3051
  }
2861
3052
  activate(page, openType) {
2862
3053
  for (const other of this.pages.values()) {
2863
- if (other !== page) other.hide();
3054
+ if (other !== page) this.hidePage(other);
2864
3055
  }
2865
3056
  page.show();
3057
+ this.webViewWindows?.pageShown(page.id);
2866
3058
  page.navigation = { ...page.navigation, showBack: this.stack.size > 1 };
2867
3059
  this.shell.setNavigation(page.navigation);
2868
3060
  this.shell.renderTabBar(this.tabPaths.has(page.path) ? page.path : null);
@@ -2875,6 +3067,14 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2875
3067
  }, ROUTE_DONE_DELAY);
2876
3068
  this.routeListeners.forEach((listener) => listener(page, openType));
2877
3069
  }
3070
+ hidePage(page) {
3071
+ page.hide();
3072
+ this.webViewWindows?.pageHidden(page.id);
3073
+ }
3074
+ /** 页面里设置过 src 的 web-view,按创建顺序 */
3075
+ webViewsOf(page) {
3076
+ return this.webViewWindows ? this.webViewWindows.list(page.id) : iframeWebViews(page.win?.document);
3077
+ }
2878
3078
  /** 宿主地址栏同步为当前页面,刷新后可回到同一页面 */
2879
3079
  updateLocation(page) {
2880
3080
  const params = new URLSearchParams(location.search);
@@ -2995,7 +3195,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2995
3195
  }),
2996
3196
  ...createWebViewApis({
2997
3197
  documentOf: (webviewId) => this.pages.get(webviewId)?.win?.document,
2998
- emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data)
3198
+ emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data),
3199
+ windows: this.webViewWindows
2999
3200
  }),
3000
3201
  ...createMiscApis(this.info.appid, {
3001
3202
  devtoolsLogin: this.info.devtoolsLogin,
@@ -3354,6 +3555,10 @@ ${script}
3354
3555
  ready,
3355
3556
  currentPage: () => snapshot(host.currentPage()),
3356
3557
  pageStack: () => host.pageStack().map((page) => snapshot(page)),
3558
+ webViews: () => {
3559
+ const page = host.currentPage();
3560
+ return page ? host.webViewsOf(page) : [];
3561
+ },
3357
3562
  waitForPage(path, timeout = 1e4) {
3358
3563
  const target = path.replace(/^\//, "");
3359
3564
  return new Promise((resolve, reject) => {
@@ -3424,6 +3629,7 @@ ${script}
3424
3629
  ${info.buildError}`);
3425
3630
  return;
3426
3631
  }
3632
+ if (window.__wxa_webview_window__ === true) info.webViewWindow = true;
3427
3633
  const host = new Host(info, root, sendReport);
3428
3634
  window.__wxaHost = host;
3429
3635
  let markReady;