React Native调试源码分析——HMR

1、前言

   上次的远程加载简单描述了app reload(开发时运行也是一个逻辑)和MetroServer交互的流程,现在开始讲讲HMR,对应到RN调试界面中的Enable HotLoading,HMR全名为Hot Module Replacement。它能让你在修改代码后马上在app上展现结果而且不会丢失状态。

2、HMR通讯方式

  先看其创建的位置,也是在runServer中

//metro/src/index.js.flow

exports.runServer = async (
......
) => {
....
  const {
    attachHmrServer,
    middleware,
    metroServer,
    end,
  } = await exports.createConnectMiddleware(config);
  serverApp.use(middleware);
....

  let httpServer;
.....
    httpServer = http.createServer(serverApp);
....

  if (hmrEnabled) {
    attachHmrServer(httpServer);
  }

来看看attachHmrServer函数:

index.js.flow

attachHmrServer(httpServer: HttpServer | HttpsServer) {
      attachWebsocketServer({
        httpServer,
        path: '/hot',
        websocketServer: new MetroHmrServer(
          metroServer.getBundler(),
          metroServer.getCreateModuleId(),
          config,
        ),
      });
    }

这里没有逻辑,放出attachWebsocketServer代码:
module.exports = function attachWebsocketServer({
  httpServer,
  websocketServer,
  path,
}: HMROptions) {
  const WebSocketServer = require('ws').Server;
  const wss = new WebSocketServer({
    server: httpServer,
    path,
  });

  wss.on('connection', async ws => {
    let connected = true;
    const url = ws.upgradeReq.url;

    const sendFn = (...args) => {
      if (connected) {
        ws.send(...args);
      }
    };

    const client = await websocketServer.onClientConnect(url, sendFn);
...
    ws.on('error', e => {
      websocketServer.onClientError && websocketServer.onClientError(client, e);
    });
    ws.on('close', () => {
      websocketServer.onClientDisconnect &&
        websocketServer.onClientDisconnect(client);
      connected = false;
    });
    ws.on('message', message => {
      websocketServer.onClientMessage &&
        websocketServer.onClientMessage(client, message);
    });
  });
};

attachWebsocketServer里面主要是创建WebSocketServer并监听连接,将逻辑全交给MetroHmrServer处理,不得不说这代码把连接和逻辑处理分得很清楚

在来看MetroHmrServer:

//HmrServer.js.flow
 
 async onClientConnect(
    clientUrl: string,
    sendFn: (data: string) => void,
  ): Promise {
    const urlObj = nullthrows(url.parse(clientUrl, true));
    const query = nullthrows(urlObj.query);

    let revPromise;
    if (query.bundleEntry != null) {
      urlObj.pathname = query.bundleEntry.replace(/\.js$/, '.bundle');
      delete query.bundleEntry;

      const {options} = parseOptionsFromUrl(
        url.format(urlObj),
        new Set(this._config.resolver.platforms),
      );

      const {entryFile, transformOptions} = splitBundleOptions(options);

      const resolutionFn = await transformHelpers.getResolveDependencyFn(
        this._bundler.getBundler(),
        transformOptions.platform,
      );
      const resolvedEntryFilePath = resolutionFn(
        `${this._config.projectRoot}/.`,
        entryFile,
      );//1<-- 这之前都是在转换来获取到打包入口文件的绝对路径
      const graphId = getGraphId(resolvedEntryFilePath, transformOptions);
      revPromise = this._bundler.getRevisionByGraphId(graphId);

      if (!revPromise) {//2<--由于是热加载,所以肯定需要是之前打包过的,否则报错
        send([sendFn], {
          type: 'error',
          body: formatBundlingError(new GraphNotFoundError(graphId)),
        });
        return null;
      }
    } else {
      const revisionId = query.revisionId;
      revPromise = this._bundler.getRevision(revisionId);

      if (!revPromise) {
        send([sendFn], {
          type: 'error',
          body: formatBundlingError(new RevisionNotFoundError(revisionId)),
        });
        return null;
      }
    }

    const {graph, id} = await revPromise;//3<--获取到前版本的ud

    const client = {//4<--注意,这里的client是后面交互的载体
      sendFn,
      revisionId: id,
    };

    let clientGroup = this._clientGroups.get(id);//5<--所有client都保存在_clientGroups
    if (clientGroup != null) {
      clientGroup.clients.add(client);
    } else {
      clientGroup = {
        clients: new Set([client]),
        unlisten: () => unlisten(),
        revisionId: id,
      };

      this._clientGroups.set(id, clientGroup);

      const unlisten = this._bundler
        .getDeltaBundler()
        .listen(//6<--这里监听了文件的变化,文件有变化后调用_handleFileChange函数
          graph,
          debounceAsyncQueue(
            this._handleFileChange.bind(this, clientGroup),
            50,//7<--节流
          ),
        );
    }

    await this._handleFileChange(clientGroup);

    return client;
  }

以上为有客户端连接上socket时的的处理,主要是创建client对象作为后面真正执行热更新的对象,文件监听变化仅仅在初始化clientGroup时设置了一次,后面的交互其实就围绕着文件来,即_handleFileChange

HmrServer.js.flow

  async _handleFileChange(group: ClientGroup) {
    const processingHmrChange = log(
      createActionStartEntry({action_name: 'Processing HMR change'}),
    );

    const sendFns = [...group.clients].map(client => client.sendFn);

    send(sendFns, {type: 'update-start'});
    const message = await this._prepareMessage(group);
    send(sendFns, message);
    send(sendFns, {type: 'update-done'});

  }

可以看到文件更新消息是群发给client的,_prepareMessage函数中包含了所有打包的逻辑

HMRServer.js.flow
 
async _prepareMessage(
    group: ClientGroup,
  ): Promise {
    try {
      const revPromise = this._bundler.getRevision(group.revisionId);//1<-- 获取整个group的版本信息

      if (!revPromise) {
        return {
          type: 'error',
          body: formatBundlingError(
            new RevisionNotFoundError(group.revisionId),
          ),
        };
      }

      const {revision, delta} = await this._bundler.updateGraph(//2<--根据现有版本计算出差量delta并返回新版本
        await revPromise,
        false,
      );

      this._clientGroups.delete(group.revisionId);
      group.revisionId = revision.id;//3<--更新group版本信息
      for (const client of group.clients) {
        client.revisionId = revision.id;//4<--更新client信息
      }
      this._clientGroups.set(group.revisionId, group);

      const hmrUpdate = hmrJSBundle(delta, revision.graph, {//5<--获取差量包
        createModuleId: this._createModuleId,
        projectRoot: this._config.projectRoot,
      });

      return {
        type: 'update',
        body: {
          revisionId: revision.id,
          ...hmrUpdate,
        },
      };
    } catch (error) {
      const formattedError = formatBundlingError(error);

      this._config.reporter.update({type: 'bundling_error', error});

      return {type: 'error', body: formattedError};
    }
  }

这里主要就是打包并返回更新内容,delta是差量信息,hmrUpdate对象的格式如下:

export type HmrUpdate = {|
  +revisionId: string,               版本号
  +modules: ModuleMap,       修改的、添加的module
  +deleted: $ReadOnlyArray,被删除的moduleID
  +sourceMappingURLs: $ReadOnlyArray,  源码map,顺序和modules一一对应
  +sourceURLs: $ReadOnlyArray,  源码地址,顺序和modules一一对应,即js文件路径
|};

顺着this._bundler.updateGraph的执行深度,我们找到了计算差量的模块DeltaCalculator.js.flow

  async _getChangedDependencies(
    modifiedFiles: Set,
    deletedFiles: Set,
  ): Promise> {
    if (!this._graph.dependencies.size) {//1<--这里是初始化打包的操作,忽略
      const {added} = await initialTraverseDependencies(
        this._graph,
        this._options,
      );

      return {
        added,
        modified: new Map(),
        deleted: new Set(),
        reset: true,
      };
    }

    // If a file has been deleted, we want to invalidate any other file that
    // depends on it, so we can process it and correctly return an error.
    deletedFiles.forEach(filePath => {//2<--遍历删除的文件
      const module = this._graph.dependencies.get(filePath);

      if (module) {//3<--获取模块的反向依赖,这步算是RN的兜底操作
        module.inverseDependencies.forEach(path => {
          // Only mark the inverse dependency as modified if it's not already
          // marked as deleted (in that case we can just ignore it).
          if (!deletedFiles.has(path)) {
            modifiedFiles.add(path);
          }
        });
      }
    });

    // We only want to process files that are in the bundle.
    const modifiedDependencies = Array.from(modifiedFiles).filter(filePath =>
      this._graph.dependencies.has(filePath),//4<--这里过滤掉不在依赖图中文件<--
    );

    // No changes happened. Return empty delta.
    if (modifiedDependencies.length === 0) {
      return {
        added: new Map(),
        modified: new Map(),
        deleted: new Set(),
        reset: false,
      };
    }
    //5<--在这才正在计算打包差量
    const {added, modified, deleted} = await traverseDependencies(
      modifiedDependencies,
      this._graph,
      this._options,
    );

    return {
      added,
      modified,
      deleted,
      reset: false,
    };
  }

3、HMRClient

打包后的代码如何执行呢?在Server发送消息 send(sendFns, message);给HMRClient,由HMRClient处理:

//HMRClient.js.flow

class HMRClient extends WebSocketHMRClient {
  constructor(url: string) {
    super(url);

    this.on('update', update => {
      injectUpdate(update);
    });
  }
}

这个client实现很简单,WebSocketHMRClient只是做连接和转发工作,没有业务逻辑,所以不做展开,而HMRClient的逻辑只有一个——injectUpdate

function injectModules(
  modules: ModuleMap,
  sourceMappingURLs: $ReadOnlyArray,
  sourceURLs: $ReadOnlyArray,
) {
  modules.forEach(([id, code], i) => {
    // In JSC we need to inject from native for sourcemaps to work
    // (Safari doesn't support `sourceMappingURL` nor any variant when
    // evaluating code) but on Chrome we can simply use eval.
    const injectFunction =
      typeof global.nativeInjectHMRUpdate === 'function'
        ? global.nativeInjectHMRUpdate
        : eval; // eslint-disable-line no-eval

    // Fool regular expressions trying to remove sourceMappingURL comments from
    // source files, which would incorrectly detect and remove the inlined
    // version.
    const pragma = 'sourceMappingURL';
    injectFunction(//3<--这里使用的一般是eval
      code + `\n//# ${pragma}=${sourceMappingURLs[i]}`,
      sourceURLs[i],
    );
  });
}

function injectUpdate(update: HmrUpdate) {
  injectModules(//1<--先注入添加的module
    update.added,
    update.addedSourceMappingURLs,
    update.addedSourceURLs,
  );
  injectModules(//2<--再注入修改后的module
    update.modified,
    update.modifiedSourceMappingURLs,
    update.modifiedSourceURLs,
  );
}

4、HMRClient起点

   上面描述了HMRServer如何启动、如何建立连接、差量包打包、差量包运行,但却留下了几个疑问:1、是谁与HMRServer建立连接 2、差量包运行在哪里

与HMRServer建立连接的是 App 本身,起点是DevSupportManagerImpl

if (mDevSettings.isHotModuleReplacementEnabled() && mCurrentContext != null) {
      try {
        URL sourceUrl = new URL(getSourceUrl());
        String path = sourceUrl.getPath().substring(1); // strip initial slash in path
        String host = sourceUrl.getHost();
        int port = sourceUrl.getPort();
        mCurrentContext.getJSModule(HMRClient.class).enable("android", path, host, port);
      } catch (MalformedURLException e) {
        showNewJavaError(e.getMessage(), e);
      }
    }

这里调用了js模块  HMRClient的enable函数。

/**
 * HMR Client that receives from the server HMR updates and propagates them
 * runtime to reflects those changes.
 */
const HMRClient = {
  enable(platform: string, bundleEntry: string, host: string, port: number) {
    ... ...
    // Moving to top gives errors due to NativeModules not being initialized
    const HMRLoadingView = require('HMRLoadingView');

    const wsHostPort = port !== null && port !== '' ? `${host}:${port}` : host;

    bundleEntry = bundleEntry.replace(/\.(bundle|delta)/, '.js');

    // Build the websocket url
    const wsUrl =
      `ws://${wsHostPort}/hot?` +
      `platform=${platform}&` +
      `bundleEntry=${bundleEntry}`;

    const hmrClient = new MetroHMRClient(wsUrl);

    hmrClient.on('connection-error', e => {
      let error = `Hot loading isn't working because it cannot connect to the development server.
... ...
    });

    hmrClient.on('update-start', () => {
      HMRLoadingView.showMessage('Hot Loading...');
    });

    hmrClient.on('update', () => {
      if (Platform.OS === 'ios') {
        const RCTRedBox = require('NativeModules').RedBox;
        RCTRedBox && RCTRedBox.dismiss && RCTRedBox.dismiss();
      } else {
        const RCTExceptionsManager = require('NativeModules').ExceptionsManager;
        RCTExceptionsManager &&
          RCTExceptionsManager.dismissRedbox &&
          RCTExceptionsManager.dismissRedbox();
      }
    });

    hmrClient.on('update-done', () => {
      HMRLoadingView.hide();
    });

    hmrClient.on('error', data => {
      HMRLoadingView.hide();
    ... ... 
    });

    hmrClient.on('close', data => {
      HMRLoadingView.hide();
    ... ...
    });

    hmrClient.enable();
  },
};

这个函数中new 了MetroHMRClient并与HMRServer建立了连接,并根据收到的消息展示UI,MetroHMRClient就是Metro包中的HMRClient模块

由于MetroHMRClient也是运行在js模块HMRClient中,因此差量包是运行在RN代码运行的js环境中的。而这个环境在我们没开debug的时候是本地的jscore,开了debug就是一个代理的jscore,这个代理的jscore下次再解释。

5、总结

   App<-->HMRClient jsModule<-->MetroHMRClient<-->HMRServer

1、App通过js模块HMRClient创建了MetroHMRClient对象并建立连接,MetroHMRClient的“update”监听是执行差量包的函数,而MetroHMRClient又是在js模块中创建,因此差量包运行在RN代码的运行环境

2、HMRServer接收到连接后把连接后的句柄保存为client,并添加到clientGroup数组中,初始化时注册了文件变更监听

3、当有文件更改时,促发文件变更监听,HMRServer群发MetroHMRClient,消息体为变更后的所有js代码差量

4、MetroHMRClient接收到差量代码并执行

你可能感兴趣的:(react,naitve)