構建工具篇 - react 的 yarn eject 構建命令都作了什麼

前言

前段時間,一直在研究 react 技術棧,對於項目的構建方面,又有必定的特殊需求,經過 npx create-react-app [filename] 安裝之後,發現沒有 webpack 相關的配置的目錄,在讀了 react 官方文檔後,發現經過 yarn eject 能夠彈出相關的配置,進行自定義配置。javascript

因而,我就想知道 eject 到底作了什麼,發現裏面涉及到不少的知識點,也有不少是我以前沒有接觸到的地方,自從看了 ejectbuild 的源碼,我以爲,咱們其實還能夠作不少事。css

初始化聲明

其實,裏面絕大部份內容都是基於 node 去實現的:html

若是是 node 小白,能夠學習到有關 node 的一些知識點;前端

若是是 node 大佬,也能夠看看是否有能夠學習的思想。java

若是以爲哪裏寫的不對的,或者解釋不夠清晰,還請大佬們指出
複製代碼

訂閱 promise 的 reject

process.on("unhandledRejection", err => {
  throw err;
});
複製代碼

在初始化執行 yarn reject 的時候,會先發佈一個 unhandledRejection 的訂閱,這個訂閱是在若是在事件循環的一次輪詢中,一個 Promiserejected,而且此 Promise 沒有綁定錯誤處理器, unhandledRejection 事件會被觸發。node

這裏直接 throw err 的目的,是爲了在發生 rejected 的時候,直接崩潰,而不是忽略;react

因爲這裏訂閱了,未來一旦發生了 rejected ,就會直接退出 node 進程。webpack

聲明要使用的方法 (初始化)

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,經過開發者選擇的狀態,去執行對應的處理,效果以下: git

shouldEject 屬性,就是 name 屬性的值,當開發者輸入 y 時,shouldEjecttrue,若是輸入 n 時,shouldEjectfalseweb

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*<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 包,是頗有利的

但願這篇文章能夠幫到你們,另外多點贊,謝謝啦

相關文章
相關標籤/搜索