前言
对于RN开发,调试是再熟悉不过的了。其通常涉及reload、hmr和debugger这三个功能点。
本文从reload开始,简要分析下整个流程。使用的RN版本为0.68.0。
业务入口
以下场景相信大家很熟悉了,摇一摇出现调试菜单后,点击reload就可以从server重新加载新的bundle包,我们就从这个节点开始。
Android端流程
下面是菜单功能项设置入口,
// DevSupportManagerBase.java
@Override
public void showDevOptionsDialog() {
if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()){
return;
}
LinkedHashMap<String, DevOptionHandler> options = new LinkedHashMap<>();
/* register standard options */
options.put(
mApplicationContext.getString(R.string.catalyst_reload),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
if (!mDevSettings.isJSDevModeEnabled()
&& mDevSettings.isHotModuleReplacementEnabled()) {
Toast.makeText(
mApplicationContext,
mApplicationContext.getString(R.string.catalyst_hot_reloading_auto_disable),
Toast.LENGTH_LONG)
.show();
mDevSettings.setHotModuleReplacementEnabled(false);
}
// 入口
handleReloadJS();
}
});
});
点击reload项后,会执行handleReloadJS方法:
// BridgeDevSupportManager.java
@Override
public void handleReloadJS() {
UiThreadUtil.assertOnUiThread();
ReactMarker.logMarker(
ReactMarkerConstants.RELOAD,
getDevSettings().getPackagerConnectionSettings().getDebugServerHost());
// dismiss redbox if exists
hideRedboxDialog();
if (getDevSettings().isRemoteJSDebugEnabled()) {
// debug模式下的reLoad
PrinterHolder.getPrinter()
.logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy");
showDevLoadingViewForRemoteJSEnabled();
// debug入口
reloadJSInProxyMode();
} else {
PrinterHolder.getPrinter()
.logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
String bundleURL =
getDevServerHelper()
.getDevServerBundleURL(Assertions.assertNotNull(getJSAppBundleName()));
// bundleURL就是url链接,例如http://10.0.2.2:8081/index.android.bundle?platform=android&dev=true&minify=false
reloadJSFromServer(bundleURL);
}
}
这里主要有两个步骤:
- 首先获取metro服务请求的地址,里面会进行各种类型匹配和数据组装,最终得出请求bundle的URL,例如 xxx.xxx.xxx.xxx:8081/index.andro…;
- reloadJSFromServer -> 1. 发起http请求;2.保存下载的bundle到本地;3. 加载bundle刷新。
public void reloadJSFromServer(final String bundleURL) {
reloadJSFromServer(
bundleURL,
new BundleLoadCallback() {
@Override
public void onSuccess() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mReactInstanceDevHelper.onJSBundleLoadedFromServer();
}
});
}
});
}
沿着reloadJSFromServer方法往下挖,从DevServerHelper到BundleDownloader,最终可以看到实际调用了BundleDownloader的downloadBundleFromURL方法,根据传参,并请求bundle后保存到本地。大家感兴趣可以自行翻阅相关代码。
好,到这里为止,app就已经发送http请求获取到bundle位置并保存到本地了。我们接着看后续流程。
往上追溯,可以发现,mReactInstanceDevHelper实例是在ReactInstanceManager.createDevHelperInterface()创建的,里面也重写了一系列方法。
// ReactInstanceManager.java
private ReactInstanceDevHelper createDevHelperInterface() {
return new ReactInstanceDevHelper() {
// ...
@Override
public void onJSBundleLoadedFromServer() {
ReactInstanceManager.this.onJSBundleLoadedFromServer();
}
// ...
}
@ThreadConfined(UI)
private void onJSBundleLoadedFromServer() {
FLog.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
JSBundleLoader bundleLoader =
JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile()); // -> 1
recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader); // -> 2
}
-
创建了JSbundleLoader实例,由方法名可知这个loader是面向网络请求保存到本地的bundle。除此之外还有例如加载本地、远程debugger的loader;
-
将创建bundleLoader作为参数丢入到recreateReactContextInBackground中,开始接入到RN环境加载主流程中。这里就不介绍加载主流程的内容了,大家可以自行去了解。
OK。native端逻辑就分析完成了。
总的来说,native逻辑:reload -> http请求 -> bundle cache -> 创建JSBundleloader -> 触发RN重新加载。
接下来我们看看JS端是如何提供bundle的。
JS端流程
JS端核心为metro服务,我们一般通过npm执行命令拉起服务。整体流程涉及的模块顺序为:
npm start -> node_modules/react-native/local-cli/cli.js start -> @react-native-community/cli -> @react-native-community/cli-plugin-metro -> runServer -> metro.runServer。
我们具体看看:
// @react-native-community\cli-plugin-metro\build\commands\start\runServer.js
async function runServer(_argv, ctx, args) {
let reportEvent;
const terminal = new (_metroCore().Terminal)(process.stdout);
const ReporterImpl = getReporterImpl(args.customLogReporterPath);
const terminalReporter = new ReporterImpl(terminal);
const reporter = {
update(event) {
terminalReporter.update(event);
if (reportEvent) {
reportEvent(event);
}
}
};
// 基本配置
const metroConfig = await (0, _loadMetroConfig.default)(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, // 中间件,对应不同页面
// 下面三个都是websocket server
websocketEndpoints, // websocket的映射表,key:目录,value为server实例,例如 debug: /debugger-proxy
messageSocketEndpoint,
eventsSocketEndpoint
} = (0, _cliServerApi().createDevServerMiddleware)({
host: args.host,
port: metroConfig.server.port,
watchFolders: metroConfig.watchFolders
});
middleware.use(_cliServerApi().indexPageMiddleware);
const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
metroConfig.server.enhanceMiddleware = (metroMiddleware, server) => {
if (customEnhanceMiddleware) {
metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
}
return middleware.use(metroMiddleware);
};
// 启动websocket server,后面app或者chrome都会以client形式访问
const serverInstance = await _metro().default.runServer(metroConfig, {
host: args.host,
secure: args.https,
secureCert: args.cert,
secureKey: args.key,
hmrEnabled: true,
websocketEndpoints
});
reportEvent = eventsSocketEndpoint.reportEvent;
if (args.interactive) {
(0, _watchMode.default)(messageSocketEndpoint);
} // In Node 8, the default keep-alive for an HTTP connection is 5 seconds. In
// early versions of Node 8, this was implemented in a buggy way which caused
// some HTTP responses (like those containing large JS bundles) to be
// terminated early.
//
// As a workaround, arbitrarily increase the keep-alive from 5 to 30 seconds,
// which should be enough to send even the largest of JS bundles.
//
// For more info: https://github.com/nodejs/node/issues/13391
//
serverInstance.keepAliveTimeout = 30000;
await (0, _cliTools().releaseChecker)(ctx.root);
}
runServer方法里面有些重要点需要关注下:
- _cliServerApi().createDevServerMiddleware里面创建了一系列http中间件,应用于connect框架。后面我们探索hmr和debugger时会详细分析,现在可以先不用管;
- 同理,三个websocket的endPoint是对应到其他业务的,和我们reload无关。可以看下代码:
// node_modules\@react-native-community\cli-server-api\build\index.js
function createDevServerMiddleware(options) {
const debuggerProxyEndpoint = (0, _createDebuggerProxyEndpoint.default)();
const isDebuggerConnected = debuggerProxyEndpoint.isDebuggerConnected;
const messageSocketEndpoint = (0, _createMessageSocketEndpoint.default)();
const broadcast = messageSocketEndpoint.broadcast;
const eventsSocketEndpoint = (0, _createEventsSocketEndpoint.default)(broadcast);
// 中间件,里面有着不同的path和相应执行的函数
const middleware = (0, _connect().default)().use(_securityHeadersMiddleware.default) // @ts-ignore compression and connect types mismatch
.use((0, _compression().default)()).use((0, _nocache().default)()).use('/debugger-ui', (0, _cliDebuggerUi().debuggerUIMiddleware)()).use('/launch-js-devtools', (0, _devToolsMiddleware.default)(options, isDebuggerConnected)).use('/open-stack-frame', (0, _openStackFrameInEditorMiddleware.default)(options)).use('/open-url', _openURLMiddleware.default).use('/status', _statusPageMiddleware.default).use('/symbolicate', _rawBodyMiddleware.default).use('/systrace', _systraceProfileMiddleware.default).use('/reload', (_req, res) => {
broadcast('reload');
res.end('OK');
}).use((0, _errorhandler().default)());
options.watchFolders.forEach(folder => {
// @ts-ignore mismatch between express and connect middleware types
middleware.use((0, _serveStatic().default)(folder));
});
return {
// websocket
websocketEndpoints: {
'/debugger-proxy': debuggerProxyEndpoint.server,
'/message': messageSocketEndpoint.server,
'/events': eventsSocketEndpoint.server
},
debuggerProxyEndpoint,
messageSocketEndpoint,
eventsSocketEndpoint,
middleware
};
}
- 重点为_metro().default.runServer方法,其内部会建立起http server并监听特定端口,默认是8081:
// node_modules\metro\src\index.js
exports.runServer = async (
config,
{
hasReducedPerformance = false,
host,
onError,
onReady,
secureServerOptions,
secure,
//deprecated
secureCert,
// deprecated
secureKey,
// deprecated
waitForBundler = false,
websocketEndpoints = {},
}
) => {
// ...
const connect = require("connect"); // 1. 使用了connect开源框架
const serverApp = connect();
const {
middleware, // metro服务,以中间件形式存在
end,
metroServer
} = await createConnectMiddleware(
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 = Object.assign(
{
key: fs.readFileSync(secureKey),
cert: fs.readFileSync(secureCert),
},
secureServerOptions
);
}
httpServer = https.createServer(options, serverApp);
} else {
httpServer = http.createServer(serverApp);
}
httpServer.on("error", (error) => {
if (onError) {
onError(error);
}
end();
});
return new Promise((resolve, reject) => {
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
),
}),
});
// 升级协议为websocket
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();
}
});
// ...
}); // 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("error", (error) => {
end();
reject(error);
});
httpServer.on("close", () => {
end();
});
});
};
OK,我们接着看看createConnectMiddleware做了什么:
// node_modules\metro\src\index.js
const createConnectMiddleware = async function (config, options) {
// 1. 启动metro服务
const metroServer = await runMetro(config, options);
// 2. 中间件服务
let enhancedMiddleware = metroServer.processRequest; // Enhance the resulting middleware using the config options
if (config.server.enhanceMiddleware) {
enhancedMiddleware = config.server.enhanceMiddleware(
enhancedMiddleware,
metroServer
);
}
return {
// hmr相关,先不管
attachHmrServer(httpServer) {
const wss = createWebsocketServer({
websocketServer: new MetroHmrServer(
metroServer.getBundler(),
metroServer.getCreateModuleId(),
config
),
});
// 协议升级相关
httpServer.on("upgrade", (request, socket, head) => {
const { pathname } = parse(request.url);
if (pathname === "/hot") {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});
},
metroServer,
middleware: enhancedMiddleware,
end() {
metroServer.end();
},
};
};
// 运行metro服务
async function runMetro(config, options) {
const mergedConfig = await getConfig(config);
const {
reporter,
server: { port },
} = mergedConfig;
reporter.update({
hasReducedPerformance: options
? Boolean(options.hasReducedPerformance)
: false,
port,
type: "initialize_started",
});
const { waitForBundler = false, ...serverOptions } =
options !== null && options !== void 0 ? options : {};
// 创建MetroServer实例
const server = new MetroServer(mergedConfig, serverOptions);
const readyPromise = server
.ready()
.then(() => {
reporter.update({
type: "initialize_done",
port,
});
})
.catch((error) => {
reporter.update({
type: "initialize_failed",
port,
error,
});
});
if (waitForBundler) {
await readyPromise;
}
return server;
}
- runMetro函数返回了MetroServer实例,它是metro服务中间件的处理者;
- metroServer的processRequest作为统一请求处理入口,以中间件形式应用到connect框架中。即url请求会经过它,如果有匹配到相应的路径,就做对应处理。
// node_modules\metro\src\Server.js
processRequest = (req, res, next) => {
this._processRequest(req, res, next).catch(next);
};
async _processRequest(req, res, next) {
const originalUrl = req.url;
req.url = this._config.server.rewriteRequestUrl(req.url);
const urlObj = url.parse(req.url, true);
const { host } = req.headers;
// ...
// path匹配
if (pathname.endsWith(".bundle")) {
const options = this._parseOptions(formattedUrl);
if (options.runtimeBytecodeVersion) {
await this._processBytecodeBundleRequest(req, res, options);
} else {
await this._processBundleRequest(req, res, options); // -> 1
}
if (this._serverOptions && this._serverOptions.onBundleBuilt) {
this._serverOptions.onBundleBuilt(pathname);
}
} else if (pathname.endsWith(".map")) {
// ...
} else if (pathname.startsWith("/assets/") || pathname === "/assets") {
// ...
} else if (pathname === "/symbolicate") {
// ...
} else {
next();
}
}
// 处理bundle请求
_processBundleRequest = this._createRequestProcessor({ // -> 2
// ...
build: async ({ // -> 3
entryFile,
graphId,
graphOptions,
onProgress,
serializerOptions,
transformOptions,
}) => {
const revPromise = this._bundler.getRevisionByGraphId(graphId);
const { delta, revision } = await (revPromise != null
? this._bundler.updateGraph(await revPromise, false)
// 初始化依赖的graph结构,如果这个revisionId打包过的情况下是生成差量的结构,最后就打包成bundle对象
: this._bundler.initializeGraph(entryFile, transformOptions, {
onProgress,
shallow: graphOptions.shallow,
})); // -> 4
const serializer =
this._config.serializer.customSerializer ||
((...args) => bundleToString(baseJSBundle(...args)).code); // -> 5
// bundle生成
const bundle = await serializer(
entryFile,
revision.prepend,
revision.graph,
{
asyncRequireModulePath: await this._resolveRelativePath(
this._config.transformer.asyncRequireModulePath,
{
transformOptions,
relativeTo: "project",
}
),
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
dev: transformOptions.dev,
projectRoot: this._config.projectRoot,
modulesOnly: serializerOptions.modulesOnly,
runBeforeMainModule:
this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryFile)
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
sourceUrl: serializerOptions.sourceUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
}
);
const bundleCode = typeof bundle === "string" ? bundle : bundle.code;
return {
numModifiedFiles: delta.reset
? delta.added.size + revision.prepend.length
: delta.added.size + delta.modified.size + delta.deleted.size,
lastModifiedDate: revision.date,
nextRevId: revision.id,
bundle: bundleCode,
};
},
finish({ req, mres, result }) {
if (
// We avoid parsing the dates since the client should never send a more
// recent date than the one returned by the Delta Bundler (if that's the
// case it's fine to return the whole bundle).
req.headers["if-modified-since"] ===
result.lastModifiedDate.toUTCString()
) {
debug("Responding with 304");
mres.writeHead(304);
mres.end();
} else {
mres.setHeader(
FILES_CHANGED_COUNT_HEADER,
String(result.numModifiedFiles)
);
mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId));
mres.setHeader("Content-Type", "application/javascript; charset=UTF-8");
mres.setHeader("Last-Modified", result.lastModifiedDate.toUTCString());
mres.setHeader(
"Content-Length",
String(Buffer.byteLength(result.bundle))
);
mres.end(result.bundle);
}
},
// ...
});
我们来分析下箭头标识点:
- processRequest作为metro server处理请求的统一入口,可以响应多个请求url。例如请求index.android.bundle就会执行_processBundleRequest方法;
- _createRequestProcessor是公共构建流程,通过注入不同节点的处理函数从而生成不同的产物;
- build为具体生成bundle包方法,不同产物对应不同的build方法;
- 首先通过graphId获取revPromise,进而判断之前是否有打包过,如果没有就初始化依赖的graph结果,作为bundle打包源料;
- bundle生成并序列化方法,需要传入各种配置项;
- finish是返回结果的方法,这里直接返回bundle包给app等客户端。
至此,app端就能通过http请求拿到metro server返回的bundle包了。而server的websocket通信是应用于其他业务,reload暂时用不上。
总结
整个流程下来,和我们开发RN方式基本是吻合的,即:
- 启动metro服务,这时server就通过node把http服务搭建好了;
- APP通过reload发起重新打包请求;
- metro服务接收请求,重新计算依赖并打包返回;
- APP接收到新的bundle包并保存到本地,RN加载bundle并重新构建。
client为app,server是metro运行的http服务,双方通信使用http协议。