create-react-app教程-源碼篇

原文連接:create-react-app教程-源碼篇node

以前介紹了create-react-app的基本使用, 爲了便於理解一個腳手架腳本是如何運做的,如今來看一下 create-react-app v1.5.2 的源碼react

入口index.js

create-react-app 通常會做爲全局命令,由於便於更新等緣由,create-react-app 只會作初始化倉庫 執行當前版本命令等操做。webpack

找到 create-react-app 入口index文件:git

'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 這個文件,主要的邏輯在該文件實現。web

createReactApp.js

雖然 createReactApp.js 有751行,可是裏面有一大半是註釋和錯誤友好信息。typescript

除了聲明的依賴。跟着執行順序先看到的是第56行 programnpm

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 命令行接口的解決方案,正如咱們所看到的 處理用戶輸入的參數,輸出友好的提示信息等。json

接着到了第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應用名稱,若是沒有輸入的話,輸出一些提示信息就終止程序。app

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~

相關文章
相關標籤/搜索