LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

VS Code 插件 Webview 热更新配置

zhenglin
2025年12月3日 10:33 本文热度 164

做过 VS Code 插件开发的同学应该都有这个体会:每次改完 Webview 的代码,都得手动刷新才能看到效果,有时候甚至要重启整个插件。

最近在做项目的时候,也是深感没有热更新的痛苦,所以查了一些资料,解决了这个问题,下面分享一下解决过程,希望对你有用:



问题在哪

先说说为什么 Webview 不能像普通 Web 项目那样用 HMR。

VS Code 的 Webview 本质上是一个受限的 iframe 环境。它的 HTML 是通过字符串注入的,不是从服务器加载的。再加上 CSP 安全策略的限制,传统的热更新方案根本用不了。

没有热更新的情况下,开发流程大概是这样的:


改代码 → 等编译 → 切到调试窗口 → 手动刷新 Webview → 看效果

如果是频繁调试 UI 样式,这个流程能把人逼疯。



思路

既然没法用传统方案,那就换个思路:监听编译产物的变化,变了就重新设置一遍 webview.html

具体来说:

  1. 用 VS Code 的 FileSystemWatcher 监听 dist/webview.js 文件

  2. 文件一变,就重新生成 HTML 并赋值给 webview

  3. 加个防抖,避免编译过程中频繁触发


流程图大概是这样:


源码修改

    ↓

esbuild watch 编译

    ↓

dist/webview.js 更新

    ↓

FileSystemWatcher 触发

    ↓

防抖 300ms

    ↓

重设 webview.html

具体实现

构建配置

首先确保构建工具支持 watch 模式。我用的是 esbuild,配置大概是这样:


// build.js

const esbuild = require("esbuild");


const isWatch = process.argv.includes('--watch');


async function build() {

  const ctx = await esbuild.context({

    entryPoints: ['src/webview/index.tsx'],

    bundle: true,

    outfile: 'dist/webview.js',

    platform: 'browser',

    loader: {

      '.tsx': 'tsx',

      '.css': 'css',

    },

  });


  if (isWatch) {

    await ctx.watch();

    console.log('Watching...');

  } else {

    await ctx.rebuild();

    await ctx.dispose();

  }

}


build();

package.json 里加上对应的脚本:


{

  "scripts": {

    "dev": "node build.js --watch",

    "build": "node build.js"

  }

}

Provider 改造

重点在 WebviewViewProvider 里面。这里给一个简化版的实现:



import * as vscode from 'vscode';


export class MyWebviewProvider implements vscode.WebviewViewProvider {

  private view?: vscode.WebviewView;

  private watcher?: vscode.FileSystemWatcher;


  constructor(

    private extensionUri: vscode.Uri,

    private context: vscode.ExtensionContext

  ) {

    this.setupHotReload();

  }


  resolveWebviewView(webviewView: vscode.WebviewView) {

    this.view = webviewView;

    

    webviewView.webview.options = {

      enableScripts: true,

      localResourceRoots: [this.extensionUri]

    };

    

    webviewView.webview.html = this.getHtml(webviewView.webview);

  }


  // 热更新的核心逻辑

  private setupHotReload() {

    // 只在开发模式下启用

    if (this.context.extensionMode !== vscode.ExtensionMode.Development) {

      return;

    }


    const pattern = new vscode.RelativePattern(

      this.extensionUri,

      'dist/webview.{js,css}'

    );

    

    this.watcher = vscode.workspace.createFileSystemWatcher(pattern);


    // 防抖处理

    let timer: NodeJS.Timeout;

    const reload = () => {

      clearTimeout(timer);

      timer = setTimeout(() => {

        if (this.view) {

          this.view.webview.html = this.getHtml(this.view.webview);

        }

      }, 300);

    };


    this.watcher.onDidChange(reload);

    this.context.subscriptions.push(this.watcher);

  }


  private getHtml(webview: vscode.Webview): string {

    const scriptUri = webview.asWebviewUri(

      vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js')

    );

    

    const nonce = this.getNonce();


    return `<!DOCTYPE html>

<html>

<head>

  <meta charset="UTF-8">

  <meta http-equiv="Content-Security-Policy" 

        content="default-src 'none'; script-src 'nonce-${nonce}';">

</head>

<body>

  <div id="root"></div>

  <script nonce="${nonce}" src="${scriptUri}"></script>

</body>

</html>`;

  }


  private getNonce(): string {

    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    let result = '';

    for (let i = 0; i < 32; i++) {

      result += chars.charAt(Math.floor(Math.random() * chars.length));

    }

    return result;

  }

}

代码不复杂,关键点有几个:


开发模式判断

extensionMode 是 VS Code 提供的 API,可以区分插件是正式安装的还是调试运行的。我们只在调试模式下启用热更新,避免影响生产环境。


文件监听

FileSystemWatcher 监听编译产物的变化。用 glob 模式可以同时监听 js 和 css 文件。


防抖

esbuild 编译时可能会触发多次文件变化事件,加个 300ms 的防抖可以避免频繁刷新。


重新生成 nonce

每次刷新都要生成新的 nonce 值,不然 CSP 会阻止脚本执行。


备用方案:手动刷新命令

有时候自动刷新可能不生效(比如 watcher 没正常触发),这时候可以加一个手动刷新的命令作为兜底。

在 package.json 的 contributes 里注册命令:



{

  "contributes": {

    "commands": [{

      "command": "myExtension.reloadWebview",

      "title": "Reload Webview"

    }]

  }

}

然后在代码里注册处理函数就行了。



使用方法

  1. 终端运行 npm run dev 启动 watch 模式

  2. ​按 F5 启动插件调试

  3. 改代码,保存,等一两秒,Webview 自动刷新


实测下来效果还不错,基本能满足日常开发需求。


几个坑

状态丢失

Webview 刷新后状态会丢失。如果有需要保留的状态,得用 vscode.getState()vscode.setState() 来持久化。


缓存问题

有时候改了代码但效果没变,可能是浏览器缓存了旧的 js 文件。可以在 script src 后面加个时间戳参数来强制刷新:


const uri = `${scriptUri}?t=${Date.now()}`;

CSP 报错

如果控制台报 CSP 相关的错误,检查一下 nonce 是不是每次都重新生成了。


为什么不做真正的 HMR

可能有人会问,能不能做到像 Vite 那样的真正 HMR,改了代码组件状态还能保留?

理论上可以,但实现成本很高,而且收益有限。


真正的 HMR 需要在 Webview 端配合:不重设整个 HTML,而是通过 postMessage 通知 Webview,让它动态替换 script 标签,或者接入 React Fast Refresh 之类的运行时。

但问题来了:首先是 CSP 限制太严。Webview 默认禁止 eval 和动态脚本执行,而 React Fast Refresh 依赖的一些机制恰好需要这些能力。你可以放宽 CSP,但那样就牺牲了安全性。


其次是通信机制的限制。普通 Web 项目的 HMR 依赖 WebSocket 保持长连接,但 Webview 里没有这个条件,只能用 postMessage 来回传消息。要在这个基础上实现一套 HMR runtime,工作量不小。


最后是投入产出比的问题。插件的 Webview 通常不会特别复杂,整页刷新也就 1-2 秒的事。为了省这点时间去折腾一套 HMR 方案,性价比实在不高。

如果你确实需要保留状态,更务实的做法是好好利用 VS Code 提供的状态 API:


// Webview 端保存状态

const vscode = acquireVsCodeApi();


// 保存

vscode.setState({ formData: {...}, activeTab: 'settings' });


// 恢复(页面加载时)

const state = vscode.getState();

if (state) {

  // 用 state 恢复界面

}

这个方案简单直接,刷新后状态自动恢复,用户体验也不差。


总结

这个方案的核心就是利用 FileSystemWatcher 监听编译产物,然后重设 HTML 触发刷新。实现起来不算复杂,但确实能明显改善开发体验。

 当然这个方案也有局限性,比如没法做到真正的 HMR(保留组件状态的热替换)。不过对于大多数场景来说,整页刷新已经够用了。



参考文章:原文链接


该文章在 2025/12/3 10:33:10 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved