ReactNative项目构建分析与思考之native_modules.gradle

这篇具有很好参考价值的文章主要介绍了ReactNative项目构建分析与思考之native_modules.gradle。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

上一篇文章分析完 react-native-gradle-plugin 后,我们知道了react-native-gradle-plugin 主要是对依赖环境,以及RN的依赖版本进行管理。
本篇文章来接着分析 native_modules.gradle 这个脚本,这个脚本是React Native构建中比较重要的一个角色。

native_modules.gradle

这是一个源码形式的脚本文件,虽然只有一个文件,但是实际上要比插件的逻辑还要复杂一些, 源码如下

import groovy.json.JsonSlurper
import org.gradle.initialization.DefaultSettings
import org.apache.tools.ant.taskdefs.condition.Os

def generatedFileName = "PackageList.java"
def generatedFilePackage = "com.facebook.react"
def generatedFileContentsTemplate = """
package $generatedFilePackage;

import android.app.Application;
import android.content.Context;
import android.content.res.Resources;

import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;

{{ packageImports }}

public class PackageList {
  private Application application;
  private ReactNativeHost reactNativeHost;
  private MainPackageConfig mConfig;

  public PackageList(ReactNativeHost reactNativeHost) {
    this(reactNativeHost, null);
  }

  public PackageList(Application application) {
    this(application, null);
  }

  public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
    this.reactNativeHost = reactNativeHost;
    mConfig = config;
  }

  public PackageList(Application application, MainPackageConfig config) {
    this.reactNativeHost = null;
    this.application = application;
    mConfig = config;
  }

  private ReactNativeHost getReactNativeHost() {
    return this.reactNativeHost;
  }

  private Resources getResources() {
    return this.getApplication().getResources();
  }

  private Application getApplication() {
    if (this.reactNativeHost == null) return this.application;
    return this.reactNativeHost.getApplication();
  }

  private Context getApplicationContext() {
    return this.getApplication().getApplicationContext();
  }

  public ArrayList<ReactPackage> getPackages() {
    return new ArrayList<>(Arrays.<ReactPackage>asList(
      new MainReactPackage(mConfig){{ packageClassInstances }}
    ));
  }
}
"""

def cmakeTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli)

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

{{ libraryIncludes }}

set(AUTOLINKED_LIBRARIES
  {{ libraryModules }}
)
"""

def rncliCppTemplate = """/**
 * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 */

#include "rncli.h"
{{ rncliCppIncludes }}

namespace facebook {
namespace react {

{{ rncliReactLegacyComponentNames }}

std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ rncliCppModuleProviders }}
  return nullptr;
}

void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ rncliCppComponentDescriptors }}
{{ rncliReactLegacyComponentDescriptors }}
  return;
}

} // namespace react
} // namespace facebook
"""

def rncliHTemplate = """/**
 * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 */

#pragma once

#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook {
namespace react {

std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);

} // namespace react
} // namespace facebook
"""

class ReactNativeModules {
  private Logger logger
  private String packageName
  private File root
  private ArrayList<HashMap<String, String>> reactNativeModules
  private ArrayList<String> unstable_reactLegacyComponentNames
  private HashMap<String, ArrayList> reactNativeModulesBuildVariants
  private String reactNativeVersion

  private static String LOG_PREFIX = ":ReactNative:"

  ReactNativeModules(Logger logger, File root) {
    this.logger = logger
    this.root = root

    def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig()
    this.reactNativeModules = nativeModules
    this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
    this.packageName = androidProject["packageName"]
    this.unstable_reactLegacyComponentNames = androidProject["unstable_reactLegacyComponentNames"]
    this.reactNativeVersion = reactNativeVersion
  }

  /**
   * Include the react native modules android projects and specify their project directory
   */
  void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
    reactNativeModules.forEach { reactNativeModule ->
      String nameCleansed = reactNativeModule["nameCleansed"]
      String androidSourceDir = reactNativeModule["androidSourceDir"]
      defaultSettings.include(":${nameCleansed}")
      defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
    }
  }

  /**
   * Adds the react native modules as dependencies to the users `app` project
   */
  void addReactNativeModuleDependencies(Project appProject) {
    reactNativeModules.forEach { reactNativeModule ->
      def nameCleansed = reactNativeModule["nameCleansed"]
      def dependencyConfiguration = reactNativeModule["dependencyConfiguration"]
      appProject.dependencies {
        if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
          reactNativeModulesBuildVariants
            .get(nameCleansed)
            .forEach { buildVariant ->
              if(dependencyConfiguration != null) {
                "${buildVariant}${dependencyConfiguration}"
              } else {
                "${buildVariant}Implementation" project(path: ":${nameCleansed}")
              }
            }
        } else {
          if(dependencyConfiguration != null) {
            "${dependencyConfiguration}"
          } else {
             implementation project(path: ":${nameCleansed}")
          }
        }
      }
    }
  }

  /**
   * Code-gen a java file with all the detected ReactNativePackage instances automatically added
   *
   * @param outputDir
   * @param generatedFileName
   * @param generatedFileContentsTemplate
   */
  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
                // Before adding the package replacement mechanism,
                // BuildConfig and R classes were imported automatically
                // into the scope of the file. We want to replace all
                // non-FQDN references to those classes with the package name
                // of the MainApplication.
                //
                // We want to match "R" or "BuildConfig":
                //  - new Package(R.string…),
                //  - Module.configure(BuildConfig);
                //    ^ hence including (BuildConfig|R)
                // but we don't want to match "R":
                //  - new Package(getResources…),
                //  - new PackageR…,
                //  - new Royal…,
                //    ^ hence excluding \w before and after matches
                // and "BuildConfig" that has FQDN reference:
                //  - Module.configure(com.acme.BuildConfig);
                //    ^ hence excluding . before the match.
                .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
    }
  }

  void generateCmakeFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>> packages = this.reactNativeModules
    String packageName = this.packageName
    String codegenLibPrefix = "react_codegen_"
    String libraryIncludes = ""
    String libraryModules = ""

    if (packages.size() > 0) {
      libraryIncludes = packages.collect {
        if (it.libraryName != null && it.cmakeListsPath != null) {
          // If user provided a custom cmakeListsPath, let's honor it.
          String nativeFolderPath = it.cmakeListsPath.replace("CMakeLists.txt", "")
          "add_subdirectory($nativeFolderPath ${it.libraryName}_autolinked_build)"
        } else {
          null
        }
      }.minus(null).join('\n')
      libraryModules = packages.collect {
        it.libraryName ? "${codegenLibPrefix}${it.libraryName}" : null
      }.minus(null).join('\n  ')
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ libraryIncludes }}", libraryIncludes)
      .replace("{{ libraryModules }}", libraryModules)

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

  void generateRncliCpp(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>> packages = this.reactNativeModules
    ArrayList<String> unstable_reactLegacyComponentNames = this.unstable_reactLegacyComponentNames
    String rncliCppIncludes = ""
    String rncliCppModuleProviders = ""
    String rncliCppComponentDescriptors = ""
    String rncliReactLegacyComponentDescriptors = ""
    String rncliReactLegacyComponentNames = ""
    String codegenComponentDescriptorsHeaderFile = "ComponentDescriptors.h"
    String codegenReactComponentsDir = "react/renderer/components"

    if (packages.size() > 0) {
      rncliCppIncludes = packages.collect {
        if (!it.libraryName) {
          return null
        }
        def result = "#include <${it.libraryName}.h>"
        if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
          result += "\n#include <${codegenReactComponentsDir}/${it.libraryName}/${codegenComponentDescriptorsHeaderFile}>"
        }
        result
      }.minus(null).join('\n')
      rncliCppModuleProviders = packages.collect {
        it.libraryName ? """  auto module_${it.libraryName} = ${it.libraryName}_ModuleProvider(moduleName, params);
  if (module_${it.libraryName} != nullptr) {
    return module_${it.libraryName};
  }""" : null
      }.minus(null).join("\n")
      rncliCppComponentDescriptors = packages.collect {
        def result = ""
        if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
          result += it.componentDescriptors.collect {
            "  providerRegistry->add(concreteComponentDescriptorProvider<${it}>());"
          }.join('\n')
        }
        result
      }.join("\n")
    }

    rncliReactLegacyComponentDescriptors = unstable_reactLegacyComponentNames.collect {
      "  providerRegistry->add(concreteComponentDescriptorProvider<UnstableLegacyViewManagerInteropComponentDescriptor<${it}>>());"
    }.join("\n")
    rncliReactLegacyComponentNames = unstable_reactLegacyComponentNames.collect {
      "extern const char ${it}[] = \"${it}\";"
    }.join("\n")
    if (unstable_reactLegacyComponentNames && unstable_reactLegacyComponentNames.size() > 0) {
      rncliCppIncludes += "\n#include <react/renderer/components/legacyviewmanagerinterop/UnstableLegacyViewManagerInteropComponentDescriptor.h>"
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ rncliCppIncludes }}", rncliCppIncludes)
      .replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders)
      .replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors)
      .replace("{{ rncliReactLegacyComponentDescriptors }}", rncliReactLegacyComponentDescriptors)
      .replace("{{ rncliReactLegacyComponentNames }}", rncliReactLegacyComponentNames)

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

  void generateRncliH(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    String generatedFileContents = generatedFileContentsTemplate

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

  /**
   * Runs a specified command using Runtime exec() in a specified directory.
   * Throws when the command result is empty.
   */
  String getCommandOutput(String[] command, File directory) {
    try {
      def output = ""
      def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
      def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
      def buff = ""
      def readBuffer = new StringBuffer()
      while ((buff = bufferedReader.readLine()) != null) {
        readBuffer.append(buff)
      }
      output = readBuffer.toString()
      if (!output) {
        this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
        def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
        def errBuff = ""
        def readErrorBuffer = new StringBuffer()
        while ((errBuff = bufferedErrorReader.readLine()) != null) {
          readErrorBuffer.append(errBuff)
        }
        throw new Exception(readErrorBuffer.toString())
      }
      return output
    } catch (Exception exception) {
      this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
      throw exception
    }
  }

  /**
   * Runs a process to call the React Native CLI Config command and parses the output
   */
  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>()

    /**
     * Resolve the CLI location from Gradle file
     *
     * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
     * will fail to resolve the script and the dependencies. We should resolve this soon.
     *
     * @todo: `fastlane` has been reported to not work too.
     */
    def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
    String[] nodeCommand = ["node", "-e", cliResolveScript]
    def cliPath = this.getCommandOutput(nodeCommand, this.root)

    String[] reactNativeConfigCommand = ["node", cliPath, "config"]
    def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)

    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"]
    def reactNativeVersion = json["version"]

    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()}")
    }

    def engine = new groovy.text.SimpleTemplateEngine()

    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"])
        reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"])
        reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"])
        reactNativeModuleConfig.put("cmakeListsPath", androidConfig["cmakeListsPath"])

        if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
          reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
        }
        if(androidConfig.containsKey("dependencyConfiguration")) {
          reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"])
        } else if (project.containsKey("dependencyConfiguration")) {
          def bindings = ["dependencyName": nameCleansed]
          def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings)

          reactNativeModuleConfig.put("dependencyConfiguration", template.toString())
        }

        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"], reactNativeVersion];
  }
}


/*
 * Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory
 * where build files of an active project are located.
 */
def projectRoot = rootProject.projectDir

def autoModules = new ReactNativeModules(logger, projectRoot)

def reactNativeVersionRequireNewArchEnabled(autoModules) {
    def rnVersion = autoModules.reactNativeVersion
    def regexPattern = /^(\d+)\.(\d+)\.(\d+)(?:-(\w+(?:[-.]\d+)?))?$/

    if (rnVersion =~ regexPattern) {
        def result = (rnVersion =~ regexPattern).findAll().first()

        def major = result[1].toInteger()
        if (major > 0 && major < 1000) {
            return true
        }
    }
    return false
}

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

ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings ->
  autoModules.addReactNativeModuleProjects(defaultSettings)
}

ext.applyNativeModulesAppBuildGradle = { Project project ->
  autoModules.addReactNativeModuleDependencies(project)

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

  task generatePackageList {
    doLast {
      autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
    }
  }

  task generateNewArchitectureFiles {
    doLast {
      autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate)
      autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
      autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
    }
  }

  preBuild.dependsOn generatePackageList
  def isNewArchEnabled = (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") ||
    reactNativeVersionRequireNewArchEnabled(autoModules)
  if (isNewArchEnabled) {
    preBuild.dependsOn generateNewArchitectureFiles
  }

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

还记得在settings.gradle中有如下代码吗?

apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
applyNativeModulesSettingsGradle(settings)

也就是在settings.gradle中引入了native_modules.gradle,并且随后调用了 applyNativeModulesSettingsGradle 方法。

那就以 applyNativeModulesSettingsGradle 方法为入口,来逐步的进行分析。

def autoModules = new ReactNativeModules(logger, projectRoot)

//该方法会在settings.gradle中被调用,作用是把RN需要的三方库include到settings.gradle中
ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings ->
    autoModules.addReactNativeModuleProjects(defaultSettings)
}

可以看到,实际上这个方法就是调用了ReactNativeModules实例中的addReactNativeModuleProjects方法,我们重点来看下ReactNativeModules类

//部分源码
class ReactNativeModules {
    //日志
    private Logger logger
    //Android包名
    private String packageName
    //根目录
    private File root
    //react-native模块
    private ArrayList<HashMap<String, String>> reactNativeModules
    //不稳定的遗留组件名称
    private ArrayList<String> unstable_reactLegacyComponentNames
    //react-native模块构建变体
    private HashMap<String, ArrayList> reactNativeModulesBuildVariants
    //react-native版本
    private String reactNativeVersion

    private static String LOG_PREFIX = ":ReactNative:"

    ReactNativeModules(Logger logger, File root) {
        this.logger = logger
        this.root = root

        //获取react-native配置
        def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig()
        this.reactNativeModules = nativeModules
        this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
        this.packageName = androidProject["packageName"]
        this.unstable_reactLegacyComponentNames = androidProject["unstable_reactLegacyComponentNames"]
        this.reactNativeVersion = reactNativeVersion
    }
}

首先在创建ReactNativeModules实例的时候,会调用getReactNativeConfig 方法获取react-native的配置信息,随后把这些配置信息保存到ReactNativeModules实例中。

我们接着看一下 getReactNativeConfig 方法
下面的代码我增加了些注释,方便大家理解


    /**
     * 执行react-native config命令,获取react-native配置
     * @return
     */
    ArrayList<HashMap<String, String>> getReactNativeConfig() {
        //如果已经获取过配置,直接返回
        if (this.reactNativeModules != null) return this.reactNativeModules

        //存储react-native模块
        ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
        //存储react-native模块构建变体
        HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()

        /**
         * 执行一段脚本,获取react-native-cli的路径
         * 最终获取的结果:xxx/node_modules/@react-native-community/cli/build/bin.js
         */
        def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
        String[] nodeCommand = ["node", "-e", cliResolveScript]
        def cliPath = this.getCommandOutput(nodeCommand, this.root)
        /**
         * 执行 node xxx/node_modules/@react-native-community/cli/build/bin.js config  获取rn配置的结果
         * 这个node命令执行的逻辑比较复杂,内部的流程很多,大致上就是调用了react-native-cli的config命令,从而获取到了RN的版本,配置,依赖库等关键信息
         *
         */
        String[] reactNativeConfigCommand = ["node", cliPath, "config"]
        def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)

        def json
        try {
            //将json字符串转换成json对象
            json = new JsonSlurper().parseText(reactNativeConfigOutput)
        } catch (Exception exception) {
            throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
        }

        //获取react-native的依赖模块
        def dependencies = json["dependencies"]
        //获取react-native的android配置
        def project = json["project"]["android"]
        //获取react-native的版本
        def reactNativeVersion = json["version"]

        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()}")
        }

        def engine = new groovy.text.SimpleTemplateEngine()
        //处理react-native的依赖模块
        dependencies.each { name, value ->
            //获取react-native模块的android配置
            def platformsConfig = value["platforms"];
            //获取android配置
            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"])
                reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"])
                reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"])
                reactNativeModuleConfig.put("cmakeListsPath", androidConfig["cmakeListsPath"])

                if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
                    reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
                }
                if (androidConfig.containsKey("dependencyConfiguration")) {
                    reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"])
                } else if (project.containsKey("dependencyConfiguration")) {
                    def bindings = ["dependencyName": nameCleansed]
                    def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings)

                    reactNativeModuleConfig.put("dependencyConfiguration", template.toString())
                }

                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"], reactNativeVersion];
    }
}

可以看到,getReactNativeConfig 主要做了以下几件事情

  • 通过node命令获取react-native-cli的路径
    node -e "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
    
    • 这里实际上就是通过node -e 执行一段js代码,上面的js代码会从当前目录的node_modules中获取cli的路径.
    • 返回值是一个路径,示例:xxx/node_modules/@react-native-community/cli/build/bin.js
  • 接着执行node xxx/node_modules/@react-native-community/cli/build/bin.js config命令,获取react-native的配置信息
    • 这个node命令执行的逻辑比较复杂,内部的流程很多,后面再详细分析
    • 返回值是一个json字符串,包含了react-native的版本,配置,依赖库等关键信息
  • 最后把这些配置信息处理以下保存到ReactNativeModules实例中

下面接着看一下调用 autoModules.addReactNativeModuleProjects(defaultSettings) 的 addReactNativeModuleProjects 方法的源码

/**
 * Include the react native modules android projects and specify their project directory
 */
void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
    reactNativeModules.forEach { reactNativeModule ->
        String nameCleansed = reactNativeModule["nameCleansed"]
        String androidSourceDir = reactNativeModule["androidSourceDir"]

        System.out.println("nameCleansed: ${nameCleansed}, androidSourceDir: ${androidSourceDir}")
        defaultSettings.include(":${nameCleansed}")
        defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
    }
}

可以看到,实际上就是把通过 getReactNativeConfig 获取到的 reactNativeModules 信息,添加到settings.gradle中
看下打印出来的值:

nameCleansed: react-native-device-info, androidSourceDir: /Users/yuzhiqiang/workspace/RN/personal/RNProjectAnalysis/node_modules/react-native-device-info/android

就相当于我们在setting.gradle中手动添加了一个模块

include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File('/Users/yuzhiqiang/workspace/RN/personal/RNProjectAnalysis/node_modules/react-native-device-info/android')

同理,在app模块的build.gradle中,有如下代码

//应用了一个脚本文件
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
applyNativeModulesAppBuildGradle(project)

同样看下 applyNativeModulesAppBuildGradle 方法的源码

ext.applyNativeModulesAppBuildGradle = { Project project ->
    autoModules.addReactNativeModuleDependencies(project)

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

    task generatePackageList {
        doLast {
            autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
        }
    }

    task generateNewArchitectureFiles {
        doLast {
            autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate)
            autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
            autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
        }
    }

    preBuild.dependsOn generatePackageList
    def isNewArchEnabled = (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") ||
            reactNativeVersionRequireNewArchEnabled(autoModules)
    if (isNewArchEnabled) {
        preBuild.dependsOn generateNewArchitectureFiles
    }

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

首先第一步就看到了 autoModules.addReactNativeModuleDependencies(project) 方法,很明显,这个方法跟在settings.gradle中调用的方法类似,是用来添加依赖的。 源码如下:


    /**
     * 添加react-native模块依赖到app项目
     * @param appProject
     */
    void addReactNativeModuleDependencies(Project appProject) {
        reactNativeModules.forEach { reactNativeModule ->
            def nameCleansed = reactNativeModule["nameCleansed"]
            def dependencyConfiguration = reactNativeModule["dependencyConfiguration"]
            appProject.dependencies {
                if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
                    reactNativeModulesBuildVariants
                            .get(nameCleansed)
                            .forEach { buildVariant ->
                                if (dependencyConfiguration != null) {
                                    "${buildVariant}${dependencyConfiguration}"
                                } else {
                                    System.out.println("" + nameCleansed + "${buildVariant}Implementation project(path: \":${nameCleansed}\")")
                                    "${buildVariant}Implementation" project(path: ":${nameCleansed}")
                                }
                            }
                } else {
                    if (dependencyConfiguration != null) {
                        "${dependencyConfiguration}"
                    } else {
                        // 把依赖添加到app模块里,相当于 implementation project(path: ":xxx")
                        implementation project(path: ":${nameCleansed}")
                    }
                }
            }
        }
    }

接着,再来看下 applyNativeModulesAppBuildGradle 后续的逻辑。

在完成了app模块的依赖添加之后,紧接着会声明一个generatePackageList 的task,这个task主要就是做了一件事情,就是生成一个java文件,这个java文件里面包含了所有的react-native模块的信息。

    task generatePackageList {
        doLast {
            autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
        }
    }

来看看 generatePackagesFile 方法里的逻辑。

    /**
     * 通过上面定义的模版生成一个PackageList.java文件,替换导包和实例
     * @param outputDir
     * @param generatedFileName
     * @param generatedFileContentsTemplate
     */
    void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
        ArrayList<HashMap<String, String>> packages = this.reactNativeModules
        String packageName = this.packageName

        String packageImports = ""
        String packageClassInstances = ""

        System.out.println("outputDir: ${outputDir}, generatedFileName: ${generatedFileName}, generatedFileContentsTemplate: ${generatedFileContentsTemplate}")
        System.out.println("packages: ${packages}")

        if (packages.size() > 0) {

            /**
             * 针对BuildConfig和R的引用,进行替换,确保使用的是正确的项目路径和包名
             */
            def interpolateDynamicValues = {
                it
                // Before adding the package replacement mechanism,
                // BuildConfig and R classes were imported automatically
                // into the scope of the file. We want to replace all
                // non-FQDN references to those classes with the package name
                // of the MainApplication.
                //
                // We want to match "R" or "BuildConfig":
                //  - new Package(R.string…),
                //  - Module.configure(BuildConfig);
                //    ^ hence including (BuildConfig|R)
                // but we don't want to match "R":
                //  - new Package(getResources…),
                //  - new PackageR…,
                //  - new Royal…,
                //    ^ hence excluding \w before and after matches
                // and "BuildConfig" that has FQDN reference:
                //  - Module.configure(com.acme.BuildConfig);
                //    ^ hence excluding . before the match.
                        .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, { wholeString, prefix, className, suffix -> "${prefix}${packageName}.${className}${suffix}"
                        })
            }

            //拼接导包
            packageImports = packages.collect {
                "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
            }.join('\n')

            System.out.println("""packageImports: ${packageImports}""")

            //拼接实例
            packageClassInstances = ",\n      " + packages.collect {
                interpolateDynamicValues(it.packageInstance)
            }.join(",\n      ")

            System.out.println("""packageClassInstances: ${packageClassInstances}""")
        }

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

        System.out.println("generatedFileContents: ${generatedFileContents}")

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

加了些注释,可以看到,实际上就是用源码中的 generatedFileContentsTemplate 模版字符串,替换了一些变量,生成了 PackageList.java 文件。 后续在执行 preBuild 任务之前,会先执行generatePackageList任务,prebuild 是 gradle 执行阶段生命周期中的一个基础任务,只要执行gradle build命令,就会执行prebuild任务,在执行 preBuild 任务之前,会先执行 generatePackageList 任务。

    //在执行 preBuild 任务之前,先执行generatePackageList任务
    preBuild.dependsOn generatePackageList

至于后面的 generateNewArchitectureFiles 任务,是用来生成一些新架构所需文件,跟上面逻辑类似。

    task generateNewArchitectureFiles {
        doLast {
            autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate)
            autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
            autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
        }
    }

    preBuild.dependsOn generatePackageList
    def isNewArchEnabled = (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") ||
            reactNativeVersionRequireNewArchEnabled(autoModules)
    if (isNewArchEnabled) {
        preBuild.dependsOn generateNewArchitectureFiles
    }

下面是生成后的代码位置
ReactNative项目构建分析与思考之native_modules.gradle,混合开发,react native,rn三方库依赖,RN构建

生成好的PackageList代码会在 MainApplication 中被用到。

ReactNative项目构建分析与思考之native_modules.gradle,混合开发,react native,rn三方库依赖,RN构建
到这里,整个流程大致上就清楚了,下面我们就可以回答下上一篇文章中提出的问题。

为什么在使用RN插件时,只需要执行一下 yarn add react-native-device-info 后就可以直接在前端代码中使用了,而不需要关心native方面的代码。

实际上在Android部分是由gradle插件以及 native_modules.gradle 脚本还有结合前端的cli工具,帮我们做了很多事情。

到这里我们基本把 native_modules.gradle 中的逻辑分析完毕了,总结一下,也能够初步的回答上面的问题了:

  • 通过执行cli命令获取react-native的配置信息,其中包含了react-native的版本,配置,依赖库等关键信息
  • 把react-native的依赖库信息进行处理,把模块添加到settings.gradle中,以便于后续在模块中依赖使用
  • 把react-native的模块通过源码依赖的方式添加到app模块中
  • 生成一个PackageList.java文件,里面包含了所有的react-native模块的信息
  • 如果是新架构,还会生成一些新架构所需的文件,主要是jin相关的文件
  • 最后在app模块的build.gradle中,会把生成好的PackageList.java文件引入到MainApplication中使用

这样,我们就可以在前端代码中直接使用react-native-device-info这个模块了。


到这里,算是初步对Android端RN项目构建的基本流程分析的差不多了。

本篇文章我们遗留了一个步骤中流程的分析是:执行node xxx/node_modules/@react-native-community/cli/build/bin.js config命令,获取react-native的配置信息

实际上,获取react-native的配置信息是非常重要的一个环节,是由前端cli来完成的,这个我们后续再来分析。


感谢阅读,觉有有帮助点赞支持,如果有任何疑问或建议,欢迎在评论区留言。如需转载,请注明出处:喻志强的博客 ,谢谢!文章来源地址https://www.toymoban.com/news/detail-841733.html

到了这里,关于ReactNative项目构建分析与思考之native_modules.gradle的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ReactNative进阶(三十四):Jenkins 流水线 组包 iOS 应用包 ipa Archive 阶段报错error: Multiple commands produce问题修复及思考

    在应用RN开发跨平台APP阶段,从git中拉取项目,应用Jenkins进行组包时,发现最终生成的ipa安装包版本号始终与项目中设置的版本号不一致。 经过仔细排查,发现Jenkins在 Archive 编译、归档阶段失败,但是后续 Export 阶段生成了ipa包。 在xCode中打开应用,选中项目 target - Build p

    2024年02月10日
    浏览(41)
  • 使用ReactNative构建视频应用程序:流处理、编解码和WebSockets

    作者:禅与计算机程序设计艺术 在本文中,我们将会使用React Native框架,结合Websockets协议,实现一个完整的基于WebRTC的视频直播应用。该项目旨在学习如何从零开始构建一个复杂且实用的视频应用程序。文章假定读者已经掌握了React Native开发的基础知识。我们需要安装Reac

    2024年02月09日
    浏览(39)
  • APP中RN页面渲染流程-ReactNative源码分析

    在APP启动后,RN框架开始启动。等RN框架启动后,就开始进行RN页面渲染了。 RN页面原生侧页面渲染的主要逻辑实现是在RCTUIManager和RCTShadowView完成的。 通过看UIMananger的源码可以看到,UIMananger导出给JS端的API接口在对UI的操作上,基本都会同时对 View 和 ShadowView 进行操作。 以更

    2024年02月06日
    浏览(34)
  • APP中RN页面热更新流程-ReactNative源码分析

    平时使用WebStorm或VSCode对RN工程中的文件修改后,在键盘上按一下快捷cmd+s进行文件保存,此时当前调试的RN页面就会自动进行刷新,这是RN开发相比于原生开发一个很大的优点:热更新。 那么,从按一下快捷cmd+s到RN页面展示出最新的JS页面,这个过程是怎样发生的呢?下面根

    2024年02月05日
    浏览(33)
  • React Native and Native Modules: A Developer's Guide to Bridging the Gap

    React Native is a popular framework for building cross-platform mobile applications using JavaScript and React. It allows developers to write code once and deploy it on both iOS and Android platforms. Native modules are an essential part of React Native, as they provide access to native APIs and platform-specific features. In this guide, we will explore the

    2024年04月11日
    浏览(41)
  • 云计算项目十一:构建完整的日志分析平台

    检查k8s集群环境,master主机操作,确定是ready 启动harbor 检查head插件是否启动,如果没有,需要启动 随便找一台机器 安装filebeat的软件包 创建文件baseos.yaml,使用Secret regcred 还有一句:- name: regcred

    2024年03月10日
    浏览(56)
  • 【异常】Jenkins执行前端项目Npm构建时,出现报错This dependency was not found: vue-class-component in ./node_modules/vu

    同事的代码变更,导致了在Jenkins无法执行构建 错误的原因是项目中引入了vue-class-component,但是在项目的依赖列表中,却没有找到它,因此会报如上的错误。 vue-class-component是一个用于类式Vue组件的TypeScript装饰器。 它允许您将Vue组件定义为类,这可以使您的代码更有组织性和

    2024年02月03日
    浏览(62)
  • 实时数仓构建:Flink+OLAP查询的一些实践与思考

    今天是一篇架构分享内容。 以Flink为主的计算引擎配合OLAP查询分析引擎组合进而构建实时数仓 ,其技术方案的选择是我们在技术选型过程中最常见的问题之一。也是很多公司和业务支持过程中会实实在在遇到的问题。 很多人一提起实时数仓,就直接大谈特谈Hudi,Flink的流批

    2024年04月15日
    浏览(43)
  • 深入思考Sui的独特性如何构建出跨时代的产品

    近日,我们与 Mysten Labs产品总监 Janet Wu 面对面探讨了Web3的产品开发过程,了解了她对Sui上最激动人心的产品用例的看法,以及她对该行业未来的展望。 对我而言,产品总监的职位不只是一系列具体的角色和责任,它更是一种思维方式。准确地说,这是一种思维方式和技能的

    2024年02月15日
    浏览(42)
  • 框架分析(8)-React Native

    link 主要对目前市面上常见的框架进行分析和总结,希望有兴趣的小伙伴们可以看一下,会持续更新的。希望各位可以监督我,我们一起学习进步。 React Native是Facebook开源的一款用于构建跨平台移动应用的框架。它基于React,可以使用JavaScript编写应用程序,并将其转换为原生

    2024年02月10日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包