资讯专栏INFORMATION COLUMN

React Native Debug原理浅析

avwu / 3422人阅读

摘要:过程涉及到三个对象,一个是或,一个是,另外一个就是浏览器或或其他。在中进行配置了,也就是会执行脚本。然后会给这个注册一些监听在收到消息时会回调。发出一个消息让浏览器准备的运行环境在收到消息会调用。

第一次在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脚本。

</>复制代码

  1. "scripts": {
  2. "start": "node node_modules/react-native/local-cli/cli.js start"
  3. },

然后cli.js会执行runServer.js,在这里启动一个NodeJS Server:

</>复制代码

  1. const serverInstance = args.https
  2. ? https.createServer(
  3. {
  4. key: fs.readFileSync(args.key),
  5. cert: fs.readFileSync(args.cert),
  6. },
  7. app,
  8. )
  9. : http.createServer(app);
  10. serverInstance.listen(args.port, args.host, 511, function() {
  11. attachHMRServer({
  12. httpServer: serverInstance,
  13. path: "/hot",
  14. packagerServer,
  15. });
  16. wsProxy = webSocketProxy.attachToServer(serverInstance, "/debugger-proxy");
  17. ms = messageSocket.attachToServer(serverInstance, "/message");
  18. readyCallback(reporter);
  19. });

有了中介Server后就可以建立App与浏览器之间的关系了。

2.建立连接

在手机菜单中点击Debug JS Remotely,App就会发出一个Http请求

</>复制代码

  1. GET /launch-js-devtools HTTP/1.1

Server接收到这个请求会执行opn操作,主要做两件事:

</>复制代码

  1. 打开Chrome的一个tab

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

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

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

</>复制代码

  1. Dark Theme
  2. Maintain Priority
  3. React Native JS code runs as a web worker inside this tab.
  4. Press ⌘⌥I to open Developer Tools. Enable Pause On Caught Exceptions for a better debugging experience.

  5. You may also install the standalone version of React Developer Tools to inspect the React component hierarchy, their props, and state.

  6. Status: Loading...

浏览器在执行index.html的时候会发出下面的请求:

</>复制代码

  1. GET /debugger-proxy?role=debugger&name=Chrome HTTP/1.1

我们来看看发出这个请求有什么目的,扒一扒源码:

</>复制代码

  1. function connectToDebuggerProxy() {
  2. const ws = new WebSocket("ws://" + window.location.host + "/debugger-proxy?role=debugger&name=Chrome"); //Chrome通过websocket和Packager保持通讯
  3. //WebSocket注册监听
  4. ws.onopen = function() {
  5. Page.setState({status: {type: "connecting"}});
  6. };
  7. ws.onmessage = async function(message) {
  8. if (!message.data) {
  9. return;
  10. }
  11. const object = JSON.parse(message.data);
  12. if (object.$event === "client-disconnected") {
  13. shutdownJSRuntime();
  14. Page.setState({status: {type: "disconnected"}});
  15. return;
  16. }
  17. if (!object.method) {
  18. return;
  19. }
  20. // Special message that asks for a new JS runtime
  21. if (object.method === "prepareJSRuntime") {
  22. shutdownJSRuntime();
  23. console.clear();
  24. createJSRuntime();
  25. ws.send(JSON.stringify({replyID: object.id}));
  26. Page.setState({status: {type: "connected", id: object.id}});
  27. } else if (object.method === "$disconnected") {
  28. shutdownJSRuntime();
  29. Page.setState({status: {type: "disconnected"}});
  30. } else if (object.method === "executeApplicationScript") {
  31. worker.postMessage({
  32. ...object,
  33. url: await getBlobUrl(object.url),
  34. });
  35. } else {
  36. // Otherwise, pass through to the worker.
  37. worker.postMessage(object);
  38. }
  39. };
  40. ws.onclose = function(error) {
  41. shutdownJSRuntime();
  42. Page.setState({status: {type: "error", error}});
  43. if (error.reason) {
  44. console.warn(error.reason);
  45. }
  46. setTimeout(connectToDebuggerProxy, 500);
  47. };
  48. // Let debuggerWorker.js know when we"re not visible so that we can warn about
  49. // poor performance when using remote debugging.
  50. document.addEventListener("visibilitychange", updateVisibility, false);
  51. }

首先就是通过new WebSocket浏览器建立与Server的联系,WebSocket就是可以保持长连接的全双工通信协议,在握手阶段通过Http进行,后面就和Http没有什么关系了。然后会给这个webSocket注册一些监听:

</>复制代码

  1. ws.onopen
  2. ws.onmessage
  3. ws.onclose

在webSocket收到消息时会回调ws.onmessage。

到这里App和浏览器之间就已经建立连接了,接下来App会发出几个消息让浏览器加载需要调试的代码, 接着往下看。

3.加载调试代码

首先需要强调的就是浏览器加载项目代码肯定不能在UI线程加载吧,要不然肯定影响浏览器的正常工作。那怎么去加载?启一个后台线程,有的小伙伴就要不信了,别急,我们接着去扒一扒源码。
App发出一个消息让浏览器准备JS的运行环境:

</>复制代码

  1. 在收到‘prepareJSRuntime’消息会调用createJSRuntime。
  2. // Special message that asks for a new JS runtime
  3. if (object.method === "prepareJSRuntime") {
  4. shutdownJSRuntime();
  5. console.clear();
  6. createJSRuntime();
  7. ws.send(JSON.stringify({replyID: object.id}));
  8. Page.setState({status: {type: "connected", id: object.id}});
  9. } else if (object.method === "$disconnected") {
  10. shutdownJSRuntime();
  11. Page.setState({status: {type: "disconnected"}});
  12. } else if (object.method === "executeApplicationScript") {
  13. worker.postMessage({
  14. ...object,
  15. url: await getBlobUrl(object.url),
  16. });
  17. } else {
  18. // Otherwise, pass through to the worker.
  19. worker.postMessage(object);
  20. }

接着看‘createJSRuntime’这个函数, 主要工作就是‘new Worker’,看下Worker的定义:

</>复制代码

  1. 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’这个脚本。

</>复制代码

  1. function createJSRuntime() {
  2. // This worker will run the application JavaScript code,
  3. // making sure that it"s run in an environment without a global
  4. // document, to make it consistent with the JSC executor environment.
  5. worker = new Worker("debuggerWorker.js");
  6. worker.onmessage = function(message) {
  7. ws.send(JSON.stringify(message.data));
  8. };
  9. window.onbeforeunload = function() {
  10. return "If you reload this page, it is going to break the debugging session. " +
  11. "You should press" + refreshShortcut + "in simulator to reload.";
  12. };
  13. updateVisibility();
  14. }

接着看看debuggerWorker.js,主要就是一个消息的监听,可以看到在messageHandlers里主要处理两类消息:

</>复制代码

  1. "executeApplicationScript", "setDebuggerVisibility"

</>复制代码

  1. /* global __fbBatchedBridge, self, importScripts, postMessage, onmessage: true */
  2. /* eslint no-unused-vars: 0 */
  3. "use strict";
  4. onmessage = (function() {
  5. var visibilityState;
  6. var showVisibilityWarning = (function() {
  7. var hasWarned = false;
  8. return function() {
  9. // Wait until `YellowBox` gets initialized before displaying the warning.
  10. if (hasWarned || console.warn.toString().includes("[native code]")) {
  11. return;
  12. }
  13. hasWarned = true;
  14. console.warn(
  15. "Remote debugger is in a background tab which may cause apps to " +
  16. "perform slowly. Fix this by foregrounding the tab (or opening it in " +
  17. "a separate window)."
  18. );
  19. };
  20. })();
  21. var messageHandlers = {
  22. "executeApplicationScript": function(message, sendReply) {
  23. for (var key in message.inject) {
  24. self[key] = JSON.parse(message.inject[key]);
  25. }
  26. var error;
  27. try {
  28. importScripts(message.url);
  29. } catch (err) {
  30. error = err.message;
  31. }
  32. sendReply(null /* result */, error);
  33. },
  34. "setDebuggerVisibility": function(message) {
  35. visibilityState = message.visibilityState;
  36. },
  37. };
  38. return function(message) {
  39. if (visibilityState === "hidden") {
  40. showVisibilityWarning();
  41. }
  42. var object = message.data;
  43. var sendReply = function(result, error) {
  44. postMessage({replyID: object.id, result: result, error: error});
  45. };
  46. var handler = messageHandlers[object.method];
  47. if (handler) {
  48. // Special cased handlers
  49. handler(object, sendReply);
  50. } else {
  51. // Other methods get called on the bridge
  52. var returnValue = [[], [], [], 0];
  53. var error;
  54. try {
  55. if (typeof __fbBatchedBridge === "object") {
  56. returnValue = __fbBatchedBridge[object.method].apply(null, object.arguments);
  57. } else {
  58. error = "Failed to call function, __fbBatchedBridge is undefined";
  59. }
  60. } catch (err) {
  61. error = err.message;
  62. } finally {
  63. sendReply(JSON.stringify(returnValue), error);
  64. }
  65. }
  66. };
  67. })();

App在点击调试的时候会给浏览器还发送这么一个‘executeApplicationScript’消息,让浏览器去加载项目代码:

这个messageEvent的数据比较多,我就截取一部分,里面包含了方法名,url(这个url就是后面浏览器需要去下载bundle的地方),inject包含的数据最多,主要是会赋值给浏览器全局对象的方法。

</>复制代码

  1. {
  2. "id": 1,
  3. "method": "executeApplicationScript",
  4. "url": "http://localhost:8081/index.android.bundle?platform=android&dev=true&minify=false",
  5. "inject": {
  6. "__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",{}
  7. }

webSocket首先接收到这个消息, 然后通过worker.postMessage给上面的worker发送‘executeApplicationScript’消息

</>复制代码

  1. ws.onmessage = async function(message) {
  2. ......
  3. // Special message that asks for a new JS runtime
  4. if (object.method === "prepareJSRuntime") {
  5. shutdownJSRuntime();
  6. console.clear();
  7. createJSRuntime();
  8. ws.send(JSON.stringify({replyID: object.id}));
  9. Page.setState({status: {type: "connected", id: object.id}});
  10. } else if (object.method === "$disconnected") {
  11. shutdownJSRuntime();
  12. Page.setState({status: {type: "disconnected"}});
  13. } else if (object.method === "executeApplicationScript") {
  14. worker.postMessage({
  15. ...object,
  16. url: await getBlobUrl(object.url),
  17. });
  18. } else {
  19. // Otherwise, pass through to the worker.
  20. worker.postMessage(object);
  21. }
  22. };

worker接收到这个消息在messageHandlers找到相应的处理方法,在里面首选循环取出inject里面的字段和value然后赋值给self,在这里我理解就是这个worker线程的全局对象,然后通过 importScripts(message.url)去加载bundle。

</>复制代码

  1. var messageHandlers = {
  2. "executeApplicationScript": function(message, sendReply) {
  3. for (var key in message.inject) {
  4. self[key] = JSON.parse(message.inject[key]);
  5. }
  6. var error;
  7. try {
  8. importScripts(message.url);
  9. } catch (err) {
  10. error = err.message;
  11. }
  12. sendReply(null /* result */, error);
  13. },
  14. ......
  15. };

为了证明我上面的分析没错,决定捉包看下发起的请求是不是这样的:

在加载bundle后面还有一个map,体积也很大,有1.74MB的体积,这个是用于映射bundle里面的代码成一个个工程项目里的类文件,这样就和在代码编译器里面调试效果一样了。

4.总结

根据上面的捉包请求简单总结下建立连接的过程,首先通过/launch-jsdevtools打开调试Tab,浏览器通过/debugger-proxy建立与Server的WebSocket连接,然后浏览器打开index.html文件,发起/debugger-ui/debuggerWorker.js建立后台线程,通过这个后台线程加载bundle。

到这里建立Debug连接的原理分析就差不多了,希望对小伙伴们有帮助,欢迎点赞和关注哈。

谢谢大家!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/51959.html

相关文章

  • React Native Debug原理浅析

    摘要:过程涉及到三个对象,一个是或,一个是,另外一个就是浏览器或或其他。在中进行配置了,也就是会执行脚本。然后会给这个注册一些监听在收到消息时会回调。发出一个消息让浏览器准备的运行环境在收到消息会调用。 第一次在segmentfault写博客,很紧张~~~公司项目上ReactNative,之前也是没有接触过,所以也是一边学习一边做项目了,最近腾出手来更新总结了一下RN的Debug的一个小知识...

    senntyou 评论0 收藏0
  • 浅析React之事件系统(二)

    摘要:因为阻止事件冒泡的行为只能用于合成事件中,没法阻止原生事件的冒泡。同时的创建和冒泡是在原生事件冒泡到最顶层的之后的。浅析之事件系统一 上篇文章中,我们谈到了React事件系统的实现方式,和在React中使用原生事件的方法,那么这篇文章我们来继续分析下,看看React中合成事件和原生事件混用的各种情况。 上一个例子 在上篇文章中,我们举了个例子。为了防止大家不记得,我们来看看那个例子的代...

    villainhr 评论0 收藏0
  • SegmentFault 技术周刊 Vol.4 - 这份 Android 有点甜

    摘要:阅读本期周刊,你将快速入门,开启甜蜜之旅。然则的原理负责发送以及处理消息,创建消息队列并不断从队列中取出消息交给,则用于保存消息。 showImg(/img/bVCN99?w=900&h=385); 2016 年 8 月,Android 7.0 Nougat(牛轧糖)正式发布,那么问题来了,你 Marshmallow 了么(¬ -̮ ¬) Cupcake、Donut、Gingerbre...

    jay_tian 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<