create-react-app(v3.7.2)
能夠很快很方便初始化一個
react
開發項目,這個東西究竟是怎樣運做的,作了哪些處理呢?今天揭開內部祕密。源碼用到的一些有用的第三方庫也列了出來,方便之後你們在本身的
cli
中使用。
// CRA的全部的命令以下
/** npm package commander: 命令基礎工具 */
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {
projectName = name;
})
.option('--verbose', 'print additional logs')
.option('--info', 'print environment debug info')
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)
.option(
'--template <path-to-template>',
'specify a template for the created project'
)
.option('--use-npm')
.option('--use-pnp')
// TODO: Remove this in next major release.
.option(
'--typescript',
'(this option will be removed in favour of templates in the next major release of create-react-app)'
)
.allowUnknownOption()
.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 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);
複製代碼
-V, --version
版本號輸出node
// 當前create-react-app版本號輸出
new commander.Command(packageJson.name)
.version(packageJson.version) // 默認已經生成該命令選項
複製代碼
--verbose
展現詳細的logsreact
--info
展現當前系統以及環境的一些信息git
// 源碼中,若是命令中有這個參數, 則會執行
/** npm package: envinfo: 快速獲取當前各類軟件環境的信息 */
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);
複製代碼
--scripts-version
指定一個特定的react-scripts
運行腳本github
--template
指定項目的模板,能夠指定一個本身的模板typescript
--use-pnp
使用pnp --> pnp是什麼npm
--typescript
使用ts開發,以後版本會移除這個選項json
這個選項即將被棄用,可使用--template typescript
代替app
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';
}
}
複製代碼
建立項目會調用createApp
方法, node版本要求>=8.10.0
, 低於這個版本會拋錯less
createApp
dom
首先會先調用createApp
方法
createApp(
projectName, // 項目名稱
program.verbose, // --verbose
program.scriptsVersion, // --scripts-version
program.template, // --template
program.useNpm, // --use-npm
program.usePnp, // --use-pnp
program.typescript // --typescript
);
複製代碼
建立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
);
複製代碼
若是有使用yarn
, 會先將當前目錄下的yarn.lock.cached
文件拷貝到項目根目錄下並重命名爲yarn.lock
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
接着調用run
,繼續建立新項目
/** npm 包 semver: 版本號校驗以及比較等的工具庫 */
run(
root,
appName,
version, // scriptsVersion
verbose,
originalDirectory,
template,
useYarn,
usePnp
);
複製代碼
處理react-scripts
引用腳本和--template
入參
// ...
let packageToInstall = 'react-scripts';
// ...
// 將所用到的依賴蒐集
const allDependencies = ['react', 'react-dom', packageToInstall];
Promise.all([
getInstallPackage(version, originalDirectory),
getTemplateInstallPackage(template, originalDirectory),
])
複製代碼
調用getInstallPackage
處理react-scripts
的使用 --scripts-version
選項的入參能夠爲多種:
react-scripts
的版本 -> react-scripts@x.x.x
typescript
模板,若指定scriptsVersion
爲react-scripts-ts
,會有確認提示/** npm package inquirer: 輸入輸出交互處理工具 */
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;
});
}
}
複製代碼
調用getTemplateInstallPackage
處理--template
的使用
://
或者tgz|tar.gz
壓縮包@xxx/xxx/xxxx
或者@xxxx
的指定路徑或者模板名字const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/);
const scope = packageMatch[1] || '';
const templateName = packageMatch[2];
if (
templateName === templateToInstall ||
templateName.startsWith(`${templateToInstall}-`)
) {
// Covers:
// - cra-template
// - @SCOPE/cra-template
// - cra-template-NAME
// - @SCOPE/cra-template-NAME
templateToInstall = `${scope}${templateName}`;
} else if (templateName.startsWith('@')) {
// Covers using @SCOPE only
templateToInstall = `${templateName}/${templateToInstall}`;
} else {
// Covers templates without the `cra-template` prefix:
// - NAME
// - @SCOPE/NAME
templateToInstall = `${scope}${templateToInstall}-${templateName}`;
}
// cra-template: This is the official base template for Create React App.
複製代碼
最終處理成@xxx/cra-template
或者@xxx/cra-template-xxx
、cra-template-xxx
、cra-template
, 官方指定的兩個模板爲cra-template-typescript
、cra-template
。模板具體內容你們能夠去 官方倉庫 去查看,能夠本身自定義或者魔改一些東西
接着獲取--scripts-version
和--template
處理後的安裝包信息
/** npm package tem: 用於在node.js環境中建立臨時文件和目錄。 hyperquest: 將http請求轉化爲流(stream)輸出 tar-pack: tar/gz的壓縮或者解壓縮 */
Promise.all([
getPackageInfo(packageToInstall),
getPackageInfo(templateToInstall),
])
// getPackageInfo是一個頗有用的工具函數
// Extract package name from tarball url or path.
function getPackageInfo(installPackage) {
if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
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+')) {
// 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:/)) {
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 });
}
function extractStream(stream, dest) {
return new Promise((resolve, reject) => {
stream.pipe(
unpack(dest, err => {
if (err) {
reject(err);
} else {
resolve(dest);
}
})
);
});
}
複製代碼
run
方法主要的工做就是處理--scripts-version
和--template
提供的包,蒐集項目的依賴
install
run
處理收集完依賴後會調用install
方法
return install(
root, // 項目的名稱
useYarn,
usePnp,
allDependencies,
verbose,
isOnline // 若使用yarn,dns.lookup檢測registry.yarnpkg.com是否正常的結果
)
// install主要是處理安裝前的一些命令參數處理以及上面蒐集依賴的安裝
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
return new Promise((resolve, reject) => {
let command;
let args;
if (useYarn) {
command = 'yarnpkg';
args = ['add', '--exact'];
if (!isOnline) {
args.push('--offline');
}
if (usePnp) {
args.push('--enable-pnp');
}
[].push.apply(args, dependencies);
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 {
command = 'npm';
args = [
'install',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
if (usePnp) {
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');
}
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
複製代碼
依賴安裝後檢查react-scripts
執行包的版本與當前的node版本是否匹配,檢查react
、react-dom
是否正確安裝,並在它們的版本號前面加^
(上面安裝命令帶有exact
選項,會精確安裝依賴,版本號不帶^
),將依賴從新寫入package.json
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])); `
);
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();
});
});
}
複製代碼
正確檢查依賴後將執行提供的scripts腳本下的init
初始化:
package.json
添加scripts
/eslintConfig
/browserslist
等配置README.md
,將其重命名README.old.md
yarn
,將模板README.md
的命令說明給爲yarn
ts
項目則初始化相關配置(verifyTypeScriptSetup
)node_modules
的模板git
相關到此整個項目建立完畢
package.json
package.json
的react/react-dom
依賴版本號,並校驗node
版本是否符合要求react-scripts
依賴,並經過子進程調用依賴下的react-scripts/scripts/init.js
,進行項目模板初始化ts
則初始化其配置git
create-react-app
這個包的源碼相對簡單,可是很是細密精煉,整個流程很是清晰,絕對是一個cli
的範本,感興趣的小夥伴能夠本身閱讀。文正若是有不正確的地方歡迎指正批評!