Webview_VSCode插件开发笔记5

写在前面

Webview 开启了一扇新的大门:

The webview API allows extensions to create fully customizable views within Visual Studio Code. Webviews can also be used to build complex user interfaces beyond what VS Code’s native APIs support.

VS Code 插件能够通过渲染 HTML 来创建复杂 UI,而不仅限于其 API 支持,这种灵活性让插件有了更多的可能性:

This freedom makes webviews incredibly powerful, and opens up a whole new range of extension possibilities.

一.vscode.previewHtml 命令

早期通过vscode.previewHtml命令来渲染 HTML 内容:

// Render the html of the resource in an editor view.
vscode.commands.executeCommand(
  'vscode.previewHtml',
  uri,
  viewColumn,
  title
);

本质上是个iframe(具体见html preview part and command),用来支持内置的 Markdown 预览等功能

后来遇到了安全性和兼容性方面的问题:

However the vscode.previewHtml command suffered from some important security and compatibility issues that we determined could not be fixed without breaking existing users of the command.

遂改用 Webview API 替代:

The webview API is significantly easier to work with, correctly supports different filesystem setups, and webviews also offer many security benefits over htmlPreviews.

二.Webview API

比起previewHtmlWebview 更安全,但也更耗资源

Webviews are resource heavy and run in a separate context from normal extensions.

其运行环境是 Electron 的原生Webview 标签,与iframe相比,最大的区别在于 Webview 运行在独立进程中,安全隔离性更强:

Unlike an iframe, the webview runs in a separate process than your app. It doesn’t have the same permissions as your web page and all interactions between your app and embedded content will be asynchronous. This keeps your app safe from the embedded content.

另一方面,由于使用 Webview 存在性能负担,官方再三强调“术高莫用”:

Webviews are pretty amazing, but they should also be used sparingly and only when VS Code’s native API is inadequate.

并建议在使用 Webview 之前,考虑 3 点:

  • 该功能是否真的需要放在 VS Code 里?作为独立应用或者网站是不是更合适?

  • Webview 是实现目标功能的唯一方式吗?能用常规插件 API 替代吗?

  • 所能创造的用户价值对得起 Webview 所耗费的资源吗?

三.具体用法

具体的,通过vscode.window.createWebviewPanel创建 Webview:

// 1.创建并显示Webview
const panel = vscode.window.createWebviewPanel(
  // 该webview的标识,任意字符串
  'catCoding',
  // webview面板的标题,会展示给用户
  'Cat Coding',
  // webview面板所在的分栏
  vscode.ViewColumn.One,
  // 其它webview选项
  {}
);

P.S.Webview 面板创建之后,还可以通过webview.title修改 Tab 页标题

接着通过webview.html设置要在 Webview 内渲染的 HTML 内容:

// 2.设置webview所要渲染的HTML内容
panel.webview.html = `<!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>`;

vscode.previewHtml类似,所指定的 HTML 内容最终通过iframe来加载,只是这个iframe是由 Webview 渲染的。所以,与之前的方式相比,只是多了一层用来解决安全问题的 Webview环境

生命周期

Webview 面板在创建之后,还有 2 个重要的生命周期事件:

  • 隐藏/恢复:onDidChangeViewState,可见性(webview.visible)发生变化、以及 Webview 被拖放到不同分栏(panel.viewColumn)时触发,通常用来保存/恢复状态

  • 销毁:onDidDispose,面板被关掉时触发,用来完成一些清理工作,如停掉 timer

特殊的,Webview 进入后台时内容会被销毁,再次可见时重新创建这些内容

The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.

比如用户切换 Tab 后,Webview 正在显示的内容会被销毁,运行时状态也会被清除。用户切换回来,或者由插件通过panel.reveal()让 Webview 回到用户眼前时,Webview 内容会重新加载。而被用户关掉,或者由插件通过panel.dispose()关掉时,Webview 及其内容都会被销毁掉

状态保存与恢复

所以,Webview 提供了保留状态的机制:

// webview
vscode.getState({ ... })
vscode.setState({ ... })

可以用来恢复 Webview 内容,例如:

// webview
const vscode = acquireVsCodeApi();

const counter = document.getElementById('lines-of-code-counter');

// 取出之前保存的状态值
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;

setInterval(() => {
  counter.textContent = count++;
  // 状态值更新时写回去
  vscode.setState({ count });
}, 100);

P.S.其中,acquireVsCodeApi是注入到 Webview 环境的全局函数,用来访问 VS Code 提供的getState等 API

需要注意的是,通过setState()保存的状态会在 Webview 面板关闭时销毁(而不持久化保存):

The state is destroyed when the webview panel is destroyed.

如果想要持久化保留,还需要实现WebviewPanelSerializer接口:

// package.json
// 1. 在package.json中声明onWebviewPanel:viewType插件激活方式
"activationEvents": [
    ...,
    "onWebviewPanel:catCoding"
]

// extension.ts
// 2.实现WebviewPanelSerializer接口
vscode.window.registerWebviewPanelSerializer('catCoding',
  new class implements vscode.WebviewPanelSerializer {
    async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
      // 恢复Webview内容,state就是webview中通过setState保存的状态
      webviewPanel.webview.html = restoreMyWebview(state);
    }
  }
);

如此这般,VS Code 就能在重启后自动恢复 Webview 内容了

除手动保存恢复外,另一种简单办法是设置retainContextWhenHidden选项(createWebviewPanel时作为参数传入),要求 Webview 在不可见时仍保留内容(相当于挂起),但会带来较大的性能开销,建议慎用该选项

通信

Webview 内容虽然运行在隔离的环境,但 VS Code 在插件与 Webview 之间提供了消息机制,能够实现双向通信

// 插件 发
webview.postMessage({ ... })
// webview 收
window.addEventListener('message', event => { ... })

// webview 发
const vscode = acquireVsCodeApi()
vscode.postMessage({ ... })
// 插件 收
webview.onDidReceiveMessage(
  message => { ... },
  undefined,
  context.subscriptions
);

因此,Webview 状态的保存与恢复完全可以手动实现,如果setState()等 API 无法满足的话

主题适配

除了注入 JS 提供额外 API,VS Code 还预置了一些 class 以及 CSS 变量,用来支持样式适配

例如,body有 3 个预置的 class 值:

  • vscode-light:浅色主题

  • vscode-dark:深色主题

  • vscode-high-contrast:高对比度主题

可以借助这三个状态完成主题适配,例如:

body.vscode-light {
  color: black;
}
body.vscode-dark {
  color: white;
}
body.vscode-high-contrast {
  color: red;
}

并且,用户配置的具体色值也通过 CSS 变量透出来了:

--vscode-editor-foreground 对应 editor.foreground
--vscode-editor-font-size 对应 editor.fontSize

四.调试

Webview 运行在独立环境中,无法直接通过 DevTools 调试。为此,VS Code 提供了 2 个命令:

  • Developer: Open Webview Developer Tools:打开当前可见 Webview 的 DevTools

  • Developer: Reload Webview:reload 所有 Webview,重置其内部状态,重新读取本地资源

针对 Webview 的 DevTools 能够调试 Webview 内容,就像通过Toggle Developer Tools命令打开 DevTools 调试 VS Code 自身的 UI 一样

如果 Webview 内容中加载了本地资源,可以通过Reload Webview命令重新加载,而不必重启插件或重新打开 Webview

五.安全限制

无论是之前的vscode.previewHtml命令,还是现在的 Webview API,都存在着大量的安全限制

  • Webview 中不支持跳转。点击a标签没有反应,建议通过插件修改 Webview 内容曲线实现跳转

  • 仍然受限于iframe环境(只是iframe放到了 Webview 里)。例如,无法加载响应头含有X-Frame-Options: SAMEORIGIN设置的页面(具体见#76384#70339

  • Electron webview标签一些安全选项没有放开。如allow-modals,导致无法alert(具体见#67109

  • 加载本地资源受限,默认只允许访问插件目录、以及打开的工作空间目录,且需通过特定 API(webview.asWebviewUri)转换,或者通过<base href="${mediaPath}">标签设置本地资源根路径(具体见#47631

例如,同源策略导致无法通过iframe加载一些资源:

Refused to display ‘https://code.visualstudio.com/api/extension-guides/webview’ in a frame because it set ‘X-Frame-Options’ to ‘sameorigin’.

此类错误无法直接捕获(具体见Catch error if iframe src fails to load),但可以在通过iframe加载资源之前,尝试访问该资源,确认可访问才加载:

fetch(url).then(() => {
  // 可通过iframe加载
  frames[0].src = url;
}, () => {
  // 无法通过iframe加载,提示出来
});

六.总结

看似灵活开放实际限制极多,目前(2019/12/14),VS Code 对 Webview 能力的定位只是个 HTML 渲染器,作为 UI 扩展能力的补充:

You should think of the webview more as an html view (one that does not have any server or origin) rather than a webpage.

(摘自#72900,Webview API 作者亲述)

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code