原文鏈接:http://axuebin.com/articles/fe-solution/cli/cra.html,轉載請聯系
Create React App is an officially supported way to create single-page React applications. It offers a modern build setup with no configuration.
create react app
是 React
官方創建單頁應用的方式,為了方便,下文皆簡稱 CRA
。
它的核心思想我理解主要是:
- 腳手架核心功能中心化:使用
npx
保證每次用戶使用的都是最新版本,方便功能的升級 - 模板去中心化:方便地進行模板管理,這樣也允許用戶自定義模板
- 腳手架邏輯和初始化代碼邏輯分離:在
cra
中只執行了腳手架相關邏輯,而初始化代碼的邏輯在react-scripts
包里執行
本文主要就是通過源碼分析對上述的理解進行闡述。
按照自己的理解,畫了個流程圖,大家可以帶着該流程圖去閱讀源碼(主要包含兩個部分 create-react-app
和 react-scripts/init
):
如果圖片不清晰可以微信搜索公眾號 玩相機的程序員,回復 CRA
獲取。
0. 用法
CRA
的用法很簡單,兩步:
- 安裝:
npm install -g create-react-app
- 使用:
create-react-app my-app
這是常見的用法,會在全局環境下安裝一個 CRA
,在命令行中可以通過 create react app
直接使用。
現在更推薦的用法是使用 npx
來執行 create react app
:
npx create-react-app my-app
這樣確保每次執行 create-reat-app
使用的都是 npm
上最新的版本。
注:npx 是 npm 5.2+
之后引入的功能,如需使用需要 check
一下本地的 npm
版本。
默認情況下,CRA
命令只需要傳入 project-directory
即可,不需要額外的參數,更多用法查看:https://create-react-app.dev/docs/getting-started#creating-an-app,就不展開了。
可以看一下官方的 Demo 感受一下:
我們主要還是通過 CRA
的源碼來了解一下它的思路。
1. 入口
本文中的
create-react-app
版本為4.0.1
。若閱讀本文時存在break change
,可能就需要自己理解一下啦
按照正常邏輯,我們在 package.json
里找到了入口文件:
{
"bin": {
"create-react-app": "./index.js"
}
}
index.js
里的邏輯比較簡單,判斷了一下 node
環境是否是 10
以上,就調用 init
了,所以核心還是在 init
方法里。
// index.js
const { init } = require('./createReactApp');
init();
打開 createReactApp.js
文件一看,好家伙,1017 行代碼(別慌,跟着我往下看,1000 行代碼也分分鍾看明白)
吐槽一下,雖然代碼邏輯寫得很清楚,但是為啥不拆幾個模塊呢?
找到 init
方法之后發現,其實就執行了一個 Promise
:
// createReactApp.js
function init() {
checkForLatestVersion()
.catch()
.then();
}
注意這里是先 catch
再 then
。
跟着我往下看唄 ~ 一步一步理清楚 CRA
,你也能依葫蘆畫瓢造一個。
2. 檢查版本
checkForLatestVersion
就做了一件事,獲取 create-react-app
這個 npm
包的 latest
版本號。
如果你想獲取某個 npm
包的版本號,可以通過開放接口 [https://registry.npmjs.org/-/package/{pkgName}/dist-tags](https://registry.npmjs.org/-/package/%7BpkgName%7D/dist-tags "https://registry.npmjs.org/-/package/{pkgName}/dist-tags")
獲得,其返回值為:
{
"next": "4.0.0-next.117",
"latest": "4.0.1",
"canary": "3.3.0-next.38"
}
如果你想獲取某個 npm
包完整信息,可以通過開放接口 [https://registry.npmjs.org/{pkgName}](https://registry.npmjs.org/%7BpkgName%7D "https://registry.npmjs.org/{pkgName}")
獲得,其返回值為:
{
"name": "create-react-app", # 包名
"dist-tags": {}, # 版本語義化標簽
"versions": {}, # 所有版本信息
"readme": "", # README 內容(markdown 文本)
"maintainers": [],
"time": {}, # 每個版本的發布時間
"license": "",
"readmeFilename": "README.md",
"description": "",
"homepage": "", # 主頁
"keywords": [], # 關鍵詞
"repository": {}, # 代碼倉庫
"bugs": {}, # 提 bug 鏈接
"users": {}
}
回到源碼,checkForLatestVersion().catch().then()
,注意這里是先 catch
再 then
,也就是說如果 checkForLatestVersion
里拋錯誤了,會被 catch
住,然后執行一些邏輯,再執行 then
。
是的,Promise
的 catch
后面的 then
還是會執行。
2.1 Promise catch 后的 then
我們可以做個小實驗:
function promise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Promise 失敗了');
}, 1000);
});
}
promise()
.then(res => {
console.log(res);
})
.catch(error => {
console.log(error); // Promise 失敗了
return `ErrorMessage: ${error}`;
})
.then(res => {
console.log(res); // ErrorMessage: Promise 失敗了
});
原理也很簡單,then
和 catch
返回的都是一個 promise
,當然可以繼續調用。
OK,checkForLatestVersion
以及之后的 catch
都是只做了一件事,獲取 latest
版本號,如果沒有就是 null
。
這里拿到版本號之后也就判斷一下當前使用的版本是否比 latest
版本低,如果是就推薦你把全局的 CRA
刪了,使用 npx
來執行 CRA
。
3. 核心方法 createApp
再往下看就是執行了一個 createApp
了,看這名字就知道最關鍵的方法就是它了。
function createApp(name, verbose, version, template, useNpm, usePnp) {
// 此處省略 100 行代碼
}
createApp
傳入了 6 個參數,對應的是 CRA
命令行傳入的一些配置。
我在思考為啥這里不設計成一個 options
對象來接受這些參數?如果后期需要增刪一些參數,是不是比較不好維護?這樣的想法是我過度設計嗎?
4. 檢查應用名
CRA
會檢查輸入的 project name
是否符合以下兩條規范:
- 檢查是否符合
npm
命名規范 - 檢查是否含有
react
/react-dom
/react-scripts
等關鍵字
不符合規范則直接process.exit(1)
退出進程。
5. 創建 package.json
和一般腳手架不同的是,CRA
會在創建項目時新創建一個 package.json
,而不是直接復制代碼模板的文件。
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
6. 選擇模板
function getTemplateInstallPackage(template, originalDirectory) {
let templateToInstall = 'cra-template';
if (template) {
// 一些處理邏輯 doTemplate(template);
templateToInstall = doTemplate(template);
}
return Promise.resolve(templateToInstall);
}
默認使用 cra-template
模板,如果傳入 template
參數,則使用對用的模板,該方法主要是給額外的 template
加 scope
和 prefix
,比如 @scope/cra-template-${template}
,具體邏輯不展開。
這里 CRA
的核心思想是通過 npm
來對模板進行管理,這樣方便擴展和管理。
7. 安裝依賴
CRA
會自動給項目安裝 react
、react-dom
和 react-scripts
以及模板。
command = 'npm';
args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(
dependencies
);
const child = spawn(command, args, { stdio: 'inherit' });
8. 初始化代碼
CRA
的功能其實不多,安裝完依賴之后,實際上初始化代碼的工作還沒做。
接着往下看,看到這樣一段代碼代碼:
await executeNodeScript(
{
cwd: process.cwd(),
},
[root, appName, verbose, originalDirectory, templateName],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
除此之外,CRA
貌似看不到任何復制代碼的代碼了,那我們需要的“初始化代碼”的工作應該就是在這里完成了。
為了分析方便,忽略了上下文代碼,說明一下,這段代碼中的 packageName
的值是 react-scripts
。也就是這里執行了 react-scripts
包中的 scripts/init
方法,並傳入了幾個參數。
8.1 react-scripts/init.js
老規矩,只分析主流程代碼,主流程主要就做了四件事:
- 處理
template
里的packages.json
- 處理
package.json
的scripts
:默認值和template
合並 - 寫入
package.json
- 拷貝
template
文件
除此之外還有一些 git
和 npm
相關的操作,這里就不展開了。
// init.js
// 刪除了不影響主流程的代碼
module.exports = function(
appPath,
appName,
verbose,
originalDirectory,
templateName
) {
const appPackage = require(path.join(appPath, 'package.json'));
// 通過一些判斷來處理 template 中的 package.json
// 返回 templatePackage
const templateScripts = templatePackage.scripts || {};
// 修改實際 package.json 中的 scripts
// start、build、test 和 eject 是默認的命令,如果模板里還有其它 script 就 merge
appPackage.scripts = Object.assign(
{
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
},
templateScripts
);
// 寫 package.json
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2) + os.EOL
);
// 拷貝 template 文件
const templateDir = path.join(templatePath, 'template');
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, appPath);
}
};
到這里,CRA
的主流程就基本走完了,關於 react-scripts
的命令,比如 start
和 build
,后續會單獨有文章進行講解。
9. 從 CRA 中借鑒的工具方法
CRA
的代碼和思路其實並不復雜,但是不影響我們讀它的代碼,並且從中學習到一些好的想法。(當然,有一些代碼我們也是可以拿來直接用的 ~
9.1 npm 相關
9.1.1 獲取 npm 包版本號
const https = require('https');
function getDistTags(pkgName) {
return new Promise((resolve, reject) => {
https
.get(
`https://registry.npmjs.org/-/package/${pkgName}/dist-tags`,
res => {
if (res.statusCode === 200) {
let body = '';
res.on('data', data => (body += data));
res.on('end', () => {
resolve(JSON.parse(body));
});
} else {
reject();
}
}
)
.on('error', () => {
reject();
});
});
}
// 獲取 react 的版本信息
getDistTags('react').then(res => {
const tags = Object.keys(res);
console.log(tags); // ['latest', 'next', 'experimental', 'untagged']
console.log(res.latest]); // 17.0.1
});
9.1.2 比較 npm 包版本號
使用 semver
包來判斷某個 npm
的版本號是否符合你的要求:
const semver = require('semver');
semver.gt('1.2.3', '9.8.7'); // false
semver.lt('1.2.3', '9.8.7'); // true
semver.minVersion('>=1.0.0'); // '1.0.0'
9.1.3 檢查 npm 包名
可以通過 validate-npm-package-name
來檢查包名是否符合 npm
的命名規范。
const validateProjectName = require('validate-npm-package-name');
const validationResult = validateProjectName(appName);
if (!validationResult.validForNewPackages) {
console.error('npm naming restrictions');
// 輸出不符合規范的 issue
[
...(validationResult.errors || []),
...(validationResult.warnings || []),
].forEach(error => {
console.error(error);
});
}
對應的 npm
命名規范可以見:Naming Rules
9.2 git 相關
9.2.1 判斷本地目錄是否是一個 git 倉庫
const execSync = require('child_process').execSync;
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
9.2.2 git init
腳手架初始化代碼之后,正常的研發鏈路都希望能夠將本地代碼提交到 git
進行托管。在這之前,就需要先對本地目錄進行 init
:
const execSync = require('child_process').execSync;
function tryGitInit() {
try {
execSync('git --version', { stdio: 'ignore' });
if (isInGitRepository()) {
return false;
}
execSync('git init', { stdio: 'ignore' });
return true;
} catch (e) {
console.warn('Git repo not initialized', e);
return false;
}
}
9.2.3 git commit
對本地目錄執行 git commit
:
function tryGitCommit(appPath) {
try {
execSync('git add -A', { stdio: 'ignore' });
execSync('git commit -m "Initialize project using Create React App"', {
stdio: 'ignore',
});
return true;
} catch (e) {
// We couldn't commit in already initialized git repo,
// maybe the commit author config is not set.
// In the future, we might supply our own committer
// like Ember CLI does, but for now, let's just
// remove the Git files to avoid a half-done state.
console.warn('Git commit not created', e);
console.warn('Removing .git directory...');
try {
// unlinkSync() doesn't work on directories.
fs.removeSync(path.join(appPath, '.git'));
} catch (removeErr) {
// Ignore.
}
return false;
}
}
10. 總結
回到 CRA
,看完本文,對於 CRA
的思想可能有了個大致了解:
CRA
是一個通用的React
腳手架,它支持自定義模板的初始化。將模板代碼托管在npm
上,而不是傳統的通過git
來托管模板代碼,這樣方便擴展和管理CRA
只負責核心依賴、模板的安裝和腳手架的核心功能,具體初始化代碼的工作交給react-scripts
這個包
但是具體細節上它是如何做的這個我沒有詳細的闡述,如果感興趣的同學可以自行下載其源碼閱讀。推薦閱讀源碼流程:
- 看它的單測
- 一步一步 debug 它
- 看源碼細節