React脚手架工具创建项目的详细介绍
Posted 学全栈的灌汤包
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React脚手架工具创建项目的详细介绍相关的知识,希望对你有一定的参考价值。
文章目录
React脚手架工具
脚手架工具解析
如果我们只是开发几个小的demo程序,那么永远不需要考虑一些复杂的问题:
比如目录结构如何组织划分;
比如如何管理文件之间的相互依赖;
比如如何管理第三方模块的依赖;
比如项目发布前如何压缩、打包项目;
等等…
但是现代的前端项目已经越来越复杂了:
不会再是在html中引入几个css文件,引入几个编写的js文件或者第三方的js文件这么简单;
比如css可能是使用less、sass等预处理器进行编写,我们需要将它们转成普通的css才能被浏览器解析;
比如javascript代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖;
比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等);
为了解决上面这些问题,我们需要再去学习一些工具:
比如babel、webpack、gulp,配置它们转换规则、打包依赖、热更新等等一些的内容;
脚手架的出现,就是帮助我们解决这一系列问题的;
编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构;
每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;
既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板;
不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可;
这样也可以间接保证项目的基本机构一致性,方便后期的维护;
小结: 脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;
create-react-app
对于现在比较流行的三大框架都有属于自己的脚手架:
Vue的脚手架:@vue/cli
Angular的脚手架:@angular/cli
React的脚手架:
create-react-app
它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好。
使用这些脚手架需要依赖什么呢?
目前这些脚手架都是使用node编写的,并且都是基于webpack的;
所以我们必须在自己的电脑上安装node环境;
这里我们主要是学习React,所以我们以React的脚手架工具:create-react-app作为讲解;
React脚手架本身需要依赖node,所以我们需要安装node环境:
无论是windows还是Mac OS,都可以通过node官网直接下载;
官网地址:https://nodejs.org/en/download/
注意:这里推荐大家下载LTS(Long-term support )版本,是长期支持版本,会比较稳定;
下载后,双击安装即可:
安装过程中,会自动配置环境变量;
安装时,会同时帮助我们安装npm管理工具;
创建React项目
我们需要安装React脚手架工具, 全局安装npm i create-react-app -g
安装完成后, 我们就可以通过脚手架来创建React项目了
创建React项目的命令: create-react-app 项目名称
注意:项目名称不能包含大写字母
另外还有更多创建项目的方式,可以参考GitHub的readme
创建完成后,进入对应的目录,就可以将项目跑起来:
React早期使用的包管理工具yarn, 输入
yarn start
将项目跑起来;目前React创建的项目是使用的npm包管理工具, 输入
npm run start
可以将项目跑起来
目录的结构分析
整个目录结构都非常好理解,只是有一个PWA相关的概念:
PWA全称Progressive Web App,即渐进式WEB应用;
一个 PWA 应用首先是
一个网页
, 可以通过 Web 技术编写出一个网页应用;随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能;
这种Web存在的形式,我们也称之为是
Web App
;
PWA解决了哪些问题呢?
可以将PWA的网站像应用程序一样添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
实现了消息推送;
等等一系列类似于Native App相关的功能;
更多PWA相关的知识,可以自行去学习更多(在公司是基本用不到PWA的, 了解即可);
链接: https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps
从零编写代码
通过脚手架创建完项目,很多人还是会感觉目录结构过于复杂,所以我打算从零带着大家来编写代码。
我们先将不需要的文件统统删掉:
将public文件下, 除favicon.ico和index.html之外的文件都删除掉
src文件夹是我们编写源代码的地方, 默认创建项目在src文件夹中生成的文件全部都是可以删除的,
删除完成我们需要创建一个index.js文件, 因为这是webpack打包的入口文件, 在index.js中开始编写React代码:
我们会发现和写的代码是逻辑是一致的;
只是在模块化开发中,我们需要手动的来导入React、ReactDOM,因为它们都是在我们安装的模块中;
import ReactDOM from "react-dom/client"
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<h2>哈哈哈哈</h2>)
**root.render
是可以渲染组件的, 我们可以编写一个组件通过render渲染 **
import React from "react"
import ReactDOM from "react-dom/client"
class App extends React.Component
constructor()
super()
this.state =
message: "Hello React"
render()
const message = this.state
return (
<div>
<h2>message</h2>
</div>
)
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
如果我们不希望直接在 root.render
中编写过多的代码,就可以单独抽取一个组件到App.js
文件或者App.jsx
文件:
// App.js
import React from "react"
class App extends React.Component
constructor()
super()
this.state =
message: "Hello React"
render()
const message = this.state
return (
<div>
<h2>message</h2>
</div>
)
export default App
// index.js
import ReactDOM from "react-dom/client"
import App from "./App"
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
零Node基础看懂React-Native脚手架工具
做过RN开发的同学肯定对react-native-cli命令行工具不陌生,官方文档一开始在搭建开发环境的章节就会介绍到利用这个工具快速创建一个新项目。刚开始接触RN开发的你,一定会对这个命令行工具感到好奇,它是如何快速的创建一个RN项目的呢,又是如何完成一系列的工程目录结构创建,配置信息添加,各种依赖安装的呢,本文就带你来一探究竟。
react-native-cli是Facebook开源项目ReactNative自带的一个脚手架工具,可以很方便帮助开发者快速的从0开始创建一个完整的RN项目。react-native-cli其实是一个node项目,你可能没有接触过node开发,就像作者一样,是从原生转RN开发的,别担心,没有node基础,一样可以看懂,下面我们就正式开始吧。
这里简单描述一下本地搭建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究竟替我们做了哪些工作,又是如何完成的。
其实整个react-native-cli命令行工具包括两部分
•react-native-cli包内部分:主要完成React-Native引擎的下载安装
•React-Native内的node_modules/@react-native-community/cli:主要负责RN工程的创建,初始化,依赖安装等
具体这两部分是如何工作的呢,不要着急,下面我们一步一步的拆解并跟踪整个工程创建过程,看完你就明白了。
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
我们打开这个目录看看:
原来是一个替身,让他现行吧,右键显示原身:
如图所示,最终指向的是react-native-cli模块中的index.js文件。可以看出react-native-cli其实就是一个node.js项目,运行在node上。
看到这里可能有些同学有点担心了,“我不懂node呀,是个node小白,进行不下去了”。别担心,笔者对node的理解也只是停留在使用node模块层面,也没有过node模块的开发经验。
废话不多说,虽然没开发过node项目,但是RN项目还是有些经验的,不都是JS的项目嘛,结合以往RN项目的开发经验,让我们大胆的用webstorm打开这个node工程看看。
2)剖析真身(react-native-cli部分)
打开工程,我们发现,这个node工程还是比较简单的:
除了node_modules文件夹,只有一个代码文件:index.js,这个文件既是入口文件又是全部功能实现逻辑。下面我们来分析一下这个index.js文件。
分析代码最好的方式就是调试、跟踪代码执行的每一个步骤。笔者使用的是webstorm,将react-native-cli文件夹作为node工程打开后,添加调试配置。添加->选择Node.js模板->配置Configuration标签下的参数,具体如下:
•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就可以一步一步跟踪调试了。
可以看到,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 <ProjectName> [--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:
然后进入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(同时包括相关依赖包):
执行结果如上图所示,可以看到新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的工程吧。
首先,我们用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作为入口文件直接运行的话,不会调用主体方法,无法执行后续的创建任务)
1)添加入口文件
关键的地方来了,总结一下截止到目前我们的源码深入和调试的经过(例如react-native-cli这个node工程的目录结构和我们的调试过程),结合以往的开发经验,我们是不是可以大胆的猜想或者说推测一下:如果我们给这个React-Native local-cli的node项目也加一个入口文件呢?
说干就干,下面我们创建一个index.js的文件,作为React-Native local-cli的node项目的入口文件。index.js文件内容是什么呢,很显然,我们又想起了之前注释掉的那行代码,我们把之前那行调用的代码放在这里,是不是整个流程一下就通了,豁然开朗,完美!所以最终我们加入的index.js就是这样:
//本文作者注:开头部分,为啥这样写,直接从react-nativ-cli工程的index.js拷贝过来的(其实是指定运行环境,这里是shell-node)
;
var cli;
cli = require('./cli'); //本文作者注:对应之前的 ‘cli = require(CLI_MODULE_PATH());’
cli.init('/Users/dingxin/Documents/workspace/Node/TestRNProject', 'TestRNProject'); //本文作者注:对应之前的‘cli.init(root, projectName);’
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启动的入口文件,(相对于当前工程目录的相对路径)
为什么这里我们不能像react-native-cli工程调试配置的时候一样通过Application parameters 参数来传入参数呢?当然也可以,我们可以在index.js文件中通过var options = require('minimist')(process.argv.slice(2));来读取参数输入参数,然后传给cli.init调用。这里我们仅是为了测试验证问题,简单起见,我们直接写死就好了,正常开发中肯定是要用这种传参的形式。
3)运行、调试
一切准备就绪,我们的猜想是不是正确呢,终究要实际运行验证一下。我们直接点运行,可以看到:
成功了,看日志输出应该是成功了。这时候查看刚才的新RN工程目录:
对比之前的目录,明显多了许多文件、文件夹,实际上这就是react-native脚手架替我们创建的新RN工程的最终样子。
等等,这样就结束了吗,好像少点什么。当然,本文的重点是探究RN脚手架究竟做了什么,光看到结果显然不是我们想要的。下面我们就一步一步深入看看。
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第三部分:这里文本文件的处理是重点,其实现方法是先把文本文件读成一个字符串,然后查找替换字符串中的模板名称,替换为我们设置在模板工程的新名称。
至此,工程目录结构、文件(文件夹)的创建工作完成。
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 install 或 yarn add等npm或yarn相关命令的调用封装。
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的安装状态,版本校验等。具体实现不是本文重点,感兴趣的可以自行查看源码。
7)React-Native Local-Cli回顾
可以看到,第二部分(React-Native Local-Cli部分)的创建过程实际包括了如下几个步骤:
•创建工程结构:通过_templates.createProjectFromTemplate方法根据RN包内置的模板工程,创建用户的新RN工程结构
•安装node依赖包
–安装react库
–安装开发依赖库:babel、eslint等
•安装iOS工程依赖库:通过_installPods.default安装iOS工程的依赖pods,最终会生成iOS的workspace
至此,我们终于搞明白了整个脚手架的原理和执行过程,实际包括了两大部分,每个部分的具体步骤总结如下:
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依赖)
本文带领大家以node.js小白的身份从头到尾捋了一遍react-native脚手架的执行流程和原理,文中介绍的一些调试技巧和方法希望对大家有所帮助或启发。特别是在面对一些新技术或者不熟悉的领域时,我们一定要多思考,充分利用已有的一些知识、经验,联想、推测和大胆假设,一步一步验证尝试,最终肯定会越来越接近事实的真相。
以上是关于React脚手架工具创建项目的详细介绍的主要内容,如果未能解决你的问题,请参考以下文章