WebView API
Trae IDE 的 WebView API 允许扩展创建自定义的 Web 界面,为用户提供丰富的交互体验。
概述
WebView 功能包括:
- 创建自定义 Web 界面
- 双向通信机制
- 安全沙箱环境
- 资源管理和生命周期控制
主要接口
WebviewPanel
typescript
interface WebviewPanel {
// 基本属性
readonly webview: Webview
readonly viewType: string
readonly title: string
readonly iconPath?: Uri | { light: Uri; dark: Uri }
// 状态管理
readonly active: boolean
readonly visible: boolean
// 事件
readonly onDidChangeViewState: Event<WebviewPanelOnDidChangeViewStateEvent>
readonly onDidDispose: Event<void>
// 操作方法
reveal(viewColumn?: ViewColumn, preserveFocus?: boolean): void
dispose(): void
}Webview
typescript
interface Webview {
// 内容管理
html: string
// 选项配置
options: WebviewOptions
// 通信
postMessage(message: any): Promise<boolean>
readonly onDidReceiveMessage: Event<any>
// 资源管理
asWebviewUri(localResource: Uri): Uri
cspSource: string
}创建 WebView
基本创建
typescript
import { window, ViewColumn } from 'trae-api'
// 创建 WebView 面板
const panel = window.createWebviewPanel(
'myWebview', // 视图类型
'My WebView', // 标题
ViewColumn.One, // 显示列
{
enableScripts: true, // 启用 JavaScript
retainContextWhenHidden: true // 隐藏时保持上下文
}
)
// 设置 HTML 内容
panel.webview.html = getWebviewContent()高级配置
typescript
const panel = window.createWebviewPanel(
'advancedWebview',
'Advanced WebView',
ViewColumn.Two,
{
enableScripts: true,
enableCommandUris: true,
localResourceRoots: [
Uri.joinPath(context.extensionUri, 'media'),
Uri.joinPath(context.extensionUri, 'out')
],
portMapping: [
{ webviewPort: 3000, extensionHostPort: 3000 }
]
}
)HTML 内容生成
基本 HTML 模板
typescript
function getWebviewContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My WebView</title>
<style>
body {
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
</style>
</head>
<body>
<h1>欢迎使用 WebView</h1>
<button id="sendMessage">发送消息</button>
<script>
const vscode = acquireVsCodeApi();
document.getElementById('sendMessage').addEventListener('click', () => {
vscode.postMessage({
command: 'hello',
text: '来自 WebView 的问候!'
});
});
</script>
</body>
</html>
`
}使用外部资源
typescript
function getWebviewContent(webview: Webview, extensionUri: Uri): string {
// 获取资源 URI
const scriptUri = webview.asWebviewUri(
Uri.joinPath(extensionUri, 'media', 'main.js')
)
const styleUri = webview.asWebviewUri(
Uri.joinPath(extensionUri, 'media', 'main.css')
)
// 生成随机 nonce 用于 CSP
const nonce = getNonce()
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleUri}" rel="stylesheet">
<title>My WebView</title>
</head>
<body>
<div id="app"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`
}
function getNonce(): string {
let text = ''
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}双向通信
从扩展发送消息到 WebView
typescript
// 扩展端
panel.webview.postMessage({
command: 'update',
data: { count: 42 }
})javascript
// WebView 端
window.addEventListener('message', event => {
const message = event.data
switch (message.command) {
case 'update':
document.getElementById('count').textContent = message.data.count
break
}
})从 WebView 发送消息到扩展
javascript
// WebView 端
const vscode = acquireVsCodeApi()
vscode.postMessage({
command: 'save',
data: { content: 'Hello World' }
})typescript
// 扩展端
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'save':
// 保存数据
saveData(message.data.content)
break
}
},
undefined,
context.subscriptions
)状态管理
WebView 状态持久化
javascript
// WebView 端 - 保存状态
const vscode = acquireVsCodeApi()
// 获取之前保存的状态
const previousState = vscode.getState()
// 保存新状态
vscode.setState({ count: 42, data: 'some data' })监听状态变化
typescript
// 监听 WebView 面板状态变化
panel.onDidChangeViewState(
e => {
const panel = e.webviewPanel
console.log(`WebView 现在${panel.active ? '激活' : '非激活'}`)
console.log(`WebView 现在${panel.visible ? '可见' : '不可见'}`)
},
null,
context.subscriptions
)
// 监听面板销毁
panel.onDidDispose(
() => {
console.log('WebView 面板已销毁')
// 清理资源
},
null,
context.subscriptions
)高级功能
自定义协议处理
typescript
// 注册自定义协议
const provider = new class implements WebviewUriHandler {
handleUri(uri: Uri): ProviderResult<Uri> {
// 处理自定义 URI
return Uri.parse(`https://example.com${uri.path}`)
}
}
context.subscriptions.push(
window.registerWebviewUriHandler('myscheme', provider)
)WebView 序列化
typescript
// 实现 WebView 序列化器
class MyWebviewSerializer implements WebviewPanelSerializer {
async deserializeWebviewPanel(
webviewPanel: WebviewPanel,
state: any
): Promise<void> {
// 恢复 WebView 状态
webviewPanel.webview.html = getWebviewContent()
// 恢复数据
webviewPanel.webview.postMessage({
command: 'restore',
state: state
})
}
}
// 注册序列化器
window.registerWebviewPanelSerializer(
'myWebview',
new MyWebviewSerializer()
)实用示例
文件预览器
typescript
class FilePreviewPanel {
private panel: WebviewPanel
private disposables: Disposable[] = []
constructor(
private readonly extensionUri: Uri,
private readonly filePath: string
) {
this.panel = window.createWebviewPanel(
'filePreview',
`预览: ${path.basename(filePath)}`,
ViewColumn.Two,
{
enableScripts: true,
localResourceRoots: [this.extensionUri]
}
)
this.panel.webview.html = this.getHtmlContent()
this.setupMessageHandling()
}
private setupMessageHandling() {
this.panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'refresh':
this.refreshContent()
break
}
},
null,
this.disposables
)
}
private async refreshContent() {
const content = await workspace.fs.readFile(Uri.file(this.filePath))
this.panel.webview.postMessage({
command: 'updateContent',
content: content.toString()
})
}
}最佳实践
- 安全性: 始终使用 CSP 和 nonce 来保护 WebView
- 性能: 避免频繁的消息传递,批量处理数据
- 用户体验: 提供加载状态和错误处理
- 资源管理: 正确清理事件监听器和定时器
- 状态持久化: 保存重要的 WebView 状态