create-react-app教程-源码篇

原文链接:create-react-app教程-源码篇

之前介绍了create-react-app的基本使用, 为了便于理解一个脚手架脚本是如何运作的,现在来看一下 create-react-app v1.5.2 的源码

入口index.js

create-react-app 一般会作为全局命令,因为便于更新等原因,create-react-app 只会做初始化仓库 执行当前版本命令等操作。

找到 create-react-app 入口index文件:

'use strict';
var chalk = require('chalk');
// 返回Node版本信息,如果有多个版本返回多个版本
var currentNodeVersion = process.versions.node; 
var semver = currentNodeVersion.split('.');
var major = semver[0];// 取出第一个Node版本信息
//小于 4.x的提示并终止程序
if (major < 4) {
  console.error(
    chalk.red(
      'You are running Node ' +
        currentNodeVersion +
        '.\n' +
        'Create React App requires Node 4 or higher. \n' +
        'Please update your version of Node.'
    )
  );
  process.exit(1);
}
// 没有小于4就引入以下文件继续执行
require('./createReactApp');
复制代码

可以看到 index 文件没有做什么,只是做为一个入口文件判断一下 node版本,小于 4.x的提示并终止程序, 如果正常则加载 ./createReactApp 这个文件,主要的逻辑在该文件实现。

createReactApp.js

虽然 createReactApp.js 有751行,但是里面有一大半是注释和错误友好信息。

除了声明的依赖。跟着执行顺序先看到的是第56行 program

const program = new commander.Command(packageJson.name)
  .version(packageJson.version)// create-react-app -v 时输出 ${packageJson.version}
  .arguments('<project-directory>')// 这里用<> 包着project-directory 表示 project-directory为必填项
  .usage(`${chalk.green('<project-directory>')} [options]`)// 用绿色字体输出 <project-directory>
  .action(name => {
    projectName = name;
  })// 获取用户传入的第一个参数作为 projectName
  .option('--verbose', 'print additional logs')
  // option用于配置`create-react-app -[option]`的选项,
  //比如这里如果用户参数带了 --verbose, 会自动设置program.verbose = true;
  .option('--info', 'print environment debug info')
  // info,用于打印出环境调试的版本信息
  .option(
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  )
  .option('--use-npm')// 默认使用`yarn`,指定使用`npm`
  .allowUnknownOption()
  .on('--help', () => {
    //help 信息
  })
  .parse(process.argv);// 解析传入的参数 
复制代码

这里用到 commander 的依赖,这是 node.js 命令行接口的解决方案,正如我们所看到的 处理用户输入的参数,输出友好的提示信息等。

接着到了第109行:

//没有输入projectName的话,输出一些提示信息就终止程序
if (typeof projectName === 'undefined') {
    if (program.info) {// 如果参数输入了 --info,就会进入这里
    envinfo.print({// envinfo 是一个用来输出当前环境系统的而一些系统信息
      packages: ['react', 'react-dom', 'react-scripts'],
      noNativeIDE: true,
      duplicates: true,
    });
    process.exit(0);
  }
  //略去部分log...
  process.exit(1);
}
复制代码

这里的 projectName 就是我们要创建的web应用名称,如果没有输入的话,输出一些提示信息就终止程序。

createApp 检测判断

然后到了第148行 执行createApp

createApp(
  projectName,//项目名称 
  program.verbose, //是否暑促额外信息
  program.scriptsVersion, //传入的脚本版本
  program.useNpm, //是否使用npm
  hiddenProgram.internalTestingTemplate //调试的模板路径,这个不管它,给开发人员调试用的……
);

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name);// 获取当前进程运行的位置,也就是文件目录的绝对路径
  const appName = path.basename(root);// 返回root路径下最后一部分

  checkAppName(appName);// 检查传入的项目名合法性
  fs.ensureDirSync(name);//这里的 fs = require('fs-extra');
  if (!isSafeToCreateProjectIn(root, name)) {
    process.exit(1);
  }

  // 写入 package.json 文件
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2)
  );

  const useYarn = useNpm ? false : shouldUseYarn();
  const originalDirectory = process.cwd();
  process.chdir(root);// 在这里就把进程目录修改为了我们创建的目录
  // 如果是使用npm,检查npm是否能正常执行
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  }
  //这里的 semver = require('semver'); 做版本处理的
  //如果node版本不符合要求就使用旧版本的 react-scripts
  if (!semver.satisfies(process.version, '>=6.0.0')) {
    //略去log信息...
    // Fall back to latest supported react-scripts on Node 4
    version = 'react-scripts@0.9.x';
  }

  // 如果npm版本小于3.x,使用旧版的 react-scripts
  if (!useYarn) {
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {
      //略去log信息...
      // Fall back to latest supported react-scripts for npm 3
      version = 'react-scripts@0.9.x';
    }
  }
  // 判断结束之后,执行 run 方法
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
复制代码

可以了解到 createApp 主要做的事情就是做一些安全判断比如:检查项目名是否合法,检查新建的话是否安全,检查npm版本,处理react-script的版本兼容。然后看下在createApp中用到的 checkAppName

checkAppName 检查项目名
function checkAppName(appName) {
  //这里 validateProjectName = require('validate-npm-package-name');
  //可以用来判断当前的项目名是否符合npm规范 比如不能大写等
  const validationResult = validateProjectName(appName);
  // 判断是否符合npm规范如果不符合,输出提示并结束任务
  if (!validationResult.validForNewPackages) {
    //略去log信息...
    process.exit(1);
  }

  const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
  // 判断是否重名,如果重名则输出提示并结束任务
  if (dependencies.indexOf(appName) >= 0) {    
    //略去log信息...
    process.exit(1);
  }
}
复制代码

run 安装依赖拷贝模版

在 createApp 方法体内调用了run方法,run方法体内完成主要的安装依赖 拷贝模板等功能。

function run(root,appName,version,verbose,originalDirectory,template,useYarn) {
  // 这里获取要安装的package,
  //    getInstallPackage 默认情况下packageToInstall是 `react-scripts`。
  //    也可能是根据去本地拿到对应的package
  //    react-scripts是一系列的webpack配置与模版
  const packageToInstall = getInstallPackage(version, originalDirectory);
  // 需要安装所有的依赖
  const allDependencies = ['react', 'react-dom', packageToInstall];

  // getPackageName 获取依赖包原始名称并返回
  getPackageName(packageToInstall)
    .then(packageName =>
      // 如果是yarn,判断是否在线模式(对应的就是离线模式),处理完判断就返回给下一个then处理
      checkIfOnline(useYarn).then(isOnline => ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info => {
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      //略去log信息...      
      //传参数给install 负责安装 allDependencies
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        () => packageName
      );
    })
    .then(packageName => {
      //检查当前环境运行的node版本是否符合要求
      checkNodeVersion(packageName);
      //修改react, react-dom的版本信息,将准确版本信息改为高于等于版本
      //    例如 15.0.0 => ^15.0.0
      setCaretRangeForRuntimeDeps(packageName);
      // `react-scripts`脚本的目录
      const scriptsPath = path.resolve(
        process.cwd(),
        'node_modules',
        packageName,
        'scripts',
        'init.js'
      );
      const init = require(scriptsPath);
      //调用安装了的 react-scripts/script/init 去拷贝模版
      init(root, appName, verbose, originalDirectory, template);
      //略去log信息...  
    })
    
    .catch(reason => {
      // 出错的话,把安装了的文件全删了 并输出一些日志信息等
      // 错误处理 略
      process.exit(1);
    });
}
复制代码

可以猜到其中最重要的逻辑是 install 安装依赖和 init 拷贝模板。

install 安装依赖

install 方法体中是根据参数拼装命令行,然后用node去跑安装脚本 ,执行完成后返回一个 Promise

function install(root, useYarn, dependencies, verbose, isOnline) {
  return new Promise((resolve, reject) => {
    let command;
    let args;
    // 参数拼装命令行,
    //    例如 使用yarn : `yarn add react react-dom`
    //    或 使用npm : `npm install react react-dom --save` 
    if (useYarn) {
      command = 'yarnpkg';
      args = ['add', '--exact'];
      if (!isOnline) {
        args.push('--offline');
      }
      [].push.apply(args, dependencies);
      args.push('--cwd');
      args.push(root);
      //略去log信息... 
    } else {
      command = 'npm';
      args = [
        'install',
        '--save',
        '--save-exact',
        '--loglevel',
        'error',
      ].concat(dependencies);
    }

    if (verbose) {
      args.push('--verbose');
    }
    //然后用node去跑安装脚本 
    //这里 spawn = require('cross-spawn'); 出来处理平台差异
    const child = spawn(command, args, { stdio: 'inherit' });
    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(' ')}`,
        });
        return;
      }
      resolve();
    });
  });
}
复制代码

init 拷贝模板

init 方法默认是在 【当前web项目路径】/node_modules/react-scripts/script/init.js 中 :

module.exports = function(
  appPath,
  appName,
  verbose,
  originalDirectory,
  template
) {
  const ownPath = path.dirname(
    require.resolve(path.join(__dirname, '..', 'package.json'))
  );
  const appPackage = require(path.join(appPath, 'package.json'));
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
  appPackage.dependencies = appPackage.dependencies || {};

  const useTypeScript = appPackage.dependencies['typescript'] != null;

  // 设置package.json 中 scripts/eslint/browserslist 信息
  appPackage.scripts = {
    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',
  };
  appPackage.eslintConfig = {
    extends: 'react-app',
  };
  appPackage.browserslist = defaultBrowsers;

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

  // 如果已有 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')
    );
  }

  //把预设的模版拷贝到项目下
  //  可以在 react-scripts/template 看到这些文件 public目录 src目录 gitignore README.md
  const templatePath = template
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
  if (fs.existsSync(templatePath)) {
    fs.copySync(templatePath, appPath);
  } else {
    console.error(
      `Could not locate supplied template: ${chalk.green(templatePath)}`
    );
    return;
  }

  // 如果发现没有安装react和react-dom,重新安装一次 代码略

  // Install additional template dependencies, if present
  //略去log信息... 
};
复制代码

简化一下逻辑这里的主要内容就是 修改package.json信息和拷贝模板文件

~END~

问题中提到了使用命令"create-react-app"时出现了"Unknown command: 'create-react-app'"的错误。 根据引用中的提示,如果改了环境变量还不行,你可以尝试使用命令"npx create-react-app react-cli-app"。这个命令会自动下载并执行create-react-app工具,从而创建React应用程序。 另外,引用中提到了一个解决方案。你可以右击"我的电脑",选择"属性",然后点击"高级系统设置",再点击"环境变量"。在安装Node.js时,你可以指定一个node_global的地址。将这个地址添加到环境变量的path中,这样就可以解决"Unknown command"的问题。 综上所述,你可以尝试使用命令"npx create-react-app react-cli-app"来创建React应用程序,并确保在环境变量中添加了正确的node_global地址。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [create-react-app不是内部或外部命令,也不是可运行的程序?](https://blog.csdn.net/qq_44930379/article/details/117525875)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [create-react-redux-app:基于create-react-app的React样板](https://download.csdn.net/download/weixin_42131443/15070891)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值