探索 create-react-app 源碼

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

系列二地址css

最近工做開始穩定下來,沒有那麼多加班...因此開始有空閒的時間能夠學習一些前端知識前端

以前公司有個大佬寫了個相似 create-react-app 的腳手架,用來建立公司的項目。一直不知道里面實現的原理,藉此機會一探 create-react-app 源碼,瞭解下里面運行的機制。java

你們不要一看到源碼就懼怕不敢去看,如今這麼優秀項目都開源了,加上各類IDE支持很好,直接打個斷點進去調試,很容易看出個大概。 也能夠用這種思路去了解其餘的開源項目node

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

快速瞭解

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

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

能夠簡單分爲如下幾個步驟:github

  • 判斷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~

繼續往下看的小夥伴能夠跟着一步一步瞭解裏面的實現邏輯,先例行交代下環境版本:web

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配置、更新日誌、開源聲明等等...這些咱們全均可以不用去看,那咱們要看的核心源碼放在哪裏呢

劃重點: 若是項目不知道從哪裏入手的話,首先從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 斷點,直接在行號的左邊點一下鼠標就能夠了

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 調試詳細能夠看這裏 傳送門

終端啓動node調試
(ps:這裏能夠看出來node在模塊化的實現是經過用一個函數包裹起來,而後把 exports, requestd等參數傳進來以供使用)

開始斷點閱讀源碼

packages/create-react-app/index.js github文件傳送門

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。

那麼他是怎麼快速生成這些東西的呢? 還記得一開始說了有一個 隱藏的命令行參數 --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配置居多,斷點幫助應該不大。

相關文章
相關標籤/搜索