第一次在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. 工作常见的git命令

     Git创建项目仓库: 1.git init 初始化   2.git remote add origin url 关联远程仓库   3.git pull  拉取远程仓库到本地  相当于(git fet ...

  2. linux_nano命令

    nano是一个字符终端的文本编辑器,有点像DOS下的editor程序.它比vi/vim要简单得多,比较适合Linux初学者使用.某些Linux发行版的默认编辑器就是nano. nano命令可以打开指定 ...

  3. 27)PHP,视图

    其实,视图就是一堆select形成的一个表格,但是这个表格也是存在一个数据库里面的,但是,他不会和一般的表格似得在数据库中显示,就好像虚拟存储器的那种感觉一样. 比如  必看我的一下句子 create ...

  4. vim模式及基础命令

    VIM基本介绍vi和在修改vim命令是linux中强大的文本编辑器,由于linux系统一切皆文件,而配置一个服务就是在修改其配置文件的参数vim其实是vi的升级版yum install -y vim ...

  5. tomcat打印接口延迟时间

    项目中有些页面时延不稳定,需要看每次接口调用时延,怎么看,有两种方法:一种是直接去catalina.out日志中看,一种是直接去localhost_access_log日志中看,第一种需要在代码中实现 ...

  6. iOS渐变视图&动画库、腰杆、音频水滴水波手势、多种对话框、四级展开效果等源码

    iOS精选源码 用户行为追踪--无侵入埋点 .终端日志的打印 支持storyboard的渐变视图&动画库 支持圆形.竖直.横向的摇杆 纯swift实现的类似excel表格效果 swift实现自 ...

  7. 71)PHP,使用cookie的语法问题

    1) 为啥用数组的形式,就是这样好区分,你看都是跟student相关的东西, (2)

  8. .vimrc文件

    1 set number 2 set shiftwidth=4 3 set softtabstop=4 4 set tabstop=4 5 set expandtab 6 "set hlse ...

  9. docker pull很慢解决办法

    经常拉取镜像的时候很慢或者拉不下来,这里可以使用阿里云镜像加速器,然后试试看有没有效果. ##使用阿里云镜像加速器 [root@localhost ~]# mkdir -p /etc/docker [ ...

  10. [LC] 236. Lowest Common Ancestor of a Binary Tree

    Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. According ...