• VSCode的Electron通信方案
  • 发布于 5天前
  • 26 热度
    0 评论
vscode的ipc方案经历以下几个过程:
1.preload.js中暴露ipcRender.invoke,ipcRender.call等方法供渲染进程调用
2.在主进程创建Server类管理各个窗口和主进程的Connection
3.在Server上注册Channel供渲染进程调用
4.在渲染进程创建Client类连接Server
5.最后就可以在渲染进程通过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)
  }
 // 堆代码 duidaima.com
  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源码查看。
用户评论