ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

vscode摸鱼插件开发:用Webview+TreeView+postMessage搭一个TaoToken配置骨架

vscode摸鱼插件开发:用Webview+TreeView+postMessage搭一个TaoToken配置骨架 1. 从「摸鱼插件」说起为什么需要 Webview TreeView 双视图VS Code 插件开发里TreeView 和 Webview 是两种最常见的 UI 形态。TreeView 适合做列表、目录、状态树天然嵌在侧边栏Webview 适合做富交互页面能跑完整的前端代码。很多摸鱼小工具、数据面板、配置中心本质上都是「侧边栏列条目 点开看详情」的结构也就是 TreeView 负责导航、Webview 负责内容。问题在于这两个视图是隔离的。TreeView 跑在扩展宿主进程里Webview 跑在一个沙箱化的 iframe 里两者不能直接共享变量也不能直接调用对方的函数。你想让 Webview 里的「上一页」按钮去触发扩展侧重新拉数据或者让扩展侧把新数据推给已经打开的 Webview就必须靠postMessage这条消息通道。这篇要交付的就是一套可以直接复制的骨架package.json里注册 TreeView 和 Webview 的视图片段、TreeDataProvider 的最小实现、Webview 与扩展之间的双向消息流、以及把 TaoToken 的统一 Key 和 API 通道写进settings.json的配置示例。最后给出 F5 调试和消息回环验证的具体动作。适合已经会写 Hello World 插件、但卡在「两个视图怎么通信」这一步的人。2. TaoToken 前置统一 Key 与 API 通道怎么接在写通信骨架之前先把「数据从哪来」这件事定下来。摸鱼插件要展示的内容不管是热搜列表还是详情最终都得有一个稳定的数据出口。我这边习惯把这类外部调用统一走 TaoToken 的 API 通道好处是 Key 只有一份插件配置里不用散落多个地址。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意 API 地址后面不加 UTM 参数直接作为 base URL 使用。你需要在控制台里创建一个 API Key这个 Key 就是插件settings.json里要填的那一项。创建 Key 的入口在控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 进去之后找到 API Keys 页面新建一个 Key 并复制。如果你后面要做长期编码或者 Agent 类的功能可以顺带看一下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它和单次调用是两条不同的使用路径。这里要强调一点插件里绝对不要把 Key 硬编码进源码。正确做法是让用户在 VS Code 设置里填插件通过vscode.workspace.getConfiguration读取。这样 Key 存在用户的 settings.json 里不会跟着插件包分发出去。3. 可复制配置package.json 视图注册与 settings.json3.1 package.json 里的视图注册片段先解决「侧边栏出现一个图标点开是一个树」这件事。在package.json的contributes字段里加两块viewsContainers定义活动栏上的容器views定义容器里的视图。{ contributes: { viewsContainers: { activitybar: [ { id: taoFishContainer, title: TaoFish, icon: resources/taofish.svg } ] }, views: { taoFishContainer: [ { id: taoFishTree, name: 摸鱼列表 } ] }, commands: [ { command: taoFish.openDetail, title: 打开详情 } ], configuration: { title: TaoFish, properties: { taoFish.apiKey: { type: string, default: , description: TaoToken API Key }, taoFish.baseUrl: { type: string, default: https://taotoken.net/api, description: TaoToken API 基址 }, taoFish.refreshInterval: { type: number, default: 300, description: 列表刷新间隔秒 } } } } }viewsContainers.activitybar里的id是容器标识views里用同一个id作为 key把taoFishTree挂进去。configuration段就是给用户填 Key 和刷新间隔的地方taoFish.baseUrl默认值直接写成 TaoToken 的 API 地址。3.2 settings.json 配置示例用户在 VS Code 的settings.json里填成这样{ taoFish.apiKey: sk-你的TaoToken密钥, taoFish.baseUrl: https://taotoken.net/api, taoFish.refreshInterval: 300 }插件侧读取的代码const config vscode.workspace.getConfiguration(taoFish); const apiKey config.getstring(apiKey, ); const baseUrl config.getstring(baseUrl, https://taotoken.net/api); const interval config.getnumber(refreshInterval, 300);这样 Key 和地址都从配置来换环境不用改代码。如果你还没建 Key去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 新建一个即可。4. 消息通道骨架TreeView 与 Webview 双向 postMessage4.1 TreeDataProvider 最小实现TreeView 的数据来源是一个实现了vscode.TreeDataProvider的类。核心是getChildren和getTreeItem两个方法。import * as vscode from vscode; interface FishItem { label: string; id: string; } class FishTreeProvider implements vscode.TreeDataProviderFishItem { private _onDidChangeTreeData new vscode.EventEmitterFishItem | undefined(); readonly onDidChangeTreeData this._onDidChangeTreeData.event; private items: FishItem[] []; refresh(data: FishItem[]) { this.items data; this._onDidChangeTreeData.fire(undefined); } getTreeItem(element: FishItem): vscode.TreeItem { const item new vscode.TreeItem(element.label); item.command { command: taoFish.openDetail, title: 打开详情, arguments: [element] }; return item; } getChildren(): FishItem[] { return this.items; } }getTreeItem里给每个节点挂了command点击时触发taoFish.openDetail并把当前节点作为参数传出去。这就是 TreeView 到 Webview 的入口。4.2 扩展侧创建 Webview 并建立消息通道在activate里注册命令创建 WebviewPanel然后挂上onDidReceiveMessage监听。export function activate(context: vscode.ExtensionContext) { const provider new FishTreeProvider(); vscode.window.createTreeView(taoFishTree, { treeDataProvider: provider }); context.subscriptions.push( vscode.commands.registerCommand(taoFish.openDetail, (node: FishItem) { const panel vscode.window.createWebviewPanel( taoFishDetail, node.label, vscode.ViewColumn.One, { enableScripts: true } ); const scriptUri panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, media, main.js) ); panel.webview.html !DOCTYPE html html langzh-CN headmeta charsetUTF-8 //head body div idroot加载中.../div script src${scriptUri}/script /body /html; // 扩展侧接收 Webview 发来的消息 panel.webview.onDidReceiveMessage((message) { switch (message.command) { case pageUp: // 重新拉数据后推回 Webview panel.webview.postMessage({ command: render, text: 上一页数据 }); break; case pageDown: panel.webview.postMessage({ command: render, text: 下一页数据 }); break; case refresh: panel.webview.postMessage({ command: render, text: 刷新后的数据 }); break; } }); // 扩展侧主动推初始数据 panel.webview.postMessage({ command: render, text: node.label }); }) ); }注意asWebviewUri这一步Webview 里的资源路径必须经过它转换直接写文件路径是不生效的这是新手最容易踩的坑。4.3 Webview 侧接收与发送Webview 里的main.js用acquireVsCodeApi拿到通信句柄监听message事件接收扩展侧数据用postMessage往扩展侧发。const vscode acquireVsCodeApi(); const root document.getElementById(root); window.addEventListener(message, (event) { const message event.data; if (message.command render) { root.textContent message.text; } }); document.getElementById(prev).addEventListener(click, () { vscode.postMessage({ command: pageUp }); }); document.getElementById(next).addEventListener(click, () { vscode.postMessage({ command: pageDown }); });对应的 HTML 里要有prev和next两个按钮。这样一条完整的回环就成立了Webview 点按钮 → 扩展侧onDidReceiveMessage收到 → 扩展侧postMessage推数据 → Webviewmessage事件收到 → 更新 DOM。5. 验证请求与消息回环F5 调试动作5.1 启动调试在.vscode/launch.json里配置扩展调试{ version: 0.2.0, configurations: [ { name: Run Extension, type: extensionHost, request: launch, args: [--extensionDevelopmentPath${workspaceFolder}] } ] }按 F5 会弹出一个新的「扩展开发宿主」窗口左侧活动栏能看到 TaoFish 图标。点开图标TreeView 应该显示你refresh进去的条目。5.2 验证消息回环点 TreeView 里的任意条目右侧打开 Webview。在 Webview 里点「上一页」观察扩展侧是否收到pageUp。最直接的验证方式是在onDidReceiveMessage里打日志panel.webview.onDidReceiveMessage((message) { console.log([扩展侧收到], message.command); // ... });在调试控制台里看到[扩展侧收到] pageUp同时 Webview 里的文字变成「上一页数据」说明双向通道都通了。如果只通了一半通常是enableScripts没开或者asWebviewUri用错了。5.3 验证 TaoToken 配置读取在扩展侧加一段读取配置的日志const config vscode.workspace.getConfiguration(taoFish); console.log([配置], config.get(baseUrl), config.get(apiKey) ? Key已填 : Key为空);F5 之后在调试控制台确认baseUrl是https://taotoken.net/apiKey 状态正确。这一步过了后面接真实请求就只是把fetch塞进pageUp分支的事。6. 本篇常见错排查Webview 白屏或脚本不执行。九成是enableScripts没设成true或者script标签的src直接写了本地路径。必须用panel.webview.asWebviewUri转换转换后的 URI 才是 Webview 能加载的。postMessage 发了但对面收不到。检查command字段名是否两边一致大小写敏感。扩展侧监听的是panel.webview.onDidReceiveMessageWebview 侧监听的是window.addEventListener(message)这两个不能写反。TreeView 不刷新。getChildren返回了新数组但界面没变是因为没有触发onDidChangeTreeData。每次数据更新后必须fire一次否则 VS Code 不知道要重绘。点击 TreeView 没反应。getTreeItem里的command必须在package.json的contributes.commands里注册过且registerCommand的字符串完全一致。少一个字符都不会触发。配置读出来是空。getConfiguration的 section 名要和package.json里configuration.title对应的前缀一致。这里用的是taoFish所以读的时候也必须是taoFish不能写成TaoFish。Webview 里 fetch 外部接口失败。Webview 有 CSP 限制跨域请求会被拦。正确做法是把请求放在扩展侧做拿到数据后用postMessage推给 WebviewWebview 只负责渲染。这也是为什么消息通道是这套骨架的核心。7. 下一步把骨架接上真实数据骨架跑通之后接数据就是填空题。在扩展侧的pageUp、pageDown、refresh分支里用fetch调 TaoToken 的 API把返回结果通过postMessage推给 Webview。请求头里带上从配置读出来的 Keyconst res await fetch(${baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: gpt-4o-mini, messages: [{ role: user, content: 用一句话介绍 VS Code 插件开发 }] }) }); const data await res.json(); panel.webview.postMessage({ command: render, text: data.choices[0].message.content });如果你想先在网页里验证模型通不通可以直接用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条消息试试确认 Key 和通道没问题再写进插件。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求格式和参数说明。长期做编码类插件的话Coding Plan 那条路径也值得看一眼和单次调用是分开计费的。这套骨架的价值不在于「摸鱼」本身而在于它把 TreeView 导航、Webview 渲染、postMessage 双向通信、配置读取这四件事串成了一条可复用的链路。你换成任何数据源结构都不用动。
返回列表