寫在前面
Webview 開啟了一扇新的大門:
The webview API allows extensions to create fully customizable views within Visual Studio Code. Webviews can also be used to build complex user interfaces beyond what VS Code's native APIs support.
讓VS Code 插件能夠通過渲染 HTML 來創建複雜 UI,而不僅限於其 API 支持,這種靈活性讓插件有了更多的可能性:
This freedom makes webviews incredibly powerful, and opens up a whole new range of extension possibilities.
一、vscode.previewHtml 命令
早期通過 vscode.previewHtml 命令來渲染 HTML 內容:
// Render the html of the resource in an editor view.
vscode.commands.executeCommand(
'vscode.previewHtml',
uri,
viewColumn,
title
);
本質上是個 iframe(具體見 html preview part and command),用來支持內置的 Markdown 預覽等功能
後來遇到了安全性和兼容性方面的問題:
However the vscode.previewHtml command suffered from some important security and compatibility issues that we determined could not be fixed without breaking existing users of the command.
遂改用 Webview API 替代:
The webview API is significantly easier to work with, correctly supports different filesystem setups, and webviews also offer many security benefits over htmlPreviews.
二、Webview API
比起 previewHtml,Webview 更安全,但也更耗資源:
Webviews are resource heavy and run in a separate context from normal extensions.
其運行環境是 Electron 的原生 Webview 標籤,與 iframe 相比,最大的區別在於 Webview 運行在獨立進程中,安全隔離性更強:
Unlike an iframe, the webview runs in a separate process than your app. It doesn't have the same permissions as your web page and all interactions between your app and embedded content will be asynchronous. This keeps your app safe from the embedded content.
另一方面,由於使用 Webview 存在性能負擔,官方再三強調「術高莫用」:
Webviews are pretty amazing, but they should also be used sparingly and only when VS Code's native API is inadequate.
並建議在使用 Webview 之前,考慮 3 點:
-
該功能是否真的需要放在 VS Code 裡?作為獨立應用或者網站是不是更合適?
-
Webview 是實現目標功能的唯一方式嗎?能用常規插件 API 替代嗎?
-
所能創造的用戶價值對得起 Webview 所耗費的資源嗎?
三、具體用法
具體的,通過 vscode.window.createWebviewPanel 創建 Webview:
// 1.創建並顯示 Webview
const panel = vscode.window.createWebviewPanel(
// 該 webview 的標識,任意字符串
'catCoding',
// webview 面板的標題,會展示給用戶
'Cat Coding',
// webview 面板所在的分欄
vscode.ViewColumn.One,
// 其它 webview 選項
{}
);
P.S. Webview 面板創建之後,還可以通過 webview.title 修改 Tab 頁標題
接著通過 webview.html 設置要在 Webview 內渲染的 HTML 內容:
// 2.設置 webview 所要渲染的 HTML 內容
panel.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
</body>
</html>`;
與 vscode.previewHtml 類似,所指定的 HTML 內容最終通過 iframe 來加載,只是這個 iframe 是由 Webview 渲染的。所以,與之前的方式相比,只是多了一層用來解決安全問題的 Webview 環境
生命周期
Webview 面板在創建之後,還有 2 個重要的生命周期事件:
-
隱藏/恢復:
onDidChangeViewState,可見性(webview.visible)發生變化、以及 Webview 被拖放到不同分欄(panel.viewColumn)時觸發,通常用來保存/恢復狀態 -
銷毀:
onDidDispose,面板被關掉時觸發,用來完成一些清理工作,如停掉 timer
特殊的,Webview 進入後台時內容會被銷毀,再次可見時重新創建這些內容:
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
比如用戶切換 Tab 後,Webview 正在顯示的內容會被銷毀,運行時狀態也會被清除。用戶切換回來,或者由插件通過 panel.reveal() 讓 Webview 回到用戶眼前時,Webview 內容會重新加載。而被用戶關掉,或者由插件通過 panel.dispose() 關掉時,Webview 及其內容都會被銷毀掉
狀態保存與恢復
所以,Webview 提供了保留狀態的機制:
// webview
vscode.getState({ ... })
vscode.setState({ ... })
可以用來恢復 Webview 內容,例如:
// webview
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
// 取出之前保存的狀態值
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// 狀態值更新時寫回去
vscode.setState({ count });
}, 100);
P.S. 其中,acquireVsCodeApi 是注入到 Webview 環境的全局函數,用來訪問 VS Code 提供的 getState 等 API
需要注意的是,通過 setState() 保存的狀態會在 Webview 面板關閉時銷毀(而不持久化保存):
The state is destroyed when the webview panel is destroyed.
如果想要持久化保留,還需要實現 WebviewPanelSerializer 接口:
// package.json
// 1. 在 package.json 中聲明 onWebviewPanel:viewType 插件激活方式
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
// extension.ts
// 2. 實現 WebviewPanelSerializer 接口
vscode.window.registerWebviewPanelSerializer('catCoding',
new class implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
// 恢復 Webview 內容,state 就是 webview 中通過 setState 保存的狀態
webviewPanel.webview.html = restoreMyWebview(state);
}
}
);
如此這般,VS Code 就能在重啟後自動恢復 Webview 內容了
除手動保存恢復外,另一種簡單辦法是設置 retainContextWhenHidden 選項(createWebviewPanel 時作為參數傳入),要求 Webview 在不可見時仍保留內容(相當於掛起),但會帶來較大的性能開銷,建議慎用 該選項
通信
Webview 內容雖然運行在隔離的環境,但 VS Code 在插件與 Webview 之間提供了消息機制,能夠實現雙向通信:
// 插件 發
webview.postMessage({ ... })
// webview 收
window.addEventListener('message', event => { ... })
// webview 發
const vscode = acquireVsCodeApi()
vscode.postMessage({ ... })
// 插件 收
webview.onDidReceiveMessage(
message => { ... },
undefined,
context.subscriptions
);
因此,Webview 狀態的保存與恢復完全可以手動實現,如果 setState() 等 API 無法滿足的話
主題適配
除了注入 JS 提供額外 API,VS Code 還預置了一些 class 以及 CSS 變量,用來支持樣式適配
例如,body 有 3 個預置的 class 值:
-
vscode-light:淺色主題 -
vscode-dark:深色主題 -
vscode-high-contrast:高對比度主題
可以藉助這三個狀態完成主題適配,例如:
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
並且,用戶配置的具體色值也通過 CSS 變量透出來了:
--vscode-editor-foreground 對應 editor.foreground
--vscode-editor-font-size 對應 editor.fontSize
四、調試
Webview 運行在獨立環境中,無法直接通過 DevTools 調試。為此,VS Code 提供了 2 個命令:
-
Developer: Open Webview Developer Tools:打開當前可見 Webview 的 DevTools -
Developer: Reload Webview:reload 所有 Webview,重置其內部狀態,重新讀取本地資源
針對 Webview 的 DevTools 能夠調試 Webview 內容,就像通過 Toggle Developer Tools 命令打開 DevTools 調試 VS Code 自身的 UI 一樣
如果 Webview 內容中加載了本地資源,可以通過 Reload Webview 命令重新加載,而不必重啟插件或重新打開 Webview
五、安全限制
無論是之前的 vscode.previewHtml 命令,還是現在的 Webview API,都存在着大量的安全限制:
-
Webview 中不支持跳轉。點擊
a標籤沒有反應,建議通過插件修改 Webview 內容曲線實現跳轉 -
仍然受限於
iframe環境(只是iframe放到了 Webview 裡)。例如,無法加載響應頭含有X-Frame-Options: SAMEORIGIN設置的頁面(具體見 #76384、#70339) -
Electron
webview標籤一些安全選項沒有放開。如allow-modals,導致無法alert(具體見 #67109) -
加載本地資源受限,默認只允許訪問插件目錄、以及打開的工作空間目錄,且需通過特定 API(
webview.asWebviewUri)轉換,或者通過<base href="${mediaPath}">標籤設置本地資源根路徑(具體見 #47631)
例如,同源策略導致無法通過 iframe 加載一些資源:
Refused to display 'https://code.visualstudio.com/api/extension-guides/webview' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
此類錯誤無法直接捕獲(具體見 Catch error if iframe src fails to load),但可以在通過 iframe 加載資源之前,嘗試訪問該資源,確認訪問才加載:
fetch(url).then(() => {
// 可通過 iframe 加載
frames[0].src = url;
}, () => {
// 無法通過 iframe 加載,提示出來
});
六、總結
看似靈活開放實際限制極多,目前(2019/12/14),VS Code 對 Webview 能力的定位只是個 HTML 渲染器,作為 UI 擴展能力的補充:
You should think of the webview more as an html view (one that does not have any server or origin) rather than a webpage.
(摘自 #72900,Webview API 作者親述)
暫無評論,快來發表你的看法吧