探索 create-react-app 原始碼

Jsonz發表於2019-03-02

本文連結: jsonz1993.github.io/2018/05/cre…

系列二地址

最近工作開始穩定下來,沒有那麼多加班…所以開始有空閒的時間可以學習一些前端知識

之前公司有個大佬寫了個類似 create-react-app 的腳手架,用來建立公司的專案。一直不知道里面實現的原理,藉此機會一探 create-react-app 原始碼,瞭解下里面執行的機制。

大家不要一看到原始碼就害怕不敢去看,現在這麼優秀專案都開源了,加上各種IDE支援很好,直接打個斷點進去除錯,很容易看出個大概。
也可以用這種思路去了解其他的開源專案

emmmm 第一次寫文~接受任何吐槽

快速瞭解

對於想快速瞭解的直接瀏覽這一塊即可

create-react-app 其實就是用node去跑一些包安裝流程,並且把檔案模板demo考到對於的目錄下。

可以簡單分為以下幾個步驟:

  • 判斷Node版本
  • 做一些命令列處理的初始化,比如輸入 -help 則輸出幫助內容
  • 判斷是否有輸入專案名,有則根據引數去跑包安裝,預設是yarn安裝方式,eg: yarn add react react-dom react-scripts
  • 修改package.json裡面已安裝的依賴版本,從精確版本16.0.0改為^向上相容版本^16.0.0 並加入 start,build等啟動指令碼
  • 拷貝 react-scripts下的 template 到目標檔案,裡面有public,src等資料夾,其實就是一個簡單的可執行demo
  • END~

繼續往下看的小夥伴可以跟著一步一步瞭解裡面的實現邏輯,先例行交代下環境版本:

create-react-app v1.1.4
macOS 10.13.4
node v8.9.4
npm 6.0.0
yarn 1.6.0
vsCode 1.22.2
複製程式碼

專案初始化

先上github 拉專案程式碼,拉下來之後切換到指定的 tag

  • git clone https://github.com/facebook/create-react-app.git
  • git checkout v1.1.4
  • yarn //如果不需要斷點除錯,這一步可以跳過

這裡可能yarn 版本太低的話,會報一系列錯誤,之前用的是 0.x版本的,升級到1.x就沒問題了

下面我們用 root 代替專案的根目錄,方便理解

首先我們開啟專案能看到一堆的配置檔案和兩個資料夾:eslint配置檔案、travis部署配置、yarn配置、更新日誌、開源宣告等等…這些我們全都可以不用去看,那我們要看的核心原始碼放在哪裡呢

探索 create-react-app 原始碼

劃重點: 如果專案不知道從哪裡入手的話,首先從package.json檔案開始

root/package.json

{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "start": "cd packages/react-scripts && node scripts/start.js",
  },
  "devDependencies": {
  },
  "lint-staged": {
  }
}
複製程式碼

開啟根目錄 package.json 我們可以看到裡面很簡潔~ npm指令碼命令,開發依賴,還有提交鉤子,剩下的就是我們要關注的 workspaces 這裡指向的是 "packages/*",所以我們現在的重點就放在 packages 資料夾

packages 資料夾下面也有幾個資料夾,這裡資料夾命名很規範,一看就知道功能劃分,所以還是老套路直接看 root/packages/create-react-app/package.json

packages/create-react-app/package.json

{
  "name": "create-react-app",
  "version": "1.5.2",
  "license": "MIT",
  "engines": {
  },
  "bugs": {
  },
  "files": [
    "index.js",
    "createReactApp.js"
  ],
  "bin": {
    "create-react-app": "./index.js"
  },
  "dependencies": {
  }
}

複製程式碼

這時候沒有 workspaces項, 我們可以看 bin bin的功能是把命令對應到可執行的檔案,具體的介紹可以看package Document

這裡可以簡單理解成,當我們全域性安裝了 create-react-app 之後,跑 create-react-app my-react-app 系統會幫我們去跑 packages/create-react-app/index.js my-react-app

終於找到原始碼的入口了,對於簡單的原始碼我們可以直接看,對於比較複雜的 或者想要看到執行到每一行程式碼時那些變數是什麼值的情況,我們就要用IDE或其他工具來斷點除錯程式碼了。

配置斷點除錯

對於vscode或node除錯 比較熟悉的可以跳過直接看 開始斷點閱讀原始碼

vscode debug

對於vscode使用者來說,除錯非常簡單,點選側邊欄的小甲蟲圖示,點選設定
然後直接修改 “program”的值,修改完點選左上角的綠色箭頭就可以跑起來了,如果要在某一處斷點,比如 create-react-app/index.js line39 斷點,直接在行號的左邊點一下滑鼠就可以了

探索 create-react-app 原始碼
探索 create-react-app 原始碼

launch.json 配置

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "啟動程式",
      "program": "${workspaceFolder}/packages/create-react-app/index.js",
    }
  ]
}
複製程式碼

node 除錯

如果平時沒有用vscode開發或者習慣chrome-devtool的,可以直接用node命令跑,然後在chrome裡面除錯
首先保證node的版本的 6 以上
然後在專案根目錄下執行 node --inspect-brk packages/create-react-app/index.js
在chrome位址列輸入 chrome://inspect/#devices 然後就可以看到我們要除錯的指令碼了
關於node chrome-devtool 除錯詳細可以看這裡 傳送門

探索 create-react-app 原始碼
終端啟動node除錯
探索 create-react-app 原始碼
探索 create-react-app 原始碼

(ps:這裡可以看出來node在模組化的實現是通過用一個函式包裹起來,然後把 exports, requestd等引數傳進來以供使用)

開始斷點閱讀原始碼

packages/create-react-app/index.js github檔案傳送門

探索 create-react-app 原始碼
packages/creat-react-app/index.js

這個檔案十分簡單,只是做為一個入口檔案判斷一下 node版本,小於 4.x的提示並終止程式, 如果正常則載入 ./createReactApp 這個檔案,主要的邏輯在該檔案實現。

packages/create-react-app/createReactApp.js github檔案傳送門

順著我們的斷點進入到 createReactApp.js 這個檔案有750行乍一看很多,檔案頭又有十幾個依賴引入,但是不要被嚇到,一般這種高質量的開源專案,裡面有一大半是註釋和錯誤友好資訊。

這裡建議沒有打斷點除錯的小夥伴試一下把程式碼複製到另一個js檔案,然後先不看前面的依賴,下面用到再去 npm查一下是什麼作用的。不要被繞進去看了一個又一個的依賴,核心程式碼反而沒有看到。 然後看一部分之後就把那部分的程式碼刪掉,比如我看了200行,就把前面200行刪了,這樣剩下500行看著就沒有那麼心虛了。當然還是建議用斷點除錯閱讀,邏輯會比較清晰。

首先檔案頭部這一大串的依賴,我們暫時不去關注他們,等後面用到再去查

const validateProjectName = require(`validate-npm-package-name`);
const chalk = require(`chalk`);
const commander = require(`commander`);
const fs = require(`fs-extra`);
const path = require(`path`);
const execSync = require(`child_process`).execSync;
const spawn = require(`cross-spawn`);
const semver = require(`semver`);
const dns = require(`dns`);
const tmp = require(`tmp`);
const unpack = require(`tar-pack`).unpack;
const url = require(`url`);
const hyperquest = require(`hyperquest`);
const envinfo = require(`envinfo`);
複製程式碼

commander 命令列處理程式

接下來順著我們的斷點,第一行被執行的程式碼是 L56

const program = new commander.Command(packageJson.name)
  .version(packageJson.version) // create-react-app -v 時輸出 ${packageJson.version}
  .arguments(`<project-directory>`) // 這裡用<> 包著project-directory 表示 project-directory為必填項
  .usage(`${chalk.green(`<project-directory>`)} [options]`) // 用綠色字型輸出 <project-directory>
  .action(name => {
    projectName = name;
  }) // 獲取使用者傳入的第一個引數作為 projectName **下面就會用到**
  .option(`--verbose`, `print additional logs`) // option用於配置`create-react-app -[option]`的選項,比如這裡如果使用者引數帶了 --verbose, 會自動設定program.verbose = true;
  .option(`--info`, `print environment debug info`) // 後面會用到這個引數,用於列印出環境除錯的版本資訊
  .option(
    `--scripts-version <alternative-package>`,
    `use a non-standard version of react-scripts`
  )
  .option(`--use-npm`)
  .allowUnknownOption()
   // on(`option`, cb) 輸入 create-react-app --help 自動執行後面的操作輸出幫助
  .on(`--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 custom fork published on npm: ${chalk.green(
        `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/facebookincubator/create-react-app/issues/new`
      )}`
    );
    console.log();
  })
  .parse(process.argv); // 解析傳入的引數 可以不用理會
複製程式碼

這裡用到了一個 commander 的依賴,這時候我們就可以去npm 搜一下他的作用了。官網的描述是 The complete solution for node.js command-line interfaces, inspired by Ruby`s commander.API documentation 翻譯過來是 node.js 命令列介面的完整解決方案,基本的功能看註釋即可,大概瞭解一下有這麼一個東西,後面自己要做的時候有門路即可。github傳送門

判斷是否有傳projectName

if (typeof projectName === `undefined`) {
  if (program.info) { // 如果命令列有帶 --info 引數,輸出 react,react-dom,react-scripts版本 然後退出
    envinfo.print({
      packages: [`react`, `react-dom`, `react-scripts`],
      noNativeIDE: true,
      duplicates: true,
    });
    process.exit(0);
  }
  ...
  這裡輸出了一些錯誤提示資訊
  ...
  process.exit(1);
}
複製程式碼

往下看是一個判斷必須傳入的引數 projectName,這裡的 projectName 就是上面通過 .action(name => { projectName = name;}) 獲取的。
判斷如果沒有輸入的話,直接做一些資訊提示,然後終止程式。
這裡引數如果傳入了 --info 的話, 會執行到envinfo.print。 日常npm 搜一下 envinfo 這是一個用來輸出當前環境系統的一些系統資訊,比如系統版本,npm等等還有react,react-dom,react-scripts這些包的版本,非常好用。這個包現在的版本和create-react-app的版本差異比較大,但是不影響我們使用~ envinfo npm傳送門

如果是用我上面提供的 vscode debug配置的話,到這裡程式應該就執行結束了,因為我們在啟動除錯服務的時候,沒有給指令碼傳入引數作為 projectName,所以我們修改一下 vscode launch.json 加多個欄位 "args": ["test-create-react-app"] 忘記怎麼設定的點這裡~ 傳入了 projectName 引數 然後重新啟動除錯服務

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "啟動程式",
      "program": "${workspaceFolder}/packages/create-react-app/index.js",
      "args": [
        "test-create-react-app"
      ]
    }
  ]
}
複製程式碼

隱藏的 commander 引數

接著走判斷完 projectName 之後,來到 Line140

const hiddenProgram = new commander.Command()
  .option(
    `--internal-testing-template <path-to-template>`,
    `(internal usage only, DO NOT RELY ON THIS) ` +
      `use a non-standard application template`
  )
  .parse(process.argv);
複製程式碼

可以看到這個是一個隱藏的除錯選項,給出一個引數用於傳入模版路徑,給開發人員除錯用的…沒事不折騰他

createApp

createApp(
  projectName,
  program.verbose,
  program.scriptsVersion,
  program.useNpm,
  hiddenProgram.internalTestingTemplate
);
複製程式碼

接著往下就是呼叫了 createApp, 傳入的引數對於的含義是:專案名是否輸出額外資訊傳入的指令碼版本是否使用npm除錯的模板路徑。接下來單步進入函式體看一下 createApp 到底做了什麼事情。

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name);
  const appName = path.basename(root);

  checkAppName(appName); // 檢查傳入的專案名合法性
  fs.ensureDirSync(name); // 這裡的fs用的是 fs-extra, 對node的fs提供一些擴充套件方法
  // 判斷新建這個資料夾是否是安全的 不安全直接退出
  if (!isSafeToCreateProjectIn(root, name)) {
    process.exit(1);
  }

  // 在新建的資料夾下寫入 package.json 檔案
  const packageJson = {
    name: appName,
    version: `0.1.0`,
    private: true,
  };
  fs.writeFileSync(
    path.join(root, `package.json`),
    JSON.stringify(packageJson, null, 2)
  );

  const useYarn = useNpm ? false : shouldUseYarn();
  const originalDirectory = process.cwd();
  process.chdir(root);
  // 如果是使用npm,檢測npm是否在正確目錄下執行
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  }

  // 判斷node環境,輸出一些提示資訊, 並採用舊版本的 react-scripts
  if (!semver.satisfies(process.version, `>=6.0.0`)) {
    // 輸出一些提示更新資訊
    version = `react-scripts@0.9.x`;
  }

  if (!useYarn) {
    // 檢測npm版本 判斷npm版本,如果低於3.x,使用舊版的 react-scripts舊版本
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {
      version = `react-scripts@0.9.x`;
    }
  }

  // 判斷結束之後,跑run 方法
  // 傳入 專案路徑,專案名, reactScripts版本, 是否輸入額外資訊, 執行的路徑, 模板(開發除錯用的), 是否使用yarn
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
複製程式碼

createReactApp.js createApp 傳送門
這裡我精簡了一些東西,刪除一些輸出資訊,加了一些註釋
createApp 主要做的事情就是做一些安全判斷比如:檢查專案名是否合法,檢查新建的話是否安全,檢查npm版本,處理react-script的版本相容
具體的執行邏輯寫在註釋裡了,一系列的檢查處理之後,呼叫 run 方法,傳入引數為 專案路徑專案名reactScripts版本是否輸入額外資訊執行的路徑模板(開發除錯用的), 是否使用yarn
瞭解大概的流程之後,再一個函式一個函式進去看。

checkAppName() // 檢查傳入的專案名合法性
isSafeToCreateProjectIn(root, name) // 判斷新建這個資料夾是否是安全的
shouldUseYarn() // 檢查yarn
checkThatNpmCanReadCwd() // 檢查npm
run() // 檢查完之後呼叫run執行安裝等操作

checkAppName 檢查projectName是否合法

function checkAppName(appName) {
  const validationResult = validateProjectName(appName);
  if (!validationResult.validForNewPackages) {
    // 判斷是否符合npm規範如果不符合,輸出提示並結束任務
  }

  const dependencies = [`react`, `react-dom`, `react-scripts`].sort();
  if (dependencies.indexOf(appName) >= 0) {
    // 判斷是否重名,如果重名則輸出提示並結束任務
  }
}
複製程式碼

checkAppName 用於判斷當前的專案名是否符合npm規範,比如不能大寫等,用的是一個validate-npm-package-name的npm包。這裡簡化了大部分的錯誤提示程式碼,但是不影響口感。

shouldUseYarn 判斷是否有裝yarn 同理的有 checkThatNpmCanReadCwd 用來判斷npm

function shouldUseYarn() {
  try {
    execSync(`yarnpkg --version`, { stdio: `ignore` });
    return true;
  } catch (e) {
    return false;
  }
}
複製程式碼

run

前面的那些操作可以說都是處理一些判斷與相容邏輯,到run這裡才是 真正的核心安裝邏輯,__開始安裝依賴,拷貝模版__等。

function run(...) {
  // 這裡獲取要安裝的package,預設情況下是 `react-scripts`。 也可能是根據傳參去拿對應的包
  const packageToInstall = getInstallPackage(version, originalDirectory);
  // 需要安裝所有的依賴, react, react-dom, react-script
  const allDependencies = [`react`, `react-dom`, packageToInstall];
  ...
}
複製程式碼

run 做的事情主要有這麼幾個,先根據傳入的版本version 和原始目錄originalDirectory 去獲取要安裝的某個 package。
預設的 version 為空,獲取到的 packageToInstall 值是 react-scripts, 然後將packageToInstall拼接到 allDependencies意為所有需要安裝的依賴。
這裡說一下react-scripts其實就是一系列的webpack配置與模版,屬於 create-react-app 另一個核心的一個大模組。傳送門

function run(...) {
  ...
  // 獲取包名,支援 taz|tar格式、git倉庫、版本號、檔案路徑等等
  getPackageName(packageToInstall)
    .then(packageName =>
      // 如果是yarn,判斷是否線上模式(對應的就是離線模式),處理完判斷就返回給下一個then處理
      checkIfOnline(useYarn).then(isOnline => ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info => {
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      /** 開始核心的安裝部分 傳入`安裝路徑`,`是否使用yarn`,`所有依賴`,`是否輸出額外資訊`,`線上狀態` **/
      /** 這裡主要的操作是 根據傳入的引數,開始跑 npm || yarn 安裝react react-dom等依賴 **/
      /** 這裡如果網路不好,可能會掛 **/
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        () => packageName
      );
    })
  ...
}
複製程式碼

然後如果當前是採用yarn安裝方式的話,就判斷是否處於離線狀態。判斷完連著前面的 packageToInstallallDependencies 一起丟給 install 方法,再由install方法去跑安裝。

run方法
getInstallPackage(); // 獲取要安裝的模版包 預設是 react-scripts
install(); // 傳引數給install 負責安裝 allDependencies
init(); // 呼叫安裝了的 react-scripts/script/init 去拷貝模版
.catch(); // 錯誤處理

install

function install(root, useYarn, dependencies, verbose, isOnline) {
  // 主要根據引數拼裝命令列,然後用node去跑安裝指令碼 如 `npm install react react-dom --save` 或者 `yarn add react react-dom`
  return new Promise((resolve, reject) => {
    let command;
    let args;

    // 開始拼裝 yarn 命令列
    if (useYarn) {
      command = `yarnpkg`;
      args = [`add`, `--exact`]; // 使用確切版本模式
      // 判斷是否是離線狀態 加個狀態
      if (!isOnline) {
        args.push(`--offline`);
      }
      [].push.apply(args, dependencies);
      // 將cwd設定為我們要安裝的目錄路徑
      args.push(`--cwd`);
      args.push(root);
      // 如果是離線的話輸出一些提示資訊

    } else {

      // npm 安裝模式,與yarn同理
      command = `npm`;
      args = [
        `install`,
        `--save`,
        `--save-exact`,
        `--loglevel`,
        `error`,
      ].concat(dependencies);

    }

    // 如果有傳verbose, 則加該引數 輸出額外的資訊
    if (verbose) {
      args.push(`--verbose`);
    }

    // 用 cross-spawn 跨平臺執行命令列
    const child = spawn(command, args, { stdio: `inherit` });

    //  關閉的處理
    child.on(`close`, code => {
      if (code !== 0) {
        return reject({ command: `${command} ${args.join(` `)}`, });
      }
      resolve();
    });
  });
}
複製程式碼

我們順著斷點從run跑到install方法,能看到程式碼里根據是否使用yarn分成兩種處理方法。
if (useYarn) { yarn 安裝邏輯 } else { npm 安裝邏輯 }
處理方法都是同個邏輯,根據傳入的 dependencies 去拼接需要安裝的依賴,主要有 react,react-dom,react-script 。再判斷verboseisOnline 加一些命令列的引數。
最後再用node跑命令,平臺差異的話是藉助cross-spawn去處理的,這裡不再贅述。
具體邏輯見上面程式碼,去掉不重要的資訊輸出,程式碼還是比較易懂。

install
根據傳進來的引數判斷用yarn還是npm
拼裝需要的依賴
用cross-spawn跑命令安裝

install會返回一個Promise在安裝完之後,斷點又回到我們的run函式繼續走接下來的邏輯。

function run() {
  ...
  getPackageName()
    .then(()=> {
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
            () => packageName
      );
    })
  ...
}
複製程式碼

既然我們的install已經把開發需要的依賴安裝完了,接下來我們可以開判斷當前執行的node是否符合我們已經安裝的react-scripts裡面的packages.json要求的node版本。
這句話有點繞,簡單來說就是判斷當前執行的node版本是否react-scripts這個依賴所需。

然後就把開始修改package.json我們已經安裝的依賴(react, react-dom, react-scripts)版本從原本的精確版本eg(16.0.0)修改為高於等於版本eg(^16.0.0)。
這些處理做完之後,我們的目錄是長這樣子的,裡面除了安裝的依賴和package.json外沒有任何東西。所以接下來的操作是生成一些webpack的配置和一個簡單的可啟動demo。

探索 create-react-app 原始碼

那麼他是怎麼快速生成這些東西的呢?
還記得一開始說了有一個 隱藏的命令列引數 --internal-testing-template 用來給開發者除錯用的嗎,所以其實create-react-app生成這些的方法就是直接把某一個路徑的模板拷貝到對應的地方。是不是很簡單粗暴hhhhh

run(...) {
  ...
    getPackageName(packageToInstall)
    .then(...)
    .then(info => install(...).then(()=> packageName))
    /** install 安裝完之後的邏輯 **/
    /** 從這裡開始拷貝模板邏輯 **/
    .then(packageName => {
      // 安裝完 react, react-dom, react-scripts 之後檢查當前環境執行的node版本是否符合要求
      checkNodeVersion(packageName);
      // 該項package.json裡react, react-dom的版本範圍,eg: 16.0.0 => ^16.0.0
      setCaretRangeForRuntimeDeps(packageName);

      // 載入script指令碼,並執行init方法
      const scriptsPath = path.resolve(
        process.cwd(),
        `node_modules`,
        packageName,
        `scripts`,
        `init.js`
      );
      const init = require(scriptsPath);
      // init 方法主要執行的操作是
      // 寫入package.json 一些指令碼。eg: script: {start: `react-scripts start`}
      // 改寫README.MD
      // 把預設的模版拷貝到專案下
      // 輸出成功與後續操作的資訊
      init(root, appName, verbose, originalDirectory, template);

      if (version === `react-scripts@0.9.x`) {
        // 如果是舊版本的 react-scripts 輸出提示
      }
    })
    .catch(reason => {
      // 出錯的話,把安裝了的檔案全刪了 並輸出一些日誌資訊等
    });
}
複製程式碼

這裡安裝完依賴之後,執行checkNodeVersion判斷node版本是否與依賴相符。
之後拼接路徑去跑目錄/node_modules/react-scripts/scripts/init.js,傳參讓他去做一些初始化的事情。
然後對出錯情況做一些相應的處理

/node_modules/react-scripts/script/init.js

目標資料夾/node_modules/react-scripts/script/init.js

module.exports = function(
  appPath,
  appName,
  verbose,
  originalDirectory,
  template
) {
  const ownPackageName = require(path.join(__dirname, `..`, `package.json`))
    .name;
  const ownPath = path.join(appPath, `node_modules`, ownPackageName);
  const appPackage = require(path.join(appPath, `package.json`));
  const useYarn = fs.existsSync(path.join(appPath, `yarn.lock`));

  // 1. 把啟動指令碼寫入目標 package.json 
  appPackage.scripts = {
    start: `react-scripts start`,
    build: `react-scripts build`,
    test: `react-scripts test --env=jsdom`,
    eject: `react-scripts eject`,
  };

  fs.writeFileSync(
    path.join(appPath, `package.json`),
    JSON.stringify(appPackage, null, 2)
  );

  // 2. 改寫README.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`)
    );
  }

  // 3. 把預設的模版拷貝到專案下,主要有 public, src/[APP.css, APP.js, index.js,....], .gitignore
  const templatePath = template
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, `template`);
  if (fs.existsSync(templatePath)) {
    fs.copySync(templatePath, appPath);
  } else {
    return;
  }
  fs.move(
    path.join(appPath, `gitignore`),
    path.join(appPath, `.gitignore`),
    [],
    err => { /* 錯誤處理 */  }
  );

  // 這裡再次進行命令列的拼接,如果後面發現沒有安裝react和react-dom,重新安裝一次
  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`);

  const templateDependenciesPath = path.join(
    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);
  }

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

  // 5. 輸出成功的日誌
};
複製程式碼

init檔案又是一個大頭,處理的邏輯主要有

  1. 修改package.json,寫入一些啟動指令碼,比如script: {start: `react-scripts start`},用來啟動開發專案
  2. 改寫README.MD,把一些幫助資訊寫進去
  3. 把預設的模版拷貝到專案下,主要有 public, src/[APP.css, APP.js, index.js,....], .gitignore
  4. 對舊版的node做一些相容的處理,這裡補一句,在選擇 react-scripts 時就有根據node版本去判斷選擇比較老的 @0.9.x 版。
  5. 如果完成輸出對應的資訊,如果失敗,做一些輸出日誌等操作。

這裡程式碼有點多,所以刪了一小部分,如果對初始的程式碼感興趣可以跳轉到這兒看react-scripts/scripts/init.js 傳送門

END~

到這裡 create-react-app 專案構建的部分大流程已經走完了,我們來回顧一下:

  1. 判斷node版本如果小於4就退出,否則執行 createReactApp.js 檔案
  2. createReactApp.js先做一些命令列的處理響應處理,然後判斷是否有傳入 projectName 沒有就提示並退出
  3. 根據傳入的 projectName 建立目錄,並建立package.json
  4. 判斷是否有特殊要求指定安裝某個版本的react-scripts,然後用cross-spawn去處理跨平臺的命令列問題,用yarnnpm安裝react, react-dom, react-scripts
  5. 安裝完之後跑 react-scripts/script/init.js 修改 package.json 的依賴版本,執行指令碼,並拷貝對應的模板到目錄裡。
  6. 處理完這些之後,輸出提示給使用者。

本來想把整個 create-react-app 說完,但是發現說一個建立就寫了這麼多,所以後面如果有想繼續看 react-scripts的話,會另外開一篇來講。
大家也可以根據這個思路自己斷點去看,不過 react-scripts 主要可能是webpack配置居多,斷點幫助應該不大。

相關文章