VSCode的Electron通信方案
点击关注公众号,“技术干货”及时达!
vscode方案梳理vscode的ipc方案经历以下几个过程:
preload.js中暴露ipcRender.invoke,ipcRender.call等方法供渲染进程调用在主进程创建Server类管理各个窗口和主进程的Connection在Server上注册Channel供渲染进程调用在渲染进程创建Client类连接Server最后就可以在渲染进程通过Channel调用主进程的方法连接篇由上面的架构图开始,我们先来看渲染端的Client类
exportclassClientextendsIPCClientimplementsIDisposable{
privateprotocol:Protocol
privatestaticcreateProtocol():Protocol{
constonMessage=Event.fromNodeEventEmitterELBuffer(ipcRenderer,'vscode:message',(_,message)=ELBuffer.wrap(message))
ipcRenderer.send('vscode:hello')
returnnewProtocol(ipcRenderer,onMessage)
}
constructor(id:string){
constprotocol=Client.createProtocol()
super(protocol,id)
this.protocol=protocol
}
overridedispose():void{
this.protocol.disconnect()
super.dispose()
}
}
解释一下,Client先监听了ipcRenderer上的vscode:message事件,然后发送了vscode:hello通知主进程Client连接,然后创建了Protocol类传给IPCClient基类
接下来看一下IPCClient
classIPCClientTContext=stringimplementsIDisposable{
privatechannelClient:ChannelClient
privatechannelServer:ChannelServer
constructor(protocol:IMessagePassingProtocol,ctx:TContext){
constwriter=newBufferWriter()
serialize(writer,ctx)
protocol.send(writer.buffer)
this.channelClient=newChannelClient(protocol)
this.channelServer=newChannelServer(protocol,ctx)
}
getChannelTextendsIChannel(channelName:string):T{
returnthis.channelClient.getChannel(channelName)asT
}
registerChannel(channelName:string,channel:IServerChannelstring):void{
this.channelServer.registerChannel(channelName,channel)
}
dispose():void{
this.channelClient.dispose()
this.channelServer.dispose()
}
}
可以看到IPCClient的作用就是管理Channel.
注意看这一段代码
constwriter=newBufferWriter()
serialize(writer,ctx)//ctx的实际值为windowId
rotocol.send(writer.buffer)
这里其实是向主进程发送了当前窗口的id作为标识
接下来再看下主进程的Server类
classServerextendsIPCServer{
privatestaticreadonlyClients=newMapnumber,IDisposable()
privatestaticgetOnDidClientConnect():EventClientConnectionEvent{
constonHello=Event.fromNodeEventEmitterWebContents(ipcMain,'vscode:hello',({sender})=sender)
returnEvent.map(onHello,(webContents)={
constid=webContents.id
constclient=Server.Clients.get(id)
client?.dispose()
constonDidClientReconnect=newEmittervoid()
Server.Clients.set(id,toDisposable(()=onDidClientReconnect.fire()))
constonMessage=createScopedOnMessageEvent(id,'vscode:message')asEventELBuffer
constonDidClientDisconnect=Event.any(Event.signal(createScopedOnMessageEvent(id,'vscode:disconnect')),onDidClientReconnect.event)
constprotocol=newElectronProtocol(webContents,onMessage)
return{protocol,onDidClientDisconnect}
})
}
constructor(){
super(Server.getOnDidClientConnect())
}
}
这段代码比较简单,就是处理了一下重新连接的问题
接下来IPCServer
classIPCServerTContextextendsstring=string{
privatechannels=newMapstring,IServerChannelTContext()
private_connections=newSetConnectionTContext()
privatereadonly_onDidAddConnection=newEmitterConnectionTContext()
readonlyonDidAddConnection:EventConnectionTContext=this._onDidAddConnection.event
privatereadonly_onDidRemoveConnection=newEmitterConnectionTContext()
readonlyonDidRemoveConnection:EventConnectionTContext=this._onDidRemoveConnection.event
privatereadonlydisposables=newDisposableStore()
getconnections():ConnectionTContext[]{
constresult:ConnectionTContext[]=[]
this._connections.forEach(ctx=result.push(ctx))
returnresult
}
constructor(onDidClientConnect:EventClientConnectionEvent){
this.disposables.add(onDidClientConnect(({protocol,onDidClientDisconnect})={
constonFirstMessage=Event.once(protocol.onMessage)
this.disposables.add(onFirstMessage((msg)={
constreader=newBufferReader(msg)
constctx=deserialize(reader)asTContext
constchannelServer=newChannelServerTContext(protocol,ctx)
constchannelClient=newChannelClient(protocol)
this.channels.forEach((channel,name)=channelServer.registerChannel(name,channel))
constconnection:ConnectionTContext={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)
}))
}))
}))
}
getChannelTextendsIChannel(channelName:string,routerOrClientFilter:IClientRouterTContext|((client:ClientTContext)=boolean)):T{
}
privategetMulticastEventTextendsIChannel(channelName:string,clientFilter:(client:ClientTContext)=boolean,eventName:string,arg:any):EventT{
}
registerChannel(channelName:string,channel:IServerChannelTContext):void{
}
dispose():void{
}
可以看到Server上管理了所有的Channel和Connection
通信篇通过连接篇的介绍,主进程和渲染进程建立起了连接,那接下来就是如何进行通信,以下是一个简易的例子:
//services/fileSystem.ts
classIFileSystem{
stat:(source:string)=PromiseStat
}
//main.ts
constserver=newServer()
server.registerChannel(
"fileSystem",
ProxyChannel.fromService({
stat(source:string){
returnfs.stat(source)
}
}))//这里的ProxyChannel.fromService后面再解释
//renderer.ts
constclient=newClient()
constfileSystemChannel=client.getChannel("fileSystem")
conststat=awitefileSystemChannel.call("stat")
//或者
constclient=newClient()
constfileSystemChannel=client.getChannel("fileSystem")
constfileSystemService=ProxyChannel.toServiceIFileSystemChannel(fileSystemChannel)//后面解释
conststat=awitefileSystemChannel.call("stat")
要搞清楚它们如何调用,首先要明白Channel的构成以及Channel如何被创建
interfaceIChannel{
call:T(command:string,arg?:any,cancellationToken?:CancellationToken)=Promise
listen:T(event:string,arg?:any)=Event
}
//Channel的创建在ChannelClient类中
getChannelTextendsIChannel(channelName:string):T{
constthat=this
return{
call(command:string,arg?:any,cancellationToken?:CancellationToken){
if(that.isDisposed){
returnPromise.reject(newCancellationError())
}
returnthat.requestPromise(channelName,command,arg,cancellationToken)
},
listen(event:string,arg:any){
if(that.isDisposed){
returnEvent.None
}
returnthat.requestEvent(channelName,event,arg)
},
}asT
}
这里的that.requestPromise其实就是调用ipcRender.invoke('vscode:message',.....),会把channelName,command和其他参数一起传过去
然后我们看一下Server怎么处理的请求(删减了部分代码)
privateonPromise(request:IRawPromiseRequest):void{
constchannel=this.channels.get(request.channelName)
if(!channel){
return
}
letpromise:Promiseany
try{
promise=channel.call(this.ctx,request.name,request.arg)
}
catch(e){
promise=Promise.reject(e)
}
constid=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的代码
exportfunctionfromServiceTContext(service:unknown,disposables:DisposableStore,options?:ICreateServiceChannelOptions):IServerChannelTContext{
consthandler=serviceas{[key:string]:unknown}
constdisableMarshalling=optionsoptions.disableMarshalling
constmapEventNameToEvent=newMapstring,Eventunknown()
for(constkeyinhandler){
if(propertyIsEvent(key)){
mapEventNameToEvent.set(key,EventType.buffer(handler[key]asEventunknown,true,undefined,disposables))
}
}
returnnewclassimplementsIServerChannel{
listen(_:unknown,event:string,arg:any):Event{
consteventImpl=mapEventNameToEvent.get(event)
if(eventImpl){
returneventImplasEvent
}
consttarget=handler[event]
if(typeoftarget==='function'){
if(propertyIsDynamicEvent(event)){
returntarget.call(handler,arg)
}
if(propertyIsEvent(event)){
mapEventNameToEvent.set(event,EventType.buffer(handler[event]asEventunknown,true,undefined,disposables))
returnmapEventNameToEvent.get(event)asEvent
}
}
thrownewError(`Eventnotfound:${event}`)
}
call(_:unknown,command:string,args?:any[]):Promiseany{
consttarget=handler[command]
if(typeoftarget==='function'){
//Reviveunlessmarshallingdisabled
if(!disableMarshallingArray.isArray(args)){
for(leti=0;iargs.length;i++){
args[i]=revive(args[i])
}
}
letres=target.apply(handler,args)
if(!(resinstanceofPromise)){
res=Promise.resolve(res)
}
returnres
}
thrownewError(`Methodnotfound:${command}`)
}
}()
}
exportfunctiontoServiceTextendsobject(channel:IChannel,options?:ICreateProxyServiceOptions):T{
returnnewProxy({},{
get(_target:T,propKey:PropertyKey){
if(typeofpropKey==='string'){
if(options?.properties?.has(propKey)){
returnoptions.properties.get(propKey)
}
returnasyncfunction(...args:any[]){
constresult=awaitchannel.call(propKey,args)
returnresult
}
}
thrownewError(`Propertynotfound:${String(propKey)}`)
},
})asT
}
可以看到fromService是将类转换为Channel,而toService是将Channel转换为有类型提示的Service
总结vscode通过这样抽象了一套Channel机制进行通信,便于代码的管理和跨平台。源码还删减了很多关于副作用处理以及事件机制的代码,感兴趣的可以拉取vscode源码查看
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线