create-react-app 源碼學習

前言

最近想寫一些東西,學習學習全棧開發,就從熟悉的react開始,一直開發模式都是組件形式,對 create-react-app源碼不瞭解,須要從新學習下!
網上找了不少資料,刷官網...
找到一個寫的很全面的,可是 create-react-app 已更新版本,我下載的是v3.3.0版本,版本不同源碼也會有些出入,因此這裏也記錄下本身的學習內容,方便後期自我查看和學習。

文中都是借鑑和一些自我理解,有些錯誤或者理解上的誤差,歡迎指正。node

原文連接:https://segmentfault.com/a/11...react

create-react-app 源碼**webpack

目錄

├── .github --- 項目中提的issuepr的規範
├── docusaurus--- facebook的開源的用於簡化構建,部署,和維護的博客網站
├── packages --- 包 項目核心
├── tasks--- 任務
├── test--- 測試文件
├── .eslintignore --- eslint檢查時忽略文件
├── .eslintrc.json --- eslint 配置文件
├── .gitignore --- git提交時忽略文件
├── .travis.yml --- travis配置文件
├── .yarnrc --- yarn配置文件
├── azure-pipelines-test-job.yml--- Azure Pipelines test配置
├── azure-pipelines.yml --- Azure Pipelines配置,用於在LinuxWindowsmacOS上構建和測試create-react-app
├── CHANGELOG-0.x.md --- 版本變動說明文件
├── CHANGELOG-1.x.md --- 版本變動說明文件
├── CHANGELOG-2.x.md --- 版本變動說明文件
├── CHANGELOG.md --- 當前版本變動說明文件
├── CODE_OF_CONDUCT.md --- facebook 代碼行爲準則說明
├── CONTRIBUTING.md--- 項目的核心說明
├── lerna.json--- lerna 配置文件
├── LICENSE --- 開源協議
├── netlify.toml --- 能夠理解爲docusaurus的配置設置
├── package.json --- 項目配置文件
├── README.md --- 項目使用說明
├── screencast-error.svg
└── screencast.svggit

packages 文件

├── babel-plugin-named-asset-import
├── babel-preset-react-app
├── confusing-browser-globals
├── cra-template
├── cra-template-typescript
├── create-react-app
├── eslint-config-react-app
├── react-app-polyfill
├── react-dev-utils
├── react-error-overlay
└── react-scripts github

代碼的入口在packages/create-react-app/index.js下,核心代碼在createReactApp.jsweb

斷點調試:配置 vscode 的 debug 文件

image.png

這裏添加三種環境,是 create-react-app 的不一樣種使用方式typescript

  • create-react-app study-create-react-app-source
  • create-react-app
  • create-react-app study-create-react-app-source-ts --typescript

VsCode中Debug調試插件配置shell

依賴包做用分析

包做用
validate-npm-package-name 判斷給定的字符串是否是符合npm包名稱的規範,檢查包名是否合法 validate
chalk 給命令行輸入的log設置顏色 chalk
commander 自定義shell命令的工具,也就是能夠用它代管Node命令,具體使用能夠查看commander
semver 用於版本比較的工具,好比哪一個版本大,版本命名是否合法等
envinfo 輸出當前環境的信息,用於比較Node版本 envinfo
fs-extra Node自帶文件模塊的外部擴展模塊 fs-extra
cross-spawn 用來執行node進程,Node跨平臺解決方案,解決在windows下各類問題,詳細看 cross-spawn
hyperquest 用於將http請求流媒體傳輸,詳細看 hyperquest
dns 用來檢測是否可以請求到指定的地址 dns

image.png

index.js

'use strict';

var currentNodeVersion = process.versions.node; // 返回Node版本信息,若是有多個版本返回多個版本
var semver = currentNodeVersion.split('.'); // 全部Node版本的集合
var major = semver[0]; // 取出第一個Node版本信息

if (major < 8) {  // 校驗當前node版本是否低於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');

index.js 的代碼很是的簡單,其實就是對 node 的版本作了一下校驗,若是版本號低於 8,就退出應用程序,不然直接進入到核心文件中,createReactApp.jsnpm

createReactApp.js

createReactApp 的功能也很是簡單其實,大概流程:json

  • 命令初始化,好比自定義create-react-app --info的輸出等
  • 判斷是否輸入項目名稱,若是有,則根據參數去跑安裝,若是沒有,給提示,而後退出程序
  • 修改 package.json
  • 拷貝react-script下的模板文件
let projectName;  //定義了一個用來存儲項目名稱的變量

const program = new commander.Command(packageJson.name)
  .version(packageJson.version) //create-react-app -v 時候輸出的值 packageJson 來自上面 const packageJson = require('./package.json');
  .arguments('<project-directory>')//定義 project-directory ,必填項
  .usage(`${chalk.green('<project-directory>')} [options]`) //  使用`create-react-app`第一行打印的信息,也就是使用說明

  .action(name => {
    projectName = name; //獲取用戶的輸入,存爲 projectName
  })
  .option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的選項,相似 --help -V
  .option('--info', 'print environment debug info') // 打印本地相關開發環境,操做系統,`Node`版本等等
  .option( // 指定 react-scripts
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  )
  .option(// 項目建立template
    '--template <path-to-template>',
    'specify a template for the created project'
  )
  .option('--use-npm') // 指定使用npm 默認使用yarn
  .option('--use-pnp') //指定使用 pnp
  // TODO: Remove this in next major release.
  .option( // 指定使用ts,不過這裏也有備註下一個版本就刪除這項
    '--typescript',
    '(this option will be removed in favour of templates in the next major release of create-react-app)'
  )
  .allowUnknownOption()
  .on('--help', () => {
  // on('option', cb) 語法,輸入 create-react-app --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(`    A custom ${chalk.cyan('--template')} can be one of:`);
    console.log(
      `      - a custom fork published on npm: ${chalk.green(
        'cra-template-typescript'
      )}`
    );
    console.log(
      `      - a local path relative to the current working directory: ${chalk.green(
        'file:../my-custom-template'
      )}`
    );
    console.log(
      `      - a .tgz archive: ${chalk.green(
        'https://mysite.com/my-custom-template-0.8.2.tgz'
      )}`
    );
    console.log(
      `      - a .tar.gz archive: ${chalk.green(
        'https://mysite.com/my-custom-template-0.8.2.tar.gz'
      )}`
    );
    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);// commander 文檔傳送門,解析正常的`node
// 判斷當前環境信息,退出
if (program.info) {
  console.log(chalk.bold('\nEnvironment Info:'));
  console.log(
    `\n  current version of ${packageJson.name}: ${packageJson.version}`
  );
  console.log(`  running from ${__dirname}`);
  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);
}

//判斷 projectName 是否爲 undefined,而後輸出相關提示信息,退出~
if (typeof projectName === 'undefined') {
  //沒有項目名稱也沒有--info 會拋出異常
  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);
}

image.png
create-react-app <my-project>中的項目名稱賦予了projectName 變量,此處的做用就是看看用戶有沒有傳這個<my-project>參數,若是沒有就會報錯,並顯示一些幫助信息,這裏用到了另一個外部依賴envinfo

createApp

修改vscode 配置文件

{  
  "type": "node",  
  "request": "launch",  
  "name": "CreateReactAppTs",  
  "program": "${workspaceFolder}/packages/create-react-    app/index.js",  
  "args": \[  
  "study-create-react-app-source-ts",  
  "--typescript",  
  "--use-npm"  
   \]  
  }

image.png

image.png

image.png
commanderoption選項,若是加了這個選項這個值就是true,不然就是false,也就是說這裏若是加了--typescript--use-npm,那這個參數就是true,這些也都是咱們在commander中定義的 options,在源碼裏面 createApp 中,傳入的參數分別爲:

  • projectName :項目名稱
  • program.verbose 是否輸出額外信息
  • program.scriptsVersion 傳入的腳本版本
  • program.useNpm  是否使用 npm ,默認使用yarn
  • program.usePnp  是否使用 Pnp,默認使用yarn
  • program.typescript  是否使用 ts
  • hiddenProgram.internalTestingTemplate 給開發者用的調試模板路徑
function createApp(
  name,
  verbose,
  version,
  template,
  useNpm,
  usePnp,
  useTypeScript
) {

  // 檢測node版本
  const unsupportedNodeVersion = !semver.satisfies(process.version, '>=8.10.0');
  // 判斷node版本,若是結合TS一塊兒使用就須要8.10或者更高版本,具體的方法在semver.js
  
  if (unsupportedNodeVersion && useTypeScript) {
    console.error(
      chalk.red(
        `You are using Node ${process.version} with the TypeScript template. Node 8.10 or higher is required to use TypeScript.\n`
      )
    );

    process.exit(1);
  } else if (unsupportedNodeVersion) {
    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';
  }

  const root = path.resolve(name); //path 拼接路徑
  const appName = path.basename(root); //獲取文件名

  checkAppName(appName); //檢查傳入的文件名合法性
  fs.ensureDirSync(name); //確保目錄存在,若是不存在則建立一個
  if (!isSafeToCreateProjectIn(root, name)) {//判斷新建這個文件夾是否安全,不然直接退出
    process.exit(1);
  }
  console.log();

  // 到這裏打印成功建立了一個`react`項目在指定目錄下
  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,
  };
  // 往咱們建立的文件夾中寫入package.json文件
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL
  );

  // 定義常量 useYarn 若是傳參有 --use-npm useYarn就是false,不然執行 shouldUseYarn() 檢查yarn是否存在
  // 這一步就是以前說的他默認使用`yarn`,可是能夠指定使用`npm`,若是指定使用了`npm`,`useYarn`就是`false`,否則執行 shouldUseYarn 函數
  // shouldUseYarn 用於檢測本機是否安裝了`yarn`
  const useYarn = useNpm ? false : shouldUseYarn();
  // 取得當前node進程的目錄,提早獲取保存下來,接下來這個值會被改變,若是後面須要用到這個值,後續其實取得值將不是這個
  // 因此這裏的目的就是提早存好,省得我後續使用的時候很差去找,這個地方就是我執行初始化項目的目錄,而不是初始化好的目錄,是初始化的上級目錄
  const originalDirectory = process.cwd();
  // 修改進程目錄爲底下子進程目錄
  // 在這裏就把進程目錄修改成了咱們建立的目錄
  process.chdir(root);
  // checkThatNpmCanReadCwd 這個函數的做用是檢查進程目錄是不是咱們建立的目錄,也就是說若是進程不在咱們建立的目錄裏面,後續再執行`npm`安裝的時候就會出錯,因此提早檢查
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  }

  if (!useYarn) { //關於 npm、pnp、yarn 的使用判斷,版本校驗等
    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) {
    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;
    }
  }

  if (useTypeScript) {
    console.log(
      chalk.yellow(
        'The --typescript option has been deprecated and will be removed in a future release.'
      )
    );
    console.log(
      chalk.yellow(
        `In future, please use ${chalk.cyan('--template typescript')}.`
      )
    );
    console.log();
    if (!template) {
      template = 'typescript';
    }
  }

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

image.png

代碼很是簡單,部分註釋已經加載代碼中,簡單的說就是對一個本地環境的一些校驗,版本檢查、目錄建立等,若是建立失敗,則退出,若是版本較低,則使用對應低版本的create-react-app,最後調用 run 方法

  • checkAppName():用於檢測文件名是否合法,
  • isSafeToCreateProjectIn():用於檢測文件夾是否安全
  • shouldUseYarn():用於檢測yarn在本機是否已經安裝
  • checkThatNpmCanReadCwd():用於檢測npm是否在正確的目錄下執行
  • checkNpmVersion():用於檢測npm在本機是否已經安裝了

checkAppName

checkAPPName 方法主要的核心代碼是validate-npm-package-namepackage,從名字便可看出,檢查是否爲合法的 npm 包名

function checkAppName(appName) {
  // 關於validateProjectName方法 在插件`validate-npm-package-name`index文件
  const validationResult = validateProjectName(appName);
  if (!validationResult.validForNewPackages) {
    console.error(
      chalk.red(
        `Cannot create a project named ${chalk.green(
          `"${appName}"`
        )} because of npm naming restrictions:\n`
      )
    );
    [
      ...(validationResult.errors || []),
      ...(validationResult.warnings || []),
    ].forEach(error => {
      console.error(chalk.red(`  * ${error}`));
    });
    console.error(chalk.red('\nPlease choose a different project name.'));
    process.exit(1);
  }

  // TODO: there should be a single place that holds the dependencies
  const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
  if (dependencies.includes(appName)) {
    console.error(
      chalk.red(
        `Cannot create a project named ${chalk.green(
          `"${appName}"`
        )} because a dependency with the same name exists.\n` +
          `Due to the way npm works, the following names are not allowed:\n\n`
      ) +
        chalk.cyan(dependencies.map(depName => `  ${depName}`).join('\n')) +
        chalk.red('\n\nPlease choose a different project name.')
    );
    process.exit(1);
  }
}

image.png

image.png
這個函數用了一個外部依賴來校驗文件名是否符合npm包文件名的規範,而後定義了三個不能取得名字react、react-dom、react-scripts,紅色框出來的,以前引用的是一個函數依賴printValidationResults(),最新的代碼裏面換成了ES6屬性spread,不過這個屬性在ES9裏也有新的用法

printValidationResults():函數引用,這個函數就是我說的特別簡單的類型,裏面就是把接收到的錯誤信息循環打印出來

isSafeToCreateProjectIn

function isSafeToCreateProjectIn(root, name) {
  const validFiles = [
    '.DS_Store',
    '.git',
    '.gitattributes',
    '.gitignore',
    '.gitlab-ci.yml',
    '.hg',
    '.hgcheck',
    '.hgignore',
    '.idea',
    '.npmignore',
    '.travis.yml',
    'docs',
    'LICENSE',
    'README.md',
    'mkdocs.yml',
    'Thumbs.db',
  ];
  // These files should be allowed to remain on a failed install, but then
  // silently removed during the next create.
  const errorLogFilePatterns = [
    'npm-debug.log',
    'yarn-error.log',
    'yarn-debug.log',
  ];
  const isErrorLog = file => {
    return errorLogFilePatterns.some(pattern => file.startsWith(pattern));
  };

  const conflicts = fs
    .readdirSync(root)
    .filter(file => !validFiles.includes(file))
    // IntelliJ IDEA creates module files before CRA is launched
    .filter(file => !/\.iml$/.test(file))
    // Don't treat log files from previous installation as conflicts
    .filter(file => !isErrorLog(file));

  if (conflicts.length > 0) {
    console.log(
      `The directory ${chalk.green(name)} contains files that could conflict:`
    );
    console.log();
    for (const file of conflicts) {
      try {
        const stats = fs.lstatSync(path.join(root, file));
        if (stats.isDirectory()) {
          console.log(`  ${chalk.blue(`${file}/`)}`);
        } else {
          console.log(`  ${file}`);
        }
      } catch (e) {
        console.log(`  ${file}`);
      }
    }
    console.log();
    console.log(
      'Either try using a new directory name, or remove the files listed above.'
    );

    return false;
  }

  // Remove any log files from a previous installation.
  fs.readdirSync(root).forEach(file => {
    if (isErrorLog(file)) {
      fs.removeSync(path.join(root, file));
    }
  });
  return true;
}

image.png

安全性校驗,檢查當前目錄下是否存在已有文件,判斷建立的這個目錄是否包含除了上述validFiles裏面的文件

shouldUseYarn

function shouldUseYarn() {
  try {
    execSync('yarnpkg --version', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false;
  }
}

execSync是由node自身模塊child_process引用而來,用來執行命令的,這個函數執行yarnpkg --version來判斷咱們是否正確安裝了yarn,若是沒有正確安裝yarn的話,useYarn依然爲false,無論指沒有指定--use-npm

  • execSync:引用自child_process.execSync,用於執行須要執行的子進程

checkThatNpmCanReadCwd

// See https://github.com/facebook/create-react-app/pull/3355
function checkThatNpmCanReadCwd() {
  const cwd = process.cwd();// 當前的進程目錄
  let childOutput = null; // 保存npm信息
  try {
    // Note: intentionally using spawn over exec since
    // the problem doesn't reproduce otherwise.
    // `npm config list` is the only reliable way I could find
    // to reproduce the wrong path. Just printing process.cwd()
    // in a Node process was not enough.
    childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
    // 至關於執行`npm config list`並將其輸出的信息組合成爲一個字符串
  } catch (err) {
    // Something went wrong spawning node.
    // Not great, but it means we can't do this check.
    // We might fail later on, but let's continue.
    return true;
  }
  if (typeof childOutput !== 'string') {
    return true;
  }
  //字符串換行分割
  const lines = childOutput.split('\n');
  // `npm config list` output includes the following line:
  // "; cwd = C:\path\to\current\dir" (unquoted)
  // I couldn't find an easier way to get it.
  const prefix = '; cwd = ';// 定義須要的信息前綴
  const line = lines.find(line => line.startsWith(prefix));// 取整個lines裏面的每一個line查找有沒有這個前綴的一行
  if (typeof line !== 'string') {
    // Fail gracefully. They could remove it.
    return true;
  }
  const npmCWD = line.substring(prefix.length);
  if (npmCWD === cwd) {// 判斷當前目錄和執行目錄是否一致
    return true;
  }
  console.error(
    chalk.red( // 不一致就打印如下信息,大概意思就是`npm`進程沒有在正確的目錄下執行
      `Could not start an npm process in the right directory.\n\n` +
        `The current directory is: ${chalk.bold(cwd)}\n` +
        `However, a newly started npm process runs in: ${chalk.bold(
          npmCWD
        )}\n\n` +
        `This is probably caused by a misconfigured system terminal shell.`
    )
  );
  if (process.platform === 'win32') {// 對window的狀況做了單獨判斷
    console.error(
      chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
        chalk.red(`Try to run the above two lines in the terminal.\n`) +
        chalk.red(
          `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
        )
    );
  }
  return false;
}

上述代碼表示已經解析了,其中用到了一個外部依賴:

cross-spawn:用來執行node進程,Node跨平臺解決方案,解決在windows下各類問題npm地址

checkNpmVersion

function checkNpmVersion() {
  let hasMinNpm = false;
  let npmVersion = null;
  try {
    npmVersion = execSync('npm --version')
      .toString()
      .trim();
    hasMinNpm = semver.gte(npmVersion, '5.0.0');
  } catch (err) {
    // ignore
  }
  return {
    hasMinNpm: hasMinNpm,
    npmVersion: npmVersion,
  };
}

檢測node版本,react-scrpts是須要依賴Node版本的,低版本的Node不支持,版本比較使用的是一個semver package.

run

run 主要作的事情就是安裝依賴、拷貝模板。
run()函數在createApp()函數的全部內容執行完畢後執行,它接收7個參數。

  • root:咱們建立的目錄的絕對路徑
  • appName:咱們建立的目錄名稱
  • versionreact-scripts的版本
  • verbose:繼續傳入verbose,在createApp中沒有使用到
  • originalDirectory:原始目錄,這個以前說到了,到run函數中就有用了
  • tempalte:模板,這個參數以前也說過了,不對外使用
  • useYarn:是否使用yarn
  • usePnp: 是否使用pnp
function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp
) {
  Promise.all([
    getInstallPackage(version, originalDirectory), //獲取依賴安裝包信息
    getTemplateInstallPackage(template, originalDirectory),// 獲取模板安裝包信息
  ]).then(([packageToInstall, templateToInstall]) => {
    const allDependencies = ['react', 'react-dom', packageToInstall];

    console.log('Installing packages. This might take a couple of minutes.');

    Promise.all([
      // 從網址或路徑中提取軟件包名稱,也就是獲取安裝包名稱這裏以前用的getPackageName()
      getPackageInfo(packageToInstall),
      getPackageInfo(templateToInstall),
    ])
      .then(([packageInfo, templateInfo]) =>
        checkIfOnline(useYarn).then(isOnline => ({
          isOnline,
          packageInfo,
          templateInfo,
        }))
      )
      .then(({ isOnline, packageInfo, templateInfo }) => {
        let packageVersion = semver.coerce(packageInfo.version);

        const templatesVersionMinimum = '3.3.0';

        // Assume compatibility if we can't test the version.
        if (!semver.valid(packageVersion)) {
          packageVersion = templatesVersionMinimum;
        }

        // Only support templates when used alongside new react-scripts versions.
        const supportsTemplates = semver.gte(
          packageVersion,
          templatesVersionMinimum
        );
        if (supportsTemplates) {
          allDependencies.push(templateToInstall);
        } else if (template) {
          console.log('');
          console.log(
            `The ${chalk.cyan(packageInfo.name)} version you're using ${
              packageInfo.name === 'react-scripts' ? 'is not' : 'may not be'
            } compatible with the ${chalk.cyan('--template')} option.`
          );
          console.log('');
        }

        // TODO: Remove with next major release.
        if (!supportsTemplates && (template || '').includes('typescript')) {
          allDependencies.push(
            '@types/node',
            '@types/react',
            '@types/react-dom',
            '@types/jest',
            'typescript'
          );
        }

        console.log(
          `Installing ${chalk.cyan('react')}, ${chalk.cyan(
            'react-dom'
          )}, and ${chalk.cyan(packageInfo.name)}${
            supportsTemplates ? ` with ${chalk.cyan(templateInfo.name)}` : ''
          }...`
        );
        console.log();
        // 安裝依賴
        return install(
          root,
          useYarn,
          usePnp,
          allDependencies,
          verbose,
          isOnline
        ).then(() => ({
          packageInfo,
          supportsTemplates,
          templateInfo,
        }));
      })
      .then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
        const packageName = packageInfo.name;
        const templateName = supportsTemplates ? templateInfo.name : undefined;
        checkNodeVersion(packageName);// 判斷node_modules下面全部包中對於node版本的最低要求
        setCaretRangeForRuntimeDeps(packageName);// 給package.json中的依賴添加^

        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, templateName],
          `
        var init = require('${packageName}/scripts/init.js');
        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);
      });
  });
}

image.png

run在這裏對react-script作了不少處理,大概是因爲react-script自己是有node版本的依賴的,並且在用create-react-app init <project>初始化一個項目的時候,是能夠指定react-script的版本;簡單來講run 主要作的事情就是安裝依賴、拷貝模板。

其中函數列表:

  • getInstallPackage():獲取要安裝的react-scripts版本或者開發者本身定義的react-scripts
    image.png
  • getTemplateInstallPackage: 獲取模板安裝包信息,根據選擇的模板建立模板安裝包名
    image.png
    image.png
  • getPackageInfo(): 從網址或路徑中提取軟件包名稱,也就是獲取安裝包名稱這裏以前用的getPackageName()

image.png

  • checkIfOnline():檢查網絡鏈接是否正常
  • install():安裝開發依賴包,
  • checkNodeVersion():檢查Node版本信息,判斷node_modules下面全部包中對於node版本的最低要求
  • setCaretRangeForRuntimeDeps():檢查發開依賴是否正確安裝,版本是否正確
  • executeNodeScript(): 安裝依賴同時copy react-scripts下面的template到當前目錄,在create-react-app以前的版本中,這裏是經過調用react-script下的init方法來執行後續動做的。這裏經過調用executeNodeScript方法

getInstallPackage

function getInstallPackage(version, originalDirectory) {
  let packageToInstall = 'react-scripts';
  const validSemver = semver.valid(version);
  if (validSemver) {
    packageToInstall += `@${validSemver}`;
  } else if (version) {
    if (version[0] === '@' && !version.includes('/')) {
      packageToInstall += version;
    } else if (version.match(/^file:/)) {
      packageToInstall = `file:${path.resolve(
        originalDirectory,
        version.match(/^file:(.*)?$/)[1]
      )}`;
    } else {
      // for tar.gz or alternative paths
      packageToInstall = version;
    }
  }

  const scriptsToWarn = [
    {
      name: 'react-scripts-ts',
      message: chalk.yellow(
        `The react-scripts-ts package is deprecated. TypeScript is now supported natively in Create React App. You can use the ${chalk.green(
          '--template typescript'
        )} option instead when generating your app to include TypeScript support. Would you like to continue using react-scripts-ts?`
      ),
    },
  ];

  for (const script of scriptsToWarn) {
    if (packageToInstall.startsWith(script.name)) {
      return inquirer
        .prompt({
          type: 'confirm',
          name: 'useScript',
          message: script.message,
          default: false,
        })
        .then(answer => {
          if (!answer.useScript) {
            process.exit(0);
          }

          return packageToInstall;
        });
    }
  }

  return Promise.resolve(packageToInstall);
}

getInstallPackage根據傳入的 version 和原始路徑 originalDirectory 去獲取要安裝的 package 列表,默認狀況下version 爲 undefined,獲取到的 packageToInstall 爲react-scripts,也就是咱們如上圖的 resolve 回調

getTemplateInstallPackage

function getTemplateInstallPackage(template, originalDirectory) {
  let templateToInstall = 'cra-template';
  if (template) {
    if (template.match(/^file:/)) {
      templateToInstall = `file:${path.resolve(
        originalDirectory,
        template.match(/^file:(.*)?$/)[1]
      )}`;
    } else if (
      template.includes('://') ||
      template.match(/^.+\.(tgz|tar\.gz)$/)
    ) {
      // for tar.gz or alternative paths
      templateToInstall = template;
    } else {
      // Add prefix 'cra-template-' to non-prefixed templates, leaving any
      // @scope/ intact.
      const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/);
      const scope = packageMatch[1] || '';
      const templateName = packageMatch[2];

      const name = templateName.startsWith(templateToInstall)
        ? templateName
        : `${templateToInstall}-${templateName}`;
      templateToInstall = `${scope}${name}`;
    }
  }

  return Promise.resolve(templateToInstall);
}

image.png
image.png
getTemplateInstallPackage建立一個cra-template模板前綴,把傳入的模板名稱合併返回一個新的模板名稱,

getPackageInfo

// Extract package name from tarball url or path.
// 從網址或路徑中提取軟件包名稱
function getPackageInfo(installPackage) {
  if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {// 判斷`react-scripts`的信息來安裝這個包,用於返回正規的包名
    return getTemporaryDirectory() //建立一個臨時目錄
      .then(obj => {
        let stream;
        if (/^http/.test(installPackage)) {
          stream = hyperquest(installPackage);
        } else {
          stream = fs.createReadStream(installPackage);
        }
        return extractStream(stream, obj.tmpdir).then(() => obj);
      })
      .then(obj => {
        const { name, version } = require(path.join(
          obj.tmpdir,
          'package.json'
        ));
        obj.cleanup();
        return { name, version };
      })
      .catch(err => {
        // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
        // However, this function returns package name only without semver version.
        console.log(
          `Could not extract the package name from the archive: ${err.message}`
        );
        const assumedProjectName = installPackage.match(
          /^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
        )[1];
        console.log(
          `Based on the filename, assuming it is "${chalk.cyan(
            assumedProjectName
          )}"`
        );
        return Promise.resolve({ name: assumedProjectName });
      });
  } else if (installPackage.startsWith('git+')) { // 此處爲信息中包含`git+`信息的狀況
    // Pull package name out of git urls e.g:
    // git+https://github.com/mycompany/react-scripts.git
    // git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
    return Promise.resolve({
      name: installPackage.match(/([^/]+)\.git(#.*)?$/)[1],
    });
  } else if (installPackage.match(/.+@/)) { // 此處爲只有版本信息的時候的狀況
    // Do not match @scope/ when stripping off @version or @tag
    return Promise.resolve({
      name: installPackage.charAt(0) + installPackage.substr(1).split('@')[0],
      version: installPackage.split('@')[1],
    });
  } else if (installPackage.match(/^file:/)) {  // 此處爲信息中包含`file:`開頭的狀況
    const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
    const { name, version } = require(path.join(
      installPackagePath,
      'package.json'
    ));
    return Promise.resolve({ name, version });
  }
  return Promise.resolve({ name: installPackage });
}

這個函數的做用就是返回正常的包名,不帶任何符號的,這裏使用了一個外部依賴hyperquest,看了下之前版本的源碼,這個函數以前是
getPackageName()同一個方法
image.png

install

function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
  return new Promise((resolve, reject) => {  // 封裝在一個回調函數中
    let command;// 定義一個命令
    let args;// 定義一個命令的參數
    if (useYarn) { // 若是使用yarn
      command = 'yarnpkg';
      args = ['add', '--exact'];
      if (!isOnline) {
        args.push('--offline'); // 是否離線模式
      }
      if (usePnp) {
        args.push('--enable-pnp');
      }
      [].push.apply(args, dependencies); // 組合參數和開發依賴 `react` `react-dom` `react-scripts`

      // Explicitly set cwd() to work around issues like
      // https://github.com/facebook/create-react-app/issues/3326.
      // Unfortunately we can only do this for Yarn because npm support for
      // equivalent --prefix flag doesn't help with this issue.
      // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
      args.push('--cwd');// 指定命令執行目錄的地址
      args.push(root);// 地址的絕對路徑

      if (!isOnline) { // 離線模式時候會發出警告
        console.log(chalk.yellow('You appear to be offline.'));
        console.log(chalk.yellow('Falling back to the local Yarn cache.'));
        console.log();
      }
    } else { //使用npm 的狀況
      command = 'npm';
      args = [
        'install',
        '--save',
        '--save-exact',
        '--loglevel',
        'error',
      ].concat(dependencies);

      if (usePnp) { // 若是是pnp發出警告npm 不支持pnp
        console.log(chalk.yellow("NPM doesn't support PnP."));
        console.log(chalk.yellow('Falling back to the regular installs.'));
        console.log();
      }
    }

    if (verbose) {
      args.push('--verbose');
    }
    // 在spawn執行完命令後會有一個回調,判斷code是否爲 0,而後 resolve Promise,
    const child = spawn(command, args, { stdio: 'inherit' });
    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(' ')}`,
        });
        return;
      }
      resolve();
    });
  });
}

image.png
install就是把梳理好的package交給npm或者yarn安裝依賴

executeNodeScript

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

executeNodeScript方法主要是經過 spawn 來經過 node命令執行react-script下的 init 方法

react-script/init.js

module.exports = function(
  appPath,
  appName,
  verbose,
  originalDirectory,
  templateName
) {
  const appPackage = require(path.join(appPath, 'package.json')); //項目目錄下的 package.json
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //經過判斷目錄下是否有 yarn.lock 來判斷是否使用 yarn

  if (!templateName) { // 判斷是否有模板名稱
    console.log('');
    console.error(
      `A template was not provided. This is likely because you're using an outdated version of ${chalk.cyan(
        'create-react-app'
      )}.`
    );
    console.error(
      `Please note that global installs of ${chalk.cyan(
        'create-react-app'
      )} are no longer supported.`
    );
    return;
  }

  const templatePath = path.join( //獲取模板的路徑
    require.resolve(templateName, { paths: [appPath] }),
    '..'
  );

  let templateJsonPath;
  if (templateName) {  //
    templateJsonPath = path.join(templatePath, 'template.json');
  } else {
    // TODO: Remove support for this in v4.
    templateJsonPath = path.join(appPath, '.template.dependencies.json');
  }

  let templateJson = {};
  if (fs.existsSync(templateJsonPath)) { //判斷路徑是否正確
    templateJson = require(templateJsonPath);
  }

  // Copy over some of the devDependencies
  appPackage.dependencies = appPackage.dependencies || {};

  // Setup the script rules 定義scripts
  const templateScripts = templateJson.scripts || {};
  appPackage.scripts = Object.assign(
    {
      start: 'react-scripts start',
      build: 'react-scripts build',
      test: 'react-scripts test',
      eject: 'react-scripts eject',
    },
    templateScripts
  );

  // Update scripts for Yarn users
  if (useYarn) {// 判斷是否使用yarn,若是是替換成你怕嗎 npm 
    appPackage.scripts = Object.entries(appPackage.scripts).reduce(
      (acc, [key, value]) => ({
        ...acc,
        [key]: value.replace(/(npm run |npm )/, 'yarn '),
      }),
      {}
    );
  }

  // Setup the eslint config 設置eslint配置
  appPackage.eslintConfig = {
    extends: 'react-app',
  };

  // Setup the browsers list  這是瀏覽器 openBrowser
  appPackage.browserslist = defaultBrowsers;

  fs.writeFileSync( //寫入咱們須要建立的目錄下的 package.json 中
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  // 判斷項目目錄是否有`README.md`,模板目錄中已經定義了`README.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')
    );
  }

  // Copy the files for the user
  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;
  }

  // modifies README.md commands based on user used package manager.
  if (useYarn) {// 判斷是否使用yarn,若是是替換成你怕嗎 npm 默認使用npm
    try {
      const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8');
      fs.writeFileSync(
        path.join(appPath, 'README.md'),
        readme.replace(/(npm run |npm )/g, 'yarn '),
        'utf8'
      );
    } catch (err) {
      // Silencing the error. As it fall backs to using default npm commands.
    }
  }

  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'),
      []
    );
  }

  let command;
  let remove;
  let args;

  if (useYarn) {
    command = 'yarnpkg';
    remove = 'remove';
    args = ['add'];
  } else {
    command = 'npm';
    remove = 'uninstall';
    args = ['install', '--save', verbose && '--verbose'].filter(e => e);
  }

  // Install additional template dependencies, if present 安裝其餘模板依賴項若是有
  const templateDependencies = templateJson.dependencies;
  if (templateDependencies) {
    args = args.concat(
      Object.keys(templateDependencies).map(key => {
        return `${key}@${templateDependencies[key]}`;
      })
    );
  }

  // Install react and react-dom for backward compatibility with old CRA cli
  // which doesn't install react and react-dom along with react-scripts
  if (!isReactInstalled(appPackage)) {
    args = args.concat(['react', 'react-dom']);
  }
  // 安裝react和react-dom以便與舊CRA cli向後兼容
  // 沒有安裝react和react-dom以及react-scripts
  // 或模板是presetend(經過--internal-testing-template)

  // Install template dependencies, and react and react-dom if missing.
  if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) {
    console.log();
    console.log(`Installing template dependencies using ${command}...`);

    const proc = spawn.sync(command, args, { stdio: 'inherit' });
    if (proc.status !== 0) {
      console.error(`\`${command} ${args.join(' ')}\` failed`);
      return;
    }
  }

  if (args.find(arg => arg.includes('typescript'))) {
    console.log();
    verifyTypeScriptSetup();
  }

  // Remove template
  console.log(`Removing template package using ${command}...`);
  console.log();

  const proc = spawn.sync(command, [remove, templateName], {
    stdio: 'inherit',
  });
  if (proc.status !== 0) {
    console.error(`\`${command} ${args.join(' ')}\` failed`);
    return;
  }

  if (tryGitInit(appPath)) {
    console.log();
    console.log('Initialized a git repository.');
  }

  // Display the most elegant way to cd.
  // This needs to handle an undefined originalDirectory for
  // backward compatibility with old global-cli's.
  // 顯示最優雅的CD方式。
  // 這須要處理一個未定義的originalDirectory
  // 與舊的global-cli的向後兼容性。
  let cdpath;
  if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
    cdpath = appName;
  } else {
    cdpath = appPath;
  }

  // Change displayed command to yarn instead of yarnpkg
  // 將顯示的命令更改成yarn而不是yarnpkg
  const displayedCommand = useYarn ? 'yarn' : 'npm';

  console.log();
  console.log(`Success! Created ${appName} at ${appPath}`);
  console.log('Inside that directory, you can run several commands:');
  console.log();
  console.log(chalk.cyan(`  ${displayedCommand} start`));
  console.log('    Starts the development server.');
  console.log();
  console.log(
    chalk.cyan(`  ${displayedCommand} ${useYarn ? '' : 'run '}build`)
  );
  console.log('    Bundles the app into static files for production.');
  console.log();
  console.log(chalk.cyan(`  ${displayedCommand} test`));
  console.log('    Starts the test runner.');
  console.log();
  console.log(
    chalk.cyan(`  ${displayedCommand} ${useYarn ? '' : 'run '}eject`)
  );
  console.log(
    '    Removes this tool and copies build dependencies, configuration files'
  );
  console.log(
    '    and scripts into the app directory. If you do this, you can’t go back!'
  );
  console.log();
  console.log('We suggest that you begin by typing:');
  console.log();
  console.log(chalk.cyan('  cd'), cdpath);
  console.log(`  ${chalk.cyan(`${displayedCommand} start`)}`);
  if (readmeExists) {
    console.log();
    console.log(
      chalk.yellow(
        'You had a `README.md` file, we renamed it to `README.old.md`'
      )
    );
  }
  console.log();
  console.log('Happy hacking!');
}

image.png
初始化方法主要作的事情就是修改目標路徑下的 package.json,添加一些配置命令,而後 copy!react-script 下的模板到目標路徑下

image.png
項目初始化完成

簡單總結下

  • 判斷 node 版本,若是大版本小於 8 ,則直接退出(截止目前是 8)
  • createReactApp.js 初始化一些命令參數,而後再去判斷是否傳入了 packageName,不然直接退出
  • 各類版本的判斷,而後經過cross-spawn來用命令行執行全部的安裝
  • 當全部的依賴安裝完後,依舊經過命令行,初始化 node 環境,來執行 react-script 下的初始化方法:修改 package.json 中的一些配置、以及 copy 模板文件
  • 處理完成,給出用戶友好提示

看完大佬的分享,在本身跑一遍果真理順不少,感謝大佬的分享,寫出來也是記錄自我一個學習的過程,方便後期查看

相關文章
相關標籤/搜索