react
开发工具,还在测试阶段,之后完成了,喜欢的可以用一用哦 qz-tools
之前写了几篇关于搭建 react
环境的文,一直还没有完善它,这次撸完这波源码在重新完善之前的从零搭建完美的 react
开发打包测试环境。
如果后续有更正或者更新的地方,会在顶部加以说明。
前言
这段时间公司的事情变得比较少,空下了很多时间,作为一个刚刚毕业初入职场的菜鸟级程序员,一点都不敢放松,秉持着我为人人的思想也想为开源社区做点小小的贡献,但是一直又没有什么明确的目标,最近在努力的准备吃透 react
,加上 react
的脚手架工具 create-react-app
已经很成熟了,初始化一个 react
项目根本看不到它到底是怎么给我搭建的这个开发环境,又是怎么做到的,我还是想知道知道,所以就把他拖出来溜溜,顺便构建了我自己的开发工具 qz-tools
。
文中若有错误或者需要指正的地方,多多指教,共同进步。
目录分析
随着它版本的迭代,源码肯定是会发生变化的,我这里下载的是 v1.1.0
,大家可以自行在 github
上下载这个版本,找不到的 戳链接
。
主要说明
我们来看一下它的目录结构
├── .github ├── packages ├── tasks ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .yarnrc ├── appveyor.cleanup-cache.txt ├── appveyor.yml ├── CHANGELOG-0.x.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── lerna.json ├── LICENSE ├── package.json ├── README.md └── screencast.svg
咋一看好多啊,我的天啊,到底要怎么看,其实仔细一晃,好像很多一眼就能看出来是什么意思,大概说一下每个文件都是干嘛的,具体的我也不知道啊,往下看,一步一步来。
-
.github:这里面放着当你在这个项目提issue和pr时候的规范 -
packages:字面意思就是包们.....暂时不管,后面详说 ----> 重点 -
tasks:字面意思就是任务们.....暂时不管,后面详说 ----> 重点 -
.eslintignore:eslint检查时忽略文件 -
.eslintrc:eslint检查配置文件 -
.gitignore:git提交时忽略文件 -
.travis.yml:travis配置文件 -
.yarnrc:yarn配置文件 -
appveyor.cleanup-cache.txt:里面有一行Edit this file to trigger a cache rebuild编辑此文件触发缓存,具体干嘛的,暂时不议 -
appveyor.yml:appveyor配置文件 -
CHANGELOG-0.x.md:版本0.X开头的变更说明文件 -
CHANGELOG.md:当前版本变更说明文件 -
CODE_OF_CONDUCT.md:facebook代码行为准则说明 -
CONTRIBUTING.md:项目的核心说明 -
lerna.json:lerna配置文件 -
LICENSE:开源协议 -
package.json:项目配置文件 -
README.md:项目使用说明 -
screencast.svg:图片...
看了这么多文件,是不是打退堂鼓了?哈哈哈哈,好了好了,进入正题,其实上述对于我们阅读源码有用的只有 packages
、 tasks
、 package.json
三个文件而已,是不是想打我.....我也只是想告诉大家这些文件有什么用,它们都是有各自的作用的,如果还不了解,参考下面的参考链接。
参考链接
eslint
相关的: eslint官网
yarn
相关的: yarn官网
appveyor
相关的: appveyor官网
lerna
相关的: lerna官网
工具自行了解,本文只说源码相关的 packages
、 tasks
、 package.json
。
寻找入口
现在的前端项目大多数都有很多别的依赖,不在像以前那些原生 javascript
的工具库,拿到源码文件,就可以开始看了,像 jQuery
、 underscore
等等,一个两个文件包含了它所有的内容,虽然也有很框架会有 umd
规范的文件可以直接阅读,像 better-scroll
等等,但是其实他的源码还是拆分了很多的,只是在最后整合在一起了。但是像 create-react-app
这样的脚手架工具好像不能像之前那种方法来看了,必须找到整个程序的入口,在逐步突破,所以最开始的工具肯定是寻找入口。
开始关注
拿到一个项目我们应该从哪个文件开始看起呢?只要是基于 npm
管理的,我都推荐从 package.json
文件开始看,人家是项目的介绍文件,你不看它看啥。
它里面理论上应该是有名称、版本等等一些说明性信息,但是都没用,看几个重要的配置。
"workspaces": [
"packages/*"
],
关于 workspaces
一开始我在 npm
的说明文档里面没找到,虽然从字面意思我们也能猜到它的意思是实际工作的目录是 packages
,后来我查了一下是 yarn
里面的东东,具体看 这篇文章
,用于在本地测试,具体不关注,只是从这里我们知道了真正的起作用的文件都在 packages
里面。
重点关注
从上述我们知道现在真正需要关注的内容都在 packages
里面,我们来看看它里面都是有什么东东:
├── babel-preset-react-app ├── create-react-app ├── eslint-config-react-app ├── react-dev-utils ├── react-error-overlay └── react-scripts
里面有六个文件夹,哇塞,又是6个单独的项目,这要看到何年何月.....是不是有这种感触,放宽心大胆的看,先想一下我们在安装了 create-react-app
后在,在命令行输入的是 create-react-app
的命令,所以我们大胆的推测关于这个命令应该都是存在了 create-react-app
下,在这个目录下同样有 package.json
文件,现在我们把这6个文件拆分成6个项目来分析,上面也说了,看一个项目首先看 package.json
文件,找到其中的重点:
"bin": {
"create-react-app": "./index.js"
}
找到重点了, package.json
文件中的 bin
就是在命令行中可以运行的命令,也就是说我们在执行 create-react-app
命令的时候,就是执行 create-react-app
目录下的 index.js
文件。
多说两句
关于 package.json
中的 bin
选项,其实是基于 node
环境运行之后的内容。举个简单的例子,在我们安装 create-react-app
后,执行 create-react-app
等价于执行 node index.js
。
create-react-app目录解析
经过以上一系列的查找,我们终于艰难的找到了 create-react-app
命令的中心入口,其他的都先不管,我们打开 packages/create-react-app
目录,仔细一瞅,噢哟,只有四个文件,四个文件我们还搞不定吗?除了 package.json
、 README.md
就只剩两个能看的文件了,我们来看看这两个文件。
index.js
既然之前已经看到 packages/create-react-app/package.json
中关于 bin
的设置,就是执行 index.js
文件,我们就从 index.js
入手,开始瞅瞅源码到底都有些虾米。
除了一大串的注释以外,代码其实很少,全贴上来了:
var chalk = require('chalk');
var currentNodeVersion = process.versions.node; // 返回Node版本信息,如果有多个版本返回多个版本
var semver = currentNodeVersion.split('.'); // 所有Node版本的集合
var major = semver[0]; // 取出第一个Node版本信息
// 如果当前版本小于4就打印以下信息并终止进程
if (major < 4) {
console.error(
chalk.red(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 4 or higher. \n' +
'Please update your version of Node.'
)
);
process.exit(1); // 终止进程
}
// 没有小于4就引入以下文件继续执行
require('./createReactApp');
咋一眼看过去其实你就知道它大概是什么意思了....检查 Node.js
的版本,小于 4
就不执行了,我们分开来看一下,这里他用了一个库 chalk
,理解起来并不复杂,一行一行的解析。
-
chalk:这个对这段代码的实际影响就是在命令行中,将输出的信息变色。也就引出了这个库的作用改变命令行中输出信息的样式。 npm地址
其中有几个 Node
自身的 API
:
-
process.versions返回一个对象,包含Node以及它的依赖信息 -
process.exit结束Node进程,1是状态码,表示有异常没有处理
在我们经过 index.js
后,就来到了 createReactApp.js
,下面再继续看。
createReactApp.js
当我们本机上的 Node
版本大于 4
的时候就要继续执行这个文件了,打开这个文件,代码还不少,大概 700
多行吧,我们慢慢拆解。
这里放个小技巧,在读源码的时候,可以在开一个写代码的窗口,跟着写一遍,执行过的代码可以在源文件中先删除,这样 700行
代码,当你读了 200行
的时候,源文件就只剩 500行
了,不仅有成就感继续阅读,也把不执行的逻辑先删除了,影响不到你读其他地方。
const validateProjectName = require('validate-npm-package-name');
const chalk = require('chalk');
const commander = require('commander');
const fs = require('fs-extra');
const path = require('path');
const execSync = require('child_process').execSync;
const spawn = require('cross-spawn');
const semver = require('semver');
const dns = require('dns');
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
const hyperquest = require('hyperquest');
const envinfo = require('envinfo');
const packageJson = require('./package.json');
打开代码一排依赖,懵逼....我不可能挨着去查一个个依赖是用来干嘛的吧?所以,我的建议就是先不管,用到的时候在回来看它是干嘛的,理解更加透彻一些,继续往下看。
const program = new commander.Command(packageJson.name)
.version(packageJson.version) // 输入版本信息,使用`create-react-app -v`的时候就用打印版本信息
.arguments('<project-directory>') // 使用`create-react-app <la>` 尖括号中的参数
.usage(`${chalk.green('<project-directory>')} [options]`) // 使用`create-react-app`第一行打印的信息
.action(name => {
projectName = name; // 此处action函数的参数就是之前argument中的<project-directory> 初始化项目名称 --> 此处影响后面
})
.option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的选项,类似 --help -V
.option('--info', 'print environment debug info') //
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)
.option('--use-npm')
.allowUnknownOption() // 这个我没有在文档上查到,直译就是允许无效的option 大概意思就是我可以这样`create-react-app <my-project> -la` 其实 -la 并没有定义,但是我还是可以这么做而不会保存
.on('--help', () => {
// 此处省略了一些打印信息
}) // on('--help') 用来定制打印帮助信息 当使用`create-react-app -h(or --help)`的时候就会执行其中的代码,基本都是些打印信息
.parse(process.argv); // 这个就是解析我们正常的`Node`进程,可以这么理解没有这个东东,`commander`就不能接管`Node`
在上面的代码中,我把无关紧要打印信息省略了,这段代码算是这个文件的关键入口地此处他 new
了一个 commander
,这是个啥东东呢?这时我们就返回去看它的依赖,找到它是一个外部依赖,这时候怎么办呢?不可能打开 node_modules
去里面找撒,很简单,打开 npm
官网查一下这个外部依赖。
-
commander:概述一下,Node命令接口,也就是可以用它代管Node命令。 npm地址
上述只是 commander
用法的一种实现,没有什么具体好说的,了解了 commander
就不难,我们继续往下来。
// 判断在命令行中执行`create-react-app <name>` 有没有name,如果没有就继续
if (typeof projectName === 'undefined') {
// 当没有传name的时候,如果带了 --info 的选项继续执行下列代码
if (program.info) {
// 打印当前环境信息和`react`、`react-dom`, `react-scripts`三个包的信息
envinfo.print({
packages: ['react', 'react-dom', 'react-scripts'],
noNativeIDE: true,
duplicates: true,
});
process.exit(0); // 正常退出进程
}
// 在没有带项目名称又没带 --info 选项的时候就会打印一堆错误信息
console.error('Please specify the project directory:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
);
console.log();
console.log('For example:');
console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`);
console.log();
console.log(
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
);
process.exit(1); // 抛出异常退出进程
}
还记得上面把 create-react-app <my-project>
中的项目名称赋予了 projectName
变量吗?此处的作用就是看看用户有没有传这个 <my-project>
参数,如果没有就会报错,并显示一些帮助信息,这里用到了另外一个外部依赖 envinfo
。
-
envinfo:可以打印当前操作系统的环境和指定包的信息。 npm地址
到这里我还要吐槽一下 segmentfault
的编辑器...我同时打开视图和编辑好卡...捂脸。png
我们继续往下看,有几个提前定义的函数,我们先不管,先找第一个执行的函数:
createApp( projectName, program.verbose, program.scriptsVersion, program.useNpm, hiddenProgram.internalTestingTemplate );
一个 createAPP
函数,接收了5个参数
-
projectName: 执行create-react-app <name>name的值,也就是初始化项目的名称 -
program.verbose:这里在说一下commander的option选项,如果加了这个选项这个值就是true,否则就是false,也就是说这里如果加了--verbose,那这个参数就是true,至于verbose是什么,我们在后面继续看 -
program.scriptsVersion:与上述同理 -
program.useNpm:以上述同理 -
hiddenProgram.internalTestingTemplate:这个东东,其实前面我省略了一个函数,关于hiddenProgram,其实就是选择初始化的模板,默认是一个普通的项目,还可以是个测试模板,这个函数简单,不多说。
找到了第一个执行的函数 createApp
,我们就来看看 createApp
函数到底做了什么?
createApp()
function createApp(name, verbose, version, useNpm, template) {
const root = path.resolve(name); // 获取当前进程运行的位置,也就是文件目录的绝对路径
const appName = path.basename(root); // 返回root路径下最后一部分
checkAppName(appName); // 执行 checkAppName 函数 检查文件名是否合法
fs.ensureDirSync(name); // 此处 ensureDirSync 方法是外部依赖包 fs-extra 而不是 node本身的fs模块,作用是创建目录
// isSafeToCreateProjectIn 函数 判断文件夹是否安全
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1); // 不合法结束进程
}
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
// 定义package.json中内容
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
// 往我们创建的文件夹中写入package.json文件
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
// 定义常量 useYarn 如果传参有 --use-npm useYarn就是false,否则执行 shouldUseYarn() 检查yarn是否存在
const useYarn = useNpm ? false : shouldUseYarn();
// 取得当前node进程的目录
const originalDirectory = process.cwd();
// 修改进程目录为底下子进程目录
process.chdir(root);
// 如果不使用yarn 并且checkThatNpmCanReadCwd()函数 检查到npm也不存在,就结束进程
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
// 比较 node 版本,小于6的时候发出警告
if (!semver.satisfies(process.version, '>=6.0.0')) {
console.log(
chalk.yellow(
`You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to Node 6 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = 'react-scripts@0.9.x';
}
// 如果没有使用yarn 也发出警告
if (!useYarn) {
const npmInfo = checkNpmVersion();
if (!npmInfo.hasMinNpm) {
if (npmInfo.npmVersion) {
console.log(
chalk.yellow(
`You are using npm ${npmInfo.npmVersion} so the project will be boostrapped with an old unsupported version of tools.\n\n` +
`Please update to npm 3 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = 'react-scripts@0.9.x';
}
}
// 传入这些参数执行run函数
run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
我们来分析一下这个函数,就是对各个参数进行了校验,在校验结束后,又执行了 run
函数,具体看每行解析,我们来看看这个函数用了哪些外部依赖:
函数依赖又有哪些:
-
checkAppName():用于检测文件名是否合法,后面解析 -
isSafeToCreateProjectIn():用于检测文件夹是否安全,后面解析 -
shouldUseYarn():用于检测yarn是否存在,后面解析
其中有两个外部依赖,分别在这段函数中的作用是创建文件夹,写入文件和比较 node
版本,来解析一下上述用到的函数。
checkAppName()
function checkAppName(appName) {
const validationResult = validateProjectName(appName); // 使用 validateProjectName 检查包名是否合法返回结果
// 如果对象中有错继续
if (!validationResult.validForNewPackages) {
console.error(
`Could not create a project called ${chalk.red(
`"${appName}"`
)} because of npm naming restrictions:`
);
printValidationResults(validationResult.errors);
printValidationResults(validationResult.warnings);
process.exit(1);
}
// TODO: there should be a single place that holds the dependencies
const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
if (dependencies.indexOf(appName) >= 0) {
console.error(
chalk.red(
`We cannot create a project called ${chalk.green(
appName
)} because a dependency with the same name exists.\n` +
`Due to the way npm works, the following names are not allowed:\n\n`
) +
chalk.cyan(dependencies.map(depName => ` ${depName}`).join('\n')) +
chalk.red('\n\nPlease choose a different project name.')
);
process.exit(1);
}
}
它这个函数其实还蛮简单的,用了一个外部依赖来校验文件名是否符合 npm
包文件名的规范,然后定义了三个不能取得名字 react
、 react-dom
、 react-scripts
,外部依赖:
-
validate-npm-package-name:外部依赖,检查包名是否合法。 npm地址
其中的函数依赖:
-
printValidationResults():函数引用,这个函数我就不贴了,里面就是循环输出错误信息。
继续往下执行。
isSafeToCreateProjectIn()
function isSafeToCreateProjectIn(root, name) {
const validFiles = [
'.DS_Store',
'Thumbs.db',
'.git',
'.gitignore',
'.idea',
'README.md',
'LICENSE',
'web.iml',
'.hg',
'.hgignore',
'.hgcheck',
'.npmignore',
'mkdocs.yml',
'docs',
'.travis.yml',
'.gitlab-ci.yml',
'.gitattributes',
];
console.log();
const conflicts = fs
.readdirSync(root)
.filter(file => !validFiles.includes(file));
if (conflicts.length < 1) {
return true;
}
console.log(
`The directory ${chalk.green(name)} contains files that could conflict:`
);
console.log();
for (const file of conflicts) {
console.log(` ${file}`);
}
console.log();
console.log(
'Either try using a new directory name, or remove the files listed above.'
);
return false;
}
他这个函数也算比较简单,就是判断创建的这个目录是否包含除了上述 validFiles
数组中文件以外的文件,如果没有就是安全的,有就是不安全的,但是我确实没明白为什么除了这些文件以外就是不安全的....
shouldUseYarn()
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
就三行...其中 execSync
是由 node
自动模块 child_process
引用而来,就是用来执行命令的,这个函数就是执行一下 yarn --version
来判断我们是否安装了 yarn
。
checkNpmVersion()
function checkNpmVersion() {
let hasMinNpm = false;
let npmVersion = null;
try {
npmVersion = execSync('npm --version')
.toString()
.trim();
hasMinNpm = semver.gte(npmVersion, '3.0.0');
} catch (err) {
// ignore
}
return {
hasMinNpm: hasMinNpm,
npmVersion: npmVersion,
};
}
这里同上面一个函数,执行了一下 npm --version
并将这些信息存到了 npmVersion
变量里返回去,还用到了 semver
外部依赖, semver.gte()
这个方法就是比较其中两个参数,如果前大于后的版本,就返回 true
否则就返回 false
。
到这边 createApp()
函数的依赖和执行都撸完了,接着执行了 run()
函数,我们继续来看 run()
函数都是什么,我又想吐槽了,算了,忍住!!!
run()
函数在 createApp()
函数中检验...等等执行完毕后执行,它接收7个参数,先来看看。
-
root:我们创建的目录的绝对路径 -
appName:我们创建的目录名称 -
version;这个前面没说react-script的版本,他把这个控制react环境的代码存入了react-script包里面。 -
verbose:继续传入verbose,在create-react-app中没有使用,用到再说 -
originalDirectory:原始目录 -
tempalte:模板 -
useYarn:是否使用yarn
具体的来看下面 run()
函数。
run()
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn
) {
const packageToInstall = getInstallPackage(version, originalDirectory); // 获取依赖包信息
const allDependencies = ['react', 'react-dom', packageToInstall]; // 所有的开发依赖包
console.log('Installing packages. This might take a couple of minutes.');
getPackageName(packageToInstall) // 获取依赖包原始名称并返回
.then(packageName =>
// 检查是否离线模式,并返回结果和包名
checkIfOnline(useYarn).then(isOnline => ({
isOnline: isOnline,
packageName: packageName,
}))
)
.then(info => {
// 接收到上述的包名和是否为离线模式
const isOnline = info.isOnline;
const packageName = info.packageName;
console.log(
`Installing ${chalk.cyan('react')}, ${chalk.cyan(
'react-dom'
)}, and ${chalk.cyan(packageName)}...`
);
console.log();
// 安装依赖
return install(root, useYarn, allDependencies, verbose, isOnline).then(
() => packageName
);
})
.then(packageName => {
// 检查当前`Node`版本是否支持包
checkNodeVersion(packageName);
// 检查`package.json`的开发依赖是否正常
setCaretRangeForRuntimeDeps(packageName);
// `react-scripts`脚本的目录
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'scripts',
'init.js'
);
// 引入`init`函数
const init = require(scriptsPath);
// 执行目录的拷贝
init(root, appName, verbose, originalDirectory, template);
// 当`react-scripts`的版本为0.9.x发出警告
if (version === 'react-scripts@0.9.x') {
console.log(
chalk.yellow(
`\nNote: the project was boostrapped with an old unsupported version of tools.\n` +
`Please update to Node >=6 and npm >=3 to get supported tools in new projects.\n`
)
);
}
})
// 异常处理
.catch(reason => {
console.log();
console.log('Aborting installation.');
// 根据命令来判断具体的错误
if (reason.command) {
console.log(` ${chalk.cyan(reason.command)} has failed.`);
} else {
console.log(chalk.red('Unexpected error. Please report it as a bug:'));
console.log(reason);
}
console.log();
// 出现异常的时候将删除目录下的这些文件
const knownGeneratedFiles = [
'package.json',
'npm-debug.log',
'yarn-error.log',
'yarn-debug.log',
'node_modules',
];
// 挨着删除
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach(file => {
knownGeneratedFiles.forEach(fileToMatch => {
if (
(fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) ||
file === fileToMatch
) {
console.log(`Deleting generated file... ${chalk.cyan(file)}`);
fs.removeSync(path.join(root, file));
}
});
});
// 判断当前目录下是否还存在文件
const remainingFiles = fs.readdirSync(path.join(root));
if (!remainingFiles.length) {
console.log(
`Deleting ${chalk.cyan(`${appName} /`)} from ${chalk.cyan(
path.resolve(root, '..')
)}`
);
process.chdir(path.resolve(root, '..'));
fs.removeSync(path.join(root));
}
console.log('Done.');
process.exit(1);
});
}
他这里对 react-script
做了很多处理,大概是由于 react-script
本身是有 node
版本的依赖的,而且在用 create-react-app init <project>
初始化一个项目的时候,是可以指定 react-script
的版本或者是外部自身定义的东东。
他在 run()
函数中的引用都是用 Promise
回调的方式来完成的,从我正式接触 Node
开始就习惯用 async/await
,所以对 Promise
还真不熟,恶补了一番,下面我们来拆解其中的每一句和每一个函数的作用,先来看一下用到外部依赖还是之前那些不说了,来看看函数列表:
-
getInstallPackage():获取要安装的react-scripts版本或者开发者自己定义的react-scripts -
getPackageName():获取到正式的react-scripts的包名 -
checkIfOnline(): -
install(): -
checkNodeVersion(): -
setCaretRangeForRuntimeDeps(): -
init():
知道了个大概,我们在来逐一分析每个函数的作用:
getInstallPackage()
function getInstallPackage(version, originalDirectory) {
let packageToInstall = 'react-scripts'; // 定义常量 packageToInstall
const validSemver = semver.valid(version); // 校验版本号是否合法
if (validSemver) {
packageToInstall += `@${validSemver}`; // 合法的话执行,就安装指定版本,在`npm install`安装的时候指定版本为加上`@x.x.x`版本号
} else if (version && version.match(/^file:/)) {
// 不合法并且版本号参数带有`file:`执行以下代码,作用是指定版本为当前目录下的react-scripts 也就是我们自己定义的
packageToInstall = `file:${path.resolve(
originalDirectory,
version.match(/^file:(.*)?$/)[1]
)}`;
} else if (version) {
// 不合法并且没有`file:`开头,默认为`tar.gz`文件
// for tar.gz or alternative paths
packageToInstall = version;
}
// 返回最终需要安装的`react-scripts`的信息,或版本号或本地文件或线上`.tar.gz`资源
return packageToInstall;
}
这个方法接收两个参数 version
版本号, originalDirectory
原始目录,主要的作用是判断 react-scripts
应该安装的信息,具体看每一行。
这里 create-react-app
本身提供了安装 react-scripts
的三种机制,一开始初始化的项目是可以指定 react-scripts
的版本或者是自定义这个东西的,所以在这里他就提供了这几种机制,其中用到的外部依赖只有一个 semver
,之前就说过了,不多说。
getPackageName()
function getPackageName(installPackage) {
// 函数进来就根据上面的那个判断`react-scripts`的信息来安装这个包,返回包名
// 此处为线上`tar.gz`包的情况
if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
return getTemporaryDirectory()
.then(obj => {
let stream;
if (/^http/.test(installPackage)) {
stream = hyperquest(installPackage);
} else {
stream = fs.createReadStream(installPackage);
}
return extractStream(stream, obj.tmpdir).then(() => obj);
})
.then(obj => {
const packageName = require(path.join(obj.tmpdir, 'package.json')).name;
obj.cleanup();
return packageName;
})
.catch(err => {
console.log(
`Could not extract the package name from the archive: ${err.message}`
);
const assumedProjectName = installPackage.match(
/^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
)[1];
console.log(
`Based on the filename, assuming it is "${chalk.cyan(
assumedProjectName
)}"`
);
return Promise.resolve(assumedProjectName);
});
// 此处为信息中包含`git+`信息的情况
} else if (installPackage.indexOf('git+') === 0) {
return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);
// 此处为只有版本信息的时候的情况
} else if (installPackage.match(/.+@/)) {
return Promise.resolve(
installPackage.charAt(0) + installPackage.substr(1).split('@')[0]
);
// 此处为信息中包含`file:`开头的情况
} else if (installPackage.match(/^file:/)) {
const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
const installPackageJson = require(path.join(installPackagePath, 'package.json'));
return Promise.resolve(installPackageJson.name);
}
// 什么都没有直接返回包名
return Promise.resolve(installPackage);
}
好了,到了比较关键的函数了,接收一个 installPackage
参数,从这函数开始就采用 Promise
回调的方式一直执行到最后,我们来看看这个函数都做了什么,具体看上面每一行的注释。
总结一句话,这个函数的作用就是返回正常的包名,不带任何符号的,来看看它的外部依赖:
-
hyperquest:这个用于将http请求流媒体传输。 npm地址
他本身还有函数依赖,这两个函数依赖我都不单独再说,函数的意思很好理解,至于为什么这么做我还没想明白:
-
getTemporaryDirectory():不难,他本身是一个回调函数,用来创建一个临时目录。 -
extractStream():主要用到node本身的一个流,这里我真没懂为什么药改用流的形式,就不发表意见了。
PS:其实这个函数很好理解就是返回正常的包名,但是里面的处理很多我都没想通,以后理解深刻了在回溯一下。
checkIfOnline()
function checkIfOnline(useYarn) {
if (!useYarn) {
return Promise.resolve(true);
}
return new Promise(resolve => {
dns.lookup('registry.yarnpkg.com', err => {
let proxy;
if (err != null && (proxy = getProxy())) {
dns.lookup(url.parse(proxy).hostname, proxyErr => {
resolve(proxyErr == null);
});
} else {
resolve(err == null);
}
});
});
}
这个函数本身接收一个是否使用 yarn
的参数来判断是否进行后续,后续的作用就是 yarn
本身有个功能叫离线安装,这个函数来判断是否离线安装,其中用到了外部依赖:
-
dns:用来检测是否能够请求到指定的地址。 npm地址
继续往下走。
install()
function install(root, useYarn, dependencies, verbose, isOnline) {
// 封装在一个回调函数中
return new Promise((resolve, reject) => {
let command; // 定义一个命令
let args; // 定义一个命令的参数
// 如果使用yarn
if (useYarn) {
command = 'yarnpkg'; // 命令名称
args = ['add', '--exact']; // 命令参数的基础
if (!isOnline) {
args.push('--offline'); // 此处接上面一个函数判断是否是离线模式
}
[].push.apply(args, dependencies); // 组合参数和开发依赖 `react` `react-dom` `react-scripts`
args.push('--cwd'); // 指定命令执行目录的地址
args.push(root); // 地址的绝对路径
// 在使用离线模式时候会发出警告
if (!isOnline) {
console.log(chalk.yellow('You appear to be offline.'));
console.log(chalk.yellow('Falling back to the local Yarn cache.'));
console.log();
}
// 不使用yarn的情况使用npm
} else {
// 此处于上述一样,命令的定义 参数的组合
command = 'npm';
args = [
'install',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
}
// 这是我一开始没说的参数,在包安装的时候如果加了这个参数,以帮助你查看安装过程中可能出现的问题
if (verbose) {
args.push('--verbose');
}
// 这里就把命令组合起来执行
const child = spawn(command, args, { stdio: 'inherit' });
// 命令执行完毕后关闭
child.on('close', code => {
// code 为0代表正常关闭,不为零就打印命令执行错误的那条
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
// 正常继续往下执行
resolve();
});
});
}
又到了比较关键的地方了,仔细看每一行代码注释,此处函数的作用就是组合一个 yarn
或者 npm
的安装命令,把这些模块安装到项目的文件夹中,其中用到的外部依赖:
-
cross-spawn:解决node跨平台的命令执行。 npm地址
其实执行到这里, create-react-app
已经帮我们创建好了目录, package.json
并且安装了所有的依赖, react
、 react-dom
和 react-scrpts
,复杂的部分已经结束,继续往下走。
checkNodeVersion()
function checkNodeVersion(packageName) {
// 找到`react-scripts`的`package.json`路径
const packageJsonPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'package.json'
);
// 引入`react-scripts`的`package.json`
const packageJson = require(packageJsonPath);
// 在`package.json`中定义了一个`engines`其中放着`Node`版本的信息
if (!packageJson.engines || !packageJson.engines.node) {
return;
}
// 比较进程的`Node`版本信息和最小支持的版本,如果比他下,报错,退出进程
if (!semver.satisfies(process.version, packageJson.engines.node)) {
console.error(
chalk.red(
'You are running Node %s.\n' +
'Create React App requires Node %s or higher. \n' +
'Please update your version of Node.'
),
process.version,
packageJson.engines.node
);
process.exit(1);
}
}
这个函数直译一下,检查 Node
版本,为什么要检查了?之前我已经说过了 react-scrpts
是需要依赖 Node
版本的,也就是说低版本的 Node
不支持,其实的外部依赖也是之前的几个,没什么好说的。
setCaretRangeForRuntimeDeps()
function setCaretRangeForRuntimeDeps(packageName) {
const packagePath = path.join(process.cwd(), 'package.json'); // 取出创建项目的目录中的`package.json`路径
const packageJson = require(packagePath); // 引入`package.json`
// 判断其中`dependencies`是否存在
if (typeof packageJson.dependencies === 'undefined') {
console.error(chalk.red('Missing dependencies in package.json'));
process.exit(1);
}
// 拿出`react-scripts`或者是自定义的看看`package.json`中是否存在
const packageVersion = packageJson.dependencies[packageName];
if (typeof packageVersion === 'undefined') {
console.error(chalk.red(`Unable to find ${packageName} in package.json`));
process.exit(1);
}
// 检查`react` `react-dom` 的版本
makeCaretRange(packageJson.dependencies, 'react');
makeCaretRange(packageJson.dependencies, 'react-dom');
// 重新写入文件`package.json`
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
}
这个函数我也不想说太多了,他的作用并没有那么大,就是用来检测我们之前安装的依赖是否写入了 package.json
里面,并且对依赖的版本做了检测,其中一个函数依赖:
-
makeCaretRange():用来对依赖的版本做检测
我没有单独对其中的子函数进行分析,是因为我觉得不难,而且对主线影响不大,我不想贴太多说不完。
到这里 createReactApp.js
里面的源码都分析完了,咦!你可能会说你都没说 init()
函数,哈哈哈,看到这里说明你很认真哦, init()
函数是放在 packages/react-scripts/script
目录下的,但是我还是要给他说了,因为它其实跟 react-scripts
包联系不大,就是个 copy
他本身定义好的模板目录结构的函数。
init()
它本身接收 5
个参数:
-
appPath:之前的root,项目的绝对路径 -
appName:项目的名称 -
verbose:这个参数我之前说过了,npm安装时额外的信息 -
originalDirectory:原始目录,命令执行的目录 -
template:其实其中只有一种类型的模板,这个选项的作用就是配置之前我说过的那个函数,测试模板
// 当前的包名,也就是这个命令的包
const ownPackageName = require(path.join(__dirname, '..', 'package.json')).name;
// 当前包的路径
const ownPath = path.join(appPath, 'node_modules', ownPackageName);
// 项目的`package.json`
const appPackage = require(path.join(appPath, 'package.json'));
// 检查项目中是否有`yarn.lock`来判断是否使用`yarn`
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
appPackage.dependencies = appPackage.dependencies || {};
// 定义其中`scripts`的
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
};
// 重新写入`package.json`
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2)
);
// 判断项目目录是否有`README.md`,模板目录中已经定义了`README.md`防止冲突
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')
);
}
// 是否有模板选项,默认为当前执行命令包目录下的`template`目录,也就是`packages/react-scripts/tempalte`
const templatePath = template
? path.resolve(originalDirectory, template)
: path.join(ownPath, 'template');
if (fs.existsSync(templatePath)) {
// 拷贝目录到项目目录
fs.copySync(templatePath, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templatePath)}`
);
return;
}
这个函数我就不把代码贴全了,里面的东西也蛮好理解,基本上就是对目录结构的修改和重名了那些,挑了一些来说,到这里, create-react-app
从零到目录依赖的安装完毕的源码已经分析完毕,但是其实这只是个初始化目录和依赖,其中控制环境的代码都存在 react-scripts
中,所以其实离我想知道的关键的地方还有点远,但是本篇已经很长了,不打算现在说了,多多包涵。
希望本篇对大家有所帮助吧。
啰嗦两句
本来这篇我是打算把 create-react-app
中所有的源码的拿出来说一说,包括其中的 webpack
的配置啊, eslint
的配置啊, babel
的配置啊.....等等,但是实在是有点多,他自己本身把初始化的命令和控制 react
环境的命令分离成了 packages/create-react-app
和 packages/react-script
两边,这个篇幅才把 packages/create-react-app
说完,更复杂的 packages/react-script
在说一下这篇幅都不知道有多少了,所以我打算之后空了,在单独写一篇关于 packages/react-script
的源码分析的文。
码字不易,可能出现错别字什么的,说的不清楚的,说错的,欢迎指正,多多包涵!
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。