零Node基础看懂React-Native脚手架工具

做过RN开发的同学肯定对react-native-cli命令行工具不陌生,官方文档一开始在搭建开发环境的章节就会介绍到利用这个工具快速创建一个新项目。刚开始接触RN开发的你,一定会对这个命令行工具感到好奇,它是如何快速的创建一个RN项目的呢,又是如何完成一系列的工程目录结构创建,配置信息添加,各种依赖安装的呢,本文就带你来一探究竟。

react-native-cli是Facebook开源项目ReactNative自带的一个脚手架工具,可以很方便帮助开发者快速的从0开始创建一个完整的RN项目。react-native-cli其实是一个node项目,你可能没有接触过node开发,就像作者一样,是从原生转RN开发的,别担心,没有node基础,一样可以看懂,下面我们就正式开始吧。

1. React-Native脚手架的使用方法

这里简单描述一下本地搭建React-Native开发环境的主要流程,按照RN官网的文档描述主要有如下步骤:

  • 安装必须的软件

    本文重点是RN脚手架,这里简要概况,具体安装方法见React-Native官方文档

    • Homebrew:Mac系统的包管理器,用于安装NodeJS和一些其他必需的工具软件。
    • Node.js:使用Homebrew来安装Node.js
    • Yarn:是Facebook提供的替代npm的工具,非必须安装,可跳过
    • Xcode:iOS开发工具,提供了iOS开发环境,运行iOS端需要该开发环境
    • WebStorm:RN开发工具,用来编写React Native应用,推荐使用,另外Nuclide、VSCode、Sublime Text也可以
  • 安装react-native-cli命令行工具(RN脚手架)

    npm install -g react-native-cli
    

    npm 常用的安装命令,用来安装node包,react-native-cli是一个node包,

    -g是全局安装,根据需要,这里选择全局安装

  • 快速创建RN应用

    • 创建RN工程
    react-native init MyRNProject
    

    init:初始工程,快速创建RN工程

    MyRNProject: 项目名称

    • 启动工程(运行iOS项目,查看效果)
    react-native run-ios
    

如果以上步骤都安装正确,这里应该能启动iOS模拟器,并运行RN工程了。整个流程看起来是不是很简单。之所以看起来很方便、简单,是因为react-native-cli命令行工具替我们完成了一系列的创建、配置、初始化、安装依赖的工作。本文的重点就是带大家探究一下,react-native-cli究竟替我们做了哪些工作,又是如何完成的。

2. react-native-cli命令行工具解密

2.1 概览

其实整个react-native-cli命令行工具包括两部分

  • react-native-cli包内部分:主要完成React-Native引擎的下载安装
  • React-Native内的node_modules/@react-native-community/cli:主要负责RN工程的创建,初始化,依赖安装等

具体这两部分是如何工作的呢,不要着急,下面我们一步一步的拆解并跟踪整个工程创建过程,看完你就明白了。

2.2 react-native-cli部分

2.2.1 找到真身

首先,来看react-native-cli部分,当安装完成后,我们就可以全局使用react-native命令了,例如:

 ~ % react-native --version
> react-native-cli: 2.0.1
> react-native: n/a - not inside a React Native project directory

可以看到,当执行react-native --version时,会输出react-native-cli的版本号,(这里是2.0.1)还有react-native的版本号(由于这里不是在RN项目根目录下执行的,所以这里提示当前目录不是RN项目的目录)。

那这个react-native命令究竟是个啥呢?通过which命令,可以查看一个命令的安装路径,所以。。。

~ % which react-native
> /usr/local/bin/react-native

我们打开这个目录看看


零Node基础看懂React-Native脚手架工具_第1张图片
bin_react-native

原来是一个替身,让他现行吧,右键显示原身:

零Node基础看懂React-Native脚手架工具_第2张图片
nm_react-native-cli

如图所示,最终指向的是react-native-cli模块中的index.js文件。可以看出react-native-cli其实就是一个node.js项目,运行在node上。

看到这里可能有些同学有点担心了,“我不懂node呀,是个node小白,进行不下去了”。别担心,笔者对node的理解也只是停留在使用node模块层面,也没有过node模块的开发经验。

废话不多说,虽然没开发过node项目,但是RN项目还是有些经验的,不都是JS的项目嘛,结合以往RN项目的开发经验,让我们大胆的用webstorm打开这个node工程看看。

2.2.2 剖析真身(react-native-cli部分)

打开工程,我们发现,这个node工程还是比较简单的:


零Node基础看懂React-Native脚手架工具_第3张图片
react-native-cli_project

除了node_modules文件夹,只有一个代码文件:index.js,这个文件既是入口文件又是全部功能实现逻辑。下面我们来分析一下这个index.js文件。

2.2.2.1 配置调试参数

分析代码最好的方式就是调试、跟踪代码执行的每一个步骤。笔者使用的是webstorm,将react-native-cli文件夹作为node工程打开后,添加调试配置。添加->选择Node.js模板->配置Configuration标签下的参数,具体如下:

零Node基础看懂React-Native脚手架工具_第4张图片
rn-cli_debug_config
  • Node interpreter: Project //node(usr/local/bin/node) 默认会选择,不需要修改
  • Working directory : ~/Documents/workspace/Node/ 运行时的所在的目录,会在当前目录下创能你的新RN工程
  • Application parameters:init TestRNProject 命令行参数,即shell命令react-native init TestRNProject 中的init TestRNProject部分

点击‘OK’,保存后,在index.js文件中打上断点,点击debug就可以一步一步跟踪调试了:

2.2.2.2 代码跟踪

可以看到,index.js文件首先是引入了一堆工具类(node的工具类)

var fs = require('fs');
var path = require('path');
var exec = require('child_process').exec;
var execSync = require('child_process').execSync;
var chalk = require('chalk');
var prompt = require('prompt');
var semver = require('semver');

fs-文件读写等处理工具模块、path-文件路径处理模块、child_process-子进程模块。具体每个模块的作用,可自行搜索资料了解。

接着出现了这么一行:

var options = require('minimist')(process.argv.slice(2));

后续调试得知该行的作用是读取命令行的输入参数,例如react-native init TestRNProject命令,会把'init' 'myProject'作为两个参数读入options

实际调试中可以看到,options最终得到的是一个对象:

 {
  "_": [
    "init",
    "TestRNProject"
  ]
}

接着是两个工具方法,用来获取目标文件的路径

var CLI_MODULE_PATH = function() {
...
  
var REACT_NATIVE_PACKAGE_JSON_PATH = function() {
...
  

然后是cli的定义和赋值及运行(react-native-cli 的核心逻辑部分)

var cli;
var cliPath = CLI_MODULE_PATH();
if (fs.existsSync(cliPath)) {
  cli = require(cliPath);
}

var commands = options._;
if (cli) {
  cli.run();
}

上面这一段实际上是判断当前路径是否是一个RN的工程根目录,如果是,则调用RN工程中的 cli工具进行初始化和运行。咱们当前是创建一个全新的RN工程,所以这段代码暂时不用关注。

接下来是:

if (cli) {
  cli.run();
} else {
  if (options._.length === 0 && (options.h || options.help)) {
    console.log([
      。。。 //本文作者注:一些帮助信息的输出,这里省略
    ].join('\n'));
    process.exit(0);
  }

  if (commands.length === 0) {
    console.error(
      'You did not pass any commands, run `react-native --help` to see a list of all available commands.'
    );
    process.exit(1);
  }

  switch (commands[0]) {
  case 'init':
    if (!commands[1]) {
      console.error(
        'Usage: react-native init  [--verbose]'
      );
      process.exit(1);
    } else {
      init(commands[1], options);   //本文作者注:关键部分,完成初始化创建工作
    }
    break;
  default:
    console.error(
      'Command `%s` unrecognized. ' +
      'Make sure that you have run `npm install` and that you are inside a react-native project.',
      commands[0]
    );
    process.exit(1);
    break;
  }
}

上面这段代码的else部分就是创建RN工程的核心部分了:

  • 可以看到首先是进行了一些参数校验处理,校验不通过直接退出进程;
  • 然后判断第一个参数是否是'init'(通过一个switch匹配),最终实际上会调用init方法( init(commands[1], options);)

下面是init方法的实现:

function init(name, options) {
  validateProjectName(name);

  if (fs.existsSync(name)) {
    createAfterConfirmation(name, options);
  } else {
    createProject(name, options);
  }
}

可以看到,init方法中实际进行了一些工程名称校验、文件是否存在校验后,最终调用了createProject方法,并将参数传了过去

这里name:'‌TestRNProject2' options:‌{"_":["init","TestRNProject"]}

createProject方法:

function createProject(name, options) {
  var root = path.resolve(name);   //本文作者注:这里是获取新RN项目的根目录的路径
  var projectName = path.basename(root);  //本文作者注:项目名称:TestRNProject

  console.log(
    'This will walk you through creating a new React Native project in',
    root
  );

  //本文作者注:判断项目根目录是否存现,不存在就创建该目录
  if (!fs.existsSync(root)) {
    fs.mkdirSync(root);
  }

  var packageJson = {
    name: projectName,
    version: '0.0.1',
    private: true,
    scripts: {
       start: 'node node_modules/react-native/local-cli/cli.js start'
    }
  };
  //本文作者注:将配置信息写入新项目根目录下的package.json文件
  fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson));  
  
  process.chdir(root);   //本文作者注:设置当前工作目录到新RN工程,这里是~/Documents/workspace/Node/TestRNProject

  run(root, projectName, options);  //本文作者注:root同上,projectName:‌TestRNProject, options同上
}

这一步,主要是在新RN工程根目录下写入了一个配置文件package.json:

零Node基础看懂React-Native脚手架工具_第5张图片
testRn_package

然后进入run方法:

function run(root, projectName, options) {
  // E.g. '0.38' or '/path/to/archive.tgz'
  const rnPackage = options.version;
  。。。
  //===============本文作者注:上面这一段主要是对yarn的版本校验和诱导安装,可以跳过
  
  try {
  /*
    本文作者注:调用创建进程方法,同步执行 ‌npm install --save --save-exact react-nativ 命令
    这一步就是安装react-nativ的node包
    */
    execSync(installCommand, {stdio: 'inherit'});    //installCommand:‌npm install --save --save-exact react-native
  } catch (err) {
    console.error(err);
    console.error('Command `' + installCommand + '` failed.');
    process.exit(1);
  }
  checkNodeVersion();
  cli = require(CLI_MODULE_PATH());
  cli.init(root, projectName);
}

注意 当在webstorm下调试时,执行到execSync(installCommand, {stdio: 'inherit'});这一步时,如果是debug模式下,会出现无法执行,至于为什么,先暂不讨论。

不过分析代码可以看到,正常流程下,运行完这一段安装命令后,会执行

cli = require(CLI_MODULE_PATH()); 
cli.init(root, projectName);

分析CLI_MODULE_PATH()方法的返回结果可以看到,这里实际上是调用了新RN工程下的'node_modules/react-native/cli.js'的init方法。这里是相当于调用了新的node工程的代码,当前打开的工程中无法继续跟踪源码。

综上,我们直接注释掉cli.init(root, projectName);这行代码,非调试模式下,直接运行。由运行结果可见,在调用cli.init方法之前,最终实际是在新RN工程目录下安装了react-native的nodemodule(同时包括相关依赖包):

零Node基础看懂React-Native脚手架工具_第6张图片
nm_RN

执行结果如上图所示,可以看到新RN项目的根目录下新增了一个node_modules文件夹,这里面放的就是react-native引擎和相关依赖包。

看到这里,离我们的目标工程似乎还差了不少东西,没错,iOS、Android的原生工程,还有一些App.js入口文件等等都还没有。不急,别忘了,上面我们注释掉的那行代码:cli.init(root, projectName);

如之前所述,最后的这段代码:

cli = require(CLI_MODULE_PATH());
cli.init(root, projectName);

实际上是调用了新RN工程下的node_modules/react-native/cli.js的init方法,但是当前工程又无法继续调试跟踪进去,怎么办呢?先来看一下react-native的工程吧。

2.3 React-Native local-cli

首先,我们用webstorm打开react-native引擎项目:进入我们新生成的RN工程下的node_modules文件夹下的react-native,用webstorm打开该目录。

受前面的启发,我们是不是也可以把这个react-native看做一个node工程呢,答案是肯定的。

打开工程后我们发现,根目录下面存在一个cli.js的文件,这正是之前react-native-cli工程我们最后跟踪到的那个引用文件。不错,视乎很顺利,但是别高兴的太早,再仔细看看,我们发现,之前react-native-cli在调用react-native中的cli时是调用了init方法,并传入了两个参数。但是这里的cli.js文件只是定义了cli并导出了,我们该如何调用呢?直接运行,显然是不行的(把这个cli作为入口文件直接运行的话,不会调用主体方法,无法执行后续的创建任务)

2.3.1 添加入口文件

关键的地方来了,总结一下截止到目前我们的源码深入和调试的经过(例如react-native-cli这个node工程的目录结构和我们的调试过程),结合以往的开发经验,我们是不是可以大胆的猜想或者说推测一下:如果我们给这个React-Native local-cli的node项目也加一个入口文件呢?

说干就干,下面我们创建一个index.js的文件,作为React-Native local-cli的node项目的入口文件。index.js文件内容是什么呢,很显然,我们又想起了之前注释掉的那行代码,我们把之前那行调用的代码放在这里,是不是整个流程一下就通了,豁然开朗,完美!所以最终我们加入的index.js就是这样:

#!/usr/bin/env node
//本文作者注:开头部分,为啥这样写,直接从react-nativ-cli工程的index.js拷贝过来的(其实是指定运行环境,这里是shell-node)
'use strict';

var cli;
cli = require('./cli');    //本文作者注:对应之前的 ‘cli = require(CLI_MODULE_PATH());’
cli.init('/Users/dingxin/Documents/workspace/Node/TestRNProject', 'TestRNProject');   //本文作者注:对应之前的‘cli.init(root, projectName);’
零Node基础看懂React-Native脚手架工具_第7张图片
rn_cli_index

2.3.2 WebStorm调试配置:

好了,有了之前的调试经验,下面的调试配置就比较简单了:

  • Node interpreter: Project //node(usr/local/bin/node) 默认会选择,不需要修改
  • Working directory: ~/Documents/workspace/Node/TestRNProject //目标RN工程的根目录
  • JavaScript file: node_modules/react-native/index.js //node启动的入口文件,(相对于当前工程目录的相对路径)
零Node基础看懂React-Native脚手架工具_第8张图片
rn_cli_ws_debug_config

为什么这里我们不能像react-native-cli工程调试配置的时候一样通过Application parameters 参数来传入参数呢?当然也可以,我们可以在index.js文件中通过var options = require('minimist')(process.argv.slice(2));来读取参数输入参数,然后传给cli.init调用。这里我们仅是为了测试验证问题,简单起见,我们直接写死就好了,正常开发中肯定是要用这种传参的形式。

2.3.3 运行、调试

一切准备就绪,我们的猜想是不是正确呢,终究要实际运行验证一下。我们直接点运行,可以看到:


零Node基础看懂React-Native脚手架工具_第9张图片
rn_cli_run_log

成功了,看日志输出应该是成功了。这时候查看刚才的新RN工程目录:


零Node基础看懂React-Native脚手架工具_第10张图片
rn_proj_finial

对比之前的目录,明显多了许多文件、文件夹,实际上这就是react-native脚手架替我们创建的新RN工程的最终样子。

等等,这样就结束了吗,好像少点什么。当然,本文的重点是探究RN脚手架究竟做了什么,光看到结果显然不是我们想要的。下面我们就一步一步深入看看。

2.3.4 工程目录、文件创建

再次对比刚才生成的工程目录,和上一步对比,明显可以看到多了许多文件、文件夹(图中项目根目录下红框部分)。这些文件(夹)是如何创建的呢?让我们来看看代码。

首先打开react-native/cli.js,找到cli的定义:var cli = require('@react-native-community/cli');,这里是cli实际上定义在@react-native-community下的模块。继续找到定义,最终实际上是当前工程目录(RN引擎包)下的:node_modules/@react-native-community/cli/build/index.js
打开这个文件,搜索init,可以看到,实际上是这个方法:

注:这里我们的当前工程目录(RN引擎包):~/Documents/workspace/Node/TestRNProject/node_modules/react-native

Object.defineProperty(exports, "init", {
  enumerable: true,
  get: function () {
    return _initCompat.default;
  }
});
exports.bin = void 0;

继续跳转定义,可以找到_initCompat的实际实现,node_modules/@react-native-community/cli/build/commands/init/initCompat.js 文件下的:

async function initCompat(projectDir, argsOrName) {
  const args = Array.isArray(argsOrName) ? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose']
  : [argsOrName].concat(_process().default.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp'
  // args array is e.g. ['AwesomeApp', '--verbose', '--template', 'navigation']

  if (!args || args.length === 0) {
    _cliTools().logger.error('react-native init requires a project name.');

    return;
  }

  const newProjectName = args[0];
  const options = (0, _minimist().default)(args);

  _cliTools().logger.info(`Setting up new React Native app in ${projectDir}`);

  await generateProject(projectDir, newProjectName, options);
}

可以看到,这里首先进行了一系列的校验,然后会调用generateProject方法。看来这个generateProject应该就是整个操作的核心。那么这个generateProject究竟都干了啥呢?

async function generateProject(destinationRoot, newProjectName, options) {
  const pkgJson = require('react-native/package.json');

  const reactVersion = pkgJson.peerDependencies.react;
  //笔者注:第一步,是根据RN包内置的模板工程,创建用户的新RN工程
  await (0, _templates.createProjectFromTemplate)(destinationRoot, newProjectName, options.template);

  _cliTools().logger.info('Adding required dependencies');

//笔者注:第二步,安装依赖的react库(node Module)
  await PackageManager.install([`react@${reactVersion}`], {
    root: destinationRoot
  });

  _cliTools().logger.info('Adding required dev dependencies');

//笔者注:这里是安装开发环境下依赖的辅助工具库,例如下面的babel、eslint等等
  await PackageManager.installDev(['@babel/core', '@babel/runtime', '@react-native-community/eslint-config', 'eslint', 'jest', 'babel-jest', 'metro-react-native-babel-preset', `react-test-renderer@${reactVersion}`], {
    root: destinationRoot
  });
  addJestToPackageJson(destinationRoot);

  if (_process().default.platform === 'darwin') {
  //笔者注:第三步,判断如果是mac环境,自动安装iOS工程的依赖库(通过cocopods安装)
    _cliTools().logger.info('Installing required CocoaPods dependencies');

    await (0, _installPods.default)({
      projectName: newProjectName
    });
  }

//至此,安装完成,打印帮助信息。
  (0, _printRunInstructions.default)(destinationRoot, newProjectName);
}

先来看第一步,可以看到,这里调用了_templates.createProjectFromTemplate方法,从方法名来看,应该是通过一个工程模板去创建一个新的RN工程。继续追踪,可以看到其实现方法:node_modules/@react-native-community/cli/build/tools/generator/copyProjectTemplateAndReplace.js 下的copyProjectTemplateAndReplace。添加断点,可以看到该方法的入参分别为:

  • ‌‌srcPath: 源路径(模板工程) ‌/Users/dingxin/Documents/workspace/Node/TestRNProject/node_modules/react-native/template
  • ‌‌destPath: ‌模板路径(目标新工程) /Users/dingxin/Documents/workspace/Node/TestRNProject
  • ‌‌newProjectName: ‌新工程名称 TestRNProject
  • ‌‌options: {}

源码如下:

function copyProjectTemplateAndReplace(srcPath, destPath, newProjectName, options = {}) {

//笔者注:首先是一些参数校验,这里有删减,具体实现参考源码
  if (!srcPath) {
    throw new Error('Need a path to copy from');
  }

  
  这里是一个递归循环,源模板工程目录下依次递归执行处理(文件夹创建、文件复制修改等)
  (0, _walk.default)(srcPath).forEach(absoluteSrcFilePath => {
    // 'react-native upgrade'
    if (options.upgrade) {
      // Don't upgrade these files
      const fileName = _path().default.basename(absoluteSrcFilePath); // This also includes __tests__/index.*.js
      。。。
      //笔者注:这里省略了一些校验逻辑,具体实现参考源码
    }

    const relativeFilePath = translateFilePath(_path().default.relative(srcPath, absoluteSrcFilePath)).replace(/HelloWorld/g, newProjectName).replace(/helloworld/g, newProjectName.toLowerCase()); // Templates may contain files that we don't want to copy.
     。。。
      //笔者注:这里省略了一些校验逻辑,具体实现参考源码

    let contentChangedCallback = null;

    if (options.upgrade && !options.force) {
      contentChangedCallback = (_destPath, contentChanged) => upgradeFileContentChangedCallback(absoluteSrcFilePath, relativeFilePath, contentChanged);
    }

 //笔者注:下面是文件(夹)复制修改的最终实现方法
    (0, _copyAndReplace.default)(absoluteSrcFilePath, _path().default.resolve(destPath, relativeFilePath), {
      'Hello App Display Name': options.displayName || newProjectName,
      HelloWorld: newProjectName,
      helloworld: newProjectName.toLowerCase()
    }, contentChangedCallback);
  });
}

可以看到,该方法的核心是,对模板工程目录下所有文件(文件夹)递归调用了_copyAndReplace.defaul方法。(node_modules/@react-native-community/cli/build/tools/copyAndReplace.js),该方法包括三个部分,下面分别介绍:

function copyAndReplace(srcPath, destPath, replacements, contentChangedCallback) {

  if (_fs().default.lstatSync(srcPath).isDirectory()) {
    if (!_fs().default.existsSync(destPath)) {
      _fs().default.mkdirSync(destPath);
    } // Not recursive
    return;
  }
  ...
 }

_copyAndReplace第一部分:这段代码实际上是判断是否是文件夹,如果是,则直接创建新文件夹。

function copyAndReplace(srcPath, destPath, replacements, contentChangedCallback) {
 。。。
  const extension = _path().default.extname(srcPath);
  
  if (binaryExtensions.indexOf(extension) !== -1) {
    // Binary file  二进制文件
    let shouldOverwrite = 'overwrite';
    if (contentChangedCallback) {
      const newContentBuffer = _fs().default.readFileSync(srcPath);
      。。。
      //笔者注:这里是对二进制文件是否需要重写的校验,具体实现参考源码
    }

    if (shouldOverwrite === 'overwrite') {
      copyBinaryFile(srcPath, destPath, err => {
        if (err) {
          throw err;
        }
      });
    }
  } else {
  。。。

_copyAndReplace第二部分:这一段,是对二进制文件的直接复制处理

function copyAndReplace(srcPath, destPath, replacements, contentChangedCallback) {
。。。
  } else {
    // Text file  文本文件
    const srcPermissions = _fs().default.statSync(srcPath).mode;

    //笔者注:读入文本文件,保存在变量content中
    let content = _fs().default.readFileSync(srcPath, 'utf8');  
    
    /*笔者注:下面的forEach是对读入的文本文件进行查找替换文本,主要是把模板工程中的HelloWorld替换成我们的目标工程名字
      调试可看到当前replacements参数:
        {
            'Hello App Display Name': 'MyRNProject',
            HelloWorld: 'MyRNProject',
            helloworld: 'myrnproject'
        }
    */
    Object.keys(replacements).forEach(regex => {
      content = content.replace(new RegExp(regex, 'g'), replacements[regex]);
    });
    let shouldOverwrite = 'overwrite';
    
    。。。

    //笔者注:写入文本文件
    if (shouldOverwrite === 'overwrite') {
      _fs().default.writeFileSync(destPath, content, {
        encoding: 'utf8',
        mode: srcPermissions
      });
    }
  //笔者注:文本文件处理结束
  }
  
//笔者注:copyAndReplace方法结束
}

_copyAndReplace第三部分:这里文本文件的处理是重点,其实现方法是先把文本文件读成一个字符串,然后查找替换字符串中的模板名称,替换为我们设置在模板工程的新名称。

至此,工程目录结构、文件(文件夹)的创建工作完成。

2.3.5 node依赖包:node modules安装

generateProject方法的第二步,这里比较简单,直接上源码:

async function generateProject(destinationRoot, newProjectName, options) {
  。。。

//笔者注:第二步,安装依赖的react库(node Module)
  await PackageManager.install([`react@${reactVersion}`], {
    root: destinationRoot
  });

  _cliTools().logger.info('Adding required dev dependencies');

//笔者注:这里是安装开发环境下依赖的辅助工具库,例如下面的babel、eslint等等
  await PackageManager.installDev(['@babel/core', '@babel/runtime', '@react-native-community/eslint-config', 'eslint', 'jest', 'babel-jest', 'metro-react-native-babel-preset', `react-test-renderer@${reactVersion}`], {
    root: destinationRoot
  });
  addJestToPackageJson(destinationRoot);

  。。。
}

这里比较简单,通过PackageManager进行npm包的安装,PackageManager的具体实现,大家可以自行查看源码,实际上就是对npm installyarn add等npm或yarn相关命令的调用封装。

2.3.6 iOS依赖包:pods安装

generateProject方法的第三步,这里同样比较简单,直接上源码:

async function generateProject(destinationRoot, newProjectName, options) {
。。。
  if (_process().default.platform === 'darwin') {
   //笔者注:这里是判断如果是mac环境,自动安装iOS工程的依赖库(通过cocopods安装)
    _cliTools().logger.info('Installing required CocoaPods dependencies');

    await (0, _installPods.default)({
      projectName: newProjectName
    });
  }

//笔者注:至此,安装完成,打印帮助信息。
  (0, _printRunInstructions.default)(destinationRoot, newProjectName);
}

这里是调用了node_modules/@react-native-community/cli/build/tools/installPods.js模块,installPods是对Cocopods的调用封装,包括cocopods的安装状态,版本校验等。具体实现不是本文重点,感兴趣的可以自行查看源码。

2.3.7 React-Native Local-Cli回顾

可以看到,第二部分(React-Native Local-Cli部分)的创建过程实际包括了如下几个步骤:

  • 创建工程结构:通过_templates.createProjectFromTemplate方法根据RN包内置的模板工程,创建用户的新RN工程结构
  • 安装node依赖包
    • 安装react库
    • 安装开发依赖库:babel、eslint等
  • 安装iOS工程依赖库:通过_installPods.default安装iOS工程的依赖pods,最终会生成iOS的workspace

2.4 总结

至此,我们终于搞明白了整个脚手架的原理和执行过程,实际包括了两大部分,每个部分的具体步骤总结如下:

  1. react-native-cli部分
    • 1.1 下载安装react-native-cli Node包(命令行工具) npm install -g react-native-cli
    • 1.2 执行react-native init MyRNProject (react-native-cli包内的init命令),创建工程根目录,下载安装react-native 的node module。
  2. react-native local-cli 部分
    • 2.1 创建工程结构(从模板template拷贝修改)
    • 2.2 安装node依赖包(公共依赖、dev依赖等)
    • 2.3 安装iOS依赖(pods依赖)

3. 写在最后

本文带领大家以node.js小白的身份从头到尾捋了一遍react-native脚手架的执行流程和原理,文中介绍的一些调试技巧和方法希望对大家有所帮助或启发。特别是在面对一些新技术或者不熟悉的领域时,我们一定要多思考,充分利用已有的一些知识、经验,联想、推测和大胆假设,一步一步验证尝试,最终肯定会越来越接近事实的真相。

你可能感兴趣的:(零Node基础看懂React-Native脚手架工具)