RN reload原理解析

649 阅读7分钟

前言

对于RN开发,调试是再熟悉不过的了。其通常涉及reload、hmr和debugger这三个功能点。

本文从reload开始,简要分析下整个流程。使用的RN版本为0.68.0。

业务入口

以下场景相信大家很熟悉了,摇一摇出现调试菜单后,点击reload就可以从server重新加载新的bundle包,我们就从这个节点开始。

image.png

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

这里主要有两个步骤:

  1. 首先获取metro服务请求的地址,里面会进行各种类型匹配和数据组装,最终得出请求bundle的URL,例如 xxx.xxx.xxx.xxx:8081/index.andro…
  2. 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

}
  1. 创建了JSbundleLoader实例,由方法名可知这个loader是面向网络请求保存到本地的bundle。除此之外还有例如加载本地、远程debugger的loader;

  2. 将创建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方法里面有些重要点需要关注下:

  1. _cliServerApi().createDevServerMiddleware里面创建了一系列http中间件,应用于connect框架。后面我们探索hmr和debugger时会详细分析,现在可以先不用管;
  2. 同理,三个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
  };
}
  1. 重点为_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;
}
  1. runMetro函数返回了MetroServer实例,它是metro服务中间件的处理者;
  2. 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);
      }
    },
    // ...
  });

我们来分析下箭头标识点:

  1. processRequest作为metro server处理请求的统一入口,可以响应多个请求url。例如请求index.android.bundle就会执行_processBundleRequest方法;
  2. _createRequestProcessor是公共构建流程,通过注入不同节点的处理函数从而生成不同的产物;
  3. build为具体生成bundle包方法,不同产物对应不同的build方法;
  4. 首先通过graphId获取revPromise,进而判断之前是否有打包过,如果没有就初始化依赖的graph结果,作为bundle打包源料;
  5. bundle生成并序列化方法,需要传入各种配置项;
  6. finish是返回结果的方法,这里直接返回bundle包给app等客户端。

至此,app端就能通过http请求拿到metro server返回的bundle包了。而server的websocket通信是应用于其他业务,reload暂时用不上。

总结

整个流程下来,和我们开发RN方式基本是吻合的,即:

  1. 启动metro服务,这时server就通过node把http服务搭建好了;
  2. APP通过reload发起重新打包请求;
  3. metro服务接收请求,重新计算依赖并打包返回;
  4. APP接收到新的bundle包并保存到本地,RN加载bundle并重新构建。

client为app,server是metro运行的http服务,双方通信使用http协议。