likes
comments
collection
share

React源码解析系列(十四) -- create-react-app源码解读

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

前言

本章主要带大家来一起看一下React官方提供的快速开发脚手架,因为CRA本身构建出来的工程化项目只是包含了基础的页面包,像路由状态管理库等都是没有的,他也不像vue-cli一样能够支持可选项配置,后面我们可能会定制一下属于我们自己的CRA来快速开发。工欲善其事,必先利其器,我们只有了解了官方的CRA细节,才知道下一步怎么去定制自己的CRA,废话少说,开整!

基础介绍

我们从CRA仓库 clone下来的项目长这样。

React源码解析系列(十四) -- create-react-app源码解读

官方提供了npmnpx的命令来安装CRA

  • 全局安装CRA

    • npm i -g create-react-app
  • CRA来创建项目

    • create-react-app my-app
  • npx先检查一下本地CRA,没有的话就去下载,再创建项目。

    • npx create-react-app my-app
  • 在项目根目录运行npm run start:

React源码解析系列(十四) -- create-react-app源码解读

特点:

  • CRA脚手架: 用来帮助程序员快速创建一个基于react库的模板项目,包含了所有需要的配置(语法检查jsx编译devServer…)。
  • 下载好了所有相关的依赖,可以直接运行一个简单效果
  • 项目的整体技术架构为: react + webpack + es6 + eslint
  • 使用脚手架开发的项目的特点: 模块化, 组件化, 工程化

如何调试源码

我们可以打开源码目录的package.json文件,我们可以清晰的看到CRA的入口文件为:

React源码解析系列(十四) -- create-react-app源码解读

所以我们在tasks/cra.js里面进行debugger

React源码解析系列(十四) -- create-react-app源码解读

之后我们在vscode打开调试工具。

React源码解析系列(十四) -- create-react-app源码解读

选择nodejs环境调试

React源码解析系列(十四) -- create-react-app源码解读

添加配置

React源码解析系列(十四) -- create-react-app源码解读

新建RuntimeArgs参数

这里用cra举例。

React源码解析系列(十四) -- create-react-app源码解读

F5或者点击开始调试

进入调试状态

React源码解析系列(十四) -- create-react-app源码解读

源码解读

// cra.js
const craScriptPath = path.join(packagesDir, 'create-react-app', 'index.js');
cp.execSync(
  `node ${craScriptPath} ${args.join(' ')} --scripts-version="${scriptsPath}"`,
  {
    cwd: rootDir,
    stdio: 'inherit',
  }
);

根据调试,在此行代码中加载了create-react-app/index.js,然后用node去执行该文件,所以程序会调到packages/create-react-app/index.js里面去。

const currentNodeVersion = process.versions.node; //获取node版本
const semver = currentNodeVersion.split('.'); // 处理版本
const major = semver[0]; // '16'
if (major < 14) { // 如果node版本小于14则报错
  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'); // 加载createReactApp里面的init方法

init(); // 执行,也就是cra的入口函数

init

function init() {
  debugger;
  // packageJson.name = "create-react-app"
  const program = new commander.Command(packageJson.name) 
    .version(packageJson.version) // 设置版本
    .arguments('<project-directory>') // 必传项目名字
    .usage(`${chalk.green('<project-directory>')} [options]`)
    .action(name => {
      projectName = name; // 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-pnp')
    .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 template 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);
    ...
    ...

init函数开头去new commander.Command(),把create-react-app传入了进去,之后便是很常规的定义版本用法参数等。

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

关于init函数还有如下代码,当没有给定projectName的时候,就会报错,进程结束。

React源码解析系列(十四) -- create-react-app源码解读 给定my-projectName之后,就会进行create-react-app的版本更新。

checkForLatestVersion

checkForLatestVersion()
    .catch(() => {
      try {
        return execSync('npm view create-react-app version').toString().trim();
      } catch (e) {
        return null;
      }
    })
    .then(latest => { // 5.0.1
      if (latest && semver.lt(packageJson.version, latest)) {
        console.log();
        console.error(
          chalk.yellow(
            `You are running \`create-react-app\` 
            ${packageJson.version}, which is behind the latest 
            release (${latest}).\n\n` +
              'We recommend always using the latest version
              of create-react-app if possible.'
          )
        );
        console.log();
        console.log(
          'The latest instructions for creating a new app can be found here:\n' +
            'https://create-react-app.dev/docs/getting-started/'
        );
        console.log();
      } else {
        const useYarn = isUsingYarn(); // 使用yarn
        createApp(
          projectName,
          program.verbose,
          program.scriptsVersion,
          program.template,
          useYarn,
          program.usePnp
        );
      }
    });
}
  • tips:这里可以用这个方法来判断终端用户的使用包管理工具的方式
function isUsingYarn() {
  return (process.env.npm_config_user_agent || '').indexOf('yarn') === 0;
}

create-react-app的最新版本校验通过的时候,就会else逻辑,调用createApp函数。

createApp

先来说一下createApp的参数。

  • projectName:项目名,本次调试用的是my-project
function createApp(name, verbose, version, template, useYarn, usePnp) {
  const unsupportedNodeVersion = !semver.satisfies(
    // Coerce strings with metadata (i.e. `15.0.0-nightly`).
    semver.coerce(process.version),
    '>=14'
  ); // 再一次判断node版本

  if (unsupportedNodeVersion) {
    console.log( // 不支持node版本给提示升级
      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 14 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); // 获得项目更目录
  const appName = path.basename(root); // 获得root的名字

  checkAppName(appName); // 检查app的名字是否合法
  ...

checkAppName

这里来阅读一下别人判断一个合法的名字是怎么做的,在这里他也是借助于validate-npm-package-name这个库实现的。

var validate = module.exports = function (name) {
var warnings = [] // 用来存储warnings
var errors = []  // 用来存储errors

if (name === null) { // 名字不能为null
  errors.push('name cannot be null')
  return done(warnings, errors)
}

if (name === undefined) { // 名字不能为undefined
  errors.push('name cannot be undefined')
  return done(warnings, errors)
}

if (typeof name !== 'string') { // 名字必须为string
  errors.push('name must be a string')
  return done(warnings, errors)
}

if (!name.length) { // 名字不能为空
  errors.push('name length must be greater than zero')
}

if (name.match(/^\./)) { // 名字不能包含./
  errors.push('name cannot start with a period')
}

if (name.match(/^_/)) { // 名字不能包含_/
  errors.push('name cannot start with an underscore')
}

if (name.trim() !== name) {  // 名字不能包含_/
  errors.push('name cannot contain leading or trailing spaces')
}

// 不能包含黑名单 node_modules里面的和favicon.ico
blacklist.forEach(function (blacklistedName) {
  if (name.toLowerCase() === blacklistedName) {
    errors.push(blacklistedName + ' is a blacklisted name')
  }
})

// 不能跟核心模块关键字重名 http, events, util, etc 
builtins.forEach(function (builtin) {
  if (name.toLowerCase() === builtin) {
    warnings.push(builtin + ' is a core module name')
  }
})

// 不允许名字过长 最多214字节
if (name.length > 214) {
  warnings.push('name can no longer contain more than 214 characters')
}

// 不能与命令关键字冲突mIxeD CaSe nAMEs
if (name.toLowerCase() !== name) {
  warnings.push('name can no longer contain capital letters')
}

// 不能含有特殊字符
if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
  warnings.push('name can no longer contain special characters ("~\'!()*")')
}

// 不能与encodeURIComponent处理过后的不一致
if (encodeURIComponent(name) !== name) {
  // Maybe it's a scoped package name, like @user/package
  var nameMatch = name.match(scopedPackagePattern)
  if (nameMatch) {
    var user = nameMatch[1]
    var pkg = nameMatch[2]
    if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
      return done(warnings, errors)
    }
  }
  
  // 报错
  errors.push('name can only contain URL-friendly characters')
}
// 把警告和报错传出去
return done(warnings, errors)
}

经过上述代码阅读,validate-npm-package-name这个包,所能够做的检查也就是包括名字、核心模块、包名、特殊字符等。

生成package.json文件

// 开始在控制台打印创建项目
console.log();
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();

const packageJson = { // 生成package.json文件
  name: appName,
  version: '0.1.0',
  private: true,
};

fs.writeFileSync( // 写入到根目录
  path.join(root, 'package.json'),
  JSON.stringify(packageJson, null, 2) + os.EOL
);

包管理工具的版本检查

  const originalDirectory = process.cwd(); // 获得当前路径
  process.chdir(root); // 开启一个子进程
  
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  } // 不能使用npm 或者yarn 结束进程

  if (!useYarn) { // 不能使用yarn 就检测npm相关
    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 6 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) { // 是否用pnp
    const yarnInfo = checkYarnVersion();
    if (yarnInfo.yarnVersion) { // 检查 yarn版本
      if (!yarnInfo.hasMinYarnPnp) {
        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 (!yarnInfo.hasMaxYarnPnp) {
        console.log(
          chalk.yellow(
            'The --use-pnp flag is no 
            longer necessary with yarn 2 and
            will be deprecated and removed in a future release.\n'
          )
        );
        // 2 supports PnP by default and breaks when trying to use the flag
        usePnp = false;
      }
    }
  }
  
  // 调用方法,开始执行create流程。
  run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp
  );
}

run

function run(
  root, // /Users/mac/Desktop/create-react-app/my-project'
  appName, // my-project
  version,
  verbose,
  originalDirectory, // /Users/mac/Desktop/create-react-app
  template,
  useYarn,
  usePnp
) {
  // 通过Promise.all来并发请求 依赖的信息与模板的信息
  Promise.all([
    getInstallPackage(version, originalDirectory), // resolve('react-scripts')
    getTemplateInstallPackage(
        template, originalDirectory
     ), // resolve('cra-template')
  ])

getInstallPackage

  • getInstallPackage(version, originalDirectory) =>
    • return Promise.resolve('react-scripts')

getTemplateInstallPackage

  • getTemplateInstallPackage( template, originalDirectory ) =>
    • return Promise.resolve('cra-template')

两个函数分别返回了两个Promise实例,通过Promise.all方法,依次接受。

   Promise.all([Promsie.resolve('react-scripts'), Promise.resolve('cra-template')])
  .then(([packageToInstall, templateToInstall]) => {
    // packageToInstall : react-scripts
    // templateToInstall : cra-template
    const allDependencies = ['react', 'react-dom', packageToInstall];
    
    // 开始装包,打印信息
    console.log('Installing packages. This might take a couple of minutes.');

    // 通过Promise.all并行请求,包与模板文件
    Promise.all([
      getPackageInfo(packageToInstall),
      getPackageInfo(templateToInstall),
    ])

getPackageInfo

  • getPackageInfo(packageToInstall) =>
    • return Promise.resolve({ name: 'react-scripts' });

getPackageInfo

  • getPackageInfo(templateToInstall) =>
    • return Promise.resolve({ name: 'cra-template' });
      Promise.all([
          Promise.resolve({ name: 'react-scripts' }), 
          Promise.resolve({ name: 'cra-template' })]
      )
      .then(([packageInfo, templateInfo]) => // 检查包的状态
        checkIfOnline(useYarn).then(isOnline => ({
          isOnline, // true
          packageInfo,
          templateInfo,
        }))
      )
      .then(({ isOnline, packageInfo, templateInfo }) => {
        // 检查版本
        let packageVersion = semver.coerce(packageInfo.version);
        
        // 赋值版本,最低的为3.3.0
        const templatesVersionMinimum = '3.3.0';
        if (!semver.valid(packageVersion)) {
          packageVersion = templatesVersionMinimum;
        }

        // 只有在这个版本下才支持cra-template
        const supportsTemplates = semver.gte(
          packageVersion,
          templatesVersionMinimum
        );
        if (supportsTemplates) {
          // cra-template版本支持之后,让依赖数组里面添加cra-template
          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('');
        }
        
        // 跳出判断,安装react、react-dom、react-scripts、cra-template
        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(...)
      .catch(reason => {
        ...
        // Promise实例返回失败 终止进程
        console.log('Done.');
        process.exit(1);
      });
  });
}

install

先来看一看install函数的关键入参:

  • root:表示根路径。
  • useYarn: 表示用yarn管理工具,这里为true。
  • dependencies: 前面处理过的依赖。

React源码解析系列(十四) -- create-react-app源码解读

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']; // 用yarn的话就用yarn add装包。
      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 { // 用npm install
      command = 'npm';
      args = [
        'install',
        '--no-audit', // https://github.com/facebook/create-react-app/issues/11174
        '--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');
    }
    
    // cross-spawn 批处理脚本,在终端执行npm install
    const child = spawn(command, args, { stdio: 'inherit' });
    
    // 监听子进程 报错就close
    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(' ')}`,
        });
        return;
      }
      resolve(); // 装完了依赖 react、react-dom、cra-template、react-scripts
    });
  });
}

在这里用spawn处理npm i脚本 安装了依赖包,npmj包会执行装包,成功之后会执行行内命令:

const init = require('${packageName}/scripts/init.js'); // packageName: react-scripts
init.apply(null, JSON.parse(process.argv[1])); // 执行init.js

react-scripts run init.js

这一部分代码可以认为是在装包过程中,包的自检流程,那么我们来一起看看吧。

const fs = require('fs-extra');
const path = require('path');
const chalk = require('react-dev-utils/chalk');
const execSync = require('child_process').execSync;
const spawn = require('react-dev-utils/crossSpawn');
const { defaultBrowsers } = require('react-dev-utils/browsersHelper');
const os = require('os');
const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup');
...

module.exports = function (
  appPath, // 包的路径
  appName, // 包的名字
  verbose,
  originalDirectory, // 当前路径
  templateName  // cra-template
) {
  // 获取package.json
  const appPackage = require(path.join(appPath, 'package.json'));
  // 是否使用yarn
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));

  if (!templateName) { // 检测cra-template,如果没有,就会报错,由官方提供。
    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.`
    );
    console.error(
      `You can fix this by running ${chalk.cyan(
        'npm uninstall -g create-react-app'
      )} or ${chalk.cyan(
        'yarn global remove create-react-app'
      )} before using ${chalk.cyan('create-react-app')} again.`
    );
    return;
  }

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

  // This was deprecated in CRA v5.
  if (templateJson.dependencies || templateJson.scripts) {
    console.log();
    console.log(
      chalk.red(
        'Root-level `dependencies` and `scripts
        ` keys in `template.json` were deprecated for Create React App 5.\n' +
          'This template needs to be updated to use the new `package` key.'
      )
    );
    console.log('For more information, visit https://cra.link/templates');
  }

  // Keys to ignore in templatePackage
  const templatePackageBlacklist = [ // 生成package.json字段
    'name',
    'version',
    'description',
    'keywords',
    'bugs',
    'license',
    'author',
    'contributors',
    'files',
    'browser',
    'bin',
    'man',
    'directories',
    'repository',
    'peerDependencies',
    'bundledDependencies',
    'optionalDependencies',
    'engineStrict',
    'os',
    'cpu',
    'preferGlobal',
    'private',
    'publishConfig',
  ];

  // Keys from templatePackage that will be merged with appPackage
  const templatePackageToMerge = ['dependencies', 'scripts'];

  // 添加scripts 和 deps
  const templatePackageToReplace = Object.keys(templatePackage).filter(key => {
    return (
      !templatePackageBlacklist.includes(key) &&
      !templatePackageToMerge.includes(key)
    );
  });
  appPackage.dependencies = appPackage.dependencies || {};

  // 设置scripts
  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
  );

  // 如果是yarn,则处理yarn的命令
  if (useYarn) {
    appPackage.scripts = Object.entries(appPackage.scripts).reduce(
      (acc, [key, value]) => ({
        ...acc,
        [key]: value.replace(/(npm run |npm )/, 'yarn '),
      }),
      {}
    );
  }

  // 设置eslint config
  appPackage.eslintConfig = {
    extends: 'react-app',
  };

  // 设置浏览器
  appPackage.browserslist = defaultBrowsers;

  // 添加模板key:value
  templatePackageToReplace.forEach(key => {
    appPackage[key] = templatePackage[key];
  });

  // 写入package.json
  fs.writeFileSync(
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  // 检查readme文件
  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) {
    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'),
      []
    );
  }

  // 初始化git仓库
  let initializedGit = false;
  if (tryGitInit()) {
    initializedGit = true;
    console.log();
    console.log('Initialized a git repository.');
  }

  let command;
  let remove;
  let args;

  if (useYarn) {
    command = 'yarnpkg';
    remove = 'remove';
    args = ['add']; // yarn add
  } else {
    command = 'npm';
    remove = 'uninstall'; // npm install
    args = [
      'install',
      '--no-audit', // https://github.com/facebook/create-react-app/issues/11174
      '--save',
      verbose && '--verbose',
    ].filter(e => e);
  }

  // 用npm / yarn deps devps
  const dependenciesToInstall = Object.entries({
    ...templatePackage.dependencies,
    ...templatePackage.devDependencies,
  });
  if (dependenciesToInstall.length) {
    args = args.concat(
      dependenciesToInstall.map(([dependency, version]) => {
        return `${dependency}@${version}`;
      })
    );
  }

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

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

  // 创建git commit
  if (initializedGit && tryGitCommit(appPath)) {
    console.log();
    console.log('Created git commit.');
  }

  // 获取启动目录,用npm run serve 还是yarn serve
  let cdpath;
  if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
    cdpath = appName;
  } else {
    cdpath = appPath;
  }

  // Change displayed command to yarn instead of 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!');
};

function isReactInstalled(appPackage) {
  const dependencies = appPackage.dependencies || {};

  return (
    typeof dependencies.react !== 'undefined' &&
    typeof dependencies['react-dom'] !== 'undefined'
  );
}

经过上述代码分析,差不多就是这样的一个流程。

React源码解析系列(十四) -- create-react-app源码解读

npm run start

至此项目就可以完全启动了,推荐使用npm run serve 或者 yarn serve

React源码解析系列(十四) -- create-react-app源码解读

// react-scripts/bin/react-scripts.js
const spawn = require('react-dev-utils/crossSpawn');
const args = process.argv.slice(2); // start

const scriptIndex = args.findIndex( // 0
  x => x === 'build' || x === 'eject' || x === 'start' || x === 'test'
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];

if (['build', 'eject', 'start', 'test'].includes(script)) {
  const result = spawn.sync( // 终端执行行内命令 ../scripts/start.js
    process.execPath,
    nodeArgs
      .concat(require.resolve('../scripts/' + script))
      .concat(args.slice(scriptIndex + 1)),
    { stdio: 'inherit' }
  );
  if (result.signal) {
    if (result.signal === 'SIGKILL') {
      console.log(
        'The build failed because the process exited too early. ' +
          'This probably means the system ran out of memory or someone called ' +
          '`kill -9` on the process.'
      );
    } else if (result.signal === 'SIGTERM') {
      console.log(
        'The build failed because the process exited too early. ' +
          'Someone might have called `kill` or `killall`, or the system could ' +
          'be shutting down.'
      );
    }
    process.exit(1);
  }
  process.exit(result.status);
} else { // 不包括 build start eject test就报错,命令未知。
  console.log('Unknown script "' + script + '".');
  console.log('Perhaps you need to update react-scripts?');
  console.log(
    'See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases'
  );
}

start

执行react-scripts/scripts/start.js文件

// react-scripts/scripts/start.js

// 进来首先做的第一件事情就是 判断环境
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
...
// Ensure environment variables are read.
require('../config/env');

一些工具

const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
  choosePort,
  createCompiler,
  prepareProxy,
  prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const semver = require('semver');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));

一些变量

const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}

// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';

if (process.env.HOST) {
  console.log(
    chalk.cyan(
      `Attempting to bind to HOST environment variable: ${chalk.yellow(
        chalk.bold(process.env.HOST)
      )}`
    )
  );
  console.log(
    `If this was unintentional, check that 
    you haven't mistakenly set it in your shell.`
  );
  console.log(
    `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
  );
  console.log();
}

默认浏览器检查

// We require that you explicitly set browsers and do not fall back to
// 默认浏览器检查
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    // We attempt to use the default port but if it is busy, we offer the user to
    // run on a different port. `choosePort()`
    Promise resolves to the next free port.
    return choosePort(HOST, DEFAULT_PORT);
  })
  .then(port => {
    if (port == null) {
      // We have not found a port.
      return;
    }

    const config = configFactory('development');
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;

    const useTypeScript = fs.existsSync(paths.appTsConfig);
    const urls = prepareUrls(
      protocol,
      HOST,
      port,
      paths.publicUrlOrPath.slice(0, -1)
    );

创建webpack 编译器

    const compiler = createCompiler({
      appName,
      config,
      urls,
      useYarn,
      useTypeScript,
      webpack,
    });

加载代理config

    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );

创建sevServer实例

    // Serve webpack assets generated by the compiler over a web server.
    const serverConfig = {
      ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
      host: HOST,
      port,
    };
    
    
    const devServer = new WebpackDevServer(serverConfig, compiler);
    // Launch WebpackDevServer.
    devServer.startCallback(() => {
      if (isInteractive) {
        clearConsole();
      }

      if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
        console.log(
          chalk.yellow(
            `Fast Refresh requires React 
            16.10 or higher. You are using React ${react.version}.`
          )
        );
      }

      console.log(chalk.cyan('Starting the development server...\n'));
      openBrowser(urls.localUrlForBrowser);
    });

    ['SIGINT', 'SIGTERM'].forEach(function (sig) {
      process.on(sig, function () {
        devServer.close();
        process.exit();
      });
    });

    if (process.env.CI !== 'true') {
      // Gracefully exit when stdin ends
      process.stdin.on('end', function () {
        devServer.close();
        process.exit();
      });
    }
  })
  .catch(err => {
    if (err && err.message) {
      console.log(err.message);
    }
    process.exit(1);
  });

npm run build

其实跟start一样,在react-scripts文件里面区分命令,执行不同的文件,这里执行的是build文件

// 环境判断
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

require('../config/env');

一些工具

const path = require('path');
const chalk = require('react-dev-utils/chalk');
const fs = require('fs-extra');
const bfj = require('bfj');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');

一些变量

const measureFileSizesBeforeBuild =
  FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);

// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;

const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}

const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;

// Generate configuration
const config = configFactory('production');

// We require that you explicitly set browsers and do not fall back to

默认浏览器检查,并计算打包文件大小

const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    // First, read the current file sizes in build directory.
    // This lets us display how much they changed later.
    return measureFileSizesBeforeBuild(paths.appBuild);
  })
  .then(previousFileSizes => {
    // Remove all content but keep the directory so that
    // if you're in it, you don't end up in Trash
    fs.emptyDirSync(paths.appBuild);
    // Merge with the public folder
    copyPublicFolder();
    // Start the webpack build
    return build(previousFileSizes);
  })
  .then(
    ({ stats, previousFileSizes, warnings }) => {
      if (warnings.length) {
        console.log(chalk.yellow('Compiled with warnings.\n'));
        console.log(warnings.join('\n\n'));
        console.log(
          '\nSearch for the ' +
            chalk.underline(chalk.yellow('keywords')) +
            ' to learn more about each warning.'
        );
        console.log(
          'To ignore, add ' +
            chalk.cyan('// eslint-disable-next-line') +
            ' to the line before.\n'
        );
      } else {
        console.log(chalk.green('Compiled successfully.\n'));
      }

      console.log('File sizes after gzip:\n');

输出打包文件信息

      printFileSizesAfterBuild(
        stats,
        previousFileSizes,
        paths.appBuild,
        WARN_AFTER_BUNDLE_GZIP_SIZE,
        WARN_AFTER_CHUNK_GZIP_SIZE
      );
      console.log();

      const appPackage = require(paths.appPackageJson);
      const publicUrl = paths.publicUrlOrPath;
      const publicPath = config.output.publicPath;
      const buildFolder = path.relative(process.cwd(), paths.appBuild);
      printHostingInstructions(
        appPackage,
        publicUrl,
        publicPath,
        buildFolder,
        useYarn
      );
    },
    err => {
      const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
      if (tscCompileOnError) {
        console.log(
          chalk.yellow(
            'Compiled with the following type
            errors (you may want to check these before deploying your app):\n'
          )
        );
        printBuildError(err);
      } else {
        console.log(chalk.red('Failed to compile.\n'));
        printBuildError(err);
        process.exit(1);
      }
    }
  )
  .catch(err => {
    if (err && err.message) {
      console.log(err.message);
    }
    process.exit(1);
  });

build函数

checkBrowsers.then里面开始打包,调用build函数。

function build(previousFileSizes) {
  console.log('Creating an optimized production build...');

  const compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      let messages;
      if (err) {
        if (!err.message) {
          return reject(err);
        }

        let errMessage = err.message;

        // Add additional information for postcss errors
        if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
          errMessage +=
            '\nCompileError: Begins at CSS selector ' +
            err['postcssNode'].selector;
        }

        messages = formatWebpackMessages({
          errors: [errMessage],
          warnings: [],
        });
      } else {
        messages = formatWebpackMessages(
          stats.toJson({ all: false, warnings: true, errors: true })
        );
      }
      if (messages.errors.length) {
        // Only keep the first error. Others are often indicative
        // of the same problem, but confuse the reader with noise.
        if (messages.errors.length > 1) {
          messages.errors.length = 1;
        }
        return reject(new Error(messages.errors.join('\n\n')));
      }
      if (
        process.env.CI &&
        (typeof process.env.CI !== 'string' ||
          process.env.CI.toLowerCase() !== 'false') &&
        messages.warnings.length
      ) {
        // Ignore sourcemap warnings in CI builds. See #8227 for more info.
        const filteredWarnings = messages.warnings.filter(
          w => !/Failed to parse source map/.test(w)
        );
        if (filteredWarnings.length) {
          console.log(
            chalk.yellow(
              '\nTreating warnings as errors because process.env.CI = true.\n' +
                'Most CI servers set it automatically.\n'
            )
          );
          return reject(new Error(filteredWarnings.join('\n\n')));
        }
      }

      const resolveArgs = {
        stats,
        previousFileSizes,
        warnings: messages.warnings,
      };

      if (writeStatsJson) {
        return bfj
          .write(paths.appBuild + '/bundle-stats.json', stats.toJson())
          .then(() => resolve(resolveArgs))
          .catch(error => reject(new Error(error)));
      }

      return resolve(resolveArgs);
    });
  });
}

function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    filter: file => file !== paths.appHtml,
  });
}

不管是serve还是buildreact都给我们写了一套webpack相关的配置,这个配置本身是隐藏的,只要我们执行npm run eject,就会在目录里面生成这些文件,大家可以去尝试的看一下。

总结

这一章我们手把手带领大家debug了一下CRA的源码,以及完成template安装之后启动的源码,CRAVue-cli差不多,都是借助于webpack进行打包的,webpack-dev-server提供一个本地的express服务器,能够很大程度上还原真实项目开发。

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