第一次在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操作,主要做两件事:

  1. 打开Chrome的一个tab
  2. 让这个tab打开urlhttp://localhost:8081/debugger-ui/

这个界面就是我们打开Debug时在浏览器见到的第一个界面

这个界面的文件就是Server的index.html,我截取了body的代码:

<body>
<div class="content">
<label for="dark">
<input type="checkbox" id="dark" onclick="Page.toggleDarkTheme()"> Dark Theme
</label>
<label for="maintain-priority">
<input type="checkbox" id="maintain-priority" onclick="Page.togglePriorityMaintenance()"> Maintain Priority
</label>
<p>
React Native JS code runs as a web worker inside this tab.
</p>
<p>Press <kbd id="shortcut" class="shortcut">⌘⌥I</kbd> to open Developer Tools. Enable <a href="https://stackoverflow.com/a/17324511/232122" target="_blank">Pause On Caught Exceptions</a> for a better debugging experience.</p>
<p>You may also install <a href="https://github.com/facebook/react-devtools/tree/master/packages/react-devtools" target="_blank">the standalone version of React Developer Tools</a> to inspect the React component hierarchy, their props, and state.</p>
<p>Status: <span id="status">Loading...</span></p>
</div>
</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连接的原理分析就差不多了,希望对小伙伴们有帮助,欢迎点赞和关注哈。

谢谢大家!

React Native Debug原理浅析的更多相关文章

  1. 《React Native 精解与实战》书籍连载「React Native 底层原理」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  2. React Native & debug & debugger

    React Native & debug & debugger http://localhost:8081/debugger-ui/ react-devtools # yarn: $ ...

  3. React Native运行原理解析

    Facebook 于2015年9月15日推出react native for Android 版本, 加上2014年底已经开源的IOS版本,至此RN (react-native)真正成为跨平台的客户端 ...

  4. React Native 从入门到原理

    React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却寥寥无几. 本文分为两个部分:上半部分用通 ...

  5. 关于React Native 火热的话题,从入门到原理

    本文授权转载,作者:bestswifter(简书) React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原 ...

  6. React Native 从入门到原理一

    React Native 从入门到原理一 React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却 ...

  7. React Native 入门到原理(详解)

    抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...

  8. 小谈React、React Native、React Web

    React有三个东西,React JS 前端Web框架,React Native 移动终端Hybrid框架,React Web是一个源码转换工具(React Native 转 Web,并之所以特别提出 ...

  9. ReactJs和React Native的那些事

    介绍 1,React Js的目的 是为了使前端的V层更具组件化,能更好的复用,它能够使用简单的html标签创建更多的自定义组件标签,内部绑定事件,同时可以让你从操作dom中解脱出来,只需要操作数据就会 ...

随机推荐

  1. Linux基本操作_20191117

    VMware和Ubuntu的安装, 想来想去,还是需要安装,不能老是使用Windows的,后面还有很多都要用到Linux系统的,这个可以说是开发人员必备的了, 基本的使用: 1,Windows下面C: ...

  2. 在Python 中怎么表示一个元素在一个list中的数量?

    commonest = [1,2,2,2,1,3,4,5,1,1] print(commonest.count(1))

  3. Struts配置文件以Spring的方式实现自定义加载

    在使用struts时,我们需要在web.xml中配置过滤器,同时我们需要配置struts的配置文件路径来加载项目中struts的相关配置信息.如果我们不配置路径的话,Struts会有一些默认的加载路径 ...

  4. 吴裕雄--天生自然python学习笔记:python文档操作自动查找替换 Word 文件中的指定文字

    Win32com 组件提供了自动替换 Word 文件中指定文字 的功能 .在使用“查找” 功能替换文字之前,可先清除源文字及目标文字的格式,以免影响替换效果,语法为 : 替换 Word 文件特定文字的 ...

  5. TreeMap简介

    在Map集合框架中,除了HashMap以外,TreeMap也是常用到的集合对象之一.与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序.其中,可以使用 ...

  6. django框架基础-ORM单表操作-长期维护

    ###############    单表操作-添加数据    ################ import os if __name__ == '__main__': os.environ.set ...

  7. android cpu affinity

    暂时无法获取当前线程运行在哪个CPU上,待调查... int omask = 0; int nmask = 0xF0; static void affinity() { int err; int sy ...

  8. Python之循环条件、变量、字符串格式化

    一.认识python python语言的优缺点,自行百度,这里不概述,简单说下,python是一门面向对象,解释型计算机语言.那么问题来了,解释型和编译型语言有什么区别? 1.解释型和编译型语言区别 ...

  9. 转载【docker】CMD ENTRYPOINT 的使用方法

    原文:https://blog.csdn.net/u010900754/article/details/78526443

  10. mysql 优化一

    从几个方面出发: ① 数据库设计② sql语句优化③ 数据库参数配置④ 恰当的硬件资源和操作系统 下面详细介绍: ① 数据库设计 通俗地理解三个范式,对于数据库设计大有好处.在数据库设计中,为了更好地 ...