create-react-app教程-原始碼篇

66CCFF發表於2019-03-11

原文連結:create-react-app教程-原始碼篇

之前介紹了create-react-app的基本使用, 為了便於理解一個腳手架指令碼是如何運作的,現在來看一下 create-react-app v1.5.2 的原始碼

入口index.js

create-react-app 一般會作為全域性命令,因為便於更新等原因,create-react-app 只會做初始化倉庫 執行當前版本命令等操作。

找到 create-react-app 入口index檔案:

'use strict';
var chalk = require('chalk');
// 返回Node版本資訊,如果有多個版本返回多個版本
var currentNodeVersion = process.versions.node; 
var semver = currentNodeVersion.split('.');
var major = semver[0];// 取出第一個Node版本資訊
//小於 4.x的提示並終止程式
if (major < 4) {
  console.error(
    chalk.red(
      'You are running Node ' +
        currentNodeVersion +
        '.\n' +
        'Create React App requires Node 4 or higher. \n' +
        'Please update your version of Node.'
    )
  );
  process.exit(1);
}
// 沒有小於4就引入以下檔案繼續執行
require('./createReactApp');
複製程式碼

可以看到 index 檔案沒有做什麼,只是做為一個入口檔案判斷一下 node版本,小於 4.x的提示並終止程式, 如果正常則載入 ./createReactApp 這個檔案,主要的邏輯在該檔案實現。

createReactApp.js

雖然 createReactApp.js 有751行,但是裡面有一大半是註釋和錯誤友好資訊。

除了宣告的依賴。跟著執行順序先看到的是第56行 program

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')
  // info,用於列印出環境除錯的版本資訊
  .option(
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  )
  .option('--use-npm')// 預設使用`yarn`,指定使用`npm`
  .allowUnknownOption()
  .on('--help', () => {
    //help 資訊
  })
  .parse(process.argv);// 解析傳入的引數 
複製程式碼

這裡用到 commander 的依賴,這是 node.js 命令列介面的解決方案,正如我們所看到的 處理使用者輸入的引數,輸出友好的提示資訊等。

接著到了第109行:

//沒有輸入projectName的話,輸出一些提示資訊就終止程式
if (typeof projectName === 'undefined') {
    if (program.info) {// 如果引數輸入了 --info,就會進入這裡
    envinfo.print({// envinfo 是一個用來輸出當前環境系統的而一些系統資訊
      packages: ['react', 'react-dom', 'react-scripts'],
      noNativeIDE: true,
      duplicates: true,
    });
    process.exit(0);
  }
  //略去部分log...
  process.exit(1);
}
複製程式碼

這裡的 projectName 就是我們要建立的web應用名稱,如果沒有輸入的話,輸出一些提示資訊就終止程式。

createApp 檢測判斷

然後到了第148行 執行createApp

createApp(
  projectName,//專案名稱 
  program.verbose, //是否暑促額外資訊
  program.scriptsVersion, //傳入的指令碼版本
  program.useNpm, //是否使用npm
  hiddenProgram.internalTestingTemplate //除錯的模板路徑,這個不管它,給開發人員除錯用的……
);

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name);// 獲取當前程式執行的位置,也就是檔案目錄的絕對路徑
  const appName = path.basename(root);// 返回root路徑下最後一部分

  checkAppName(appName);// 檢查傳入的專案名合法性
  fs.ensureDirSync(name);//這裡的 fs = require('fs-extra');
  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);
  }
  //這裡的 semver = require('semver'); 做版本處理的
  //如果node版本不符合要求就使用舊版本的 react-scripts
  if (!semver.satisfies(process.version, '>=6.0.0')) {
    //略去log資訊...
    // Fall back to latest supported react-scripts on Node 4
    version = 'react-scripts@0.9.x';
  }

  // 如果npm版本小於3.x,使用舊版的 react-scripts
  if (!useYarn) {
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {
      //略去log資訊...
      // Fall back to latest supported react-scripts for npm 3
      version = 'react-scripts@0.9.x';
    }
  }
  // 判斷結束之後,執行 run 方法
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
複製程式碼

可以瞭解到 createApp 主要做的事情就是做一些安全判斷比如:檢查專案名是否合法,檢查新建的話是否安全,檢查npm版本,處理react-script的版本相容。然後看下在createApp中用到的 checkAppName

checkAppName 檢查專案名

function checkAppName(appName) {
  //這裡 validateProjectName = require('validate-npm-package-name');
  //可以用來判斷當前的專案名是否符合npm規範 比如不能大寫等
  const validationResult = validateProjectName(appName);
  // 判斷是否符合npm規範如果不符合,輸出提示並結束任務
  if (!validationResult.validForNewPackages) {
    //略去log資訊...
    process.exit(1);
  }

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

run 安裝依賴拷貝模版

在 createApp 方法體內呼叫了run方法,run方法體內完成主要的安裝依賴 拷貝模板等功能。

function run(root,appName,version,verbose,originalDirectory,template,useYarn) {
  // 這裡獲取要安裝的package,
  //    getInstallPackage 預設情況下packageToInstall是 `react-scripts`。
  //    也可能是根據去本地拿到對應的package
  //    react-scripts是一系列的webpack配置與模版
  const packageToInstall = getInstallPackage(version, originalDirectory);
  // 需要安裝所有的依賴
  const allDependencies = ['react', 'react-dom', packageToInstall];

  // getPackageName 獲取依賴包原始名稱並返回
  getPackageName(packageToInstall)
    .then(packageName =>
      // 如果是yarn,判斷是否線上模式(對應的就是離線模式),處理完判斷就返回給下一個then處理
      checkIfOnline(useYarn).then(isOnline => ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info => {
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      //略去log資訊...      
      //傳引數給install 負責安裝 allDependencies
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        () => packageName
      );
    })
    .then(packageName => {
      //檢查當前環境執行的node版本是否符合要求
      checkNodeVersion(packageName);
      //修改react, react-dom的版本資訊,將準確版本資訊改為高於等於版本
      //    例如 15.0.0 => ^15.0.0
      setCaretRangeForRuntimeDeps(packageName);
      // `react-scripts`指令碼的目錄
      const scriptsPath = path.resolve(
        process.cwd(),
        'node_modules',
        packageName,
        'scripts',
        'init.js'
      );
      const init = require(scriptsPath);
      //呼叫安裝了的 react-scripts/script/init 去拷貝模版
      init(root, appName, verbose, originalDirectory, template);
      //略去log資訊...  
    })
    
    .catch(reason => {
      // 出錯的話,把安裝了的檔案全刪了 並輸出一些日誌資訊等
      // 錯誤處理 略
      process.exit(1);
    });
}
複製程式碼

可以猜到其中最重要的邏輯是 install 安裝依賴和 init 拷貝模板。

install 安裝依賴

install 方法體中是根據引數拼裝命令列,然後用node去跑安裝指令碼 ,執行完成後返回一個 Promise

function install(root, useYarn, dependencies, verbose, isOnline) {
  return new Promise((resolve, reject) => {
    let command;
    let args;
    // 引數拼裝命令列,
    //    例如 使用yarn : `yarn add react react-dom`
    //    或 使用npm : `npm install react react-dom --save` 
    if (useYarn) {
      command = 'yarnpkg';
      args = ['add', '--exact'];
      if (!isOnline) {
        args.push('--offline');
      }
      [].push.apply(args, dependencies);
      args.push('--cwd');
      args.push(root);
      //略去log資訊... 
    } else {
      command = 'npm';
      args = [
        'install',
        '--save',
        '--save-exact',
        '--loglevel',
        'error',
      ].concat(dependencies);
    }

    if (verbose) {
      args.push('--verbose');
    }
    //然後用node去跑安裝指令碼 
    //這裡 spawn = require('cross-spawn'); 出來處理平臺差異
    const child = spawn(command, args, { stdio: 'inherit' });
    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(' ')}`,
        });
        return;
      }
      resolve();
    });
  });
}
複製程式碼

init 拷貝模板

init 方法預設是在 【當前web專案路徑】/node_modules/react-scripts/script/init.js 中 :

module.exports = function(
  appPath,
  appName,
  verbose,
  originalDirectory,
  template
) {
  const ownPath = path.dirname(
    require.resolve(path.join(__dirname, '..', 'package.json'))
  );
  const appPackage = require(path.join(appPath, 'package.json'));
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
  appPackage.dependencies = appPackage.dependencies || {};

  const useTypeScript = appPackage.dependencies['typescript'] != null;

  // 設定package.json 中 scripts/eslint/browserslist 資訊
  appPackage.scripts = {
    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',
  };
  appPackage.eslintConfig = {
    extends: 'react-app',
  };
  appPackage.browserslist = defaultBrowsers;

  fs.writeFileSync(
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  // 如果已有 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')
    );
  }

  //把預設的模版拷貝到專案下
  //  可以在 react-scripts/template 看到這些檔案 public目錄 src目錄 gitignore README.md
  const templatePath = template
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
  if (fs.existsSync(templatePath)) {
    fs.copySync(templatePath, appPath);
  } else {
    console.error(
      `Could not locate supplied template: ${chalk.green(templatePath)}`
    );
    return;
  }

  // 如果發現沒有安裝react和react-dom,重新安裝一次 程式碼略

  // Install additional template dependencies, if present
  //略去log資訊... 
};
複製程式碼

簡化一下邏輯這裡的主要內容就是 修改package.json資訊和拷貝模板檔案

~END~

相關文章