Create-React-App 的webpack配置解读

1,685 阅读8分钟

之前开发了好些项目,webpack都是用了很多现成的配置,每次使用都只关注到需要调整的地方。借着这次cra的项目,我把webpack的配置文件过了一遍,以一个初学者的角度重新书写一次webpack的作用。附webpack文档网址:webpack.docschina.org/

首先大家需要先知道webpack的作用,通俗来说,我们打开cmd命令窗口,执行脚本的启动命令,便会触发项目的启动以及打包,而webpack就是启动过程中发挥作用的模块打包工具,通过各种配置项,它能让项目的启动和执行拥有很多强大的功能,如压缩文件、代码提高性能,使用转换器转移ES,TS代码,设置路径别名等常用功能

在搭建好CRA之后,默认的webpack设置是隐藏的,可通过npm eject指令引出webpack的配置文件,此操作不可逆,为便于学习,我这次使用了eject,生成了scripts跟config两个文件夹如图。其中比较重要的有几个:

  1. env.js : 读取env环境变量配置文件并把变量赋值给process.env这全局变量中
  2. path.js : 提供输出模块的路径名
  3. start.js : 本地启动项目的脚本 : npm start
  4. build.js : 把项目打包起来的命令,最终把打包好的文件拿去服务器部署: npm build
  5. webpackDevServer.config.js : 配置服务器
  6. webpack.config : 最关键的配置文件,入口出口模块加载器等的配置
"scripts": {
    "start": "cross-env REACT_APP_NODE_ENV=development react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
  },

image.png

我把配置文件中一些注释和不必要的代码去掉,以便于精准读取关键代码

start.js

process.env.BABEL_ENV = 'development'; // 定义环境为开发环境
process.env.NODE_ENV = 'development'; 
process.on('unhandledRejection', err => { // 打包时查看具体报错抛出异常
  throw err; });
require('../config/env');  //  加载环境变量

const chalk = require('react-dev-utils/chalk'); // 在命令窗口打印highlight用的插件
const WebpackDevServer = require('webpack-dev-server'); // 开启服务器
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
// devServer的配置文件
const createDevServerConfig = require('../config/webpackDevServer.config'); 
// 获取环境变量
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));
// 拿到环境变量文件,优先级是最后一个
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); 
const useYarn = fs.existsSync(paths.yarnLockFile); // 是否使用yarn
// 判断 Node.js 是否运行在一个 TTY 环境中,tty 模块提供终端相关的接口,用来获取终端的行数列数等
const isInteractive = process.stdout.isTTY; 
// 检查必要文件,不存在就退出
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 
  process.exit(1); // node 进程结束
}

const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; // 端口
const HOST = process.env.HOST || '0.0.0.0'; // IP

const { checkBrowsers } = require('react-dev-utils/browsersHelper'); // 检查浏览器
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    return choosePort(HOST, DEFAULT_PORT); // 当前port忙碌时 使用其他port
  })
  .then(port => {
    if (port == null) { // 找不到端口直接退出
      return;
    }

    const config = configFactory('development'); // 返回开发环境的配置
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; // 网络协议
    const appName = require(paths.appPackageJson).name; // 项目名称

    const useTypeScript = fs.existsSync(paths.appTsConfig); // 存在ts-config时为true
    const urls = prepareUrls( // 返回一个本地和远程的协议+ip+端口给开发环境的服务器使用
      protocol,
      HOST,
      port,
      paths.publicUrlOrPath.slice(0, -1)
    );
    const compiler = createCompiler({ // 创建一个webpack编译器,
      appName,
      config,
      urls,
      useYarn,
      useTypeScript,
      webpack,
    });
    // Load proxy config 代理配置,可在package.json中配置
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );
    const serverConfig = { // webpack-dev-server的配置 
      ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
      host: HOST,
      port,
    };
    const devServer = new WebpackDevServer(serverConfig, compiler); 
    devServer.startCallback(() => { // 启动devServer服务
      if (isInteractive) { 
        clearConsole(); // 把控制台清除掉
      }

      if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { // 检查react 版本
        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); // 打开浏览器
    });
  })

env.js : 引用相应的环境变量文件并注入到process.env中,,并导出一个可以读取环境变量的函数

// Node.js中,require首先会在require.cache中查找,把缓存清除掉这样每次调用就会获取最新的变量值
delete require.cache[require.resolve('./paths')];

const NODE_ENV = process.env.REACT_APP_NODE_ENV; 
// node环境,开发环境设置为development,生产环境是production
if (!NODE_ENV) { // 无node环境配置直接抛出异常退出
  throw new Error(
    'The NODE_ENV environment variable is required but was not specified.'
  );
}
const dotenvFiles = [ // 从几个路径下拿到有效的配置文件,需要自定义env文件需要在根目录创建
  `${paths.dotenv}.${NODE_ENV}.local`,
  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
  `${paths.dotenv}.${NODE_ENV}`,
  paths.dotenv,
].filter(Boolean);
// 使用到了 dotenv 和 dotenv-expand 两个专门针对环境变量文件的库 —— 这两个库支持将环境变量文件中的内容读取、解析(支持变量)然后插入 process.env 中
dotenvFiles.forEach(dotenvFile => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv-expand')(
      require('dotenv').config({
        path: dotenvFile,
      })
    );
  }
});

const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
  .split(path.delimiter)
  .filter(folder => folder && !path.isAbsolute(folder))
  .map(folder => path.resolve(appDirectory, folder))
  .join(path.delimiter);

// 利用正则表达式,只有以REACT_APP为前缀的变量会被保留加入process.env中,跟官方文档的说法吻合
const REACT_APP = /^REACT_APP_/i;

function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter(key => REACT_APP.test(key))  // 保留REACT_APP_前缀的变量
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        NODE_ENV: process.env.NODE_ENV || 'development',
        PUBLIC_URL: publicUrl,
        WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
        WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
        WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
        FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
      }
    );
  const stringified = { // 将value转为字符型
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return { raw, stringified };
}

module.exports = getClientEnvironment;

webpack.config 直接去马吧


// fork一个进程进行检查,将错误信息反馈给webpack
const ForkTsCheckerWebpackPlugin =
  process.env.TSC_COMPILE_ON_ERROR === 'true'
    ? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
    : require('react-dev-utils/ForkTsCheckerWebpackPlugin'); // 
// sourceMap 是否启动,默认true
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; 
// 是否内联runtime文件
const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime'); 
const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
  '@pmmmwh/react-refresh-webpack-plugin'
);
// 是否使用内联runtimeChunk
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
// 是否使用eslint警告提示, 校验代码
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';

const imageInlineSizeLimit = parseInt( // 最大转换base64图片的大小 10000
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

const useTypeScript = fs.existsSync(paths.appTsConfig); // 判断是否存在ts配置文件

// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc; // service-worker

// style files regexes style文件的正则表达式 用来匹配style相关的文件
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

const hasJsxRuntime = (() => { // 检查是否配置jsx-runtime
  ...
})();

// 生成最终webpack开发或生成环境配置的函数
module.exports = function (webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';

  const isEnvProductionProfile =
    isEnvProduction && process.argv.includes('--profile');
// 加载.env文件的环境变量,REACT_APP 开头
  const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); 
// 热更新 react 组件,开发环境下开启
  const shouldUseReactRefresh = env.raw.FAST_REFRESH; 

  // common function to get style loaders
  const getStyleLoaders = (cssOptions, preProcessor) => { 
    // 处理style-loader 通过文件后缀名匹配文件并通过loader打包
    test: option
    use: module
  };

  return {
    target: ['browserslist'], // 
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', // 模式
    // 在第一个错误出现时抛出失败结果,而不是容忍它.开发环境设置为false,生产打包设置为true
    bail: isEnvProduction,
    devtool: isEnvProduction // 默认生产环境使用source-map
      ? shouldUseSourceMap
        ? 'source-map'
        : false
      : isEnvDevelopment && 'cheap-module-source-map',
    entry: paths.appIndexJs, // 入口文件 src/index
    output: {
      path: paths.appBuild,  // 出口文件 build
      pathinfo: isEnvDevelopment, // 是否添加注释到文件中,开发环境为true
      filename: isEnvProduction // 文件名,使用contenthash进行命名
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
      chunkFilename: isEnvProduction // 代码分割 出来的文件会以 chunkFilename进行命名
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
      assetModuleFilename: 'static/media/[name].[hash][ext]',
      publicPath: paths.publicUrlOrPath,  // 默认为 / ,可通过package.json中的homepage进行配置
      devtoolModuleFilenameTemplate: isEnvProduction // 在生成SourceMap时的函数的文件名模版字符串。
        ? info =>
            path
              .relative(paths.appSrc, info.absoluteResourcePath)
              .replace(/\\/g, '/')
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
    },
    cache: { // 持久化缓存 webpack 5.0新增模块 通过cache缓存跟实际变化的hash对比判断出需要重新编译的文件,极大增强了性能
      type: 'filesystem', //  开启持久缓存
      version: createEnvironmentHash(env.raw),
      cacheDirectory: paths.appWebpackCache, // 缓存路径
      store: 'pack',
      buildDependencies: {
        defaultWebpack: ['webpack/lib/'],
        config: [__filename],
        tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
          fs.existsSync(f)
        ),
      },
    },
    infrastructureLogging: {
      level: 'none',
    },
    optimization: { // 启动压缩, 优化性能
      minimize: isEnvProduction, // 生产环境才压缩体积
      minimizer: [
        new TerserPlugin({ // 压缩js的插件
          parallel: true, // 多进程进行提高构建速度
          terserOptions: ...
        }),
        new CssMinimizerPlugin(), // 压缩css
      ],
    },
    resolve: { // 定义解析规则
    // 告诉 webpack 解析模块是去找哪个目录 默认node_modules
      modules: ['node_modules', paths.appNodeModules].concat(
        modules.additionalModulePaths || []
      ),
      extensions: paths.moduleFileExtensions // 省略拓展名,自动在文件名后面加入list中的文件后缀
        .map(ext => `.${ext}`)
        .filter(ext => useTypeScript || !ext.includes('ts')),
      alias: { // 别名 可通过config-overrides.js 加入拓展
      },
      plugins: [ // 应该使用的额外的解析插件列表
        new ModuleScopePlugin(paths.appSrc, [ //为避免混乱,限制查找定定义模块的范围,只能在src内部
          paths.appPackageJson,
          reactRefreshRuntimeEntry,
          reactRefreshWebpackPluginRuntimeEntry,
          babelRuntimeEntry,
          babelRuntimeEntryHelpers,
          babelRuntimeRegenerator,
        ]),
      ],
    },
    module: {
      strictExportPresence: true,
      rules: [ // 解析模块的规则
        shouldUseSourceMap && { // 使用sourceMap
          enforce: 'pre',
          exclude: /@babel(?:\/|\\{1,2})runtime/,
          test: /\.(js|mjs|jsx|ts|tsx|css)$/,
          loader: require.resolve('source-map-loader'),
        },
        {
          oneOf: [ // 匹配第一个Loader就结束,提高性能
              // ... 各种loader的功能可在官方文档查阅
          ],
        },
      ].filter(Boolean),
    },
    plugins: [
      // 处理html 插件 : 以template为模板,创建一个html文件,并将 处理好的bundle文件自动引入到html文件中 (css,js,dll单独打包的依赖文件等)
      new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appHtml,
          },
          isEnvProduction
            ? {
                minify: { // 生产环境下 配置html的压缩,去除注释
                  removeComments: true, // 去除注释
                  ...
                },
              }
            : undefined
        )
      ),
      // 是否内联runtime文件,作用就是少发一个请求,通过cross-env 将环境变量设置为true
      isEnvProduction &&
        shouldInlineRuntimeChunk &&
        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]), 
      // HtmlWebpackPlugin的辅助插件,可以在html文件中加入变量
      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), 
      // 找不到模块,有更好的提示
      new ModuleNotFoundPlugin(paths.appPath), 
      // 等于把环境变量process.env注入全局中,我们可以在模块当中直接使用这些变量。无需作任何声明,
      new webpack.DefinePlugin(env.stringified),
      // 热更新 react 组件,开发环境下开启,修改js,css等文件会触发更新
      isEnvDevelopment &&
        shouldUseReactRefresh &&
        new ReactRefreshWebpackPlugin({ 
          overlay: false,
        }),
      // 路径的大小写敏感校验模块
      isEnvDevelopment && new CaseSensitivePathsPlugin(),
      isEnvProduction &&
        new MiniCssExtractPlugin({ // 抽离css文件插件,生产环境下开启
          filename: 'static/css/[name].[contenthash:8].css',
          chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
        }),
      //  生成一个文件清单, 内容是打包前文件对应打包后的文件名
      new WebpackManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: paths.publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);
          const entrypointFiles = entrypoints.main.filter(
            fileName => !fileName.endsWith('.map')
          );

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),
      new webpack.IgnorePlugin({ // wepack内置插件,可以在打包时有选择的忽略一些内容,
      // 这里的配置是在打包moment的时候忽略moment的本地化内容
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      }),
      isEnvProduction &&
        fs.existsSync(swSrc) &&
        new WorkboxWebpackPlugin.InjectManifest({
          swSrc,
          dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
          exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
          // Bump up the default maximum size (2mb) that's precached,
          // to make lazy-loading failure scenarios less likely.
          // See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
        }),
      // ts 配置 开启typescript必备配置
      useTypeScript :{
          ...
      }
    performance: false, // 在资源的大小超过限制的时候,做出提示。
  };
};


这篇文章旨在简单介绍cra的webpack默认配置,还有更多的插件,模块里面并未全部罗列,最重要的是配置自己适合的。必须承认在刚学webpack时碰到了不少壁,要了解工作原理还是自己搭一个简单的webpack工程,直接在现有的开源项目或cra中学习的难度会比较大,内容过多也无法知道哪些是关键配置。可以尝试把一些配置改掉或删除看看打包的效果和性能的变化,会对学习更有帮助哈。