React Native自定义脚手架
一、react-native-cli 官方脚手架分析
1:nodejs加载文件并执行:
首先看个node加载其它文件代码并执行例子:
文件index.js:
var 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
那当我们执行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目录下,即可进行调试开发。