乐趣区

关于前端:一篇文章搞定-create-react-app-核心思路


主题列表: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 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 上最新的版本。

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

留神这里是先 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. 看源码细节

集体原创技术文章会同步更新在公众号 玩相机的程序员 上,欢送大家关注。我是 axuebin,用键盘和相机记录生存。

退出移动版