likes
comments
collection
share

create-react-app react-scripts 脚手架源码解析

作者站长头像
站长
· 阅读数 49

前言

在 vite 没有问世之前, 我们大部分 react 项目还是通过 create-react-app 来生成的, 抱着对知识的探索的精神, 今天就对其脚手架相关源码进行解析。

准备工作

  1. 找到 cra 的 package.json 文件里的 bin 对应的路径就是我们 cli 脚手架的入口, 打上断点
  2. 在 cra 下的 package.json 新增一条 scripts 语句 "create": "node ./packages/create-react-app/index.js cra-playground",
  3. 点击 package.json 里 scripts 上的 debug 按钮, 选择 create

create-react-app react-scripts 脚手架源码解析

入口文件, 进入到 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);
}
  1. 初始化 command 命令行
  2. 拿到命令行项目名称 create-react-app cra-playground, cra-playground 就是目录名字
  3. 执行 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);
}
  1. 拿到 cra-playground 的绝对路径, 判断目录是否存在, 不存在就创建。
  2. 生成并写入 package.json 文件
  3. 切换工作目录到 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);
  });
}
  1. 执行 run 方法安装项目需要用到的依赖 react、 react-dom、react-scripts(脚手架)、cra-template(需要生成的项目模版)
  2. 用 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
  1. 把 cra-template/template 内的文件拷贝到我们生成的项目里
  2. 拷贝完之后还要删除 cra-template 这个依赖包

OK, 到这里我们主干代码都已经解析完成。

补充知识

可能有的同学对上面 node -e source -- JSON.stringify(data) 的操作看不太明白, 这里写个例子给大家看一下。

create-react-app react-scripts 脚手架源码解析

相关文章

create-react-app build 命令源码解析 create-react-app start 命令源码解析

文档参考

cra 官方文档: create-react-app cra github: create-react-app

转载自:https://juejin.cn/post/7176164662260531256
评论
请登录