摘要:过程涉及到三个对象,一个是或,一个是,另外一个就是浏览器或或其他。在中进行配置了,也就是会执行脚本。然后会给这个注册一些监听在收到消息时会回调。发出一个消息让浏览器准备的运行环境在收到消息会调用。
第一次在segmentfault写博客,很紧张~~~公司项目上ReactNative,之前也是没有接触过,所以也是一边学习一边做项目了,最近腾出手来更新总结了一下RN的Debug的一个小知识点,不是说怎么去Debug,而是Debug的代码原理,下面开始正文。
Debug过程涉及到三个对象,一个是App(Android或iOS),一个是Server,另外一个就是浏览器(Chrome或FireFox或其他)。Server是App和浏览器之间通信的桥梁,比如App发Http请求给Server,Server再通过WebSocket发送给浏览器,反过来也是。首先肯定需要准备一下中介,就是Server
1.Server
这里的Server不用专门准备一台服务器,只需要配置一个Node.js环境,然后启动npm start就行。npm start在package.json中进行配置了,也就是会执行cli.js脚本。
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
然后cli.js会执行runServer.js,在这里启动一个NodeJS Server:
const serverInstance = args.https
? https.createServer(
{
key: fs.readFileSync(args.key),
cert: fs.readFileSync(args.cert),
},
app,
)
: http.createServer(app);
serverInstance.listen(args.port, args.host, 511, function() {
attachHMRServer({
httpServer: serverInstance,
path: "/hot",
packagerServer,
});
wsProxy = webSocketProxy.attachToServer(serverInstance, "/debugger-proxy");
ms = messageSocket.attachToServer(serverInstance, "/message");
readyCallback(reporter);
});
有了中介Server后就可以建立App与浏览器之间的关系了。
2.建立连接
在手机菜单中点击Debug JS Remotely,App就会发出一个Http请求
GET /launch-js-devtools HTTP/1.1
Server接收到这个请求会执行opn操作,主要做两件事:
打开Chrome的一个tab
让这个tab打开urlhttp://localhost:8081/debugger-ui/
这个界面就是我们打开Debug时在浏览器见到的第一个界面
这个界面的文件就是Server的index.html,我截取了body的代码:
浏览器在执行index.html的时候会发出下面的请求:
GET /debugger-proxy?role=debugger&name=Chrome HTTP/1.1
我们来看看发出这个请求有什么目的,扒一扒源码:
function connectToDebuggerProxy() {
const ws = new WebSocket("ws://" + window.location.host + "/debugger-proxy?role=debugger&name=Chrome"); //Chrome通过websocket和Packager保持通讯
//WebSocket注册监听
ws.onopen = function() {
Page.setState({status: {type: "connecting"}});
};
ws.onmessage = async function(message) {
if (!message.data) {
return;
}
const object = JSON.parse(message.data);
if (object.$event === "client-disconnected") {
shutdownJSRuntime();
Page.setState({status: {type: "disconnected"}});
return;
}
if (!object.method) {
return;
}
// Special message that asks for a new JS runtime
if (object.method === "prepareJSRuntime") {
shutdownJSRuntime();
console.clear();
createJSRuntime();
ws.send(JSON.stringify({replyID: object.id}));
Page.setState({status: {type: "connected", id: object.id}});
} else if (object.method === "$disconnected") {
shutdownJSRuntime();
Page.setState({status: {type: "disconnected"}});
} else if (object.method === "executeApplicationScript") {
worker.postMessage({
...object,
url: await getBlobUrl(object.url),
});
} else {
// Otherwise, pass through to the worker.
worker.postMessage(object);
}
};
ws.onclose = function(error) {
shutdownJSRuntime();
Page.setState({status: {type: "error", error}});
if (error.reason) {
console.warn(error.reason);
}
setTimeout(connectToDebuggerProxy, 500);
};
// Let debuggerWorker.js know when we"re not visible so that we can warn about
// poor performance when using remote debugging.
document.addEventListener("visibilitychange", updateVisibility, false);
}
首先就是通过new WebSocket浏览器建立与Server的联系,WebSocket就是可以保持长连接的全双工通信协议,在握手阶段通过Http进行,后面就和Http没有什么关系了。然后会给这个webSocket注册一些监听:
ws.onopen
ws.onmessage
ws.onclose
在webSocket收到消息时会回调ws.onmessage。
到这里App和浏览器之间就已经建立连接了,接下来App会发出几个消息让浏览器加载需要调试的代码, 接着往下看。
3.加载调试代码
首先需要强调的就是浏览器加载项目代码肯定不能在UI线程加载吧,要不然肯定影响浏览器的正常工作。那怎么去加载?启一个后台线程,有的小伙伴就要不信了,别急,我们接着去扒一扒源码。
App发出一个消息让浏览器准备JS的运行环境:
在收到‘prepareJSRuntime’消息会调用createJSRuntime。
// Special message that asks for a new JS runtime
if (object.method === "prepareJSRuntime") {
shutdownJSRuntime();
console.clear();
createJSRuntime();
ws.send(JSON.stringify({replyID: object.id}));
Page.setState({status: {type: "connected", id: object.id}});
} else if (object.method === "$disconnected") {
shutdownJSRuntime();
Page.setState({status: {type: "disconnected"}});
} else if (object.method === "executeApplicationScript") {
worker.postMessage({
...object,
url: await getBlobUrl(object.url),
});
} else {
// Otherwise, pass through to the worker.
worker.postMessage(object);
}
接着看‘createJSRuntime’这个函数, 主要工作就是‘new Worker’,看下Worker的定义:
Web Workers is a simple means for web content to run scripts in
background threads. The worker thread can perform tasks without
interfering with the user interface.
也就是会起一个后台线程,来运行‘debuggerWorker.js’这个脚本。
function createJSRuntime() {
// This worker will run the application JavaScript code,
// making sure that it"s run in an environment without a global
// document, to make it consistent with the JSC executor environment.
worker = new Worker("debuggerWorker.js");
worker.onmessage = function(message) {
ws.send(JSON.stringify(message.data));
};
window.onbeforeunload = function() {
return "If you reload this page, it is going to break the debugging session. " +
"You should press" + refreshShortcut + "in simulator to reload.";
};
updateVisibility();
}
接着看看debuggerWorker.js,主要就是一个消息的监听,可以看到在messageHandlers里主要处理两类消息:
"executeApplicationScript", "setDebuggerVisibility"
/* global __fbBatchedBridge, self, importScripts, postMessage, onmessage: true */
/* eslint no-unused-vars: 0 */
"use strict";
onmessage = (function() {
var visibilityState;
var showVisibilityWarning = (function() {
var hasWarned = false;
return function() {
// Wait until `YellowBox` gets initialized before displaying the warning.
if (hasWarned || console.warn.toString().includes("[native code]")) {
return;
}
hasWarned = true;
console.warn(
"Remote debugger is in a background tab which may cause apps to " +
"perform slowly. Fix this by foregrounding the tab (or opening it in " +
"a separate window)."
);
};
})();
var messageHandlers = {
"executeApplicationScript": function(message, sendReply) {
for (var key in message.inject) {
self[key] = JSON.parse(message.inject[key]);
}
var error;
try {
importScripts(message.url);
} catch (err) {
error = err.message;
}
sendReply(null /* result */, error);
},
"setDebuggerVisibility": function(message) {
visibilityState = message.visibilityState;
},
};
return function(message) {
if (visibilityState === "hidden") {
showVisibilityWarning();
}
var object = message.data;
var sendReply = function(result, error) {
postMessage({replyID: object.id, result: result, error: error});
};
var handler = messageHandlers[object.method];
if (handler) {
// Special cased handlers
handler(object, sendReply);
} else {
// Other methods get called on the bridge
var returnValue = [[], [], [], 0];
var error;
try {
if (typeof __fbBatchedBridge === "object") {
returnValue = __fbBatchedBridge[object.method].apply(null, object.arguments);
} else {
error = "Failed to call function, __fbBatchedBridge is undefined";
}
} catch (err) {
error = err.message;
} finally {
sendReply(JSON.stringify(returnValue), error);
}
}
};
})();
App在点击调试的时候会给浏览器还发送这么一个‘executeApplicationScript’消息,让浏览器去加载项目代码:
这个messageEvent的数据比较多,我就截取一部分,里面包含了方法名,url(这个url就是后面浏览器需要去下载bundle的地方),inject包含的数据最多,主要是会赋值给浏览器全局对象的方法。
{
"id": 1,
"method": "executeApplicationScript",
"url": "http://localhost:8081/index.android.bundle?platform=android&dev=true&minify=false",
"inject": {
"__fbBatchedBridgeConfig": "{"remoteModuleConfig":[["AccessibilityInfo",{},["isTouchExplorationEnabled"]],["LocationObserver",{},["getCurrentPosition","startObserving","stopObserving"]],["CameraRollManager",{},["getPhotos","saveToCameraRoll"],[0,1]],["NetInfo",{},["getCurrentConnectivity","isConnectionMetered"],[0,1]],["PlatformConstants",{"ServerHost":"localhost:8081","reactNativeVersion":{"patch":0,"prerelease":null,"minor":51,"major":0},"Version":21,"isTesting":false}],["TimePickerAndroid",{}
}
webSocket首先接收到这个消息, 然后通过worker.postMessage给上面的worker发送‘executeApplicationScript’消息
ws.onmessage = async function(message) {
......
// Special message that asks for a new JS runtime
if (object.method === "prepareJSRuntime") {
shutdownJSRuntime();
console.clear();
createJSRuntime();
ws.send(JSON.stringify({replyID: object.id}));
Page.setState({status: {type: "connected", id: object.id}});
} else if (object.method === "$disconnected") {
shutdownJSRuntime();
Page.setState({status: {type: "disconnected"}});
} else if (object.method === "executeApplicationScript") {
worker.postMessage({
...object,
url: await getBlobUrl(object.url),
});
} else {
// Otherwise, pass through to the worker.
worker.postMessage(object);
}
};
worker接收到这个消息在messageHandlers找到相应的处理方法,在里面首选循环取出inject里面的字段和value然后赋值给self,在这里我理解就是这个worker线程的全局对象,然后通过 importScripts(message.url)去加载bundle。
var messageHandlers = {
"executeApplicationScript": function(message, sendReply) {
for (var key in message.inject) {
self[key] = JSON.parse(message.inject[key]);
}
var error;
try {
importScripts(message.url);
} catch (err) {
error = err.message;
}
sendReply(null /* result */, error);
},
......
};
为了证明我上面的分析没错,决定捉包看下发起的请求是不是这样的:
在加载bundle后面还有一个map,体积也很大,有1.74MB的体积,这个是用于映射bundle里面的代码成一个个工程项目里的类文件,这样就和在代码编译器里面调试效果一样了。
4.总结
根据上面的捉包请求简单总结下建立连接的过程,首先通过/launch-jsdevtools打开调试Tab,浏览器通过/debugger-proxy建立与Server的WebSocket连接,然后浏览器打开index.html文件,发起/debugger-ui/debuggerWorker.js建立后台线程,通过这个后台线程加载bundle。
到这里建立Debug连接的原理分析就差不多了,希望对小伙伴们有帮助,欢迎点赞和关注哈。
谢谢大家!