VSCode的Electron通信方案vscode中ipc通信是通过invoke直接调用吗,还是有其他的机制?下面我们一
vscode方案梳理
vscode的ipc方案经历以下几个过程:
- preload.js中暴露
ipcRender.invoke
,ipcRender.call
等方法供渲染进程调用 - 在主进程创建
Server
类管理各个窗口和主进程的Connection
- 在
Server
上注册Channel
供渲染进程调用 - 在渲染进程创建
Client
类连接Server
- 最后就可以在渲染进程通过
Channel
调用主进程的方法
连接篇
由上面的架构图开始,我们先来看渲染端的Client类
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol
private static createProtocol(): Protocol {
const onMessage = Event.fromNodeEventEmitter<ELBuffer>(ipcRenderer, 'vscode:message', (_, message) => ELBuffer.wrap(message))
ipcRenderer.send('vscode:hello')
return new Protocol(ipcRenderer, onMessage)
}
constructor(id: string) {
const protocol = Client.createProtocol()
super(protocol, id)
this.protocol = protocol
}
override dispose(): void {
this.protocol.disconnect()
super.dispose()
}
}
解释一下,Client
先监听了ipcRenderer
上的vscode:message
事件,然后发送了vscode:hello
通知主进程Client
连接,然后创建了Protocol
类传给IPCClient
基类
接下来看一下IPCClient
class IPCClient<TContext = string> implements IDisposable {
private channelClient: ChannelClient
private channelServer: ChannelServer
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
const writer = new BufferWriter()
serialize(writer, ctx)
protocol.send(writer.buffer)
this.channelClient = new ChannelClient(protocol)
this.channelServer = new ChannelServer(protocol, ctx)
}
getChannel<T extends IChannel>(channelName: string): T {
return this.channelClient.getChannel(channelName) as T
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.channelServer.registerChannel(channelName, channel)
}
dispose(): void {
this.channelClient.dispose()
this.channelServer.dispose()
}
}
可以看到IPCClient
的作用就是管理Channel
.
注意看这一段代码
const writer = new BufferWriter()
serialize(writer, ctx) // ctx的实际值为windowId
rotocol.send(writer.buffer)
这里其实是向主进程发送了当前窗口的id
作为标识
接下来再看下主进程的Server
类
class Server extends IPCServer {
private static readonly Clients = new Map<number, IDisposable>()
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'vscode:hello', ({ sender }) => sender)
return Event.map(onHello, (webContents) => {
const id = webContents.id
const client = Server.Clients.get(id)
client?.dispose()
const onDidClientReconnect = new Emitter<void>()
Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire()))
const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event<ELBuffer>
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event)
const protocol = new ElectronProtocol(webContents, onMessage)
return { protocol, onDidClientDisconnect }
})
}
constructor() {
super(Server.getOnDidClientConnect())
}
}
这段代码比较简单,就是处理了一下重新连接的问题
接下来IPCServer
class IPCServer<TContext extends string = string> {
private channels = new Map<string, IServerChannel<TContext>>()
private _connections = new Set<Connection<TContext>>()
private readonly _onDidAddConnection = new Emitter<Connection<TContext>>()
readonly onDidAddConnection: Event<Connection<TContext>> = this._onDidAddConnection.event
private readonly _onDidRemoveConnection = new Emitter<Connection<TContext>>()
readonly onDidRemoveConnection: Event<Connection<TContext>> = this._onDidRemoveConnection.event
private readonly disposables = new DisposableStore()
get connections(): Connection<TContext>[] {
const result: Connection<TContext>[] = []
this._connections.forEach(ctx => result.push(ctx))
return result
}
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
this.disposables.add(onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
const onFirstMessage = Event.once(protocol.onMessage)
this.disposables.add(onFirstMessage((msg) => {
const reader = new BufferReader(msg)
const ctx = deserialize(reader) as TContext
const channelServer = new ChannelServer<TContext>(protocol, ctx)
const channelClient = new ChannelClient(protocol)
this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel))
const connection: Connection<TContext> = { channelServer, channelClient, ctx }
this._connections.add(connection)
this._onDidAddConnection.fire(connection)
this.disposables.add(onDidClientDisconnect(() => {
channelServer.dispose()
channelClient.dispose()
this._connections.delete(connection)
this._onDidRemoveConnection.fire(connection)
}))
}))
}))
}
getChannel<T extends IChannel>(channelName: string, routerOrClientFilter: IClientRouter<TContext> | ((client: Client<TContext>) => boolean)): T {
}
private getMulticastEvent<T extends IChannel>(channelName: string, clientFilter: (client: Client<TContext>) => boolean, eventName: string, arg: any): Event<T> {
}
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
}
dispose(): void {
}
可以看到Server
上管理了所有的Channel
和Connection
通信篇
通过连接篇的介绍,主进程和渲染进程建立起了连接,那接下来就是如何进行通信,以下是一个简易的例子:
// services/fileSystem.ts
class IFileSystem {
stat:(source:string)=>Promise<Stat>
}
// main.ts
const server = new Server()
server.registerChannel(
"fileSystem",
ProxyChannel.fromService({
stat(source:string){
return fs.stat(source)
}
})) // 这里的ProxyChannel.fromService后面再解释
// renderer.ts
const client = new Client()
const fileSystemChannel = client.getChannel("fileSystem")
const stat = awite fileSystemChannel.call("stat")
// 或者
const client = new Client()
const fileSystemChannel = client.getChannel("fileSystem")
const fileSystemService = ProxyChannel.toService<IFileSystemChannel>(fileSystemChannel)// 后面解释
const stat = awite fileSystemChannel.call("stat")
要搞清楚它们如何调用,首先要明白Channel
的构成以及Channel
如何被创建
interface IChannel {
call: <T>(command: string, arg?: any, cancellationToken?: CancellationToken) => Promise<T>
listen: <T>(event: string, arg?: any) => Event<T>
}
// Channel的创建在ChannelClient类中
getChannel<T extends IChannel>(channelName: string): T {
const that = this
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
if (that.isDisposed) {
return Promise.reject(new CancellationError())
}
return that.requestPromise(channelName, command, arg, cancellationToken)
},
listen(event: string, arg: any) {
if (that.isDisposed) {
return Event.None
}
return that.requestEvent(channelName, event, arg)
},
} as T
}
这里的that.requestPromise
其实就是调用ipcRender.invoke('vscode:message',.....)
,会把channelName
,command
和其他参数一起传过去
然后我们看一下Server
怎么处理的请求(删减了部分代码)
private onPromise(request: IRawPromiseRequest): void {
const channel = this.channels.get(request.channelName)
if (!channel) {
return
}
let promise: Promise<any>
try {
promise = channel.call(this.ctx, request.name, request.arg)
}
catch (e) {
promise = Promise.reject(e)
}
const id = request.id
promise.then((data) => {
this.sendResponse({ id, data, type: ResponseType.PromiseSuccess })
}, (err) => {
this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj })
})
}
Server
在接受request
之后找到对应Channel
的Command
进行调用,然后返回封装后的执行结果
最后我们看一下ProxyChannel.toService
和ProxyChannel.fromService
的代码
export function fromService<TContext>(service: unknown, disposables: DisposableStore, options?: ICreateServiceChannelOptions): IServerChannel<TContext> {
const handler = service as { [key: string]: unknown }
const disableMarshalling = options && options.disableMarshalling
const mapEventNameToEvent = new Map<string, Event<unknown>>()
for (const key in handler) {
if (propertyIsEvent(key)) {
mapEventNameToEvent.set(key, EventType.buffer(handler[key] as Event<unknown>, true, undefined, disposables))
}
}
return new class implements IServerChannel {
listen<T>(_: unknown, event: string, arg: any): Event<T> {
const eventImpl = mapEventNameToEvent.get(event)
if (eventImpl) {
return eventImpl as Event<T>
}
const target = handler[event]
if (typeof target === 'function') {
if (propertyIsDynamicEvent(event)) {
return target.call(handler, arg)
}
if (propertyIsEvent(event)) {
mapEventNameToEvent.set(event, EventType.buffer(handler[event] as Event<unknown>, true, undefined, disposables))
return mapEventNameToEvent.get(event) as Event<T>
}
}
throw new Error(`Event not found: ${event}`)
}
call(_: unknown, command: string, args?: any[]): Promise<any> {
const target = handler[command]
if (typeof target === 'function') {
// Revive unless marshalling disabled
if (!disableMarshalling && Array.isArray(args)) {
for (let i = 0; i < args.length; i++) {
args[i] = revive(args[i])
}
}
let res = target.apply(handler, args)
if (!(res instanceof Promise)) {
res = Promise.resolve(res)
}
return res
}
throw new Error(`Method not found: ${command}`)
}
}()
}
export function toService<T extends object>(channel: IChannel, options?: ICreateProxyServiceOptions): T {
return new Proxy({}, {
get(_target: T, propKey: PropertyKey) {
if (typeof propKey === 'string') {
if (options?.properties?.has(propKey)) {
return options.properties.get(propKey)
}
return async function (...args: any[]) {
const result = await channel.call(propKey, args)
return result
}
}
throw new Error(`Property not found: ${String(propKey)}`)
},
}) as T
}
可以看到fromService
是将类转换为Channel
,而toService
是将Channel
转换为有类型提示的Service
总结
vscode通过这样抽象了一套Channel
机制进行通信,便于代码的管理和跨平台。源码还删减了很多关于副作用处理以及事件机制的代码,感兴趣的可以拉取vscode源码查看
转载自:https://juejin.cn/post/7418879190684549171