最近想寫一些東西,學習學習全棧開發,就從熟悉的react開始,一直開發模式都是組件形式,對create-react-app
源碼不瞭解,須要從新學習下!
網上找了不少資料,刷官網...
找到一個寫的很全面的,可是create-react-app
已更新版本,我下載的是v3.3.0版本,版本不同源碼也會有些出入,因此這裏也記錄下本身的學習內容,方便後期自我查看和學習。
文中都是借鑑和一些自我理解,有些錯誤或者理解上的誤差,歡迎指正。node
原文連接:https://segmentfault.com/a/11...react
create-react-app 源碼**webpack
├── .github --- 項目中提的issue
和pr
的規範
├── docusaurus--- facebook的開源的用於簡化構建,部署,和維護的博客網站
├── packages --- 包 項目核心
├── tasks--- 任務
├── test--- 測試文件
├── .eslintignore --- eslint
檢查時忽略文件
├── .eslintrc.json --- eslint
配置文件
├── .gitignore --- git
提交時忽略文件
├── .travis.yml --- travis
配置文件
├── .yarnrc --- yarn
配置文件
├── azure-pipelines-test-job.yml--- Azure Pipelines
test配置
├── azure-pipelines.yml --- Azure Pipelines
配置,用於在Linux
,Windows
和macOS
上構建和測試create-react-app
├── CHANGELOG-0.x.md --- 版本變動說明文件
├── CHANGELOG-1.x.md --- 版本變動說明文件
├── CHANGELOG-2.x.md --- 版本變動說明文件
├── CHANGELOG.md --- 當前版本變動說明文件
├── CODE_OF_CONDUCT.md --- facebook
代碼行爲準則說明
├── CONTRIBUTING.md--- 項目的核心說明
├── lerna.json--- lerna
配置文件
├── LICENSE --- 開源協議
├── netlify.toml --- 能夠理解爲docusaurus
的配置設置
├── package.json --- 項目配置文件
├── README.md --- 項目使用說明
├── screencast-error.svg
└── screencast.svggit
├── babel-plugin-named-asset-import
├── babel-preset-react-app
├── confusing-browser-globals
├── cra-template
├── cra-template-typescript
├── create-react-app
├── eslint-config-react-app
├── react-app-polyfill
├── react-dev-utils
├── react-error-overlay
└── react-scripts github
代碼的入口在packages/create-react-app/index.js
下,核心代碼在createReactApp.js
中web
這裏添加三種環境,是 create-react-app 的不一樣種使用方式typescript
create-react-app study-create-react-app-source
create-react-app
create-react-app study-create-react-app-source-ts --typescript
VsCode中Debug調試插件配置shell
包 | 包做用 |
---|---|
validate-npm-package-name | 判斷給定的字符串是否是符合npm包名稱的規範,檢查包名是否合法 validate |
chalk | 給命令行輸入的log設置顏色 chalk |
commander | 自定義shell命令的工具,也就是能夠用它代管Node命令,具體使用能夠查看commander |
semver | 用於版本比較的工具,好比哪一個版本大,版本命名是否合法等 |
envinfo | 輸出當前環境的信息,用於比較Node 版本 envinfo |
fs-extra | Node 自帶文件模塊的外部擴展模塊 fs-extra |
cross-spawn | 用來執行node 進程,Node 跨平臺解決方案,解決在windows 下各類問題,詳細看 cross-spawn |
hyperquest | 用於將http請求流媒體傳輸,詳細看 hyperquest |
dns | 用來檢測是否可以請求到指定的地址 dns |
'use strict'; var currentNodeVersion = process.versions.node; // 返回Node版本信息,若是有多個版本返回多個版本 var semver = currentNodeVersion.split('.'); // 全部Node版本的集合 var major = semver[0]; // 取出第一個Node版本信息 if (major < 8) { // 校驗當前node版本是否低於8 console.error( 'You are running Node ' + currentNodeVersion + '.\n' + 'Create React App requires Node 8 or higher. \n' + 'Please update your version of Node.' ); process.exit(1); } require('./createReactApp');
index.js 的代碼很是的簡單,其實就是對 node 的版本作了一下校驗,若是版本號低於 8,就退出應用程序,不然直接進入到核心文件中,createReactApp.js
中npm
createReactApp 的功能也很是簡單其實,大概流程:json
create-react-app --info
的輸出等react-script
下的模板文件let projectName; //定義了一個用來存儲項目名稱的變量 const program = new commander.Command(packageJson.name) .version(packageJson.version) //create-react-app -v 時候輸出的值 packageJson 來自上面 const packageJson = require('./package.json'); .arguments('<project-directory>')//定義 project-directory ,必填項 .usage(`${chalk.green('<project-directory>')} [options]`) // 使用`create-react-app`第一行打印的信息,也就是使用說明 .action(name => { projectName = name; //獲取用戶的輸入,存爲 projectName }) .option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的選項,相似 --help -V .option('--info', 'print environment debug info') // 打印本地相關開發環境,操做系統,`Node`版本等等 .option( // 指定 react-scripts '--scripts-version <alternative-package>', 'use a non-standard version of react-scripts' ) .option(// 項目建立template '--template <path-to-template>', 'specify a template for the created project' ) .option('--use-npm') // 指定使用npm 默認使用yarn .option('--use-pnp') //指定使用 pnp // TODO: Remove this in next major release. .option( // 指定使用ts,不過這裏也有備註下一個版本就刪除這項 '--typescript', '(this option will be removed in favour of templates in the next major release of create-react-app)' ) .allowUnknownOption() .on('--help', () => { // on('option', cb) 語法,輸入 create-react-app --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 specific npm tag: ${chalk.green('@next')}`); console.log( ` - a custom fork published on npm: ${chalk.green( 'my-react-scripts' )}` ); console.log( ` - a local path relative to the current working directory: ${chalk.green( 'file:../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(` A custom ${chalk.cyan('--template')} can be one of:`); console.log( ` - a custom fork published on npm: ${chalk.green( 'cra-template-typescript' )}` ); console.log( ` - a local path relative to the current working directory: ${chalk.green( 'file:../my-custom-template' )}` ); console.log( ` - a .tgz archive: ${chalk.green( 'https://mysite.com/my-custom-template-0.8.2.tgz' )}` ); console.log( ` - a .tar.gz archive: ${chalk.green( 'https://mysite.com/my-custom-template-0.8.2.tar.gz' )}` ); console.log(); console.log( ` If you have any problems, do not hesitate to file an issue:` ); console.log( ` ${chalk.cyan( 'https://github.com/facebook/create-react-app/issues/new' )}` ); console.log(); }) .parse(process.argv);// commander 文檔傳送門,解析正常的`node
// 判斷當前環境信息,退出 if (program.info) { console.log(chalk.bold('\nEnvironment Info:')); console.log( `\n current version of ${packageJson.name}: ${packageJson.version}` ); console.log(` running from ${__dirname}`); return envinfo .run( { System: ['OS', 'CPU'], Binaries: ['Node', 'npm', 'Yarn'], Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'], npmPackages: ['react', 'react-dom', 'react-scripts'], npmGlobalPackages: ['create-react-app'], }, { duplicates: true, showNotFound: true, } ) .then(console.log); } //判斷 projectName 是否爲 undefined,而後輸出相關提示信息,退出~ if (typeof projectName === 'undefined') { //沒有項目名稱也沒有--info 會拋出異常 console.error('Please specify the project directory:'); console.log( ` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}` ); console.log(); console.log('For example:'); console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`); console.log(); console.log( `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` ); process.exit(1); }
create-react-app <my-project>
中的項目名稱賦予了projectName
變量,此處的做用就是看看用戶有沒有傳這個<my-project>
參數,若是沒有就會報錯,並顯示一些幫助信息,這裏用到了另一個外部依賴envinfo
。
修改vscode 配置文件
{ "type": "node", "request": "launch", "name": "CreateReactAppTs", "program": "${workspaceFolder}/packages/create-react- app/index.js", "args": \[ "study-create-react-app-source-ts", "--typescript", "--use-npm" \] }
commander
的option
選項,若是加了這個選項這個值就是true
,不然就是false
,也就是說這裏若是加了--typescript
和--use-npm
,那這個參數就是true
,這些也都是咱們在commander
中定義的 options,在源碼裏面 createApp 中,傳入的參數分別爲:
npm
,默認使用yarn
Pnp
,默認使用yarn
ts
function createApp( name, verbose, version, template, useNpm, usePnp, useTypeScript ) { // 檢測node版本 const unsupportedNodeVersion = !semver.satisfies(process.version, '>=8.10.0'); // 判斷node版本,若是結合TS一塊兒使用就須要8.10或者更高版本,具體的方法在semver.js if (unsupportedNodeVersion && useTypeScript) { console.error( chalk.red( `You are using Node ${process.version} with the TypeScript template. Node 8.10 or higher is required to use TypeScript.\n` ) ); process.exit(1); } else if (unsupportedNodeVersion) { console.log( chalk.yellow( `You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` + `Please update to Node 8.10 or higher for a better, fully supported experience.\n` ) ); // Fall back to latest supported react-scripts on Node 4 version = 'react-scripts@0.9.x'; } const root = path.resolve(name); //path 拼接路徑 const appName = path.basename(root); //獲取文件名 checkAppName(appName); //檢查傳入的文件名合法性 fs.ensureDirSync(name); //確保目錄存在,若是不存在則建立一個 if (!isSafeToCreateProjectIn(root, name)) {//判斷新建這個文件夾是否安全,不然直接退出 process.exit(1); } console.log(); // 到這裏打印成功建立了一個`react`項目在指定目錄下 console.log(`Creating a new React app in ${chalk.green(root)}.`); console.log(); // 定義package.json基礎內容 const packageJson = { name: appName, version: '0.1.0', private: true, }; // 往咱們建立的文件夾中寫入package.json文件 fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL ); // 定義常量 useYarn 若是傳參有 --use-npm useYarn就是false,不然執行 shouldUseYarn() 檢查yarn是否存在 // 這一步就是以前說的他默認使用`yarn`,可是能夠指定使用`npm`,若是指定使用了`npm`,`useYarn`就是`false`,否則執行 shouldUseYarn 函數 // shouldUseYarn 用於檢測本機是否安裝了`yarn` const useYarn = useNpm ? false : shouldUseYarn(); // 取得當前node進程的目錄,提早獲取保存下來,接下來這個值會被改變,若是後面須要用到這個值,後續其實取得值將不是這個 // 因此這裏的目的就是提早存好,省得我後續使用的時候很差去找,這個地方就是我執行初始化項目的目錄,而不是初始化好的目錄,是初始化的上級目錄 const originalDirectory = process.cwd(); // 修改進程目錄爲底下子進程目錄 // 在這裏就把進程目錄修改成了咱們建立的目錄 process.chdir(root); // checkThatNpmCanReadCwd 這個函數的做用是檢查進程目錄是不是咱們建立的目錄,也就是說若是進程不在咱們建立的目錄裏面,後續再執行`npm`安裝的時候就會出錯,因此提早檢查 if (!useYarn && !checkThatNpmCanReadCwd()) { process.exit(1); } if (!useYarn) { //關於 npm、pnp、yarn 的使用判斷,版本校驗等 const npmInfo = checkNpmVersion(); if (!npmInfo.hasMinNpm) { if (npmInfo.npmVersion) { console.log( chalk.yellow( `You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` + `Please update to npm 5 or higher for a better, fully supported experience.\n` ) ); } // Fall back to latest supported react-scripts for npm 3 version = 'react-scripts@0.9.x'; } } else if (usePnp) { const yarnInfo = checkYarnVersion(); if (!yarnInfo.hasMinYarnPnp) { if (yarnInfo.yarnVersion) { console.log( chalk.yellow( `You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` + `Please update to Yarn 1.12 or higher for a better, fully supported experience.\n` ) ); } // 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still) usePnp = false; } } if (useTypeScript) { console.log( chalk.yellow( 'The --typescript option has been deprecated and will be removed in a future release.' ) ); console.log( chalk.yellow( `In future, please use ${chalk.cyan('--template typescript')}.` ) ); console.log(); if (!template) { template = 'typescript'; } } if (useYarn) { let yarnUsesDefaultRegistry = true; try { yarnUsesDefaultRegistry = execSync('yarnpkg config get registry') .toString() .trim() === 'https://registry.yarnpkg.com'; } catch (e) { // ignore } if (yarnUsesDefaultRegistry) { fs.copySync( require.resolve('./yarn.lock.cached'), path.join(root, 'yarn.lock') ); } } run( root, appName, version, verbose, originalDirectory, template, useYarn, usePnp ); }
代碼很是簡單,部分註釋已經加載代碼中,簡單的說就是對一個本地環境的一些校驗,版本檢查、目錄建立等,若是建立失敗,則退出,若是版本較低,則使用對應低版本的create-react-app
,最後調用 run 方法
checkAppName()
:用於檢測文件名是否合法,isSafeToCreateProjectIn()
:用於檢測文件夾是否安全shouldUseYarn()
:用於檢測yarn
在本機是否已經安裝checkThatNpmCanReadCwd()
:用於檢測npm
是否在正確的目錄下執行checkNpmVersion()
:用於檢測npm
在本機是否已經安裝了checkAPPName 方法主要的核心代碼是validate-npm-package-name
package,從名字便可看出,檢查是否爲合法的 npm 包名
function checkAppName(appName) { // 關於validateProjectName方法 在插件`validate-npm-package-name`index文件 const validationResult = validateProjectName(appName); if (!validationResult.validForNewPackages) { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${appName}"` )} because of npm naming restrictions:\n` ) ); [ ...(validationResult.errors || []), ...(validationResult.warnings || []), ].forEach(error => { console.error(chalk.red(` * ${error}`)); }); console.error(chalk.red('\nPlease choose a different project name.')); process.exit(1); } // TODO: there should be a single place that holds the dependencies const dependencies = ['react', 'react-dom', 'react-scripts'].sort(); if (dependencies.includes(appName)) { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${appName}"` )} because a dependency with the same name exists.\n` + `Due to the way npm works, the following names are not allowed:\n\n` ) + chalk.cyan(dependencies.map(depName => ` ${depName}`).join('\n')) + chalk.red('\n\nPlease choose a different project name.') ); process.exit(1); } }
這個函數用了一個外部依賴來校驗文件名是否符合npm包文件名的規範,而後定義了三個不能取得名字react、react-dom、react-scripts,紅色框出來的,以前引用的是一個函數依賴printValidationResults()
,最新的代碼裏面換成了ES6
屬性spread
,不過這個屬性在ES9
裏也有新的用法
printValidationResults()
:函數引用,這個函數就是我說的特別簡單的類型,裏面就是把接收到的錯誤信息循環打印出來
function isSafeToCreateProjectIn(root, name) { const validFiles = [ '.DS_Store', '.git', '.gitattributes', '.gitignore', '.gitlab-ci.yml', '.hg', '.hgcheck', '.hgignore', '.idea', '.npmignore', '.travis.yml', 'docs', 'LICENSE', 'README.md', 'mkdocs.yml', 'Thumbs.db', ]; // These files should be allowed to remain on a failed install, but then // silently removed during the next create. const errorLogFilePatterns = [ 'npm-debug.log', 'yarn-error.log', 'yarn-debug.log', ]; const isErrorLog = file => { return errorLogFilePatterns.some(pattern => file.startsWith(pattern)); }; const conflicts = fs .readdirSync(root) .filter(file => !validFiles.includes(file)) // IntelliJ IDEA creates module files before CRA is launched .filter(file => !/\.iml$/.test(file)) // Don't treat log files from previous installation as conflicts .filter(file => !isErrorLog(file)); if (conflicts.length > 0) { console.log( `The directory ${chalk.green(name)} contains files that could conflict:` ); console.log(); for (const file of conflicts) { try { const stats = fs.lstatSync(path.join(root, file)); if (stats.isDirectory()) { console.log(` ${chalk.blue(`${file}/`)}`); } else { console.log(` ${file}`); } } catch (e) { console.log(` ${file}`); } } console.log(); console.log( 'Either try using a new directory name, or remove the files listed above.' ); return false; } // Remove any log files from a previous installation. fs.readdirSync(root).forEach(file => { if (isErrorLog(file)) { fs.removeSync(path.join(root, file)); } }); return true; }
安全性校驗,檢查當前目錄下是否存在已有文件,判斷建立的這個目錄是否包含除了上述validFiles
裏面的文件
function shouldUseYarn() { try { execSync('yarnpkg --version', { stdio: 'ignore' }); return true; } catch (e) { return false; } }
execSync
是由node
自身模塊child_process
引用而來,用來執行命令的,這個函數執行yarnpkg --version
來判斷咱們是否正確安裝了yarn
,若是沒有正確安裝yarn
的話,useYarn
依然爲false
,無論指沒有指定--use-npm
。
execSync
:引用自child_process.execSync
,用於執行須要執行的子進程// See https://github.com/facebook/create-react-app/pull/3355 function checkThatNpmCanReadCwd() { const cwd = process.cwd();// 當前的進程目錄 let childOutput = null; // 保存npm信息 try { // Note: intentionally using spawn over exec since // the problem doesn't reproduce otherwise. // `npm config list` is the only reliable way I could find // to reproduce the wrong path. Just printing process.cwd() // in a Node process was not enough. childOutput = spawn.sync('npm', ['config', 'list']).output.join(''); // 至關於執行`npm config list`並將其輸出的信息組合成爲一個字符串 } catch (err) { // Something went wrong spawning node. // Not great, but it means we can't do this check. // We might fail later on, but let's continue. return true; } if (typeof childOutput !== 'string') { return true; } //字符串換行分割 const lines = childOutput.split('\n'); // `npm config list` output includes the following line: // "; cwd = C:\path\to\current\dir" (unquoted) // I couldn't find an easier way to get it. const prefix = '; cwd = ';// 定義須要的信息前綴 const line = lines.find(line => line.startsWith(prefix));// 取整個lines裏面的每一個line查找有沒有這個前綴的一行 if (typeof line !== 'string') { // Fail gracefully. They could remove it. return true; } const npmCWD = line.substring(prefix.length); if (npmCWD === cwd) {// 判斷當前目錄和執行目錄是否一致 return true; } console.error( chalk.red( // 不一致就打印如下信息,大概意思就是`npm`進程沒有在正確的目錄下執行 `Could not start an npm process in the right directory.\n\n` + `The current directory is: ${chalk.bold(cwd)}\n` + `However, a newly started npm process runs in: ${chalk.bold( npmCWD )}\n\n` + `This is probably caused by a misconfigured system terminal shell.` ) ); if (process.platform === 'win32') {// 對window的狀況做了單獨判斷 console.error( chalk.red(`On Windows, this can usually be fixed by running:\n\n`) + ` ${chalk.cyan( 'reg' )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` + ` ${chalk.cyan( 'reg' )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` + chalk.red(`Try to run the above two lines in the terminal.\n`) + chalk.red( `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/` ) ); } return false; }
上述代碼表示已經解析了,其中用到了一個外部依賴:
cross-spawn
:用來執行node進程,Node跨平臺解決方案,解決在windows下各類問題npm地址
function checkNpmVersion() { let hasMinNpm = false; let npmVersion = null; try { npmVersion = execSync('npm --version') .toString() .trim(); hasMinNpm = semver.gte(npmVersion, '5.0.0'); } catch (err) { // ignore } return { hasMinNpm: hasMinNpm, npmVersion: npmVersion, }; }
檢測node
版本,react-scrpts
是須要依賴Node
版本的,低版本的Node
不支持,版本比較使用的是一個semver package.
run 主要作的事情就是安裝依賴、拷貝模板。run()
函數在createApp()
函數的全部內容執行完畢後執行,它接收7個參數。
root
:咱們建立的目錄的絕對路徑appName
:咱們建立的目錄名稱version
;react-scripts
的版本verbose
:繼續傳入verbose
,在createApp
中沒有使用到originalDirectory
:原始目錄,這個以前說到了,到run
函數中就有用了tempalte
:模板,這個參數以前也說過了,不對外使用useYarn
:是否使用yarn
usePnp
: 是否使用pnp
function run( root, appName, version, verbose, originalDirectory, template, useYarn, usePnp ) { Promise.all([ getInstallPackage(version, originalDirectory), //獲取依賴安裝包信息 getTemplateInstallPackage(template, originalDirectory),// 獲取模板安裝包信息 ]).then(([packageToInstall, templateToInstall]) => { const allDependencies = ['react', 'react-dom', packageToInstall]; console.log('Installing packages. This might take a couple of minutes.'); Promise.all([ // 從網址或路徑中提取軟件包名稱,也就是獲取安裝包名稱這裏以前用的getPackageName() getPackageInfo(packageToInstall), getPackageInfo(templateToInstall), ]) .then(([packageInfo, templateInfo]) => checkIfOnline(useYarn).then(isOnline => ({ isOnline, packageInfo, templateInfo, })) ) .then(({ isOnline, packageInfo, templateInfo }) => { let packageVersion = semver.coerce(packageInfo.version); const templatesVersionMinimum = '3.3.0'; // Assume compatibility if we can't test the version. if (!semver.valid(packageVersion)) { packageVersion = templatesVersionMinimum; } // Only support templates when used alongside new react-scripts versions. const supportsTemplates = semver.gte( packageVersion, templatesVersionMinimum ); if (supportsTemplates) { allDependencies.push(templateToInstall); } else if (template) { console.log(''); console.log( `The ${chalk.cyan(packageInfo.name)} version you're using ${ packageInfo.name === 'react-scripts' ? 'is not' : 'may not be' } compatible with the ${chalk.cyan('--template')} option.` ); console.log(''); } // TODO: Remove with next major release. if (!supportsTemplates && (template || '').includes('typescript')) { allDependencies.push( '@types/node', '@types/react', '@types/react-dom', '@types/jest', 'typescript' ); } console.log( `Installing ${chalk.cyan('react')}, ${chalk.cyan( 'react-dom' )}, and ${chalk.cyan(packageInfo.name)}${ supportsTemplates ? ` with ${chalk.cyan(templateInfo.name)}` : '' }...` ); console.log(); // 安裝依賴 return install( root, useYarn, usePnp, allDependencies, verbose, isOnline ).then(() => ({ packageInfo, supportsTemplates, templateInfo, })); }) .then(async ({ packageInfo, supportsTemplates, templateInfo }) => { const packageName = packageInfo.name; const templateName = supportsTemplates ? templateInfo.name : undefined; checkNodeVersion(packageName);// 判斷node_modules下面全部包中對於node版本的最低要求 setCaretRangeForRuntimeDeps(packageName);// 給package.json中的依賴添加^ const pnpPath = path.resolve(process.cwd(), '.pnp.js'); const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : []; // 安裝依賴同時copy react-scripts下面的template到當前目錄 await executeNodeScript( { cwd: process.cwd(), args: nodeArgs, }, [root, appName, verbose, originalDirectory, templateName], ` var init = require('${packageName}/scripts/init.js'); init.apply(null, JSON.parse(process.argv[1])); ` ); if (version === 'react-scripts@0.9.x') { console.log( chalk.yellow( `\nNote: the project was bootstrapped with an old unsupported version of tools.\n` + `Please update to Node >=8.10 and npm >=5 to get supported tools in new projects.\n` ) ); } }) .catch(reason => { console.log(); console.log('Aborting installation.'); if (reason.command) { console.log(` ${chalk.cyan(reason.command)} has failed.`); } else { console.log( chalk.red('Unexpected error. Please report it as a bug:') ); console.log(reason); } console.log(); // On 'exit' we will delete these files from target directory. // 在「退出」時,咱們將從目標目錄中刪除這些文件 const knownGeneratedFiles = [ 'package.json', 'yarn.lock', 'node_modules', ]; const currentFiles = fs.readdirSync(path.join(root)); currentFiles.forEach(file => { knownGeneratedFiles.forEach(fileToMatch => { // This removes all knownGeneratedFiles. if (file === fileToMatch) { console.log(`Deleting generated file... ${chalk.cyan(file)}`); fs.removeSync(path.join(root, file)); } }); }); const remainingFiles = fs.readdirSync(path.join(root)); if (!remainingFiles.length) {//判斷當前目錄下是否還存在文件 // Delete target folder if empty console.log( `Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan( path.resolve(root, '..') )}` ); process.chdir(path.resolve(root, '..')); fs.removeSync(path.join(root)); } console.log('Done.'); process.exit(1); }); }); }
run在這裏對react-script
作了不少處理,大概是因爲react-script
自己是有node
版本的依賴的,並且在用create-react-app init <project>
初始化一個項目的時候,是能夠指定react-script
的版本;簡單來講run 主要作的事情就是安裝依賴、拷貝模板。
其中函數列表:
getInstallPackage()
:獲取要安裝的react-scripts
版本或者開發者本身定義的react-scripts
。getTemplateInstallPackage
: 獲取模板安裝包信息,根據選擇的模板建立模板安裝包名getPackageInfo()
: 從網址或路徑中提取軟件包名稱,也就是獲取安裝包名稱這裏以前用的getPackageName()checkIfOnline()
:檢查網絡鏈接是否正常install()
:安裝開發依賴包,checkNodeVersion()
:檢查Node
版本信息,判斷node_modules下面全部包中對於node版本的最低要求setCaretRangeForRuntimeDeps()
:檢查發開依賴是否正確安裝,版本是否正確executeNodeScript()
: 安裝依賴同時copy react-scripts下面的template到當前目錄,在create-react-app
以前的版本中,這裏是經過調用react-script
下的init
方法來執行後續動做的。這裏經過調用executeNodeScript
方法function getInstallPackage(version, originalDirectory) { let packageToInstall = 'react-scripts'; const validSemver = semver.valid(version); if (validSemver) { packageToInstall += `@${validSemver}`; } else if (version) { if (version[0] === '@' && !version.includes('/')) { packageToInstall += version; } else if (version.match(/^file:/)) { packageToInstall = `file:${path.resolve( originalDirectory, version.match(/^file:(.*)?$/)[1] )}`; } else { // for tar.gz or alternative paths packageToInstall = version; } } const scriptsToWarn = [ { name: 'react-scripts-ts', message: chalk.yellow( `The react-scripts-ts package is deprecated. TypeScript is now supported natively in Create React App. You can use the ${chalk.green( '--template typescript' )} option instead when generating your app to include TypeScript support. Would you like to continue using react-scripts-ts?` ), }, ]; for (const script of scriptsToWarn) { if (packageToInstall.startsWith(script.name)) { return inquirer .prompt({ type: 'confirm', name: 'useScript', message: script.message, default: false, }) .then(answer => { if (!answer.useScript) { process.exit(0); } return packageToInstall; }); } } return Promise.resolve(packageToInstall); }
getInstallPackage
根據傳入的 version 和原始路徑 originalDirectory 去獲取要安裝的 package 列表,默認狀況下version 爲 undefined,獲取到的 packageToInstall 爲react-scripts
,也就是咱們如上圖的 resolve 回調
function getTemplateInstallPackage(template, originalDirectory) { let templateToInstall = 'cra-template'; if (template) { if (template.match(/^file:/)) { templateToInstall = `file:${path.resolve( originalDirectory, template.match(/^file:(.*)?$/)[1] )}`; } else if ( template.includes('://') || template.match(/^.+\.(tgz|tar\.gz)$/) ) { // for tar.gz or alternative paths templateToInstall = template; } else { // Add prefix 'cra-template-' to non-prefixed templates, leaving any // @scope/ intact. const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/); const scope = packageMatch[1] || ''; const templateName = packageMatch[2]; const name = templateName.startsWith(templateToInstall) ? templateName : `${templateToInstall}-${templateName}`; templateToInstall = `${scope}${name}`; } } return Promise.resolve(templateToInstall); }
getTemplateInstallPackage
建立一個cra-template
模板前綴,把傳入的模板名稱合併返回一個新的模板名稱,
// Extract package name from tarball url or path. // 從網址或路徑中提取軟件包名稱 function getPackageInfo(installPackage) { if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {// 判斷`react-scripts`的信息來安裝這個包,用於返回正規的包名 return getTemporaryDirectory() //建立一個臨時目錄 .then(obj => { let stream; if (/^http/.test(installPackage)) { stream = hyperquest(installPackage); } else { stream = fs.createReadStream(installPackage); } return extractStream(stream, obj.tmpdir).then(() => obj); }) .then(obj => { const { name, version } = require(path.join( obj.tmpdir, 'package.json' )); obj.cleanup(); return { name, version }; }) .catch(err => { // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz // However, this function returns package name only without semver version. console.log( `Could not extract the package name from the archive: ${err.message}` ); const assumedProjectName = installPackage.match( /^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/ )[1]; console.log( `Based on the filename, assuming it is "${chalk.cyan( assumedProjectName )}"` ); return Promise.resolve({ name: assumedProjectName }); }); } else if (installPackage.startsWith('git+')) { // 此處爲信息中包含`git+`信息的狀況 // Pull package name out of git urls e.g: // git+https://github.com/mycompany/react-scripts.git // git+ssh://github.com/mycompany/react-scripts.git#v1.2.3 return Promise.resolve({ name: installPackage.match(/([^/]+)\.git(#.*)?$/)[1], }); } else if (installPackage.match(/.+@/)) { // 此處爲只有版本信息的時候的狀況 // Do not match @scope/ when stripping off @version or @tag return Promise.resolve({ name: installPackage.charAt(0) + installPackage.substr(1).split('@')[0], version: installPackage.split('@')[1], }); } else if (installPackage.match(/^file:/)) { // 此處爲信息中包含`file:`開頭的狀況 const installPackagePath = installPackage.match(/^file:(.*)?$/)[1]; const { name, version } = require(path.join( installPackagePath, 'package.json' )); return Promise.resolve({ name, version }); } return Promise.resolve({ name: installPackage }); }
這個函數的做用就是返回正常的包名,不帶任何符號的,這裏使用了一個外部依賴hyperquest
,看了下之前版本的源碼,這個函數以前是getPackageName()
同一個方法
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) { return new Promise((resolve, reject) => { // 封裝在一個回調函數中 let command;// 定義一個命令 let args;// 定義一個命令的參數 if (useYarn) { // 若是使用yarn command = 'yarnpkg'; args = ['add', '--exact']; if (!isOnline) { args.push('--offline'); // 是否離線模式 } if (usePnp) { args.push('--enable-pnp'); } [].push.apply(args, dependencies); // 組合參數和開發依賴 `react` `react-dom` `react-scripts` // Explicitly set cwd() to work around issues like // https://github.com/facebook/create-react-app/issues/3326. // Unfortunately we can only do this for Yarn because npm support for // equivalent --prefix flag doesn't help with this issue. // This is why for npm, we run checkThatNpmCanReadCwd() early instead. args.push('--cwd');// 指定命令執行目錄的地址 args.push(root);// 地址的絕對路徑 if (!isOnline) { // 離線模式時候會發出警告 console.log(chalk.yellow('You appear to be offline.')); console.log(chalk.yellow('Falling back to the local Yarn cache.')); console.log(); } } else { //使用npm 的狀況 command = 'npm'; args = [ 'install', '--save', '--save-exact', '--loglevel', 'error', ].concat(dependencies); if (usePnp) { // 若是是pnp發出警告npm 不支持pnp console.log(chalk.yellow("NPM doesn't support PnP.")); console.log(chalk.yellow('Falling back to the regular installs.')); console.log(); } } if (verbose) { args.push('--verbose'); } // 在spawn執行完命令後會有一個回調,判斷code是否爲 0,而後 resolve Promise, const child = spawn(command, args, { stdio: 'inherit' }); child.on('close', code => { if (code !== 0) { reject({ command: `${command} ${args.join(' ')}`, }); return; } resolve(); }); }); }
install
就是把梳理好的package
交給npm
或者yarn
安裝依賴
function executeNodeScript({ cwd, args }, data, source) { return new Promise((resolve, reject) => { const child = spawn( process.execPath, [...args, '-e', source, '--', JSON.stringify(data)], { cwd, stdio: 'inherit' } ); child.on('close', code => { if (code !== 0) { reject({ command: `node ${args.join(' ')}`, }); return; } resolve(); }); }); }
executeNodeScript
方法主要是經過 spawn 來經過 node命令執行react-script
下的 init 方法
module.exports = function( appPath, appName, verbose, originalDirectory, templateName ) { const appPackage = require(path.join(appPath, 'package.json')); //項目目錄下的 package.json const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //經過判斷目錄下是否有 yarn.lock 來判斷是否使用 yarn if (!templateName) { // 判斷是否有模板名稱 console.log(''); console.error( `A template was not provided. This is likely because you're using an outdated version of ${chalk.cyan( 'create-react-app' )}.` ); console.error( `Please note that global installs of ${chalk.cyan( 'create-react-app' )} are no longer supported.` ); return; } const templatePath = path.join( //獲取模板的路徑 require.resolve(templateName, { paths: [appPath] }), '..' ); let templateJsonPath; if (templateName) { // templateJsonPath = path.join(templatePath, 'template.json'); } else { // TODO: Remove support for this in v4. templateJsonPath = path.join(appPath, '.template.dependencies.json'); } let templateJson = {}; if (fs.existsSync(templateJsonPath)) { //判斷路徑是否正確 templateJson = require(templateJsonPath); } // Copy over some of the devDependencies appPackage.dependencies = appPackage.dependencies || {}; // Setup the script rules 定義scripts const templateScripts = templateJson.scripts || {}; appPackage.scripts = Object.assign( { start: 'react-scripts start', build: 'react-scripts build', test: 'react-scripts test', eject: 'react-scripts eject', }, templateScripts ); // Update scripts for Yarn users if (useYarn) {// 判斷是否使用yarn,若是是替換成你怕嗎 npm appPackage.scripts = Object.entries(appPackage.scripts).reduce( (acc, [key, value]) => ({ ...acc, [key]: value.replace(/(npm run |npm )/, 'yarn '), }), {} ); } // Setup the eslint config 設置eslint配置 appPackage.eslintConfig = { extends: 'react-app', }; // Setup the browsers list 這是瀏覽器 openBrowser appPackage.browserslist = defaultBrowsers; fs.writeFileSync( //寫入咱們須要建立的目錄下的 package.json 中 path.join(appPath, 'package.json'), JSON.stringify(appPackage, null, 2) + os.EOL ); // 判斷項目目錄是否有`README.md`,模板目錄中已經定義了`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') ); } // Copy the files for the user const templateDir = path.join(templatePath, 'template'); if (fs.existsSync(templateDir)) { fs.copySync(templateDir, appPath); } else { console.error( `Could not locate supplied template: ${chalk.green(templateDir)}` ); return; } // modifies README.md commands based on user used package manager. if (useYarn) {// 判斷是否使用yarn,若是是替換成你怕嗎 npm 默認使用npm try { const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8'); fs.writeFileSync( path.join(appPath, 'README.md'), readme.replace(/(npm run |npm )/g, 'yarn '), 'utf8' ); } catch (err) { // Silencing the error. As it fall backs to using default npm commands. } } const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore')); if (gitignoreExists) { // Append if there's already a `.gitignore` file there const data = fs.readFileSync(path.join(appPath, 'gitignore')); fs.appendFileSync(path.join(appPath, '.gitignore'), data); fs.unlinkSync(path.join(appPath, 'gitignore')); } else { // Rename gitignore after the fact to prevent npm from renaming it to .npmignore // See: https://github.com/npm/npm/issues/1862 fs.moveSync( path.join(appPath, 'gitignore'), path.join(appPath, '.gitignore'), [] ); } let command; let remove; let args; if (useYarn) { command = 'yarnpkg'; remove = 'remove'; args = ['add']; } else { command = 'npm'; remove = 'uninstall'; args = ['install', '--save', verbose && '--verbose'].filter(e => e); } // Install additional template dependencies, if present 安裝其餘模板依賴項若是有 const templateDependencies = templateJson.dependencies; if (templateDependencies) { args = args.concat( Object.keys(templateDependencies).map(key => { return `${key}@${templateDependencies[key]}`; }) ); } // Install react and react-dom for backward compatibility with old CRA cli // which doesn't install react and react-dom along with react-scripts if (!isReactInstalled(appPackage)) { args = args.concat(['react', 'react-dom']); } // 安裝react和react-dom以便與舊CRA cli向後兼容 // 沒有安裝react和react-dom以及react-scripts // 或模板是presetend(經過--internal-testing-template) // Install template dependencies, and react and react-dom if missing. if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) { console.log(); console.log(`Installing template dependencies using ${command}...`); const proc = spawn.sync(command, args, { stdio: 'inherit' }); if (proc.status !== 0) { console.error(`\`${command} ${args.join(' ')}\` failed`); return; } } if (args.find(arg => arg.includes('typescript'))) { console.log(); verifyTypeScriptSetup(); } // Remove template console.log(`Removing template package using ${command}...`); console.log(); const proc = spawn.sync(command, [remove, templateName], { stdio: 'inherit', }); if (proc.status !== 0) { console.error(`\`${command} ${args.join(' ')}\` failed`); return; } if (tryGitInit(appPath)) { console.log(); console.log('Initialized a git repository.'); } // Display the most elegant way to cd. // This needs to handle an undefined originalDirectory for // backward compatibility with old global-cli's. // 顯示最優雅的CD方式。 // 這須要處理一個未定義的originalDirectory // 與舊的global-cli的向後兼容性。 let cdpath; if (originalDirectory && path.join(originalDirectory, appName) === appPath) { cdpath = appName; } else { cdpath = appPath; } // Change displayed command to yarn instead of yarnpkg // 將顯示的命令更改成yarn而不是yarnpkg const displayedCommand = useYarn ? 'yarn' : 'npm'; console.log(); console.log(`Success! Created ${appName} at ${appPath}`); console.log('Inside that directory, you can run several commands:'); console.log(); console.log(chalk.cyan(` ${displayedCommand} start`)); console.log(' Starts the development server.'); console.log(); console.log( chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`) ); console.log(' Bundles the app into static files for production.'); console.log(); console.log(chalk.cyan(` ${displayedCommand} test`)); console.log(' Starts the test runner.'); console.log(); console.log( chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`) ); console.log( ' Removes this tool and copies build dependencies, configuration files' ); console.log( ' and scripts into the app directory. If you do this, you can’t go back!' ); console.log(); console.log('We suggest that you begin by typing:'); console.log(); console.log(chalk.cyan(' cd'), cdpath); console.log(` ${chalk.cyan(`${displayedCommand} start`)}`); if (readmeExists) { console.log(); console.log( chalk.yellow( 'You had a `README.md` file, we renamed it to `README.old.md`' ) ); } console.log(); console.log('Happy hacking!'); }
初始化方法主要作的事情就是修改目標路徑下的 package.json,添加一些配置命令,而後 copy!react-script 下的模板到目標路徑下
項目初始化完成
簡單總結下
cross-spawn
來用命令行執行全部的安裝看完大佬的分享,在本身跑一遍果真理順不少,感謝大佬的分享,寫出來也是記錄自我一個學習的過程,方便後期查看