最近一直思考着如何让react native开发时分步加载js代码,而RN本身就有支持类似的功能,比如hot reload,而hot reload是根据文件的变化而进行差量加载代码的,如果需要实现分步加载的功能,就需要魔改react native相关的加载源码,因此经过几天的研究,总体了解了React Naitve调试过程中的流程,现分享给需要魔改或者了解相关源码的同学们。
这里大致把内容分成三部分,这里先说“远程代码加载”。
相信大家开发RN的时候经常有这样的操作——修改代码后摇一摇手机唤出rn dev弹框,点击reload重新加载。现以此为切入点,展开调试源码之旅。
UI入口源码如下:
//DevSupportManagerImpl.java
@Override
public void showDevOptionsDialog() {
if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) {
return;
}
LinkedHashMap options = new LinkedHashMap<>();
/* register standard options */
options.put(
mApplicationContext.getString(R.string.catalyst_reloadjs),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
handleReloadJS();//这里重载js
}
});
handleReloadJS代码如下:
@Override
public void handleReloadJS() {
UiThreadUtil.assertOnUiThread();
// dismiss redbox if exists
hideRedboxDialog();
if (mDevSettings.isRemoteJSDebugEnabled()) {
///略略略
} else {
String bundleURL =
mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
reloadJSFromServer(bundleURL);//从服务端reload
}
}
这一步生成了一个bundleURL对象,其生成过程就是根据你当前加载的js入口名进行拼接,给个例子:
http://10.0.2.2:8081/index.delta?platform=android&dev=true&minify=false
然后看看加载bundleURl的函数reloadJSFromServer:
//DevSupportManagerImpl.java
public void reloadJSFromServer(final String bundleURL) {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START);
mDevLoadingViewController.showForUrl(bundleURL);
mDevLoadingViewVisible = true;
final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo();
mDevServerHelper.downloadBundleFromURL(
new DevBundleDownloadListener() {
@Override
public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) {
................
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient);
}
});
}
............
...........
},
mJSBundleTempFile,
bundleURL,
bundleInfo);
}
reloadJSFromServer函数把文件下载成功后抛给mReactInstanceManagerHelper加载,中间并无其他逻辑,最后执行如下代码:
//ReactInstanceManager.java
@ThreadConfined(UI)
private void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
JSBundleLoader bundleLoader = nativeDeltaClient == null
? JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(),
mDevSupportManager.getDownloadedJSBundleFile())
: JSBundleLoader.createDeltaFromNetworkLoader(
mDevSupportManager.getSourceUrl(), nativeDeltaClient);
recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader);
}
这里有根据nativeDeltaClient是否为空来选择加载器,因为reload操作是全量重载,因此这个情况下总是空,因此这里选择的是CachedBundleFromNetworkLoader加载已经下载下来的JSBundleFile。最后就执行我们的recreateReactContextInBackground真正地重新加载代码了,这个函数我就不解释了,网上有许多资料可以自己了解了解。
到这里App端就完成了Reload操作了,而我们的JSBundleFile是从哪里下载呢?
metro是谁?其实百分之百的RN开发者都见过它,只是有些人不知道它的名字。
看到Running Metro Bundler on port 8081了么?没错!这个进程就是Metro打包服务,调试中100%的打包工作都由它来完成。
我们来看看打包服务大致是怎么接活的吧。
直接开门见山,找到metro代码位置./node_modules/metro,总入口runServer:
//index.js.flow
exports.runServer = async (
config: ConfigT,
{
host,
onReady,
onError,
secure = false,
secureKey,
secureCert,
hmrEnabled = false,
}: RunServerOptions,
) => {
// Lazy require
const connect = require('connect');
const serverApp = connect();//1 <--
const {
attachHmrServer,
middleware,
metroServer,//2 <--
end,
} = await exports.createConnectMiddleware(config);
serverApp.use(middleware);
........
let httpServer;
if (secure) {
httpServer = https.createServer(
{
key: await readFile(secureKey),
cert: await readFile(secureCert),
},
serverApp,
);
} else {
httpServer = http.createServer(serverApp);
}
httpServer.on('error', error => {
onError && onError(error);
end();
});
if (hmrEnabled) {
attachHmrServer(httpServer);
}
return new Promise((resolve, reject) => {
httpServer.listen(config.server.port, host, () => {//3 <--
onReady && onReady(httpServer);
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('error', error => {
end();
reject(error);
});
httpServer.on('close', () => {
end();
});
});
};
代码中有三个箭头:
1、metro的服务使用了connect开源框架实现
2、metroServer就是metro服务,它以中间件形式存在
3、listen后代表这个服务已经启动
MetroServer是打包的关键,我们来看看Server的源码:
Server.js.flow
async _processRequest(
req: IncomingMessage,
res: ServerResponse,
next: (?Error) => mixed,
) {
const urlObj = url.parse(req.url, true);
const {host} = req.headers;
debug(`Handling request: ${host ? 'http://' + host : ''}${req.url}`);
/* $FlowFixMe: Could be empty if the URL is invalid. */
const pathname: string = urlObj.pathname;
if (pathname.match(/\.bundle$/)) {
await this._processBundleRequest(req, res);
} else if (pathname.match(/\.map$/)) {
await this._processSourceMapRequest(req, res);
} else if (pathname.match(/\.assets$/)) {
await this._processAssetsRequest(req, res);
} else if (pathname.match(/\.delta$/)) {//1 <--
await this._processDeltaRequest(req, res);
}
.... ....
else {
next();
}
}
_processDeltaRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting delta',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
build_id: context.buildID,
bundle_options: context.bundleOptions,
bundle_hash: context.graphId,
};
},
createEndEntry(context) {
return {
outdated_modules: context.result.numModifiedFiles,
};
},
build: async ({
revisionId,
graphId,
entryFile,
transformOptions,
serializerOptions,
onProgress,
}) => {
let revPromise;
if (revisionId != null) {
revPromise = this._bundler.getRevision(revisionId);//2 <--
}
// Even if we receive a revisionId, it might have expired.
if (revPromise == null) {
revPromise = this._bundler.getRevisionByGraphId(graphId);
}
let delta;
let revision;
if (revPromise != null) {
const prevRevision = await revPromise;
({delta, revision} = await this._bundler.updateGraph(
prevRevision,
prevRevision.id !== revisionId,
));
} else {
({delta, revision} = await this._bundler.initializeGraph(//3 <--
entryFile,
transformOptions,
{onProgress},
));
}
const bundle = deltaJSBundle(
entryFile,
revision.prepend,
delta,
revision.id,
revision.graph,
... ...
);
return {
numModifiedFiles:
delta.added.size + delta.modified.size + delta.deleted.size,
nextRevId: revision.id,
bundle,
};
},
finish({mres, result}) {//4 <--
const bundle = serializeDeltaJSBundle.toJSON(result.bundle);
mres.setHeader(
FILES_CHANGED_COUNT_HEADER,
String(result.numModifiedFiles),
);
mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId));
mres.setHeader('Content-Type', 'application/json');
mres.setHeader('Content-Length', String(Buffer.byteLength(bundle)));
mres.end(bundle);
},
});
1、所有的请求都会走到_processRequest,会根据请求的类型来做不同操作,如reload对应的是_processDeltaRequest
2、_processDeltaRequest中会先根据revisionId来判断这个版本是不是打包过,而reload都是会生成新的revisionId,所以这里获取到的revPromise必为空
3、初始化依赖的graph结构,如果这个revisionId打包过的情况下是生成差量的结构,最后就打包成bundle对象
4、finish的时候会把打包后的bundle回传给app
本节的内容比较简单,主要的流程大概是
1、App通过reload发起重新打包请求
2、Metro服务受理请求,重新计算依赖并重新打包
3、App收到打包后的bundle保存到临时文件中并recreateReactContextInBackground
App <---> MetroServer <--> Bundler