上回談到翻修 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 化」推進!

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


61 - 1 =