VsCode extension using webview and message posting
Tags: vscode, typescript, extension, webview
Published: 2020-12-12 08:03:40

In this article I will show how to create a simple VsCode extension that uses a webview with a html canvas to export graphics from the canvas to a PNG file.

![VsCode extension example](https://dev-to-uploads.s3.amazonaws.com/i/3j2b8nreiq9lxva5pzpt.gif)

When we have a webview panel in our extension then we cannot save data directly to the users disk from the html page or have download links inside the webview because it is contained in a sandbox for security reasons. 

To show how we can get around this barrier this example extension will create a html canvas and display a rectangle. The image data from the canvas will then be saved to the root folder of the current workspace when the user clicks a button. This will illustrate how the message posting can be used to send data from a html page in a webview back to the VsCode context.

I have created a [simple extension](https://code.visualstudio.com/api/get-started/your-first-extension) with a single command. When activated the command will create a [webview panel](https://code.visualstudio.com/api/extension-guides/webview) using the createWebView panel command. Then I created an eventlistener for catching the onDidReceiveMessage events that I will use to send data from my html canvas

Below you can see the code for creating the webview panel and setup the eventlistner.

```typescript
export function activate(context: vscode.ExtensionContext) {
  const vscodeTestDisposable = vscode.commands.registerCommand('vscodetest.webview', () => {
    const webviewPanel = vscode.window.createWebviewPanel(
      'vscodeTest',
      'VsCode test webview',
      vscode.ViewColumn.One,
      {
        enableScripts: true
      }
    );    
    webviewPanel.webview.onDidReceiveMessage(
      message => {
        switch (message.command) {
          case 'saveAsPng':
            saveAsPng(message.text);
            return;
        }
      },
      undefined,
      context.subscriptions
    );
    setHtmlContent(webviewPanel.webview, context);
  });
  context.subscriptions.push(vscodeTestDisposable);
}
```

In the eventlistener I call the function `saveToPng(...)` that will decode the received data from the html page and then save the data to a PNG file. The data is base64 encoded because the message posting can only transmit text data. So the Javascript inside the webview has to convert all binary data to a text format before posting the message. This example extension will use base64 encoded data for the posted messages.

Below you can see the method that decodes the received messages.

```typescript
function saveAsPng(messageText: string) {
  const dataUrl = messageText.split(',');
  if (dataUrl.length > 0) {
    const u8arr = Base64.toUint8Array(dataUrl[1]);
    const workspaceDirectory = getWorkspaceFolder();
    const newFilePath = path.join(workspaceDirectory, 'VsCodeExtensionTest.png');
    writeFile(newFilePath, u8arr, () => {
      vscode.window.showInformationMessage(`The file ${newFilePath} has been created in the root of the workspace.`);      
    });
  }
}
```

The html the webview is using for showing the html canvas is shown below. There are a few details I would like to highlight here. Because of security reasons you should have `Content-Security-Policy` metatag in the html that restricts the code executing in the extension to avoid code injection. Below you can see I use a script nonce. This is a random string that is generated each time the page is reloaded and it prevent external injection of foreign code into you html page. 
Also for security reasons all Javascript code and CSS styles should be contained in files. Inline code should be avoided. To allow the html to load files from the local drive you need to encode the uri's to the files using the `asWebviewUri(...)` method.

```typescript

function getNonce() {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

function setHtmlContent(webview: vscode.Webview, extensionContext: vscode.ExtensionContext) {
  let htmlContent = `<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src cspSource; script-src 'nonce-nonce';">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="vscodeTest.css" rel="stylesheet">
  </head>
  <body>
    <div id="buttons">
      <input type="button" id="saveAsPngButton" value="Save as png">
    </div>
    <div id="canvasSection"><canvas id="vscodeTestCanvas" /></div>
    <script type="text/javascript" src="vscodeTest.js"></script>
  </body>
</html>`;
  const jsFilePath = vscode.Uri.joinPath(extensionContext.extensionUri, 'javascript', 'vscodeTest.js');
  const visUri = webview.asWebviewUri(jsFilePath);
  htmlContent = htmlContent.replace('vscodeTest.js', visUri.toString());

  const cssPath = vscode.Uri.joinPath(extensionContext.extensionUri, 'stylesheet', 'vscodeTest.css');
  const cssUri = webview.asWebviewUri(cssPath);
  htmlContent = htmlContent.replace('vscodeTest.css', cssUri.toString());
  
  const nonce = getNonce();
  htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
  htmlContent = htmlContent.replace(/<script /g, `<script nonce="${nonce}" `);
  htmlContent = htmlContent.replace('cspSource', webview.cspSource);
  
  webview.html = htmlContent;
}
```

In the example webview html page I draw a simple rectangle on the canvas and then the user must click the 'Save as Png' button to get the image data posted back to the extension context to be saved.
As described in a previous section the data has to be base64 encoded, but this is fortunately built into the canvas, so when you call the `toBaseURL()` function it returns the image data as a PNG base64 encoded. All we need to do is create a message and then post it using the `postMessage(...)` function that is part of the VsCode JS API.

```javascript

function saveAsPng() {
  // Call back to the extension context to save the image to the workspace folder.
  const vscode = acquireVsCodeApi();
  vscode.postMessage({
    command: 'saveAsPng',
    text: canvas.toDataURL()
  });
}

const saveAsPngButton = document.getElementById('saveAsPngButton');
saveAsPngButton.addEventListener('click', saveAsPng);
```

I hope you have found the article interesting and can use some of the techniques I have illustrated.

If you have not yet tried to create a Visual Studio Code extension, there are a lot of good articles on the [VsCode website](https://code.visualstudio.com/api). To get started have a look at this [article](https://code.visualstudio.com/api/get-started/your-first-extension). 

The entire code for this example extension can be found on [GitHub: vscode-extension-webview](https://github.com/CoderAllan/vscode-extension-webview) 

The techniques used in this article can be seen in a real-world extension in the [AngularTools extension](https://github.com/CoderAllan/vscode-angulartools). I made a short introduction to the extension [in a previous article here on Dev.io](https://dev.to/coderallan/analyze-your-angular-app-15bi)