react-native-cli源码分析之RN调试

启动metro server

调试RN的第一步是开启metro server,通过命令行react-natvie start可以启动本地node服务,这里会调用metro的API来启动
(https://github.com/react-native-community/cli/blob/main/packages/cli-plugin-metro/src/commands/start/runServer.ts)

async function runServer(_argv: Array, ctx: Config, args: Args) {
 //...省略无关代码

 const metroConfig = await loadMetroConfig(ctx, {
   config: args.config,
   maxWorkers: args.maxWorkers,
   port: args.port,
   resetCache: args.resetCache,
   watchFolders: args.watchFolders,
   projectRoot: args.projectRoot,
   sourceExts: args.sourceExts,
   reporter,
 });

 if (args.assetPlugins) {
   metroConfig.transformer.assetPlugins = args.assetPlugins.map((plugin) =>
     require.resolve(plugin),
   );
 }

 const {
   middleware,
   websocketEndpoints,
   messageSocketEndpoint,
   eventsSocketEndpoint,
 } = createDevServerMiddleware({
   host: args.host,
   port: metroConfig.server.port,
   watchFolders: metroConfig.watchFolders,
 });
 middleware.use(indexPageMiddleware);

 const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
 metroConfig.server.enhanceMiddleware = (
   metroMiddleware: any,
   server: unknown,
 ) => {
   if (customEnhanceMiddleware) {
     metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
   }
   return middleware.use(metroMiddleware);
 };

 const serverInstance = await Metro.runServer(metroConfig, {
   host: args.host,
   secure: args.https,
   secureCert: args.cert,
   secureKey: args.key,
   hmrEnabled: true,
   websocketEndpoints,
 });

 //...省略无关代码
}

Metro.runServer 会启动一个 http server来跟客户端通信(https://github.com/facebook/metro/blob/main/packages/metro/src/index.flow.js)

exports.runServer = async (
 config,
 {
   hasReducedPerformance = false,
   host,
   onError,
   onReady,
   secureServerOptions,
   secure,
   //deprecated
   secureCert,
   // deprecated
   secureKey,
   // deprecated
   waitForBundler = false,
   websocketEndpoints = {},
 }
) => {
 //省略无关代码...

 const connect = require("connect");

 const serverApp = connect();
 const { middleware, end, metroServer } = await createConnectMiddleware(//这里metroServer作为中间件使用
   config,
   {
     hasReducedPerformance,
     waitForBundler,
   }
 );
 serverApp.use(middleware);
 let inspectorProxy = null;

 if (config.server.runInspectorProxy) {
   inspectorProxy = new InspectorProxy(config.projectRoot);
 }

 let httpServer;

 if (secure || secureServerOptions != null) {
   let options = secureServerOptions;

   if (typeof secureKey === "string" && typeof secureCert === "string") {
     options = {
       key: fs.readFileSync(secureKey),
       cert: fs.readFileSync(secureCert),
       ...secureServerOptions,
     };
   }

   httpServer = https.createServer(options, serverApp);
 } else {
   httpServer = http.createServer(serverApp);
 }

 return new Promise((resolve, reject) => {
   httpServer.on("error", (error) => {
     if (onError) {
       onError(error);
     }

     reject(error);
     end();
   });
   httpServer.listen(config.server.port, host, () => {
     if (onReady) {
       onReady(httpServer);
     }

     Object.assign(websocketEndpoints, {
       ...(inspectorProxy
         ? { ...inspectorProxy.createWebSocketListeners(httpServer) }
         : {}),
       "/hot": createWebsocketServer({
         websocketServer: new MetroHmrServer(
           metroServer.getBundler(),
           metroServer.getCreateModuleId(),
           config
         ),
       }),
     });
     httpServer.on("upgrade", (request, socket, head) => {
       const { pathname } = parse(request.url);

       if (pathname != null && websocketEndpoints[pathname]) {
         websocketEndpoints[pathname].handleUpgrade(
           request,
           socket,
           head,
           (ws) => {
             websocketEndpoints[pathname].emit("connection", ws, request);
           }
         );
       } else {
         socket.destroy();
       }
     });

     if (inspectorProxy) {
       // TODO(hypuk): Refactor inspectorProxy.processRequest into separate request handlers
       // so that we could provide routes (/json/list and /json/version) here.
       // Currently this causes Metro to give warning about T31407894.
       // $FlowFixMe[method-unbinding] added when improving typing for this parameters
       serverApp.use(inspectorProxy.processRequest.bind(inspectorProxy));
     }

     resolve(httpServer);
   }); // Disable any kind of automatic timeout behavior for incoming
   // requests in case it takes the packager more than the default
   // timeout of 120 seconds to respond to a request.

   httpServer.timeout = 0;
   httpServer.on("close", () => {
     end();
   });
 });
};

这里createConnectMiddleware方法将metroServer创建出来并返回给外面,作为MetroHmrServer的参数使用。

RN包的加载

在我们启动APP,并调用RN框架提供的函数初始化bridge后,就开始RN包的加载流程了。 本地包的加载比较简单,就是调用本地的JS引擎执行对应的JS代码,这里着重讲一下远程加载。
在远程模式下,客户端会通过websocket链接到本地npm服务的/debugger-proxy端点下的launch-js-devtools接口,如果此时本地node服务已经开启,则会收到websocket的请求,并创建debugger server,同时会调用devToolsMiddleware的launchDevTools方法,打开浏览器的debugger-ui页面。 debugger-ui页面会创建一个websocket的连接,对端是debuggerproxy

//devToolsMiddleware.ts

function launchDevTools(
 {host, port, watchFolders}: LaunchDevToolsOptions,
 isDebuggerConnected: () => boolean,
) {
 // Explicit config always wins
 const customDebugger = process.env.REACT_DEBUGGER;
 if (customDebugger) {
   startCustomDebugger({watchFolders, customDebugger});
 } else if (!isDebuggerConnected()) {
   // Debugger is not yet open; we need to open a session
   launchDefaultDebugger(host, port);
 }
}

function launchDefaultDebugger(
 host: string | undefined,
 port: number,
 args = '',
) {
 const hostname = host || 'localhost';
 const debuggerURL = `http://${hostname}:${port}/debugger-ui${args}`;
 logger.info('Launching Dev Tools...');
 launchDebugger(debuggerURL);
}

//index.js

function connectToDebuggerProxy() {
 const ws = new WebSocket(
   'ws://' +
     window.location.host +
     '/debugger-proxy?role=debugger&name=Chrome',
 );
 let worker;

 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. ' +
       'Press ' +
       refreshShortcut +
       ' on the device to reload.'
     );
   };
   updateVisibility();
 }
 //省略部分代码...
}

接着,客户端发送prepareJSRuntime请求到node服务,node会在本地通过webworker开启一个debuggerWorker.js的线程(https://github.com/react-native-community/cli/blob/main/packages/cli-debugger-ui/src/ui/index.js),监听客户端发过来的请求,并转发给该webworker处理。

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. ' +
       'Press ' +
       refreshShortcut +
       ' on the device to reload.'
     );
   };
   updateVisibility();
 }

再接下来,客户端会把bundle的内容转成字符串,并调用executeApplicationScript接口将该字符串传递给node服务,该请求会附带bundle在服务端的地址,node收到后通过importScript加载本地的bundle(https://github.com/react-native-community/cli/blob/main/packages/cli-debugger-ui/src/ui/debuggerWorker.js)

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;
   },
 };

chrome debugger消息转发

在cli-server-api的index.ts文件中,我们看到createDevServerMiddleware方法创建了三个endpoint,分别是 debuggerProxyEndpoint、messageSocketEndpoint和eventsSocketEndpoint。其中debuggerProxyEndpoint是作为代理用来转发debugger消息的,我们看下内部如何实现。
在createDebuggerProxyEndpoint方法中,我们创建了两个socket,一个是debuggerSocket,用来连接chrome debugger,另一个是clientSocket,用来连接客户端。在websocket的connection的回调事件中,根据url中role的不同,分别走了两段逻辑,一段是将debuggerSocket的onmessage方法定义为把数据发送给clientSocket,另一段是把clientSocket的onmessage方法定义为把数据发送给debuggerSocket,这样就实现了数据的转发。

wss.on('connection', (socket, request) => {
   const {url} = request;

   if (url && url.indexOf('role=debugger') > -1) {
     if (debuggerSocket) {
       socket.close(1011, 'Another debugger is already connected');
       return;
     }
     debuggerSocket = socket;
     if (debuggerSocket) {
       debuggerSocket.onerror = debuggerSocketCloseHandler;
       debuggerSocket.onclose = debuggerSocketCloseHandler;
       debuggerSocket.onmessage = ({data}) => send(clientSocket, data);
     }
   } else if (url && url.indexOf('role=client') > -1) {
     if (clientSocket) {
       clientSocket.onerror = () => {};
       clientSocket.onclose = () => {};
       clientSocket.onmessage = () => {};
       clientSocket.close(1011, 'Another client connected');
     }
     clientSocket = socket;
     clientSocket.onerror = clientSocketCloseHandler;
     clientSocket.onclose = clientSocketCloseHandler;
     clientSocket.onmessage = ({data}) => send(debuggerSocket, data);
   } else {
     socket.close(1011, 'Missing role param');
   }
 });

你可能感兴趣的:(react-native-cli源码分析之RN调试)