启动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');
}
});