create-react-app默認webpack配置解析及自定義


這邊文章邏輯較亂,如果你有看過eject的create-react-app相關webpack源碼,閱讀下面的文章說不定會有意外收獲(或者直接看10. 如何修改create-react-app的默認配置)

1. dotenv

Dotenv是一個零依賴模塊,可以將.env文件中的環境變量加載process.env

2. 修改配置項,如端口號

//Windows (cmd.exe)
set PORT=true&&npm start

//Windows (Powershell)
($env:PORT = "true") -and (npm start)

//Linux, macOS (Bash)
PORT=true npm start

3. react-dev-utils

此程序包包含Create React App使用的一些實用程序。主要用於webpack;

//可以在GitHub參照源代碼
clearConsole(); //清空控制台信息
openBrowser(url); //在控制台打開網址

4. path模塊相關介紹

// 返回絕對路徑(fs.realpathSync)
const appDirectory = fs.realpathSync(process.cwd());
path.isAbsolute() 方法檢測path是否為絕對路徑

path.delimiter 系統分隔符
  delete require.cache[require.resolve('./paths')];//清除緩存

5. config/paths.js

'use strict';

const path = require('path');
const fs = require('fs');
const url = require('url');

// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

const envPublicUrl = process.env.PUBLIC_URL;

// 路徑是否添加'/'后綴
function ensureSlash(inputPath, needsSlash) { const hasSlash = inputPath.endsWith('/'); if (hasSlash && !needsSlash) { return inputPath.substr(0, inputPath.length - 1); } else if (!hasSlash && needsSlash) { return `${inputPath}/`; } else { return inputPath; } } const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; //通過require加載json文件,然后讀取里面的配置 // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. // Webpack needs to know it to put the right <script> hrefs into HTML even in // single-page apps that may serve index.html for nested URLs like /todos/42. // We can't use a relative path in HTML because we don't want to load something // like /todos/42/static/js/bundle.7289d.js. We have to know the root. function getServedPath(appPackageJson) { const publicUrl = getPublicUrl(appPackageJson); const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); return ensureSlash(servedUrl, true); } const moduleFileExtensions = [ 'web.mjs', 'mjs', 'web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', ]; // Resolve file paths in the same order as webpack const resolveModule = (resolveFn, filePath) => { const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`)) ); if (extension) { return resolveFn(`${filePath}.${extension}`); } return resolveFn(`${filePath}.js`); }; // config after eject: we're in ./config/ 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'),
 //我們可以在package.json的proxy配置開發環境的后端地址,如果需要更多自定義,
//我們可以安裝'http-proxy-middleware',然后在src目錄下創建setupProxy.js
// 沒有調用resolveModule,所以文件的后綴必須為.js proxySetup: resolveApp(
'src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), publicUrl: getPublicUrl(resolveApp('package.json')), servedPath: getServedPath(resolveApp('package.json')), }; module.exports.moduleFileExtensions = moduleFileExtensions;

 6. config/env.js

 
         
 const REACT_APP = /^REACT_APP_/i; // 可以看到create-react-app的process.env 除了NODE_ENV,PUBLIC_URL外,其余都是以REACT_APP_開頭才生效
//注意,這里所說的是在編譯時能讀取的變量。比如通過.env 配置PORT=3010,還是能起作用,但是在index.html中通過%PORT%並獲取不到
function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter(key => REACT_APP.test(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        // Useful for determining whether we’re running in production mode.
        // Most importantly, it switches React into the correct mode.
        NODE_ENV: process.env.NODE_ENV || 'development',
        // Useful for resolving the correct path to static assets in `public`.
        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
        // This should only be used as an escape hatch. Normally you would put
        // images into the `src` and `import` them in code to get their paths.
        PUBLIC_URL: publicUrl,
      }
    );
  // Stringify all values so we can feed into Webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]); // key=value; value是被JSON.stringify() return env;
    }, {}),
  };

  return { raw, stringified };
}

7. webpack-dev-server

https://webpack.docschina.org/configuration/dev-server

//compiler 類似const compiler = Webpack(webpackConfig);
//devServerOptions為上述鏈接的配置項
// 這是參數的類型
// https://github.com/webpack/webpack-dev-server/blob/84cb4817a3fb9d8d98ac84390964cd56d533a3f5/lib/options.json
new WebpackDevServer(compiler, devServerOptions);

 8. fs模塊

fs.accessSync(path[, mode]) //同步地測試用戶對 path 指定的文件或目錄的權限

F_OK,表明文件對調用進程可見。 這對於判斷文件是否存在很有用,但對 rwx 權限沒有任何說明。 如果未指定模式,則默認值為該值。
R_OK,表明調用進程可以讀取文件。
W_OK,表明調用進程可以寫入文件。
X_OK,表明調用進程可以執行文件。 在 Windows 上無效(表現得像 fs.constants.F_OK)

var fs = require('fs');
var path = require('path');
var chalk = require('chalk');

function checkRequiredFiles(files) {
  var currentFilePath;
  try {
    files.forEach(filePath => {
      currentFilePath = filePath;
      fs.accessSync(filePath, fs.F_OK);
    });
    return true;
  } catch (err) {
    var dirName = path.dirname(currentFilePath);
    var fileName = path.basename(currentFilePath);
    console.log(chalk.red('Could not find a required file.'));
    console.log(chalk.red('  Name: ') + chalk.cyan(fileName));
    console.log(chalk.red('  Searched in: ') + chalk.cyan(dirName));
    return false;
  }
}

module.exports = checkRequiredFiles;
fs.existsSync(path) //判斷文件是否存在

9. config/webpack.config.js

#html-webpack-plugin
new HtmlWebpackPlugin(
  Object.assign(
    {},
    {
      inject: true,
      template: paths.appHtml,
    },
    isEnvProduction
      ? {
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeRedundantAttributes: true,
            useShortDoctype: true,
            removeEmptyAttributes: true,
            removeStyleLinkTypeAttributes: true,
            keepClosingSlash: true,
            minifyJS: true,
            minifyCSS: true,
            minifyURLs: true,
          },
        }
      : undefined
  )
),
// 其中minify的參數配置可借鑒
https://github.com/kangax/html-minifier#options-quick-reference

 

#pnp-webpack-plugin
但是 Yarn 作為一個包管理器, 它知道你的項目的依賴樹. 那能不能讓 Yarn 告訴 Node? 讓它直接到某個目錄去加載模塊.
這樣即可以提高 Node 模塊的查找效率, 也可以減少 node_modules 文件的拷貝. 這就是Plug'n'Play的基本原理.

const PnpWebpackPlugin = require(`pnp-webpack-plugin`);
 
module.exports = {
  resolve: {
    plugins: [
      PnpWebpackPlugin,
    ],
  },
  resolveLoader: {
    plugins: [
      PnpWebpackPlugin.moduleLoader(module),
    ],
  },
};


//需要注意的是
如果您的部分配置來自使用自己的加載器的第三方軟件包,請確保它們使用require.resolve- 這將確保解決過程在整個環境中都是可移植的

module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      loader: require.resolve('babel-loader'), // 原先是 loader: 'babel-loader'
    }]
  },
};

 

#case-sensitive-paths-webpack-plugin
此Webpack插件強制所有必需模塊的完整路徑與磁盤上實際路徑的確切大小寫相匹配

var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
 
var webpackConfig = {
    plugins: [
        new CaseSensitivePathsPlugin()
        // other plugins ...
    ]
    // other webpack config ...
}
#terser-webpack-plugin
new TerserPlugin({
  terserOptions: {
    parse: {
      ecma: 8,
    },
    compress: {
      ecma: 5,
      warnings: false,
      comparisons: false,
      drop_console: false, //默認為false
      inline: 2,
    },
    mangle: {
      safari10: true,
    },
    output: {
      ecma: 5,
      comments: false,
      // Turned on because emoji and regex is not minified properly using default
      // https://github.com/facebook/create-react-app/issues/2488
      ascii_only: true,
    },
  },
  parallel: !isWsl,
  // Enable file caching
  cache: true,
  sourceMap: shouldUseSourceMap,
})
// Makes some environment variables available to the JS code
new webpack.DefinePlugin(env.stringified),

// Makes some environment variables available in index.html

const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)

 

10. 如何修改create-react-app的默認配置

npm install react-app-rewired -S
npm install customize-cra -S

//package.json
"scripts": {
    + "start": "react-app-rewired start",
    + "build": "react-app-rewired build",
    + "test": "react-app-rewired test",
    "eject": "react-scripts eject"
 },

一般在根目錄下創建config-overrides.js

customize-cra提供了一些簡遍的api(customize-cra/api.md),通常就可以滿足大部分的開發需求

源碼(customize-cra/src/customizes/webpack.js)(比如當前版本https://github.com/arackaf/customize-cra/blob/0b50907a724b04fa347164a5e8b6cd1f0c2c067b/src/customizers/webpack.js)

看了customize-cra提供的API,有簡單的提供了addWebpackPlugin這個API,但是,如果我想修改CRA默認的配置項,比如HtmlWebpackPlugin

並沒有提供對應的API;聯想到customize-cra1.0x是開發者自己修改config,然后看了下某個API的源碼,eg

// This will remove the CRA plugin that prevents to import modules from
// outside the `src` directory, useful if you use a different directory
export const removeModuleScopePlugin = () => config => {
  config.resolve.plugins = config.resolve.plugins.filter(
    p => p.constructor.name !== "ModuleScopePlugin"
  );
  return config;
};

可以看的這些API其實也是返回一個函數,然后通過override()包裝傳入config。

const {
  override,
  addWebpackPlugin
} = require("customize-cra")

const HtmlWebpackPlugin = require('html-webpack-plugin');

const isEnvProduction = process.env.NODE_ENV === 'production'

const paths = require('react-scripts/config/paths') // 第五點有其源碼

// 自己定義一個函數,過濾config.plugins數組 const removeHtmlWebpackPlugin
= () => { return (config) => { config.plugins = config.plugins.filter( p => p.constructor.name !== "HtmlWebpackPlugin" ); // throw new Error() return config; } }

//生產環境去除console.* functions
   const dropConsole = () => {
    return (config) => {
      if(config.optimization.minimizer){
        config.optimization.minimizer.forEach( (minimizer) => {
          if( minimizer.constructor.name === 'TerserPlugin'){  
            minimizer.options.terserOptions.compress.drop_console = true
          }
 
        })
      }
      return config;
    }
  }
// console.log(paths, 'paths')

module.exports = override(
  removeHtmlWebpackPlugin(),
  addWebpackPlugin(new HtmlWebpackPlugin(
    Object.assign(
      {},
      {
        inject: true,
        template: paths.appHtml,
      },
      isEnvProduction
        ? {
            minify: {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              // minifyJS: true, // 我們去掉默認的壓縮js配置項,具體配置項參數第9點有提及
              minifyCSS: true,
              minifyURLs: true,
            },
          }
        : undefined
    )
  )),
 dropConsole() )
如果你只想刪除console.log,可以用pure_funcs代替

const dropConsole = () => {
  return (config) => {
    if(config.optimization.minimizer){
      config.optimization.minimizer.forEach( (minimizer) => {
        if( minimizer.constructor.name === 'TerserPlugin'){
          // minimizer.options.terserOptions.compress.drop_console = true
          minimizer.options.terserOptions.compress.pure_funcs = ['console.log']
          
        }
       
      })       
    }
    return config;
  }
}

override()函數詳解

import flow from "lodash.flow";

//flow;創建一個函數。 返回的結果是調用提供函數的結果,this 會綁定到創建函數。 每一個連續調用,傳入的參數都是前一個函數返回的結果。
// 這里的plugins是個數組,里面是每個函數,返回值都是config,然后傳給下一個函數 export const override
= (...plugins) => flow(...plugins.filter(f => f)); // Use this helper to override the webpack dev server settings // it works just like the `override` utility export const overrideDevServer = (...plugins) => configFunction => ( proxy, allowedHost ) => { const config = configFunction(proxy, allowedHost); const updatedConfig = override(...plugins)(config); return updatedConfig; };

 11.  webpack-stats-plugin 通過這個插件,可以默認生成stats.json的構建信息(在線分析: http://webpack.github.io/analyse)

查找不到如何獲取create-react-app的打包時間;這里只是簡單用於大概獲取打包所需時間,具體的打包文件分析可以使用webpack-bundle-analyzer 插件

//config-overrides.js
const {
  override,
  addWebpackPlugin
} = require("customize-cra");
const { StatsWriterPlugin } = require("webpack-stats-plugin")
let startTime = Date.now()

module.exports = override(
  addWebpackPlugin(new StatsWriterPlugin({
    fields: null,
    stats: {
      timings: true,// 不生效
    },
    transform: (data) => {
      let endTime = Date.now()
      data = {
        Time: (endTime - startTime)/1000 + 's'
      }
      return JSON.stringify(data, null, 2);
    }
  }))
)

 12. progress-bar-webpack-plugin 用於顯示構建的進度條

其他npm包

//Check if the process is running inside Windows Subsystem for Linux (Bash on Windows)

const isWsl = require('is-wsl');
 
// When running inside Windows Subsystem for Linux
console.log(isWsl);
//=> true

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM