文章轉載於:https://www.cnblogs.com/liuxianan/p/vscode-plugin-webview.htmlcss
你們都知道,整個VSCode編輯器就是一張大的網頁,其實,咱們還能夠在Visual Studio Code
中建立徹底自定義的、能夠間接和nodejs
通訊的特殊網頁(經過一個acquireVsCodeApi
特殊方法),這個網頁就叫WebView
。內置的Markdown
的預覽就是使用WebView
實現的。使用Webview
能夠構建複雜的、支持本地文件操做的用戶界面。html
VSCode插件的WebView相似於iframe的實現,但並非真正的iframe(我猜底層應該仍是基於iframe實現的,只不過上層包裝了一層),經過開發者工具能夠看到:vue
在咱們的vscode-plugin-demo中,我寫了一個很是簡單、沒啥實際意義的Webview
示例僅供參考,在任意編輯器右鍵能夠看到打開Webview
的菜單:node
雖然Webview使人很振奮,由於基於它咱們能夠隨意發揮不受限制,但必須注意仍是要慎用,畢竟VSCode是很注重性能的,不能由於你一個插件拖累了整個IDE,通常僅在原有API和功能以及交互方式沒法知足你時才須要考慮,另外,設計糟糕的Webview也很容易在VS Code
中讓人感受不溫馨,不能讓人家一看就以爲你這是一張網頁,好看的UI也很重要。git
這是官網給出的建議,在使用webview以前請考慮如下事項:github
VSCode
中嗎?做爲單獨的應用程序或網站會不會更好呢?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
選項輕鬆啓用;retainContextWhenHidden: true
會一直保存,但會佔用較大內存開銷,僅在須要時開啓;出於安全考慮,Webview默認沒法直接訪問本地資源,它在一個孤立的上下文中運行,想要加載本地圖片、js、css等必須經過特殊的vscode-resource:
協議,網頁裏面全部的靜態資源都要轉換成這種格式,不然沒法被正常加載。json
vscode-resource:
協議相似於file:
協議,但它只容許訪問特定的本地文件。和file:
同樣,vscode-resource:
從磁盤加載絕對路徑的資源。api
我簡單封裝了一個轉換方法:安全
/** * 獲取某個擴展文件相對於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中嵌入資源,這種方式沒有限制;默認不支持從文件加載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');
重頭戲來了,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);
爲了雙方通訊方便,我把它們簡單封裝了一下,僅供參考,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, '打開文件成功!'); } };
以上封裝的比較隨便,只是給你們提供一個思路,有時間能夠好好封裝一下。
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; }
深色主題效果:
webview
由建立它的擴展程序全部,返回的panel
對象你必須本身保存,若是你的擴展程序丟失了這個引用,那麼將沒法再次從新訪問該webview
,即便Web視圖繼續顯示在vscode
中。
用戶也能夠隨時關閉webview
面板。當用戶關閉webview面板時,webview自己將被銷燬,此時不能再使用panel引用,不然將會出現異常,能夠經過監聽onDidDispose
事件在這裏面作一些銷燬操做。
能夠經過panel.dispose()
方法主動關閉webview。
當webview移動到後臺又再次顯示時,webview中的任何狀態都將丟失。
解決此問題的最佳方法是使你的webview無狀態,經過消息傳遞來保存webview的狀態。
在webview的js中咱們可使用vscode.getState()
和vscode.setState()
方法來保存和恢復JSON可序列化狀態對象。當webview被隱藏時,即便webview內容自己被破壞,這些狀態仍然會保存。固然了,當webview被銷燬時,狀態將被銷燬。
經過註冊WebviewPanelSerializer
能夠實如今VScode
重啓後自動恢復你的webview
,固然,序列化其實也是創建在getState
和setState
之上的。
註冊方法:vscode.window.registerWebviewPanelSerializer
對於具備很是複雜的UI或狀態且沒法快速保存和恢復的webview
,咱們能夠直接使用retainContextWhenHidden
選項。設置retainContextWhenHidden: true
後即便webview被隱藏到後臺其狀態也不會丟失。
儘管retainContextWhenHidden
頗有吸引力,但它須要很高的內存開銷,通常建議在實在沒辦法的時候才啓用。getState
和setState
是持久化的首選方式,由於它們的性能開銷要比retainContextWhenHidden
低得多。
若是配置了快捷鍵,那麼能夠直接在插件頁面按command+shift+p,便可調出控制檯,以下圖所示
注意,要調試Webview不能直接把VSCode的開發者工具打開,直接打開就會和咱們最前面的截圖看到的那樣,你只能看到一個<webview></webview>
標籤,看不到代碼,要看代碼須要按下Ctrl+Shift+P
而後執行打開Webview開發工具
,英文版應該是Open Webview Developer Tools
:
審查Webview:
這個時候須要特別注意錯誤日誌出現的位置,若是是Webview的錯誤,通常打印在前面說的這個開發者工具,但若是是插件端的錯誤只會打印在整個VSCode的開發者工具裏。