原文链接: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 appReact 官方创建单页应用的方式,为了方便,下文皆简称 CRA

它的核心思想我理解主要是:

  1. 脚手架核心功能中心化:使用 npx 保证每次用户使用的都是最新版本,方便功能的升级
  2. 模板去中心化:方便地进行模板管理,这样也允许用户自定义模板
  3. 脚手架逻辑和初始化代码逻辑分离:在 cra 中只执行了脚手架相关逻辑,而初始化代码的逻辑在 react-scripts 包里执行

本文主要就是通过源码分析对上述的理解进行阐述。

按照自己的理解,画了个流程图,大家可以带着该流程图去阅读源码(主要包含两个部分 create-react-appreact-scripts/init):

如果图片不清晰可以微信搜索公众号 玩相机的程序员,回复 CRA 获取。

0. 用法

CRA 的用法很简单,两步:

  1. 安装:npm install -g create-react-app
  2. 使用:create-react-app my-app

这是常见的用法,会在全局环境下安装一个 CRA,在命令行中可以通过 create react app 直接使用。

现在更推荐的用法是使用 npx 来执行 create react app

npx create-react-app my-app

这样确保每次执行 create-reat-app 使用的都是 npm 上最新的版本。

注:npxnpm 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();
}

注意这里是先 catchthen

跟着我往下看呗 ~ 一步一步理清楚 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(),注意这里是先 catchthen,也就是说如果 checkForLatestVersion 里抛错误了,会被 catch 住,然后执行一些逻辑,再执行 then

是的,Promisecatch 后面的 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 失败了
});

原理也很简单,thencatch 返回的都是一个 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 参数,则使用对用的模板,该方法主要是给额外的 templatescopeprefix,比如 @scope/cra-template-${template},具体逻辑不展开。

这里 CRA  的核心思想是通过 npm 来对模板进行管理,这样方便扩展和管理。

7. 安装依赖

CRA 会自动给项目安装 reactreact-domreact-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

老规矩,只分析主流程代码,主流程主要就做了四件事:

  1. 处理 template 里的 packages.json
  2. 处理 package.jsonscripts:默认值和 template 合并
  3. 写入 package.json
  4. 拷贝 template 文件

除此之外还有一些 gitnpm 相关的操作,这里就不展开了。

// 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 的命令,比如 startbuild,后续会单独有文章进行讲解。

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 的思想可能有了个大致了解:

  1. CRA  是一个通用的 React  脚手架,它支持自定义模板的初始化。将模板代码托管在 npm  上,而不是传统的通过 git  来托管模板代码,这样方便扩展和管理
  2. CRA  只负责核心依赖、模板的安装和脚手架的核心功能,具体初始化代码的工作交给 react-scripts  这个包

但是具体细节上它是如何做的这个我没有详细的阐述,如果感兴趣的同学可以自行下载其源码阅读。推荐阅读源码流程:

  1. 看它的单测
  2. 一步一步 debug 它
  3. 看源码细节

create-react-app 核心思路分析的更多相关文章

  1. 深入 Create React App 核心概念

    本文差点难产而死.因为总结的过程中,多次怀疑本文是对官方文档的直接翻译和简单诺列:同时官方文档很全面,全范围的介绍无疑加深了写作的心智负担.但在最终的梳理中,发现走出了一条与众不同的路,于是坚持分享出 ...

  2. 如何扩展 Create React App 的 Webpack 配置

    如何扩展 Create React App 的 Webpack 配置  原文地址https://zhaozhiming.github.io/blog/2018/01/08/create-react-a ...

  3. tap news:week5 0.0 create react app

    参考https://blog.csdn.net/qtfying/article/details/78665664 先创建文件夹 安装create react app 这个脚手架(facebook官方提 ...

  4. 使用create react app教程

    This project was bootstrapped with Create React App. Below you will find some information on how to ...

  5. 在 .NET Core 5 中集成 Create React app

    翻译自 Camilo Reyes 2021年2月22日的文章 <Integrate Create React app with .NET Core 5> [1] Camilo Reyes ...

  6. Create React App

    Facebook开源了React前端框架(MIT Licence),也同时提供了React脚手架 - create-react-app. create-react-app遵循约定优于配置(Coc)的原 ...

  7. Create React App 安装less 报错

    执行npm run eject 暴露模块 安装 npm i  less less-loader -D 1.打开 react app 的 webpack.config.js const sassRege ...

  8. [React] Use the Fragment Short Syntax in Create React App 2.0

    create-react-app version 2.0 added a lot of new features. One of the new features is upgrading to Ba ...

  9. [React] {svg, css module, sass} support in Create React App 2.0

    create-react-app version 2.0 added a lot of new features. One of the new features is added the svgr  ...

随机推荐

  1. UDP编程详解

    目录 报文格式 通信过程 UDP客户端流程 UDP客户端编码 UDP服务器流程 UDP服务器编码 参考文献 UDP与TCP的不同之处是:他的通信不需要建立连接的过程.中文名称用户数据报协议.时OSI参 ...

  2. pyhton的函数

    目录 一.函数引入 二.函数的定义 三.如何定义一个函数 四.定义函数的三种形式 1.空函数 2.有参函数 3.无参函数 五.函数的调用 六.函数的返回值 七.函数的参数 1.形参 1.1 位置形参 ...

  3. 在 Svelte 中使用 CSS-in-JS

    你即便不需要,但你可以. 注意:原文发表于2018-12-26,随着框架不断演进,部分内容可能已不适用. CSS 是任何 Web 应用程序的核心部分. 宽泛而论,如果一个 UI 框架没有内置向组件添加 ...

  4. 卧槽,好强大的魔法,竟能让Python支持方法重载

    1. 你真的了解方法重载吗? 方法重载是面向对象中一个非常重要的概念,在类中包含了成员方法和构造方法.如果类中存在多个同名,且参数(个数和类型)不同的成员方法或构造方法,那么这些成员方法或构造方法就被 ...

  5. 身份认证:JSON Web Token

    JSON Web Token(JWT)是一种基于JSON的开放标准((RFC 7519),也是目前最流行的跨域认证解决方案. 传统的 cookie 认证方式看起来遵守了 REST 架构的无状态要求,但 ...

  6. ng-class动态类几种用法

    方法1.逻辑在后面的中括号里面 ng-class="{true : 'checker disabled',false : 'checker' }[selectAllButton]" ...

  7. 单细胞分析实录(9): 展示marker基因的4种图形(二)

    在上一篇中,我已经讲解了展示marker基因的前两种图形,分别是tsne/umap图.热图,感兴趣的读者可以回顾一下.这一节我们继续学习堆叠小提琴图和气泡图. 3. 堆叠小提琴图展示marker基因 ...

  8. docker的安装和基本的docker命令、镜像和容器的操作

    1.yum 包更新到最新 yum update 2.安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的 yum insta ...

  9. 基于Hi3559AV100 RFCN实现细节解析-(3)系统输入VI分析(HiISP)二 :

    下面随笔系列将对Hi3559AV100 RFCN实现细节进行解析,整个过程涉及到VI.VDEC.VPSS.VGS.VO.NNIE,其中涉及的内容,大家可以参考之前我写的博客: 基于Hi3559AV10 ...

  10. go中errgroup源码解读

    errgroup 前言 如何使用 实现原理 WithContext Go Wait 错误的使用 总结 errgroup 前言 来看下errgroup的实现 如何使用 func main() { var ...