[源碼解析] 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 的值,支持多類環境變量文件,如下:

b9fb4a4059a1fe37bdf59ef6376a0097.jpeghttps://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.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/create-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%" />

就能得到以下效果(我這邊的例子只是做示範的,讀者可以自行想象更有意義的應用)

2d27926996331c2ab691040f7ce70c5e.jpeg



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 如何集成
  • 一些有趣的三方庫
  • ...

這就是閱讀源碼的魅力,我們不僅更瞭解了這個庫,還能借鑑它的代碼和設計,還能發現一些有趣的三方庫。


如果覺得這篇文章對你有用、有啓發,歡迎點贊、喜歡、收藏三連!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章