博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
构建工具篇 - react 的 yarn eject 构建命令都做了什么
阅读量:6605 次
发布时间:2019-06-24

本文共 8628 字,大约阅读时间需要 28 分钟。

前言

前段时间,一直在研究 react 技术栈,对于项目的构建方面,又有一定的特殊需求,通过 npx create-react-app [filename] 安装以后,发现没有 webpack 相关的配置的目录,在读了 react 官方文档后,发现通过 yarn eject 可以弹出相关的配置,进行自定义配置。

于是,我就想知道 eject 到底做了什么,发现里面涉及到很多的知识点,也有很多是我之前没有接触到的地方,自从看了 ejectbuild 的源码,我觉得,我们其实还可以做很多事。

初始化声明

其实,里面绝大部分内容都是基于 node 去实现的:

如果是 node 小白,可以学习到有关 node 的一些知识点;

如果是 node 大佬,也可以看看是否有可以学习的思想。

如果觉得哪里写的不对的,或者解释不够清晰,还请大佬们指出复制代码

订阅 promise 的 reject

process.on("unhandledRejection", err => {  throw err;});复制代码

在初始化执行 yarn reject 的时候,会先发布一个 unhandledRejection 的订阅,这个订阅是在如果在事件循环的一次轮询中,一个 Promiserejected,并且此 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、cssconsole.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 时,shouldEjecttrue,如果输入 n 时,shouldEjectfalse

shouldEjectfalse 的时候,就代表开发者选择了不弹出 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*
.*(?:\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 包,是很有利的

希望这篇文章可以帮到大家,另外多点赞,谢谢啦

转载地址:http://xdzso.baihongyu.com/

你可能感兴趣的文章
Android----- 改变图标原有颜色 和 搜索框
查看>>
Markdown 常用语法
查看>>
4:spring mvc 数据绑定
查看>>
Eclipse支持Python单词补全
查看>>
一致性算法探寻(扩展版)13
查看>>
微信小程序 | 程序员开发实战系列文章④
查看>>
CKEditor粘贴图片上传功能
查看>>
ElasticSearch+Solr几个案例笔记
查看>>
程序中的@Override是什么意思?
查看>>
CentOS 编译安装Apache2.4 PHP5.6.30 Mysql5.6.16
查看>>
Visual SourceSafe 入门教学
查看>>
express 4.0以上的版本 express找不到的问题
查看>>
commons-lang中常用方法
查看>>
spring 定时任务
查看>>
thinkphp 路由规则终极详解(附伪静态)
查看>>
网络安全-加密算法
查看>>
This tag and its children can be replaced by ~~~
查看>>
XCode快捷键
查看>>
struts2 修改action的后缀
查看>>
php保存canvas生成的图片
查看>>