React Native Debug原理浅析
第一次在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操作,主要做两件事:
这个界面就是我们打开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线程加载吧,要不然肯定影响浏览器的正常工作。那怎么去加载?启一个后台线程,有的小伙伴就要不信了,别急,我们接着去扒一扒源码。 在收到‘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',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 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’消息,让浏览器去加载项目代码: { "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"],["PlatformConstants",{"ServerHost":"localhost:8081","reactNativeVersion":{"patch":0,"prerelease":null,"minor":51,"major":0},"Version":21,"isTesting":false}],["TimePickerAndroid",{} } webSocket首先接收到这个消息,然后通过 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',pass through to the worker. worker.postMessage(object); } }; worker接收到这个消息在messageHandlers找到相应的处理方法,在里面首选循环取出inject里面的字段和value然后赋值给self,在这里我理解就是这个worker线程的全局对象,然后通过 importScripts(message.url)去加载bundle。 var messageHandlers = { 'executeApplicationScript': function(message,...... }; 为了证明我上面的分析没错,决定捉包看下发起的请求是不是这样的: 在加载bundle后面还有一个map,体积也很大,有1.74MB的体积,这个是用于映射bundle里面的代码成一个个工程项目里的类文件,这样就和在代码编译器里面调试效果一样了。 4.总结根据上面的捉包请求简单总结下建立连接的过程,首先通过/launch-jsdevtools打开调试Tab,浏览器通过/debugger-proxy建立与Server的WebSocket连接,然后浏览器打开index.html文件,发起/debugger-ui/debuggerWorker.js建立后台线程,通过这个后台线程加载bundle。 到这里建立Debug连接的原理分析就差不多了,希望对小伙伴们有帮助,欢迎点赞和关注哈。 谢谢大家! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |