我们都知道我们在调试的时候都会开一个chrome,在chrome的调试界面中打断点、看log,本节就主要了解以下Debuger模式下js代码是如何加载的、js代码如何更新、如何断点源码。
跟着Debug JS Remotely按钮走,直接上reloadJSInProxyMode函数
//DevSupportManagerImpl.java
private void reloadJSInProxyMode() {
// When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
// anyway
mDevServerHelper.launchJSDevtools();//0<--
JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
@Override
public JavaJSExecutor create() throws Exception {
WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor();//1<--
SimpleSettableFuture future = new SimpleSettableFuture<>();
executor.connect(//2<--
mDevServerHelper.getWebsocketProxyURL(),
getExecutorConnectCallback(future));
// TODO(t9349129) Don't use timeout
try {
future.get(90, TimeUnit.SECONDS);//3<--
return executor;
} catch (ExecutionException e) {
throw (Exception) e.getCause();
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException(e);
}
}
};
mReactInstanceManagerHelper.onReloadWithJSDebugger(factory);//4<--
}
可以看到这个函数名叫做reloadJSInProxyMode,也就是说js环境是被代理的
0、launchJSDevtools,这里就是启动chrome的操作
1、这里创建了一个由websocket实现的js代码运行环境,说明js代码是远程执行的
2、让executor去连接远程远程的WebSokcetServer
3、连接等待时间是90s,超时则算失败
4、使用这个远程js运行环境去reload我们的js代码
启动chrome时会加载debugger-ui界面,源码在node_modules/react-native/local-cli/server/util/debugger-ui/index.html位置
index.html
function connectToDebuggerProxy() {
const ws = new WebSocket('ws://' + window.location.host + '/debugger-proxy?role=debugger&name=Chrome');//1<--
let worker;
let queuedMessages = [];
let appExecuted = false;
function createJSRuntime() {
worker = new Worker('debuggerWorker.js');//2<--
worker.onmessage = function(message) {
ws.send(JSON.stringify(message.data));
};
......
}
function shutdownJSRuntime() {
if (worker) {
worker.terminate();
worker = null;
window.onbeforeunload = null;
}
}
......
ws.onmessage = async function(message) {//3<--
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 === 'prepareJSRuntime') {//4<--
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') {//5<--
appExecuted = false;
worker.postMessage({
...object,
url: await getBlobUrl(object.url),
});
appExecuted = true;
// Flush any messages queued up and clear them
for (const message of queuedMessages) {
worker.postMessage(message);
}
queuedMessages = [];
} else {
if (appExecuted) {//6<--
worker.postMessage(object);
} else {
queuedMessages.push(object);
}
}
};
ws.onclose = function(error) {
shutdownJSRuntime();
......
};
......
}
1、额?预期中这个chrome应是个websocketserver供app连接,但看这个代码表明chrome也是个websocketclient,不过这里给server传递了参数role=debugger,表明自己的身份,回过头看app的url拼接"ws://%s/debugger-proxy?role=client",说明app的身份是client,可以推测他们连的是同一个服务器,使用身份字段让区分各自的请求并由websocketserver转发
2、这里出现了debuggerWorker,这是个js线程,这个线程是真正运行RN js代码的地方
3、websocket收到message后创建js运行环境或者转发给debuggerWorker线程
4、创建js线程消息
5、这是执行js代码的消息,这个消息体内包含了代码信息,这一步中根据消息体中的url参数发送给Metro Server,等待Metro打包后返回BlobUrl并传递给debuggerWorker
6、这时其他信息,一般都是jscall
前面我们知道app和chrome都是使用WebSocket Client来连接的,因此我们需要找出WebSocket Server
function attachToServer(server, path) {
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({
server: server,
path: path,
});
var debuggerSocket, clientSocket;
function send(dest, message) {
if (!dest) {
return;
}
try {
dest.send(message);
} catch (e) {
console.warn(e);
// Sometimes this call throws 'not opened'
}
}
wss.on('connection', function(ws) {
const {url} = ws.upgradeReq;
if (url.indexOf('role=debugger') > -1) {
if (debuggerSocket) {
ws.close(1011, 'Another debugger is already connected');
return;
}
debuggerSocket = ws;
debuggerSocket.onerror = debuggerSocket.onclose = () => {
debuggerSocket = null;
if (clientSocket) {
clientSocket.close(1011, 'Debugger was disconnected');
}
};
debuggerSocket.onmessage = ({data}) => send(clientSocket, data);
} else if (url.indexOf('role=client') > -1) {
if (clientSocket) {
clientSocket.onerror = clientSocket.onclose = clientSocket.onmessage = null;
clientSocket.close(1011, 'Another client connected');
}
clientSocket = ws;
clientSocket.onerror = clientSocket.onclose = () => {
clientSocket = null;
send(debuggerSocket, JSON.stringify({method: '$disconnected'}));
};
clientSocket.onmessage = ({data}) => send(debuggerSocket, data);
} else {
ws.close(1011, 'Missing role param');
}
});
return {
server: wss,
isChromeConnected: function() {
return !!debuggerSocket;
},
};
}
这个代码很清晰,大致的内容就是创建一个WebSocketServer,将client(App)、debuger(Chrome)分别保存,并通过本身转发client和debuger的消息,这也应证了之前的猜测
这个类是在debug模式中运行js代码的真实环境,它是个单独的线程,接收到websocket的消息并做对应操作
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);//1<--
} catch (err) {
error = err.message;
}
sendReply(null /* result */, error);
},
setDebuggerVisibility: function(message) {
visibilityState = message.visibilityState;
},
};
1、这里收到消息后就直接importScript(mesage.url),importScript是js自带的函数,这样就能执行url中的代码
message.url中对应的代码都是打包过后的代码,已经跟源码对不上了,这就需要sourceMapping
在打包后的代码后面都跟着一串注释
# sourceMappingURL=http://localhost:8081/index2.map?platform=android&dev=true&minify=false&revisionId=1c71084ee7e99ed7
这段注释执行的时候会促发chrome去请求sourceMapping,chrome通过获取到的sourceMapping就能对应上源码的位置了