大家好,我是韋恩,今天是第十七天,我們會繼續了解如何在VSCode使用Webview。
今天,我們會來學習如何載入本地資源與Javascript,這些教學是幫助我們進一步整合當前流行的SPA前端框架至WebView的基礎。
在前面的文章裡,我們已經了解怎麼在WebView裡使用Javascript,但在實務上,通常我們不會將Javascript跟Html混在一起寫,style也會跟javascript一起抽出去成為獨立的檔案。在關注點分離
的狀況下,專案整體可以更好維護,也更好擴充。
現在我們就將script檔案從webview.html
抽出,原本的html改為:
<!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 src="./webview.js"></script>
</body>
</html>
抽出去的webview.js
檔內容則是
const $h1 = document.querySelector('h1');
$h1.innerHTML = 'Hello Webview and Javascript!';
現在我們的檔案結構如下:
接著我們調整前面範例的extension.ts
裡的載入html路徑:
webviewPanel.webview.html = loadWebView("./webview/webview.html");
執行extension,會發現script調整過路徑後再也不起作用。
為什麼呢?原來,VSCode基於安全的考量,限制webview裡的程式無法直接存取本地資源(Local Resouces),除非使用vscode自己的webviewUri讀取。因此不管是圖片、icon還是style或script檔案,都沒有辦法像原本的前端直接使用相對路徑。
為此,我們在extension這一端,就需要先使用VSCode的Uri方法,將原本需載入路徑的字串轉成VSCode.Uri物件。
const vsUri = vscode.Uri.parse(
path.join(context.extensionPath, 'src/webview/webview.js')
);
這個Uri物件的格式為:
{
$mid:1
path:"${yourRootPath}/day17-webview-spa-practice/src/webview/webview.js"
scheme:"file"
}
但有了VSCode的Uri物件還不夠,我們還需要再提供Uri物件給webview物件,轉換成webviewUri,如下所示:
const webviewUri = webviewPanel.webview.asWebviewUri(vsUri);
這個webviewUri一樣是個vscode.Uri物件,內容格式如下:
{
$mid:1
path:"/file///${yourRootPath}/day17-webview-spa-practice/src/webview/webview.js"
scheme:"vscode-webview-resource"
authority:"${authorityUrl}" // 此處筆者的url為bc1d4f5e-6f25-4f26-89b8-9deaf9b75116
}
可以看到轉換後的uri的schema型態已經改為vscode-webview-resource
,並且指定了一個authority的編號屬性。
如果我們於webview.js多加一段方法監聽extension傳來的訊息
window.addEventListener('message', (e) => { console.log(e.origin); });
並在extension部分調用postMessage傳任意訊息
則可以透過e.origin
讀取到origin的url為vscode-webview://${authorityUrl}
。
好的,現在我們就可以將script的url source指定給我們script裡面的src屬性,為了方便起見,我們先將剛才的webview.html字串全部透過loadWebViewWithUri方法回傳,並可以傳入scriptUri參數指定
function loadWebViewWithUri(scriptUri: vscode.Uri) {
return `<!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 src="${scriptUri}"></script>
</body>
</html>
`;
};
現在我們再將webview的html指定為替過過script的src屬性的字串
webviewPanel.webview.html = loadWebViewWithUri(webviewUri);
重新執行Extension,我們可以看到script順利被讀取且執行
好的,現在我們總算可以讀取外部js的檔案並執行,但讀者可能也感覺得到,這種直接傳回html字串樣板的方式並不是很美觀,被迫將html寫在js裡,其實官方網站範例都是使用這種方法載入資源的。在明天,我們會介紹community開源專案的不同載入方法,讓讀者可以有更多開發專案時的選擇。
在剛才,我們已經成功了讀取檔案。如果我們只想讓我們專案裡的某個資料夾檔案可以被extension存取呢?
為此,VSCode提供了localResourceRoots
這個選項,讓我們可以限制只有某些路徑下的檔案能夠被讀取。localResourceRoots的使用示例如下:
const webviewPanel = vscode.window.createWebviewPanel(
'webviewId', // viewType
'WebView Title', // WebviewPanel Title
vscode.ViewColumn.One,
{
...
localResourceRoots: [
vscode.Uri.file(path.join(context.extensionPath, 'src/webview'))
vscode.Uri.file(path.join(context.extensionPath, 'assets'))
]
}
);
現在,若我們使用了src/disallow.js
這隻檔案,內容如下
document.querySelector('h1').innerHTML = 'Disallow Javascript!';
並將其於html中載入
function loadWebViewWithUri(scriptUri: vscode.Uri, disallowScriptUri: vscode.Uri) {
return `
...
<script src="${scriptUri}"></script>
<script src="${disallowScriptUri}"></script>
...
`;
}
const scriptUri = ...;
const webviewUri = webviewPanel.webview.asWebviewUri(scriptUri);
const disallowWebviewUri = webviewPanel.webview.asWebviewUri(
vscode.Uri.parse(path.join(context.extensionPath, 'src/disallow.js'))
);
webviewPanel.webview.html = loadWebViewWithUri(
webviewUri,
disallowWebviewUri
);
現在我們就可以啟動extension並實際驗證disallow.js並不會被讀取與執行。
如果我們的webview完全不需要加載程式,則可以直接將localResourceRoots
指定為[]
的空陣列,如此一來不會有任何外部的js、style和任何圖檔可以被載入。
VSCode團隊建議我們在不需要使用資源時,可將localResourceRoots設為
...
localResourceRoots: [
vscode.Uri.file(extensionContext.extensionPath)
]
...
這樣會限制只能載入extension的路徑底下的資源。
如無其他載入資源需要,甚至就直接使用最嚴格的[]
來阻止任何路徑下的檔案加載,以此保障應用程式的安全性。
在剛才我們已經限制了讀取資源的路徑,對於瀏覽器的安全性,內容安全政策(Content-Security-Policy,簡稱為CSP)有著更近一步的設定與規範,用以限制可以被載入瀏覽器與執行的檔案,讓我們的網站更安全。
對於WebView,我們一樣可以在html下面加入meta標籤用以向瀏覽器設定CSP規則。
首先,我們在html下面插入此一meta標籤,並設定啟用CSP限制。
<meta http-equiv="Content-Security-Policy">
接著,我們就可以在meta標籤後的content屬性加上我們對於圖像、檔案、Js的載入方式與限制。
現在我們設置了第一個規則default-src,default-src可以設為self或none。當設為self時,這樣csp只會允許同一個origin的src連結,當設為none時,會擋下所有外部的連結與inline的javascript,阻止我們昨天範例那樣在html裡的script標籤下的js程式執行。
這裡我們跟官方團隊的範例一樣,先設置default-src為none。
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
阻止所有外部內容載入後,CSP允許我們以白名單的方式載入我們在default-src後面的其他設定中允許的連結。
以img的圖像載入為例,我們現在想要限制所有的img的外部連結只會允予https的協定才能載入,我們就可以在meta標籤裡這樣設定:
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://*;">
接著我們除了外部的連結以外,也要允許vscode自己的webviewUri能夠被瀏覽器載入,我們就可以對img-src設置第二個規則:
function loadWebViewWithUri(webview: vscode.Webview, scriptUri: vscode.Uri,...) {
const webviewCSPSource = webview.cspSource;
return `
...
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://* ${webviewCSPSource}">
...
`;
}
這個webview.cspSource如果我們使用conosle.log查看,會是一個vscode-webview-resource:
的連結。
當我們把這兩項都設定好的,即可成功的告訴WebView的瀏覽器我們只允許載入來自https或webviewUri的圖像連結。
好了,到這裡,相信讀者們已經大致了解csp設定的方式與規則,讓我們把style跟script的連結也設定CSP規則吧!
function loadWebViewWithUri(webview: vscode.Webview, scriptUri: vscode.Uri, ...) {
const webviewCSPSource = webview.cspSource;
return `
...
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://* ${webviewCSPSource}; script-src ${webviewCSPSource}; style-src ${webviewCSPSource};">
...
`;
}
因為筆者的專案不需要cdn,因此上面的script與style就不多設定https的規則,讀者如有使用外部cdn載入檔案的需求可自行加上。
設定完這幾個規則,為了讓script的載入更加安全,讓我們在script上面加上nonce
的規則吧!
nonce是什麼呢?根據維基百科中的說明:
在資訊安全中,Nonce是一個在加密通信只能使用一次的數字。在認證協定中,它往往是一個隨機或偽隨機數,以避免重送攻擊。Nonce也用於串流加密法以確保安全。如果需要使用相同的金鑰加密一個以上的訊息,就需要Nonce來確保不同的訊息與該金鑰加密的金鑰流不同。
簡言之,nonce可以是一個隨機產生的隨機字串,在每一次通訊時產生。
現在我們要在csp裡的script裡使用nonce,就需要準備一段產生nonce的函式。並以
'nonce-<base64-value>'
的格式指定我們產生的base64編碼的nonce字串給content的script-src。
相關nonce與script-src設定選項,詳見MDN網站的script-src一節。
此處我們使用VSCode官方的Webview-sample中提供的函數來產生nonce數值,如下所示:
function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
現在有了nonce的隨機產生函數,我們即可在html的csp規則裡指定產生的nonce。除此之外,根據CSP的規範,我們也同時需要在載入連結的script標籤下設置nonce,如下所示:
function loadWebViewWithUri(webview: vscode.Webview, scriptUri: vscode.Uri,...) {
const webviewCSPSource = webview.cspSource;
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
...
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://* ${webviewCSPSource}; script-src ${webviewCSPSource} 'nonce-${nonce}'; style-src ${webviewCSPSource};">
...
</head>
<body>
...
<script nonce="${nonce}" src="${scriptUri}"></script>
...
</body>
</html>
`;
}
好的,以上就是我們對於WebView CSP規則使用的講解說明。CSP的規則繁多,讀者可至MDN網站查閱更多可用的設定與相關說明。
總算結束啦!
今天,我們對WebView載入外部資源與javascript有了更多了解,同時也認識了讓WebView載入資源更加安全的幾個做法。
明天我們將練習使用物件導向的方式組織WebView API,並進一步整合當前流行的SPA前端框架至WebView。我們明天見,謝謝大家。