Webpack配置详解与实战指南

Webpack配置详解与实战:从入门到精通

目录

前言

Webpack是现代前端开发中最重要的构建工具之一,它通过强大的模块打包能力,将复杂的项目结构转化为可部署的静态资源。本篇文章将从Webpack的基本概念开始,逐步深入到Loader和Plugin的开发,最后通过实战案例和性能优化技巧,帮助你全面掌握Webpack的使用。

Webpack基础

什么是Webpack

Webpack是一个模块打包器,它的主要功能是分析项目结构,找到JavaScript模块以及其他一些浏览器不能直接运行的扩展语言(如TypeScript、Sass等),并将它们打包为合适的格式供浏览器使用。

核心特性

  • 模块打包:将模块化的代码打包为浏览器可执行的格式
  • 代码分割:将代码分割成多个chunk,按需加载
  • 插件系统:强大的插件系统用于扩展功能
  • 模块转换:支持多种模块格式(ES6、CommonJS、AMD等)
  • 热更新:开发时支持热模块替换(HMR)

Webpack工作原理

Entry入口文件
依赖图
Loader转换
Module模块
Plugin插件
Bundle打包输出

Webpack通过从入口文件开始,递归地构建一个依赖图,然后将所有模块打包成一个或多个bundle。

核心概念深度解析

1. Entry(入口)

入口是Webpack构建的起始点,它告诉Webpack从哪里开始构建依赖图。

javascript 复制代码
// 单入口
module.exports = {
  entry: './src/index.js'
};

// 对象形式(多入口)
module.exports = {
  entry: {
    main: './src/main.js',
    admin: './src/admin.js',
    vendor: ['react', 'react-dom']
  }
};

// 函数形式(动态入口)
module.exports = {
  entry: () => {
    return {
      main: './src/main.js',
      vendor: './src/vendor.js'
    };
  }
};

2. Output(输出)

Output指定Webpack如何输出打包后的文件。

javascript 复制代码
module.exports = {
  output: {
    // 输出文件路径(绝对路径)
    path: path.resolve(__dirname, 'dist'),

    // 输出文件名
    filename: '[name].js', // [name]会被替换为chunk名

    // 输出文件名包含contenthash,用于缓存
    filename: '[name].[contenthash].js',

    // 非入口chunk文件名
    chunkFilename: '[name].[contenthash].chunk.js',

    // 公共路径
    publicPath: '/',

    // 或者CDN路径
    publicPath: 'https://cdn.example.com/',

    // 清理输出目录
    clean: true,

    // 静态资源文件名模板
    assetModuleFilename: 'assets/[hash][ext][query]'
  }
};

3. Module(模块)

Module定义了如何处理项目中的各种模块。

javascript 复制代码
module.exports = {
  module: {
    rules: [
      // Loader规则
      {
        test: /\.js$/, // 匹配文件
        exclude: /node_modules/, // 排除文件
        include: path.resolve(__dirname, 'src'), // 包含文件
        use: [ // 使用Loader
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        ]
      }
    ]
  }
};

4. Resolve(解析)

Resolve配置如何解析模块路径。

javascript 复制代码
module.exports = {
  resolve: {
    // 文件扩展名
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],

    // 路径别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      '$': path.resolve(__dirname, 'public')
    },

    // 模块查找目录
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules'
    ],

    // 主文件
    mainFiles: ['index'],

    // 主字段
    mainFields: ['browser', 'module', 'main']
  }
};

5. Plugin(插件)

插件用于扩展Webpack功能,处理各种任务。

javascript 复制代码
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    // 生成HTML文件
    new HtmlWebpackPlugin({
      template: './src/index.html',
      inject: 'head',
      minify: true
    }),

    // 提取CSS为独立文件
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[name].[contenthash].chunk.css'
    })
  ]
};

6. Mode(模式)

Mode指定运行环境,影响Webpack的优化策略。

javascript 复制代码
module.exports = {
  mode: 'development', // 或 'production' 或 'none'
  // 开发模式:启用调试、源码映射、快速的增量构建
  // 生产模式:启用所有优化,如压缩、Tree-shaking等
  // none:不启用任何优化
};

基础配置详解

1. 开发环境配置

javascript 复制代码
// webpack.dev.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',

  entry: './src/index.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js',
    publicPath: '/',
    clean: true
  },

  devtool: 'eval-cheap-module-source-map',

  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    port: 3000,
    hot: true, // 热更新
    open: true, // 自动打开浏览器
    compress: true, // 启用gzip压缩
    historyApiFallback: true, // SPA路由支持
    proxy: { // 代理配置
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  },

  module: {
    rules: [
      // JavaScript/JSX
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }],
              ['@babel/preset-react', { runtime: 'automatic' }]
            ],
            plugins: ['react-refresh/babel']
          }
        }
      },

      // CSS
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },

      // SCSS/SASS
      {
        test: /\.(scss|sass)$/,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
      },

      // 图片和字体
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024 // 8KB
          }
        },
        generator: {
          filename: 'images/[name].[hash][ext]'
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash][ext]'
        }
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      inject: 'body'
    })
  ],

  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
};

2. 生产环境配置

javascript 复制代码
// webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  mode: 'production',

  entry: {
    main: './src/index.js',
    vendor: ['react', 'react-dom']
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash].js',
    chunkFilename: 'js/[name].[contenthash].chunk.js',
    assetModuleFilename: 'assets/[hash][ext][query]',
    publicPath: '/',
    clean: true
  },

  devtool: 'source-map',

  module: {
    rules: [
      // JavaScript/JSX
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: { browsers: ['> 1%', 'last 2 versions'] },
                useBuiltIns: 'usage',
                corejs: 3
              }],
              ['@babel/preset-react', { runtime: 'automatic' }]
            ],
            plugins: [
              '@babel/plugin-transform-react-constant-elements',
              '@babel/plugin-transform-react-inline-elements'
            ]
          }
        }
      },

      // CSS
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  ['autoprefixer', {}],
                  ['cssnano', { preset: 'default' }]
                ]
              }
            }
          }
        ]
      },

      // SCSS/SASS
      {
        test: /\.(scss|sass)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['autoprefixer']
              }
            }
          },
          'sass-loader'
        ]
      },

      // 图片
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        },
        generator: {
          filename: 'images/[name].[hash][ext]'
        }
      },

      // 字体
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash][ext]'
        }
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    }),

    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename: 'css/[name].[contenthash].chunk.css'
    }),

    // 分析打包结果
    process.env.ANALYZE && new BundleAnalyzerPlugin()
  ].filter(Boolean),

  optimization: {
    minimize: true,
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 第三方库分离
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },

        // React相关
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20,
          enforce: true
        },

        // 公共代码
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },

    // 运行时提取
    runtimeChunk: {
      name: 'runtime'
    },

    // 压缩配置
    minimizer: [
      (compiler) => {
        const TerserPlugin = require('terser-webpack-plugin');
        new TerserPlugin({
          extractComments: false,
          terserOptions: {
            compress: {
              drop_console: true,
              drop_debugger: true,
              pure_funcs: ['console.log', 'console.info', 'console.debug']
            },
            mangle: {
              safari10: true
            },
            format: {
              safari10: true
            }
          }
        }).apply(compiler);
      }
    ]
  },

  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 250000,
    hints: 'warning'
  }
};

3. 完整配置文件

javascript 复制代码
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';

  return {
    // 基本配置
    entry: './src/index.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? '[name].[contenthash].js' : '[name].js',
      clean: true,
      publicPath: '/'
    },

    // 模式
    mode: isProduction ? 'production' : 'development',

    // 源码映射
    devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',

    // 开发服务器
    devServer: {
      static: path.resolve(__dirname, 'dist'),
      port: 3000,
      hot: true,
      open: true
    },

    // 模块规则
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  targets: 'defaults',
                  useBuiltIns: 'usage',
                  corejs: 3
                }],
                ['@babel/preset-react', {
                  runtime: 'automatic'
                }]
              ],
              plugins: isProduction ? [] : ['react-refresh/babel']
            }
          }
        },

        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            'css-loader'
          ]
        },

        {
          test: /\.(png|jpe?g|gif|svg)$/i,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              maxSize: 8 * 1024
            }
          }
        }
      ]
    },

    // 插件
    plugins: [
      new CleanWebpackPlugin(),

      new HtmlWebpackPlugin({
        template: './public/index.html'
      }),

      ...(isProduction ? [
        new MiniCssExtractPlugin({
          filename: 'css/[name].[contenthash].css'
        })
      ] : [])
    ],

    // 解析配置
    resolve: {
      extensions: ['.js', '.jsx', '.json'],
      alias: {
        '@': path.resolve(__dirname, 'src')
      }
    },

    // 优化配置
    optimization: {
      splitChunks: {
        chunks: 'all'
      },

      runtimeChunk: {
        name: 'runtime'
      }
    }
  };
};

Loader开发实战

Loader是Webpack的核心概念之一,它用于对模块的源代码进行转换。Loader可以将任何类型的文件转换为Webpack能够处理的有效模块。

1. Loader基础

Loader本质是一个函数,接收源文件内容,返回转换后的内容。

javascript 复制代码
// 基本Loader结构
module.exports = function(source, sourceMap) {
  // source: 源文件内容
  // sourceMap: 源码映射(可选)

  // 转换逻辑
  const result = transform(source);

  // 如果有源码映射,需要返回它
  if (sourceMap) {
    return result + '\n//# sourceMappingURL=' + sourceMap;
  }

  return result;
};

2. 简单示例:日志Loader

创建一个在控制台输出文件路径的Loader。

javascript 复制代码
// loaders/logger-loader.js
const path = require('path');

module.exports = function(source) {
  // 获取文件信息
  const filePath = this.resourcePath;
  const fileName = path.basename(filePath);

  console.log(`[Logger Loader] Processing file: ${fileName}`);
  console.log(`[Logger Loader] Path: ${filePath}`);

  // 返回原内容
  return source;
};

使用方式:

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/logger-loader.js')
          }
        ]
      }
    ]
  }
};

3. 异步Loader

javascript 复制代码
// loaders/async-loader.js
const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const callback = this.async();

  // 异步处理
  const options = getOptions(this);

  setTimeout(() => {
    // 处理结果
    const result = source.replace(/console\.log/g, '// console.log');
    callback(null, result);
  }, 100);
};

4. 实战案例:自定义Markdown Loader

将Markdown文件转换为HTML。

javascript 复制代码
// loaders/markdown-loader.js
const marked = require('marked');
const hljs = require('highlight.js');

module.exports = function(source) {
  // 配置marked
  marked.setOptions({
    highlight: function(code, lang) {
      if (lang && hljs.getLanguage(lang)) {
        return hljs.highlight(code, { language: lang }).value;
      }
      return hljs.highlightAuto(code).value;
    },
    gfm: true,
    breaks: true,
    headerIds: true,
    mangle: false
  });

  // 转换为HTML
  const html = marked.parse(source);

  // 返回模块代码
  return `
    export default \`${html}\`;
  `;
};

使用:

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.md$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/markdown-loader.js')
          }
        ]
      }
    ]
  }
};

// 在代码中使用
import content from './readme.md';

console.log(content); // HTML字符串

5. 实战案例:国际化Loader

自动提取字符串进行国际化。

javascript 复制代码
// loaders/i18n-loader.js
const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const options = getOptions(this);
  const locale = options.locale || 'zh';

  // 正则匹配 t('key') 格式
  const regex = /t\(['"](.+?)['"]\)/g;

  let result = source;
  const keys = [];

  // 提取所有key
  let match;
  while ((match = regex.exec(source)) !== null) {
    keys.push(match[1]);
  }

  // 替换为对应的翻译
  result = source.replace(regex, (match, key) => {
    const translations = {
      zh: {
        'hello': '你好',
        'welcome': '欢迎'
      },
      en: {
        'hello': 'Hello',
        'welcome': 'Welcome'
      }
    };

    return translations[locale][key] || key;
  });

  return result;
};

6. 实战案例:CSS变量注入Loader

javascript 复制代码
// loaders/css-vars-loader.js
const { getOptions } = require('loader-utils');

module.exports = function(source) {
  const options = getOptions(this);
  const vars = options.vars || {};

  // 替换CSS变量
  let result = source;

  Object.keys(vars).forEach(key => {
    const regex = new RegExp(`var\\(--${key}\\)`, 'g');
    result = result.replace(regex, vars[key]);
  });

  return result;
};

使用:

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: path.resolve(__dirname, 'loaders/css-vars-loader.js'),
            options: {
              vars: {
                'primary-color': '#007bff',
                'font-size-base': '14px'
              }
            }
          }
        ]
      }
    ]
  }
};

7. 完整Loader开发指南

javascript 复制代码
// loaders/advanced-loader.js
const { getOptions, stringifyRequest } = require('loader-utils');
const path = require('path');

module.exports = function(source, sourceMap) {
  // 异步模式
  const callback = this.async();

  try {
    // 获取配置
    const options = getOptions(this) || {};

    // Loader上下文
    console.log('Loader Context:');
    console.log('- Resource Path:', this.resourcePath);
    console.log('- Resource Query:', this.resourceQuery);
    console.log('- Context Directory:', this.context);
    console.log('- Current Loader:', this.loaders[this.loaderIndex]);

    // 源文件信息
    console.log('Source Info:');
    console.log('- Source Length:', source.length);
    console.log('- Has Source Map:', !!sourceMap);

    // 转换逻辑
    let result = source;

    // 示例:添加头部注释
    if (options.addHeader) {
      result = `// Compiled by Webpack\n${result}`;
    }

    // 示例:自定义处理
    if (options.replace) {
      result = result.replace(
        new RegExp(options.replace.search, 'g'),
        options.replace.replace
      );
    }

    // 示例:添加源码映射
    if (options.generateSourceMap && sourceMap) {
      const consumer = new sourceMap.SourceMapConsumer(sourceMap);
      consumer.then(consumerInstance => {
        // 修改源码映射
        consumerInstance._absolutePosition = true;
        callback(null, result, sourceMap);
      });
    } else {
      callback(null, result);
    }

  } catch (error) {
    callback(error);
  }
};

8. Loader工具函数

javascript 复制代码
// loaders/utils.js
const { getOptions } = require('loader-utils');
const schemaUtils = require('schema-utils');

/**
 * 获取Loader配置
 */
function getLoaderOptions(loaderName, schema) {
  return function() {
    const options = getOptions(this);

    if (schema) {
      schemaUtils.validate(schema, options, {
        name: loaderName,
        baseDataPath: 'options'
      });
    }

    return options;
  };
}

/**
 * 路径解析
 */
function absolutify(context, request) {
  const requestSplit = request.split('!');
  const result = requestSplit.pop();
  const prefix = requestSplit.length ? requestSplit.join('!') + '!' : '';
  return prefix + path.posix.resolve(context, result);
}

/**
 * 相对路径转换
 */
function relative(context, absolutePath) {
  if (!path.isAbsolute(absolutePath)) {
    return absolutePath;
  }

  return path.relative(context, absolutePath).replace(/\\/g, '/');
}

module.exports = {
  getLoaderOptions,
  absolutify,
  relative
};

9. Loader测试

javascript 复制代码
// loaders/__tests__/markdown-loader.test.js
const path = require('path');
const { runLoader } = require('webpack');

describe('Markdown Loader', () => {
  test('should convert markdown to HTML', (done) => {
    const loaderPath = path.resolve(__dirname, '../markdown-loader.js');

    runLoader(
      {
        resource: path.resolve(__dirname, 'fixtures/test.md'),
        resourcePath: path.resolve(__dirname, 'fixtures/test.md'),
        context: {},
        loaders: [loaderPath],
        loaderIndex: 0,
        value: '# Hello World',
        options: {}
      },
      (err, result, sourceMap) => {
        if (err) {
          return done(err);
        }

        expect(result).toContain('<h1>Hello World</h1>');
        done();
      }
    );
  });
});

Plugin开发实战

Plugin是Webpack的核心特性,它扩展了Webpack的功能。Plugin可以在Webpack的整个生命周期中钩入,执行各种任务。

1. Plugin基础

Plugin是一个类,包含apply方法。

javascript 复制代码
// plugins/basic-plugin.js
class BasicPlugin {
  // 构造函数,接收配置
  constructor(options = {}) {
    this.options = options;
  }

  // apply方法在Webpack compiler安装此plugin时调用
  apply(compiler) {
    // 钩子列表:
    // https://webpack.js.org/api/compiler-hooks/

    // 示例:在emit钩子中执行
    compiler.hooks.emit.tap('BasicPlugin', (compilation) => {
      console.log('BasicPlugin: emit hook called');
      console.log('Number of assets:', Object.keys(compilation.assets).length);
    });
  }
}

module.exports = BasicPlugin;

2. 使用Plugin

javascript 复制代码
// webpack.config.js
const BasicPlugin = require('./plugins/basic-plugin');

module.exports = {
  plugins: [
    new BasicPlugin({
      // 配置项
    })
  ]
};

3. 实战案例:清理输出目录Plugin

javascript 复制代码
// plugins/clean-output-plugin.js
const fs = require('fs');
const path = require('path');

class CleanOutputPlugin {
  constructor(options = {}) {
    this.options = {
      exclude: [],
      ...options
    };
  }

  apply(compiler) {
    const outputPath = compiler.options.output.path;

    compiler.hooks.beforeRun.tap('CleanOutputPlugin', (compiler) => {
      if (fs.existsSync(outputPath)) {
        console.log(`Cleaning output directory: ${outputPath}`);

        const files = fs.readdirSync(outputPath);

        files.forEach(file => {
          const filePath = path.join(outputPath, file);

          // 跳过排除的文件
          if (this.options.exclude.includes(file)) {
            return;
          }

          if (fs.statSync(filePath).isDirectory()) {
            fs.rmSync(filePath, { recursive: true, force: true });
          } else {
            fs.unlinkSync(filePath);
          }
        });
      }
    });
  }
}

module.exports = CleanOutputPlugin;

4. 实战案例:生成文件清单Plugin

javascript 复制代码
// plugins/manifest-plugin.js
const fs = require('fs');
const path = require('path');

class ManifestPlugin {
  constructor(options = {}) {
    this.options = {
      filename: 'manifest.json',
      ...options
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tap('ManifestPlugin', (compilation) => {
      const manifest = {
        name: compiler.options.output.filename,
        timestamp: new Date().toISOString(),
        assets: [],
        chunks: {}
      };

      // 收集资源文件信息
      Object.keys(compilation.assets).forEach(assetName => {
        const asset = compilation.assets[assetName];
        const info = asset.info || {};

        manifest.assets.push({
          name: assetName,
          size: asset.size(),
          isCompressed: !!info.compressed,
          sourceMap: !!info.sourceMap
        });
      });

      // 收集chunk信息
      Object.keys(compilation.chunks).forEach(chunkId => {
        const chunk = compilation.chunks[chunkId];
        manifest.chunks[chunkId] = {
          files: Array.from(chunk.files),
          hash: chunk.renderedHash,
          size: chunk.size()
        };
      });

      // 生成清单文件
      const manifestJson = JSON.stringify(manifest, null, 2);
      compilation.assets[this.options.filename] = {
        source() {
          return manifestJson;
        },
        size() {
          return manifestJson.length;
        }
      };
    });
  }
}

module.exports = ManifestPlugin;

5. 实战案例:进度显示Plugin

javascript 复制代码
// plugins/progress-plugin.js
class ProgressPlugin {
  constructor(options = {}) {
    this.options = {
      activeModules: true,
      entries: true,
      profiles: true,
      modules: true,
      dependencies: true,
      ...options
    };

    this.lastModuleCount = 0;
    this.startTime = Date.now();
  }

  apply(compiler) {
 this.options;

    // 编译开始
    compiler    const options =.hooks.beforeCompile.tap('ProgressPlugin', () => {
      this.log('Starting compilation...');
    });

    // 模块处理
    compiler.hooks.normalModuleFactory.tap('ProgressPlugin', (factory) => {
      factory.hooks.beforeResolve.tap('ProgressPlugin', (data) => {
        if (options.dependencies) {
          this.log(`Resolving module: ${data.request}`);
        }
      });
    });

    // 模块完成
    compiler.hooks.compilation.tap('ProgressPlugin', (compilation) => {
      compilation.hooks.buildModule.tap('ProgressPlugin', (module) => {
        if (options.modules) {
          this.log(`Building module: ${module.resource}`);
        }
      });

      // 进度统计
      compilation.hooks.afterOptimizeModuleIds.tap('ProgressPlugin', (modules) => {
        this.updateProgress(modules.length, 'modules');
      });
    });

    // 打包完成
    compiler.hooks.afterEmit.tap('ProgressPlugin', () => {
      const duration = Date.now() - this.startTime;
      this.log(`Compilation completed in ${duration}ms`);
    });
  }

  log(message) {
    console.log(`[Progress] ${message}`);
  }

  updateProgress(current, total, label) {
    const percentage = Math.round((current / total) * 100);
    const bar = '='.repeat(percentage / 2) + ' '.repeat(50 - percentage / 2);

    process.stdout.write(`\r[Progress] [${bar}] ${percentage}% ${label || ''}`);

    if (percentage === 100) {
      console.log('');
    }
  }
}

module.exports = ProgressPlugin;

6. 实战案例:环境变量注入Plugin

javascript 复制代码
// plugins/env-inject-plugin.js
const DefinePlugin = require('webpack/lib/DefinePlugin');

class EnvInjectPlugin {
  constructor(options = {}) {
    this.options = {
      // 从环境变量获取
      fromEnv: process.env.NODE_ENV,
      // 自定义变量
      custom: {},
      // 条件注入
      conditional: {},
      ...options
    };
  }

  apply(compiler) {
    const definitions = {};

    // 注入环境变量
    Object.keys(process.env).forEach(key => {
      definitions[`process.env.${key}`] = JSON.stringify(process.env[key]);
    });

    // 注入自定义变量
    Object.keys(this.options.custom).forEach(key => {
      definitions[key] = JSON.stringify(this.options.custom[key]);
    });

    // 条件注入
    Object.keys(this.options.conditional).forEach(key => {
      const condition = this.options.conditional[key];
      if (condition.enabled) {
        definitions[key] = JSON.stringify(condition.value);
      }
    });

    // 使用DefinePlugin
    new DefinePlugin(definitions).apply(compiler);
  }
}

module.exports = EnvInjectPlugin;

使用:

javascript 复制代码
// webpack.config.js
const EnvInjectPlugin = require('./plugins/env-inject-plugin');

module.exports = {
  plugins: [
    new EnvInjectPlugin({
      fromEnv: process.env.NODE_ENV,
      custom: {
        'process.env.APP_NAME': '"My App"',
        'process.env.APP_VERSION': '"1.0.0"'
      },
      conditional: {
        'process.env.DEBUG': {
          enabled: process.env.NODE_ENV === 'development',
          value: true
        }
      }
    })
  ]
};

7. 实战案例:自动复制文件Plugin

javascript 复制代码
// plugins/copy-files-plugin.js
const fs = require('fs');
const path = require('path');

class CopyFilesPlugin {
  constructor(options = {}) {
    this.options = {
      patterns: [],
      ...options
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tap('CopyFilesPlugin', async (compilation) => {
      for (const pattern of this.options.patterns) {
        const { from, to, flatten = false } = pattern;

        const srcPath = path.resolve(from);
        const destPath = path.resolve(to);

        if (!fs.existsSync(srcPath)) {
          console.warn(`Source file not found: ${srcPath}`);
          continue;
        }

        const fileName = flatten ? path.basename(srcPath) : srcPath.replace(path.dirname(srcPath), '');
        const finalDest = path.join(destPath, fileName);

        // 复制文件
        const content = fs.readFileSync(srcPath);

        compilation.assets[path.relative(compiler.options.output.path, finalDest)] = {
          source() {
            return content;
          },
          size() {
            return content.length;
          }
        };

        console.log(`Copied: ${srcPath} -> ${finalDest}`);
      }
    });
  }
}

module.exports = CopyFilesPlugin;

8. 实战案例:Bundle分析Plugin

javascript 复制代码
// plugins/bundle-analyzer-plugin.js
class BundleAnalyzerPlugin {
  constructor(options = {}) {
    this.options = {
      analyzerMode: 'static',
      analyzerPort: 8888,
      openAnalyzer: true,
      ...options
    };
  }

  apply(compiler) {
    compiler.hooks.done.tap('BundleAnalyzerPlugin', (stats) => {
      this.analyzeBundle(stats);
    });
  }

  analyzeBundle(stats) {
    const compilation = stats.compilation;

    console.log('\n=== Bundle Analysis ===\n');

    // 总览信息
    console.log('Bundle Size:');
    this.printSize('Total', compilation.hash);

    // 各chunk大小
    console.log('\nChunks:');
    compilation.chunks.forEach(chunk => {
      const files = Array.from(chunk.files);
      files.forEach(file => {
        const asset = compilation.assets[file];
        console.log(`  - ${chunk.name}: ${this.formatSize(asset.size())}`);
      });
    });

    // 模块统计
    console.log('\nTop 10 Modules by Size:');
    const modules = Array.from(compilation.modules);
    const sortedModules = modules
      .filter(module => module.size)
      .sort((a, b) => b.size() - a.size())
      .slice(0, 10);

    sortedModules.forEach(module => {
      console.log(`  - ${module.name}: ${this.formatSize(module.size())}`);
    });

    // 依赖统计
    console.log('\nDependencies:');
    console.log(`  - Total modules: ${modules.length}`);
    console.log(`  - Total chunks: ${compilation.chunks.length}`);

    // 警告和错误
    if (stats.hasErrors()) {
      console.log('\n❌ Errors:', stats.compilation.errors.length);
    }
    if (stats.hasWarnings()) {
      console.log('\n⚠️  Warnings:', stats.compilation.warnings.length);
    }
  }

  printSize(label, value) {
    console.log(`  ${label}: ${this.formatSize(value)}`);
  }

  formatSize(bytes) {
    if (bytes < 1024) {
      return `${bytes} B`;
    } else if (bytes < 1024 * 1024) {
      return `${(bytes / 1024).toFixed(2)} KB`;
    } else {
      return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    }
  }
}

module.exports = BundleAnalyzerPlugin;

9. 高级Plugin开发

javascript 复制代码
// plugins/advanced-plugin.js
class AdvancedPlugin {
  constructor(options = {}) {
    this.options = options;
    this.modules = new Map();
  }

  apply(compiler) {
    const pluginName = 'AdvancedPlugin';

    // 访问NormalModuleFactory
    compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
      nmf.hooks.beforeResolve.tap(pluginName, (resolveData) => {
        console.log(`Resolving module: ${resolveData.request}`);
        return resolveData;
      });

      nmf.hooks.afterResolve.tap(pluginName, (resolveData) => {
        console.log(`Resolved module: ${resolveData.resource}`);
        return resolveData;
      });
    });

    // 访问Compilation
    compiler.hooks.compilation.tap(pluginName, (compilation, params) => {
      // 访问ModuleFactory
      compilation.hooks.buildModule.tap(pluginName, (module) => {
        this.trackModule(module);
      });

      compilation.hooks.rebuildModule.tap(pluginName, (module) => {
        this.trackModule(module);
      });

      compilation.hooks.succeedModule.tap(pluginName, (module) => {
        this.onModuleSucceed(module);
      });

      compilation.hooks.failedModule.tap(pluginName, (module) => {
        this.onModuleFailed(module);
      });
    });

    // 钩子类型示例
    compiler.hooks.entryOption.tap(pluginName, (context, entry) => {
      console.log('Entry option:', entry);
    });

    compiler.hooks.afterPlugins.tap(pluginName, () => {
      console.log('After plugins loaded');
    });

    compiler.hooks.beforeRun.tap(pluginName, (compiler) => {
      console.log('Before run');
    });

    compiler.hooks.run.tap(pluginName, (compiler) => {
      console.log('Run');
    });

    compiler.hooks.watchRun.tap(pluginName, (compiler) => {
      console.log('Watch run');
    });

    compiler.hooks.make.tap(pluginName, (compilation) => {
      console.log('Make');
    });

    compiler.hooks.afterCompile.tap(pluginName, (compilation) => {
      console.log('After compile');
    });

    compiler.hooks.emit.tap(pluginName, (compilation) => {
      console.log('Emit');
      this.onEmit(compilation);
    });

    compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
      console.log('After emit');
    });

    compiler.hooks.done.tap(pluginName, (stats) => {
      console.log('Done');
      this.onDone(stats);
    });

    compiler.hooks.failed.tap(pluginName, (error) => {
      console.log('Failed:', error);
    });
  }

  trackModule(module) {
    this.modules.set(module.identifier(), {
      startTime: Date.now(),
      size: module.size()
    });
  }

  onModuleSucceed(module) {
    const moduleInfo = this.modules.get(module.identifier());
    if (moduleInfo) {
      moduleInfo.endTime = Date.now();
      moduleInfo.duration = moduleInfo.endTime - moduleInfo.startTime;
    }
  }

  onModuleFailed(module) {
    const moduleInfo = this.modules.get(module.identifier());
    if (moduleInfo) {
      moduleInfo.failed = true;
    }
  }

  onEmit(compilation) {
    // 添加自定义资源
    compilation.assets['custom-asset.txt'] = {
      source() {
        return 'This is a custom asset';
      },
      size() {
        return 'This is a custom asset'.length;
      }
    };
  }

  onDone(stats) {
    console.log('=== Build Stats ===');
    console.log('Time:', stats.endTime - stats.startTime);
    console.log('Modules:', this.modules.size);
  }
}

module.exports = AdvancedPlugin;

10. 插件间通信

javascript 复制代码
// plugins/plugin-communication.js
// 插件1:收集模块信息
class ModuleCollectorPlugin {
  constructor() {
    this.modules = [];
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('ModuleCollectorPlugin', (compilation) => {
      compilation.hooks.succeedModule.tap('ModuleCollectorPlugin', (module) => {
        this.modules.push({
          identifier: module.identifier(),
          name: module.name,
          size: module.size()
        });

        // 通过compilation实例传递数据
        compilation.pluginInfo = compilation.pluginInfo || {};
        compilation.pluginInfo.modules = this.modules;
      });
    });
  }
}

// 插件2:使用模块信息
class ModuleReporterPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('ModuleReporterPlugin', (stats) => {
      const compilation = stats.compilation;
      const modules = compilation.pluginInfo?.modules || [];

      console.log('=== Module Report ===');
      console.log(`Total modules: ${modules.length}`);

      if (modules.length > 0) {
        console.log('Largest module:', modules.reduce((a, b) => a.size > b.size ? a : b).name);
      }
    });
  }
}

module.exports = {
  ModuleCollectorPlugin,
  ModuleReporterPlugin
};

高级配置与优化

1. 代码分割详解

javascript 复制代码
module.exports = {
  optimization: {
    splitChunks: {
      // 分离策略
      chunks: 'all', // 'initial', 'async', 'all'

      // 缓存组
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },

        // React相关
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20
        },

        // 大型库
        'large-libs': {
          test: /[\\/]node_modules[\\/](lodash|axios|moment)[\\/]/,
          name: 'large-libs',
          chunks: 'all',
          minSize: 20000,
          maxSize: 244000,
          priority: 5
        },

        // 公共模块
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 0,
          reuseExistingChunk: true
        }
      },

      // 分割条件
      maxInitialRequests: Infinity,
      minSize: 20000,
      maxSize: 244000,
      minChunks: 1,

      // 自动命名
      automaticNameDelimiter: '~',

      // 最大异步请求数
      maxAsyncRequests: 5
    },

    // 运行时提取
    runtimeChunk: {
      name: 'runtime'
    },

    // 模块标识符
    moduleIds: 'deterministic',

    // Chunk标识符
    chunkIds: 'deterministic',

    // 压缩配置
    minimizer: [
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          },
          mangle: true,
          format: {
            comments: false
          }
        }
      })
    ]
  }
};

2. 缓存配置

javascript 复制代码
module.exports = {
  // 内存缓存
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    },
    cacheDirectory: path.resolve(__dirname, '.cache'),
    cacheLocation: path.resolve(__dirname, '.cache/webpack'),
    name: 'my-app-cache',
    compression: 'gzip'
  },

  // 持久化缓存
  infrastructureLogging: {
    level: 'info'
  },

  // 缓存组
  module: {
    unknownContextModules: false,
    exprContextRecursive: true,
    wrappedContextCritical: true
  }
};

3. 高级Loader配置

javascript 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        oneOf: [
          {
            resourceQuery: '?inline',
            use: 'url-loader'
          },
          {
            test: /\.svg$/,
            use: [
              'file-loader',
              {
                loader: 'svgo-loader',
                options: {
                  plugins: [
                    {
                      name: 'removeViewBox',
                      active: false
                    },
                    {
                      name: 'removeEmptyAttrs',
                      active: false
                    }
                  ]
                }
              }
            ]
          }
        ]
      },

      {
        test: /\.module\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]___[hash:base64:5]'
            }
          }
        ]
      }
    ]
  }
};

4. 多页面应用配置

javascript 复制代码
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: {
    home: './src/pages/home/index.js',
    about: './src/pages/about/index.js',
    contact: './src/pages/contact/index.js'
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash].js',
    clean: true
  },

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css'
    }),

    new HtmlWebpackPlugin({
      template: './src/pages/home/index.html',
      filename: 'home.html',
      chunks: ['home', 'runtime', 'vendors']
    }),

    new HtmlWebpackPlugin({
      template: './src/pages/about/index.html',
      filename: 'about.html',
      chunks: ['about', 'runtime', 'vendors']
    }),

    new HtmlWebpackPlugin({
      template: './src/pages/contact/index.html',
      filename: 'contact.html',
      chunks: ['contact', 'runtime', 'vendors']
    })
  ],

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        }
      }
    },

    runtimeChunk: 'single'
  }
};

5. TypeScript支持

javascript 复制代码
module.exports = {
  entry: './src/index.ts',

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true, // 加快编译速度
              getCustomTransformers: () => ({
                before: [require.resolve('typescript-plugin-styled-components').default]
              })
            }
          }
        ]
      }
    ]
  },

  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
};

6. React支持

javascript 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: {
                  browsers: ['> 1%', 'last 2 versions']
                }
              }],
              ['@babel/preset-react', {
                runtime: 'automatic' // 自动导入React
              }]
            ],
            plugins: [
              '@babel/plugin-transform-runtime',
              '@babel/plugin-proposal-class-properties'
            ]
          }
        }
      }
    ]
  }
};

性能优化策略

1. 构建速度优化

javascript 复制代码
module.exports = {
  // 1. 使用多进程
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1
            }
          },
          'babel-loader'
        ]
      }
    ]
  },

  // 2. 缓存
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.cache'),
    buildDependencies: {
      config: [__filename]
    }
  },

  // 3. 并行压缩
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    ]
  },

  // 4. 排除不必要的处理
  module: {
    noParse: /^(jquery|chart\.js)$/
  },

  // 5. 排除node_modules
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  }
};

2. 包体积优化

javascript 复制代码
module.exports = {
  optimization: {
    // 1. Tree Shaking
    usedExports: true,
    sideEffects: false,

    // 2. 代码分割
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },

        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },

    // 3. 压缩
    minimize: true,

    // 4. 移除未使用代码
    removeEmptyChunks: true,
    mergeDuplicateChunks: true
  },

  // 5. 分析包大小
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false
    })
  ]
};

3. 运行时优化

javascript 复制代码
module.exports = {
  optimization: {
    // 提取公共运行时
    runtimeChunk: 'single',

    // 模块标识符
    moduleIds: 'deterministic',

    // Chunk标识符
    chunkIds: 'deterministic',

    // 命名
    namedChunks: 'namedModules',
    namedModules: true
  }
};

多场景实战配置

1. Vue项目配置

javascript 复制代码
// webpack.vue.config.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/main.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash].js',
    clean: true
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },

      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },

      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      },

      {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin(),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],

  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
};

2. 小程序项目配置

javascript 复制代码
// webpack.miniprogram.config.js
const path = require('path');
const MiniProgramPlugin = require('webpack-miniprogram-plugin');

module.exports = {
  entry: './src/app.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    clean: true
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          },
          'miniprogram-babel-loader'
        ]
      },

      {
        test: /\.wxml$/,
        use: 'wxml-loader'
      },

      {
        test: /\.wxss$/,
        use: [
          {
            loader: 'wxss-loader',
            options: {
              ex: 'rpx'
            }
          }
        ]
      },

      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'images/[name].[ext]'
            }
          }
        ]
      }
    ]
  },

  plugins: [
    new MiniProgramPlugin({
      appJSON: './app.json',
      pageJSON: './pages/**/*.json'
    })
  ]
};

3. Electron应用配置

javascript 复制代码
// webpack.electron.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const config = {
  main: {
    entry: './electron/main.js',
    output: {
      path: path.resolve(__dirname, 'dist/main'),
      filename: 'main.js'
    },
    target: 'electron-main',
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        }
      ]
    }
  },

  renderer: {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist/renderer'),
      filename: 'renderer.js'
    },
    target: 'electron-renderer',
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env', '@babel/preset-react']
            }
          }
        },

        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        }
      ]
    },

    plugins: [
      new HtmlWebpackPlugin({
        template: './src/index.html'
      })
    ]
  }
};

module.exports = config;

原理与源码分析

1. Webpack执行流程

javascript 复制代码
// Webpack执行步骤(简化版)

// 1. 初始化配置
const compiler = new Compiler(options);
loadConfig(options, compiler);

// 2. 加载插件
loadPlugins(pluginFactory, compiler);

// 3. 编译
compiler.run((err, stats) => {
  if (err) {
    console.error(err);
    return;
  }

  if (stats.hasErrors()) {
    console.error(stats.toString({
      colors: true,
      chunks: false
    }));
    return;
  }

  console.log(stats.toString({
    chunks: false,
    colors: true
  }));
});

// 编译流程
compiler.compile() {
  // 3.1 创建Compilation
  const compilation = new Compilation(compiler);

  // 3.2 构建依赖图
  compilation.buildModule();

  // 3.3 优化
  compilation.optimize();

  // 3.4 生成输出
  compilation.createChunkAssets();
}

2. Tapable钩子系统

javascript 复制代码
// Tapable是Webpack的核心,用于实现钩子系统
const { Tapable } = require('tapable');

// Hook类示例
class SyncHook {
  constructor() {
    this.taps = [];
  }

  tap(name, fn) {
    this.taps.push({ name, fn });
  }

  call(...args) {
    this.taps.forEach(tap => {
      tap.fn(...args);
    });
  }
}

// 使用
const hook = new SyncHook();

hook.tap('plugin1', (arg) => {
  console.log('Plugin 1:', arg);
});

hook.tap('plugin2', (arg) => {
  console.log('Plugin 2:', arg);
});

hook.call('test');
// 输出:
// Plugin 1: test
// Plugin 2: test

// 异步钩子
class AsyncSeriesHook {
  constructor() {
    this.taps = [];
  }

  tapAsync(name, fn) {
    this.taps.push({ name, fn });
  }

  callAsync(...args, callback) {
    let index = 0;

    const next = () => {
      if (index >= this.taps.length) {
        callback();
        return;
      }

      const tap = this.taps[index++];
      tap.fn(...args, next);
    };

    next();
  }
}

3. 自定义Compiler

javascript 复制代码
const { Compiler } = require('webpack');
const { Tapable } = require('tapable');

class CustomCompiler extends Compiler {
  constructor() {
    super();

    // 自定义钩子
    this.hooks = Object.assign(
      Object.create(Tapable.prototype),
      Compiler.prototype.hooks,
      {
        customHook: new SyncHook(['params'])
      }
    );
  }

  run(callback) {
    // 自定义编译逻辑
    const compilation = this.newCompilation();

    // 调用自定义钩子
    this.hooks.customHook.call(compilation.params);

    // 完成编译
    this.compile();

    callback(null, {
      compilation,
      stats: this.createStats()
    });
  }
}

常见问题与解决方案

1. 构建速度慢

问题:构建时间过长,影响开发效率

解决方案

javascript 复制代码
// 方案1:使用缓存
module.exports = {
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.cache'),
    buildDependencies: {
      config: [__filename]
    }
  }
};

// 方案2:并行处理
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1
            }
          },
          'babel-loader'
        ]
      }
    ]
  }
};

// 方案3:exclude优化
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除大型库
        use: 'babel-loader'
      }
    ]
  }
};

// 方案4:noParse
module.exports = {
  module: {
    noParse: /jquery|chart\.js/ // 不解析某些库
  }
};

2. 内存溢出

问题:构建时内存溢出

解决方案

javascript 复制代码
// 方案1:增加Node.js内存限制
// package.json
{
  "scripts": {
    "build": "node --max_old_space_size=4096 node_modules/.bin/webpack --mode production"
  }
}

// 方案2:减少并行度
module.exports = {
  parallelism: 1 // 限制并行处理的chunk数量
};

3. 热更新失效

问题:热更新不生效

解决方案

javascript 复制代码
// 检查HMR配置
module.exports = {
  devServer: {
    hot: true,
    hotOnly: false
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

// 检查Loader支持
// babel-loader需要react-refresh
{
  loader: 'babel-loader',
  options: {
    plugins: ['react-refresh/babel']
  }
}

4. 动态导入错误

问题:import()语法报错

解决方案

javascript 复制代码
// 确保有正确的分隔符
// 错误
import('./pages/' + pageName)

// 正确
import('./pages/' + pageName + '.js')

// 或使用require.context
const files = require.context('./pages', false, /\.js$/);
const modules = {};
files.keys().forEach(key => {
  modules[key.replace('./', '')] = files(key);
});

5. CSS重复引入

问题:CSS被多次打包

解决方案

javascript 复制代码
// 方案1:使用ExtractTextPlugin
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: 'css-loader'
        })
      }
    ]
  }
};

// 方案2:确保模块ID正确
module.exports = {
  optimization: {
    namedModules: true
  }
};

6. Source Map错误

问题:源码映射不正确

解决方案

javascript 复制代码
// 开发环境
module.exports = {
  devtool: 'eval-cheap-module-source-map'
};

// 生产环境
module.exports = {
  devtool: 'source-map'
};

// 禁用
module.exports = {
  devtool: false
};

最佳实践总结

1. 配置建议

javascript 复制代码
// ✅ 好的做法
module.exports = {
  // 使用函数配置,支持环境变量
  mode: (process.env.NODE_ENV || 'development'),

  // 使用路径别名
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components')
    }
  },

  // 清理输出目录
  output: {
    clean: true
  },

  // 使用contenthash缓存
  output: {
    filename: '[name].[contenthash].js'
  },

  // 代码分割
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

// ❌ 避免的做法
module.exports = {
  // 硬编码路径
  entry: '/absolute/path/to/src/index.js',

  // 忘记清理
  output: {
    clean: false
  },

  // 禁用缓存优化
  optimization: {
    minimize: false
  }
};

2. Loader使用建议

javascript 复制代码
// ✅ 使用Loader配置对象
{
  loader: 'babel-loader',
  options: {
    presets: ['@babel/preset-env']
  }
}

// ✅ 排除node_modules
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: 'babel-loader'
}

// ✅ 使用绝对路径
{
  loader: path.resolve(__dirname, 'loaders/custom-loader.js')
}

3. Plugin使用建议

javascript 复制代码
// ✅ 条件性加载Plugin
const plugins = [
  new HtmlWebpackPlugin()
];

if (process.env.ANALYZE) {
  plugins.push(new BundleAnalyzerPlugin());
}

// ✅ 使用实例缓存
const instanceName = new SomePlugin({
  instance: 'production'
});

4. 性能优化建议

javascript 复制代码
// ✅ 缓存配置
module.exports = {
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.cache')
  }
};

// ✅ 并行处理
module.exports = {
  parallelism: 10
};

// ✅ 合理拆分
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

5. 调试建议

javascript 复制代码
// ✅ 使用Stats查看构建信息
{
  "scripts": {
    "build": "webpack --mode production --stats verbose"
  }
}

// ✅ 分析包大小
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

plugins: [
  new BundleAnalyzerPlugin({
    analyzerMode: 'static',
    openAnalyzer: false
  })
]

// ✅ 查看模块依赖
{
  "scripts": {
    "analyze": "npm run build && npx webpack-bundle-analyzer dist/main.js"
  }
}

6. 项目结构建议

复制代码
project/
├── public/
│   └── index.html
├── src/
│   ├── index.js
│   ├── components/
│   ├── utils/
│   └── assets/
├── webpack.config.js          # 通用配置
├── webpack.dev.js            # 开发环境
├── webpack.prod.js           # 生产环境
└── package.json

总结

本文全面深入地讲解了Webpack的配置与实战,包括:

核心内容回顾

  1. 基础概念:Entry、Output、Module、Resolve、Plugin、Mode
  2. 基础配置:开发/生产环境配置详解
  3. Loader开发:从基础到高级,5个实战案例
  4. Plugin开发:从简单到复杂,8个实战案例
  5. 高级配置:代码分割、缓存、多页面应用等
  6. 性能优化:构建速度与包体积优化策略
  7. 多场景实战:Vue、小程序、Electron等
  8. 原理分析:执行流程、钩子系统
  9. 问题解决:常见问题与最佳实践

关键要点

  • Loader是函数,用于转换模块源码
  • Plugin是类,用于扩展Webpack功能
  • 合理配置代码分割提升性能
  • 缓存是提升构建速度的关键
  • 根据项目选择合适的配置策略
相关推荐
南囝coding1 天前
发现一个宝藏图片对比工具!速度比 ImageMagick 快 6 倍,还是开源的
前端
前端小黑屋1 天前
查看 Base64 编码的字体包对应的字符集
前端·css·字体
阿珊和她的猫1 天前
CommonJS:Node.js 的模块化基石
node.js·状态模式
每天吃饭的羊1 天前
媒体查询
开发语言·前端·javascript
XiaoYu20021 天前
第8章 Three.js入门
前端·javascript·three.js
这个一个非常哈1 天前
element之,自定义form的label
前端·javascript·vue.js
阿东在coding1 天前
Flutter 测试框架对比指南
前端
是李嘉图呀1 天前
npm推送包失败需要Two-factor权限认证问题解决
前端
自己记录_理解更深刻1 天前
本地完成「新建 GitHub 仓库 react-ts-demo → 关联本地 React+TS 项目 → 提交初始代码」的完整操作流程
前端