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
。javascript
它的核心思想我理解主要是:java
npx
保證每次用戶使用的都是最新版本,方便功能的升級cra
中只執行了腳手架相關邏輯,而初始化代碼的邏輯在 react-scripts
包裏執行本文主要就是經過源碼分析對上述的理解進行闡述。node
按照本身的理解,畫了個流程圖,你們能夠帶着該流程圖去閱讀源碼(主要包含兩個部分 create-react-app
和 react-scripts/init
):react
若是圖片不清晰能夠微信搜索公衆號 玩相機的程序員,回覆 CRA
獲取。git
CRA
的用法很簡單,兩步:程序員
npm install -g create-react-app
create-react-app my-app
這是常見的用法,會在全局環境下安裝一個 CRA
,在命令行中能夠經過 create react app
直接使用。github
如今更推薦的用法是使用 npx
來執行 create react app
:npm
npx create-react-app my-app
這樣確保每次執行 create-reat-app
使用的都是 npm
上最新的版本。json
注:npx 是 npm 5.2+
以後引入的功能,如需使用須要 check
一下本地的 npm
版本。promise
默認狀況下,CRA
命令只須要傳入 project-directory
便可,不須要額外的參數,更多用法查看:https://create-react-app.dev/docs/getting-started#creating-an-app,就不展開了。
能夠看一下官方的 Demo 感覺一下:
咱們主要仍是經過 CRA
的源碼來了解一下它的思路。
本文中的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
,你也能依葫蘆畫瓢造一個。
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
仍是會執行。
咱們能夠作個小實驗:
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
。
再往下看就是執行了一個 createApp
了,看這名字就知道最關鍵的方法就是它了。
function createApp(name, verbose, version, template, useNpm, usePnp) { // 此處省略 100 行代碼 }
createApp
傳入了 6 個參數,對應的是 CRA
命令行傳入的一些配置。
我在思考爲啥這裏不設計成一個 options
對象來接受這些參數?若是後期須要增刪一些參數,是否是比較很差維護?這樣的想法是我過分設計嗎?
CRA
會檢查輸入的 project name
是否符合如下兩條規範:
npm
命名規範react
/react-dom
/react-scripts
等關鍵字process.exit(1)
退出進程。和通常腳手架不一樣的是,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 );
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
來對模板進行管理,這樣方便擴展和管理。
CRA
會自動給項目安裝 react
、react-dom
和 react-scripts
以及模板。
command = 'npm'; args = [ 'install', '--save', '--save-exact', '--loglevel', 'error', ].concat(dependencies); const child = spawn(command, args, { stdio: 'inherit' });
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
方法,並傳入了幾個參數。
老規矩,只分析主流程代碼,主流程主要就作了四件事:
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
,後續會單獨有文章進行講解。
CRA
的代碼和思路其實並不複雜,可是不影響咱們讀它的代碼,而且從中學習到一些好的想法。(固然,有一些代碼咱們也是能夠拿來直接用的 ~
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 });
使用 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'
能夠經過 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
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; } }
腳手架初始化代碼以後,正常的研發鏈路都但願可以將本地代碼提交到 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; } }
對本地目錄執行 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; } }
回到 CRA
,看完本文,對於 CRA
的思想可能有了個大體瞭解:
CRA
是一個通用的 React
腳手架,它支持自定義模板的初始化。將模板代碼託管在 npm
上,而不是傳統的經過 git
來託管模板代碼,這樣方便擴展和管理CRA
只負責核心依賴、模板的安裝和腳手架的核心功能,具體初始化代碼的工做交給 react-scripts
這個包可是具體細節上它是如何作的這個我沒有詳細的闡述,若是感興趣的同窗能夠自行下載其源碼閱讀。推薦閱讀源碼流程:
我的原創技術文章會同步更新在公衆號 玩相機的程序員 上,歡迎你們關注。我是 axuebin,用鍵盤和相機記錄生活。