create-react-app 源码学习(上)

栏目: Node.js · 发布时间: 4年前

内容简介:原文地址对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~代码的入口在

原文地址 Nealyang/personalBlog

前言

对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~

create-react-app 源码

代码的入口在 packages/create-react-app/index.js 下,核心代码在 createReactApp.js 中,虽然有大概 900+行代码,但是删除注释和一些友好提示啥的大概核心代码也就六百多行吧,我们直接来看

index.js

create-react-app 源码学习(上)

index.js 的代码非常的简单,其实就是对 node 的版本做了一下校验,如果版本号低于 8,就退出应用程序,否则直接进入到核心文件中, createReactApp.js

createReactApp.js

createReactApp 的功能也非常简单其实,大概流程:

  • 命令初始化,比如自定义 create-react-app --info 的输出等
  • 判断是否输入项目名称,如果有,则根据参数去跑安装,如果没有,给提示,然后退出程序
  • 修改 package.json
  • 拷贝 react-script 下的模板文件

准备工作:配置 vscode 的 debug 文件

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

这里我们添加三种环境,其实就是 create-react-app 的不同种使用方式

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

commander 命令行处理程序

commander 文档传送门

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]`)
  .action(name => {
    projectName = name;//获取用户的输入,存为 projectName
  })
  .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('--use-npm')
  .option('--use-pnp')
  .option('--typescript')
  .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(
      `    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 的使用,这里就不介绍了,对于 create-react-app 的流程我们需要知道的是,它,初始化了一些 create-react-app 的命令行环境,这一波操作后,我们可以看到 program 张这个样纸:

create-react-app 源码学习(上)

接着往下走

create-react-app 源码学习(上)

当我们 debug 启动 noArgs 环境的时候,走到这里就结束了,判断 projectName 是否为 undefined,然后输出相关提示信息,退出~

createApp

在查看 createApp function 之前,我们再回头看下命令行的一些参数定义,方便我们理解 createApp 的一些参数

create-react-app 源码学习(上)

我们使用

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

debugger 我们项目的时候,就可以看到, program.typescripttrueuseNpmtrue ,当然,这些也都是我们在 commander 中定义的 options,所以源码里面 createApp 中,我们传入的参数分别为:

  • projectName : 项目名称
  • program.verbose 是否输出额外信息
  • program.scriptsVersion 传入的脚本版本
  • program.useNpm 是否使用 npm
  • program.usePnp 是否使用 Pnp
  • program.typescript 是否使用 ts
  • hiddenProgram.internalTestingTemplate 给开发者用的调试模板路径
function createApp(
  name,
  verbose,
  version,
  useNpm,
  usePnp,
  useTypescript,
  template
) {
  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(`Creating a new React app in ${chalk.green(root)}.`);
  console.log();

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

  const useYarn = useNpm ? false : shouldUseYarn();//判断是使用 yarn 呢还是 npm
  const originalDirectory = process.cwd();
  process.chdir(root);
  if (!useYarn && !checkThatNpmCanReadCwd()) {//如果是使用npm,检测npm是否在正确目录下执行
    process.exit(1);
  }

  if (!semver.satisfies(process.version, '>=8.10.0')) {//判断node环境,输出一些提示信息, 并采用旧版本的 react-scripts
    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';
  }

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

代码非常简单,部分注释已经加载代码中,简单的说就是对一个本地环境的一些校验,版本检查啊、目录创建啊啥的,如果创建失败,则退出,如果版本较低,则使用对应低版本的 create-react-app ,最后调用 run 方法

checkAppName

这些 工具 方法,其实在写我们自己的构建工具的时候,也可以直接 copy 的哈,所以这里我们也是简单看下里面的实现,

checkAPPName 方法主要的核心代码是 validate-npm-package-name package,从名字即可看出,检查是否为合法的 npm 包名

var done = function (warnings, errors) {
  var result = {
    validForNewPackages: errors.length === 0 && warnings.length === 0,
    validForOldPackages: errors.length === 0,
    warnings: warnings,
    errors: errors
  }
  if (!result.warnings.length) delete result.warnings
  if (!result.errors.length) delete result.errors
  return result
}
...
...
var validate = module.exports = function (name) {
  var warnings = []
  var errors = []

  if (name === null) {
    errors.push('name 不能使 null')
    return done(warnings, errors)
  }

  if (name === undefined) {
    errors.push('name 不能是 undefined')
    return done(warnings, errors)
  }

  if (typeof name !== 'string') {
    errors.push('name 必须是 string 类型')
    return done(warnings, errors)
  }

  if (!name.length) {
    errors.push('name 的长度必须大于 0')
  }

  if (name.match(/^\./)) {
    errors.push('name 不能以点开头')
  }

  if (name.match(/^_/)) {
    errors.push('name 不能以下划线开头')
  }

  if (name.trim() !== name) {
    errors.push('name 不能包含前空格和尾空格')
  }

  // No funny business
  // var blacklist = [
  //   'node_modules',
  //   'favicon.ico'
  // ]
  blacklist.forEach(function (blacklistedName) {
    if (name.toLowerCase() === blacklistedName) { //不能是“黑名单”内的
      errors.push(blacklistedName + ' is a blacklisted name')
    }
  })

  // Generate warnings for stuff that used to be allowed
  // 为以前允许的内容生成警告

 // 后面的就不再赘述了

  return done(warnings, errors)
}

create-react-app 源码学习(上)

最终,checkAPPName返回的东西如截图所示,后面写代码可以直接拿来借鉴!借鉴~

create-react-app 源码学习(上)

isSafeToCreateProjectIn

create-react-app 源码学习(上)

所谓安全性校验,其实就是检查当前目录下是否存在已有文件。

checkNpmVersion

create-react-app 源码学习(上)

后面的代码也都比较简单,这里就不展开说了,版本比较实用的是一个 semver package.

run

代码跑到这里,该检查的都检查了,鸡也不叫了、狗也不咬了,该干点正事了~

run 主要做的事情就是安装依赖、拷贝模板。

create-react-app 源码学习(上)

getInstallPackage 做的事情非常简单,根据传入的 version 和原始路径 originalDirectory 去获取要安装的 package 列表,默认情况下version 为 undefined,获取到的 packageToInstall 为 react-scripts ,也就是我们如上图的 resolve 回调。

create-react-app 源码学习(上)

最终,我们拿到需要安装的 info 为

{
  isOnline:true,
  packageName:"react-scripts"
}

当我们梳理好需要安装的 package 后,就交给 npm 或者 yarn 去安装我们的依赖即可

create-react-app 源码学习(上)

spawn 执行完命令后会有一个回调,判断code是否为 0,然后 resolve Promise,

.then(async packageName => {
         // 安装完 react, react-dom, react-scripts 之后检查当前环境运行的node版本是否符合要求
        checkNodeVersion(packageName);
        // 检查 package.json 中的版本号
        setCaretRangeForRuntimeDeps(packageName);

        const pnpPath = path.resolve(process.cwd(), '.pnp.js');

        const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];

        await executeNodeScript(
          {
            cwd: process.cwd(),
            args: nodeArgs,
          },
          [root, appName, verbose, originalDirectory, template],
          `
        var init = require('${packageName}/scripts/init.js');
        init.apply(null, JSON.parse(process.argv[1]));
      `
        );

create-react-app 之前的版本中,这里是通过调用 react-script 下的 init 方法来执行后续动作的。这里通过调用 executeNodeScript 方法

function executeNodeScript({ cwd, args }, data, source) {
  // cwd:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"

  // data:
  // 0:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
  // 1:"study-create-react-app-source"
  // 2:undefined
  // 3:"/Users/nealyang/Desktop/create-react-app"
  // 4:undefined

  // source
  // "  var init = require('react-scripts/scripts/init.js');
  //   init.apply(null, JSON.parse(process.argv[1]));
  // "
  
  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 方法。所以截止当前, create-react-app 完成了他的工作: npm i ,

react-script/init.js

修改 vscode 的 debugger 配置,然后我们来 debugger react-script 下的 init 方法

function init(appPath, appName, verbose, originalDirectory, template) {
  // 获取当前包中包含 package.json 所在的文件夹路径
  const ownPath = path.dirname(
    //"/Users/nealyang/Desktop/create-react-app/packages/react-scripts"
    require.resolve(path.join(__dirname, '..', 'package.json'))
  );
  const appPackage = require(path.join(appPath, 'package.json')); //项目目录下的 package.json
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //通过判断目录下是否有 yarn.lock 来判断是否使用 yarn

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

  //   react:"16.8.6"
  // react-dom:"16.8.6"
  // react-scripts:"3.0.1"
  const useTypeScript = appPackage.dependencies['typescript'] != null;

  // Setup the script rules 设置 script 命令
  appPackage.scripts = {
    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',
  };

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

  // Setup the browsers list 这是浏览器 openBrowser
  appPackage.browserslist = defaultBrowsers;

  // 写入我们需要创建的目录下的 package.json 中
  fs.writeFileSync(
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  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 templatePath = template //"/Users/nealyang/Desktop/create-react-app/packages/react-scripts/template"
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
  if (fs.existsSync(templatePath)) {
    // 这一步就过分了, 直接 copy!  appPath:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
    fs.copySync(templatePath, appPath);
  } else {
    console.error(
      `Could not locate supplied template: ${chalk.green(templatePath)}`
    );
    return;
  }

  // Rename gitignore after the fact to prevent npm from renaming it to .npmignore 重命名gitignore以防止npm将其重命名为.npmignore
  // See: https://github.com/npm/npm/issues/1862
  try {
    fs.moveSync(
      path.join(appPath, 'gitignore'),
      path.join(appPath, '.gitignore'),
      []
    );
  } catch (err) {
    // Append if there's already a `.gitignore` file there
    if (err.code === 'EEXIST') {
      const data = fs.readFileSync(path.join(appPath, 'gitignore'));
      fs.appendFileSync(path.join(appPath, '.gitignore'), data);
      fs.unlinkSync(path.join(appPath, 'gitignore'));
    } else {
      throw err;
    }
  }

  let command;
  let args;

  if (useYarn) {
    command = 'yarnpkg';
    args = ['add'];
  } else {
    command = 'npm';
    args = ['install', '--save', verbose && '--verbose'].filter(e => e);
  }
  args.push('react', 'react-dom');
  // args Array
  // 0:"install"
  // 1:"--save"
  // 2:"react"
  // 3:"react-dom"

  // 安装其他模板依赖项(如果存在)
  const templateDependenciesPath = path.join(//"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source/.template.dependencies.json"
    appPath,
    '.template.dependencies.json'
  );
  if (fs.existsSync(templateDependenciesPath)) {
    const templateDependencies = require(templateDependenciesPath).dependencies;
    args = args.concat(
      Object.keys(templateDependencies).map(key => {
        return `${key}@${templateDependencies[key]}`;
      })
    );
    fs.unlinkSync(templateDependenciesPath);
  }

  // 安装react和react-dom以便与旧CRA cli向后兼容
  // 没有安装react和react-dom以及react-scripts
  // 或模板是presetend(通过--internal-testing-template)
  if (!isReactInstalled(appPackage) || template) {
    console.log(`Installing react and react-dom using ${command}...`);
    console.log();

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

  if (useTypeScript) {
    verifyTypeScriptSetup();
  }

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

  // 显示最优雅的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
  const displayedCommand = useYarn ? 'yarn' : 'npm';

  console.log('xxxx....xxxxx');
}

初始化方法主要做的事情就是修改目标路径下的 package.json,添加一些配置命令,然后 copy!react-script 下的模板到目标路径下。

create-react-app 源码学习(上)

走到这一步,我们的项目基本已经初始化完成了。

所以我们 copy 了这么多 scripts

start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',

究竟是如何工作的呢,其实也不难,就是一些开发、测试、生产的环境配置。鉴于篇幅,咱就下一篇来分享下大佬们的前端构建的代码写法吧~~

总结

本来想用一张流程图解释下,但是。。。create-react-app 着实没有做啥!咱还是等下一篇分析完,自己写构建脚本的时候再画一下整体流程图(架构图)吧~

create-react-app 源码学习(上)

ok~ 简单概述下:

cross-spawn

通篇看完 package 的职能后,发现,哇,这有点简答啊~~其实,我们学习源码的其实就是为了学习大佬们的一些边界情况处理,在后面自己开发的时候再去 copy~ 借鉴一些判断方法的编写。后面会再简单分析下 react-scripts ,然后写一个自己的一些项目架构脚本~


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacking

Hacking

Jon Erickson / No Starch Press / 2008-2-4 / USD 49.95

While other books merely show how to run existing exploits, Hacking: The Art of Exploitation broke ground as the first book to explain how hacking and software exploits work and how readers could deve......一起来看看 《Hacking》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试