~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月6日

我不知道的 VSCode 扩展(03)— Webview 与 Output Channel

上一篇介绍的菜单和树视图能覆盖大部分交互场景,但它们有一个共同的限制——只能展示 VSCode 预定义的 UI 组件。如果需要一个表单、一个图表、一个富文本编辑器,TreeView 就无能为力了。

一、当 TreeView 不够用的时候

上一篇介绍的菜单和树视图能覆盖大部分交互场景,但它们有一个共同的限制——只能展示 VSCode 预定义的 UI 组件。如果需要一个表单、一个图表、一个富文本编辑器,TreeView 就无能为力了。

这时候就需要 Webview——在 VSCode 内部嵌入一个完整的网页。Settings Sync、GitLens 的欢迎页面、Copilot Chat 的交互界面,底层都是 Webview。

而 Output Channel 则是另一端:不需要复杂 UI,只是想输出日志和状态信息。

二、Webview:在编辑器里跑一个网页

创建 Webview

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  let disposable = vscode.commands.registerCommand('my-ext.openPanel', () => {
    const panel = vscode.window.createWebviewPanel('myPanel', 'My Panel', vscode.ViewColumn.One, {
      enableScripts: true,
      retainContextWhenHidden: true,
    });

    panel.webview.html = getHtml();
  });

  context.subscriptions.push(disposable);
}

function getHtml(): string {
  return `<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Panel</title>
</head>
<body>
  <h1>Hello from Webview</h1>
  <button id="btn">Send to Extension</button>
  <script>
    const vscode = acquireVsCodeApi();
    document.getElementById('btn').addEventListener('click', () => {
      vscode.postMessage({ type: 'clicked', value: 42 });
    });
  </script>
</body>
</html>`;
}

createWebviewPanel 的参数:

viewType(第一个参数)— Webview 的类型标识,用于序列化/反序列化。

title — 面板标题栏显示的文字。

showOptions — 面板显示位置。ViewColumn.One 是第一列,ViewColumn.Beside 在当前编辑器旁边打开。

options 对象中有两个重要开关:

选项默认值作用
enableScriptsfalse是否允许运行 JavaScript
retainContextWhenHiddenfalse面板切换到后台时是否保持状态

这里有一个很多人会忽略的细节——retainContextWhenHiddenfalse 时,Webview 切到后台就会被销毁,切回来时重新创建。如果 Webview 里有用户填写的表单数据,不设 true 的话切个标签就丢了。但设为 true 会增加内存占用,所以只在确实需要时才开启。

Webview 与扩展的双向通信

Webview 运行在一个隔离的 iframe 中,和扩展代码在不同的执行环境里。两者之间只能通过消息传递通信,很像 window.postMessage 的模式。

Webview → 扩展:

Webview 端调用 acquireVsCodeApi() 获取通信对象,然后用 vscode.postMessage() 发消息:

const vscode = acquireVsCodeApi();
vscode.postMessage({ type: 'save', data: formData });

扩展端监听:

panel.webview.onDidReceiveMessage(
  (message) => {
    if (message.type === 'save') {
      // 处理保存逻辑
    }
  },
  undefined,
  context.subscriptions,
);

扩展 → Webview:

扩展端用 panel.webview.postMessage() 发消息:

panel.webview.postMessage({ type: 'update', data: newData });

Webview 端监听:

window.addEventListener('message', (event) => {
  const message = event.data;
  if (message.type === 'update') {
    renderData(message.data);
  }
});

换句话说,双方的通信模型是完全对称的——都是 postMessage + 事件监听。消息内容必须是可序列化的 JSON 对象。

安全性:CSP 和 localResourceRoots

Webview 默认禁止加载外部资源和执行内联脚本(即使 enableScriptstrue)。这是通过 Content Security Policy(CSP)实现的。

如果需要加载本地的 CSS、JS 文件(比如打包好的 React 应用),需要两步:

(1)配置 localResourceRoots,告诉 VSCode 允许从哪些目录加载资源:

const panel = vscode.window.createWebviewPanel('myPanel', 'My Panel', vscode.ViewColumn.One, {
  enableScripts: true,
  localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')],
});

(2)使用 asWebviewUri 转换路径。本地文件路径不能直接在 Webview 中使用,需要转成 Webview 专用的 URI:

const scriptUri = panel.webview.asWebviewUri(
  vscode.Uri.joinPath(context.extensionUri, 'media', 'main.js'),
);

panel.webview.html = `<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'none'; script-src ${panel.webview.cspSource};">
</head>
<body>
  <script src="${scriptUri}"></script>
</body>
</html>`;

cspSource 属性返回 Webview 的 CSP 源标识,用在 Content-Security-Policy 头中,确保只有来自合法源的脚本能执行。

不配置 CSP 也能跑,但等于没有安全防护。 一旦 Webview 加载了不可信的内容(比如渲染用户提供的 HTML),就可能被 XSS 攻击。

三、Output Channel:轻量的日志输出

不是所有信息都值得用 Webview 展示。对于日志、状态信息、调试输出,Output Channel 是更合适的选择——就是 VSCode 底部”输出”面板中的那些通道。

基本用法

export function activate(context: vscode.ExtensionContext) {
  const output = vscode.window.createOutputChannel('My Extension');

  output.appendLine('Extension activated');
  output.appendLine(`Workspace: ${vscode.workspace.rootPath}`);

  // 需要时显示输出面板
  output.show(true);

  context.subscriptions.push(output);
}

createOutputChannel 创建一个命名的输出通道,用户可以在”输出”面板的下拉菜单中切换不同的通道。

核心方法只有几个:

方法作用
append(text)追加文本,不换行
appendLine(text)追加文本并换行
clear()清空通道内容
show(preserveFocus?)显示输出面板。true = 焦点留在编辑器
dispose()销毁通道

LogOutputChannel:带日志级别的输出通道

从 VSCode 1.74 开始,createOutputChannel 支持第二个参数传入 { log: true },创建一个 LogOutputChannel

const logger = vscode.window.createOutputChannel('My Extension', { log: true });

logger.info('Extension started');
logger.warn('Config not found, using defaults');
logger.error('Failed to connect');
logger.debug('Request payload: ...');

LogOutputChannel 和普通 OutputChannel 的区别在于:每条日志自动带时间戳和级别标签,用户可以在输出面板中按级别过滤。

说白了,OutputChannelconsole.log 的升级版,LogOutputChannelwinston / pino 的轻量替代。对于扩展开发来说,大多数场景用 LogOutputChannel 就够了。

四、Webview vs Output Channel:怎么选

场景推荐原因
展示复杂 UI(表单、图表、预览)Webview需要自定义渲染
输出日志和调试信息Output Channel轻量、原生集成
实时展示构建/编译状态Output Channel流式追加,无需刷新
可视化配置编辑器Webview需要表单交互
扩展运行状态监控状态栏 + Output Channel状态栏显示摘要,Output Channel 显示详情

问题的关键在于——Webview 的成本远高于 Output Channel。每个 Webview 本质上是一个独立的 Chromium 渲染进程,内存开销在 30-100MB 级别。如果只是输出文本信息,用 Webview 就是杀鸡用牛刀。


本系列其他文章:

延伸阅读:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;