CRA源码解读

766 阅读4分钟

CRA源码解读

What CRA Is

Create React APP, facebook 为了方便快捷创建React单页面应用而实现的脚手架

How To Use

  1. 首先保证 Node >= 14.0.0 && npm >=5.6

  2. 无需下载 create-react-app, 直接执行 npx create-react-app your-project

  3. 或者指定 react-scripts version or template version (也可以是其他的第三方包)

    npx create-react-app react-app —scripts-version=react-scripts@3.2.0 —template=cra-template-typescript@1.1.0

What Did He Perform (做了什么操作)

执行上述命令之后,对比工作目录变化可以知道:

  1. 创建了一个文件夹 “your-project”
  2. 新增了一个 .git 文件夹
  3. 新增 README.md.gitignorepackage.json 配置文件
  4. 新增项目公共文件夹 public 以及 react 项目代码文件夹 src
  5. 新增了一个 node_modules 文件夹
  6. 查看git提交记录,还会发现多了一条提交记录 Initialize project using Create React App

How To Achieve(怎么实现)

命令行添加

使用 commander 来监听命令行输入, 并将匹配到的命令输入项添加校验。获取文件夹(项目)名称,以及选项中传入的模板值

// Commander 负责将参数解析为选项和命令参数
let projectName
const program = new commander.Command(packageJson.name) // 作为当前已有参数 create-react-app 
    .version(packageJson.version)
    .arguments('<project-directory>')
    .usage(`${chalk.green('<project-directory>')} [options]`)
    .action(name => {
      projectName = name;
    })
    .option('...')
    .option(
      '--template <path-to-template>',
      'specify a template for the created project'
    ).parse(process.argv)

创建项目目录

 fs.ensureDirSync(projectName);

添加 package.json

const root = path.resolve(projectName);
  const appName = path.basename(root);
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL  // 当前系统的换行符, mac or windows
  );

依赖下载

这里会做一系列的校验,比如使用的包管理 工具吗 pnp or yarn or npm, 以及 命令选项传入的 –scripts-version 值判断是否支持模板 (react-scripts >=3.3.0)

command = 'npm';
  args = [
    'install',
    '--no-audit', // <https://github.com/facebook/create-react-app/issues/11174>
    '--save',
    '--save-exact',
    '--loglevel',
    'error',
  ].concat(dependencies); // dependencies = ['react', 'react-dom', 'react-scripts','cra-template']
  // 创建一个子进行执行命令
  const child = spawn(command, args, { stdio: 'inherit' });
  child.on('close', code => {
    if (code !== 0) {
      reject({
        command: `${command} ${args.join(' ')}`,
      });
      return;
    }
    resolve();
  });

执行react-scripts

await executeNodeScript(
  {
    cwd: process.cwd(), // project dir
    args: nodeArgs,
  },
  [root, appName, verbose, originalDirectory, templateName],
  `
const 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, // node 的安装路径
      [...args, '-e', source, '--', JSON.stringify(data)],
      { cwd, stdio: 'inherit' }
    );

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

script init 执行

读取模板json文件

  • require.resolve(path,[options]): 只解析path 的完整路径,不引入文件
const templatePath = path.dirname(
    // require.resolve 解析模板的完整路径
    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 || {};

修改 project package.json 配置项添加

  1. add scripts
appPackage.scripts = Object.assign(
    {
      start: 'react-scripts start',
      build: 'react-scripts build',
      test: 'react-scripts test',
      eject: 'react-scripts eject',
    },
    templateScripts  // 模板里包含的脚本
  );
  1. add browerslist
const { defaultBrowsers } = require('react-dev-utils/browsersHelper');  
appPackage.browserslist = defaultBrowsers;
  1. 模板里一些其他的配置化项(非白名单 templatePackageBlacklist 跟合并配置项 templatePackageToMerge

以 cra-template 为例,

 "eslintConfig": {      "extends": ["react-app", "react-app/jest"]    }
  1. 将上述配置后的内容写入到 package.json
fs.writeFileSync(path.join(appPath, 'package.json'),    
JSON.stringify(appPackage, null, 2) + os.EOL  );

README.md 文件处理

如果存在的 话重命名为 README.old.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')
  );
}

复制模板文件到本地


const templatePath = path.dirname(
  // require.resolve 解析模板的完整路径, 从appPath目录下的 node_modules 开始解析 templateName的路径
  require.resolve(`${templateName}/package.json`, { paths: [appPath] })
);

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

.gitignore 文件处理

模板里放的是 gitignore 文件

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

init git

function isInGitRepository() {
  try {
    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false;
  }
}

function tryGitInit() {
  try {
    execSync('git --version', { stdio: 'ignore' });
    // 即判断当前目录是否已经初始化过  
    // Mercurial 代码管理系统
    if (isInGitRepository() || isInMercurialRepository()) {
      return false;
    }

    execSync('git init', { stdio: 'ignore' });
    return true;
  } catch (e) {
    console.warn('Git repo not initialized', e);
    return false;
  }
}

Install dependencies and devDependencies dependency In template

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

  //...
  // 如果下载失败, 则 uninstall
  const proc = spawn.sync(command, args, { stdio: 'inherit' });
  if (proc.status !== 0) {
    console.error(`\`${command} ${args.join(' ')}\` failed`);
    return;
  }

uninstall template dependency

// npm uninstall templateName || yarnpkg remove template
const proc = spawn.sync(command, [remove, templateName], {
  stdio: 'inherit',
});

execute git add git commit

execSync('git add -A', { stdio: 'ignore' });
execSync('git commit -m "Initialize project using Create React App"', {
  stdio: 'ignore',
});

总结

CRA的源码实现步骤如下:

  1. 首先在package.json 中添加关键字 bin,以便可以直接通过npx来执行对应的脚本文件
  "bin": {
    "create-react-app": "./index.js"
  },
  1. index.js文件首行指定文件执行环境node
#!/usr/bin/env node

开始脚手架的功能实现

  1. 监听命令行的输入

  2. 接受命中到的命令行输入的参数一次作为项目名称、对应的模板以及版本

  3. 在执行命令目录下创建项目,进入到创建的项目目录下添加 package.json文件,写入基本配置,项目名称、版本号等

  4. 根据本地使用的包管理工具(npm || pnp || yarn)的方式下下载基本依赖项 react、react-scripts、cra-template

  5. 依赖下载成功后便开始执行 react-scripts 脚本,补充新增项目的配置文件 package.json的脚本命令等其他属性、还有文件的 README.md 、.gitignore 配置描述文件

  6. 初始化项目git配置(如果不是git项目的话)

  7. 根据命令行输入的模板或者默认的模板及版本,下载对应的依赖、然后从node_modules 中将 template的文件拷贝复制到 新建的项目目录下

  8. 拷贝成功后将模板依赖从项目中删除

  9. 接着将新增的文件直接添加到暂存区 git add . ,接着 git commit -m "Initialize project using Create React App

  10. 至此,CRA执行完了所有的操作,你想要的项目也已经初始化成功啦。