Skip to main content

Webview_VSCode Plugin Development Notes 5

Free2019-12-14#Tool#VSCode webview#VSCode插件嵌入网页#VSCode插件嵌入HTML#VSCode内嵌浏览器#VSCode内置浏览器

Webview opens a door for VSCode, but security restrictions close it again

Preface

Webview opens a new door:

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.

It allows VS Code extensions to create complex UIs by rendering HTML, not limited to its API support. This flexibility gives extensions more possibilities:

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

I. vscode.previewHtml Command

Early on, the vscode.previewHtml command was used to render HTML content:

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

Essentially an iframe (see html preview part and command for details), used to support built-in Markdown preview and other features

Later, security and compatibility issues were encountered:

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.

So the Webview API was adopted as a replacement:

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

II. Webview API

Compared to previewHtml, Webview is safer, but also more resource-intensive:

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

Its runtime environment is Electron's native Webview tag. Compared to iframe, the biggest difference is that Webview runs in a separate process with stronger security isolation:

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.

On the other hand, since using Webview has performance overhead, the official documentation repeatedly emphasizes "don't use it unless necessary":

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

And suggests considering 3 points before using Webview:

  • Does this feature really need to be in VS Code? Would it be more appropriate as a standalone application or website?

  • Is Webview the only way to achieve the target functionality? Can it be replaced with regular extension APIs?

  • Is the user value created worth the resources consumed by Webview?

III. Specific Usage

Specifically, create a Webview through vscode.window.createWebviewPanel:

// 1. Create and display Webview
const panel = vscode.window.createWebviewPanel(
  // Identifier for this webview, any string
  'catCoding',
  // Title of the webview panel, shown to the user
  'Cat Coding',
  // Column where the webview panel is located
  vscode.ViewColumn.One,
  // Other webview options
  {}
);

P.S. After creating the Webview panel, you can still modify the Tab title through webview.title

Then set the HTML content to be rendered in the Webview through webview.html:

// 2. Set the HTML content to be rendered in the webview
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>`;

Similar to vscode.previewHtml, the specified HTML content is ultimately loaded through an iframe, except this iframe is rendered by Webview. So, compared to the previous method, there's just an additional Webview layer to solve security problems

Lifecycle

After creating a Webview panel, there are 2 important lifecycle events:

  • Hide/Restore: onDidChangeViewState, triggered when visibility (webview.visible) changes, and when the Webview is dragged to a different column (panel.viewColumn), usually used to save/restore state

  • Dispose: onDidDispose, triggered when the panel is closed, used to complete cleanup tasks like stopping timers

Specially, Webview content is destroyed when entering the background and recreated when visible again:

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.

For example, after the user switches tabs, the content being displayed by the Webview is destroyed, and runtime state is also cleared. When the user switches back, or when the extension makes the Webview visible again through panel.reveal(), the Webview content reloads. When closed by the user, or closed by the extension through panel.dispose(), the Webview and its content are all destroyed

State Save and Restore

Therefore, Webview provides a mechanism to preserve state:

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

Can be used to restore Webview content, for example:

// webview
const vscode = acquireVsCodeApi();

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

// Get the previously saved state value
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;

setInterval(() => {
  counter.textContent = count++;
  // Write back when state value updates
  vscode.setState({ count });
}, 100);

P.S. Among them, acquireVsCodeApi is a global function injected into the Webview environment, used to access VS Code's provided APIs like getState

Note that state saved through setState() is destroyed when the Webview panel is closed (not persisted):

The state is destroyed when the webview panel is destroyed.

If you want to persist it, you also need to implement the WebviewPanelSerializer interface:

// package.json
// 1. Declare the extension activation method onWebviewPanel:viewType in package.json
"activationEvents": [
    ...,
    "onWebviewPanel:catCoding"
]

// extension.ts
// 2. Implement the WebviewPanelSerializer interface
vscode.window.registerWebviewPanelSerializer('catCoding',
  new class implements vscode.WebviewPanelSerializer {
    async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
      // Restore Webview content, state is the state saved through setState in the webview
      webviewPanel.webview.html = restoreMyWebview(state);
    }
  }
);

This way, VS Code can automatically restore Webview content after restart

Besides manual save and restore, another simple method is to set the retainContextWhenHidden option (passed as a parameter when calling createWebviewPanel), requiring the Webview to retain content when invisible (equivalent to suspending), but this brings significant performance overhead. Use this option with caution

Communication

Although Webview content runs in an isolated environment, VS Code provides a messaging mechanism between extensions and Webview, enabling two-way communication:

// Extension sends
webview.postMessage({ ... })
// Webview receives
window.addEventListener('message', event => { ... })

// Webview sends
const vscode = acquireVsCodeApi()
vscode.postMessage({ ... })
// Extension receives
webview.onDidReceiveMessage(
  message => { ... },
  undefined,
  context.subscriptions
);

Therefore, Webview state save and restore can be completely implemented manually if setState() and other APIs cannot meet the requirements

Theme Adaptation

Besides injecting JS to provide additional APIs, VS Code also presets some classes and CSS variables to support style adaptation

For example, body has 3 preset class values:

  • vscode-light: Light theme

  • vscode-dark: Dark theme

  • vscode-high-contrast: High contrast theme

You can use these three states to complete theme adaptation, for example:

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

And user-configured specific color values are also exposed through CSS variables:

--vscode-editor-foreground corresponds to editor.foreground
--vscode-editor-font-size corresponds to editor.fontSize

IV. Debugging

Webview runs in an independent environment and cannot be directly debugged through DevTools. For this, VS Code provides 2 commands:

  • Developer: Open Webview Developer Tools: Open DevTools for the currently visible Webview

  • Developer: Reload Webview: Reload all Webviews, reset their internal state, and reload local resources

DevTools for Webview can debug Webview content, just like opening DevTools through the Toggle Developer Tools command to debug VS Code's own UI

If Webview content loads local resources, you can reload them through the Reload Webview command without restarting the extension or reopening the Webview

V. Security Restrictions

Whether the previous vscode.previewHtml command or the current Webview API, there exist numerous security restrictions:

  • Navigation is not supported in Webview. Clicking a tags has no effect. It's recommended to implement navigation through extensions by modifying Webview content

  • Still limited by the iframe environment (just that the iframe is placed inside Webview). For example, pages with response headers containing X-Frame-Options: SAMEORIGIN settings cannot be loaded (see #76384, #70339 for details)

  • Some security options of Electron's webview tag are not enabled. Such as allow-modals, which prevents alert (see #67109 for details)

  • Loading local resources is restricted. By default, only the extension directory and opened workspace directory are allowed to be accessed, and must be converted through a specific API (webview.asWebviewUri), or set the local resource root path through the <base href="${mediaPath}"> tag (see #47631 for details)

For example, same-origin policy prevents loading some resources through iframe:

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

Such errors cannot be directly captured (see Catch error if iframe src fails to load), but you can try to access the resource before loading it through iframe, and only load it after confirming it's accessible:

fetch(url).then(() => {
  // Can be loaded through iframe
  frames[0].src = url;
}, () => {
  // Cannot be loaded through iframe, prompt the user
});

VI. Summary

Seemingly flexible and open but actually has extremely many restrictions. Currently (2019/12/14), VS Code's positioning of Webview capability is just an HTML renderer, as a supplement to UI extension capabilities:

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

(Excerpted from #72900, personally stated by the Webview API author)

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment