@tofrankie/vscode-webview-rpc
v0.0.2
Published
Typed RPC for communication between VS Code Extension Host and WebView
Readme
@tofrankie/vscode-webview-rpc
一个用于 VS Code Extension Host 和 WebView 之间通信的小型 RPC 库。
它把消息通道包装成有类型提示的调用方式,适合 call、notify、event 这三类场景。
通信类型
分为三种通信类型,它们都可以由 WebView 或 Extension 任意一端发起。
call: 需要返回结果的请求-响应通信。适合读取数据、执行动作并等待结果。notify: 单向消息,不等待返回。适合触发宿主能力、上报日志、发起一次性动作。event: 跨端事件通道。emit发给对端,on监听对端发来的事件。
快速使用
安装
pnpm add @tofrankie/vscode-webview-rpc同一个 WebView 通道里,WebView 端创建一次 createWebviewRPC()。Extension 端创建一次 createExtensionRPC()。通常在各端的入口文件创建,后续由其他模块共享这个实例,而不是每个模块各自再创建一遍。
call
两端使用相同的 method 名称(指示例中的 'settings.get' 名称,下同)关联请求和处理函数。下面的 rpc.call('settings.get') 会交给 Extension 端 calls['settings.get'] 处理。
Extension 端处理请求:
import { createExtensionRPC } from '@tofrankie/vscode-webview-rpc'
const rpc = createExtensionRPC(webview, {
calls: {
'settings.get': async () => getSettings(),
},
})WebView 端发起请求:
import { createWebviewRPC } from '@tofrankie/vscode-webview-rpc'
const vscode = acquireVsCodeApi() // WebView 提供的 API
const rpc = createWebviewRPC(vscode)
const settings = await rpc.call('settings.get')notify
两端同样通过 method 名称关联。下面的 rpc.notify('external-link.open') 会交给 WebView 端 notifications['external-link.open'] 处理。
WebView 端处理单向消息:
const rpc = createWebviewRPC(vscode, {
notifications: {
'external-link.open': payload => {
window.open(payload.url)
},
},
})Extension 端发送单向消息:
rpc.notify('external-link.open', {
url: 'https://example.com',
})event
事件也通过 method 名称关联。下面的 rpc.emit('editor.changed') 会触发对端通过 rpc.on('editor.changed') 注册的 listener。
Extension 端监听事件:
const off = rpc.on('editor.changed', state => {
console.log(state.dirty)
})WebView 端发布事件:
rpc.emit('editor.changed', {
dirty: true,
})TypeScript
本库提供了类型声明,会根据你声明的 RPC 定义自动推导方法名、参数和返回值。
简单示例
对于复杂度不高的项目,推荐把共享 RPC 类型定义放在 shared/rpc.ts 之类的共享文件,让 WebView 和 Extension 双端都能引用。
// shared/rpc.ts
import type { RPCDefinition } from '@tofrankie/vscode-webview-rpc'
type Calls = {
'settings.get': {
params: void
result: Settings
}
}
type Notifications = {
'external-link.open': {
payload: { url: string }
}
}
type Events = {
'editor.changed': {
payload: { dirty: boolean }
}
}
// 如果没有使用到某类消息,可以使用 `Record<string, never>` 这样的“空定义”。
export type AppRPC = RPCDefinition<Calls, Notifications, Events>Calls定义rpc.call()能用哪些 method,以及它们的参数和返回值Notifications定义rpc.notify()能用哪些 method,以及它们的 payloadEvents定义rpc.on()/rpc.emit()能用哪些 method,以及它们的 payload
// extension
import type { AppRPC } from './shared/rpc'
import { createExtensionRPC } from '@tofrankie/vscode-webview-rpc'
const rpc = createExtensionRPC<AppRPC>(webview, {
calls: {
'settings.get': async () => getSettings(),
},
})// webview
import type { AppRPC } from './shared/rpc'
import { createWebviewRPC } from '@tofrankie/vscode-webview-rpc'
const rpc = createWebviewRPC<AppRPC>(vscode)
const settings = await rpc.call('settings.get')共享定义,不共享实现
建议 shared/rpc.ts 只放 AppRPC 这类类型定义,不放具体实现。原因是通信可以由 WebView 或 Extension 任意一端发起,而处理端可能会使用到另一端无法使用的 API,如果共享实现可能会导致运行时错误。
// webview/rpc.ts
import type { AppRPC } from '../shared/rpc'
import { createWebviewRPC } from '@tofrankie/vscode-webview-rpc'
const rpc = createWebviewRPC<AppRPC>(vscode)
const settings = await rpc.call('settings.get')// extension/rpc.ts
import type { AppRPC } from '../shared/rpc'
import { createExtensionRPC } from '@tofrankie/vscode-webview-rpc'
const rpc = createExtensionRPC<AppRPC>(webview, {
calls: {
// workspace 为 Extension 端特有 API,在 WebView 端无法直接使用
'settings.get': async () => workspace.getConfiguration('myExtension'),
},
})按模块拆分
大项目里可以把 RPC 定义拆到各个模块,再在入口组合起来。
import type { RPCDefinition } from '@tofrankie/vscode-webview-rpc'
type EmptyDefinitions = Record<string, never>
type SettingsRPC = RPCDefinition<
{
'settings.get': {
params: void
result: Settings
}
},
EmptyDefinitions,
{
'settings.changed': {
payload: Settings
}
}
>
type EditorRPC = RPCDefinition<
{
'editor.get-draft': {
params: void
result: Draft
}
},
EmptyDefinitions,
{
'editor.changed': {
payload: { dirty: boolean }
}
}
>
type AppRPC = RPCDefinition<
SettingsRPC['calls'] & EditorRPC['calls'],
EmptyDefinitions, // 占位类型,表示“这个模块当前没有这一类消息定义”
SettingsRPC['events'] & EditorRPC['events']
>const rpc = createExtensionRPC<AppRPC>(webview, {
calls: {
'settings.get': async () => getSettings(),
'editor.get-draft': async () => getDraft(),
},
})API
创建实例
createWebviewRPC(vscode, options?)
WebView 侧创建实例。第一个参数传 acquireVsCodeApi() 的返回值,第二个参数传处理定义和配置。通常在 WebView 入口文件里创建一次。
createExtensionRPC(webview, options?)
Extension 侧创建实例。第一个参数传 VS Code 的 webview 对象,第二个参数传处理定义和配置。通常在创建 WebviewPanel、组装 webview.html、绑定消息通道的地方创建一次。
同一个 WebView 通道只需要这一对实例:
- WebView 端一个
createWebviewRPC(...) - Extension 端一个
createExtensionRPC(...)
不要因为有多个业务模块、多个文件,或者同时需要收发消息,就重复创建多个实例。更常见的做法是由入口创建实例,再把 rpc 传给模块工厂、注册函数或业务对象复用。
// src/panels/HelloWorldPanel.ts
const rpc = createExtensionRPC<AppRPC>(webview, {
calls: {
'settings.get': async () => getSettings(),
},
})
registerEditorModule(rpc)
registerSettingsModule(rpc)// src/webview/main.ts
const rpc = createWebviewRPC<AppRPC>(vscode)
mountApp({ rpc })options
const rpc = createExtensionRPC(webview, {
calls: {
'settings.get': async () => getSettings(),
},
notifications: {
'external-link.open': async payload => {
await openExternalLink(payload.url)
},
},
timeout: 10_000,
missingNotificationBehavior: 'ignore',
})calls用来处理对端发来的call请求notifications用来处理对端发来的notify消息。missingNotificationBehavior用来控制“对端发来了一个notify,但当前端没有对应处理函数”时怎么处理。通常业务稳定后用ignore更宽松;开发和联调阶段用error更容易暴露问题。ignore:忽略这条消息error:按 method not found 处理,便于在开发期尽早发现消息名写错或接线遗漏
timeout是当前 RPC 实例里call的默认超时时间,单位是毫秒。没有在规定时间内收到对端 response 时,这次call会以RPCTimeoutError失败。也可以在单次调用时覆盖:
await rpc.call('settings.get', {
timeout: 3_000,
})
event的消息不放在options里。事件监听是运行时行为,使用公开的rpc.on()注册;rpc.emit()发给对端,rpc.on()监听对端发来的同名事件。events的方法名和 payload 类型仍然来自你的AppRPC,所以rpc.on()和rpc.emit()一样会有完整的 TypeScript 提示。
call
发送需要响应的请求。实际可用的 method、参数类型和返回值类型都来自你的 AppRPC['calls'],params: void 的方法可以省略参数。
const result = await rpc.call('settings.get')notify
发送单向消息,不等待返回值。实际可用的 method 和 payload 类型都来自你的 AppRPC['notifications']。
rpc.notify('external-link.open', {
url: 'https://example.com',
})event
emit 发送到对端,on 只监听对端事件。实际可用的 method 和 payload 类型都来自你的 AppRPC['events']。
const off = rpc.on('editor.changed', state => {
console.log(state.dirty)
})
rpc.emit('editor.changed', {
dirty: true,
})
off()dispose
释放监听、处理定义和 pending 请求。
rpc.dispose()错误类型
import {
RPCDisposedError,
RPCError,
RPCMethodNotFoundError,
RPCProtocolError,
RPCRemoteError,
RPCTimeoutError,
} from '@tofrankie/vscode-webview-rpc'导出
import type {
RPCCallDefinition,
RPCCallMethodArgs,
RPCCallParams,
RPCCallResult,
RPCDefinition,
RPCEventDefinition,
RPCEventPayload,
RPCNotificationDefinition,
RPCNotificationPayload,
RPCPayloadArgs,
} from '@tofrankie/vscode-webview-rpc'
import { createExtensionRPC, createWebviewRPC } from '@tofrankie/vscode-webview-rpc'RPCDefinition:组合应用级 RPC 类型定义RPCCallDefinition/RPCNotificationDefinition/RPCEventDefinition:分别描述三类消息的单项结构RPCCallParams/RPCCallResult:从某个callmethod 里提取参数和返回值类型RPCCallMethodArgs:表示rpc.call(method, ...)里 method 后面的完整参数列表,适合封装一层rpcCall()helper 时复用RPCNotificationPayload/RPCEventPayload:从notify或eventmethod 里提取 payload 类型RPCPayloadArgs:表示notify、emit、handler、listener 这一类 payload 参数列表
License
MIT License © Frankie
