从源码深入探究react-native 0.60 的autolink过程

React Native 0.60 加入了一个很重要的特性:autolinking,从此项目引入第三方库中的原生依赖再也不用额外调用react-native link命令了,同时link时也不会像之前一样入侵原生代码(例如修改android的setting.gradle,bulid.gradle
在RN项目开发过程中,大部分的依赖下都是能autolink成功的,但是有一小部分原生依赖默认情况下是无法autolink成功的,这部分的依赖均有一个特点:不规范的文件结构,比如像下面这样子的:

react-native-xx库文件夹
├─ lib
│    ├─ android
│    ├─ ios
│    └─ js
├─ docs
├─ node_modules
├─ index.js

android和ios的原生代码都被包裹在一个文件夹下

而规范的文件结构应该是这样的:

eact-native-xx库文件夹
├─ android
├─ ios
├─ docs
├─ node_modules
├─ index.js

为什么第一种文件结构无法autolink成功呢,这就要涉及到autolink的整个运行流程了,接下来就开始一步一步从源码理清这个过程吧(只探究android)

一、Link

众所周知,react-native link做的事情有3件:
1、在android/setting.gradle 下添加:

include ':react-native库'
project(':react-native库').projectDir = new File(rootProject.projectDir,     '../node_modules/react-native依赖/android')

2、在android/app/build.gradle

  dependencies {
  +   implementation project(':react-native库')
  }

3、在android/app/src/main/java/com/your-app/MainApplication.java

 + import com.ocetnik.timer.BackgroundTimerPackage;

 @Override
 protected List getPackages() {
   return Arrays.asList(
      +   new xx依赖Package()
   );
 }

之后项目就能调用到此原生依赖

二、autolink

那么autolink是如何代替上面这三个配置呢,我们发现autolink时并不会在这3个文件上添加任何代码(其实,autolink发生在项目编译打包过程中,而不是安装依赖时)
但是,这三处地方比以往多了一些变化
1、在setting.gradle下新增了

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

2、在android/app/build.gradle下新增了

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

3、在android/app/src/main/java/com/your-app/MainApplication.java

  protected List getPackages() {
          @SuppressWarnings("UnnecessaryLocalVariable")
          List packages = new PackageList(this).getPackages();
          // Packages that cannot be autolinked yet can be added manually here, for example:
          // packages.add(new MyReactNativePackage());
          return packages;
        }

就是这3处变化使得link过程自动化,同时避免了原生代码的改动
接下来就一起来走进这3处地方代表的autolink的3个过程

1. 在setting.gradle里配置android依赖

setting.gradle的作用是配置项目所需要用到的依赖,gradle插件会加载里面的所有依赖并添加到android项目中

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

从代码可知,这条语句是去调用/node_modules/@react-native-community/cli-platform-android/native_modules.gradle这个gradle文件下的applyNativeModulesSettingsGradle方法
我们直接去往这个文件

...
def autoModules = new ReactNativeModules(logger, projectRoot)

ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
  if (root != null) {
    logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now.");
    logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`.");
  }
  autoModules.addReactNativeModuleProjects(defaultSettings) 
}
...

可以看到,applyNativeModulesSettingsGradle会去调用ReactNativeModules类的实例autoModulesaddReactNativeModuleProjects方法
我们来看下ReactNativeModlules这个类

class ReactNativeModules {
  private Logger logger
  private String packageName
  private File root
  private ArrayList> reactNativeModules

  private static String LOG_PREFIX = ":ReactNative:"

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

    def (nativeModules, packageName) = this.getReactNativeConfig()
    this.reactNativeModules = nativeModules
    this.packageName = packageName
  }

  /**
   * 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}")
    }
  }
  ...
}

可以看到addReactNativeModuleProjects做了一件事,就是把reactNativeModules数组的每一个项提取出来,取每个项的nameCleansedandroidSourceDir属性赋给defaultSettings

  • 由构造函数可知,reactNativeModules是一个由getReactNativeConfig方法返回的数组,我们暂且不去管getReactNativeConfig的具体内容,只需要知道它返回了项目的包名packageName以及包含所有的依赖对象的数组reactNativeModules,每个对象包含了对应依赖的若干信息,包括路径,名称等等,nameCleansed是依赖的名称,androidSouceDir是依赖包的android路径。
  • 那作为参数传过来的defaultSettings是什么呢?其实它是在setting.gradle里作为applyNativeModulesSettingsGradle的参数传进来,本质是setting.gradle文件的内置隐含对象settings
... ;applyNativeModulesSettingsGradle(settings)

这里就需要额外普及一个小知识了

include ":app"
// 完整写法是下面这个,省略了settings
settings.include(“:app”)

所以说,这两行代码

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

就相当于在setting.gradle里添加

include ":${nameCleansed}"
project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")

这样就成功实现了link的第一步

2. 在build.gradle里配置依赖项并生成依赖包

// app/bulid.gradle
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
 applyNativeModulesAppBuildGradle(project)

这条语句跟setting.gradle那里一样,目标文件都是native_modules.gradle,只不过这次调用的是其中的applyNativeModulesAppBuildGradle方法

ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
  if (root != null) {
    logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now");
    logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`.");
  }
  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)
    }
  }

  preBuild.dependsOn generatePackageList // build之前执行生成包列表的task

  android {
    sourceSets {
      main {
        java {
          srcDirs += generatedSrcDir //添加一个java源文件目录
        }
      }
    }
  }
}

applyNativeModulesAppBuildGradle方法做了2件事:

  • 1、调用ReactNativeModules类的实例autoModulesaddReactNativeModuleDependencies方法,添加react-native依赖
  • 2、创建一个生成一个package列表文件的task,并使其在构建之前执行,同时将文件路径配置到sourceSets(值得注意的是,这里的android指的是加载此gradle的project里的android,这里因为是app/bulid.gradle加载的此gradle,所以对应的是bulid.gradle的android,注意不要把android{}这种形式看成是配置,而应该看成是函数的调用)
2.1 添加react-native依赖

看一下addReactNativeModuleDependencies方法

 void addReactNativeModuleDependencies(Project appProject) {
    reactNativeModules.forEach { reactNativeModule ->
      def nameCleansed = reactNativeModule["nameCleansed"]
      appProject.dependencies {
        // TODO(salakar): are other dependency scope methods such as `api` required?
        implementation project(path: ":${nameCleansed}")
      }
    }
  }

显而易见,这里相当于在app/bulid.gralde里的配置了各个react-native依赖,
如同之前的:

   dependencies {
        implementation project(':react-nativexx库')
        implementation project(':react-nativexxx库')
   }
2.2 生成Package列表文件

看一下generatePackagesFile方法

 void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList>[] 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
    }
  }

这个函数同样也是利用到了reactNativeModules,最终结果是生成了一个文件PackageList.java,路径是/android/build/generated/rn/src/main/java/com/facebook/react/PackageList.java
来看看这个PackageList.java文件长什么样

package com.facebook.react;

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;


import xx.xxPackage;

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 getPackages() {
    return new ArrayList<>(Arrays.asList(
      new MainReactPackage(mConfig),
      new xxPackage()
    ));
  }
}

定义了一个PackageList类,重点是getPackages()方法,返回所有的package,那么这个类的主要作用是什么呢,答案下面揭晓

3.在MainApplication.java里配置package依赖包

看一下现在的MainApplication.java代码

...
import com.facebook.react.PackageList;
...
 @Override
 protected List getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable")
     List packages = new PackageList(this).getPackages();
     // Packages that cannot be autolinked yet can be added manually here, for example:
     // packages.add(new MyReactNativePackage());
     return packages;
 }
...

可以看出,一个叫com.facebook.react.PackageList包被导入了,这个包就是上一步生成的PackageList.java,通过sourceSets指定源文件目录被编译和识别。
MainApplication.javagetPackages()调用了PackageList类实例的getPackages(),将返回的所有包封装成一个数组返回,这个对应了 之前的

  @Override
    protected List getPackages() {
      return Arrays.asList(
            new xx依赖Package()
      );
    }

至此,autolink的大概过程就解析完毕了,可以看出,每一个过程都能与之前的react-native link形成对应,autolink通过加载gradle插件将link过程自动化,从而省略了手动link这一步骤

三、autolink失败的原因

回到最开始的问题,为什么不规范的第三方库会导致link失败呢?
这里我们引入两个库测试,一个是文件结构规范的@react-native-community/async-storage库,另一个是文件结构不规范(android和ios文件夹在lib目录下)的react-native-amap-geolocation
我们回到在autolink过程中一直被用到的依赖对象数组reactNativeModules的构建过程,看下它构建后的值

[
  [nameCleansed:react-native-community_async-storage, 
  androidSourceDir:E:\xx项目\node_modules\@react-native-community\async- 
  storage\android,
  packageInstance:new AsyncStoragePackage(), 
  name:@react-native-community/async-storage,
  packageImportPath:import 
  com.reactnativecommunity.asyncstorage.AsyncStoragePackage;], 
]

可以发现,这个数组只包含了@react-native-community/async-storage库,而没有react-native-amap-geolocation,从而导致通过此数组做的各种配置少了这个库,这就是autolink失败的原因了。
我们再继续探究下去,看看更详细的原因
reactNativeModules的值是由getReactNativeConfig()方法返回的

...
ArrayList> getReactNativeConfig() {
    if (this.reactNativeModules != null) return this.reactNativeModules

    ArrayList> reactNativeModules = new ArrayList>()
    def cliResolveScript = "console.log(require('react-native/cli').bin);"
    String[] nodeCommand = ["node", "-e", cliResolveScript]
    def cliPath = this.getCommandOutput(nodeCommand, this.root) 
    //E:\xx项目\node_modules\@react-native-community\cli\build\bin.js
    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"]

    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"]
     //  this.logger.error("${value}") 输出日志
      if (androidConfig != null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")

        HashMap reactNativeModuleConfig = new HashMap()
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")

        reactNativeModules.add(reactNativeModuleConfig)
      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
      }
    }

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

通过打log,可以得知,这个方法执行了"node E:\xx项目\node_modules\@react-native-community\cli\build\bin.js config " 命令,将返回的数据封装成一个对象,再遍历这个对象的dependecies数组,如果某一项的.platforms.android.sourceDir不为null,则会将它的android模块的各项信息封装一个对象,作为最后返回结果reactNativeModules的其中一项,我们来打印一下dependecies中的@react-native-community/async-storage库这一项的信息:

[root:E:\work\v-next\orion-app\node_modules\@react-native-community\async-storage, 
 name:@react-native-community/async-storage, 
 platforms:[
   ios: [
       sourceDir:E:\xx项目\node_modules\@react-native-community\async-storage\ios, 
       folder:E:\xx项目\node_modules\@react-native-community\async-storage, 
       pbxprojPath:E:\work\xx项目\node_modules\@react-native-community\async-storage\ios\RNCAsyncStorage.xcodeproj\project.pbxproj, 
       podfile:null, 
       podspecPath:E:\xx项目\node_modules\@react-native-community\async-storage\RNCAsyncStorage.podspec, 
       projectPath:E:\xx项目\node_modules\@react-native-community\async-storage\ios\RNCAsyncStorage.xcodeproj, 
       projectName:RNCAsyncStorage.xcodeproj, 
       libraryFolder:Libraries, 
       sharedLibraries:[], 
       plist:[], 
       scriptPhases:[]
       ], 
   android:[
        sourceDir:E:\xx项目\node_modules\@react-native-community\async-storage\android, 
        folder:E:\xx项目\node_modules\@react-native-community\async-storage, 
        packageImportPath:import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;, 
        packageInstance:new AsyncStoragePackage()
        ]
  ], 
 assets:[], 
 hooks:[:], 
 params:[]
]

再来打印react-native-amap-geolocation库这一项的信息

[root:E:\work\v-next\orion-app\node_modules\react-native-amap-geolocation, 
 name:react-native-amap-geolocation, 
 platforms:[
   ios: [
         sourceDir:E:\work\v-next\orion-app\node_modules\react-native-amap-geolocation\ios, 
         folder:E:\work\v-next\orion-app\node_modules\react-native-amap-geolocation, 
         pbxprojPath:E:\work\v-next\orion-app\node_modules\react-native-amap-geolocation\ios\RNAMapGeolocation.xcodeproj\project.pbxproj, 
         podfile:null, 
         podspecPath:null, 
         projectPath:E:\work\v-next\orion-app\node_modules\react-native-amap-geolocation\ios\RNAMapGeolocation.xcodeproj, 
         projectName:RNAMapGeolocation.xcodeproj, 
         libraryFolder:Libraries,
         sharedLibraries:[], 
         plist:[],
         scriptPhases:[]
        ], 
   android:null
 ], 
 assets:[], 
 hooks:[:], 
 params:[]
]

android属性居然为null!这就导致了,最终形成的reactNativeModules不会有react-native-amap-geolocation这个库
为什么会出现这样的情况呢?我们继续探究下去
显而易见,原因出在执行的node命令当中,我们前往这个E:\xx项目\node_modules\@react-native-community\cli\build\bin.js文件

#!/usr/bin/env node
"use strict";

require("./tools/gracefulifyFs");

var _ = require("./"); // 引入目录下的`index.js`
(0, _.run)(); 

//# sourceMappingURL=bin.js.map

可以看出此文件引入了目录下的index.js文件,并调用其中的run方法(注意:(0,func)(params)这种形式的写法可以看成是this指向全局windows对象的func(params))
我们看下index.js里的run方法

var _config = _interopRequireDefault(require("./tools/config"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

async function run() {
  try {
    await setupAndRun();
  } catch (e) {
    handleError(e);
  }
}
async function setupAndRun() {
  // Commander is not available yet
  // when we run `config`, we don't want to output anything to the console. We
  // expect it to return valid JSON
  if (process.argv.includes('config')) {
    _cliTools().logger.disable();
  }

  _cliTools().logger.setVerbose(process.argv.includes('--verbose')); // We only have a setup script for UNIX envs currently


  if (process.platform !== 'win32') {
    const scriptName = 'setup_env.sh';

    const absolutePath = _path().default.join(__dirname, '..', scriptName);

    try {
      _child_process().default.execFileSync(absolutePath, {
        stdio: 'pipe'
      });
    } catch (error) {
      _cliTools().logger.warn(`Failed to run environment setup script "${scriptName}"\n\n${_chalk().default.red(error)}`);

      _cliTools().logger.info(`React Native CLI will continue to run if your local environment matches what React Native expects. If it does fail, check out "${absolutePath}" and adjust your environment to match it.`);
    }
  }

  for (const command of _commands.detachedCommands) {
    attachCommand(command);
  }

  try {
    const ctx = (0, _config.default)();

    _cliTools().logger.enable();

    for (const command of [..._commands.projectCommands, ...ctx.commands]) {
      attachCommand(command, ctx);
    }
  } catch (e) {
    _cliTools().logger.enable();

    _cliTools().logger.debug(e.message);

    _cliTools().logger.debug('Failed to load configuration of your project. Only a subset of commands will be available.');
  }

  _commander().default.parse(process.argv);

  if (_commander().default.rawArgs.length === 2) {
    _commander().default.outputHelp();
  } // We handle --version as a special case like this because both `commander`
  // and `yargs` append it to every command and we don't want to do that.
  // E.g. outside command `init` has --version flag and we want to preserve it.


  if (_commander().default.args.length === 0 && _commander().default.rawArgs.includes('--version')) {
    console.log(pkgJson.version);
  }
}

重点看const ctx = (0, _config.default)();这行代码,查看_config的值可知,这行代码是调用了
./tools/config/index.js里的default方法
我们继续前往./tools/config/index.js文件...

function loadConfig(projectRoot = (0, _findProjectRoot.default)()) {
  let lazyProject;
  const userConfig = (0, _readConfigFromDisk.readConfigFromDisk)(projectRoot);
  const initialConfig = {
    root: projectRoot,

    get reactNativePath() {
      return userConfig.reactNativePath ? _path().default.resolve(projectRoot, userConfig.reactNativePath) : (0, _resolveReactNativePath.default)(projectRoot);
    },

    dependencies: userConfig.dependencies,
    commands: userConfig.commands,

    get assets() {
      return (0, _findAssets.default)(projectRoot, userConfig.assets);
    },

    platforms: userConfig.platforms,

    get project() {
      if (lazyProject) {
        return lazyProject;
      }

      lazyProject = {};

      for (const platform in finalConfig.platforms) {
        const platformConfig = finalConfig.platforms[platform];

        if (platformConfig) {
          lazyProject[platform] = platformConfig.projectConfig(projectRoot, userConfig.project[platform] || {});
        }
      }

      return lazyProject;
    }

  };
  const finalConfig = Array.from(new Set([...Object.keys(userConfig.dependencies), ...(0, _findDependencies.default)(projectRoot)])).reduce((acc, dependencyName) => {
    const localDependencyRoot = userConfig.dependencies[dependencyName] && userConfig.dependencies[dependencyName].root;
    let root;
    let config;

    try {
      root = localDependencyRoot || (0, _resolveNodeModuleDir.default)(projectRoot, dependencyName);
      config = (0, _readConfigFromDisk.readDependencyConfigFromDisk)(root);
    } catch (error) {
      _cliTools().logger.warn((0, _cliTools().inlineString)(`
          Package ${_chalk().default.bold(dependencyName)} has been ignored because it contains invalid configuration.

          Reason: ${_chalk().default.dim(error.message)}`));

      return acc;
    }

    const isPlatform = Object.keys(config.platforms).length > 0;
    return (0, _assign.default)({}, acc, {
      dependencies: (0, _assign.default)({}, acc.dependencies, {
        get [dependencyName]() {
          return getDependencyConfig(root, dependencyName, finalConfig, config, userConfig, isPlatform);
        }

      }),
      commands: [...acc.commands, ...config.commands],
      platforms: { ...acc.platforms,
        ...config.platforms
      }
    });
  }, initialConfig);
  return finalConfig;
}

var _default = loadConfig;
exports.default = _default;

可以看出,default方法,其实就是loadConfig方法,loadConfig方法最终返回一个叫finalConfig的属性,由代码可知,dependecies属性是通过调用getDependencyConfig(root, dependencyName, finalConfig, config, userConfig, isPlatform)来赋值的,我们前往这个getDependencyConfig方法

function getDependencyConfig(root, dependencyName, finalConfig, config, userConfig, isPlatform) {
  return (0, _merge.default)({
    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 : platformConfig.dependencyConfig(root, config.dependency.platforms[platform]);
      return dependency;
    }, {}),
    assets: (0, _findAssets.default)(root, config.dependency.assets),
    hooks: config.dependency.hooks,
    params: config.dependency.params
  }, userConfig.dependencies[dependencyName] || {});
}

重点是platformConfig.dependencyConfig这个方法,这个方法由前面的下面这段代码添加上的:

 config = (0, _readConfigFromDisk.readDependencyConfigFromDisk)(root);

我们不再过多地叙述这段代码的具体实现,只需要知道它通过给定的root路径定位到了node_modules/react-native文件夹下的react-native-config.js

const ios = require('@react-native-community/cli-platform-ios');
const android = require('@react-native-community/cli-platform-android');

module.exports = {
  commands: [...ios.commands, ...android.commands],
  platforms: {
    ios: {
      linkConfig: ios.linkConfig,
      projectConfig: ios.projectConfig,
      dependencyConfig: ios.dependencyConfig,
    },
    android: {
      linkConfig: android.linkConfig,
      projectConfig: android.projectConfig,
      dependencyConfig: android.dependencyConfig,
    },
  },
  /**
   * Used when running RNTester (with React Native from source)
   */
  reactNativePath: '.',
  project: {
    ios: {
      project: './RNTester/RNTesterPods.xcworkspace',
    },
    android: {
      sourceDir: './RNTester',
    },
  },
};

并提取其中的dependencyConfig属性,我们前往@react-native-community/cli-platform-android

...
Object.defineProperty(exports, "dependencyConfig", {
  enumerable: true,
  get: function () {
    return _config.dependencyConfig;
  }
});

...
var _config = require("./config");
···

不多说什么,我们继续前往./config

...
function dependencyConfig(root, userConfig = {}) {
  const src = userConfig.sourceDir || (0, _findAndroidDir.default)(root);

  if (!src) {
    return null;
  }

  const sourceDir = _path().default.join(root, src);

  const manifestPath = userConfig.manifestPath ? _path().default.join(sourceDir, userConfig.manifestPath) : (0, _findManifest.default)(sourceDir);

  if (!manifestPath) {
    return null;
  }

  const manifest = (0, _readManifest.default)(manifestPath);
  const packageName = userConfig.packageName || getPackageName(manifest);
  const packageClassName = (0, _findPackageClassName.default)(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}()`;
  return {
    sourceDir,
    folder: root,
    packageImportPath,
    packageInstance
  };
}

可以看到,如果userConfig.sourceDir为空,就是用户未指定资源路径,就会调用
_findAndroidDir.default(root)方法来设置
我们继续前往这个方法(放心,这次是最后一次了。。)

function findAndroidDir(root) {
  if (_fs().default.existsSync(_path().default.join(root, 'android'))) {
    return 'android';
  }

  return null;
}

我们打印一下root的值

// react-native-amap-geolocation
E:\xx项目\node_modules\react-native-amap-geolocation
// @react-native-community\async-storage
E:\xx项目\node_modules@react-native-community\async-storage

可以看到root的值都是.\node_modules\+库名这种形式,而findAndroidDir调用existsSync()来判断${root}/android这个目录是否存在,很显然,由于react-native-amap-geolocation的android目录是${root}/lib/android所以导致这个方法返回了null, dependencyConfig方法也返回了null,层层往上,最终导致reactNativeModules中的这一个库的android属性项为空
真相终于找到了

四、如何解决

原因找到了,除了更改第三方库文件结构这种方法外,有没有什么比较优雅的方式呢?答案是有的,有两种

  • 1、跟之前一样,react-native link ‘目标库’,不走autolink的流程
  • 2、在项目根目录下的react-native-config.js配置文件下手动指定目标库的root路径
const path = require('path')
module.exports = {
  dependencies: {
    'react-native-amap-geolocation': {
      root: path.join(__dirname, 'node_modules/react-native-amap-geolocation/lib'),//注意,不能用相对路径
    },
  },
}

五、总结

总结就不说了,源码太多,看得写得太累了,以后再补了

你可能感兴趣的:(从源码深入探究react-native 0.60 的autolink过程)