前言
前段时间,一直在研究 react 技术栈,对于项目的构建方面,又有一定的特殊需求,通过 npx create-react-app [filename] 安装以后,发现没有 webpack 相关的配置的目录,在读了 react 官方文档后,发现通过 yarn eject 可以弹出相关的配置,进行自定义配置。
于是,我就想知道 eject 到底做了什么,发现里面涉及到很多的知识点,也有很多是我之前没有接触到的地方,自从看了 eject 和 build 的源码,我觉得,我们其实还可以做很多事。
初始化声明
其实,里面绝大部分内容都是基于 node 去实现的:
如果是 node 小白,可以学习到有关 node 的一些知识点;
如果是 node 大佬,也可以看看是否有可以学习的思想。
如果觉得哪里写的不对的,或者解释不够清晰,还请大佬们指出
复制代码
订阅 promise 的 reject
process.on("unhandledRejection", err => {
  throw err;
});
复制代码在初始化执行 yarn reject 的时候,会先发布一个 unhandledRejection 的订阅,这个订阅是在如果在事件循环的一次轮询中,一个 Promise 被 rejected,并且此 Promise 没有绑定错误处理器, unhandledRejection 事件会被触发。
这里直接 throw err 的目的,是为了在发生 rejected 的时候,直接崩溃,而不是忽略;
由于这里订阅了,将来一旦发生了 rejected ,就会直接退出 node 进程。
声明要使用的方法 (初始化)
const fs = require('fs-extra'); // node中fs的扩展,在支持fs所有api的基础上,还支持promise写法
const path = require('path'); // 用来获取目录模块
const execSync = require('child_process').execSync; // 执行同步命令
const chalk = require('react-dev-utils/chalk'); // 用来修改log字体颜色
const paths = require('../config/paths'); // 对于路径的处理
const createJestConfig = require('./utils/createJestConfig'); // 创建单元测试配置
const inquirer = require('react-dev-utils/inquirer'); // 常用交互式命令行用户界面的集合
const spawnSync = require('react-dev-utils/crossSpawn').sync; // 跨平台执行系统命令
const os = require('os'); // 用来操作系统的方法

const green = chalk.green; // 绿色
const cyan = chalk.cyan; // 青色

function getGitStatus() { // 获取git状态
  try {
    let stdout = execSync(`git status --porcelain`, {
      stdio: ['pipe', 'pipe', 'ignore'],
    }).toString();
    return stdout.trim();
  } catch (e) {
    return '';
  }
}

function tryGitAdd(appPath) { // 用来提交根目录下config和scripts文件夹下修改或者更新的内容,但是不包括删除部分
  try {
    spawnSync(
      'git',
      ['add', path.join(appPath, 'config'), path.join(appPath, 'scripts')],
      {
        stdio: 'inherit',
      }
    );

return true;
  } catch (e) {
    return false;
  }
}
// 一段提示信息,说明一下不需要eject也支持ts、sass、css
console.log(
  chalk.cyan.bold(
    'NOTE: Create React App 2+ supports TypeScript, Sass, CSS Modules and more without ejecting: ' +
      'https://reactjs.org/blog/2018/10/01/create-react-app-v2.html'
  )
);
复制代码弹出 webpack 相关配置 (核心)
inquirer.prompt({
    type: "confirm",
    name: "shouldEject",
    message: "Are you sure you want to eject? This action is permanent.",
    default: false
  }).then(answer => {
    if (!answer.shouldEject) { // 选择 n
      console.log(cyan("Close one! Eject aborted."));
      return;
    }
    /*...*/
  })
复制代码这里需要手动输入 y 或者 n,通过开发者选择的状态,去执行对应的处理,效果如下:

shouldEject 属性,就是 name 属性的值,当开发者输入 y 时,shouldEject 为 true,如果输入 n 时,shouldEject 为 false
当 shouldEject 为 false 的时候,就代表开发者选择了不弹出 eject 相关配置
如果选择了 y ,就要执行下列步骤了
检查当前项目的文件状态
const gitStatus = getGitStatus();
if (gitStatus) {
  console.error(
    chalk.red(
      "This git repository has untracked files or uncommitted changes:"
    ) +
      "\n\n" +
      gitStatus
        .split("\n")
        .map(line => line.match(/ .*/g)[0].trim())
        .join("\n") +
      "\n\n" +
      chalk.red(
        "Remove untracked files, stash or commit any changes, and try again."
      )
  );
  process.exit(1);
}
复制代码这里会列出来当前 git 储存库有新的文件或者修改后未提交的文件存在,出现这种情况会直接中断当前的 node 进程,目的是为了防止要弹出的文件会和这些文件出现冲突或者覆盖的情况发生
所以安全起见,会希望开发者保证当前 git 储存库当前不存在新文件或者修改后的文件
检查要弹出的文件是否存在当前项目
console.log("Ejecting...");

const ownPath = paths.ownPath; //当前文件的父级目录
const appPath = paths.appPath; //当前目录

function verifyAbsent(file) {
  if (fs.existsSync(path.join(appPath, file))) {
    //检测文件是否存在
    console.error(
      `\`${file}\` already exists in your app folder. We cannot ` +
        "continue as you would lose all the changes in that file or directory. " +
        "Please move or delete it (maybe make a copy for backup) and run this " +
        "command again."
    );
    process.exit(1);
  }
}

const folders = ["config", "config/jest", "scripts"];

// 制作浅层文件路径
const files = folders.reduce((files, folder) => {
  return files.concat(
    fs
      .readdirSync(path.join(ownPath, folder))
      //node_modules/react-scripts/config
      .map(file => path.join(ownPath, folder, file))
      //node_modules/react-scripts/config/env.js
      .filter(file => fs.lstatSync(file).isFile())
    //检测当前目录属于文件类型的
    //config下面所有文件,config/jest下面所有文件,scripts下面所有文件(不包括utils)
  );
}, []);

// 检查所有文件是否存在
folders.forEach(verifyAbsent);
files.forEach(verifyAbsent);
复制代码由于后来要弹出这两个文件夹下面的文件,于是要去检查当前的项目当中,根目录是否存在这两个文件夹,并且确认是否存在相同的文件
如果存在,就会同上一样,希望移除或者删除文件,然后再次执行命令
在根目录创建文件夹
folders.forEach(folder => {
  fs.mkdirSync(path.join(appPath, folder));
});
复制代码在根目录下创建对应的文件夹
读取文件内容
files.forEach(file => {
  let content = fs.readFileSync(file, "utf8"); //读取文件内容

// 跳过标记的文件
  if (content.match(/\/\/ @remove-file-on-eject/)) {
    return;
  }
  content =
    content
      .replace(
        /\/\/ @remove-on-eject-begin([\s\S]*?)\/\/ @remove-on-eject-end/gm,
        ""
      )
      .replace(
        /-- @remove-on-eject-begin([\s\S]*?)-- @remove-on-eject-end/gm,
        ""
      )
      .trim() + "\n";
  console.log(`  Adding ${cyan(file.replace(ownPath, ""))} to the project`);
  fs.writeFileSync(file.replace(ownPath, appPath), content);
});
复制代码读取所有文件的内容,如果有 //@remove-file-on-eject 的文件,就直接跳过,不进行创建写入
如果某个文件内存在 //@remove-on-eject-begin 开头, //@remove-on-eject-end 结尾的内容,就直接进行删除,写入剩下的内容
更新依赖
const ownPackage = require(path.join(ownPath, "package.json"));
const appPackage = require(path.join(appPath, "package.json"));

console.log(cyan("Updating the dependencies"));
const ownPackageName = ownPackage.name;
if (appPackage.devDependencies) {
  // 我们曾经把react脚本放在devDependencies中
  if (appPackage.devDependencies[ownPackageName]) {
    console.log(`  Removing ${cyan(ownPackageName)} from devDependencies`);
    delete appPackage.devDependencies[ownPackageName];
  }
}
appPackage.dependencies = appPackage.dependencies || {};
if (appPackage.dependencies[ownPackageName]) {
  console.log(`  Removing ${cyan(ownPackageName)} from dependencies`);
  delete appPackage.dependencies[ownPackageName];
}
Object.keys(ownPackage.dependencies).forEach(key => {
  // 由于某种原因,optionalDependencies在安装后以依赖关系结束
  if (ownPackage.optionalDependencies[key]) {
    return;
  }
  console.log(`  Adding ${cyan(key)} to dependencies`);
  appPackage.dependencies[key] = ownPackage.dependencies[key];
});
// 对deps进行排序
const unsortedDependencies = appPackage.dependencies;
appPackage.dependencies = {};
Object.keys(unsortedDependencies)
  .sort()
  .forEach(key => {
    appPackage.dependencies[key] = unsortedDependencies[key];
  });
console.log();

console.log(cyan("Updating the scripts"));
delete appPackage.scripts["eject"];
Object.keys(appPackage.scripts).forEach(key => {
  Object.keys(ownPackage.bin).forEach(binKey => {
    const regex = new RegExp(binKey + " (\\w+)", "g");
    if (!regex.test(appPackage.scripts[key])) {
      return;
    }
    appPackage.scripts[key] = appPackage.scripts[key].replace(
      regex,
      "node scripts/$1.js"
    );
    console.log(
      `  Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan(
        `"node scripts/${key}.js"`
      )}`
    );
  });
});

console.log();
console.log(cyan("Configuring package.json"));
// 添加 jest 配置
console.log(`  Adding ${cyan("Jest")} configuration`);
appPackage.jest = jestConfig;

// 添加 babel 配置
console.log(`  Adding ${cyan("Babel")} preset`);
appPackage.babel = {
  presets: ["react-app"]
};

// 添加 eslint 配置
console.log(`  Adding ${cyan("ESLint")} configuration`);
appPackage.eslintConfig = {
  extends: "react-app"
};

fs.writeFileSync(
  path.join(appPath, "package.json"),
  JSON.stringify(appPackage, null, 2) + os.EOL
); //写入json,2用来做格式化缩进
复制代码这里代码量看起来比较多,但是没有很复杂的知识点,所以就不做详细介绍了,大家看一下就了解了
处理弹出以后的事(扫尾)
到这里,其实弹出相对应文件的工作已经完成了,只是在这里需要在弹出以后把和项目已经无关的资源进行清理即可
从声明文件删除 react-scripts 相关
if (fs.existsSync(paths.appTypeDeclarations)) {
  try {
    // 阅读应用声明文件
    let content = fs.readFileSync(
      paths.appTypeDeclarations,
      "utf8"
    );
    const ownContent =
      fs
        .readFileSync(paths.ownTypeDeclarations, "utf8")
        .trim() + os.EOL;

// 删除 react-scripts 引用,因为它们正在获取项目中类型的副本
    content =
      content
        // 删除 react-scripts 类型
        .replace(
          /^\s*\/\/\/\s*<reference\s+types.+?"react-scripts".*\/>.*(?:\n|$)/gm,
          ""
        )
        .trim() + os.EOL;

fs.writeFileSync(
      paths.appTypeDeclarations,
      (ownContent + os.EOL + content).trim() + os.EOL
    );
  }
}
复制代码从根目录的 node_modules 删除 react-scripts 相关
if (ownPath.indexOf(appPath) === 0) {
  try {
    // 从app node_modules中删除react-scripts和react-scripts二进制文件
    Object.keys(ownPackage.bin).forEach(binKey => {
      fs.removeSync(path.join(appPath, "node_modules", ".bin", binKey));
    });
    fs.removeSync(ownPath);
  } 
}
复制代码结束语
有关 eject 相关的代码,到这里就讲解的差不多了,其实呢,代码量看起来挺大,但是仔细看的话,也不是很复杂,只是里面掺杂了有关 node 相关的知识点,这样对纯前端同学来说不是很友好
但是只要去查询对应的 api 就会发现其实实现的并不难,只是对于一些实现这种做法的思想,是值得我们去学习的
看懂了这篇文章,了解了 react 是如何隐藏 webpack 相关配置的,又是如何弹出的,会对未来我们自己去写一个相同作用的 npm 包,是很有利的。

如果你对编程感兴趣或者想往编程方向发展,可以关注微信公众号【筑梦编程】,大家一起交流讨论!小编也会每天定时更新既有趣又有用的编程知识!

构建工具篇 - react 的 yarn eject 构建命令都做了什么相关推荐

  1. v59.04 鸿蒙内核源码分析(构建工具) | 顺瓜摸藤调试构建过程 | 百篇博客分析HarmonyOS源码

    仲弓问仁.子曰:"出门如见大宾,使民如承大祭.己所不欲,勿施于人.在邦无怨,在家无怨."仲弓曰:"雍虽不敏,请事斯语矣." <论语>:颜渊篇 百篇博 ...

  2. 99数据集预处理_深度学习在放射治疗的应用—工具篇(五)数据集构建

    家园宗旨:诚邀八方志同道合之友,共谋一隅传道受业之善! 作者简介: 本期我们将继续介绍Matlab中的数据集系列函数.上期中我们介绍了imageDatastore数据集函数,该函数能够实现深度学习所需 ...

  3. 深入浅出的webpack4构建工具--webpack4+react构建环境(二十)

    下面我们来配置下webpack4+react的开发环境,之前都是针对webpack4+vue的.下面我们也是在之前项目结构的基础之上进行配置下. 首先看下如下是我为 webpack4+react 基本 ...

  4. react 组件构建_使用React Spring和Tinycolor构建色彩丰富的弹性组件

    react 组件构建 Recently, I decided to build a web application to allow designers and developers to gener ...

  5. GC调优基础知识之工具篇--jdk为我们提供的命令行命令 jps,jstat,jmap,jinfo,jstat,jstack,jhat 等

    一. JDK为我们提供的工具:    在Windows中对于这些命令的支持是源自JDK -->bin下面的.exe可执行文件的支持.    在Linux中对于这些命令的支持是源自JDK --&g ...

  6. netty案例,netty4.1源码分析篇五《一行简单的writeAndFlush都做了哪些事》

    前言介绍 对于使用netty的小伙伴来说,ctx.writeAndFlush()再熟悉不过了,它可以将我们的消息发送出去.那么它都执行了那些行为呢,是怎么将消息发送出去的呢. I/O Requestv ...

  7. 使用React和Spring Boot构建一个简单的CRUD应用

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. Reac ...

  8. 三大前端构建工具横评,谁是性能之王!

    点击上方"蓝色字体",选择"设为星标" 做积极向上的前端人! Vite一经发布就吸引了很多人的关注,NPM下载量一路攀升: 而在Vite之前,还有Snowpac ...

  9. androidsdktools安装_如何命令行安装Android SDK Build Tools(构建工具)?

    Android构建工具安装的问题 我想从命令行建立Android开发环境,遇到了如下问题: wget http://dl.google.com/android/android-sdk_r22.0.5- ...

最新文章

  1. Java:全局变量(成员变量)与局部变量
  2. ISME:二氧化碳和氮水平对植物根表菌群和功能的影响
  3. cpa机考可以用计算机吗,cpa机考计算器使用方法
  4. 皮一皮:绿灯侠是怎么诞生的...
  5. 如何更改Windows 10锁屏界面超时时间
  6. SDN火爆!未来五年年复合增长率达98%
  7. Oracle函数大全1
  8. linux打包java jar_在linux环境下修改可运行jar包配置并重新打包
  9. Python 实int型和list相互转换 现把float型列表转换为int型列表 把列表中的数字由float转换为int型...
  10. 转 常见视频编码方式以及封装格式
  11. 亚泰盛世携NB物理实验邀你莅临第66届中国教育装备展
  12. 数据仓库之指标体系建设分享
  13. 克拉夫斯曼高端定制 刘霞---【YBC中国国际青年创业计划】
  14. SDUT 2084 DOTA-人王之战(博弈论)
  15. nodeJS实现简易爬虫
  16. GBase 8s 块(chunk)
  17. 如何区分企业邮箱是哪个?电子邮箱域名是什么?真的好用吗?
  18. 嵌入式培训经验分享——C++入门基础知识
  19. 苹果系统Mac能安装office吗
  20. 扫拖一体洗地机哪个品牌好、家用洗地机品牌介绍

热门文章

  1. servlet的生命周期 (四个阶段).
  2. 微信支付查询订单java_微信支付java版本之查询订单
  3. 嵌入式从业者应知应会知识点 - 索引
  4. 【Android】 炫酷霓虹灯
  5. 检测和分割算法常用的评价指标
  6. web系统维护时显示维护状态页面
  7. 三个月自学自动化测试,鬼知道我经历了什么?薪资从4.5K到11K
  8. AUTOSAR ComM模块介绍
  9. VUE的插槽(slot和slot-scope)
  10. YTU 3014 文件格式变换