create-react-app作了什麼

create-react-app(v3.7.2)能夠很快很方便初始化一個 react開發項目,這個東西究竟是怎樣運做的,作了哪些處理呢?今天揭開內部祕密。源碼用到的一些有用的第三方庫也列了出來,方便之後你們在本身的 cli中使用。

初始化命令

// CRA的全部的命令以下
/** npm package commander: 命令基礎工具 */
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(
    '--template <path-to-template>',
    'specify a template for the created project'
  )
  .option('--use-npm')
  .option('--use-pnp')
  // TODO: Remove this in next major release.
  .option(
    '--typescript',
    '(this option will be removed in favour of templates in the next major release of create-react-app)'
  )
  .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(` 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);
複製代碼

-V, --version版本號輸出node

// 當前create-react-app版本號輸出
 new commander.Command(packageJson.name)
  .version(packageJson.version) // 默認已經生成該命令選項
複製代碼

--verbose 展現詳細的logsreact

--info 展現當前系統以及環境的一些信息git

// 源碼中,若是命令中有這個參數, 則會執行
/** npm package: envinfo: 快速獲取當前各類軟件環境的信息 */
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);
複製代碼

--scripts-version 指定一個特定的react-scripts運行腳本github

--template 指定項目的模板,能夠指定一個本身的模板typescript

--use-pnp 使用pnp --> pnp是什麼npm

--typescript 使用ts開發,以後版本會移除這個選項json

這個選項即將被棄用,可使用--template typescript代替app

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';
    }
  }
複製代碼

開始建立項目

建立項目會調用createApp方法, node版本要求>=8.10.0, 低於這個版本會拋錯less

createAppdom

首先會先調用createApp方法

createApp(
  projectName, // 項目名稱
  program.verbose, // --verbose
  program.scriptsVersion, // --scripts-version
  program.template, // --template
  program.useNpm, // --use-npm
  program.usePnp, // --use-pnp
  program.typescript // --typescript
);
複製代碼

建立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
);
複製代碼

若是有使用yarn, 會先將當前目錄下的yarn.lock.cached文件拷貝到項目根目錄下並重命名爲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

接着調用run,繼續建立新項目

/** npm 包 semver: 版本號校驗以及比較等的工具庫 */

run(
    root,
    appName,
    version, // scriptsVersion
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp
  );
  
複製代碼

處理react-scripts引用腳本和--template入參

// ...
let packageToInstall = 'react-scripts';
// ...
// 將所用到的依賴蒐集
const allDependencies = ['react', 'react-dom', packageToInstall];
Promise.all([
    getInstallPackage(version, originalDirectory),
    getTemplateInstallPackage(template, originalDirectory),
])
複製代碼

調用getInstallPackage處理react-scripts的使用 --scripts-version選項的入參能夠爲多種:

  • 標準的版本號: '1.2.3','@x.x.x'之類的,指定使用react-scripts的版本 -> react-scripts@x.x.x
  • 指定本地自定義文件:'file:xxx/xxx',返回一個本地絕對路徑
  • 其餘路徑(for tar.gz or alternative paths):能夠是個線上npm包,直接返回這個路徑 因爲默認支持typescript模板,若指定scriptsVersionreact-scripts-ts,會有確認提示
/** npm package inquirer: 輸入輸出交互處理工具 */
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;
        });
    }
  }
複製代碼

調用getTemplateInstallPackage處理--template的使用

  • 指定模板爲一個file:開頭的本地文件
  • 不帶協議的連接://或者tgz|tar.gz壓縮包
  • 相似@xxx/xxx/xxxx或者@xxxx的指定路徑或者模板名字
const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/);
   const scope = packageMatch[1] || '';
   const templateName = packageMatch[2];
    if (
       templateName === templateToInstall ||
       templateName.startsWith(`${templateToInstall}-`)
     ) {
       // Covers:
       // - cra-template
       // - @SCOPE/cra-template
       // - cra-template-NAME
       // - @SCOPE/cra-template-NAME
       templateToInstall = `${scope}${templateName}`;
     } else if (templateName.startsWith('@')) {
       // Covers using @SCOPE only
       templateToInstall = `${templateName}/${templateToInstall}`;
     } else {
       // Covers templates without the `cra-template` prefix:
       // - NAME
       // - @SCOPE/NAME
       templateToInstall = `${scope}${templateToInstall}-${templateName}`;
     }
     // cra-template: This is the official base template for Create React App.
複製代碼

最終處理成@xxx/cra-template或者@xxx/cra-template-xxxcra-template-xxxcra-template, 官方指定的兩個模板爲cra-template-typescriptcra-template。模板具體內容你們能夠去 官方倉庫 去查看,能夠本身自定義或者魔改一些東西

接着獲取--scripts-version--template處理後的安裝包信息

/** npm package tem: 用於在node.js環境中建立臨時文件和目錄。 hyperquest: 將http請求轉化爲流(stream)輸出 tar-pack: tar/gz的壓縮或者解壓縮 */
Promise.all([
    getPackageInfo(packageToInstall),
    getPackageInfo(templateToInstall),
])

// getPackageInfo是一個頗有用的工具函數
// Extract package name from tarball url or path.
function getPackageInfo(installPackage) {
  if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
    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+')) {
    // 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:/)) {
    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 });
}

function extractStream(stream, dest) {
  return new Promise((resolve, reject) => {
    stream.pipe(
      unpack(dest, err => {
        if (err) {
          reject(err);
        } else {
          resolve(dest);
        }
      })
    );
  });
}
複製代碼

run方法主要的工做就是處理--scripts-version--template提供的包,蒐集項目的依賴

install

run處理收集完依賴後會調用install方法

return install(
  root, // 項目的名稱
  useYarn,
  usePnp,
  allDependencies,
  verbose,
  isOnline // 若使用yarn,dns.lookup檢測registry.yarnpkg.com是否正常的結果
)
// install主要是處理安裝前的一些命令參數處理以及上面蒐集依賴的安裝
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
  return new Promise((resolve, reject) => {
    let command;
    let args;
    if (useYarn) {
      command = 'yarnpkg';
      args = ['add', '--exact'];
      if (!isOnline) {
        args.push('--offline');
      }
      if (usePnp) {
        args.push('--enable-pnp');
      }
      [].push.apply(args, dependencies);
      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 {
      command = 'npm';
      args = [
        'install',
        '--save',
        '--save-exact',
        '--loglevel',
        'error',
      ].concat(dependencies);

      if (usePnp) {
        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');
    }

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

複製代碼

依賴安裝後檢查react-scripts執行包的版本與當前的node版本是否匹配,檢查reactreact-dom是否正確安裝,並在它們的版本號前面加^(上面安裝命令帶有exact選項,會精確安裝依賴,版本號不帶^),將依賴從新寫入package.json

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])); `
);

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();
    });
  });
}
複製代碼

正確檢查依賴後將執行提供的scripts腳本下的init初始化:

  1. package.json添加scripts/eslintConfig/browserslist等配置
  2. 若是存在README.md,將其重命名README.old.md
  3. 將模板拷貝到項目目錄,根據是否使用yarn,將模板README.md的命令說明給爲yarn
  4. 安裝模板依賴,若是是ts項目則初始化相關配置(verifyTypeScriptSetup
  5. 移除node_modules的模板
  6. 初始化git相關

到此整個項目建立完畢

梳理

  1. 初始化腳手架各類命令
  2. 建立package.json
  3. 處理並驗證命令參數,蒐集依賴
  4. 安裝前置依賴
  5. 處理package.jsonreact/react-dom依賴版本號,並校驗node版本是否符合要求
  6. 驗證所提供的react-scripts依賴,並經過子進程調用依賴下的react-scripts/scripts/init.js,進行項目模板初始化
  7. 安裝模板依賴,用到ts則初始化其配置
  8. 驗證代碼倉庫,作兼容處理;若沒有添加倉庫,初始化git

寫在最後

create-react-app這個包的源碼相對簡單,可是很是細密精煉,整個流程很是清晰,絕對是一個cli的範本,感興趣的小夥伴能夠本身閱讀。文正若是有不正確的地方歡迎指正批評!

相關文章
相關標籤/搜索