本文連結: 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配置、更新日誌、開源宣告等等…這些我們全都可以不用去看,那我們要看的核心原始碼放在哪裡呢
劃重點: 如果專案不知道從哪裡入手的話,首先從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 除錯詳細可以看這裡 傳送門
(ps:這裡可以看出來node在模組化的實現是通過用一個函式包裹起來,然後把 exports, requestd等引數傳進來以供使用)
開始斷點閱讀原始碼
packages/create-react-app/index.js github檔案傳送門
這個檔案十分簡單,只是做為一個入口檔案判斷一下 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安裝方式的話,就判斷是否處於離線狀態。判斷完連著前面的 packageToInstall
和 allDependencies
一起丟給 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
。再判斷verbose
和isOnline
加一些命令列的引數。
最後再用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
檔案又是一個大頭,處理的邏輯主要有
- 修改package.json,寫入一些啟動指令碼,比如
script: {start: `react-scripts start`}
,用來啟動開發專案 - 改寫README.MD,把一些幫助資訊寫進去
- 把預設的模版拷貝到專案下,主要有
public
,src/[APP.css, APP.js, index.js,....]
,.gitignore
- 對舊版的node做一些相容的處理,這裡補一句,在選擇 react-scripts 時就有根據node版本去判斷選擇比較老的 @0.9.x 版。
- 如果完成輸出對應的資訊,如果失敗,做一些輸出日誌等操作。
這裡程式碼有點多,所以刪了一小部分,如果對初始的程式碼感興趣可以跳轉到這兒看react-scripts/scripts/init.js 傳送門
END~
到這裡 create-react-app
專案構建的部分大流程已經走完了,我們來回顧一下:
- 判斷node版本如果小於4就退出,否則執行
createReactApp.js
檔案 createReactApp.js
先做一些命令列的處理響應處理,然後判斷是否有傳入projectName
沒有就提示並退出- 根據傳入的
projectName
建立目錄,並建立package.json
。 - 判斷是否有特殊要求指定安裝某個版本的
react-scripts
,然後用cross-spawn
去處理跨平臺的命令列問題,用yarn
或npm
安裝react
,react-dom
,react-scripts
。 - 安裝完之後跑
react-scripts/script/init.js
修改package.json
的依賴版本,執行指令碼,並拷貝對應的模板到目錄裡。 - 處理完這些之後,輸出提示給使用者。
本來想把整個 create-react-app 說完,但是發現說一個建立就寫了這麼多,所以後面如果有想繼續看 react-scripts
的話,會另外開一篇來講。
大家也可以根據這個思路自己斷點去看,不過 react-scripts
主要可能是webpack配置居多,斷點幫助應該不大。