上回談到翻修 IE Only 網頁支援 Chrome/Edge 時汰換 showModalDialog()的方法,當時想到的解決方案是用 <iframe > 內嵌網頁取代。但使用一陣子,發現它無法 100% 取代 showModalDialog(),某些外站台網址原本用 showModalDialog() 沒問題,改用 iframe 後無法顯示。原因出在隨著資安政策趨嚴,愈來愈多的網站會透過 X-Frame-Options、CSP frame-ancestors 全面禁止或限定自家網站內嵌。showModalDialog 類似開新視窗不受此限,改用 iframe 則會踩到禁忌。

這類 showModalDialog() 開啟跨站台網址的情境,通常只是單純開啟網頁並等待關閉,受限同源限制多半不需要 JavaScript 互動,或是已有其他解法處理跨站台存取問題,理論上我們可用 window.open() 取代,但有幾個問題要解決:

  1. 開啟參數要加註 width、height,以明確開成新視窗而非開在新頁籤。(笨問題 - window.open() 有時在新頁籤有時在新視窗開啟)
  2. 需偵測新開視窗是否關閉,以便在關閉時觸發原本 showModalDialog() 之後的執行邏輯。我發現 Web API 有個 window.closed 能反映視窗開啟或關閉狀態,並且不受同源政策限制。
  3. 如開啟新視窗後要禁止原網頁操作,需加上遮罩。

想好點子,要寫出來就不算難事,範例程式如下:線上展示

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>window.open example</title>
</head>
<body>
    <button onclick="test1()">Open (Full)</button>
    <button onclick="test2()">Open (Modal)</button>
    <pre id=m></pre>
    <script>
        function test1() {
            openUrlInNewWindow("https://blog.darkthread.net").then(() => {
                document.getElementById('m').innerText += 'Window Closed\n';
            });

        }
        function test2() {
            openUrlInNewWindow("https://blog.darkthread.net", 640, 480, true).then(() => {
                document.getElementById('m').innerText += 'Window Closed\n';
            });

        }        
        function openUrlInNewWindow(url, width, height, mask) {
            let maskLayer = null;
            if (mask) {
                maskLayer = document.createElement('div');
                maskLayer.style.position = 'absolute';
                maskLayer.style.zIndex = 65535;
                maskLayer.style.opacity = 0.5;
                maskLayer.style.top = 0;
                maskLayer.style.left = 0;
                maskLayer.style.width = '100vw';
                maskLayer.style.height = '100vh';
                maskLayer.style.backgroundColor = '#444';
                document.body.appendChild(maskLayer);
            }
            let promise = new Promise((resolve, reject) => {
                let w = Math.round(width || window.innerWidth * 0.99);
                let h = Math.round(height || window.innerHeight * 0.99);
                let newWin = window.open(url, '_blank', 'width=' + w + ',height=' + h + ',top=10,left=10');
                let hnd = setInterval(function () {
                    try {
                        if (newWin.closed) {
                            clearInterval(hnd);
                            resolve();
                            maskLayer && maskLayer.remove();
                        }
                    }
                    catch {
                        clearInterval(hnd);
                        reject();
                        maskLayer && maskLayer.remove();
                    }
                }, 100);
            });
            return promise;
        }
    </script>
</body>
</html>

我寫了一個 openUrlInNewWindow() 可傳入 string url, int width, int height, bool mask 四個參數,未指定 width/height 時會抓比原視窗略小的寬高,mask 則控制是否要加上禁止操作遮罩。函式會回傳 Promise(),以 setInterval 方式偵測視窗是否關閉,關閉後要執行動作可寫在 Promise.then()。實測結果如下:

(補充:昨天分享用 ASP.NET Core Minmal API 寫的目錄轉網站小工具,有讀者問到何不用 Cloudflare Tunnelngrok?今天剛好展示到 dnFileWeb 的長處,我的 Windows Sandbox 網路有點問題,沒有 Internet 連線,我覺得連網路都隔離挺好,故意放著不修,dnFileWeb 在這種情境下也能順利掛成 localhost 網站。)

以上效果已蠻接近原本 showModalDialog() 的行為,繼續朝「去 IE 化」推進!

2022-04-19 更新,請參考最終版本

Example of how to use window.open() replace showModalDialog() cross-site url.


Comments

# by Larry

請問大大此方法可由母視窗傳變數到新開之視窗嗎?

# by Jeffrey

to Larry, 新視窗若與母視窗同網站,可透過 JavaScript 存取對方 DOM (window.open(...).document...、window.opener.document....);若跨站台時,最簡單做法是在 URL 用 ?a=..&b=.. 傳參數,或用 postMessage 雙方溝通 https://blog.darkthread.net/blog/window-postmessage/

Post a comment