【外掛開發】VSCode外掛開發全攻略(七)WebView
什麼是Webview
大家都知道,整個VSCode編輯器就是一張大的網頁,其實,我們還可以在Visual Studio Code
中建立完全自定義的、可以間接和nodejs
通訊的特殊網頁(通過一個acquireVsCodeApi
特殊方法),這個網頁就叫WebView
。內建的Markdown
的預覽就是使用WebView
實現的。使用Webview
可以構建複雜的、支援本地檔案操作的使用者介面。
VSCode外掛的WebView類似於iframe的實現,但並不是真正的iframe(我猜底層應該還是基於iframe實現的,只不過上層包裝了一層),通過開發者工具可以看到:
1.1.demo
Webview
示例僅供參考,在任意編輯器右鍵可以看到開啟Webview
的選單:
什麼時候適合使用WebView
雖然Webview令人很振奮,因為基於它我們可以隨意發揮不受限制,但必須注意還是要慎用,畢竟VSCode是很注重效能的,不能因為你一個外掛拖累了整個IDE,一般僅在原有API和功能以及互動方式無法滿足你時才需要考慮,另外,設計糟糕的Webview也很容易在VS Code
中讓人感覺不舒適,不能讓人家一看就覺得你這是一張網頁,好看的UI也很重要。
這是官網給出的建議,在使用webview之前請考慮以下事項:
- 這個功能真的需要放在
VSCode
中嗎?作為單獨的應用程式或網站會不會更好呢? - webview是實現這個功能的唯一方法嗎?可以使用常規VS Code API嗎?
- 您的webview是否會帶來足夠的使用者價值以證明其高資源成本?
正式開始WebView之旅
3.1.建立WebView
context.subscriptions.push(vscode.commands.registerCommand('extension.demo.openWebview', function (uri) { // 建立webview const panel = vscode.window.createWebviewPanel( 'testWebview', //viewType "WebView演示", // 檢視標題 vscode.ViewColumn.One, // 顯示在編輯器的哪個部位 { enableScripts: true, // 啟用JS,預設禁用 retainContextWhenHidden: true, // webview被隱藏時保持狀態,避免被重置 } ); panel.webview.html = `<html><body>你好,我是Webview</body></html>`
幾點說明:
- 預設情況下,在Web檢視中禁用
JavaScript
,但可以通過傳入enableScripts: true
選項輕鬆啟用; - 預設情況下當webview被隱藏時資源會被銷燬,通過
retainContextWhenHidden: true
會一直儲存,但會佔用較大記憶體開銷,僅在需要時開啟;
3.2.載入本地資源
出於安全考慮,Webview預設無法直接訪問本地資源,它在一個孤立的上下文中執行,想要載入本地圖片、js、css等必須通過特殊的vscode-resource:
協議,網頁裡面所有的靜態資源都要轉換成這種格式,否則無法被正常載入。
vscode-resource:
協議類似於file:
協議,但它只允許訪問特定的本地檔案。和file:
一樣,vscode-resource:
從磁碟載入絕對路徑的資源。
我簡單封裝了一個轉換方法:
/** * 獲取某個擴充套件檔案相對於webview需要的一種特殊路徑格式 * 形如:vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif * @param context 上下文 * @param relativePath 擴充套件中某個檔案相對於根目錄的路徑,如 images/test.jpg */ getExtensionFileVscodeResource: function(context, relativePath) { const diskPath = vscode.Uri.file(path.join(context.extensionPath, relativePath)); return diskPath.with({ scheme: 'vscode-resource' }).toString(); }
預設情況下,vscode-resource:
只能訪問以下位置中的資源:
- 擴充套件程式安裝目錄中的檔案。
- 使用者當前活動的工作區內。
- 當然,你還可以使用
dataURI
直接在Webview中嵌入資源,這種方式沒有限制;
3.3.從檔案載入HTML內容
預設不支援從檔案載入HTML,需要自己封裝程式碼,我簡單封裝了一個供大家參考:
/** * 從某個HTML檔案讀取能被Webview載入的HTML內容 * @param {*} context 上下文 * @param {*} templatePath 相對於外掛根目錄的html檔案相對路徑 */ function getWebViewContent(context, templatePath) { const resourcePath = path.join(context.extensionPath, templatePath); const dirPath = path.dirname(resourcePath); let html = fs.readFileSync(resourcePath, 'utf-8'); // vscode不支援直接載入本地資源,需要替換成其專有路徑格式,這裡只是簡單的將樣式和JS的路徑替換 html = html.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => { return $1 + vscode.Uri.file(path.resolve(dirPath, $2)).with({ scheme: 'vscode-resource' }).toString() + '"'; }); return html; }
執行這段程式碼之後,會自動將HTML檔案中link
、href
、script
、img
的資源相對路徑全部替換成正確的vscode-resource:
絕對路徑,例如:
../../lib/vue-2.5.17/vue.js 變成 vscode-resource:/Users/test/workspace/vscode-plugin-demo/lib/vue-2.5.17/vue.js
使用方法如下:
panel.webview.html = getWebViewContent(context, 'src/view/test-webview.html');
3.4.訊息通訊
重頭戲來了,Webview
和普通網頁非常類似,不能直接呼叫任何VSCode
API,但是,它唯一特別之處就在於多了一個名叫acquireVsCodeApi
的方法,執行這個方法會返回一個超級閹割版的vscode
物件,這個物件裡面有且僅有如下3個可以和外掛通訊的API:
外掛和Webview
之間如何互相通訊呢?
外掛給Webview
傳送訊息(支援傳送任意可以被JSON
化的資料):
panel.webview.postMessage({text: '你好,我是小茗同學!'});
Webview
端接收:
window.addEventListener('message', event => { const message = event.data; console.log('Webview接收到的訊息:', message); }
Webview
主動傳送訊息給外掛:
vscode.postMessage({text: '你好,我是Webview啊!'});
外掛接收:
panel.webview.onDidReceiveMessage(message => { console.log('外掛收到的訊息:', message); }, undefined, context.subscriptions);
3.4.1.簡單通訊封裝
為了雙方通訊方便,我把它們簡單封裝了一下,僅供參考,Webview端:
const callbacks = {}; // 存放所有的回撥函式 /** * 呼叫vscode原生api * @param data 可以是類似 {cmd: 'xxx', param1: 'xxx'},也可以直接是 cmd 字串 * @param cb 可選的回撥函式 */ function callVscode(data, cb) { if (typeof data === 'string') { data = { cmd: data }; } if (cb) { // 時間戳加上5位隨機數 const cbid = Date.now() + '' + Math.round(Math.random() * 100000); // 將回調函式分配一個隨機cbid然後存起來,後續需要執行的時候再撈起來 callbacks[cbid] = cb; data.cbid = cbid; } vscode.postMessage(data); } window.addEventListener('message', event => { const message = event.data; switch (message.cmd) { // 來自vscode的回撥 case 'vscodeCallback': console.log(message.data); (callbacks[message.cbid] || function () { })(message.data); delete callbacks[message.cbid]; // 執行完回撥刪除 break; default: break; } });
外掛端:
let global = { projectPath, panel}; panel.webview.onDidReceiveMessage(message => { if (messageHandler[message.cmd]) { // cmd表示要執行的方法名稱 messageHandler[message.cmd](global, message); } else { util.showError(`未找到名為 ${message.cmd} 的方法!`); } }, undefined, context.subscriptions); /** * 存放所有訊息回撥函式,根據 message.cmd 來決定呼叫哪個方法, * 想呼叫什麼方法,就在這裡寫一個和cmd同名的方法實現即可 */ const messageHandler = { // 彈出提示 alert(global, message) { util.showInfo(message.info); }, // 顯示錯誤提示 error(global, message) { util.showError(message.info); }, // 回撥示例:獲取工程名 getProjectName(global, message) { invokeCallback(global.panel, message, util.getProjectName(global.projectPath)); } } /** * 執行回撥函式 * @param {*} panel * @param {*} message * @param {*} resp */ function invokeCallback(panel, message, resp) { console.log('回撥訊息:', resp); // 錯誤碼在400-600之間的,預設彈出錯誤提示 if (typeof resp == 'object' && resp.code && resp.code >= 400 && resp.code < 600) { util.showError(resp.message || '發生未知錯誤!'); } panel.webview.postMessage({cmd: 'vscodeCallback', cbid: message.cbid, data: resp}); }
按上述方法封裝之後,例如,Webview端想要執行名為openFileInVscode
命令只需要這樣:
callVscode({cmd: 'openFileInVscode', path: `package.json`}, (message) => { this.alert(message); });
然後在外掛端的messageHandler
實現openFileInVscode
方法即可,其它都不用管:
const messageHandler = { // 省略其它方法 openFileInVscode(global, message) { util.openFileInVscode(`${global.projectPath}/${message.path}`); invokeCallback(global.panel, message, '開啟檔案成功!'); } };
以上封裝的比較隨便,只是給大家提供一個思路,有時間可以好好封裝一下。
3.5.主題適配
Webview
可以根據VS Code
的當前主題更改其外觀,原理是body上面添加當前主題名稱,主要有以下三種:
vscode-light - 淺色主題; vscode-dark -深色主題; vscode-high-contrast - 高對比度主題;
- 所以我們可以通過自己寫樣式來適配不同主題:
/* 淺色主題 */ .body.vscode-light { background: white; color: black; } /* 深色主題 */ body.vscode-dark { background: #252526; color: white; } /* 高對比度主題 */ body.vscode-high-contrast { background: white; color: red; }
深色主題效果:
3.6.生命週期
webview
由建立它的擴充套件程式所有,返回的panel
物件你必須自己儲存,如果你的擴充套件程式丟失了這個引用,那麼將無法再次重新訪問該webview
,即使Web檢視繼續顯示在vscode
中。
使用者也可以隨時關閉webview
面板。當用戶關閉webview面板時,webview本身將被銷燬,此時不能再使用panel引用,否則將會出現異常,可以通過監聽onDidDispose
事件在這裡面做一些銷燬操作。
可以通過panel.dispose()
方法主動關閉webview。
3.7.狀態保持
當webview移動到後臺又再次顯示時,webview中的任何狀態都將丟失。
解決此問題的最佳方法是使你的webview無狀態,通過訊息傳遞來儲存webview的狀態。
3.7.1.state
在webview的js中我們可以使用vscode.getState()
和vscode.setState()
方法來儲存和恢復JSON可序列化狀態物件。當webview被隱藏時,即使webview內容本身被破壞,這些狀態仍然會儲存。當然了,當webview被銷燬時,狀態將被銷燬。
3.7.2.序列化
通過註冊WebviewPanelSerializer
可以實現在VScode
重啟後自動恢復你的webview
,當然,序列化其實也是建立在getState
和setState
之上的。
註冊方法:vscode.window.registerWebviewPanelSerializer
3.7.3.retainContextWhenHidden
對於具有非常複雜的UI或狀態且無法快速儲存和恢復的webview
,我們可以直接使用retainContextWhenHidden
選項。設定retainContextWhenHidden: true
後即使webview被隱藏到後臺其狀態也不會丟失。
儘管retainContextWhenHidden
很有吸引力,但它需要很高的記憶體開銷,一般建議在實在沒辦法的時候才啟用。getState
和setState
是持久化的首選方式,因為它們的效能開銷要比retainContextWhenHidden
低得多。
除錯
注意,要除錯Webview不能直接把VSCode的開發者工具開啟,直接開啟就會和我們最前面的截圖看到的那樣,你只能看到一個<webview></webview>
標籤,看不到程式碼,要看程式碼需要按下Ctrl+Shift+P
然後執行開啟Webview開發工具
,英文版應該是Open Webview Developer Tools
:
審查Webview:
這個時候需要特別注意錯誤日誌出現的位置,如果是Webview的錯誤,一般列印在前面說的這個開發者工具,但如果是外掛端的錯誤只會列印在整個VSCode的開發者工具裡。