[源码解析] create-react-app start.js
当你习惯于使用 create-react-app 快速构建一个 React App 项目的时候,是否有想过 create-react-app 底层是用了什么样的魔法能让 创建、运行、热部署 一个 React App 变得如此简单?
本文将带领读者一起解析 create-react-app 的源码,不仅如此,我还会指出一些值 得借鉴的有趣、实用的技术点/代码写法 ,让你从解读 create-react-app 的源码 收获更多 !
文章篇幅原因,今天就只解读 start.js 和部分相关的文件 —— start.js 就是当你在 使用 create-react-app 创建的React app 下运行 npm run start 时调用的脚本.
阅读提示:
- 建议同时打开 create-react-app 源码 ( github链接 ),对照着阅读本文。
- 由于代码较多,手机阅读体验较差,建议 先点赞、收藏 ,然后使用电脑阅读。
开始解析 start.js
start.js 的 第二、三行是 ( 源码链接 )
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
给 BABEL_ENV 和 NODE_ENV 环境变量都赋值为 development
解读:许多NodeJS工具库会根据 环境变量 XXX_ENV 决定 采取不同策略 。例如:如果是 development 就打印出更多更详细的日志信息,如果是 production 就尽量减少日志,采取更高效的代码逻辑等。
接下来发现 start.js 调用了 env.js ,根据注释,这个env.js 将帮助读取更多环境变量
// Ensure environment variables are read.
require('../config/env');
让我们一起看看 env.js
解析 env.js
在 env.js 的前面就出现了比较有趣的两行代码
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
先 简单看看 paths.js , 这个文件里的主要内容就是导出一些主要的文件的路径,核心代码如下
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
// These properties only exist before ejecting:
ownPath: resolveOwn('.'),
ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3
appTypeDeclarations: resolveApp('src/react-app-env.d.ts'),
ownTypeDeclarations: resolveOwn('lib/react-app.d.ts'),
};
是不是看到了几个眼熟的文件名?
比如 public/index.html , src/index , node_modules, .env 等,同时也有一些平时不常见的文件名 如 jsconfig.json , src/setupTests, src/setupProxy.js
paths.js 给我们透露的信息就是 —— 这些文件在 create-react-app 中都是预设好的 重点关注对象 ,熟悉它们的功能可以让你更大限度地利用 create-react-app
--- 回到上面展示的 env.js 的源码,第二行代码是什么意思呢?
delete require.cache[require.resolve('./paths')];
这就涉及到了 nodejs 的模块缓存机制 :在 nodejs 中,require('xxx') 的背后逻辑首先会在 require.cache 中查找,如果缓存已经存在就返回缓存的模块,否则再去查找路径并实际加载,并加入缓存。
写点代码来帮助理解 —— 创建以下 3 个 js 文件
mod1.js
console.log("someone require mod1");
mod2.js
console.log("someone require mod2");
const mod1 = require('./mod1')
delete require.cache[require.resolve('./mod1')] // 尝试注释掉这行,看看运行 node main.js 的结果有什么不同
main.js
require('./mod2')
require('./mod1')
然后运行 node main.js 会得到以下结果
someone require mod2
someone require mod1
someone require mod1
即 mod1.js 被加载了2次 。
因此 env.js 里那行代码的意图是,当外部代码先调用了 env.js 再调用 paths.js 时, paths.js 也会被再次执行/加载
细解:
- env.js 执行了 const paths = require('./paths'); 因此 paths.js 内容被执行,作为模块被加载并缓存在 require.cache 里
- env. js 执行了 delete require.cache[require.resolve('./paths')]; 删除了 require.cache 里对应的缓存
- env.js 的代码会配置/更新 process.env 的环境变量
- 当外界再次调用 paths.js 时,由于查不到缓存就会再次执行 paths.js 内容 并加载为模块;paths.js 的部分代码依赖了process.env 的环境变量,假如第3步中 process.env 环境变量别更新,此次 paths.js 就能使用到了最新的 process.env 的环境变量值 (而不是缓存的旧值)
--- 继续看 env.js 的代码
const NODE_ENV = process.env.NODE_ENV;
// ...
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
`${paths.dotenv}.${NODE_ENV}`,
paths.dotenv,
].filter(Boolean);
可以看出 create-react-app 可以配合 process.env.NODE_ENV 的值,支持多类环境变量文件,如下:
NODE_ENV !== 'test' && `${paths.dotenv}.local`
另外,这边有个一个关于 javascript &&符号 的小知识点
不同于一些编程语言,JavaScript 的 && 前后可以跟上 非布尔值 ,如果 && 前后项的值有一个为非真的值,那么结果就是这个非真值;如果&& 前后项都是真值,那么返回后面那个值
console.log(0 && 'Dog'); // 0
console.log(false && 'Dog'); // false
console.log(null && 'Dog'); // null
console.log(1 && 'Dog'); // Dog
console.log('Cat' && 'Dog'); // Dog
console.log(true && 'Dog'); // Dog
而 || 有相似逻辑但是相反的结果,这边就不赘述了,读者可以自行测试。
在 create-react-app 的代码中大量使用了 && 和 ||
--- 继续看 env.js 的代码
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
这边使用到了 dotenv 和 dotenv-expand 两个专门针对环境变量文件的库 —— 这两个库支持将环境变量文件中的内容读取、解析(支持变量)然后插入 process.env 中
--- 继续看 env.js 的代码
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
// ....
module.exports = getClientEnvironment;
这个看到一个特别的逻辑 —— 只有当环境变量中符合 REACT_APP_ 为前缀格式的变量值会被保留(根据注释,这些环境变量会被 webpack DefinePlugin 插入到代码中)
这段代码逻辑正好对应了create-react-app 关于自定义环境变量的文档 Adding Custom Environment Variables | Create React App
另外值得注意的是, env.js 默认导出的是 getClientEnvironment 函数(在其他文件中这个函数被多次调用)
--- 简单总结 env.js
env.js 的代码就解析到此了,它的主要功能就是读取环境变量文件插入到 process.env 中, 并导出一个可以读取环境变量的函数
继续解析 start.js
在 require('../config/env'); 之后,我们可以看到
const verifyPackageTree = require('./utils/verifyPackageTree');
if ( process .env.SKIP_PREFLIGHT_CHECK !== 'true') {
verifyPackageTree ();
}
const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup');
verifyTypeScriptSetup ();
其中 verifyPackageTree 用于检查一些依赖库的版本是否正确,开发者是否自行在 package.json 里加入了不兼容的版本 —— 关注的依赖库有:
const depsToCheck = [
// These are packages most likely to break in practice.
// See https:// github.com/facebook/cre ate-react-app/issues/1795 for reasons why.
// I have not included Babel here because plugins typically don't import Babel (so it's not affected).
'babel-eslint',
'babel-jest',
'babel-loader',
'eslint',
'jest',
'webpack',
'webpack-dev-server',
];
而 verifyTypeScriptSetup 主要用于验证 TypeScript 相关的配置是否正确
这两个文件不做详细解析,有兴趣的读者可以自行研究。
接着看到了
const configFactory = require('../config/webpack.config');
稍微往下面翻,可以看到是这么调用的
const config = configFactory('development');
看起来这是一个用于配置 webpack config 的工厂方法,让我们深入看看 webpack.config.js 文件 ( github 链接 )
简单解析 webpack.config.js
这个文件内容相对比较多,但是对于熟悉 webpack 配置的开发者而言其实不难; 如果你还不熟悉 webpack 配置,建议先去webpack 官网简单了解一下 ( Concepts | webpack )
这边不对 webpack 配置做详细解读,只分享源码中几个有趣的点:
不同环境下的webpack配置
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
根据是开发环境还是生产环境的不同,工厂方法生成的 webpack config 也有很多差异,如:
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
// ....
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'