CRA源码解读
What CRA Is
Create React APP, facebook 为了方便快捷创建React单页面应用而实现的脚手架
How To Use
-
首先保证
Node >= 14.0.0 && npm >=5.6 -
无需下载
create-react-app, 直接执行npx create-react-app your-project -
或者指定 react-scripts version or template version (也可以是其他的第三方包)
npx create-react-app react-app —scripts-version=react-scripts@3.2.0 —template=cra-template-typescript@1.1.0
What Did He Perform (做了什么操作)
执行上述命令之后,对比工作目录变化可以知道:
- 创建了一个文件夹 “your-project”
- 新增了一个 .git 文件夹
- 新增
README.md、.gitignore、package.json配置文件 - 新增项目公共文件夹
public以及react项目代码文件夹src - 新增了一个
node_modules文件夹 - 查看git提交记录,还会发现多了一条提交记录
Initialize project using Create React App
How To Achieve(怎么实现)
命令行添加
使用 commander 来监听命令行输入, 并将匹配到的命令输入项添加校验。获取文件夹(项目)名称,以及选项中传入的模板值
// Commander 负责将参数解析为选项和命令参数
let projectName
const program = new commander.Command(packageJson.name) // 作为当前已有参数 create-react-app
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {
projectName = name;
})
.option('...')
.option(
'--template <path-to-template>',
'specify a template for the created project'
).parse(process.argv)
创建项目目录
fs.ensureDirSync(projectName);
添加 package.json
const root = path.resolve(projectName);
const appName = path.basename(root);
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL // 当前系统的换行符, mac or windows
);
依赖下载
这里会做一系列的校验,比如使用的包管理 工具吗 pnp or yarn or npm, 以及 命令选项传入的 –scripts-version 值判断是否支持模板 (react-scripts >=3.3.0)
command = 'npm';
args = [
'install',
'--no-audit', // <https://github.com/facebook/create-react-app/issues/11174>
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies); // dependencies = ['react', 'react-dom', 'react-scripts','cra-template']
// 创建一个子进行执行命令
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
执行react-scripts
await executeNodeScript(
{
cwd: process.cwd(), // project dir
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
const init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
function executeNodeScript({ cwd, args }, data, source) {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath, // node 的安装路径
[...args, '-e', source, '--', JSON.stringify(data)],
{ cwd, stdio: 'inherit' }
);
child.on('close', code => {
if (code !== 0) {
reject({
command: `node ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
script init 执行
读取模板json文件
require.resolve(path,[options]): 只解析path 的完整路径,不引入文件
const templatePath = path.dirname(
// require.resolve 解析模板的完整路径
require.resolve(`${templateName}/package.json`, { paths: [appPath] })
);
const templateJsonPath = path.join(templatePath, 'template.json');
let templateJson = {};
if (fs.existsSync(templateJsonPath)) {
templateJson = require(templateJsonPath);
}
const templatePackage = templateJson.package || {};
修改 project package.json 配置项添加
- add scripts
appPackage.scripts = Object.assign(
{
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
},
templateScripts // 模板里包含的脚本
);
- add browerslist
const { defaultBrowsers } = require('react-dev-utils/browsersHelper');
appPackage.browserslist = defaultBrowsers;
- 模板里一些其他的配置化项(非白名单
templatePackageBlacklist跟合并配置项templatePackageToMerge)
以 cra-template 为例,
"eslintConfig": { "extends": ["react-app", "react-app/jest"] }
- 将上述配置后的内容写入到 package.json
fs.writeFileSync(path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2) + os.EOL );
README.md 文件处理
如果存在的 话重命名为 README.old.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')
);
}
复制模板文件到本地
const templatePath = path.dirname(
// require.resolve 解析模板的完整路径, 从appPath目录下的 node_modules 开始解析 templateName的路径
require.resolve(`${templateName}/package.json`, { paths: [appPath] })
);
const templateDir = path.join(templatePath, 'template');
// 模板文件复制
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templateDir)}`
);
return;
}
.gitignore 文件处理
模板里放的是 gitignore 文件
const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore'));
if (gitignoreExists) {
// Append if there's already a `.gitignore` file there
const data = fs.readFileSync(path.join(appPath, 'gitignore'));
fs.appendFileSync(path.join(appPath, '.gitignore'), data);
fs.unlinkSync(path.join(appPath, 'gitignore'));
} else {
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore
// See: <https://github.com/npm/npm/issues/1862>
fs.moveSync(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[]
);
}
init git
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function tryGitInit() {
try {
execSync('git --version', { stdio: 'ignore' });
// 即判断当前目录是否已经初始化过
// Mercurial 代码管理系统
if (isInGitRepository() || isInMercurialRepository()) {
return false;
}
execSync('git init', { stdio: 'ignore' });
return true;
} catch (e) {
console.warn('Git repo not initialized', e);
return false;
}
}
Install dependencies and devDependencies dependency In template
const dependenciesToInstall = Object.entries({
...templatePackage.dependencies,
...templatePackage.devDependencies,
});
if (dependenciesToInstall.length) {
args = args.concat(
dependenciesToInstall.map(([dependency, version]) => {
return `${dependency}@${version}`;
})
);
}
//...
// 如果下载失败, 则 uninstall
const proc = spawn.sync(command, args, { stdio: 'inherit' });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
uninstall template dependency
// npm uninstall templateName || yarnpkg remove template
const proc = spawn.sync(command, [remove, templateName], {
stdio: 'inherit',
});
execute git add git commit
execSync('git add -A', { stdio: 'ignore' });
execSync('git commit -m "Initialize project using Create React App"', {
stdio: 'ignore',
});
总结
CRA的源码实现步骤如下:
- 首先在package.json 中添加关键字 bin,以便可以直接通过npx来执行对应的脚本文件
"bin": {
"create-react-app": "./index.js"
},
- 在
index.js文件首行指定文件执行环境node
#!/usr/bin/env node
开始脚手架的功能实现
-
监听命令行的输入
-
接受命中到的命令行输入的参数一次作为项目名称、对应的模板以及版本
-
在执行命令目录下创建项目,进入到创建的项目目录下添加 package.json文件,写入基本配置,项目名称、版本号等
-
根据本地使用的包管理工具(npm || pnp || yarn)的方式下下载基本依赖项
react、react-scripts、cra-template -
依赖下载成功后便开始执行
react-scripts脚本,补充新增项目的配置文件 package.json的脚本命令等其他属性、还有文件的README.md 、.gitignore配置描述文件 -
初始化项目git配置(如果不是git项目的话)
-
根据命令行输入的模板或者默认的模板及版本,下载对应的依赖、然后从
node_modules中将 template的文件拷贝复制到 新建的项目目录下 -
拷贝成功后将模板依赖从项目中删除
-
接着将新增的文件直接添加到暂存区
git add .,接着git commit -m "Initialize project using Create React App -
至此,CRA执行完了所有的操作,你想要的项目也已经初始化成功啦。