摘要:過程涉及到三個對象,一個是或,一個是,另外一個就是瀏覽器或或其他。在中進行配置了,也就是會執(zhí)行腳本。然后會給這個注冊一些監(jiān)聽在收到消息時會回調(diào)。發(fā)出一個消息讓瀏覽器準備的運行環(huán)境在收到消息會調(diào)用。
第一次在segmentfault寫博客,很緊張~~~公司項目上ReactNative,之前也是沒有接觸過,所以也是一邊學習一邊做項目了,最近騰出手來更新總結(jié)了一下RN的Debug的一個小知識點,不是說怎么去Debug,而是Debug的代碼原理,下面開始正文。
Debug過程涉及到三個對象,一個是App(Android或iOS),一個是Server,另外一個就是瀏覽器(Chrome或FireFox或其他)。Server是App和瀏覽器之間通信的橋梁,比如App發(fā)Http請求給Server,Server再通過WebSocket發(fā)送給瀏覽器,反過來也是。首先肯定需要準備一下中介,就是Server
1.Server
這里的Server不用專門準備一臺服務(wù)器,只需要配置一個Node.js環(huán)境,然后啟動npm start就行。npm start在package.json中進行配置了,也就是會執(zhí)行cli.js腳本。
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
然后cli.js會執(zhí)行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與瀏覽器之間的關(guān)系了。
2.建立連接
在手機菜單中點擊Debug JS Remotely,App就會發(fā)出一個Http請求
GET /launch-js-devtools HTTP/1.1
Server接收到這個請求會執(zhí)行opn操作,主要做兩件事:
打開Chrome的一個tab
讓這個tab打開urlhttp://localhost:8081/debugger-ui/
這個界面就是我們打開Debug時在瀏覽器見到的第一個界面
這個界面的文件就是Server的index.html,我截取了body的代碼:
瀏覽器在執(zhí)行index.html的時候會發(fā)出下面的請求:
GET /debugger-proxy?role=debugger&name=Chrome HTTP/1.1
我們來看看發(fā)出這個請求有什么目的,扒一扒源碼:
function connectToDebuggerProxy() {
const ws = new WebSocket("ws://" + window.location.host + "/debugger-proxy?role=debugger&name=Chrome"); //Chrome通過websocket和Packager保持通訊
//WebSocket注冊監(jiān)聽
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的聯(lián)系,WebSocket就是可以保持長連接的全雙工通信協(xié)議,在握手階段通過Http進行,后面就和Http沒有什么關(guān)系了。然后會給這個webSocket注冊一些監(jiān)聽:
ws.onopen
ws.onmessage
ws.onclose
在webSocket收到消息時會回調(diào)ws.onmessage。
到這里App和瀏覽器之間就已經(jīng)建立連接了,接下來App會發(fā)出幾個消息讓瀏覽器加載需要調(diào)試的代碼, 接著往下看。
3.加載調(diào)試代碼
首先需要強調(diào)的就是瀏覽器加載項目代碼肯定不能在UI線程加載吧,要不然肯定影響瀏覽器的正常工作。那怎么去加載?啟一個后臺線程,有的小伙伴就要不信了,別急,我們接著去扒一扒源碼。
App發(fā)出一個消息讓瀏覽器準備JS的運行環(huán)境:
在收到‘prepareJSRuntime’消息會調(diào)用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’這個函數(shù), 主要工作就是‘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,主要就是一個消息的監(jiān)聽,可以看到在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在點擊調(diào)試的時候會給瀏覽器還發(fā)送這么一個‘executeApplicationScript’消息,讓瀏覽器去加載項目代碼:
這個messageEvent的數(shù)據(jù)比較多,我就截取一部分,里面包含了方法名,url(這個url就是后面瀏覽器需要去下載bundle的地方),inject包含的數(shù)據(jù)最多,主要是會賦值給瀏覽器全局對象的方法。
{
"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發(fā)送‘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找到相應的處理方法,在里面首選循環(huán)取出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);
},
......
};
為了證明我上面的分析沒錯,決定捉包看下發(fā)起的請求是不是這樣的:
在加載bundle后面還有一個map,體積也很大,有1.74MB的體積,這個是用于映射bundle里面的代碼成一個個工程項目里的類文件,這樣就和在代碼編譯器里面調(diào)試效果一樣了。
4.總結(jié)
根據(jù)上面的捉包請求簡單總結(jié)下建立連接的過程,首先通過/launch-jsdevtools打開調(diào)試Tab,瀏覽器通過/debugger-proxy建立與Server的WebSocket連接,然后瀏覽器打開index.html文件,發(fā)起/debugger-ui/debuggerWorker.js建立后臺線程,通過這個后臺線程加載bundle。
到這里建立Debug連接的原理分析就差不多了,希望對小伙伴們有幫助,歡迎點贊和關(guān)注哈。
謝謝大家!