如果能做到进程运行着就能刷新代码,这对游戏这类重状态业务的开发效率提升是很有帮助的,设想我们经过一系列的登录,进副本,甚至战斗到一定时间才出现的bug,没这功能我们改完代码还需要重操作一次,如果没改对,还得继续重来。。。
Puerts结合V8提供的一些能力和自身的特点,做了一套较为完善的代码热刷新支持,我们先看看效果,然后看看是怎么实现的。
先甩个helloworld级别的演示:
https://video.zhihu.com/video/1365325088653529088?itemId=364505146&itemType=article&player=%7B%22shouldShowPageFullScreenButton%22%3Atrue%7D
虽说演示的代码十分简单,但也涵括了一个实用的代码热刷新的几个要点:
可能有人说这例子太简单了,会不会复杂点就hold不住了了,好,再来个花里胡哨点的演示:
https://video.zhihu.com/video/1365325837688946688?itemId=364505146&itemType=article&player=%7B%22shouldShowPageFullScreenButton%22%3Atrue%7D
其实我希望做一堆底层分析,写一堆高深莫测的代码来实现这功能。可惜V8没有给我这个装B的机会,它已经内置了刷代码的功能:Inspector的“Debugger.setScriptSource”命令。我们只要监听文件的变化,用url和新代码即可刷新指定代码。
虽说提供了这么个功能,但用起来其实也并不简单,因为“Debugger.setScriptSource”并不是一个C++函数,而是一个RPC消息,而且调用这个RPC之前,还要做一些交互,我下来分享下找到这方法到实现这功能的一个完整思考。
线索
一次偶然且惊喜的发现Chrome Dev Tools链接到Puerts,在上面修改代码然后“Ctrl+S”竟然能热刷新代码。这也能实现我们部分需求了,但还有两个问题:
分析
抓包发现Chrome Dev Tools是通过inspector协议的“Debugger.setScriptSource”命令去更新代码。
第一个想到的是分析“Debugger.setScriptSource”的实现,如果它是调用V8某个API实现的,那么我们直接调用那接口好了。
但遗憾的是,“Debugger.setScriptSource”调用的相关V8接口,都是private的。这功能他仅仅以inspector的方式开放。
方案确定
Dev Tools和Puerts是用WebSocket来交互,而我们是同进程就完全没必要,只要直接发消息给inspector即可。另外,由于inspector协议是JSON消息,我把和inspector交互的逻辑放到js里头,由于js支持await,还可以很方便的实现twoway rpc等待。
RPC封装关键代码:
function messageHandler(str) {
let msg = JSON.parse(str);
if (msg.method === "Debugger.scriptParsed") {
parsedScript.set(msg.params.scriptId, msg.params.url);
parsedScript.set(msg.params.url, msg.params.scriptId);
} else if (typeof msg.id === "number") {
if (msg.result && pendingCommnand.has(msg.id)) {
const resolve = pendingCommnand.get(msg.id);
pendingCommnand.delete(msg.id);
resolve(msg.result);
} else {
console.error("unexpect inspector message:" + str);
}
}
};
let commandId = 0;
function sendCommand(method, params) {
return new Promise((resolve, reject) => {
commandId++;
pendingCommnand.set(commandId, resolve); dispatchProtocolMessage(JSON.stringify({"id":commandId,"method":method,"params":params}));
});
}
代码说明:
封装好RPC交互,交互逻辑就十分简单了。
初始化关键代码,其实就是发了两个命令过去:
async function enableDebugger() {
//...
await sendCommand("Runtime.enable", {});
await sendCommand("Debugger.enable", {"maxScriptsCacheSize":10000000});
}
代码热更新关键逻辑:
async function reload(moduleName, url, source) {
//找url对应的id
if (scriptId) {
if (typeof source === "string") {
let orgSourceInfo = await sendCommand("Debugger.getScriptSource", {scriptId:"" + scriptId});
source = ("(function (exports, require, module, __filename, __dirname) { " + source + "\n});");
if (orgSourceInfo.scriptSource == source) {
return;
}
await sendCommand("Debugger.setScriptSource", {scriptId:"" + scriptId,scriptSource:source});
}
}
};
代码解释:
搞定啦!就那么简单