React Native自定义脚手架

1,413 阅读5分钟

一、react-native-cli 官方脚手架分析

1:nodejs加载文件并执行:

首先看个node加载其它文件代码并执行例子:

文件index.jsvar path = require('path');  //引入node路径库
var CLI_MODULE_PATH = function() {  //构建路径对象  当前命令行所在路径+"/run.js"
  return path.resolve(
    process.cwd(), 
    'run.js'
  );
};
var cliPath = CLI_MODULE_PATH(); //得到路径对象
var cli = require(cliPath);  //执行加载并返回
cli.default.run() //加载运行run.js文件,运行run.js文件里的run方法
----------------------------------------------------------------------------------------
文件run.js:
console.log("code load")
async function run()
{
    console.log("exec run function")
}
var _default = {
  run
};
exports.default = _default;

运行index.js:

image.png

2:react-native-cli源码解析(2.0.1版本):

React Native发布为两个npm包,分别是:react-native-cli、react-native。

1:react-native-cli需要被全局安装,作为脚手架在命令行工具中使用。react-native-cli比较轻量级,进入其目录主要能看到一个index.js代码文件,它的主要工作是将所有命令交给本地的react-native执行及创建项目;

2:react-native包含源码及模板项目等大部分功能; image.png

目录结构:

image

那当我们执行react-native init xxx命名创建项目时,代码的执行过程是怎样的呢? image.png

react-native-cli中创建项目的主要函数执行过程(RN项目启动服务过程原理类似): image.png

react-native-cli源码:

`#!/usr/bin/env node`

`'use strict'``;`

`//require导入nodejs相关功能模块`

`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'``); ``//semver可以作为一个node模块,同时也可以作为一个命令行工具。功能包括:比较两个版本号的大小、验证某个版本号是否合法、提取版本号,例如从“=v1.2.1”体取出"1.2.1"、分析版本号是否属于某个范围或符合一系列条件`

`var options = require(``'minimist'``)(process.argv.slice(``2``)); ``//命令行参数解析工具,如react-native -v,可以解析到v参数`

`//得到路径操作对象,为当前目录/node_modules/react-native/cli.js`

`var CLI_MODULE_PATH = function() {`

`return` `path.resolve(`

`process.cwd(),`

`'node_modules'``,`

`'react-native'``,`

`'cli.js'`

`);`

`};`

`//得到路径操作对象,为当前目录/node_modules/react-native/package.json`

`var REACT_NATIVE_PACKAGE_JSON_PATH = function() {`

`return` `path.resolve(`

`process.cwd(),`

`'node_modules'``,`

`'react-native'``,`

`'package.json'`

`);`

`};`

`//获取命令后面带“-”的参数,如执行react-native -v、react-native -version 会进入到if里面`

`if` `(options._.length === ``0` `&& (options.v || options.version)) {`

`printVersionsAndExit(REACT_NATIVE_PACKAGE_JSON_PATH());`

`}`

`//获取yarn版本号,如果有安装的话,创建项目时调用的方法`

`function getYarnVersionIfAvailable() {`

`var yarnVersion;`

`try` `{`

`// execSync returns a Buffer -> convert to string`

`if` `(process.platform.startsWith(``'win'``)) {  ``//执行yarn --version获取到yarn版本号`

`yarnVersion = (execSync(``'yarn --version'``).toString() || ``''``).trim();`

`} ``else` `{`

`yarnVersion = (execSync(``'yarn --version 2>/dev/null'``).toString() || ``''``).trim();`

`}`

`} ``catch` `(error) {`

`return` `null``;`

`}`

`// yarn < 0.16 has a 'missing manifest' bug`

`try` `{`

`if` `(semver.gte(yarnVersion, ``'0.16.0'``)) { ``//如果当前yarn版本号比0.16.0大,则返回当前版本号`

`return` `yarnVersion;`

`} ``else` `{`

`return` `null``;`

`}`

`} ``catch` `(error) {`

`console.error(``'Cannot parse yarn version: '` `+ yarnVersion);`

`return` `null``;`

`}`

`}`

`var cli;`

`var cliPath = CLI_MODULE_PATH();  ``//得到路径操作对象,为当前目录/node_modules/react-native/cli.js`

`if` `(fs.existsSync(cliPath)) {`

`cli = require(cliPath); ``//如果该文件存在,则得到执行cli.js的对象`

`}`

`var commands = options._; ``//获取参数,如react-native init,则可以通过commands对象获取到init,如果参数带“-”,如react-native -init,则commands长度为0`

`if` `(cli) {`

`cli.run(); ``//调用该文件中的run方法,具体机制可以参考上面的"nodejs加载文件并执行"`

`} ``else` `{`

`if` `(options._.length === ``0` `&& (options.h || options.help)) { ``//当执行该文件时如果参数带有 -h、-help时会进入到if里面,打印出相关帮助`

`console.log([`

`''``,`

`'  Usage: react-native [command] [options]'``,`

`''``,`

`''``,`

`'  Commands:'``,`

`''``,`

`'    init <ProjectName> [options]  generates a new project and installs its dependencies'``,`

`''``,`

`'  Options:'``,`

`''``,`

`'    -h, --help    output usage information'``,`

`'    -v, --version output the version number'``,`

`''``,`

`].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'``:  ``//执行react-native init时`

`if` `(!commands[``1``]) {`

`console.error(`

`'Usage: react-native init <ProjectName> [--verbose]'`

`);`

`process.exit(``1``);`

`} ``else` `{`

`init(commands[``1``], options); ``//react-native项目创建`

`}`

`break``;`

`default``: ``//执行非react-native init时`

`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``;`

`}`

`}`

`//校验项目名称是否合法`

`function validateProjectName(name) {`

`if` `(!name.match(/^[$A-Z_][``0``-9A-Z_$]*$/i)) {`

`console.error(`

`'"%s" is not a valid name for a project. Please use a valid identifier '` `+`

`'name (alphanumeric).'``,`

`name`

`);`

`process.exit(``1``);`

`}`

`if` `(name === ``'React'``) {`

`console.error(`

`'"%s" is not a valid name for a project. Please do not use the '` `+`

`'reserved word "React".'``,`

`name`

`);`

`process.exit(``1``);`

`}`

`}`

`//创建项目,决定用哪个方法创建`

`function init(name, options) {`

`validateProjectName(name);`

`if` `(fs.existsSync(name)) {`

`createAfterConfirmation(name, options);`

`} ``else` `{`

`createProject(name, options);`

`}`

`}`

`//创建项目时如果项目已经存在,则先进行提示`

`function createAfterConfirmation(name, options) {`

`prompt.start();`

`var property = {`

`name: ``'yesno'``,`

`message: ``'Directory '` `+ name + ``' already exists. Continue?'``,`

`validator: /y[es]*|n[o]?/,`

`warning: ``'Must respond yes or no'``,`

`default``: ``'no'`

`};`

`prompt.get(property, function (err, result) {`

`if` `(result.yesno[``0``] === ``'y'``) {`

`createProject(name, options);`

`} ``else` `{`

`console.log(``'Project initialization canceled'``);`

`process.exit();`

`}`

`});`

`}`

`//进行项目创建`

`function createProject(name, options) {`

`var root = path.resolve(name);`

`var projectName = path.basename(root);`

`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'`

`}`

`};`

`fs.writeFileSync(path.join(root, ``'package.json'``), JSON.stringify(packageJson));`

`process.chdir(root);`

`run(root, projectName, options);`

`}`

`//获取需要安装的包,如果有传版本号则会得到安装指定版本号`

`function getInstallPackage(rnPackage) {`

`var packageToInstall = ``'react-native'``;`

`var isValidSemver = semver.valid(rnPackage); ``//如果执行的这个命令react-native init TestPrj --version 0.60.1,则isValidSemver 得到的是0.60.1`

`if` `(isValidSemver) {`

`packageToInstall += ``'@'` `+ isValidSemver;`

`} ``else` `if` `(rnPackage) {`

`// for tar.gz or alternative paths`

`packageToInstall = rnPackage;`

`}`

`return` `packageToInstall;`

`}`

`//创建项目时运行的函数`

`function run(root, projectName, options) {`

`// E.g. '0.38' or '/path/to/archive.tgz'`

`const` `rnPackage = options.version;`

`const` `forceNpmClient = options.npm;`

`const` `yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable();`

`var installCommand;`

`if` `(options.installCommand) {`

`// In CI environments it can be useful to provide a custom command,`

`// to set up and use an offline mirror for installing dependencies, for example.`

`installCommand = options.installCommand;`

`} ``else` `{`

`if` `(yarnVersion) {`

`console.log(``'Using yarn v'` `+ yarnVersion);`

`console.log(``'Installing '` `+ getInstallPackage(rnPackage) + ``'...'``);`

`installCommand = ``'yarn add '` `+ getInstallPackage(rnPackage) + ``' --exact'``;`

`if` `(options.verbose) {`

`installCommand += ``' --verbose'``;`

`}`

`} ``else` `{`

`console.log(``'Installing '` `+ getInstallPackage(rnPackage) + ``'...'``);`

`if` `(!forceNpmClient) {`

`console.log(``'Consider installing yarn to make this faster: [https://yarnpkg.com](https://yarnpkg.com/)'``);`

`}`

`installCommand = ``'npm install --save --save-exact '` `+ getInstallPackage(rnPackage);`

`if` `(options.verbose) {`

`installCommand += ``' --verbose'``;`

`}`

`}`

`}`

`try` `{`

`execSync(installCommand, {stdio: ``'inherit'``}); ``//会执行yarn add react-native --exact`

`} ``catch` `(err) {`

`console.error(err);`

`console.error(``'Command `'` `+ installCommand + ``'` failed.'``);`

`process.exit(``1``);`

`}`

`checkNodeVersion();`

`cli = require(CLI_MODULE_PATH());`

`cli.init(root, projectName);`

`}`

`//检查node版本`

`function checkNodeVersion() {`

`var packageJson = require(REACT_NATIVE_PACKAGE_JSON_PATH());`

`if` `(!packageJson.engines || !packageJson.engines.node) {`

`return``;`

`}`

`if` `(!semver.satisfies(process.version, packageJson.engines.node)) {`

`console.error(chalk.red(`

`'You are currently running Node %s but React Native requires %s. '` `+`

`'Please use a supported version of Node.\n'` `+`

`'See [https://facebook.github.io/react-native/docs/getting-started.html'](https://facebook.github.io/react-native/docs/getting-started.html')`

`),`

`process.version,`

`packageJson.engines.node);`

`}`

`}`

`//打印版本并退出`

`function printVersionsAndExit(reactNativePackageJsonPath) {`

`console.log(``'react-native-cli: '` `+ require(``'./package.json'``).version);`

`try` `{`

`console.log(``'react-native: '` `+ require(reactNativePackageJsonPath).version);`

`} ``catch` `(e) {`

`console.log(``'react-native: n/a - not inside a React Native project directory'``);`

`}`

`process.exit();`

`}`

二、自定义脚手架

上面提到,React-native发布有两个包。一个脚手架,一个react-native包里包含源码模板等。同理,我们需要自定义脚手架的话同样需要两个包。我这里命名为rn-cli(脚手架)、rn-template(包含项目模板)两个包

rn-cli(脚手架)

1:创建目录rn-cli;

2:进入目录执行npm init;

3:在package.json dependencies中添加nodejs相关依赖

{
  "name": "rn-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "chalk": "^1.1.1",
    "minimist": "^1.2.0",
    "prompt": "^0.2.14",
    "semver": "^5.0.3"
  },
  "bin": {
    "rn-cli": "index.js"
  },
  "author": "",
  "license": "ISC"
}

4:将react-native-cli中index.js拷贝到rn-cli中,并作相应修改

修改CLI_MODULE_PATH 修改run方法,xxx image.png

rn-template

1:创建目录rn-template;

2:进入目录执行npm init;

3:在package.json dependencies中添加nodejs相关依赖

{
  "name": "rn-telmpate",
  "version": "1.0.0",
  "description": "",
  "main": "cli.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "chalk": "^1.1.1",
    "minimist": "^1.2.0",
    "prompt": "^0.2.14",
    "semver": "^5.0.3"
  },
  "author": "",
  "license": "ISC"
}

4:再一个已创建的RN项目中node_module/react-native中将创建项目的文件拷贝移植过来,并做简单修改。

image.png

5:将该包发布到npm仓库

大致流程:

image.png

注:开发调试rn-template过程中,可以先在rn-cli目录下通过命令node index.js执行创建一个项目,然后将rn-template代码放到创建的项目目录node_module目录下,即可进行调试开发。