iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 16
0

大家好,我是韋恩,今天是第十六天,我們會繼續了解如何在VSCode使用Webview與一些相關API原理。

在WebView裡使用Javascript


在昨天的練習裡,我們開啟了一個WebView,並且了解WebViewPanel的狀態屬性與生命週期方法。

但如果我們嘗試著直接在webview.html裡的script中直接操作document物件,並不會有任何作用,以下面為例:

<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Webview Practice</title>
</head>

<body>
 <h1 style="color: white;">Hello Webview!</h1>
 <img src="https://i.imgur.com/hzK0JDS.jpg" width="50%" />
 <script>
  const $h1 = document.querySelector('h1');
  $h1.innerHTML = 'Hello Webview and Javascript';
 </script>
</body>

</html>

原本打開Webview後標題應該被替換並顯示Hello Webview and Javascript,但結果仍為最初在Body下寫死的標題Hello Webview

為什麼呢?原來,Webview的設計考量到了Extension使用者的安全性。在VSCode裡,Webview預設是不能使用Javascript並載入本地資源的,這一切都要手動啟用。為的是當我們不需要這些功能時,就盡量讓我們不啟用這些相關功能,以免新增加被攻擊的可能性與漏洞。

現在我們要在Webview裡啟用Javascript,就需要在建立WebviewPanel時啟用enableScripts: true設定,如下所示:

const webviewPanel = vscode.window.createWebviewPanel(
    'webviewId', 
    'WebView Title', 
    vscode.ViewColumn.One, 
    { 
      enableScripts: true
    }
);

好的,啟用script後讓我們執行extension打開webview看看,可以看到原本script的邏輯已經順利運作,並將標題置換,如下所示:

讓WebView與Extension通信


當我們在使用Webview的Javascript時,WebView裡的Javascript並不是跟我們extension.ts編譯過後的javascript運行在同一條Thead上。WebView裡的Javascript運行在一個完全獨立的環境,只能夠訪問在WebView的script裡面的變數。但在實際使用Webview的程式時,很多時候我們要讓extension程式將vscode的狀態或資料與webview溝通,這時候,我們可以使用postMessage這個api傳送訊息,並通過相關監聽message的方法接收消息,如同我們常用的iframe。

跟我們在使用Webview的情況一樣,iframe裡的javascript,一樣沒辦法被外部使用他的javascript訪問,這時候在iframe中我們會使用兩個方法來坐傳接訊息:


// 傳送訊息給外部Js,也可傳送字串等基本型別至外部
const yourMessageData = {
   ...
};
window.parent.postMessage(yourMessageData);

// 接收外部傳來的訊息
window.addEventListener('message', (e) => {
    const receiveData = e.data;
    ...
});

實際上,VSCode Extension運作的行為與iframe相同,因此在WebView裡面,我們一樣可以通過以下方式傳接:


  const vscode = acquireVsCodeApi();

  // 傳送訊息給Extension的Js
  vscode.postMessage({
    ...
  });

  // 接收Extension傳來的訊息
  window.addEventListener('message', (e) => {
    const receiveData = e.data;
    ...
  });

從上面範例可以看到,我們在WebView裡可以特別使用acquireVsCodeApi這個方法來建立物件,並使用物件下面的postMessage方法,如同在iframe裡一樣。這個acquireVsCodeApi只能被webview調用一次建立物件,若再呼叫acquireVsCodeApi方法一次即會出錯,但已經建立後的物件可以讓我們重複呼叫postMessage方法。讀者此處需留意。

接收外部訊息的地方則一模一樣,讓我們在了解extension中如何與Webview傳接訊息:


webviewPanel = vscode.window.createWebviewPanel(
				'webviewId',
				'WebView Title',
				vscode.ViewColumn.One,
				{
					enableScripts: true
				}
			);

...

// 傳送訊息給Webview
webviewPanel.webview.postMessage({...});

// 接收Webview傳來的訊息
webviewPanel.webview.onDidReceiveMessage((e) => {
    ...
});

使用Devtool來偵錯WebView


在一般的前端應用程式裡,我們會使用console.log和中斷點來debug。對於extension.ts檔,vscode提供了我們完善的debug支援,我們可以在debug console檢視log輸出的內容,同時可以直接在VSCode裡下中斷點進行偵錯。對於WebView裡面的Javascript,我們有什麼辦法做到跟上面一樣的事情呢?

VSCode為我們提供了WebView Developer Tool,讓我們可以在這個專用的的devtool裡偵測Webview iframe裡面的javascript、html和css。

好的,那我們如何開啟WebView的devtool呢?讓我們打開Command Palette,輸入Developer: Developer tools,會顯示兩個可開啟devtool的命令: Toggle Developer Tools 與 Open WebView Developer Tools。

Toggle Developer Tools命令會開啟VSCode整個Editor的chrome devtool,無法在這裡檢視webview的內容。這裡我們選擇Developer: Open WebView Developer Tools命令,可以打開WebView的專用開發工具。

現在,我們就可以愉快的在devtool裡debug我們的樣式以及邏輯了。

WebView主題樣式


剛才我們打開了WebView Devtool。現在,讓我們查看下element,點開iframe標籤,檢視實際在WebView裡面讀取的html結構。

一打開我們就發現WebView,不只呈現了我們給定的html頁面,在html開頭的style標籤裡還插入了一堆css變數,這些變數是做什麼用的呢?原來,VSCode針對WebView的開發者,提供了一系列跟當前VSCode主題顏色對應的css變數,讓我們可以直接在WebView裡使用,以讓我們的WebView元件能夠和VSCode保持設計上的一致性。

好的,那css變數應該怎麼使用呢?讓我們繼續檢視devtool裡面的html,在style標籤下面的head標籤裡,我們可以看到。

裡面被塞入了_defaultStyles_vscodeApiScript,讓我們先來檢視_defaultStyles

可以看到,我們的vscode的body已經預設使用css變數定義好了跟VSCode編輯器主題一致的字體等設定,並預先將html的一些標籤元素定義好了跟VSCode編輯器一致的樣式。

因此對於一些標籤或字體,我們可以不必再設置大小和相關樣式。

對於有需要使用css變數的元素,我們可以參考VSCode文件的Theme Color一節,並檢視在WebView style裡對應的各種變數,做個別配置。以checkbox元件為例,我們就可以透過devtool找到對應的變數,並針對我們的元件做如下設定:

.checkbox {
    ...
    background: var(--vscode-checkbox-background); 
    border: var(--vscode-checkbox-border);
    ...
}

AcquireVsCodeApi函數設計解析


剛才我們打開Webview時,除了_defaultStyles,還有看到Webview內的head標籤內有一個id為_vscodeApiScript的script標籤,這個標籤裡面的script做了什麼事情呢?讓我們打開script看看:

打開後可以看到,vscode在script標籤下執行了一個匿名的立即執行函數(Immediately Invoked Functions Expressions,簡稱為IIFE),並將一個匿名的函數(Anonymous Function)指派給acquireVsCodeApi這個變數。在js裡,函數是一等公民,因此可以將一個function作為變數來儲存。當我們要用這個函數時,只需要呼叫執行這個變數即可得到function的回傳值,如我們之前的方式。

同時,函數也可以作為回傳值return給acquireVsCodeApi這個變數,因為函數是一等公民的特性,這種function又稱為High Order Function。

acquireVsCodeApi這個函數會回傳使用Object.freeze過的物件,使該物件無法增加或刪除裡面的屬性。

因此若有人要將acquireVsCodeApi回傳的物件下的方法替換或刪除,如下:

const const vscode = acquireVsCodeApi();

obj.postMessage = () => 'external function result';
delete vscode.postMessage

均不會起任何作用。

然後,回傳給acquireVsCodeApi的HOC匿名函數,在呼叫的時候,會在回傳物件前將IIFE函數裡的acquire變數設為true。並設定如果在acquire為真的情況下再次執行這個acquire方法,即會拋出'An instance of the VS Code API has already been acquired'的錯誤。

這即是Javascript閉包的經典運用,透過HOF與在立即函數裡面的變數,我們可以讓我們回傳的function跟class與物件一樣具有狀態

在acquireVsCodeApi的function裡,只要acquireVsCodeApi一被呼叫,便會讓原本的IIFE函數變成acquire過的狀態,並限制只能被使用一次。同時,當object內的setState方法呼叫時,IIFE裡的state變數即會被設置為傳入的新的狀態,並可以透過getState方法取得狀態。

{
    ...
    setState: function(newState) {
        state = newState;
        originalPostMessage({ 
            command: 'do-update-state', data: JSON.stringify(newState) 
        }, targetOrigin);
        return newState;
    },
    getState: function() {
        return state;
    }
}

最後,用於從WebView傳送資料至extension的postMessage,我們可以看到是保存了原webview.parent下面的postMessage方法。並藉由bind方法將該方法的context,也就是this指向綁定原本的window.parent。acquireVsCodeApi因此得以會使用原本iframe跟外部網頁相同的api與extension溝通。

const originalPostMessage = window.parent.postMessage.bind(window.parent);

最後,在宣告acquireVsCodeApi這個function變數後,WebView裡面的script會直接刪除window.parent、window.top與window.frameElement,讓第三方程式無法使用這些物件下面的api,藉以保護VSCode的安全。

結語


好啦,今天,我們對WebView操作javascript有了更多了解,並知道怎麼讓webview裡的js跟extension溝通與更多的相關知識。

明天會繼續VSCode Webview元件的介紹與實際練習,我們明天見,謝謝大家。

本日參考文件



上一篇
Day15 | Webview API (一)
下一篇
Day17 | WebView API (三)
系列文
自己用的工具自己做! 30天玩轉VS Code Extension之旅36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言