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