技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# ReactNative依赖解析流程

导语: 回到上一章节react-native bundle 到 bundle 生成到底发生了什么(metro 打包流程简析) (opens new window),我们知道 RN 构建过程大致分为 解析,转换生成三个步骤,其中有一个比较关键的点在于,我们需要利用每个 module 的依赖关系,进行性能优化,剔除不必要的 module

那么react native是如何获取到每个文件的的依赖 module 呢? 下面我们具体分析一波。

# 一、前言

还记得之前我们讲过 metro 是利用jest-haste-map去做文件解析,每一个 module 经过 jest-haste-map 之后的数据结构如下:

 [
    "App.js", // 文件路径,相对于根目录
    [
      "",  // id
      1606548079450, // mtime,时间戳
      2794, // 文件大小size
      1, // visited 0 | 1,
      "", // dependencies, 其实返回的并没有文件对应的依赖。
      "552e453f03517dc010656369ea82f6a9b6b0383b" // sha1
    ]
  ],

然而真实转换后可以得到文件的依赖关系如下:

// 以index.js文件为例
Map {
  '/Users/alexganggao/Desktop/rn-jest/MyRN/index.js' => { inverseDependencies: Set {},
    path: '/Users/alexganggao/Desktop/rn-jest/MyRN/index.js',
    dependencies:
     Map {
       '@babel/runtime/helpers/interopRequireDefault' => [Object],
       'react-native' => [Object],
       './App' => [Object],
       './app.json' => [Object] },
    getSource: [Function: getSource],
    output: [ [Object] ] },
}

那么再真实的情况下,metro 是如何知道每个 module 有哪些依赖文件呢?

答案其实很简单: 利用 AST 分析得到每个 module 使用了哪些 importrequire

# 二、metro 依赖时序图

为了验证此设想,笔者分析源码得知,整个 rn 依赖解析时序图下图所示:

对源码有兴趣的笔者,可以查阅下面一部分源码阅读,您也可以直接跳转到这里查看具体依赖分析部分。

# 1. 源码阅读

在进行图谱生成完成之后,利用 /node_modules/metro/src/DeltaBundler/DeltaCalculator.js 初始化了 dependencies 并使用getDelta进行依赖解析:

this._graph = {
  dependencies: new Map(),
  entryPoints,
  importBundleNames: new Set(),
};

getDelta -> _getChangedDependencies -> initialTraverseDependencies -> _traverseDependenciesForSingleFile -> processModule ->

 const internalOptions = getInternalOptions(options);
    yield Promise.all(
      graph.entryPoints.map(path =>
        {
        return traverseDependenciesForSingleFile(path, graph, delta, internalOptions)
      }
      )
    );
// 使用options.transform进行转换 options从哪里?其实就是上面的internalOptions,而internalOptions又使用的options
const result = yield options.transform(path); // Get the absolute path of all sub-dependencies (some of them could have been


现在我们将目光聚焦在 options 从哪里得来,利用反向查找 options:

initialTraverseDependencies -> DeltaBundler.buildGraph(entryPoints, options)

// node_modules/metro/src/DeltaBundler.js
buildGraph(entryPoints, options) {
    var _this = this;

    return _asyncToGenerator(function*() {
      const depGraph = yield _this._bundler.getDependencyGraph();
      const deltaCalculator = new DeltaCalculator(
        entryPoints,
        depGraph,
        options
      );
      yield deltaCalculator.getDelta({
        reset: true,
        shallow: options.shallow
      });
      const graph = deltaCalculator.getGraph();
      _this._deltaCalculators.set(graph, deltaCalculator);

      return graph;
    })();
  }

//node_modules/metro/src/IncrementalBundler.js
// buildGraphForEntries
const graph = yield _this._deltaBundler.buildGraph(absoluteEntryFiles, {
        resolve: yield transformHelpers.getResolveDependencyFn(
          _this._bundler,
          transformOptions.platform
        ),
        transform: yield transformHelpers.getTransformFn(
          absoluteEntryFiles,
          _this._bundler,
          _this._deltaBundler,
          _this._config,
          transformOptions
        ),
        onProgress: otherOptions.onProgress,
        experimentalImportBundleSupport:
          _this._config.transformer.experimentalImportBundleSupport,
        shallow: otherOptions.shallow
      });

// node_modules/metro/src/lib/transformHelpers.js 转换来源于Bundler
// _getTransformFn
return async (path: string) => {
    return await bundler.transformFile(path, {
      ...transformOptions,
      type: getType(transformOptions.type, path, config.resolver.assetExts),
      inlineRequires: removeInlineRequiresBlacklistFromOptions(
        path,
        inlineRequires,
      ),
    });
  };

// node_modules/metro/src/Bundler.js
class Bundler {
  _depGraphPromise: Promise<DependencyGraph>;
  _transformer: Transformer;

  constructor(config: ConfigT, options?: BundlerOptions) {
    this._depGraphPromise = DependencyGraph.load(config, options);

    this._depGraphPromise
      .then((dependencyGraph: DependencyGraph) => {
        this._transformer = new Transformer(
          config,
          dependencyGraph.getSha1.bind(dependencyGraph),
        );
      })
      .catch(error => {
        console.error('Failed to construct transformer: ', error);
      });
  }
  async transformFile(
    filePath: string,
    transformOptions: TransformOptions,
  ): Promise<TransformResultWithSource<>> {
    // We need to be sure that the DependencyGraph has been initialized.
    // TODO: Remove this ugly hack!
    await this._depGraphPromise;

    return this._transformer.transformFile(filePath, transformOptions);
  }
}
// 转换 node_modules/metro/src/DeltaBundler/Transformer.js

  async transformFile(
    filePath: string,
    transformerOptions: TransformOptions,
  ): Promise<TransformResultWithSource<>> {
    const cache = this._cache;

    const {
      customTransformOptions,
      dev,
      experimentalImportSupport,
      hot,
      inlinePlatform,
      inlineRequires,
      minify,
      unstable_disableES6Transforms,
      platform,
      type,
      ...extra
    } = transformerOptions;

    for (const key in extra) {
      if (hasOwnProperty.call(extra, key)) {
        throw new Error(
          'Extra keys detected: ' + Object.keys(extra).join(', '),
        );
      }
    }

    const localPath = path.relative(this._config.projectRoot, filePath);

    const partialKey = stableHash([
      // This is the hash related to the global Bundler config.
      this._baseHash,

      // Path.
      localPath,

      customTransformOptions,
      dev,
      experimentalImportSupport,
      hot,
      inlinePlatform,
      inlineRequires,
      minify,
      unstable_disableES6Transforms,
      platform,
      type,
    ]);

    const sha1 = this._getSha1(filePath);
    let fullKey = Buffer.concat([partialKey, Buffer.from(sha1, 'hex')]);
    const result = await cache.get(fullKey);

    // A valid result from the cache is used directly; otherwise we call into
    // the transformer to computed the corresponding result.
    const data = result
      ? {result, sha1}
      : await this._workerFarm.transform(localPath, transformerOptions);

    // Only re-compute the full key if the SHA-1 changed. This is because
    // references are used by the cache implementation in a weak map to keep
    // track of the cache that returned the result.
    if (sha1 !== data.sha1) {
      fullKey = Buffer.concat([partialKey, Buffer.from(data.sha1, 'hex')]);
    }

    cache.set(fullKey, data.result);

    return {
      ...data.result,
      getSource(): Buffer {
        return fs.readFileSync(filePath);
      },
    };
  }

打印 data.result 得知对于每个文件生成如下数据结构:

{
dependencies:
   [ { name: '@babel/runtime/helpers/interopRequireDefault',
       data: [Object] },
     { name: 'react-native', data: [Object] },
     { name: './App', data: [Object] },
     { name: './app.json', data: [Object] } ],
output: [ { data: [Object], type: 'js/module' } ]
}

接着看,文件转换:

// node_modules/metro/src/DeltaBundler/WorkerFarm.js

  async transform(
    filename: string,
    options: TransformOptions,
  ): Promise<TransformerResult> {
    try {
      const data = await this._worker.transform(
        filename,
        options,
        this._config.projectRoot,
        this._transformerConfig,
      );

      return {
        result: data.result,
        sha1: data.sha1,
      };
    } catch (err) {
      if (err.loc) {
        throw this._formatBabelError(err, filename);
      } else {
        throw this._formatGenericError(err, filename);
      }
    }
  }
// worker 其实是一个JestWorker,利用JestWorker执行自定义方法
const worker = this._makeFarm(
        this._config.transformer.workerPath, // workerPath: 'metro/src/DeltaBundler/Worker',
        ["transform"],
        this._config.maxWorkers
      );

转换相关配置信息如下:

{
transformer:
   { assetPlugins: [],
     asyncRequireModulePath: 'metro/src/lib/bundle-modules/asyncRequire',
     assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
     babelTransformerPath:
      '/Users/alexganggao/Desktop/rn-jest/MyRN/node_modules/@react-native-community/cli/node_modules/metro-react-native-babel-transformer/src/index.js',
     dynamicDepsInPackages: 'throwAtRuntime',
     enableBabelRCLookup: true,
     enableBabelRuntime: true,
     experimentalImportBundleSupport: false,
     getTransformOptions: [AsyncFunction: getTransformOptions],
     minifierConfig:
      { mangle: [Object],
        output: [Object],
        sourceMap: [Object],
        toplevel: false,
        compress: [Object] },
     minifierPath: 'metro-minify-uglify',
     optimizationSizeLimit: 153600,
     postMinifyProcess: [Function: postMinifyProcess],
     transformVariants: { default: {} },
     workerPath: 'metro/src/DeltaBundler/Worker',
     publicPath: '/assets' },
  cacheStores:
   [ FileStore {
       _root:
        '/var/folders/q_/vy78r8wn5n1cdftb86yqtz280000gn/T/metro-cache' } ],
  cacheVersion: '1.0',
  projectRoot: '/Users/alexganggao/Desktop/rn-jest/MyRN',
  stickyWorkers: true,
  watchFolders: [ '/Users/alexganggao/Desktop/rn-jest/MyRN' ],
  transformerPath:
   '/Users/alexganggao/Desktop/rn-jest/MyRN/node_modules/metro/src/JSTransformer/worker.js',
  maxWorkers: 8,
  resetCache: false,
  reporter:
   TerminalReporter {
     _activeBundles: Map {},
     _scheduleUpdateBundleProgress:
      { [Function: debounced] cancel: [Function: cancel], flush: [Function: flush] },
     terminal:
      Terminal {
        _logLines: [],
        _nextStatusStr: '',
        _scheduleUpdate: [Function],
        _statusStr: '',
        _stream: [WriteStream] } } } '00000000'
calcTransformerOptions:  { customTransformOptions: [Object: null prototype] {},
  dev: false,
  hot: false,
  inlineRequires: false,
  inlinePlatform: true,
  minify: true,
  platform: 'ios',
  experimentalImportSupport: false,
  unstable_disableES6Transforms: false,
  type: 'module'
}

继续查看 Worker.js

// node_modules/metro/src/DeltaBundler/Worker.js
async function transform(
  filename: string,
  transformOptions: JsTransformOptions,
  projectRoot: string,
  transformerConfig: TransformerConfig
): Promise<Data> {
  const transformer = getTransformer(projectRoot, transformerConfig);

  const transformFileStartLogEntry = {
    action_name: "Transforming file",
    action_phase: "start",
    file_name: filename,
    log_entry_label: "Transforming file",
    start_timestamp: process.hrtime(),
  };

  const data = fs.readFileSync(path.resolve(projectRoot, filename));
  const sha1 = crypto
    .createHash("sha1")
    .update(data)
    .digest("hex");

  const result = await transformer.transform(filename, data, transformOptions);

  return {
    result,
    sha1,
    transformFileStartLogEntry,
    transformFileEndLogEntry,
  };
}

// 查看 getTransformer得知转换使用的是transformerPath

const Transformer = require(transformerPath);

transformers[transformerKey] = new Transformer(projectRoot, transformerConfig);
return transformers[transformerKey];

而我们在上面的转换配置信息中可以得知:transformerPath为'node_modules/metro/src/JSTransformer/worker.js'

至此我们得知 react-native 使用的是JSTransformer进行代码转换

// node_modules/metro/src/JSTransformer/worker.js
 async transform(
    filename: string,
    data: Buffer,
    options: JsTransformOptions,
  ): Promise<Result> {
    const sourceCode = data.toString('utf8');
    let type = 'js/module';

    if (options.type === 'asset') {
      type = 'js/module/asset';
    }
    if (options.type === 'script') {
      type = 'js/script';
    }
    // json返回依赖为空
    if (filename.endsWith('.json')) {
      let code = JsFileWrapping.wrapJson(sourceCode);
      let map = [];

      if (options.minify) {
        ({map, code} = await this._minifyCode(filename, code, sourceCode, map));
      }

      return {
        dependencies: [],
        output: [
          {
            data: {code, lineCount: countLines(code), map, functionMap: null},
            type,
          },
        ],
      };
    }

    // $FlowFixMe TODO t26372934 Plugin system
    //  '/node_modules/@react-native-community/cli/node_modules/metro-react-native-babel-transformer/src/index.js'
    const transformer: Transformer<*> = require(this._config
      .babelTransformerPath);

    const transformerArgs = {
      filename,
      options: {
        ...options,
        enableBabelRCLookup: this._config.enableBabelRCLookup,
        enableBabelRuntime: this._config.enableBabelRuntime,
        // Inline requires are now performed at a secondary step. We cannot
        // unfortunately remove it from the internal transformer, since this one
        // is used by other tooling, and this would affect it.
        inlineRequires: false,
        projectRoot: this._projectRoot,
        publicPath: this._config.publicPath,
      },
      plugins: [],
      src: sourceCode,
    };

    const transformResult =
      type === 'js/module/asset'
        ? {
            ...(await assetTransformer.transform(
              transformerArgs,
              this._config.assetRegistryPath,
              this._config.assetPlugins,
            )),
            functionMap: null,
          }
        : await transformer.transform(transformerArgs);

    // Transformers can ouptut null ASTs (if they ignore the file). In that case
    // we need to parse the module source code to get their AST.
    let ast =
      transformResult.ast ||
      babylon.parse(sourceCode, {sourceType: 'unambiguous'});

    const {importDefault, importAll} = generateImportNames(ast);

    // Add "use strict" if the file was parsed as a module, and the directive did
    // not exist yet.
    const {directives} = ast.program;

    if (
      ast.program.sourceType === 'module' &&
      directives.findIndex(d => d.value.value === 'use strict') === -1
    ) {
      directives.push(types.directive(types.directiveLiteral('use strict')));
    }

    // Perform the import-export transform (in case it's still needed), then
    // fold requires and perform constant folding (if in dev).
    const plugins = [];
    const opts = {
      ...options,
      inlineableCalls: [importDefault, importAll],
      importDefault,
      importAll,
    };

    if (options.experimentalImportSupport) {
      plugins.push([importExportPlugin, opts]);
    }

    if (options.inlineRequires) {
      plugins.push([inlineRequiresPlugin, opts]);
    }

    if (!options.dev) {
      plugins.push([constantFoldingPlugin, opts]);
    }

    plugins.push([inlinePlugin, opts]);

    ({ast} = transformFromAstSync(ast, '', {
      ast: true,
      babelrc: false,
      code: false,
      configFile: false,
      comments: false,
      compact: false,
      filename,
      plugins,
      sourceMaps: false,
    }));

    let dependencyMapName = '';
    let dependencies;
    let wrappedAst;

    // If the module to transform is a script (meaning that is not part of the
    // dependency graph and it code will just be prepended to the bundle modules),
    // we need to wrap it differently than a commonJS module (also, scripts do
    // not have dependencies).
    if (type === 'js/script') {
      dependencies = [];
      wrappedAst = JsFileWrapping.wrapPolyfill(ast);
    } else {
      try {
        const opts = {
          asyncRequireModulePath: this._config.asyncRequireModulePath,
          dynamicRequires: getDynamicDepsBehavior(
            this._config.dynamicDepsInPackages,
            filename,
          ),
          inlineableCalls: [importDefault, importAll],
          keepRequireNames: options.dev,
        };
        ({ast, dependencies, dependencyMapName} = collectDependencies(
          ast,
          opts,
        )); // 依赖收集
      } catch (error) {
        if (error instanceof collectDependencies.InvalidRequireCallError) {
          throw new InvalidRequireCallError(error, filename);
        }
        throw error;
      }
      // 处理,globalThis,_d,polyfill,替换require部分
      ({ast: wrappedAst} = JsFileWrapping.wrapModule(
        ast,
        importDefault,
        importAll,
        dependencyMapName,
      ));
    }

    const reserved =
      options.minify && data.length <= this._config.optimizationSizeLimit
        ? normalizePseudoglobals(wrappedAst)
        : [];
    // 代码生成
    const result = generate(
      wrappedAst,
      {
        comments: false,
        compact: false,
        filename,
        retainLines: false,
        sourceFileName: filename,
        sourceMaps: true,
      },
      sourceCode,
    );

    let map = result.rawMappings ? result.rawMappings.map(toSegmentTuple) : [];
    let code = result.code;

    if (options.minify) {
      // 压缩代码
      ({map, code} = await this._minifyCode(
        filename,
        result.code,
        sourceCode,
        map,
        reserved,
      ));
    }

    const {functionMap} = transformResult;

    return {
      dependencies,
      output: [
        {data: {code, lineCount: countLines(code), map, functionMap}, type},
      ],
    };
  }

查阅如上代码,我们得知:

  • js 转换使用的是metro-react-native-babel-transformer对 js 代码进行转换
  • 针对 dependencies,主要使用了collectDependencies方法:

# 2. collectDependencies 依赖收集

下面我们具体来看一下是如何进行依赖收集的:

// node_modules/metro/src/ModuleGraph/worker/collectDependencies.js
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");

import type { Ast } from "@babel/core";
// ...
export type DynamicRequiresBehavior = "throwAtRuntime" | "reject";

/**
 * Produces a Babel template that will throw at runtime when the require call
 * is reached. This makes dynamic require errors catchable by libraries that
 * want to use them.
 */
const dynamicRequireErrorTemplate = template(`
  (function(line) {
    throw new Error(
      'Dynamic require defined at line ' + line + '; not supported by Metro',
    );
  })(LINE)
`);

/**
 * Produces a Babel template that transforms an "import(...)" call into a
 * "require(...)" call to the asyncRequire specified.
 */
const makeAsyncRequireTemplate = template(`
  require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, MODULE_NAME)
`);

const makeAsyncPrefetchTemplate = template(`
  require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, MODULE_NAME)
`);

const makeJSResourceTemplate = template(`
  require(ASYNC_REQUIRE_MODULE_PATH).resource(MODULE_ID, MODULE_NAME)
`);

function collectDependencies(
  ast: Ast,
  options: Options
): CollectedDependencies {
  const visited = new WeakSet();

  const state: State = {
    asyncRequireModulePathStringLiteral: null,
    dependency: 0,
    dependencyCalls: new Set(),
    dependencyData: new Map(),
    dependencyIndexes: new Map(),
    dependencyMapIdentifier: null,
    dynamicRequires: options.dynamicRequires,
    keepRequireNames: options.keepRequireNames,
    disableRequiresTransform: !!options.disableRequiresTransform,
  };

  const visitor = {
    // 处理所有AST中的表达式,如果call是import类型的,其arguments一定是Expression
    CallExpression(path: Path, state: State) {
      if (visited.has(path.node)) {
        return;
      }

      const callee = path.get("callee");
      const name = callee.node.name;
      // 如果是import语句,执行依赖手机
      if (callee.isImport()) {
        processImportCall(path, state, {
          prefetchOnly: false,
        });
        return;
      }
      // 如果是import语句,执行依赖收集
      if (name === "__prefetchImport" && !path.scope.getBinding(name)) {
        processImportCall(path, state, {
          prefetchOnly: true,
        });
        return;
      }
      // 如果为js资源或者分包资源同样处理import,但是生成的requore将会不同, tips: 似乎__jsResource并没有被使用过
      if (
        (name === "__jsResource" ||
          name === "__conditionallySplitJSResource") &&
        !path.scope.getBinding(name)
      ) {
        processImportCall(path, state, {
          prefetchOnly: false,
          jsResource: true,
        });
        return;
      }

      if (state.dependencyCalls.has(name) && !path.scope.getBinding(name)) {
        visited.add(processRequireCall(path, state).node);
      }
    },

    ImportDeclaration(path: Path, state: State) {
      // 针对import语句,记录import的module到一个map中,如果module被记录过那么不会重复记录
      const dep = getDependency(state, path.node.source.value, {
        prefetchOnly: false,
      });

      dep.data.isAsync = false;
    },

    Program(path: Path, state: State) {
      state.asyncRequireModulePathStringLiteral = types.stringLiteral(
        options.asyncRequireModulePath
      );

      state.dependencyMapIdentifier = path.scope.generateUidIdentifier(
        "dependencyMap"
      );

      state.dependencyCalls = new Set(["require", ...options.inlineableCalls]);
    },
  };

  traverse(ast, visitor, null, state);

  // Compute the list of dependencies.
  const dependencies = new Array(state.dependency);

  for (const [name, data] of state.dependencyData) {
    dependencies[nullthrows(state.dependencyIndexes.get(name))] = {
      name,
      data,
    };
  }

  return {
    ast,
    dependencies,
    dependencyMapName: nullthrows(state.dependencyMapIdentifier).name,
  };
}
// 处理import语句及js资源
function processImportCall(
  path: Path,
  state: State,
  options: DepOptions
): Path {
  // 获取module 名称
  const name = getModuleNameFromCallArgs(path);

  if (name == null) {
    throw new InvalidRequireCallError(path);
  }
  // 记录module依赖,如果当前module已经存在map中则返回,否则先记录
  const dep = getDependency(state, name, options);
  if (!options.prefetchOnly) {
    delete dep.data.isPrefetchOnly;
  }

  if (state.disableRequiresTransform) {
    return path;
  }

  const ASYNC_REQUIRE_MODULE_PATH = state.asyncRequireModulePathStringLiteral;
  // 生成零时的moduleId,用于在将代码中的import/require进行替换,
  const MODULE_ID = types.memberExpression(
    state.dependencyMapIdentifier,
    types.numericLiteral(dep.index),
    true
  );
  // module名称
  const MODULE_NAME = types.stringLiteral(name);
  // 如果是js资源,那么就替换对应的加载路径为指定的module
  if (options.jsResource) {
    path.replaceWith(
      makeJSResourceTemplate({
        ASYNC_REQUIRE_MODULE_PATH,
        MODULE_ID,
        MODULE_NAME,
      })
    );
  } else if (!options.prefetchOnly) {
    // 如果为不是预加载则直接将import()转换成require()()语句进行加载
    path.replaceWith(
      makeAsyncRequireTemplate({
        ASYNC_REQUIRE_MODULE_PATH,
        MODULE_ID,
        MODULE_NAME,
      })
    );
  } else {
    // 否则将import转换成require.prefetch的方式加载
    path.replaceWith(
      makeAsyncPrefetchTemplate({
        ASYNC_REQUIRE_MODULE_PATH,
        MODULE_ID,
        MODULE_NAME,
      })
    );
  }

  return path;
}
// 处理require语句
function processRequireCall(path: Path, state: State): Path {
  // 获取module 名称
  const name = getModuleNameFromCallArgs(path);

  if (name == null) {
    if (state.dynamicRequires === "reject") {
      throw new InvalidRequireCallError(path);
    }

    path.replaceWith(
      dynamicRequireErrorTemplate({
        LINE: "" + path.node.loc.start.line,
      })
    );
    return path;
  }
  // 记录module依赖,如果当前module已经存在map中则返回,否则先记录
  const dep = getDependency(state, name, { prefetchOnly: false });
  dep.data.isAsync = false;
  delete dep.data.isPrefetchOnly;
  // 如果不使用require 转换逻辑则直接返回
  if (state.disableRequiresTransform) {
    return path;
  }
  // 生成零时的moduleId,用于在将代码中的import/require进行替换,
  const moduleIDExpression = types.memberExpression(
    state.dependencyMapIdentifier,
    types.numericLiteral(dep.index),
    true
  );
  // 替换require内容为对应的id
  path.node.arguments = state.keepRequireNames
    ? [moduleIDExpression, types.stringLiteral(name)]
    : [moduleIDExpression];

  return path;
}
// 记录依赖,当前module路径是否被记录过,如果没有则记录否则直接返回对呀信息
function getDependency(
  state: State,
  name: string,
  options: DepOptions
): InternalDependencyInfo {
  let index = state.dependencyIndexes.get(name);
  let data: ?InternalDependencyData = state.dependencyData.get(name);

  if (!data) {
    index = state.dependency++;
    data = { isAsync: true };

    if (options.prefetchOnly) {
      data.isPrefetchOnly = true;
    }

    state.dependencyIndexes.set(name, index);
    state.dependencyData.set(name, data);
  }

  return { index: nullthrows(index), data: nullthrows(data) };
}
// 获取require或者import部分具体的内容
function getModuleNameFromCallArgs(path: Path): ?string {
  const expectedCount =
    path.node.callee.name === "__conditionallySplitJSResource" ? 2 : 1;
  if (path.get("arguments").length !== expectedCount) {
    throw new InvalidRequireCallError(path);
  }

  const result = path.get("arguments.0").evaluate();

  if (result.confident && typeof result.value === "string") {
    return result.value;
  }

  return null;
}
collectDependencies.getModuleNameFromCallArgs = getModuleNameFromCallArgs;

class InvalidRequireCallError extends Error {
  constructor({ node }: any) {
    const line = node.loc && node.loc.start && node.loc.start.line;

    super(
      `Invalid call at line ${line || "<unknown>"}: ${generate(node).code}`
    );
  }
}

collectDependencies.InvalidRequireCallError = InvalidRequireCallError;

module.exports = collectDependencies;

# 总结

  1. collectDependencies 主要利用 babel 去解析所有的 import 和 require 得到每个文件的依赖 module。

  2. 处理 import 语句时,会将所有的 import 语句转换为 require 进行加载

// 转换前
import { Image } from "react-native";
// 转换后
var _reactNative = r(d[2]);

其他情况:(tips: 暂未找到具体的实现方式,有发现的同学可以一起交流)

  • 对于对于预加载的情况,会转换成 require().prefetch 的方式进行加载;
  • 如果为资源文件则会转换成 require().resource 的方式加载.
  1. 在处理 require 语句时, 会将 require 的内容替换成对应的 moduleId

举个例子: 类似于require('Foo') 将会被转换成 require(_depMap[3], 'Foo'),这个_depMap 是所有的依赖集合,其实在这个时候,我们并不需要准确的知道真实的 moduleId,只需要知道文件的依赖即可。

// 转换前
<Image source={require('./demo.png')}></Image>
// 转换后
_react.default.createElement(_reactNative.Image,{source: r(d[4])  }
  1. 经过 babel 解析之后的 module 依赖如下:
// 经过 collectDependencies得到的依赖
dependencies: [
  { name: "@babel/runtime/helpers/interopRequireDefault", data: [Object] },
  { name: "react-native", data: [Object] },
  { name: "./App", data: [Object] },
  { name: "./app.json", data: [Object] },
];
  1. js 转换使用的是metro-react-native-babel-transformer对 js 代码进行转换

其他:

  1. 至于如何生成 polyfills 部分,以及\_dglobalThis 等,有兴趣的同学可以看一下node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
【未经作者允许禁止转载】 Last Updated: 7/22/2024, 7:13:58 AM