打造你的React Native脚手架


随着对React Native的使用逐渐深入,团队积累了一些应用上的最佳实践,同时整理了一批基础组件。为了帮助其他团队快速使用React Native完成需求、替换已有业务,同时也为了帮助新人快速上手、规范项目开发,准备对React Native进行定制化改造,最终形成GFRN。
首先的第一步是了解React Native脚手架工具,根据需要定制个性化脚手架。

React Native Cli 分析

1. react-native-cli

React Native被发布为两个npm包,分别为react-native-cli和react-native。其中,react-native-cli需要被全局安装,作为脚手架在命令行工具中使用。
react-native-cli本身很轻量,他的工作只是初始化项目目录,本地安装react-native,并且将所有命令交给本地的react-native执行。
react-native-cli 代码地址,分析其主要代码

var cli;
//本地react-native目录下的cli.js,node_modules/react-native/cli.js
var cliPath = CLI_MODULE_PATH();
if (fs.existsSync(cliPath)) {
  cli = require(cliPath);
}

var commands = options._;
//本地react-native目录下cli.js存在时,即已经执行过init完成初始化,执行node_modules/react-native/cli.js的run方法
if (cli) {
  cli.run();
} else {
//本地react-native目录下cli.js不存在,进行参数校验。必须项为init命令和项目名
...
init(name,options)
}

//init方法检查同名项目是否存在并提示,然后进入createProject方法,在createProject方法中,创建项目目录,生成项目的package.json文件,进入run(root, projectName, options)方法
function run(root, projectName, options) {
//检查本地环境,判断使用yarn或者npm
//调用getInstallPackage获取准备安装的react-native版本,默认安装最新稳定版,脚手架支持-v参数指定版本
...
try {
    //安装react-native
    execSync(installCommand, {stdio: 'inherit'});
  } catch (err) {
    console.error(err);
    console.error('Command `' + installCommand + '` failed.');
    process.exit(1);
  }
}
...
cli = require(CLI_MODULE_PATH());
//执行node_modules/react-native/cli.js的init方法
cli.init(root, projectName);

上述示例中看到,在执行react-native init myApp初始化项目时,会在本地生成项目目录,安装本地react-native后,执行本地react-native目录下cli.js init方法继续完成初始化工作。执行react-native start等其他命令时,会将命令交由本地react-native目录下cli.js run方法完成命令执行工作。

2. react-native init myApp 执行过程

继续阅读node_modules/react-native/cli.js,发现其代码很简单

module.exports = require('./local-cli/cli.js');

/local-cli/cli.js作为local-cli入口文件代码也很简单,代码逻辑是在/local-cli/cliEntry.js,他输出了前文提到的init和run方法

module.exports = {
  run: run,
  init: init,
};

init指向/local-cli/init/init.js,在init.js init方法中,调用generateProject()方法来创建项目。generateProject方法中,关键步骤是调用/local-cli/generator/templates.js createProjectFromTemplate()方法来根据模板生成初始模板代码文件,其代码如下

function createProjectFromTemplate(destPath, newProjectName, template, yarnVersion) {
  // 以templates/HelloWorld项目为模板,将node_modules/react-native/local-cli/templates/HelloWorld文件拷贝到项目根目录
  copyProjectTemplateAndReplace(
    path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
    destPath,
    newProjectName
  );

  if (template === undefined) {
    // 不指定模板参数template时,直接以HelloWorld项目作为模板项目
    return;
  }

//下段英文注释为源码中原有注释,解释了提供template参数时,根据template参数创建模板项目时的做饭,提到了对模板项目的结构要求
  // Keep the files from the 'HelloWorld' template, and overwrite some of them
  // with the specified project template.
  // The 'HelloWorld' template contains the native files (these are used by
  // all templates) and every other template only contains additional JS code.
  // Reason:
  // This way we don't have to duplicate the native files in every template.
  // If we duplicated them we'd make RN larger and risk that people would
  // forget to maintain all the copies so they would go out of sync.
  const builtInTemplateName = builtInTemplates[template];
  //node_modules/react-native/local-cli/templates/路径下内置模板
  if (builtInTemplateName) {
    createFromBuiltInTemplate(builtInTemplateName, destPath, newProjectName, yarnVersion);
  } else {
    // npm库中模板,template is e.g. 'ignite',
    // use the template react-native-template-ignite from npm
    createFromRemoteTemplate(template, destPath, newProjectName, yarnVersion);
  }
}

以上分析可知,react-native init myApp命令最终会根据node_modules/react-native/local-cli/templates/路径下内置模板来生成模板文件,如下即为模板项目的示例


3. react-native start 分析

前文提到,执行react-native start等其他命令时,会将命令交由本地react-native目录下cli.js run方法完成命令执行工作。
继续来看/local-cli/cliEntry.js

function run() {
  const setupEnvScript = /^win/.test(process.platform)
    ? 'setup_env.bat'
    : 'setup_env.sh';

  childProcess.execFileSync(path.join(__dirname, setupEnvScript));
  //遍历commands,在addCommand方法中,通过commander库注册该命令
  commands.forEach(cmd => addCommand(cmd, config));
//通过commander库解析当前输入
  commander.parse(process.argv);

  const isValidCommand = commands.find(cmd => cmd.name.split(' ')[0] === process.argv[2]);

  if (!isValidCommand) {
    printUnknownCommand(process.argv[2]);
    return;
  }

  if (!commander.args.length) {
    commander.help();
  }
}

上述代码中,会首先注册所有的命令,再根据当前输入匹配命中的命令去执行。
这里的commands来自/local-cli/commands.js,其中定义了当前项目中已有的命令



观察可知,在documentedCommands中列出的,即我们常用的命令,这里的每个命令,其结构的定义如下

export type CommandT = {
  name: string,
  description?: string,
  usage?: string,
  func: (argv: Array<string>, config: RNConfig, args: Object) => ?Promise<void>,
  options?: Array<{
    command: string,
    description?: string,
    parse?: (val: string) => any,
    default?: ((config: RNConfig) => mixed) | mixed,
  }>,
  examples?: Array<{
    desc: string,
    cmd: string,
  }>,
  pkg?: {
    version: string,
    name: string,
  },
};

其中react-native start命令,对应引入的./server/server。查看/local-cli/server/server.js可知,其输出为上述结构

module.exports = {
  name: 'start',
  func: server,
  description: 'starts the webserver',
  options: [{
    command: '--port [number]',
    default: 8081,
    parse: (val: string) => Number(val),
  }, ...//各种其他的option],
};

至此react-native start执行过程已经明确,其他命令的执行过程也是同样。

Create React Native App Cli 分析

了解了react-native-cli,再来看下当前社区热门的脚手架工具create-react-native-app。
create-react-native-app项目下包含两个子项目,create-react-native-app和react-native-scripts,其中,create-react-native-app为命令行工具。
create-react-native-app脚手架主要代码

async function createApp(name: string, verbose: boolean, version: ?string): Promise<void> {
  //参数校验
//packageToInstall = 'react-native-scripts'
  const packageToInstall = getInstallPackage(version);
//生成项目目录
  if (!await pathExists(name)) {
    await fse.mkdir(root);
  } else if (!await isSafeToCreateProjectIn(root)) {
    console.log(`The directory \`${name}\` contains file(s) that could conflict. Aborting.`);
    process.exit(1);
  }
//生成项目package.json文件
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  await fse.writeFile(path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2));

//调用run方法
  await run(root, appName, version, verbose, packageToInstall, packageName);
}

async function run(
  root: string,
  appName: string,
  version: ?string,
  verbose: boolean,
  packageToInstall: string,
  packageName: string
): Promise<void> {
//本地安装react-native-scripts
  install(packageToInstall, verbose, async (code: number, command: string, args: Array<string>) => {
    ...
  const scriptsPath = path.resolve(
      process.cwd(),
      'node_modules',
      packageName,
      'build',
      'scripts',
      'init.js'
    );

    const init = require(scriptsPath);
//执行node_modules/build/scripts/init.js init方法
    await init(root, appName, verbose, cwd);
  });
}

上述主要代码逻辑看出,create-react-native-app脚手架代码也很简单,在生成项目目录后,主要工作就是安装react-native-scripts并且调用react-native-scripts 的init.js完成初始化。而init.js中的主要代码逻辑,是将其template目录下模板文件拷贝到项目根目录,补充package.json文件并且安装依赖,其支持的命令,配置在package.json scripts中。

React Native 脚手架定制

在我们的实际需求中,需要通过脚手架工具,生成的模板项目中,包含我们封装的基础库和组件库,示例项目采用更有实际意义的demo,并且支持个性化的命令。

通过对react-native-cli, create-react-native-app源码的分析发现,实际上我们不需要重头开始开发我们的脚手架工具

react-native-cli本身是具有可扩展性的,在使用react-native init命令初始化项目时,可以通过-template参数指定模板项目,对于定制化方案来讲,可以将抽离的组件、基础库和demo代码发布为一个npm包,以这样的形式来初始化项目,是可以满足生成定制化模板项目的需求的。

在分析react-native-cli时,其命令是通过commands.js定义的,其中 ,documentedCommands是其内置指令,如需自定义,一种方式可以再加一个组合customerCommands,然后引入自定义指令,这种方式侵入到node_modules下进行源码级别的修改,这是一个react-native-cli扩展性的不够的地方;另一种方式,是从getProjectCommands方法入手,这一方法会读取一些配置项作为初始时的命令,这为我们提供了一个hook的方式,可以通过定义rn-cli.config.js文件,并在其中覆盖内置的方法,返回我们的自定义命令。才有这一方式,需要对hook的方法充分理解,避免产生意外的冲突。

基于react-native-cli已经可以满足定制化模板项目的脚手架需求,而create-react-native-app之所以能够流行起来,是因为其简化了react-native的环境配置。如果你的业务仅包含js代码,无需额外原生依赖,可以使用配套的expo直接扫描本地server生成的二维码来运行代码,同时,expo-sdk也为开发者提供了一套方便的组件。这种扫码运行简单代码,快速查看效果的方式,也可以集成到模板项目中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容