Skip to content

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()
    })
  }
}

最佳实践

  1. 安全性: 始终使用 CSP 和 nonce 来保护 WebView
  2. 性能: 避免频繁的消息传递,批量处理数据
  3. 用户体验: 提供加载状态和错误处理
  4. 资源管理: 正确清理事件监听器和定时器
  5. 状态持久化: 保存重要的 WebView 状态

相关 API

您的终极 AI 驱动 IDE 学习指南