大家好,我是韋恩,今天是第十六天,我們會繼續了解如何在VSCode使用Webview與一些相關API原理。
在昨天的練習裡,我們開啟了一個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的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) => {
...
});
在一般的前端應用程式裡,我們會使用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 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);
...
}
剛才我們打開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元件的介紹與實際練習,我們明天見,謝謝大家。