主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green
奉献主题:https://github.com/xitu/jueji…
theme: juejin
highlight:
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 它
- 看源码细节
集体原创技术文章会同步更新在公众号 玩相机的程序员 上,欢送大家关注。我是 axuebin,用键盘和相机记录生存。