本文主要为源码解析,代码较多。以熟悉 vscode api 为主。
一、目标
-
理解 webview 界面机制,如何创建一个 webview 以及渲染内容?
-
理解 vscode 与 webview 通信原理,webview 和 vscode 之间如何发送数据?
二、期望收获结果
-
学会在插件中使用 webview 能力渲染复杂的 UI 界面
-
能够实现 vscode 发送数据到 webview
三、分析过程
iceworks-project-creator 是 iceworks 套件之一,负责渲染一个模版选择界面。使用 vscode webview 能力渲染。
1. 创建一个 webview 界面
import * as vscode from 'vscode' export function activate(context: vscode.ExtensionContext) { const { extensionPath, subscriptions, globalState } = context let projectCreatorwebviewPanel: vscode.WebviewPanel | undefined // projectCreatorwebviewPanel是一个单例(饿汉) function activeProjectCreatorWebview() { if (projectCreatorwebviewPanel) { projectCreatorwebviewPanel.reveal() } else { projectCreatorwebviewPanel = window.createWebviewPanel( 'iceworks', // webview的 id标识 i18n.format( 'extension.iceworksProjectCreator.createProject.webViewTitle' ), // 这里是webview的title标题,iceworks内部自己封装了一个i18n方法 ViewColumn.One, // ViewColumn是一个Enum枚举,vscode的编辑器可以分割窗口,这里可以设置在哪个窗口打开 { enableScripts: true, // webview中javascript enable。默认禁用脚本 retainContextWhenHidden: true, // 保持上下文,webview实例常驻内存(保存webview内状态或keep-alive) } ) // 设置webview渲染内容,getHtmlForWebview根据path渲染指定路由 projectCreatorwebviewPanel.webview.html = getHtmlForWebview( extensionPath, 'createproject', false ) // webview窗口标题设置icon projectCreatorwebviewPanel.iconPath = vscode.Uri.parse(ICEWORKS_ICON_PATH) // webview窗口关闭时,销毁实例,释放内存 projectCreatorwebviewPanel.onDidDispose( () => { projectCreatorwebviewPanel = undefined }, null, context.subscriptions ) // 这里做的是监听webview传来的消息 connectService(projectCreatorwebviewPanel, context, { services, recorder, }) } } // 注册到vscode命令表中 subscriptions.push( registerCommand('iceworks-project-creator.create-project.start', () => { activeProjectCreatorWebview() }) ) }
这里注意到了命令 iceworks-project-creator.create-project.start,在之前的 iceworks-app 实现原理简介中说到了管理器点击菜单会调用这个命令。
connectService 中监听 webview 传来的消息:
// 简化代码 export function connectService( webviewPanel: vscode.WebviewPanel, context: vscode.ExtensionContext, options: IConnectServiceOptions ) { // 监听webview传来的消息 webview.onDidReceiveMessage( async (message: IMessage) => { // message就是webview中调用vscode postMessage发来的数据 }, undefined, subscriptions ) }
2. webview 内容渲染
getHtmlForWebview 方法没什么亮点,主要就是生成一个 html,根据 path 加载对应路由的 js、css 资源。
export function getHtmlForWebview( extensionPath: string, entryName?: string, needVendor?: boolean, cdnBasePath?: string, extraHtml?: string, resourceProcess?: (url: string) => vscode.Uri ): string // getHtmlForWebview = (path) => `<html> script、link等加载cdn资源 </html>`
值得一题的是,这里加载的 js 是用 react 写的一个单页应用,然后放到了 cdn 上来加载渲染。
3. webview 和 vscode 通信
在 react 单页应用中会调用@iceworks/vscode-webview 包中的一个 callService 方法,就是获取 vscode api,然后 postMessage:
// 下面的代码在浏览器环境中!!(vscode webview环境中) export const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null export const callService = function (service: string, method: string, ...args) { return new Promise((resolve, reject) => { const eventId = setTimeout(() => {}) const handler = event => { const message = event.data if (message.eventId === eventId) { window.removeEventListener('message', handler) message.errorMessage ? reject(new Error(message.errorMessage)) : resolve(message.result) } } // 监听vscode传来的消息 window.addEventListener('message', handler) // 向vscode发送消息 vscode.postMessage({ service, method, eventId, args, }) }) }
其实和浏览器中的 iframe 一样。
四、总结与验证
vscode 与 webview 之间通信
-
vscode 中,使用 webview.onDidReceiveMessage 接收从 webview 发来的数据,使用 webview.postMessage 向 webview 发送数据。
-
webview 中,使用 window.addEventListener('message', () => {})接收 vscode 发来的消息,使用 vscode.postMessage 向 vscode 发送消息。
demo 验证
import * as vscode from 'vscode' export function activate(context: vscode.ExtensionContext) { let webviewPanel: vscode.WebviewPanel | undefined function activeProjectCreatorWebview() { if (webviewPanel) { webviewPanel.reveal() } else { webviewPanel = vscode.window.createWebviewPanel( 'testPanel', 'Test', vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true, } ) webviewPanel.webview.html = ` <div id="root">hello webview!</div> <script> const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null const root = document.getElementById('root') window.sendMessage = () => { // 向vscode发送消息 vscode.postMessage({ text: 'from webview!!' }) } // 监听从vscode发来的消息 window.addEventListener('message', event => { root.innerText += ('\\n' + event.data.text) }) </script> <button onclick="sendMessage()">send message</button> ` // 监听从webview发来的消息 webviewPanel.webview.onDidReceiveMessage( message => { // 这里在vscode环境可以调用native能力 vscode.window.showInformationMessage(message['text']) // 也可以直接把webview发来的消息返回去 webviewPanel.webview.postMessage({ text: `from vscode: ${message['text']}`, }) }, null, context.subscriptions ) webviewPanel.iconPath = vscode.Uri.parse( 'https://static1.testcn.com/static/ty-lib/favicon.ico' ) webviewPanel.onDidDispose( () => { webviewPanel = undefined }, null, context.subscriptions ) } } context.subscriptions.push( vscode.commands.registerCommand('test-panel-manager.showList', () => { activeProjectCreatorWebview() }) ) } export function deactivate() {}
package.json
相关字段解释在【iceworks-app 实现原理简介】说明
{ "activationEvents": ["onCommand:test-panel-manager.showList"], "contributes": { "commands": [ { "command": "test-panel-manager.showList", "title": "打开发布单列表" } ], "viewsContainers": { "activitybar": [ { "id": "testPanel", "title": "test Panel Manager", "icon": "assets/sidebar-logo.png" } ] }, "views": { "testPanel": [ { "id": "welcome", "name": "Welcome" } ] }, "viewsWelcome": [ { "view": "welcome", "contents": "[发布单列表](command:test-panel-manager.showList)" } ] } }
附录:
- vscode-webview 通信机制