前面我们说明了code server是如何把ide作为一个服务提供出去的,code server除了提供服务器功能外,最重要的事情当然就是加载ide内核代码了
Code Server加载vscode内核
我们回顾一下code server加载vscode内核相关的代码,loadAMDModule函数这个方法本质就是一个require,引入了vscode内核代码中的out/bootstrap-amd
这个文件里面的load
方法,然后通过vscode原生的模块加载机制加载vs/server/node/server.main
下的createServer
方法,获取到的createVSServer
方法负责在当前进程里面加载vscode内核代码。
这也就是我们前面架构图上说明的,code server和vscode内核虽然逻辑上独立,但是是存在同一个进程的,vscode内核其实是作为一个模块被code server加载的
src/node/routes/vscode.ts
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
this._codeServerMain = await createVSServer(null, {
...(await toCodeArgs(args)),
"without-connection-token": true,
})
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] =
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] || path.join(vsRootPath, "remote", "node_modules")
require(path.join(vsRootPath, "out/bootstrap-node")).injectNodeModuleLookupPath(
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"],
)
const module = await new Promise<AMDModule<T>>((resolve, reject) => {
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
})
return module[exportName] as T
}
vscode内核加载
好吧,现在让我们进入vscode源代码,vscode server加载逻辑入口在src/vs/server/node/server.main.ts,也就是前面code server加载的模块,这个createServer调用doCreateServer方法,具体内容都在doCreateServer里面
这里面简化下来做了这几个重要的事情
1、setupServerService明确哪些服务代码需要被加载,初始化各项服务,准备之后服务间的依赖注入
2、初始化RemoteExtensionHostAgentServer,这个是最核心的类,我们等下会重点说,他处理了code sever转发过来的http请求,以及之后的websocket连接,其实搞懂了这个类,vscode的远程开发也就基本能弄懂了
export function createServer(address: string | net.AddressInfo | null): Promise<IServerAPI> {
return doCreateServer(address, args, REMOTE_DATA_FOLDER);
}
export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise<IServerAPI> {
// 初始化各项服务,准备依赖注入
const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables);
// 初始化真正的服务器管理类RemoteExtensionHostAgentServer
const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient);
}
RemoteExtensionHostAgentServer本身代码就很多,我们先看一个接口
export interface IServerAPI {
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void;
handleServerError(err: Error): void;
dispose(): void;
}
RemoteExtensionHostAgentServer就是实现了这个接口的,我们今天也就从接口的功能来说,其实最重要的两个事情也就是
1、handleRequest处理http请求
2、handleUpgrade处理websocket连接
class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {}
前端静态资源服务器
怎么突然跳到前端了呢?是不是很突兀,其实这里我想说的是,上面提到的handleRequest,这里要处理的前端http请求最重要的就是前端的静态资源
也就是说,我们这个ide后台同时承担了前端静态资源服务器的功能,相当于一个tomcat,只不过我们前后端代码都打在一个包里了(但是,运行的时候,还是一个前后端分离的架构,前端在浏览器,后端在远程机器)
this._webClientServer.handle就是去返回前端UI所需文件的方法,我们往里面看,第一个请求(“/”)来的时候,handleRoot方法会将workbench.html(vscode前端加载的入口文件)返回,workbench.html里面通过
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.js"></script>
浏览器加载workbench.html后,会根据script中的地址进一步请求需要的资源,这个时候就走到handleStatic方法,读取相应的文件并返回
// workbench web UI
if (this._webClientServer) {
this._webClientServer.handle(req, res, parsedUrl);
return;
}
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
try {
const pathname = parsedUrl.pathname!;
if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) {
//
return this._handleStatic(req, res, parsedUrl);
}
if (pathname === '/') {
// 返回替换模版后的workbench.html入口文件
return this._handleRoot(req, res, parsedUrl);
}
}
}
我们可以在前端按F12打开dev tools查看Network中的资源加载情况,还是可以明显看出来请求了workbench.html,然后再去加载workbench.html中引用的js文件
websocket服务器
我们除了前端资源的加载依靠http协议(handleRequest),其他功能实现基本上都是靠websocket协议了,比如文件查看编辑、插件运行(代码调试、语法高亮)等,也正是因为websocket特别重要,我们单独一个章节来说明,我们现在只需要知道,RemoteExtensionHostAgentServer中处理了websocket连接建立的逻辑,而且是两种websocket连接,一个主进程,一个插件进程,就可以了