大家好,我是韋恩,今天是第十五天,我們將開始了解如何在VSCode使用Webview。
在VSCode提供的元件裡面,Webview是個強大又危險的存在。雖然VSCode本身由Electron這個工具所打造,所有的UI元件都是通過Web頁面渲染出來的,但在我們前面練習裡使用的VSCode元件,我們全都通過VSCode提供的API和設定產生的,沒有任何訪問瀏覽器的document物件並操作樣式的機會。這是由於VSCode的團隊考量到效能跟穩定性的問題做了限制,因而在大部分元件裡,我們唯有照api提供的設定才能客製化我們的元件。
但現在,有了WebView,一切都不一樣了。VSCode允許我們在WebView內加載獨立的html頁面,在webview裡,我們可以自由的訪問document,做出任何我們想要的自訂元件與互動方式。可惜WebView強大的同時,也是伴隨著風險,使用Webview會消耗並佔用更多系統資源。另外,客製化的元件和VSCode的樣式和行為不一致時,也容易讓使用者覺得怪異或不適應。
因此VSCode團隊建議,在使用WebView前多考慮這幾點:
這個要實現的功能是否真的需要使用VSCode Extension開發,並在VSCode使用? 如果直接開發一個獨立的網站或應用程序,會不會更好?
是否有常用的VSCode API或元件可以滿足這個需求,WebView是唯一實現功能的方式嗎?
你現在實現的功能帶來的效益與價值,是否足以匹配其佔用並消耗的系統資源?
沒錯,只是因為我們可以使用WebView的功能,並不代表我們一定需要這樣做。
每種技術都有適合的場景,符合情境的技術,才是最好的。
好了,大致了解使用WebView前須考量的幾個點後。讓我們來了解怎麼樣操作WebView API吧!
創建WebView的API一樣是位於VSCode API的window命名空間底下,因此,和前面一樣,先讓我們create一個WebView物件出來吧!
const webviewPanel = vscode.window.createWebviewPanel(
'webviewId',
'WebView Title',
vscode.ViewColumn.One,
);
現在我們已經create一個webviewPanel的物件了,我們指定了這個WebviewPanel的id
、title
還有在editor內顯示的位置。讓我們看一下這個物件有哪些可以使用的API:
好的,我們看到webviewPanel下面有active
等狀態屬性、onDidDispose
等生命週期方法、reveal
操作webviewPanel的方法,最重要的還有webview這個物件。
讓我們繼續看webview下面有什麼方法吧!
可以看到,webview物件提供了安全載入外部連結的cspSource、使用local資源(assets)的asWebviewUri以及跟webview傳接訊息的onDidReceiveMessage
跟postMessage
,以及最後且最常用的html
方法,這個方法會讓我們指定webview的html內容或重新載入webview。
好啦,已經簡單了解API了,讓我們來直接操作並練習使用webview吧!
Contribution Points
命令配置:先讓我們設定今天練習要用的Command:
{
...
"contributes": {
...
"commands": [
{
"command": "day15-webview-api.openWebview",
"title": "Day15: Open Webview"
}
]
...
},
...
}
完成後,我們在src
資料夾下面提供一個webview.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>
<img src="https://i.imgur.com/hzK0JDS.jpg" width="300" />
</body>
</html>
好的,已經設置完Webview,在實務上,我們需要讀取這個webview.html檔案,並提供給extension的webview物件,因此我們需要一段nodejs檔案讀寫的程式:
import * as path from 'path';
import * as fs from 'fs';
function loadWebView(webviewPath: string) {
const readPath = path.resolve(__dirname, webviewPath);
return fs.readFileSync(readPath, { encoding: 'utf-8'});
}
然後,在extension.ts
裡面,我們呼叫載入webview內容的function,指派給vscode的webview物件。
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
export function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand('day15-webview-api.openWebview', () => {
try {
const webviewPanel = vscode.window.createWebviewPanel(
'webviewId',
'WebView Title',
vscode.ViewColumn.One,
);
webviewPanel.webview.html = loadWebView('./webview.html');
vscode.window.showInformationMessage('Show Webview at day15-webview-api!');
} catch (error) {
console.log(error);
}
});
context.subscriptions.push(disposable);
}
export function deactivate() {}
好的,現在按F5
啟動extension,並使用Day05: Open WebView!
的命令吧!
按完後,我們會失敗,到debug console看一下命令,會發現Error: ENOENT: no such file or directory, open ${開啟檔案路徑}
的錯誤,因為typescript的檔案編譯後,編譯完的檔案會在另一個資料夾。我們的webview.html並不是ts檔案,不會被typescript compiler搬運到目標資料夾。
讓我們在package.json的scripts裡的compile方法,加上cp指令複製我們的webview.html
到compile檔案的目標資料夾out
下面
...
scripts: {
...
"compile": "tsc -p ./ && cp ./src/**.html ./out",
...
}
...
註: 此處筆者使用mac,因此使用cp指令做檔案複製。使用windows的讀者請改為使用COPY指令進行處理,或使用gulp、webpack等工具打包。
原則上VSCode套件在打包時會直接打包compile過的檔案,因此這裡用不同平台特定的指令做compile並不會影響最後於VSCode下載套件的使用者們。如需近一步壓縮html等檔案,可再另外學習打包工具的使用,這點我們會在後面的實戰練習進行說明。
然後在terminal使用
npm run compile
可以看到compile後的out資料夾下面也已經有了webview.html
檔案了。
現在按F5
後,執行Day15:Open Webview
的命令,我們可以順利顯示Webview頁面。
WebviewPanel上有一個Visable屬性,當使用者打開另外一個檔案時,WebviewPanel物件並不會銷毀,而是隨之進入背景(background)狀態,此時visiable
與active
屬性會是false,且webview會保存之前的狀態。當使用者重新回到Webview,WebviewPanel即會回到前景(foreground),呈現之前的頁面與狀態。
透過onDidChangeViewState
生命週期方法,我們可以監聽WebviewPanel的狀態變化:
const webviewPanel = createWebViewPanel(...);
webviewPanel.onDidChangeViewState(e => {
const panel = e.webviewPanel;
const activeState = `ActiveState: ${panel.active}`;
const visibleState = `VisibleState: ${panel.visible}`;
const currentViewColumn = `CurrentVisibleColumn: ${panel.viewColumn}`;
[activeState, visibleState, currentViewColumn].forEach(msg => {
console.log(msg);
vscode.window.showInformationMessage(msg);
});
},
/**
* 設置listen的callback方法的this指向,此處我們使用arrow function,設為null即可。
*/
null,
/**
* 傳入context.subscription讓生命週期方法在extension關閉後可以dispose釋放監聽佔用的資源
*/
context.subscriptions
);
WebViewPanel另外提供了onDidDispose
讓我們在此階段dispose過程中我們額外使用的資源。比如說我們使用了setInterval
這個方法每隔2秒執行一段程式,就可以在這個階段使用clearInterval
方法清除我們註冊的timer。
const timerId = setInterval(() => {
doSomething();
}, 2000);
webviewPanel.onDidDispose(() => {
clearInterval(timerId);
}, null, context.subscriptions);
好啦,今天,我們對WebView有了初步的了解,並克服了一些實務上的困難,開啟了一個基本的WebView。
明天會繼續VSCode Webview元件的介紹與實際練習,我們明天見,謝謝大家。