Bootstrap

React Native Autolinking 源码深入分析

关于link

link 就是把 node_modules 中某个库的原生部分,加入到自己的原生项目中,例如:android的autolink是在yourapp/build.gradle、setting.gradle 添加第三方库、生成PackageList

关于 react-native link 、 Manual Linking、 autolink 的更多信息,可以参考这里
What is react-native link?,他们的目的都是link。本篇文章主要分析autolink 的源码,autolinking 是通过React-Native脚手架 来配合实现的,所以源码分析中,第2点,都是关于脚手架源码分析

autolink

autolink 的使用需要在两个文件中引入native_modules.gradle,如下:

setting.gradle

apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); 
//调用脚本中的方法
applyNativeModulesSettingsGradle(settings)

yourapp/build.gradle

apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); 
//调用脚本中的方法
applyNativeModulesAppBuildGradle(project)

关于setting.gradle、build.gradle 执行时机的具体分析,可阅读我之前写的Gradle 源码分析

这里我们直接从native_modules.gradle 开始分析

源码分析

1、 native_modules.gradle

首先看一下 脚本中 applyNativeModulesSettingsGradle、applyNativeModulesAppBuildGradle 这两个函数


def projectRoot = rootProject.projectDir

//ReactNativeModules 是native_modules.gradle 中的类,后面会分析
def autoModules = new ReactNativeModules(logger, projectRoot)


/** -----------------------
 *    Exported Extensions
 * ------------------------ */

ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
  ...
  //作用是,和 手动在 setting.gradle 中include 第三方库 的效果一样。而是向 Gradle解析setting.gradle的内容时创建 的对象加入 第三方库的信息
  autoModules.addReactNativeModuleProjects(defaultSettings)
}

ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
  ...
  //原理同setting.gradle的一样
  autoModules.addReactNativeModuleDependencies(project)

  def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
  def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))

  task generatePackageList {
    doLast {
      autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
    }
  }
  //把generatePackageList 任务 放在build之前执行
  preBuild.dependsOn generatePackageList

  android {
    sourceSets {
      main {
        java {
          srcDirs += generatedSrcDir
        }
      }
    }
  }
}

这里先给出整篇的总流程,下面在逐一进入详细分析

1、native_modules.gradle脚本中会创建ReactNativeModules对象,在构造函数中,调用getReactNativeConfig来获取所有 node_modules中的原生库信息(包括packageName、构造函数等),第2、3点的函数都是调用该对象成员函数
2、调用addReactNativeModuleProjects,向setting.gradle 中引入库
3、调用addReactNativeModuleDependencies,向build.gradle中引入库
4、调用generatePackagesFile,生成PackageList 类,用于在MainApplication 初始化ReactNativeHost时 getPackages 中使用。(关于ReactNativeHost 在RN的作用,可查看我之前写的文章React Native 源码分析(一)——启动流程

下面就开始分析,在创建ReactNativeModulesget对象时,调用的ReactNativeConfig,如何拿到、以及拿到哪些 原生工程的信息

2、getReactNativeConfig

2.1、getReactNativeConfig

ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules != null) return this.reactNativeModules

    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()
    
    //通过 require 引入cli库
    def cliResolveScript = "console.log(require('react-native/cli').bin);"
    String[] nodeCommand = ["node", "-e", cliResolveScript]
    //找到cli库 bin.js 的路径
    def cliPath = this.getCommandOutput(nodeCommand, this.root)
    //调用bin.js 参数是config
    String[] reactNativeConfigCommand = ["node", cliPath, "config"]
    def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
    //下面对 node bin.js conofig 输出的结果进行处理
    def json
    try {
      json = new JsonSlurper().parseText(reactNativeConfigOutput)
    } catch (Exception exception) {
      throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
    }
    def dependencies = json["dependencies"]
    def project = json["project"]["android"]

    if (project == null) {
      throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
    }

    dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]

      if (androidConfig != null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
        
        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        def nameCleansed = name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_')
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", nameCleansed)
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        if (!androidConfig["buildTypes"].isEmpty()) {
          reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
        }
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
        
        reactNativeModules.add(reactNativeModuleConfig)
      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
      }
    }

    return [reactNativeModules, reactNativeModulesBuildVariants, json["project"]["android"]["packageName"]];
  }
}

使用下面这条命令,来查看node_modules中有哪些使用了原生的依赖

node  ./node_modules/@react-native-community/cli/build/bin.js config

输出如下:(只贴出了一个库的信息)

{
  "root": "/xxxxxxxxxxx",
  "reactNativePath": "/xxxxxx/node_modules/react-native",
  "dependencies": {
    "react-native-code-push": {
      "root": "/xxxxxx/node_modules/react-native-code-push",
      "name": "react-native-code-push",
      "platforms": {
        "ios": {
          "sourceDir": "/xxxxx/node_modules/react-native-code-push/ios",
          "folder": "/xxxxx/node_modules/react-native-code-push",
          "pbxprojPath": "/xxxxx/node_modules/react-native-code-push/ios/CodePush.xcodeproj/project.pbxproj",
          "podfile": null,
          "podspecPath": "/xxxxx/node_modules/react-native-code-push/CodePush.podspec",
          "projectPath": "/xxxxx/node_modules/react-native-code-push/ios/CodePush.xcodeproj",
          "projectName": "CodePush.xcodeproj",
          "libraryFolder": "Libraries",
          "sharedLibraries": [],
          "plist": [],
          "scriptPhases": [],
          "configurations": []
        },
        "android": {
          "sourceDir": "/xxxxx/node_modules/react-native-code-push/android",
          "folder": "/xxxxx/node_modules/react-native-code-push",
          "packageImportPath": "import com.microsoft.codepush.react.CodePush;",
          "packageInstance": "new CodePush(BuildConfig.DEBUG ? BuildConfig.CODE_PUSH_KEY_DEV : BuildConfig.CODE_PUSH_KEY, getApplicationContext(), BuildConfig.DEBUG,BuildConfig.CODE_PUSH_SERVER)",
          "buildTypes": []
        }
      },
      "assets": [],
      "hooks": {},
      "params": []
    }
}

主要是获取node bin.js conofig 输出的结果,然后按照一定格式输出,最终给addReactNativeModuleProjects、addReactNativeModuleDependencies、generatePackagesFile 使用。下面来分析一下bin.js 的执行过程。

这里拓展一下, 你可能听过react-native脚手架,现在它是指 react-native-community/cli, 全局安装后就可以使用 react-native命令,例如:npx react-native init AwesomeProject ,react-native 是一个js文件。

通过命令which react-native 来查看一下它的位置,最后指向了/node_modules/react-native/cli.js(这里不是全局安装的),cli.js 也很简单,调用了 node_modules/@react-native-community/cli/index.js中的run函数。

分析到这里,我们回过头看看 bin.js,它里面其实也是执行node_modules/@react-native-community/cli/index.js中的run函数。是脚手架的一个入口

关于如何调试

原计划从 node_modules/@react-native-community/cli/build/bin.js 开始分析,因为node_modules中的js代码,是由ts转换的,经常跳到d.ts文件中,看起来不顺畅。而且调试的时候,webstorm很聪明的定位到ts文件,但不知道文件在哪个目录下。所以后来还是决定在 react-native-community/cli 项目中来调试(如果你不会调试TS,看这里)。

如何在调试react-native-community/cli项目,因为部分流程会检测当前是否一个react-native工程,所以直接运行cli,很多流程走不到。我是这样配置的,AwesomeProject和cli在同级目录下
在这里插入图片描述

2.2、bin.js

下面就看一下react-native-community/cli 中关于Autolinking的主干流程,进入bin.ts后,流程如下

bin.ts run() -> 同级目录 index.ts run() -> setupAndRun()

2.3 setupAndRun

该函数作用是 添加各种命令,到commander中,commander 是一个命令处理库,先把命令名称、参数 、回调处理函数等 设置给commander。在commander.parse(process.argv) 时,根据命令名,来判断需要commander中的哪个命令来处理(调用回调函数)

下面看下代码:

async function setupAndRun() {
   ...省略一些 非主流程 的代码...
   
  //添加detachedCommands中的每种命令,到commander中,
  //在commander.parse(process.argv); 时
  for (const command of detachedCommands) {
    attachCommand(command);
  }

  try {
    //加载配置,主要就是通过这里,去获取autolinking的信息。例如:package.json 依赖的第三方库,以及 android、ios项目的信息(包名、资源路径等)
    //代码2.4分析
    const config = loadConfig();
    for (const command of [...projectCommands, ...config.commands]) {
      // projectCommands 是默认命令对象的集合,每个命令对象都有的属性{name: 'config', func: xxx}
      // name 表示这个命令的名称,func 表示 执行该命令时,触发的函数
      // 代码2.5 介绍的config.ts ,就是projectCommands中的一员
      //注册命令,后续在commander.parse 执行是,就可以根据命令行中的参数信息找到对应命令对象,执行函数
      attachCommand(command, config);
    }
  } catch (error) {
    ...省略一些代码...
  }
  
  //命令行中的参数,会传入到这里。上门已经设置好了各种命令。这里根据命令行的信息,开始执行命令对应的回调函数
  //代码2.5分析
  commander.parse(process.argv);

  //默认会有两个参数(也就是命令行中没有输入参数),这时输出help结果
  if (commander.rawArgs.length === 2) {
    commander.outputHelp();
  }

  ...省略代码...
}

2.4 loadConfig

function loadConfig(projectRoot: string = findProjectRoot()): Config {
  let lazyProject: ProjectConfig;
  //从当前项目目录下,开始寻找react-native.config.js文件,
  // 在react-native.config.js文件 可以配置依赖库的autolinking规则
  const userConfig = readConfigFromDisk(projectRoot);
  //初始化配置,最后跟下面的finalConfig 合并后返回
  const initialConfig: Config = {
    root: projectRoot,
    //
    get reactNativePath() {
      return userConfig.reactNativePath
        ? path.resolve(projectRoot, userConfig.reactNativePath)
        : resolveReactNativePath(projectRoot);
    },
    dependencies: userConfig.dependencies,
    commands: userConfig.commands,
    healthChecks: [],
    platforms: userConfig.platforms,
    //该函数是在命令执行后(也就是在代码2.5之后),才会执行。
    //获取原生项目的信息,例如:包名、.gradle文件路径、manifest文件路径等。代码2.6分析
    get project() {
      if (lazyProject) {
        return lazyProject;
      }

      lazyProject = {};
      //finalConfig 在下面创建,
      //finalConfig.platforms 内容,根源是在node—module/react-native/react-native.config.js 文件中配置的。详见下图2.4.1 
      for (const platform in finalConfig.platforms) {
        const platformConfig = finalConfig.platforms[platform];
        if (platformConfig) {
          //获取原生项目的信息,在代码1.6分析。projectConfig 就是在react-native/react-native.config.js 是传入的
          lazyProject[platform] = platformConfig.projectConfig(
            projectRoot,
            userConfig.project[platform] || {},
          );
        }
      }

      return lazyProject;
    },
  };

  const finalConfig = Array.from(
    new Set([
      ...Object.keys(userConfig.dependencies),
      //这里从package.json中获取 RN依赖的第三方库
      ...findDependencies(projectRoot),
    ]),
  ).reduce((acc: Config, dependencyName) => {
    const localDependencyRoot =
      userConfig.dependencies[dependencyName] &&
      userConfig.dependencies[dependencyName].root;
    let root: string;
    let config: UserDependencyConfig;
    try {
      root =
        localDependencyRoot ||
        resolveNodeModuleDir(projectRoot, dependencyName);
       //获取当前依赖库下面的react-native.config.js 的配置信息
      config = readDependencyConfigFromDisk(root);
    } catch (error) {
      ...省略代码...
      return acc;
    }

    const isPlatform = Object.keys(config.platforms).length > 0;

    return assign({}, acc, {
      //对每一个三方库,创建一个item,key是库名,value 是函数,用于获取库的原生信息(例如:创建库中类的对象)
      dependencies: assign({}, acc.dependencies, {
        //DependencyConfig  将会调用react-native/react-native.config.js 传入的 dependencyConfig
        get [dependencyName](): DependencyConfig {
          return getDependencyConfig(
            root,
            dependencyName,
            finalConfig,
            config,
            userConfig,
            isPlatform,
          );
        },
      }),
      commands: [...acc.commands, ...config.commands],
      // 传入关于平台的信息,例如:React-native 如图2.4.1,projectConfig 获取原生项目的信息,dependencyConfig 获取第三方库的信息
      //在2.6 和 2.7 介绍这两个 
      platforms: {
        ...acc.platforms,
        ...config.platforms,
      },
      healthChecks: [...acc.healthChecks, ...config.healthChecks],
    }) as Config;
  }, initialConfig);

  return finalConfig;
}

下图 2.4.1

在这里插入图片描述

2.5 commander.parse(process.argv);

该函数是触发命令的执行,我们命令行 传入的参数是config,那么就会执行对应的回调

在这里插入图片描述

在代码2.3 中 config = loadConfig(); 传入到attachCommand,所以这里的action回调,是已经有了这些信息。下面我们进入cli-config/src/commads/config.ts 看看

config.ts

export default {
  name: 'config',
  description: 'Print CLI configuration',
  func: async (_argv: string[], ctx: Config) => {
    //filterConfig 就是去获取所有的内容
    console.log(JSON.stringify(filterConfig(ctx), null, 2));
  },
};

//config 参数内容如下图 1.5.2
function filterConfig(config: Config) {
  // 触发 project函数
  const filtered = {...config};
  Object.keys(filtered.dependencies).forEach((item) => {
    //触发 dependencies中每项的函数
    if (!isValidRNDependency(filtered.dependencies[item])) {
      delete filtered.dependencies[item];
    }
  });
  return filtered;
}

下图 2.5.2,可以看到

  1. project 是个函数,也就是代码2.4中initialConfig 中传入的。
  2. dependencies 中的每个item都是一个函数,他是 代码2.4 中finalConfig 传入的

通过代码2.4的分析,可以知道 project函数将会调用 projectConfig 获取原生项目的信息,dependencies中的每个item会调用 dependencyConfig 获取第三方库的信息。他们都是在 filterConfig 中触发的。
在这里插入图片描述

下面就来看一下 projectConfig 获取原生项目的信息、dependencyConfig 获取第三方库的信息 的代码

2.6 projectConfig

获取原生项目的信息,该函数是在 react-native/react-native.config.js 配置中传入的

拓展:在调试时发现,代码指向并没有到项目中的 packages/platform-android/src/config/index.ts ,而指向的是node_modules/@react-native-community/cli-platform-android/src/config/index.ts ,后者是前者的一个软连接,一番探索发现了一些技巧 1、 你所不知道的模块调试技巧 - npm link 2、【npm】简化本地文件引用路径

每一个信息的获取,都是优先获取配置文件中的,如果没有,再去调用函数获取。整体逻辑很简单,就不注释了

export function projectConfig(
  root: string,
  userConfig: AndroidProjectParams = {},
): AndroidProjectConfig | null {

  const src = userConfig.sourceDir || findAndroidDir(root);

  if (!src) {
    return null;
  }

  const sourceDir = path.join(root, src);

  const appName = getAppName(sourceDir, userConfig.appName);

  const manifestPath = userConfig.manifestPath
    ? path.join(sourceDir, userConfig.manifestPath)
    : findManifest(path.join(sourceDir, appName));

  if (!manifestPath) {
    return null;
  }

  const packageName = userConfig.packageName || getPackageName(manifestPath);

  if (!packageName) {
    throw new Error(`Package name not found in ${manifestPath}`);
  }

  return {
    sourceDir,
    appName,
    packageName,
    dependencyConfiguration: userConfig.dependencyConfiguration,
  };
}

下图 2.5.2 projectConfig 执行完成后,拿到的信息,是这样子的
在这里插入图片描述

2.7 dependencyConfig

代码2.5中filtered.dependencies[item] 触发的是 代码2.4中 传入的getDependencyConfig 函数,

function getDependencyConfig(
  root: string,
  dependencyName: string,
  finalConfig: Config,
  config: UserDependencyConfig,
  userConfig: UserConfig,
  isPlatform: boolean,
): DependencyConfig {
  return merge(
    {
      root,
      name: dependencyName,
      platforms: Object.keys(finalConfig.platforms).reduce(
        (dependency, platform) => {
          const platformConfig = finalConfig.platforms[platform];
          dependency[platform] =
            // Linking platforms is not supported
            isPlatform || !platformConfig
              ? null
               //这里的dependencyConfig  才是react-native/react-native.config.js 传入的函数
              : platformConfig.dependencyConfig(
                  root,
                  config.dependency.platforms[platform],
                );
          return dependency;
        },
        {} as Config['platforms'],
      ),
    },
    userConfig.dependencies[dependencyName] || {},
  ) as DependencyConfig;
}

platformConfig.dependencyConfig 调用的函数也是在 packages/platform-android/src/config/index.ts 中

优先从依赖库中的react-native.config.js 获取,若无,再调用函数获取

export function dependencyConfig(
  root: string,
  userConfig: AndroidDependencyParams = {},
): AndroidDependencyConfig | null {
  const src = userConfig.sourceDir || findAndroidDir(root);

  if (!src) {
    return null;
  }

  const sourceDir = path.join(root, src);
  const manifestPath = userConfig.manifestPath
    ? path.join(sourceDir, userConfig.manifestPath)
    : findManifest(sourceDir);

  if (!manifestPath) {
    return null;
  }

  const packageName = userConfig.packageName || getPackageName(manifestPath);
  const packageClassName = findPackageClassName(sourceDir);

  /**
   * This module has no package to export
   */
  if (!packageClassName) {
    return null;
  }

  const packageImportPath =
    userConfig.packageImportPath ||
    `import ${packageName}.${packageClassName};`;

  const packageInstance =
    userConfig.packageInstance || `new ${packageClassName}()`;

  const buildTypes = userConfig.buildTypes || [];
  const dependencyConfiguration = userConfig.dependencyConfiguration;

  return {
    sourceDir,
    packageImportPath,
    packageInstance,
    buildTypes,
    dependencyConfiguration,
  };
}

下面看一下,最后获取的内容:图2.7.1

在这里插入图片描述

命令执行完成,返回信息到2.1中的 reactNativeConfigOutput,进行一些解析赋值,Autolinking需要的原生信息,已经拿到,下面就是根据这些信息,执行上面提到的总流程的第3、4、5步,这些就非常简单了

先来看第2步autoModules.addReactNativeModuleProjects(defaultSettings)

3 、addReactNativeModuleProjects

效果是 添加三方库信息到setting.gradle

  void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
    //reactNativeModule 中保存是,每个库的信息,
    reactNativeModules.forEach { reactNativeModule ->
      //nameCleansed 库名称,对应图2.7.1 中的红框中的dependencies 的react-native-code-push这一项的key值。就是react-native-code-push
      String nameCleansed = reactNativeModule["nameCleansed"]
      //对应react-native-code-push这一项的platforms.android.sourceDir
      String androidSourceDir = reactNativeModule["androidSourceDir"]
      //和在setting.gradle中,引入本地库是一样的,只不过这里是在编译器赋值的
      defaultSettings.include(":${nameCleansed}")
      defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
    }
  }

再来看总流程的第3步addReactNativeModuleDependencies

4 、addReactNativeModuleProjects

添加三方库信息到 build.gradle中

  void addReactNativeModuleDependencies(Project appProject) {
    reactNativeModules.forEach { reactNativeModule ->
      def nameCleansed = reactNativeModule["nameCleansed"]
      appProject.dependencies {
        //reactNativeModulesBuildVariants 是从脚手架获取的,意味着可以通过配置来决定。
        //对应图2.7.1 react-native-code-push这一项的platforms.android.buildTypes
        if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
          //为每一种buildVariant,添加依赖
          reactNativeModulesBuildVariants
            .get(nameCleansed)
            .forEach { buildVariant ->
              "${buildVariant}Implementation" project(path: ":${nameCleansed}")
            }
        } else {
          // TODO(salakar): are other dependency scope methods such as `api` required?
          implementation project(path: ":${nameCleansed}")
        }
      }
    }
  }

5、generatePackagesFile

调用generatePackagesFile,生成PackageList 类,用于在MainApplication 初始化ReactNativeHost时 getPackages 中使用

参数 generatedFileContentsTemplate 是一个字符串,内容就是PackageList的模板代码。 它把需要import的第三方库,用字符串{{ packageImports }} 先代替。

generatePackagesFile 函数收集第三方库信息后,替换字符串中所得内容,最后把字符串,写入文件即可。

  void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>>[] packages = this.reactNativeModules
    String packageName = this.packageName

    String packageImports = ""
    String packageClassInstances = ""

    if (packages.size() > 0) {
      def interpolateDynamicValues = {
        it
                .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, {
                  wholeString, prefix, className, suffix ->
                    "${prefix}${packageName}.${className}${suffix}"
                })
      }
      packageImports = packages.collect {
        "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
      }.join('\n')
      packageClassInstances = ",\n      " + packages.collect {
        interpolateDynamicValues(it.packageInstance)
      }.join(",\n      ")
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ packageImports }}", packageImports)
      .replace("{{ packageClassInstances }}", packageClassInstances)

    outputDir.mkdirs()
    final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
    treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
      w << generatedFileContents
    }
  }

代码3、4是在gradle同步的时候,就会执行。代码5 是依赖build任务,所以需要编译项目,才能生成

刚开始是想断点调试native-module.gralde,但是断点进入build.gradle中后,在native-module.gralde中就是断不下来。后来询问了react-native-community/cli的一个贡献者,他使用打日志的方式,感觉这种方式太麻烦了,所以顺便请教一下大神,如何断点调试build.gradle引用的其他脚本。

至此,Autolinking的源码就分析完了,请点赞、评论 支持一下,欢迎吐槽,U·ェ·U

;