createreactapp-源码介绍

48次阅读

共计 10251 个字符,预计需要花费 26 分钟才能阅读完成。

<h1>create-react-app 的使用 </h1>

参数解析

参数 <br/> 参数解析 默认值 是否必填
<project-directory> 项目文件夹名
verbose 会打印执行 (包括依赖包的安装信息等) 的详细信息 false
info 打印环境的信息 (系统、cpu、npm、yarn、chrome、react 等) false
use-npm 是否使用 npm false, 默认使用 yarn
use-pnp yarn 解决 node_modules 的一种解决方案,将 node_modules 放在全局缓存,不放在单独项目中 false
typescript 是否使用 typescript false
internal-testing-template 这个参数的值将替代 react-script 中的 template 目录,一般用于开发者使用 false
scripts-version 直接传入版本号 <br/>
–scripts-version=@6.10.2
react-scripts
npm 方式 <br/>
–scripts-version=douchi-react-script <br/>
git+https 方式 <br/>
–scripts-version=git+https://github.com/mcfly001/d… <br/>
git+ssh 方式 <br/>
–scripts-version=git+ssh://git@github.com:mcfly001/douchi-react-script.git <br/>
file + url 方式 <br/>
–scripts-version=file:../file-react-script
压缩包的方式(以 tgz|tar.gz 结尾)<br/>
–scripts-version=./file-react-script.tar.gz

主要依赖包作用分析

包作用
validate-npm-package-name 判断给定的字符串是不是符合 npm 包名称的规范
chalk 给命令行输入的 log 设置颜色
commander TJ 写的,自定义 shell 命令的工具,具体使用可以查看 commander
semver 用于版本比较的工具,比如哪个版本大,版本命名是否合法等
envinfo 输出当前环境的信息

如何断点调试

通过 vscode(推荐使用)

  • 通过上述操作即可断点调试 create-react-app
通过 chrome
  • 进入到 packages/create-react-app 目录
  • 执行以下命令
// dirname 为目录名,需要添加其他参数只需要在命令后面相应添加即可
node --inspect-brk index.js dirname
  • 打开 chrome 浏览器,输入以下地址
chrome://inspect/#devices
  • 点击 Remote Target 下面的 inspect 即可

代码分析

环境 版本
系统 macOS 10.14.5
node v8.10.0
npm 6.10.2
yarn 1.17.3
create-react-app 3.0.1
入口文件在 packages/create-react-app/index.js
// 检查当前环境安装的 node 版本,如果版本是小于 8.0.0 就报错需要安装更高的 node 版本,同时结束进程。如果符合条件接下来正式进入 create-react-app

'use strict';

var currentNodeVersion = process.versions.node;
var semver = currentNodeVersion.split('.');
var major = semver[0];

if (major < 8) {
  console.error(
    'You are running Node' +
      currentNodeVersion +
      '.\n' +
      'Create React App requires Node 8 or higher. \n' +
      'Please update your version of Node.'
  );
  process.exit(1);
}

require('./createReactApp');
以下代码在 packages/create-react-app/createReactApp.js
/**
 * 自定义 create-react-app 这个 shell 命令
 * 首先从同级目录的 package.json 中获取字段 name,即 create-react-app)作为命令名称
 * 同时将 package.json 中的 version 作为该命令的版本号
 * 其他参数见上文详解
 */

let projectName;

const program = new commander.Command(packageJson.name)
  .version(packageJson.version)
  .arguments('<project-directory>')
  .usage(`${chalk.green('<project-directory>')} [options]`)
  .action(name => {projectName = name;})
  .option('--verbose', 'print additional logs')
  .option('--info', 'print environment debug info')
  .option(
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  )
  .option('--use-npm')
  .option('--use-pnp')
  .option('--typescript')
  .allowUnknownOption()
  .on('--help', () => {console.log(`    Only ${chalk.green('<project-directory>')} is required.`);
    console.log();
    console.log(`    A custom ${chalk.cyan('--scripts-version')} can be one of:`
    );
    console.log(`      - a specific npm version: ${chalk.green('0.8.2')}`);
    console.log(`      - a specific npm tag: ${chalk.green('@next')}`);
    console.log(
      `      - a custom fork published on npm: ${chalk.green('my-react-scripts')}`
    );
    console.log(
      `      - a local path relative to the current working directory: ${chalk.green('file:../my-react-scripts')}`
    );
    console.log(
      `      - a .tgz archive: ${chalk.green('https://mysite.com/my-react-scripts-0.8.2.tgz')}`
    );
    console.log(
      `      - a .tar.gz archive: ${chalk.green('https://mysite.com/my-react-scripts-0.8.2.tar.gz')}`
    );
    console.log(`    It is not needed unless you specifically want to use a fork.`);
    console.log();
    console.log(`    If you have any problems, do not hesitate to file an issue:`);
    console.log(
      `      ${chalk.cyan('https://github.com/facebook/create-react-app/issues/new')}`
    );
    console.log();})
  .parse(process.argv);
// 如果 crete-react-app 中传入了 -info,就将系统信息、npm、node、chrom、react 等基本信息输出

if (program.info) {console.log(chalk.bold('\nEnvironment Info:'));
  return envinfo
    .run(
      {System: ['OS', 'CPU'],
        Binaries: ['Node', 'npm', 'Yarn'],
        Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'],
        npmPackages: ['react', 'react-dom', 'react-scripts'],
        npmGlobalPackages: ['create-react-app'],
      },
      {
        duplicates: true,
        showNotFound: true,
      }
    )
    .then(console.log);
}
/**
 * 检查是否有 <project-directory> 这个参数,如果没有就输入必填提示,同时结束进程
 */
 
if (typeof projectName === 'undefined') {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);
}

function printValidationResults(results) {if (typeof results !== 'undefined') {
    results.forEach(error => {console.error(chalk.red(`  *  ${error}`));
    });
  }
}

/**
 * 在 create-react-app 中还有一个参数 --internal-testing-template
 * 该参数用于替换 react-scripts 中的 template
 */
 
const hiddenProgram = new commander.Command()
  .option(
    '--internal-testing-template <path-to-template>',
    '(internal usage only, DO NOT RELY ON THIS)' +
      'use a non-standard application template'
  )
  .parse(process.argv);
  

接下来正式进入 create-react-app 的具体内容

function createApp(
  name,
  verbose,
  version,
  useNpm,
  usePnp,
  useTypescript,
  template
) {const root = path.resolve(name);
  const appName = path.basename(root);
  // 判断项目名是不是符合规范
  checkAppName(appName);
  // 创建文件夹
  fs.ensureDirSync(name); 
  // 判断当前目录下是不是存在除了(.git、doc、DS_Store 等文件)外的文件
  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,
  };
  fs.writeFileSync(path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL
  );

  const useYarn = useNpm ? false : shouldUseYarn();
  const originalDirectory = process.cwd();
  process.chdir(root);
  
  // 检测 npm 是否在正确目录下执行
  if (!useYarn && !checkThatNpmCanReadCwd()) {process.exit(1);
  }

  // 判断 node 版本是不是小于 8.10.0, 如果是就会警告,同时 react-scripts 包会变成 0.9.x
  if (!semver.satisfies(process.version, '>=8.10.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 8.10 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';
  }

  if (!useYarn) {// 返回一个对象 {hasMinNpm: '是否大于 5.0.0', npmVersion: '具体版本信息'}
    
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {if (npmInfo.npmVersion) {
        console.log(
          chalk.yellow(
            `You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
              `Please update to npm 5 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';
    }
  } else if (usePnp) {// 返回一个对象 {hasMinYarnPnp: '是否大于 1.12.0',yarnVersion: 'yarn 的具体版本'}
    
    const yarnInfo = checkYarnVersion();
    if (!yarnInfo.hasMinYarnPnp) {if (yarnInfo.yarnVersion) {
        console.log(
          chalk.yellow(
            `You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
              `Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
          )
        );
      }
      // 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
      usePnp = false;
    }
  }

    // 如果默认 yarn 的代理就会生成 yarn.lock 文件
  if (useYarn) {
    let yarnUsesDefaultRegistry = true;
    try {
      yarnUsesDefaultRegistry =
        execSync('yarnpkg config get registry')
          .toString()
          .trim() === 'https://registry.yarnpkg.com';} catch (e) {// ignore}
    if (yarnUsesDefaultRegistry) {
      fs.copySync(require.resolve('./yarn.lock.cached'),
        path.join(root, 'yarn.lock')
      );
    }
  }

  // 安装个中依赖
  run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp,
    useTypescript
  );
}
function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp,
  useTypescript
) {
  // 获取 npm 或者 yarn 关于 react-scripts 的安装的地址
  getInstallPackage(version, originalDirectory).then(packageToInstall => {const allDependencies = ['react', 'react-dom', packageToInstall];
    if (useTypescript) {
      allDependencies.push(
        // TODO: get user's node version instead of installing latest'@types/node','@types/react','@types/react-dom',
        // TODO: get version of Jest being used instead of installing latest
        '@types/jest',
        'typescript'
      );
    }

    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,
          usePnp,
          allDependencies,
          verbose,
          isOnline
        ).then(() => packageName);
      })
      .then(async packageName => {
        // 判断 node_modules 下面所有包中对于 node 版本的最低要求
        checkNodeVersion(packageName);
        // 给 package.json 中的依赖添加 ^
        setCaretRangeForRuntimeDeps(packageName);

        const pnpPath = path.resolve(process.cwd(), '.pnp.js');

        const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
        
        // 安装依赖同时 copy react-scripts 下面的 template 到当前目录
        await executeNodeScript(
          {cwd: process.cwd(),
            args: nodeArgs,
          },
          [root, appName, verbose, originalDirectory, template],
          `
              var init = require('${packageName}/scripts/init.js');
                console.log(33333)
              init.apply(null, JSON.parse(process.argv[1]));
            `
        );

        if (version === 'react-scripts@0.9.x') {
          console.log(
            chalk.yellow(
              `\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +
                `Please update to Node >=8.10 and npm >=5 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();

        // On 'exit' we will delete these files from target directory.
        const knownGeneratedFiles = [
          'package.json',
          'yarn.lock',
          'node_modules',
        ];
        const currentFiles = fs.readdirSync(path.join(root));
        currentFiles.forEach(file => {
          knownGeneratedFiles.forEach(fileToMatch => {
            // This removes all knownGeneratedFiles.
            if (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) {
          // Delete target folder if empty
          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);
      });
  });
}
/**
 * 1. 安装依赖(为什么要先安装依赖,因为第二步,要不然找不到 react-scripts 中的 scripts 中的 init.js 文件)* 2. 在依赖的 node_modules 中找到 react-scripts 中的 scripts 里面的 init.js 并执行其对外的方法
 * 2.1 修改 package.json,写入一些 start、build、test、eject 方法
 * 2.2 修改 README.md 文件,输入一些帮助信息
 * 2.3 copy template 中的文件到项目文件夹下面
 * 2.4 重新 install 一下
 */
function executeNodeScript({cwd, args}, data, source) {return new Promise((resolve, reject) => {
    const child = spawn(
      process.execPath,
      [...args, '-e', source, '--', JSON.stringify(data)],
      {cwd, stdio: 'inherit'}
    );

    child.on('close', code => {if (code !== 0) {
        reject({command: `node ${args.join(' ')}`,
        });
        return;
      }
      resolve();});
  });
}

正文完
 0