loader

loader 解决问题
babel-loader 把ES6、React转换成ES5
css-loader 加载CSS,支持模块化、压缩、文件导入等特性
eslint-loader 通过es lint检查js代码
file-loader 把文件输出到一个文件夹中,在代码中通过相对URL去引用输出的文件
url-loader 和file loader类似,但是能在文件很小的情况下,以base64的方式把文件内容注入到代码中去
sass-loader 把sass/scss文件编译成CSS
postcss-loader 使用post CSS处理CSS
css-loader 主要处理background url还有@import这些语法。让Webpack能够正确的对其路径进行模块化处理
style-loader 把CSS代码注入到js中,通过DOM操作去加载CSS

plugin

插件 解决问题
case-sensitive-paths-webpack-plugin 如果路径有误会直接报错
terser-webpack-plugin 使用terser来压缩JS
pnp-webpack-plugin
html-webpack-plugin 自动生成带有入口文件引用的index.html
webpack-manifest-plugin 生产资产的显示清单文件
optimize-css-assets-webpack-plugin 用于优化或者压缩css文件
mini-css-extract-plugin 将css提取为独立的文件插件,对每个包含css的js文件都会创建一个css文件
DefinePlugin 创建一个在编译时可配置的全局变量,如果你定义了一个全局变量
HotModuleReplacementPlugin 启动模块热替换
ModuleNotFoundPlugin 找不到模块的时候提供一些信息
ModuleScopePlugin 如果引用了src目录外的文件报警告

下面是React官方webpack.config.js的配置文件,我们可以学习参考。

webpack.config.js

'use strict';

const webpack = require('webpack');
const paths = require("./paths");
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const PnpWebpackPlugin = require("pnp-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
const getClientEnvironment = require("./env");
const cssRegex = /\.css$/;
const sassRegex = /\.(scss|sass)$/;

// webpackEnv在执行npm xxx命令时会传入,如 "build": "cross-env key=value webpack --env=production",
module.exports = function (webpackEnv) {
  console.log("webpackEnv", webpackEnv); //webpackEnv production
  //开发环境
  const isEnvDevelopment = webpackEnv === "development";//false
  //生产环境
  const isEnvProduction = webpackEnv === "production";//true
  //set GENERATE_SOURCEMAP=false  是否在生产环境下生成sourcemap文件
  const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
  //是否把运行时的runtime内置到html中
  const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false";
  console.log("shouldInlineRuntimeChunk", shouldInlineRuntimeChunk);
  // %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
  //忽略结束的/  把环境变量中的变量注入到当前应用中来
  const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
  console.log("env", env);
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      isEnvDevelopment && require.resolve("style-loader"), // 把css样式变成一个style标签插入到页面中
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader, // 把css放到一个单独的文件中
      },
      {
        loader: require.resolve("css-loader"),
        options: cssOptions,
      },
      {
        loader: require.resolve("postcss-loader"),
      },
    ].filter(Boolean);
    if (preProcessor) {
      loaders.push(
        {
          loader: require.resolve("resolve-url-loader"),
        },
        {
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: true,
          },
        }
      );
    }
    return loaders;
  };
  return {
    mode: isEnvProduction ? "production" : "development",
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? "source-map"
        : false
      : isEnvDevelopment && "cheap-module-source-map",
    entry: [
      isEnvDevelopment &&
        require.resolve("react-dev-utils/webpackHotDevClient"), // 热更新客户端,如果webpack要支持热更新,需要在客户端注入一个热更新代码,跟服务器进行一个连接,如果服务端代码变化后会通知客户端更新
      paths.appIndexJs,
    ].filter(Boolean),
    output: {
      path: isEnvProduction ? paths.appBuild : undefined, //输出的目标路径
      //一个main bundle一个文件,每个异步代码块也对应一个文件 ,在生产环境中,并不产出真正的文件
      filename: isEnvProduction
        ? "static/js/[name].[contenthash:8].js"
        : "static/js/bundle.js",
      //如果使用了代码分割的话,这里有额外的JS代码块文件
      chunkFilename: isEnvProduction
        ? "static/js/[name].[contenthash:8].chunk.js"
        : "static/js/[name].chunk.js",
       //打包后的文件的访问路径
      publicPath: paths.publicUrlOrPath,
    },
    optimization: {
      minimize: isEnvProduction,//生产环境要压缩 开发环境不压缩
      minimizer: [
        //压缩JS
        new TerserPlugin({}),
        //压缩CSS
        new OptimizeCSSAssetsPlugin({}),
      ],
      //自动分割第三方模块和公共模块
      splitChunks: {
        chunks: "all",
        name: false,
      },
      //为了长期缓存保持运行时代码块是单独的文件
      runtimeChunk: {
        name: (entrypoint) => `runtime-${entrypoint.name}`,
      },
    },
    resolve: {
      //设置modules的目录
      modules: ["node_modules", paths.appNodeModules],
      //指定扩展名
      extensions: paths.moduleFileExtensions.map((ext) => `.${ext}`),
      alias: {
        //设置别名
        "react-native": "react-native-web",
      },
      plugins: [
        //PnpWebpackPlugin,
        //防止用户引用在src或者node_modules之外的文件
        new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
      ],
    },
    resolveLoader: {
      //plugins: [PnpWebpackPlugin.moduleLoader(module)],
    },
    module: {
      rules: [
        //在babel处理之前执行linter
        {
          test: /\.(js|mjs|jsx|ts|tsx)$/,
          enforce: "pre",
          use: [
            {
              loader: require.resolve("eslint-loader"),
            },
          ],
          include: paths.appSrc,
        },
        {
          //OneOf会遍历接下来的loader直到找一个匹配要求的,如果没有匹配的会走file-loader
          oneOf: [
            {
              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
              loader: require.resolve("url-loader"),
            },
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: paths.appSrc,
              loader: require.resolve("babel-loader"),
            },
            {
              test: cssRegex,
              //用于配置css-loader,作用于@import资源之前有多少个loader
              //0=>无(默认) 1=>postcss-loader 2 postcss-loader sass-loader
              use: getStyleLoaders({ importLoaders: 1 }),
            },
            {
              test: sassRegex,
              //postcss-loader  sass-loader
              use: getStyleLoaders({ importLoaders: 3 }, "sass-loader"),
            },
            {
              loader: require.resolve("file-loader"),
              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
              options: {
                name: "static/media/[name].[hash:8].[ext]",
              },
            },
          ],
        },
      ],
    },
    plugins: [
      //使用插入的script标签生成一个index.html插件
      new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
      }),
      //把运行时代码(webapck工作时需要用到的代码)插入到html里,这样可以节约一个请求
      isEnvProduction &&
        shouldInlineRuntimeChunk &&
        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
      //保证在index.html中获取到环境变量public URL可以通过%PUBLIC_URL%获取
      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
      //模块找不到的时候提供一些必要上下文信息
      new ModuleNotFoundPlugin(paths.appPath),
      //保证在JS中获取到环境变量if (process.env.NODE_ENV === 'production') { ... }
      new webpack.DefinePlugin(env.stringified),
      //模块热更新插件
      isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
      //当你大小写拼错的时候进行提示
      isEnvDevelopment && new CaseSensitivePathsPlugin(),
      //重新安装模块后不用重新启动开发服务器
      isEnvDevelopment &&
        new WatchMissingNodeModulesPlugin(paths.appNodeModules),
      //提取CSS
      isEnvProduction &&
        new MiniCssExtractPlugin({
          filename: "static/css/[name].[contenthash:8].css",
          chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
        }),
      //生成一个manifest文件
      new ManifestPlugin({
        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,
          };
        },
      }),
    ].filter(Boolean),
  };
};

paths.js

'use strict';

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

//当前的工作目录
const appDirectory = fs.realpathSync(process.cwd());
//从相对路径中解析绝对路径
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
//获取PublicUrlOrPath 如当前项目在package.json文件中配置"homepage": "/static/"
const publicUrlOrPath = require(resolveApp("package.json")).homepage || process.env.PUBLIC_URL || "";
//默认的模块扩展名,这里列举的是常用的,不是全部的,如我们在引入模块可能不会写文件名后缀,它会在这里面依次找这些后缀的文件
const moduleFileExtensions = [
  'js',
  'ts',
  'tsx',
  'json',
  'jsx',
];
//解析模块路径 filePath指的是文件路径 如'./title.js' resolveFn函数的作用是从相对路径得到绝对路径
const resolveModule = (resolveFn, filePath) => {
  //js
  const extension = moduleFileExtensions.find(extension =>
    fs.existsSync(resolveFn(`${filePath}.${extension}`))
  );

  if (extension) {
    return resolveFn(`${filePath}.${extension}`);//./title.js
  }

  return resolveFn(`${filePath}.js`);//如果没有,默认是.js文件
};

module.exports = {
  // 环境变量有两种方式:
  // 1. 新建个.evn文件,将环境变量写到文件中
  // 2. 在终端使用 set xxx = xxx 来设置环境变量
  // 一般我们使用.evn文件的方式,方便、快捷、便于管理
  dotenv: resolveApp('.env'),//客户端环境变量的文件名路径
  appPath: resolveApp('.'),//当前工作路径
  appBuild: resolveApp('build'),//输出的build目标路径
  appPublic: resolveApp('public'),//public目录
  appHtml: resolveApp('public/index.html'),//html文件绝对路径
  appIndexJs: resolveModule(resolveApp, 'src/index'),//入口文件
  appPackageJson: resolveApp('package.json'),//package.json文件路径
  appSrc: resolveApp('src'),//src路径
  appTsConfig: resolveApp('tsconfig.json'),
  appJsConfig: resolveApp('jsconfig.json'),
  appNodeModules: resolveApp('node_modules'),
  publicUrlOrPath,
};

module.exports.moduleFileExtensions = moduleFileExtensions;

env.js

'use strict';

const fs = require('fs');
const path = require('path');
const paths = require('./paths');
//一般可能是production或者development set NODE_ENV=development
const NODE_ENV = process.env.NODE_ENV;

//环境变量的文件路径
const dotenvFiles = [
  `${paths.dotenv}.${NODE_ENV}.local`, // .env.development.local
  `${paths.dotenv}.${NODE_ENV}`,       // .env.development
  //在测试环境下不要包括.env.local
  NODE_ENV !== 'test' && `${paths.dotenv}.local`, // .env.local
  paths.dotenv,//.env
].filter(Boolean);

//从.env*文件中加载环境变量,供程序读取如:process.env.PUBLIC_URL=//static2
process.env.username;
dotenvFiles.forEach(dotenvFile => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv-expand')(
      require('dotenv').config({
        path: dotenvFile,
      })
    );
  }
});
//支持通过NODE_PATH加载解析模块: set NODE_PATH=modules或者extraModules
const appDirectory = fs.realpathSync(process.cwd());
//配置node_modules
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);

//获取NODE_ENV and REACT_APP_*环境变量,并且准备通过DefinePlugin插入应用
//如:set REACT_APP_NAME=f
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env) // 拿到env里面所有的key
    .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下面的正确资源路径
        PUBLIC_URL: publicUrl,
      }
    );
  //把所有的值转成字符串以便在DefinePlugin中使用
  const stringified = {
    "process.env": Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return { raw, stringified };
}

module.exports = getClientEnvironment;