之前介紹了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~