Webview API

webview API为开发者提供了完全自定义视图的能力,例如内置的Markdown插件使用了webview渲染Markdown预览文件。Webview也能用于构建比VS Code原生API支持构建的更加复杂的用户交互界面。

可以把webview看成是VS Code中的iframe,它可以渲染几乎全部的HTML内容,它通过消息机制和插件通信。这样的自由度令我们的webview非常强劲并将插件的潜力提升到了新的高度。

相关链接


使用的VS Code API

我应该用webview吗?


webview虽然很赞,但是我们应该节制地使用这个功能——比如当VS Code原生API不够用时。Webview重度依赖资源,所以它脱离插件的进程而单独运行在其他环境中。在VS Code中使用设计不良的webview会让用户抓狂。

在使用webview之前,请作以下考虑:

  • 这个功能真的需要VS Code来提供吗?分离成一个应用或者网站会不会更好?
  • webview是实现这个特性的最后方案吗?VS Code原生API是否能达到同样的目的呢?
  • 你的webview所牺牲的高资源占用是否能换得同样的用户价值?

请记住:不要因为能使用webview而滥用webview。相反,如果你有充足的理由和自信,那么本篇教程对你来说会非常有用,现在就让我们开始吧。

Webviews API 基础


为了解释webviewAPI,我们先构建一个简单的Cat Coding插件。这个插件会用一个webview显示猫写代码的gif。随着我们不断了解API,我们会不断地给插件添加功能,包括我们的猫写了多少行代码的计数跟踪器,如果猫猫写出了bug还会有一个提示弹出框。

这是Cat Coding插件的第一版package.json,你可以在这里找到完整的代码。我们的第一版插件提供了一个命令,叫做catCoding.start。当用户从命令面板调用Cat Coding: Start new cat coding session,或者一个创建好的键绑定命令,我们的猫猫会出现在webview窗口内。

  1. {
  2. "name": "cat-coding",
  3. "description": "Cat Coding",
  4. "version": "0.0.1",
  5. "publisher": "bierner",
  6. "engines": {
  7. "vscode": "^1.23.0"
  8. },
  9. "activationEvents": ["onCommand:catCoding.start"],
  10. "main": "./out/src/extension",
  11. "contributes": {
  12. "commands": [
  13. {
  14. "command": "catCoding.start",
  15. "title": "Start new cat coding session",
  16. "category": "Cat Coding"
  17. }
  18. ]
  19. },
  20. "scripts": {
  21. "vscode:prepublish": "tsc -p ./",
  22. "compile": "tsc -watch -p ./",
  23. "postinstall": "node ./node_modules/vscode/bin/install"
  24. },
  25. "dependencies": {
  26. "vscode": "*"
  27. },
  28. "devDependencies": {
  29. "@types/node": "^9.4.6",
  30. "typescript": "^2.8.3"
  31. }
  32. }

现在让我们实现catCoding.start命令,在我们的主文件中,像下面这样注册一个基础的webview:

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(
  4. vscode.commands.registerCommand('catCoding.start', () => {
  5. // 创建并显示新的webview
  6. const panel = vscode.window.createWebviewPanel(
  7. 'catCoding', // 只供内部使用,这个webview的标识
  8. 'Cat Coding', // 给用户显示的面板标题
  9. vscode.ViewColumn.One, // 给新的webview面板一个编辑器视图
  10. {} // Webview选项。我们稍后会用上
  11. );
  12. })
  13. );
  14. }

vscode.window.createWebviewPanel函数创建并在编辑区展示了一个webview,下图显示了如果你试着运行catCoding.start命令会显示的东西:

一个空的webview

我们的命令以正确的标题打开了一个新的webview面板,但是没有任何内容!要想把我们的猫加到这个面板里面,我们需要webview.html设置HTML内容。

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(
  4. vscode.commands.registerCommand('catCoding.start', () => {
  5. // 创建和显示webview
  6. const panel = vscode.window.createWebviewPanel(
  7. 'catCoding',
  8. 'Cat Coding',
  9. vscode.ViewColumn.One,
  10. {}
  11. );
  12. // 设置HTML内容
  13. panel.webview.html = getWebviewContent();
  14. })
  15. );
  16. }
  17. function getWebviewContent() {
  18. return
  19. `
  20. <!DOCTYPE html>
  21. <html lang="en">
  22. <head>
  23. <meta charset="UTF-8">
  24. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  25. <title>Cat Coding</title>
  26. </head>
  27. <body>
  28. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  29. </body>
  30. </html>
  31. `;
  32. }

如果你再次运行命令,应该能看到下图:

含有html内容的webview

大功告成!

webview.html应该是一个完整的HTML文档。使用HTML片段或者格式错乱的HTML会造成异常。

更新webview内容

webview.html也能在webview创建后更新内容,让我们用猫猫轮播图使Cat Coding具有动态性:

  1. import * as vscode from 'vscode';
  2. const cats = {
  3. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  4. 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(
  8. vscode.commands.registerCommand('catCoding.start', () => {
  9. const panel = vscode.window.createWebviewPanel(
  10. 'catCoding',
  11. 'Cat Coding',
  12. vscode.ViewColumn.One,
  13. {}
  14. );
  15. let iteration = 0;
  16. const updateWebview = () => {
  17. const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
  18. panel.title = cat;
  19. panel.webview.html = getWebviewContent(cat);
  20. };
  21. // 设置初始化内容
  22. updateWebview();
  23. // 每秒更新内容
  24. setInterval(updateWebview, 1000);
  25. })
  26. );
  27. }
  28. function getWebviewContent(cat: keyof typeof cats) {
  29. return
  30. `
  31. <!DOCTYPE html>
  32. <html lang="en">
  33. <head>
  34. <meta charset="UTF-8">
  35. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  36. <title>Cat Coding</title>
  37. </head>
  38. <body>
  39. <img src="${cats[cat]}" width="300" />
  40. </body>
  41. </html>
  42. `;
  43. }

更新webview内容

因为webview.html方法替换了整个webview内容,页面看起来像重新加载了一个iframe。记住:如果你在webview中使用了脚本,那就意味着webview.html的重置会使脚本状态重置。

上述示例也使用了webview.title改变编辑器中的展示的文件名称,设置标题不会使webview重载。

生命周期

webview从属于创建他们的插件,插件必须保持住从webview返回的createWebviewPanel。如果你的插件失去了这个关联,它就不能再访问webview了,不过即使这样,webview还会继续展示在VS Code中。

因为webview是一个文本编辑器视图,所以用户可以随时关闭webview。当用户关闭了webview面板后,webview就被销毁了。在我们的例子中,销毁webview时抛出了一个异常,说明我们上面的示例中使用的seInterval实际上产生了非常严重的Bug:如果用户关闭了面板,setInterval会继续触发,而且还会尝试更新panel.webview.html,这当然会抛出异常。喵星人可不喜欢异常,我们现在就来解决这个问题吧。

onDidDispose事件在webview被销毁时触发,我们在这个事件结束之后更新并释放webview资源。

  1. import * as vscode from 'vscode';
  2. const cats = {
  3. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  4. 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(
  8. vscode.commands.registerCommand('catCoding.start', () => {
  9. const panel = vscode.window.createWebviewPanel(
  10. 'catCoding',
  11. 'Cat Coding',
  12. vscode.ViewColumn.One,
  13. {}
  14. );
  15. let iteration = 0;
  16. const updateWebview = () => {
  17. const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
  18. panel.title = cat;
  19. panel.webview.html = getWebviewContent(cat);
  20. };
  21. updateWebview();
  22. const interval = setInterval(updateWebview, 1000);
  23. panel.onDidDispose(
  24. () => {
  25. // 当面板关闭时,取消webview内容之后的更新
  26. clearInterval(interval);
  27. },
  28. null,
  29. context.subscriptions
  30. );
  31. })
  32. );
  33. }

插件也可以通过编程方式关闭webview视图——调用它们的dispose()方法。我们假设,现在限制我们的猫猫每天工作5秒钟:

  1. export function activate(context: vscode.ExtensionContext) {
  2. context.subscriptions.push(
  3. vscode.commands.registerCommand('catCoding.start', () => {
  4. const panel = vscode.window.createWebviewPanel(
  5. 'catCoding',
  6. 'Cat Coding',
  7. vscode.ViewColumn.One,
  8. {}
  9. );
  10. panel.webview.html = getWebviewContent(cats['Coding Cat']);
  11. // 5秒后,程序性地关闭webview面板
  12. const timeout = setTimeout(() => panel.dispose(), 5000);
  13. panel.onDidDispose(
  14. () => {
  15. // 在第五秒结束之前处理用户手动的关闭动作
  16. clearTimeout(timeout);
  17. },
  18. null,
  19. context.subscriptions
  20. );
  21. })
  22. );
  23. }

移动和可见性

当webview面板被移动到了非激活标签上,它就隐藏起来了。但这时并不是销毁,当重新激活标签后,VS Code会从webview.html自动恢复webview的内容。

webview自动恢复内容

.visible属性告诉你当前webview面板是否是可见的。

插件也可以通过调用reveal()方法,程序性地将webview面板激活。这个方法可以接受一个用于放置面板的目标视图布局。一个面板一次只能显示在一个编辑布局中。调用reveal()或者拖动webview面板到新的编辑布局中去。

在标签页中移动webview视图

现在更新我们的插件,一次只允许存在一个webview视图。如果面板处于非激活状态,那catCoding.start命令就把这个面板激活。

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 追踪当前webview面板
  3. let currentPanel: vscode.WebviewPanel | undefined = undefined;
  4. context.subscriptions.push(
  5. vscode.commands.registerCommand('catCoding.start', () => {
  6. const columnToShowIn = vscode.window.activeTextEditor
  7. ? vscode.window.activeTextEditor.viewColumn
  8. : undefined;
  9. if (currentPanel) {
  10. // 如果我们已经有了一个面板,那就把它显示到目标列布局中
  11. currentPanel.reveal(columnToShowIn);
  12. } else {
  13. // 不然,创建一个新面板
  14. currentPanel = vscode.window.createWebviewPanel(
  15. 'catCoding',
  16. 'Cat Coding',
  17. columnToShowIn,
  18. {}
  19. );
  20. currentPanel.webview.html = getWebviewContent(cats['Coding Cat']);
  21. // 当前面板被关闭后重置
  22. currentPanel.onDidDispose(
  23. () => {
  24. currentPanel = undefined;
  25. },
  26. null,
  27. context.subscriptions
  28. );
  29. }
  30. })
  31. );
  32. }

下面是一个新插件的行为:

在单个面板中展示

不论何时,如果webview的可见性改变了,或者当webview移动到了新的视图布局中,就会触发onDidChangeViewState。我们的插件可以利用这个时间改变布局中的webview显示的猫:

  1. const cats = {
  2. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  3. 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  4. 'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(
  8. vscode.commands.registerCommand('catCoding.start', () => {
  9. const panel = vscode.window.createWebviewPanel(
  10. 'catCoding',
  11. 'Cat Coding',
  12. vscode.ViewColumn.One,
  13. {}
  14. );
  15. panel.webview.html = getWebviewContent(cats['Coding Cat']);
  16. // 根据视图状态变动更新内容
  17. panel.onDidChangeViewState(
  18. e => {
  19. const panel = e.webviewPanel;
  20. switch (panel.viewColumn) {
  21. case vscode.ViewColumn.One:
  22. updateWebviewForCat(panel, 'Coding Cat');
  23. return;
  24. case vscode.ViewColumn.Two:
  25. updateWebviewForCat(panel, 'Compiling Cat');
  26. return;
  27. case vscode.ViewColumn.Three:
  28. updateWebviewForCat(panel, 'Testing Cat');
  29. return;
  30. }
  31. },
  32. null,
  33. context.subscriptions
  34. );
  35. })
  36. );
  37. }
  38. function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  39. panel.title = catName;
  40. panel.webview.html = getWebviewContent(cats[catName]);
  41. }

响应onDidChangeViewState事件

检查和调试webviews

在命令面板中输入Developer: Toggle Developer Tools能帮助你调试webview。运行命令之后会为当前可见的webview加载一个devtool:

Webview开发者工具

webview的内容是在webview文档中的一个iframe中的,用开发者工具检查和修改webview的DOM,在webview内调试脚本。

如果你用了webview开发者工具的console,确保你在Console面板左上角的下拉框里选中了当前激活窗体环境:

选择激活窗体

激活窗体环境是webview脚本执行的地方。

另外,Developer: Reload Webview命令会刷新所有已激活的webview。如果你需要重置一个webview的状态,这个命令会非常有用,或者你想要读取硬盘内容的webview更新一下,也可以使用这个方法。

加载本地内容


webview运行在独立的环境中,因此不能直接访问本地资源,这是出于安全性考虑的做法。这也意味着要想从你的插件中加载图片、样式等其他资源,或是从用户当前的工作区加载任何内容的话,你必须使用webview中的vscode-resource:协议。

vscode-resource:协议就像file:协议一样,不过它只允许访问本地文件。和file:一样的是,vscode-resource:只能从绝对路径中加载资源。

想象一下,我们想要从本地把喵喵们的gif打包进来,而不是从Giphy(国外出名的gif收集站)里加载进来。要想做到这点,我们首先给本地文件新建一个URI,然后用vscode-resource:协议更新这些URI:

  1. import * as vscode from 'vscode';
  2. import * as path from 'path';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(
  5. vscode.commands.registerCommand('catCoding.start', () => {
  6. const panel = vscode.window.createWebviewPanel(
  7. 'catCoding',
  8. 'Cat Coding',
  9. vscode.ViewColumn.One,
  10. {}
  11. );
  12. // 获取磁盘上的资源路径
  13. const onDiskPath = vscode.Uri.file(
  14. path.join(context.extensionPath, 'media', 'cat.gif')
  15. );
  16. // 获取在webview中使用的特殊URI
  17. const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
  18. panel.webview.html = getWebviewContent(catGifSrc);
  19. })
  20. );
  21. }

catGifSrc的值最后会像这样:

  1. vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif

默认情况下,scode-resource:只能访问下列地址的资源:

  • 你的插件安装的目录
  • 用户当前激活的工作区

你也可以用data URI将资源直接嵌套到webview中去。

控制本地资源访问

使用localResourceRoots选项,webview可以控制vscode-resource:加载的的资源。 localResourceRoots定义了可能被加载的本地内容的根URI。

我们用localResourceRoots去约束Cat Codingwebview只加载我们插件的media目录下的内容:

  1. import * as vscode from 'vscode';
  2. import * as path from 'path';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(
  5. vscode.commands.registerCommand('catCoding.start', () => {
  6. const panel = vscode.window.createWebviewPanel(
  7. 'catCoding',
  8. 'Cat Coding',
  9. vscode.ViewColumn.One,
  10. {
  11. // 只允许webview加载我们插件的`media`目录下的资源
  12. localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
  13. }
  14. );
  15. const onDiskPath = vscode.Uri.file(
  16. path.join(context.extensionPath, 'media', 'cat.gif')
  17. );
  18. const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
  19. panel.webview.html = getWebviewContent(catGifSrc);
  20. })
  21. );
  22. }

为了禁止所有的本地资源,只要把localResourceRoots设为[]就好了。

通常来说,webview应该和加载本地资源一样严格,然而,vscode-resourcelocalResourceRoots并不保证百分百的安全性。请确保你的webview遵循安全性最佳实践,强烈建议考虑添加一个内容安全政策以便约束之后加载的内容。

给webview内容加上主题

webview可以基于当前的VS Code主题和CSS改变自身的样式。VS Code将主题分成3中类别,而且在body元素上加上了特殊类名以表明当前主题:

  • vscode-light——亮色主题
  • vscode-dark——暗色主题
  • vscode-high-contrast——高反差主题

下列CSS改变了基于用户当前主题的webview字体颜色:

  1. body.vscode-light {
  2. color: black;
  3. }
  4. body.vscode-dark {
  5. color: white;
  6. }
  7. body.vscode-high-contrast {
  8. color: red;
  9. }

当开发一个webview应用的时候,请保证应用能在三种主题下都可以运作,务必在高反差模式下测试你的webview,以便有视觉障碍的用户也能正常使用。

webview可以通过CSS variables访问VS Code主题,这些变量以vscode为前缀,并且用-替代了.,例如editor.foreground变成了var(--vscode-editor-foreground)

  1. code {
  2. color: var(--vscode-editor-foreground);
  3. }

更多可用的主题变量,参阅主题色彩

下面也定义了一些与字体有关的变量:

  • -vscode-editor-font-family - 编辑器的文字类型(设置中的editor.fontFamily配置项)
  • -vscode-editor-font-weight - 编辑器的文字粗细(设置中的editor.fontWeight配置项)
  • -vscode-editor-font-size - 编辑器文字大小(设置中的editor.fontSize配置项)

脚本和信息传递


既然webview就像iframe一样,也就是说它们也可以运行脚本,webview中的Javascript默认是禁用的,不过我们能用enableScripts: true打开它。

让我们写一段脚本,追踪我们家喵星人写代码的行数。运行一个基础脚本非常的容易,但是注意这个示例只作演示用途,在实践中,你的webview应该遵循内容安全政策,禁止行内脚本。

  1. import * as path from 'path';
  2. import * as vscode from 'vscode';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  6. // 在webview中启用脚本
  7. enableScripts: true
  8. });
  9. panel.webview.html = getWebviewContent();
  10. }));
  11. }
  12. function getWebviewContent() {
  13. return
  14. `
  15. <!DOCTYPE html>
  16. <html lang="en">
  17. <head>
  18. <meta charset="UTF-8">
  19. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  20. <title>Cat Coding</title>
  21. </head>
  22. <body>
  23. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  24. <h1 id="lines-of-code-counter">0</h1>
  25. <script>
  26. const counter = document.getElementById('lines-of-code-counter');
  27. let count = 0;
  28. setInterval(() => {
  29. counter.textContent = count++;
  30. }, 100);
  31. </script>
  32. </body>
  33. </html>
  34. `;
  35. }

在webview中运行脚本

哇!真是位高产的喵主子!

!> webveiw的脚本能做到任何普通网页脚本能做到的事情,但是webview运行在自己的上下文中,脚本不能访问VS Code API。

将插件的信息传递到webview

插件可以用webview.postMessage()将数据发送到它的webview中。这个方法能发送任何序列化的JSON数据到webview中,在webview中则通过message事件接受信息。

我们现在就来看看这个实现,在Cat Coding中新增一个命令来表示我们家的喵在重构他的代码(所以会减少代码总行数)。新增catCoding.doRefactor命令,利用postMessage把指示发送到webview中,webview中的window.addEventListener('message' event => { ... })则会处理这些信息:

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 现在只有一只喵喵程序员了
  3. let currentPanel: vscode.WebviewPanel | undefined = undefined;
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. if (currentPanel) {
  6. currentPanel.reveal(vscode.ViewColumn.One);
  7. } else {
  8. currentPanel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  9. enableScripts: true
  10. });
  11. currentPanel.webview.html = getWebviewContent();
  12. currentPanel.onDidDispose(() => { currentPanel = undefined; }, undefined, context.subscriptions);
  13. }
  14. }));
  15. // 我们新的命令
  16. context.subscriptions.push(vscode.commands.registerCommand('catCoding.doRefactor', () => {
  17. if (!currentPanel) {
  18. return;
  19. }
  20. // 把信息发送到webview
  21. // 你可以发送任何序列化的JSON数据
  22. currentPanel.webview.postMessage({ command: 'refactor' });
  23. }));
  24. }
  25. function getWebviewContent() {
  26. return
  27. `
  28. <!DOCTYPE html>
  29. <html lang="en">
  30. <head>
  31. <meta charset="UTF-8">
  32. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  33. <title>Cat Coding</title>
  34. </head>
  35. <body>
  36. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  37. <h1 id="lines-of-code-counter">0</h1>
  38. <script>
  39. const counter = document.getElementById('lines-of-code-counter');
  40. let count = 0;
  41. setInterval(() => {
  42. counter.textContent = count++;
  43. }, 100);
  44. // Handle the message inside the webview
  45. window.addEventListener('message', event => {
  46. const message = event.data; // The JSON data our extension sent
  47. switch (message.command) {
  48. case 'refactor':
  49. count = Math.ceil(count * 0.5);
  50. counter.textContent = count;
  51. break;
  52. }
  53. });
  54. </script>
  55. </body>
  56. </html>
  57. `;

向webview传递信息

将webview的信息传递到插件中

webview也可以把信息传递回对应的插件中,用VS Code API 为webview提供的postMessage函数我们就可以完成这个目标。调用webview中的acquireVsCodeApi获取VS Code API对象。这个函数在一个会话中只能调用一次,你必须保持住这个方法返回的VS Code API实例,然后再转交到需要调用这个实例的地方。

现在我们在Cat Coding添加一个弹出bug的警示框:

  1. export function activate(context: vscode.ExtensionContext) {
  2. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  3. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  4. enableScripts: true
  5. });
  6. panel.webview.html = getWebviewContent();
  7. // 处理webview中的信息
  8. panel.webview.onDidReceiveMessage(message => {
  9. switch (message.command) {
  10. case 'alert':
  11. vscode.window.showErrorMessage(message.text);
  12. return;
  13. }
  14. }, undefined, context.subscriptions);
  15. }));
  16. }
  17. function getWebviewContent() {
  18. return
  19. `
  20. <!DOCTYPE html>
  21. <html lang="en">
  22. <head>
  23. <meta charset="UTF-8">
  24. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  25. <title>Cat Coding</title>
  26. </head>
  27. <body>
  28. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  29. <h1 id="lines-of-code-counter">0</h1>
  30. <script>
  31. (function() {
  32. const vscode = acquireVsCodeApi();
  33. const counter = document.getElementById('lines-of-code-counter');
  34. let count = 0;
  35. setInterval(() => {
  36. counter.textContent = count++;
  37. // Alert the extension when our cat introduces a bug
  38. if (Math.random() < 0.001 * count) {
  39. vscode.postMessage({
  40. command: 'alert',
  41. text: '🐛 on line ' + count
  42. })
  43. }
  44. }, 100);
  45. }())
  46. </script>
  47. </body>
  48. </html>
  49. `;
  50. }

从webview向插件传递信息

出于安全性考虑,你必须保证VS Code API的私有性,也不会泄露到全局状态中去。

安全性


每一个你创建的webview都必须遵循这些基础的安全性最佳实践。

限制能力

webview应该留有它所需的最小功能集合即可。例如:如果你的webview不需要运行脚本,就不要设置enableScripts: true。如果你的webview不需要加载用户工作区的资源,就把localResourceRoots设置为[vscode.Uri.file(extensionContext.extensionPath)]或者[]以便禁止访问任何本地资源。

内容安全策略

内容安全策略可以进一步限制webview可以加载和执行的内容。例如:内容安全策略强制可以运行在webview中的脚本白名单,或者告诉webview只加载带https协议的图片。

要想加上内容安全策略,将<meta http-equiv="Content-Security-Policy">指令放到webview的<head>

  1. function getWebviewContent() {
  2. return
  3. `
  4. <!DOCTYPE html>
  5. <html lang="en">
  6. <head>
  7. <meta charset="UTF-8">
  8. <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
  9. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  10. <title>Cat Coding</title>
  11. </head>
  12. <body>
  13. ...
  14. </body>
  15. </html>
  16. `;
  17. }

default-src 'none';策略直接禁止了所有内容。我们可以按插件需要的最少内容修改这个指令,如只允许通过https加载本地脚本、样式和图片:

  1. <meta
  2. http-equiv="Content-Security-Policy"
  3. content="default-src 'none'; img-src vscode-resource: https:; script-src vscode-resource:; style-src vscode-resource:;"
  4. >

上述策略也隐式地禁用了内联脚本和样式。把内联样式和脚本提取到外部文件中是一个非常好的实践,也不会与内容安全策略冲突。

只通过https加载内容

如果你的webview允许加载外部资源,我们强烈建议你只允许通过https加载而不要使用http,上面的例子已经用内容安全策略展示了使用https的方式。

审查用户输入

就像构建普通HTML页面一样,你也同样需要在webview中审查用户输入的内容。 没有审查输入内容可能会导致内容注入,也就意味着将用户置于了危险之中。

可能需要审查的值:

  • 文件内容
  • 文件和文件夹路径
  • 用户工作区设置

可以考虑用一个辅助库去构建HTML模板,或者确保所有来自用户工作区的内容都通过了审查

只依赖审查内容的安全性是不够的,你也要遵循其他安全性的最佳实践,尽可能减少潜在的内容注入。

持久性


在webview的标准生命周期中,createWebviewPanel负责创建和销毁(用户关闭或者调用.dispose()方法)webview。而webview的内容再是在webview可见时创建的,在webview处于非激活状态时销毁。webview处于非激活标签中时,任何webview中的保留的状态都会丢失。

所以最好减少webview中的状态,取而代之用消息传递储存状态。

getState和setState

运行在webview中的脚本可以使用getStatesetState方法保存和恢复JSON序列化的状态对象。这个状态可以一直保留,即使webview面板已经被隐藏,只有当它销毁时,状态则会一起销毁。

  1. // webview中的脚本
  2. const vscode = acquireVsCodeApi();
  3. const counter = document.getElementById('lines-of-code-counter');
  4. // 检查是否需要恢复状态
  5. const previousState = vscode.getState();
  6. let count = previousState ? previousState.count : 0;
  7. counter.textContent = count;
  8. setInterval(() => {
  9. counter.textContent = count++;
  10. // 更新已经保存的状态
  11. vscode.setState({ count })
  12. }, 100);

getStatesetState是用来保存状态的比较好的办法,因为他们的性能消耗要远低于retainContextWhenHidden

序列化

使用WebviewPanelSerializer之后,你的webview可以在VS Code关闭后自动恢复。序列化构建于getStatesetState之上,只有你的插件注册了WebviewPanelSerializer,这个功能才会生效。

给插件的package.json添加一个onWebviewPanel激活事件,然后我们的代码喵就能在VS Code重启后继续工作了:

  1. "activationEvents": [
  2. ...,
  3. "onWebviewPanel:catCoding"
  4. ]

这个激活事件确保我们的插件不论VS Code何时恢复catCodingwebview时都会启动。

然后在我们插件的activate方法中调用registerWebviewPanelSerializer注册一个新的WebviewPanelSerializer,这个函数负责恢复webview之前保存的内容。其中的state就是webview用setState设置的JSON格式的状态。

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 常见设置...
  3. // 确保我们注册了一个序列化器
  4. vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
  5. }
  6. class CatCodingSerializer implements vscode.WebviewPanelSerializer {
  7. async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
  8. // `state`是webview内调用`setState`保留住的
  9. console.log(`Got state: ${state}`);
  10. // 恢复我们的webview内容
  11. //
  12. // 确保我们将`webviewPanel`传递到了这里
  13. // 然后用事件侦听器恢复我们的内容
  14. webviewPanel.webview.html = getWebviewContent();
  15. }
  16. }

在VS Code中打开一个喵喵打代码的面板,关闭后重启就能看到这个面板恢复到了之前的状态和位置。

隐藏时保留上下文

如果webview的视图非常复杂,或者状态不能很快地保存和恢复,你则可以用retainContextWhenHidden选项,这个选项在不可见的状态中保存了webview的内容,即使webview本身不处于激活状态。

虽然Cat Coding说不上有很复杂的状态,不过我们可以打开retainContextWhenHidden看看webview的行为会发生什么变化:

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(
  4. vscode.commands.registerCommand('catCoding.start', () => {
  5. const panel = vscode.window.createWebviewPanel(
  6. 'catCoding',
  7. 'Cat Coding',
  8. vscode.ViewColumn.One,
  9. {
  10. enableScripts: true,
  11. retainContextWhenHidden: true
  12. }
  13. );
  14. panel.webview.html = getWebviewContent();
  15. })
  16. );
  17. }
  18. function getWebviewContent() {
  19. return
  20. `
  21. <!DOCTYPE html>
  22. <html lang="en">
  23. <head>
  24. <meta charset="UTF-8">
  25. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  26. <title>Cat Coding</title>
  27. </head>
  28. <body>
  29. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  30. <h1 id="lines-of-code-counter">0</h1>
  31. <script>
  32. const counter = document.getElementById('lines-of-code-counter');
  33. let count = 0;
  34. setInterval(() => {
  35. counter.textContent = count++;
  36. }, 100);
  37. </script>
  38. </body>
  39. </html>
  40. `;
  41. }

持久化保留

我们可以注意到计数器没有重置,webview隐藏之后就恢复了。而且不需要多余的代码!retainContextWhenHidden的行为就像浏览器一样,脚本和其他内容被暂时挂起,但是一旦webview可见之后就会立即恢复。但是在webview隐藏状态下,你还是不能给它发送消息的。

虽然retainContextWhenHidden很吸引人,但是记住这个功能的内容占用很高,只有其他的持久化技术无能为力之时再选择这种方式。

下一步

如果你想了解学习更多VS Code扩展性的内容,请查看下列主题: