js 读取json文件_[源码解析] 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 时调用的脚本.

阅读提示:

  1. 建议同时打开 create-react-app 源码 (github链接),对照着阅读本文。
  2. 由于代码较多,手机阅读体验较差,建议先点赞、收藏,然后使用电脑阅读。

开始解析 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 也会被再次执行/加载

细解:

  1. env.js 执行了 const paths = require('./paths'); 因此 paths.js 内容被执行,作为模块被加载并缓存在 require.cache 里
  2. env.js 执行了 delete require.cache[require.resolve('./paths')]; 删除了 require.cache 里对应的缓存
  3. env.js 的代码会配置/更新 process.env 的环境变量
  4. 当外界再次调用 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 的值,支持多类环境变量文件,如下:

3b4ee6ccb24c17ee6f2d0a8e5ff6a20e.png
https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
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

这边使用到了 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'
        : isEnvDevelopment && 'static/js/[name].chunk.js',

   // ...
     sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,

env.js 的使用

简单看看上面解析过的 env.js 在 webpack.config.js 中是如何被使用的

const getClientEnvironment = require('./env');
// ...
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
// ...
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),

即 env 环境变量被插入到了 index.html 和 js 代码中,我们可以利用这个设定做一些有趣的事情:

比如我在 项目根目录的 .env 文件写入

REACT_APP_GREETING='Freewheel Lee' 

然后在 public/index.html 的 header 里加入

<meta name="greeting" content="%REACT_APP_GREETING%" />

就能得到以下效果(我这边的例子只是做示范的,读者可以自行想象更有意义的应用)

0f87d1c879f2222ad02b4ead029a8d5a.png

React Fast Refresh

const shouldUseReactRefresh = env.raw.FAST_REFRESH;

在2020/08/03的 commit 上,FAST_REFRESH 被默认打开了,React Fast Refresh 是新一代热部署webpack插件 (pmmmwh/react-refresh-webpack-plugin),有兴趣的读者可以读读这两篇文章

https://mariosfakiolas.com/blog/what-the-heck-is-react-fast-refresh/

https://medium.com/javascript-in-plain-english/what-is-react-fast-refresh-f3d1e8401333

继续解析 start.js

继续看源码可以发现 create-react-app 使用了 webpack-dev-server 作为开发环境下的服务器

const WebpackDevServer = require('webpack-dev-server');
// ...
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) { clearConsole();
}
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
} console.log(chalk.cyan('Starting the development server...n')); openBrowser(urls.localUrlForBrowser);
});

webpack-dev-server 的 GitHub 简介如下

Use webpack with a development server that provides live reloading. This should be used for development only.

有兴趣进一步了解 webpack-dev-server 的读者, 可以看看他们的官网 (DevServer | webpack 和 webpack/webpack-dev-server )

正是因为 create-react-app 使用了 webpack-dev-server, 开发者才能在 http://localhost:3000/ 访问到自己开发的 React APP

另外一些有意思的代码

const useYarn = fs.existsSync(paths.yarnLockFile);

原来 create-react-app 是根据根目录下是否有 yarn.lock 文件来判断是否要使用 yarn

类似的

const useTypeScript = fs.existsSync(paths.appTsConfig);

create-react-app 是根据根目录下是否有 tsconfig.json 文件来判断是否要使用 TypeScript

此外start.js 里还使用了一些非常流行的第三方库,有兴趣可以进一步研究:

  • chalk 带颜色的terminal 输出
  • semver nodejs 库版本号工具库

总结

create-react-app 中 start.js 和 几个相关文件的源码 的解析基本结束了,虽然过程比较粗粒度,但是我们已经收获了不少:

  • 发现了 create-react-app文档中一些设定的底层实现逻辑
  • 接触了nodejs 的模块缓存机制
  • JavaScript && 和 || 的特殊使用方法
  • 根据环境变量的不同采取不同策略的设计思想
  • 初步了解 create-react-app 和 webpack 如何集成
  • 一些有趣的三方库
  • ...

这就是阅读源码的魅力,我们不仅更了解了这个库,还能借鉴它的代码和设计,还能发现一些有趣的三方库。

如果觉得这篇文章对你有用、有启发,欢迎点赞、喜欢、收藏三连!


最近在健身增肌,下图与君共勉!

8a565ce0881cdac39bd5054928663250.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值