Webview API
Webview API 允許擴充套件在 Visual Studio Code 中建立完全可定製的檢視。例如,內建的 Markdown 擴充套件使用 webview 來渲染 Markdown 預覽。Webview 還可以用於構建超越 VS Code 原生 API 支援的複雜使用者介面。
可以將 webview 視為 VS Code 中的一個 iframe
,由您的擴充套件控制。webview 可以在此框架中渲染幾乎任何 HTML 內容,並使用訊息傳遞與擴充套件進行通訊。這種自由使得 webview 功能極其強大,並開啟了全新的擴充套件可能性。
Webview 在多個 VS Code API 中使用
- 透過使用
createWebviewPanel
建立的 Webview 面板。在這種情況下,Webview 面板在 VS Code 中顯示為獨立的編輯器。這使得它們非常適合顯示自定義 UI 和自定義視覺化。 - 作為自定義編輯器的檢視。自定義編輯器允許擴充套件為編輯工作區中的任何檔案提供自定義 UI。自定義編輯器 API 還允許您的擴充套件掛接到編輯器事件(如撤銷和重做)以及檔案事件(如儲存)。
- 在側邊欄或面板區域中渲染的Webview 檢視中。有關更多詳細資訊,請參閱webview 檢視示例擴充套件。
本頁面重點介紹基本的 webview 面板 API,儘管這裡涵蓋的幾乎所有內容也適用於自定義編輯器和 webview 檢視中使用的 webview。即使您對這些 API 更感興趣,我們也建議您先閱讀本頁面,以熟悉 webview 的基本知識。
連結
VS Code API 用法
我應該使用 webview 嗎?
Webview 確實很棒,但它們也應該謹慎使用,並且僅在 VS Code 的原生 API 不足時使用。Webview 資源消耗大,並且與普通擴充套件在不同的上下文中執行。設計不佳的 webview 也很容易在 VS Code 中顯得格格不入。
在使用 webview 之前,請考慮以下幾點
-
此功能真的需要在 VS Code 中嗎?作為單獨的應用程式或網站會更好嗎?
-
webview 是實現您功能的唯一方法嗎?您可以改用常規的 VS Code API 嗎?
-
您的 webview 會增加足夠的、足以證明其高資源成本的使用者價值嗎?
請記住:僅僅因為您可以使用 webview 做某事,並不意味著您應該這樣做。但是,如果您確信需要使用 webview,那麼本文件會為您提供幫助。讓我們開始吧。
Webview API 基礎
為了解釋 webview API,我們將構建一個名為 Cat Coding 的簡單擴充套件。此擴充套件將使用 webview 顯示一隻貓編寫程式碼的 gif(可能是在 VS Code 中)。在學習 API 的過程中,我們將繼續向擴充套件新增功能,包括一個跟蹤貓已編寫程式碼行數的計數器,以及在貓引入 bug 時通知使用者的通知。
這是 Cat Coding 擴充套件第一個版本的 package.json
。您可以在此處找到示例應用程式的完整程式碼。我們的擴充套件的第一個版本貢獻了一個名為 catCoding.start
的命令。當用戶呼叫此命令時,我們將顯示一個帶有我們貓的簡單 webview。使用者將能夠從命令面板中呼叫此命令,名稱為Cat Coding: Start new cat coding session,如果他們願意,甚至可以為其建立鍵盤繫結。
{
"name": "cat-coding",
"description": "Cat Coding",
"version": "0.0.1",
"publisher": "bierner",
"engines": {
"vscode": "^1.74.0"
},
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "catCoding.start",
"title": "Start new cat coding session",
"category": "Cat Coding"
}
]
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"vscode": "*"
},
"devDependencies": {
"@types/node": "^9.4.6",
"typescript": "^2.8.3"
}
}
注意:如果您的擴充套件目標是 1.74 之前的 VS Code 版本,您必須在
activationEvents
中明確列出onCommand:catCoding.start
。
現在讓我們實現 catCoding.start
命令。在我們的擴充套件主檔案中,我們註冊 catCoding.start
命令並用它來顯示一個基本的 webview
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
})
);
}
vscode.window.createWebviewPanel
函式在編輯器中建立並顯示一個 webview。如果您嘗試運行當前狀態的 catCoding.start
命令,您會看到以下內容
我們的命令打開了一個帶有正確標題但沒有內容的新 webview 面板!要將我們的貓新增到新面板中,我們還需要使用 webview.html
設定 webview 的 HTML 內容
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show panel
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// And set its HTML content
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!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>`;
}
如果您再次執行該命令,現在 webview 看起來像這樣
進展!
webview.html
應該始終是一個完整的 HTML 文件。HTML 片段或格式錯誤的 HTML 可能會導致意外行為。
更新 webview 內容
webview.html
也可以在 webview 建立後更新其內容。讓我們用它來透過引入貓的輪換來使 Cat Coding 更具動態性
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// Set initial content
updateWebview();
// And schedule updates to the content every second
setInterval(updateWebview, 1000);
})
);
}
function getWebviewContent(cat: keyof typeof cats) {
return `<!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="${cats[cat]}" width="300" />
</body>
</html>`;
}
設定 webview.html
會替換整個 webview 內容,類似於重新載入 iframe。一旦您開始在 webview 中使用指令碼,這一點就很重要,因為這意味著設定 webview.html
也會重置指令碼的狀態。
上面的示例還使用 webview.title
更改編輯器中顯示的文件的標題。設定標題不會導致 webview 重新載入。
生命週期
Webview 面板由建立它們的擴充套件擁有。擴充套件必須保留從 createWebviewPanel
返回的 webview。如果您的擴充套件丟失了此引用,即使 webview 將繼續在 VS Code 中顯示,它也無法再次訪問該 webview。
與文字編輯器一樣,使用者也可以隨時關閉 webview 面板。當用戶關閉 webview 面板時,webview 本身會被銷燬。嘗試使用已銷燬的 webview 會引發異常。這意味著上面使用 setInterval
的示例實際上有一個重要的 bug:如果使用者關閉面板,setInterval
將繼續觸發,這將嘗試更新 panel.webview.html
,這當然會引發異常。貓討厭異常。讓我們來修復它!
當 webview 被銷燬時,會觸發 onDidDispose
事件。我們可以使用此事件取消進一步的更新並清理 webview 的資源
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
updateWebview();
const interval = setInterval(updateWebview, 1000);
panel.onDidDispose(
() => {
// When the panel is closed, cancel any future updates to the webview content
clearInterval(interval);
},
null,
context.subscriptions
);
})
);
}
擴充套件還可以透過呼叫 dispose()
以程式設計方式關閉 webview。例如,如果我們想將貓的工作日限制在五秒內
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
panel.webview.html = getWebviewContent('Coding Cat');
// After 5sec, programmatically close the webview panel
const timeout = setTimeout(() => panel.dispose(), 5000);
panel.onDidDispose(
() => {
// Handle user closing panel before the 5sec have passed
clearTimeout(timeout);
},
null,
context.subscriptions
);
})
);
}
可見性和移動
當 webview 面板被移動到後臺選項卡時,它會隱藏起來。但它不會被銷燬。當面板再次被帶到前臺時,VS Code 會自動從 webview.html
恢復 webview 的內容
.visible
屬性告訴您 webview 面板當前是否可見。
擴充套件可以透過呼叫 reveal()
以程式設計方式將 webview 面板帶到前臺。此方法接受一個可選的目標檢視列以顯示面板。一個 webview 面板一次只能顯示在一個編輯器列中。呼叫 reveal()
或將 webview 面板拖動到新的編輯器列會將 webview 移動到該新列中。
讓我們更新我們的擴充套件,使其一次只允許一個 webview 存在。如果面板在後臺,則 catCoding.start
命令會將其帶到前臺
export function activate(context: vscode.ExtensionContext) {
// Track the current panel with a webview
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (currentPanel) {
// If we already have a panel, show it in the target column
currentPanel.reveal(columnToShowIn);
} else {
// Otherwise, create a new panel
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
columnToShowIn || vscode.ViewColumn.One,
{}
);
currentPanel.webview.html = getWebviewContent('Coding Cat');
// Reset when the current panel is closed
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
null,
context.subscriptions
);
}
})
);
}
這是新擴充套件的實際操作
每當 webview 的可見性發生變化,或者當 webview 被移動到新列時,都會觸發 onDidChangeViewState
事件。我們的擴充套件可以使用此事件根據 webview 顯示在哪個列中來更改貓
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
panel.webview.html = getWebviewContent('Coding Cat');
// Update contents based on view state changes
panel.onDidChangeViewState(
e => {
const panel = e.webviewPanel;
switch (panel.viewColumn) {
case vscode.ViewColumn.One:
updateWebviewForCat(panel, 'Coding Cat');
return;
case vscode.ViewColumn.Two:
updateWebviewForCat(panel, 'Compiling Cat');
return;
case vscode.ViewColumn.Three:
updateWebviewForCat(panel, 'Testing Cat');
return;
}
},
null,
context.subscriptions
);
})
);
}
function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
panel.title = catName;
panel.webview.html = getWebviewContent(catName);
}
檢查和除錯 webview
開發人員:切換開發人員工具命令會開啟一個開發人員工具視窗,您可以使用它來除錯和檢查您的 webview。
請注意,如果您使用的是 1.56 之前的 VS Code 版本,或者您正在嘗試除錯設定了 enableFindWidget
的 webview,則必須改用開發人員:開啟 Webview 開發人員工具命令。此命令會為每個 webview 開啟一個專用的開發人員工具頁面,而不是使用一個由所有 webview 和編輯器本身共享的開發人員工具頁面。
在開發人員工具中,您可以使用開發人員工具視窗左上角的檢查工具開始檢查 webview 的內容
您還可以在開發人員工具控制檯中檢視 webview 的所有錯誤和日誌
要在 webview 的上下文中評估表示式,請確保從開發人員工具控制檯面板左上角的下拉列表中選擇活動框架環境
活動框架環境是 webview 指令碼本身執行的地方。
此外,開發人員:重新載入 Webview 命令會重新載入所有活動的 webview。如果您需要重置 webview 的狀態,或者磁碟上的某些 webview 內容已更改並且您希望載入新內容,這會很有幫助。
載入本地內容
Webview 在隔離的上下文中執行,無法直接訪問本地資源。這是出於安全原因。這意味著為了從您的擴充套件載入影像、樣式表和其他資源,或者從使用者的當前工作區載入任何內容,您必須使用 Webview.asWebviewUri
函式將本地 file:
URI 轉換為 VS Code 可以用來載入本地資源子集的特殊 URI。
假設我們想開始將貓的 gif 打包到我們的擴充套件中,而不是從 Giphy 獲取它們。為此,我們首先建立一個指向磁碟上檔案的 URI,然後透過 asWebviewUri
函式傳遞這些 URI
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// Get path to resource on disk
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
// And get the special URI to use with the webview
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
function getWebviewContent(catGifSrc: 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>Cat Coding</title>
</head>
<body>
<img src="${catGifSrc}" width="300" />
</body>
</html>`;
}
如果除錯此程式碼,我們會發現 catGifSrc
的實際值類似於
vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif
VS Code 瞭解這個特殊的 URI,並將用它從磁碟載入我們的 gif!
預設情況下,webview 只能訪問以下位置的資源
- 您的擴充套件安裝目錄內。
- 使用者當前活動工作區內。
使用 WebviewOptions.localResourceRoots
允許訪問其他本地資源。
您也可以始終使用資料 URI 將資源直接嵌入到 webview 中。
控制對本地資源的訪問
Webview 可以使用 localResourceRoots
選項控制可以從使用者計算機載入哪些資源。localResourceRoots
定義了一組可以從中載入本地內容的根 URI。
我們可以使用 localResourceRoots
將 Cat Coding webview 限制為只能從我們擴充套件中的 media
目錄載入資源
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Only allow the webview to access resources in our extension's media directory
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
}
);
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
要禁止所有本地資源,只需將 localResourceRoots
設定為 []
。
一般來說,webview 在載入本地資源時應該儘可能嚴格。但是,請記住 localResourceRoots
本身不能提供完整的安全保護。請確保您的 webview 還遵循安全最佳實踐,並新增內容安全策略以進一步限制可以載入的內容。
主題化 webview 內容
Webview 可以使用 CSS 根據 VS Code 的當前主題更改其外觀。VS Code 將主題分為三類,並在 body
元素中新增一個特殊類以指示當前主題
vscode-light
- 淺色主題。vscode-dark
- 深色主題。vscode-high-contrast
- 高對比度主題。
以下 CSS 根據使用者當前的主題更改 webview 的文字顏色
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
在開發 webview 應用程式時,請確保它適用於這三種類型的主題。並始終在高對比度模式下測試您的 webview,以確保有視力障礙的人可以使用它。
Webview 還可以使用CSS 變數訪問 VS Code 主題顏色。這些變數名稱以 vscode
為字首,並將 .
替換為 -
。例如 editor.foreground
變為 var(--vscode-editor-foreground)
code {
color: var(--vscode-editor-foreground);
}
查閱主題顏色參考以獲取可用的主題變數。有一個擴充套件提供了變數的 IntelliSense 建議。
還定義了以下字型相關變數
--vscode-editor-font-family
- 編輯器字體系列(來自editor.fontFamily
設定)。--vscode-editor-font-weight
- 編輯器字型粗細(來自editor.fontWeight
設定)。--vscode-editor-font-size
- 編輯器字型大小(來自editor.fontSize
設定)。
最後,對於需要編寫針對單個主題的 CSS 的特殊情況,webview 的 body 元素有一個名為 vscode-theme-id
的資料屬性,它儲存當前活動主題的 ID。這允許您為 webview 編寫特定於主題的 CSS
body[data-vscode-theme-id="One Dark Pro"] {
background: hotpink;
}
支援的媒體格式
Webview 支援音訊和影片,但並非所有媒體編解碼器或媒體檔案容器型別都受支援。
Webview 中可以使用以下音訊格式
- Wav
- Mp3
- Ogg
- Flac
Webview 中可以使用以下影片格式
- H.264
- VP8
對於影片檔案,請確保影片和音訊軌道的媒體格式都受支援。例如,許多 .mp4
檔案使用 H.264
作為影片,使用 AAC
作為音訊。VS Code 將能夠播放 mp4
的影片部分,但由於不支援 AAC
音訊,因此不會有任何聲音。相反,您需要為音軌使用 mp3
。
上下文選單
高階 webview 可以自定義使用者在 webview 內右鍵單擊時顯示的上下文選單。這與 VS Code 的普通上下文選單類似,使用貢獻點完成,因此自定義選單與編輯器的其餘部分完美融合。Webview 還可以為 webview 的不同部分顯示自定義上下文選單。
要向您的 webview 新增新的上下文選單項,首先在 menus
下的新 webview/context
部分新增一個新條目。每個貢獻都包含一個 command
(也是專案標題的來源)和一個 when
子句。when 子句應包含 webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'
,以確保上下文選單僅適用於您的擴充套件的 webview
"contributes": {
"menus": {
"webview/context": [
{
"command": "catCoding.yarn",
"when": "webviewId == 'catCoding'"
},
{
"command": "catCoding.insertLion",
"when": "webviewId == 'catCoding' && webviewSection == 'editor'"
}
]
},
"commands": [
{
"command": "catCoding.yarn",
"title": "Yarn 🧶",
"category": "Cat Coding"
},
{
"command": "catCoding.insertLion",
"title": "Insert 🦁",
"category": "Cat Coding"
},
...
]
}
在 webview 內部,您還可以使用 data-vscode-context
資料屬性(或在 JavaScript 中使用 dataset.vscodeContext
)設定 HTML 特定區域的上下文。data-vscode-context
值是一個 JSON 物件,它指定當使用者右鍵單擊元素時要設定的上下文。最終上下文是從文件根到單擊的元素確定的。
例如,考慮以下 HTML
<div class="main" data-vscode-context='{"webviewSection": "main", "mouseCount": 4}'>
<h1>Cat Coding</h1>
<textarea data-vscode-context='{"webviewSection": "editor", "preventDefaultContextMenuItems": true}'></textarea>
</div>
如果使用者右鍵單擊 textarea
,將設定以下上下文
webviewSection == 'editor'
- 這會覆蓋父元素的webviewSection
。mouseCount == 4
- 這是從父元素繼承的。preventDefaultContextMenuItems == true
- 這是一個特殊上下文,它會隱藏 VS Code 通常新增到 webview 上下文選單中的複製和貼上條目。
如果使用者在 <textarea>
內右鍵單擊,他們將看到
有時,在左鍵/主點選時顯示選單會很有用。例如,在分體按鈕上顯示選單。您可以透過在 onClick
事件中分發 contextmenu
事件來完成此操作
<button data-vscode-context='{"preventDefaultContextMenuItems": true }' onClick='((e) => {
e.preventDefault();
e.target.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, clientX: e.clientX, clientY: e.clientY }));
e.stopPropagation();
})(event)'>Create</button>
指令碼和訊息傳遞
Webview 就像 iframe 一樣,這意味著它們也可以執行指令碼。JavaScript 預設在 webview 中停用,但可以透過傳入 enableScripts: true
選項輕鬆重新啟用。
讓我們使用一個指令碼來新增一個計數器,跟蹤我們的貓編寫的程式碼行數。執行一個基本指令碼非常簡單,但請注意,此示例僅用於演示目的。在實踐中,您的 webview 應該始終使用內容安全策略停用內聯指令碼
import * as path from 'path';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Enable scripts in the webview
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!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" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
哇!那真是一隻高產的貓。
Webview 指令碼可以做任何普通網頁上的指令碼可以做的事情。但請記住,webview 存在於它們自己的上下文中,因此 webview 中的指令碼無法訪問 VS Code API。這就是訊息傳遞發揮作用的地方!
從擴充套件向 webview 傳遞訊息
擴充套件可以使用 webview.postMessage()
向其 webview 傳送資料。此方法將任何可 JSON 序列化的資料傳送到 webview。訊息透過標準 message
事件在 webview 內部接收。
為了演示這一點,讓我們向 Cat Coding 新增一個新命令,指示當前編碼的貓重構其程式碼(從而減少總行數)。新的 catCoding.doRefactor
命令使用 postMessage
將指令傳送到當前 webview,並在 webview 內部使用 window.addEventListener('message', event => { ... })
處理訊息
export function activate(context: vscode.ExtensionContext) {
// Only allow a single Cat Coder
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
undefined,
context.subscriptions
);
}
})
);
// Our new command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// Send a message to our webview.
// You can send any JSON serializable data.
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
function getWebviewContent() {
return `<!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" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
// Handle the message inside the webview
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'refactor':
count = Math.ceil(count * 0.5);
counter.textContent = count;
break;
}
});
</script>
</body>
</html>`;
}
從 webview 向擴充套件傳遞訊息
Webview 也可以將訊息傳回其擴充套件。這是透過 webview 中一個特殊的 VS Code API 物件上的 postMessage
函式來實現的。要訪問 VS Code API 物件,請在 webview 中呼叫 acquireVsCodeApi
。此函式每個會話只能呼叫一次。您必須保留此方法返回的 VS Code API 例項,並將其傳遞給任何需要使用它的其他函式。
我們可以在 Cat Coding webview 中使用 VS Code API 和 postMessage
來在我們的貓引入程式碼中的 bug 時提醒擴充套件
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
})
);
}
function getWebviewContent() {
return `<!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" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
// Alert the extension when our cat introduces a bug
if (Math.random() < 0.001 * count) {
vscode.postMessage({
command: 'alert',
text: '🐛 on line ' + count
})
}
}, 100);
}())
</script>
</body>
</html>`;
}
出於安全原因,您必須將 VS Code API 物件保密,並確保它永遠不會洩露到全域性作用域。
使用 Web Workers
Web Workers 在 webview 中受支援,但需要注意一些重要的限制。
首先,workers 只能使用 data:
或 blob:
URI 載入。您不能直接從擴充套件資料夾載入 worker。
如果您確實需要從擴充套件中的 JavaScript 檔案載入 worker 程式碼,請嘗試使用 fetch
const workerSource = 'absolute/path/to/worker.js';
fetch(workerSource)
.then(result => result.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
});
Worker 指令碼也不支援使用 importScripts
或 import(...)
匯入原始碼。如果您的 worker 動態載入程式碼,請嘗試使用捆綁器(例如 webpack)將 worker 指令碼打包成單個檔案。
使用 webpack
,您可以使用 LimitChunkCountPlugin
強制編譯後的 worker JavaScript 成為單個檔案
const path = require('path');
const webpack = require('webpack');
module.exports = {
target: 'webworker',
entry: './worker/src/index.js',
output: {
filename: 'worker.js',
path: path.resolve(__dirname, 'media')
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
};
安全性
與任何網頁一樣,在建立 webview 時,您必須遵循一些基本的安全最佳實踐。
限制功能
Webview 應該只擁有它所需的最低限度功能。例如,如果您的 webview 不需要執行指令碼,請不要設定 enableScripts: true
。如果您的 webview 不需要從使用者的工作區載入資源,請將 localResourceRoots
設定為 [vscode.Uri.file(extensionContext.extensionPath)]
甚至 []
以禁止訪問所有本地資源。
內容安全策略
內容安全策略進一步限制了可以在 webview 中載入和執行的內容。例如,內容安全策略可以確保只有允許的指令碼列表才能在 webview 中執行,甚至可以告訴 webview 僅透過 https
載入影像。
要新增內容安全策略,請在 webview 的 <head>
頂部放置一個 <meta http-equiv="Content-Security-Policy">
指令
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
...
</body>
</html>`;
}
策略 default-src 'none';
禁止所有內容。然後我們可以重新啟用擴充套件需要執行的最小內容量。這是一個內容安全策略,允許載入本地指令碼和樣式表,並透過 https
載入影像
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>
${webview.cspSource}
值是來自 webview 物件本身的值的佔位符。請參閱webview 示例以獲取如何使用此值的完整示例。
此內容安全策略還隱式停用內聯指令碼和樣式。最佳實踐是將所有內聯樣式和指令碼提取到外部檔案,以便它們可以在不放鬆內容安全策略的情況下正確載入。
只通過 https 載入內容
如果您的 webview 允許載入外部資源,強烈建議您只允許這些資源透過 https
載入,而不是透過 http。上面的示例內容安全策略已經透過只允許影像透過 https:
載入來實現了這一點。
清理所有使用者輸入
就像您為普通網頁構建 HTML 一樣,在為 webview 構建 HTML 時,您必須清理所有使用者輸入。未能正確清理輸入可能會導致內容注入,從而使您的使用者面臨安全風險。
必須清理的示例值
- 檔案內容。
- 檔案和資料夾路徑。
- 使用者和工作區設定。
考慮使用輔助庫來構建您的 HTML 字串,或者至少確保來自使用者工作區的所有內容都已正確清理。
永遠不要僅僅依靠清理來保證安全。請務必遵循其他安全最佳實踐,例如擁有內容安全策略,以最大程度地減少任何潛在內容注入的影響。
永續性
在標準的 webview 生命週期中,webview 由 createWebviewPanel
建立,並在使用者關閉它們或呼叫 .dispose()
時銷燬。然而,webview 的內容在 webview 可見時建立,並在 webview 移到後臺時銷燬。當 webview 移到後臺選項卡時,webview 內部的任何狀態都將丟失。
解決此問題的最佳方法是使您的 webview 無狀態。使用訊息傳遞來儲存 webview 的狀態,然後在 webview 再次可見時恢復狀態。
getState 和 setState
在 webview 內部執行的指令碼可以使用 getState
和 setState
方法來儲存和恢復 JSON 可序列化狀態物件。即使當 webview 面板變為隱藏狀態時 webview 內容本身被銷燬,此狀態也會持久存在。當 webview 面板被銷燬時,狀態也會被銷燬。
// Inside a webview script
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
// Check if we have an old state to restore from
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// Update the saved state
vscode.setState({ count });
}, 100);
getState
和 setState
是持久化狀態的首選方式,因為它們的效能開銷遠低於 retainContextWhenHidden
。
序列化
透過實現 WebviewPanelSerializer
,您的 webview 可以在 VS Code 重啟時自動恢復。序列化基於 getState
和 setState
,並且僅當您的擴充套件為您的 webview 註冊了 WebviewPanelSerializer
時才啟用。
為了讓我們的程式設計貓在 VS Code 重啟後持久存在,首先在擴充套件的 package.json
中新增一個 onWebviewPanel
啟用事件
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
此啟用事件確保每當 VS Code 需要恢復 viewType 為 catCoding
的 webview 時,我們的擴充套件都將被啟用。
然後,在擴充套件的 activate
方法中,呼叫 registerWebviewPanelSerializer
註冊一個新的 WebviewPanelSerializer
。WebviewPanelSerializer
負責從其持久化狀態恢復 webview 的內容。此狀態是 webview 內容使用 setState
設定的 JSON blob。
export function activate(context: vscode.ExtensionContext) {
// Normal setup...
// And make sure we register a serializer for our webview type
vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
}
class CatCodingSerializer implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
// `state` is the state persisted using `setState` inside the webview
console.log(`Got state: ${state}`);
// Restore the content of our webview.
//
// Make sure we hold on to the `webviewPanel` passed in here and
// also restore any event listeners we need on it.
webviewPanel.webview.html = getWebviewContent();
}
}
現在,如果您在開啟貓編碼面板的情況下重啟 VS Code,面板將自動恢復到相同的編輯器位置。
retainContextWhenHidden
對於 UI 或狀態非常複雜且無法快速儲存和恢復的 webview,您可以改用 retainContextWhenHidden
選項。此選項使 webview 保留其內容,但處於隱藏狀態,即使 webview 本身不再位於前臺。
儘管 Cat Coding 很難說具有複雜狀態,但讓我們嘗試啟用 retainContextWhenHidden
,看看此選項如何改變 webview 的行為
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!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" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
請注意,當 webview 隱藏然後恢復時,計數器現在不會重置。無需額外程式碼!使用 retainContextWhenHidden
,webview 的行為類似於 Web 瀏覽器中的後臺選項卡。指令碼和其他動態內容即使在選項卡不活動或不可見時也會繼續執行。當啟用 retainContextWhenHidden
時,您還可以向隱藏的 webview 傳送訊息。
儘管 retainContextWhenHidden
可能很吸引人,但請記住,這會帶來高記憶體開銷,並且只有在其他持久化技術不起作用時才應使用。
輔助功能
在使用者使用螢幕閱讀器操作 VS Code 的上下文中,類 vscode-using-screen-reader
將被新增到您的 webview 主體中。此外,在使用者表示希望減少視窗運動量的情況下,類 vscode-reduce-motion
將被新增到文件的主體元素中。透過觀察這些類並相應地調整您的渲染,您的 webview 內容可以更好地反映使用者的偏好。
後續步驟
如果您想了解更多關於 VS Code 擴充套件性的資訊,請嘗試以下主題