create-react-app react-scripts 脚手架源码解析
前言
在 vite 没有问世之前, 我们大部分 react 项目还是通过 create-react-app 来生成的, 抱着对知识的探索的精神, 今天就对其脚手架相关源码进行解析。
准备工作
- 找到 cra 的 package.json 文件里的 bin 对应的路径就是我们 cli 脚手架的入口, 打上断点
- 在 cra 下的 package.json 新增一条 scripts 语句
"create": "node ./packages/create-react-app/index.js cra-playground",
- 点击 package.json 里 scripts 上的 debug 按钮, 选择 create
入口文件, 进入到 create-react-app 下的 index.js
const currentNodeVersion = process.versions.node;
// process.versions 是 Node.js 中的一个对象,它包含了当前安装的 Node.js 版本的信息
const semver = currentNodeVersion.split('.');
const major = semver[0];
if (major < 14) { // Node 主版本必须 >= 14, 我的 Node 版本是 18.12.1
console.error(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 14 or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
const { init } = require('./createReactApp');
init();
入口会判断用户的 Node 主版本必须 >= 14。否则直接退出进程 Node 版本 >=14 就执行 createReactApp.js 里的 init 方法
进入核心文件 createReactApp.js
核心代码都是经过简化的, 只保留了主干流程, 根据功能来拆分一下代码, 可以拆成以下几步
1. 初始化命令行
async function init() {
let projectName;
new Command("xxx")
.version("1.0.0")
.arguments("<project-directory>")
.usage(`${chalk.green("<project-directory>")}`)
.action((name) => {
projectName = name;
})
.parse(process.argv);
await createApp(projectName);
}
- 初始化 command 命令行
- 拿到命令行项目名称
create-react-app cra-playground
, cra-playground 就是目录名字 - 执行 createApp 方法
2. 创建目录, 写入 package.json 文件
async function createApp(name) {
const root = path.resolve(name); // cra-playground 的绝对路径
const appName = path.basename(root); // 获取文件名
fs.ensureDirSync(name);
console.log(`Creating a new React app in ${chalk.green(root)}.`);
const packageJson = {
name: appName,
version: "0.1.0",
private: true,
};
// 生成 package.json 的模板
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify(packageJson, null, 2)
);
const originalDirectory = process.cwd();
process.chdir(root);
// 切换当前工作目录
await run(root, appName, originalDirectory);
}
- 拿到 cra-playground 的绝对路径, 判断目录是否存在, 不存在就创建。
- 生成并写入 package.json 文件
- 切换工作目录到 cra-playground 项目根目录, 执行 run 方法
3. 安装依赖
/**
*
* @param {*} root cra-playground 的绝对路径
* @param {*} appName 项目名 cra-playground
* @param {*} originalDirectory process.cwd()
*/
async function run(root, appName, originalDirectory) {
const scriptName = "react-scripts";
const templateName = "cra-template";
const allDependencies = ["react", "react-dom", scriptName, templateName];
console.log("Installing packages. This might take a couple of minutes.");
await install(root, allDependencies); // 安装依赖
let data = [root, appName, true, originalDirectory, templateName];
// 项目根目录 项目的名字 verbose是否显示详细信息 命令行工作目录 拉取的模板名称
let source = `
var init = require("${scriptName}/scripts/init.js");
init.apply(null, JSON.parse(process.argv[1]));
`;
/*
脚本执行的时候。process.argv[1] 拿到的就是上面的 data。
就会去执行 react-scripts/scripts/init.js 里的默认导出方法
*/
await execNodeScript({ cwd: process.cwd() }, data, source);
process.exit(0); // 正常退出进程
}
async function execNodeScript({ cwd }, data, source) {
return new Promise((resolve) => {
const child = spawn(
process.execPath,
["-e", source, "--", JSON.stringify(data)],
{ cwd, stdio: "inherit" }
);
// 比如 node -e 'console.log(1)' 他就会执行这个 console.log 这段代码
// 上述在这可以理解为 node -e source -- "[参数]" node会把参数传给source这个执行脚本。
child.on("close", resolve);
});
}
async function install(root, allDependencies) {
return new Promise((resolve) => {
const command = "yarn";
const args = ["add", "--exact", ...allDependencies, "--cwd", root];
// 开启子进程下载依赖。--exact 锁定包版本。 --cwd 添加依赖的路径
const child = spawn(command, args, { stdio: "inherit" });
child.on("close", resolve);
});
}
- 执行 run 方法安装项目需要用到的依赖
react、 react-dom、react-scripts(脚手架)、cra-template(需要生成的项目模版)
- 用 node -e 执行我们的 source 脚本, 就是去执行 react-scripts/init.js。
4. 补充 package.json 字段
const appPackage = require(path.join(appPath, 'package.json'));
const templatePath = path.dirname(
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 || {};
const templateScripts = templatePackage.scripts || {};
appPackage.scripts = Object.assign(
{
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
},
templateScripts
);
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2)
);
上面的一堆操作就是就是拿到生成项目的 package.json 文件做一下字段的合并操作, 这里只展示 scripts 脚本字段的合并。可以看到熟悉的 pnpm start、pnpm build
等可执行脚本就已经写入到 package.json 里去了
5. 拷贝模板
const templateDir = path.join(templatePath, 'template');
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, appPath);
}
spawn.sync('yarnpkg', ['remove', 'cra-template']);
// 删除模板, yarn remove cra-template
- 把 cra-template/template 内的文件拷贝到我们生成的项目里
- 拷贝完之后还要删除 cra-template 这个依赖包
OK, 到这里我们主干代码都已经解析完成。
补充知识
可能有的同学对上面 node -e source -- JSON.stringify(data) 的操作看不太明白, 这里写个例子给大家看一下。
相关文章
create-react-app build 命令源码解析 create-react-app start 命令源码解析
文档参考
cra 官方文档: create-react-app cra github: create-react-app
转载自:https://juejin.cn/post/7176164662260531256