Webview API
webview API 允許擴充功能在 Visual Studio Code 內建立完全可自訂的檢視。例如,內建的 Markdown 擴充功能使用 webview 來呈現 Markdown 預覽。Webview 也可用於建構超出 VS Code 原生 API 支援的複雜使用者介面。
將 webview 視為 VS Code 內由您的擴充功能控制的 iframe
。webview 可以在此框架中呈現幾乎任何 HTML 內容,並使用訊息傳遞與擴充功能通訊。這種自由度使 webview 非常強大,並開啟了全新的擴充功能可能性。
多個 VS Code API 中使用了 Webview
- 使用
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 來顯示一隻貓正在編寫程式碼(可能在 VS Code 中)的 gif 動畫。當我們逐步了解 API 時,我們將繼續為擴充功能新增功能,包括一個計數器,用於追蹤我們的貓已編寫了多少行原始碼,以及在貓引入錯誤時通知使用者的通知。
以下是第一個版本的 Cat Coding 擴充功能的 package.json
。您可以在此處找到範例應用程式的完整程式碼。我們擴充功能的第一個版本貢獻了一個命令,名為 catCoding.start
。當使用者調用此命令時,我們將顯示一個簡單的 webview,其中包含我們的貓。使用者可以從命令面板中調用此命令,如Cat Coding:開始新的貓編碼會話,甚至可以為其建立鍵盤快速鍵(如果他們願意的話)。
{
"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
的範例實際上存在一個重要的錯誤:如果使用者關閉面板,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
);
})
);
}
擴充功能也可以透過對 webview 呼叫 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,然後將這些 URI 傳遞到 asWebviewUri
函數
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,本機內容可以從這些根 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,請先在新的 webview/context
區段下的 menus
中新增一個新項目。每個貢獻都採用 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。訊息會在 webview 內部透過標準 message
事件接收。
為了示範這一點,讓我們為 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 執行個體,並將其傳遞給任何需要使用它的其他函數。
我們可以使用 VS Code API 和 postMessage
在我們的 Cat Coding 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,
{
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 Worker
Web Worker 在 webview 內部受支援,但有一些重要的限制需要注意。
首先,worker 只能使用 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
載入影像。
若要新增內容安全性原則,請將 <meta http-equiv="Content-Security-Policy">
指令放在 webview <head>
的頂部
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:
載入影像來執行此操作。
清理所有使用者輸入
就像對待一般網頁一樣,在建構 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 重新啟動後仍然存在,請先將 onWebviewPanel
啟動事件新增至擴充功能的 package.json
"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 的行為類似於網頁瀏覽器中的背景索引標籤。即使索引標籤未處於活動狀態或不可見,指令碼和其他動態內容也會繼續執行。當啟用 retainContextWhenHidden
時,您也可以將訊息傳送到隱藏的 webview。
儘管 retainContextWhenHidden
可能很吸引人,但請記住,這會產生很高的記憶體負擔,並且僅應在其他持久性技術無法運作時使用。
協助工具
在使用者使用螢幕閱讀器操作 VS Code 的情況下,類別 vscode-using-screen-reader
將新增至您的 webview 的主要 body 中。此外,在使用者表示偏好減少視窗中移動量的情況下,類別 vscode-reduce-motion
將新增至文件的主要 body 元素。透過觀察這些類別並相應地調整您的呈現方式,您的 webview 內容可以更好地反映使用者的偏好。
後續步驟
如果您想進一步了解 VS Code 擴充性,請嘗試以下主題