create-react-app 源碼介紹

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

參數解析

參數<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的具體內容java

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();
    });
  });
}

圖片描述

相關文章
相關標籤/搜索