Webpack 深入学习指南

目录

  • [1. Webpack 基础概念](#1. Webpack 基础概念 "#1-webpack-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
  • [2. 模块化支持 (CJS/ESM)](#2. 模块化支持 (CJS/ESM) "#2-%E6%A8%A1%E5%9D%97%E5%8C%96%E6%94%AF%E6%8C%81-cjsesm")
  • [3. 自定义 Loader 开发](#3. 自定义 Loader 开发 "#3-%E8%87%AA%E5%AE%9A%E4%B9%89-loader-%E5%BC%80%E5%8F%91")
  • [4. 自定义 Plugin 开发](#4. 自定义 Plugin 开发 "#4-%E8%87%AA%E5%AE%9A%E4%B9%89-plugin-%E5%BC%80%E5%8F%91")
  • [5. 实战案例](#5. 实战案例 "#5-%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B")
  • [6. 最佳实践](#6. 最佳实践 "#6-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")

1. Webpack 基础概念

1.1 什么是 Webpack

Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。

1.2 核心概念

  • Entry(入口): 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
  • Output(输出): 告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件
  • Loader(加载器): 让 webpack 能够去处理那些非 JavaScript 文件
  • Plugin(插件): 用于执行范围更广的任务,从打包优化和压缩,一直到重新定义环境中的变量
  • Mode(模式): 通过选择 development 或 production 之中的一个,来设置 mode 参数

1.3 基本配置示例

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

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [],
};

2. 模块化支持 (CJS/ESM)

2.1 模块化概述

现代 JavaScript 开发中,模块化是构建大型应用的基础。Webpack 支持两种主要的模块化方式:

  • CommonJS (CJS): Node.js 传统的模块化方式
  • ES Modules (ESM): ECMAScript 标准的模块化方式

2.2 CommonJS (CJS) 方式

2.2.1 基本语法

javascript 复制代码
// 导出模块
module.exports = {
  name: 'MyModule',
  version: '1.0.0',
  sayHello: function() {
    console.log('Hello from CJS module');
  }
};

// 或者导出单个函数
module.exports = function() {
  console.log('Hello from CJS function');
};

// 导出多个内容
exports.name = 'MyModule';
exports.version = '1.0.0';
exports.sayHello = function() {
  console.log('Hello from CJS exports');
};

2.2.2 导入模块

javascript 复制代码
// 导入整个模块
const myModule = require('./my-module');

// 导入特定属性
const { name, version } = require('./my-module');

// 导入函数
const myFunction = require('./my-function');

2.2.3 CJS Loader 示例

javascript 复制代码
// cjs-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  // 处理 CommonJS 模块
  let result = source;
  
  // 添加模块信息
  if (options.addModuleInfo) {
    result = `
// 模块信息 (由 cjs-loader 生成)
// 文件路径: ${this.resourcePath}
// 处理时间: ${new Date().toISOString()}

${result}
    `;
  }
  
  // 转换 require 为 import (可选)
  if (options.convertToESM) {
    result = result.replace(
      /const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
      'import $1 from "$2"'
    );
    
    result = result.replace(
      /module\.exports\s*=/g,
      'export default'
    );
  }
  
  return result;
};

2.3 ES Modules (ESM) 方式

2.3.1 基本语法

javascript 复制代码
// 默认导出
export default {
  name: 'MyModule',
  version: '1.0.0',
  sayHello: function() {
    console.log('Hello from ESM module');
  }
};

// 命名导出
export const name = 'MyModule';
export const version = '1.0.0';
export function sayHello() {
  console.log('Hello from ESM function');
}

// 混合导出
const defaultExport = {
  name: 'MyModule',
  version: '1.0.0'
};

export { defaultExport as default, name, version };

2.3.2 导入模块

javascript 复制代码
// 默认导入
import myModule from './my-module';

// 命名导入
import { name, version, sayHello } from './my-module';

// 混合导入
import myModule, { name, version } from './my-module';

// 重命名导入
import { name as moduleName } from './my-module';

// 整体导入
import * as myModule from './my-module';

2.3.3 ESM Loader 示例

javascript 复制代码
// esm-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  // 处理 ES Modules
  let result = source;
  
  // 添加模块信息
  if (options.addModuleInfo) {
    result = `
// 模块信息 (由 esm-loader 生成)
// 文件路径: ${this.resourcePath}
// 处理时间: ${new Date().toISOString()}

${result}
    `;
  }
  
  // 转换 import 为 require (可选)
  if (options.convertToCJS) {
    // 处理默认导入
    result = result.replace(
      /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
      'const $1 = require("$2")'
    );
    
    // 处理命名导入
    result = result.replace(
      /import\s*{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"]/g,
      (match, imports, modulePath) => {
        const importList = imports.split(',').map(imp => imp.trim());
        const destructure = importList.join(', ');
        return `const { ${destructure} } = require("${modulePath}")`;
      }
    );
    
    // 处理默认导出
    result = result.replace(
      /export\s+default\s+/g,
      'module.exports = '
    );
  }
  
  return result;
};

2.4 双模块化支持

2.4.1 package.json 配置

json 复制代码
{
  "name": "my-webpack-project",
  "version": "1.0.0",
  "main": "dist/index.cjs",
  "module": "dist/index.esm.js",
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs"
    }
  },
  "type": "module",
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "build:cjs": "webpack --config webpack.cjs.config.js",
    "build:esm": "webpack --config webpack.esm.config.js"
  }
}

2.4.2 双模块化 Loader

javascript 复制代码
// dual-module-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  const moduleType = options.moduleType || 'auto';
  
  // 自动检测模块类型
  if (moduleType === 'auto') {
    if (source.includes('import') || source.includes('export')) {
      return this.processESM(source, options);
    } else if (source.includes('require') || source.includes('module.exports')) {
      return this.processCJS(source, options);
    }
  }
  
  // 根据配置处理
  if (moduleType === 'esm') {
    return this.processESM(source, options);
  } else if (moduleType === 'cjs') {
    return this.processCJS(source, options);
  }
  
  return source;
};

// 处理 ESM 模块
module.exports.processESM = function(source, options) {
  let result = source;
  
  // 添加 ESM 特性
  if (options.addTreeShaking) {
    result = `
// Tree Shaking 优化标记
/*#__PURE__*/
${result}
    `;
  }
  
  return result;
};

// 处理 CJS 模块
module.exports.processCJS = function(source, options) {
  let result = source;
  
  // 添加 CJS 特性
  if (options.addStrictMode) {
    result = `'use strict';\n\n${result}`;
  }
  
  return result;
};

2.5 模块化转换工具

2.5.1 CJS 转 ESM Loader

javascript 复制代码
// cjs-to-esm-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  let result = source;
  
  // 转换 require 为 import
  result = result.replace(
    /const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
    'import $1 from "$2"'
  );
  
  result = result.replace(
    /var\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
    'import $1 from "$2"'
  );
  
  result = result.replace(
    /let\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
    'import $1 from "$2"'
  );
  
  // 转换 module.exports 为 export default
  result = result.replace(
    /module\.exports\s*=\s*/g,
    'export default '
  );
  
  // 转换 exports.xxx 为 export const
  result = result.replace(
    /exports\.(\w+)\s*=\s*/g,
    'export const $1 = '
  );
  
  // 添加严格模式
  if (options.addStrictMode) {
    result = `'use strict';\n\n${result}`;
  }
  
  return result;
};

2.5.2 ESM 转 CJS Loader

javascript 复制代码
// esm-to-cjs-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  let result = source;
  
  // 转换 import 为 require
  result = result.replace(
    /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
    'const $1 = require("$2")'
  );
  
  // 转换命名导入
  result = result.replace(
    /import\s*{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"]/g,
    (match, imports, modulePath) => {
      const importList = imports.split(',').map(imp => imp.trim());
      const destructure = importList.join(', ');
      return `const { ${destructure} } = require("${modulePath}")`;
    }
  );
  
  // 转换默认导出
  result = result.replace(
    /export\s+default\s+/g,
    'module.exports = '
  );
  
  // 转换命名导出
  result = result.replace(
    /export\s+const\s+(\w+)/g,
    'exports.$1'
  );
  
  result = result.replace(
    /export\s+function\s+(\w+)/g,
    'exports.$1 = function'
  );
  
  // 添加严格模式
  if (options.addStrictMode) {
    result = `'use strict';\n\n${result}`;
  }
  
  return result;
};

2.6 Webpack 配置支持

2.6.1 基础配置

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

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    library: {
      type: 'umd', // 支持 CJS 和 ESM
      name: 'MyLibrary'
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/dual-module-loader.js'),
            options: {
              moduleType: 'auto',
              addModuleInfo: true
            }
          }
        ]
      }
    ]
  }
};

2.6.2 多输出配置

javascript 复制代码
// webpack.multi.config.js
const path = require('path');

// CJS 配置
const cjsConfig = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.cjs',
    library: {
      type: 'commonjs2'
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/esm-to-cjs-loader.js'),
            options: {
              addStrictMode: true
            }
          }
        ]
      }
    ]
  }
};

// ESM 配置
const esmConfig = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.esm.js',
    library: {
      type: 'module'
    }
  },
  experiments: {
    outputModule: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/cjs-to-esm-loader.js'),
            options: {
              addTreeShaking: true
            }
          }
        ]
      }
    ]
  }
};

module.exports = [cjsConfig, esmConfig];

2.7 最佳实践

2.7.1 模块化选择建议

  1. 新项目: 推荐使用 ESM,它是 JavaScript 的未来标准
  2. Node.js 项目: 可以使用 CJS 或 ESM(Node.js 12+ 支持)
  3. 浏览器项目: 推荐使用 ESM,支持 Tree Shaking
  4. 库开发: 建议同时支持 CJS 和 ESM

2.7.2 兼容性处理

javascript 复制代码
// 兼容性 Loader
module.exports = function(source) {
  const options = this.getOptions();
  
  // 检测运行环境
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
  const isBrowser = typeof window !== 'undefined';
  
  let result = source;
  
  if (isNode && options.nodePolyfills) {
    // 添加 Node.js polyfills
    result = `
// Node.js polyfills
const path = require('path');
const fs = require('fs');

${result}
    `;
  }
  
  if (isBrowser && options.browserPolyfills) {
    // 添加浏览器 polyfills
    result = `
// Browser polyfills
if (typeof global === 'undefined') {
  window.global = window;
}

${result}
    `;
  }
  
  return result;
};

3. 自定义 Loader 开发

3.1 Loader 基础概念

Loader 是 webpack 的核心概念之一,它允许 webpack 处理除了 JavaScript 之外的其他类型的文件。Loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块。

3.2 Loader 的特点

  1. 链式调用: Loader 可以链式调用,从右到左(或从下到上)执行
  2. 单一职责: 每个 Loader 只负责一个转换功能
  3. 无状态: Loader 应该是无状态的,不依赖于之前的转换结果
  4. 可配置: Loader 可以通过 options 进行配置

3.3 创建自定义 Loader

3.3.1 基本 Loader 结构

javascript 复制代码
// my-loader.js
module.exports = function(source) {
  // source 是文件内容
  // 返回转换后的代码
  return source;
};

3.3.2 带选项的 Loader

javascript 复制代码
// my-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  // 使用选项进行转换
  const transformedSource = source.replace(
    /console\.log\(/g, 
    `console.log('[${options.prefix || 'DEBUG'}]: `
  );
  
  return transformedSource;
};

3.3.3 异步 Loader

javascript 复制代码
// async-loader.js
module.exports = function(source) {
  const callback = this.async();
  
  // 异步处理
  setTimeout(() => {
    const result = source.replace(/const/g, 'let');
    callback(null, result);
  }, 100);
};

3.4 实用 Loader 示例

3.4.1 注释移除 Loader

javascript 复制代码
// comment-remover-loader.js
module.exports = function(source) {
  // 移除单行注释
  let result = source.replace(/\/\/.*$/gm, '');
  
  // 移除多行注释
  result = result.replace(/\/\*[\s\S]*?\*\//g, '');
  
  // 移除多余的空行
  result = result.replace(/\n\s*\n/g, '\n');
  
  return result;
};

3.4.2 国际化 Loader

javascript 复制代码
// i18n-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  const locale = options.locale || 'zh-CN';
  
  // 简单的国际化替换
  const translations = {
    'zh-CN': {
      'Hello': '你好',
      'World': '世界',
      'Welcome': '欢迎'
    },
    'en-US': {
      'Hello': 'Hello',
      'World': 'World', 
      'Welcome': 'Welcome'
    }
  };
  
  let result = source;
  const currentTranslations = translations[locale] || translations['zh-CN'];
  
  Object.keys(currentTranslations).forEach(key => {
    const regex = new RegExp(`'${key}'|"${key}"`, 'g');
    result = result.replace(regex, `'${currentTranslations[key]}'`);
  });
  
  return result;
};

3.4.3 代码统计 Loader

javascript 复制代码
// code-stats-loader.js
module.exports = function(source) {
  const options = this.getOptions();
  
  // 统计代码行数
  const lines = source.split('\n').length;
  const characters = source.length;
  const words = source.split(/\s+/).length;
  
  // 生成统计信息
  const stats = `
// 代码统计信息 (由 code-stats-loader 生成)
// 行数: ${lines}
// 字符数: ${characters}
// 单词数: ${words}
// 生成时间: ${new Date().toISOString()}

${source}
  `;
  
  if (options.verbose) {
    console.log(`📊 文件统计: ${this.resourcePath}`);
    console.log(`   行数: ${lines}`);
    console.log(`   字符数: ${characters}`);
    console.log(`   单词数: ${words}`);
  }
  
  return stats;
};

3.5 Loader 使用准则

3.5.1 简单性原则

  • 每个 Loader 只做一件事
  • 保持转换逻辑简单明了

3.5.2 链式调用

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'comment-remover-loader',
          'i18n-loader?locale=zh-CN',
          'code-stats-loader?verbose=true'
        ]
      }
    ]
  }
};

3.5.3 模块化

javascript 复制代码
// 将复杂的 Loader 拆分为多个简单 Loader
// 1. 语法解析 Loader
// 2. 转换 Loader  
// 3. 输出格式化 Loader

4. 自定义 Plugin 开发

4.1 Plugin 基础概念

Plugin 是 webpack 的支柱功能,用于执行范围更广的任务。Plugin 的目的在于解决 Loader 无法实现的其他功能。

4.2 Plugin 的特点

  1. 事件驱动: Plugin 通过监听 webpack 构建过程中的事件来工作
  2. 生命周期: 可以在构建的不同阶段执行不同的逻辑
  3. 配置化: 支持通过构造函数参数进行配置
  4. 异步支持: 支持异步操作和 Promise

4.3 创建自定义 Plugin

4.3.1 基本 Plugin 结构

javascript 复制代码
// my-plugin.js
class MyPlugin {
  constructor(options = {}) {
    this.options = options;
  }
  
  apply(compiler) {
    // 在 compiler 上注册钩子
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('构建完成!');
    });
  }
}

module.exports = MyPlugin;

4.3.2 带配置的 Plugin

javascript 复制代码
// configurable-plugin.js
class ConfigurablePlugin {
  constructor(options = {}) {
    this.options = {
      name: 'Default Plugin',
      outputFile: 'build-info.json',
      ...options
    };
  }
  
  apply(compiler) {
    const { name, outputFile } = this.options;
    
    compiler.hooks.done.tap(name, (stats) => {
      console.log(`${name} 执行完成`);
      
      // 生成构建信息
      const buildInfo = {
        timestamp: new Date().toISOString(),
        duration: stats.endTime - stats.startTime,
        assets: Object.keys(stats.compilation.assets),
        chunks: stats.compilation.chunks.length
      };
      
      // 这里可以写入文件或发送到服务器
      console.log('构建信息:', buildInfo);
    });
  }
}

module.exports = ConfigurablePlugin;

4.4 实用 Plugin 示例

4.4.1 构建时间统计 Plugin

javascript 复制代码
// build-time-plugin.js
class BuildTimePlugin {
  constructor(options = {}) {
    this.options = {
      showDetails: true,
      format: 'human', // 'human' | 'ms'
      ...options
    };
  }
  
  apply(compiler) {
    const startTime = Date.now();
    
    compiler.hooks.done.tap('BuildTimePlugin', (stats) => {
      const endTime = Date.now();
      const duration = endTime - startTime;
      
      let formattedDuration;
      if (this.options.format === 'human') {
        formattedDuration = this.formatDuration(duration);
      } else {
        formattedDuration = `${duration}ms`;
      }
      
      console.log(`⏱️  构建耗时: ${formattedDuration}`);
      
      if (this.options.showDetails) {
        console.log(`📦 输出文件数: ${Object.keys(stats.compilation.assets).length}`);
        console.log(`🔧 模块数: ${stats.compilation.modules.size}`);
      }
    });
  }
  
  formatDuration(ms) {
    if (ms < 1000) {
      return `${ms}ms`;
    } else if (ms < 60000) {
      return `${(ms / 1000).toFixed(2)}s`;
    } else {
      const minutes = Math.floor(ms / 60000);
      const seconds = ((ms % 60000) / 1000).toFixed(2);
      return `${minutes}m ${seconds}s`;
    }
  }
}

module.exports = BuildTimePlugin;

4.4.2 文件列表生成 Plugin

javascript 复制代码
// file-list-plugin.js
const { RawSource } = require('webpack-sources');

class FileListPlugin {
  constructor(options = {}) {
    this.options = {
      outputFile: 'file-list.md',
      title: '# 构建文件列表',
      ...options
    };
  }
  
  apply(compiler) {
    const pluginName = 'FileListPlugin';
    
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
          stage: compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // 生成文件列表内容
          const content = this.generateFileList(assets);
          
          // 添加新资源到构建输出
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
  
  generateFileList(assets) {
    const { title } = this.options;
    
    let content = `${title}\n\n`;
    content += `生成时间: ${new Date().toLocaleString()}\n\n`;
    content += '## 文件列表\n\n';
    
    Object.keys(assets).forEach(filename => {
      const size = assets[filename].size();
      const sizeFormatted = this.formatSize(size);
      content += `- **${filename}** (${sizeFormatted})\n`;
    });
    
    return content;
  }
  
  formatSize(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

module.exports = FileListPlugin;

4.4.3 环境变量注入 Plugin

javascript 复制代码
// env-inject-plugin.js
const { RawSource } = require('webpack-sources');

class EnvInjectPlugin {
  constructor(options = {}) {
    this.options = {
      envFile: '.env',
      prefix: 'APP_',
      ...options
    };
  }
  
  apply(compiler) {
    const pluginName = 'EnvInjectPlugin';
    
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
          stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        (assets) => {
          // 读取环境变量
          const envVars = this.readEnvVars();
          
          // 生成环境变量文件
          const envContent = this.generateEnvFile(envVars);
          
          compilation.emitAsset(
            'env-config.js',
            new RawSource(envContent)
          );
        }
      );
    });
  }
  
  readEnvVars() {
    const envVars = {};
    const prefix = this.options.prefix;
    
    // 读取 process.env 中的环境变量
    Object.keys(process.env).forEach(key => {
      if (key.startsWith(prefix)) {
        envVars[key] = process.env[key];
      }
    });
    
    return envVars;
  }
  
  generateEnvFile(envVars) {
    let content = '// 自动生成的环境变量配置\n';
    content += 'window.ENV_CONFIG = {\n';
    
    Object.keys(envVars).forEach(key => {
      content += `  '${key}': '${envVars[key]}',\n`;
    });
    
    content += '};\n';
    content += 'export default window.ENV_CONFIG;\n';
    
    return content;
  }
}

module.exports = EnvInjectPlugin;

4.4.4 异步编译 Plugin

javascript 复制代码
// async-compilation-plugin.js
class AsyncCompilationPlugin {
  constructor(options = {}) {
    this.options = {
      delay: 1000,
      message: '异步任务完成',
      ...options
    };
  }
  
  apply(compiler) {
    // 使用 tapAsync 处理异步操作
    compiler.hooks.emit.tapAsync(
      'AsyncCompilationPlugin',
      (compilation, callback) => {
        console.log('开始异步任务...');
        
        setTimeout(() => {
          console.log(this.options.message);
          
          // 添加自定义资源
          compilation.emitAsset(
            'async-task-complete.txt',
            new (require('webpack-sources').RawSource)(
              `异步任务完成时间: ${new Date().toISOString()}`
            )
          );
          
          callback();
        }, this.options.delay);
      }
    );
  }
}

module.exports = AsyncCompilationPlugin;

4.4.5 模块化分析 Plugin

javascript 复制代码
// module-analyzer-plugin.js
const { RawSource } = require('webpack-sources');

class ModuleAnalyzerPlugin {
  constructor(options = {}) {
    this.options = {
      outputFile: 'module-analysis.json',
      analyzeImports: true,
      analyzeExports: true,
      ...options
    };
  }
  
  apply(compiler) {
    const pluginName = 'ModuleAnalyzerPlugin';
    
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
          stage: compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // 分析模块依赖关系
          const moduleAnalysis = this.analyzeModules(compilation);
          
          // 生成分析报告
          const analysisContent = JSON.stringify(moduleAnalysis, null, 2);
          
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(analysisContent)
          );
        }
      );
    });
  }
  
  analyzeModules(compilation) {
    const analysis = {
      timestamp: new Date().toISOString(),
      totalModules: compilation.modules.size,
      modules: [],
      dependencies: [],
      moduleTypes: {
        cjs: 0,
        esm: 0,
        mixed: 0
      }
    };
    
    compilation.modules.forEach(module => {
      if (module.resource) {
        const moduleInfo = this.analyzeModule(module);
        analysis.modules.push(moduleInfo);
        
        // 统计模块类型
        if (moduleInfo.hasImport && moduleInfo.hasRequire) {
          analysis.moduleTypes.mixed++;
        } else if (moduleInfo.hasImport) {
          analysis.moduleTypes.esm++;
        } else if (moduleInfo.hasRequire) {
          analysis.moduleTypes.cjs++;
        }
      }
    });
    
    return analysis;
  }
  
  analyzeModule(module) {
    const source = module._source ? module._source.source() : '';
    
    return {
      path: module.resource,
      size: source.length,
      hasImport: /import\s+/.test(source),
      hasExport: /export\s+/.test(source),
      hasRequire: /require\s*\(/.test(source),
      hasModuleExports: /module\.exports/.test(source),
      importCount: (source.match(/import\s+/g) || []).length,
      exportCount: (source.match(/export\s+/g) || []).length,
      requireCount: (source.match(/require\s*\(/g) || []).length
    };
  }
}

module.exports = ModuleAnalyzerPlugin;

4.4.6 模块化转换 Plugin

javascript 复制代码
// module-transformer-plugin.js
const { RawSource } = require('webpack-sources');

class ModuleTransformerPlugin {
  constructor(options = {}) {
    this.options = {
      targetModuleType: 'esm', // 'esm' | 'cjs'
      transformImports: true,
      transformExports: true,
      ...options
    };
  }
  
  apply(compiler) {
    const pluginName = 'ModuleTransformerPlugin';
    
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
          stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
        },
        (assets) => {
          // 转换模块格式
          Object.keys(assets).forEach(filename => {
            if (filename.endsWith('.js')) {
              const asset = assets[filename];
              const source = asset.source();
              const transformedSource = this.transformModule(source);
              
              compilation.emitAsset(
                filename,
                new RawSource(transformedSource)
              );
            }
          });
        }
      );
    });
  }
  
  transformModule(source) {
    let result = source;
    
    if (this.options.targetModuleType === 'esm') {
      result = this.transformToESM(result);
    } else if (this.options.targetModuleType === 'cjs') {
      result = this.transformToCJS(result);
    }
    
    return result;
  }
  
  transformToESM(source) {
    let result = source;
    
    if (this.options.transformImports) {
      // 转换 require 为 import
      result = result.replace(
        /const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
        'import $1 from "$2"'
      );
    }
    
    if (this.options.transformExports) {
      // 转换 module.exports 为 export default
      result = result.replace(
        /module\.exports\s*=\s*/g,
        'export default '
      );
    }
    
    return result;
  }
  
  transformToCJS(source) {
    let result = source;
    
    if (this.options.transformImports) {
      // 转换 import 为 require
      result = result.replace(
        /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
        'const $1 = require("$2")'
      );
    }
    
    if (this.options.transformExports) {
      // 转换 export default 为 module.exports
      result = result.replace(
        /export\s+default\s+/g,
        'module.exports = '
      );
    }
    
    return result;
  }
}

module.exports = ModuleTransformerPlugin;

4.5 Plugin 钩子类型

4.5.1 同步钩子 (SyncHook)

javascript 复制代码
class SyncHookPlugin {
  apply(compiler) {
    // 同步钩子,直接使用 tap
    compiler.hooks.done.tap('SyncHookPlugin', (stats) => {
      console.log('同步钩子执行完成');
    });
  }
}

4.5.2 异步钩子 (AsyncSeriesHook)

javascript 复制代码
class AsyncHookPlugin {
  apply(compiler) {
    // 异步钩子,使用 tapAsync
    compiler.hooks.emit.tapAsync('AsyncHookPlugin', (compilation, callback) => {
      // 异步操作
      setTimeout(() => {
        console.log('异步钩子执行完成');
        callback();
      }, 1000);
    });
    
    // 或者使用 tapPromise
    compiler.hooks.emit.tapPromise('AsyncHookPlugin', (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('Promise 钩子执行完成');
          resolve();
        }, 1000);
      });
    });
  }
}

4.5.3 瀑布钩子 (WaterfallHook)

javascript 复制代码
class WaterfallHookPlugin {
  apply(compiler) {
    // 瀑布钩子,前一个返回值作为后一个的输入
    compiler.hooks.optimizeChunkModules.tap(
      'WaterfallHookPlugin',
      (chunks, modules) => {
        console.log('优化前的模块数:', modules.length);
        return modules.filter(module => module.size > 1000); // 过滤大模块
      }
    );
  }
}

5. 实战案例

5.1 完整的项目配置

javascript 复制代码
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BuildTimePlugin = require('./plugins/build-time-plugin');
const FileListPlugin = require('./plugins/file-list-plugin');
const EnvInjectPlugin = require('./plugins/env-inject-plugin');
const ModuleAnalyzerPlugin = require('./plugins/module-analyzer-plugin');
const ModuleTransformerPlugin = require('./plugins/module-transformer-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true,
    library: {
      type: 'umd', // 支持 CJS 和 ESM
      name: 'MyLibrary'
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/dual-module-loader.js'),
            options: {
              moduleType: 'auto',
              addModuleInfo: true
            }
          },
          {
            loader: path.resolve(__dirname, 'loaders/comment-remover-loader.js'),
          },
          {
            loader: path.resolve(__dirname, 'loaders/code-stats-loader.js'),
            options: {
              verbose: true,
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new BuildTimePlugin({
      showDetails: true,
      format: 'human',
    }),
    new FileListPlugin({
      outputFile: 'build-files.md',
      title: '# 项目构建文件列表',
    }),
    new EnvInjectPlugin({
      prefix: 'APP_',
    }),
    new ModuleAnalyzerPlugin({
      outputFile: 'module-analysis.json',
      analyzeImports: true,
      analyzeExports: true
    }),
    new ModuleTransformerPlugin({
      targetModuleType: 'esm',
      transformImports: true,
      transformExports: true
    }),
  ],
  devServer: {
    static: './dist',
    hot: true,
  },
};

5.2 测试文件

javascript 复制代码
// src/index.js
// 这是一个测试文件,用于演示 Loader 和 Plugin 的功能
// 支持 CJS 和 ESM 两种模块化方式

// ESM 导入
import { formatDate } from './utils/date.js';
import { logger } from './utils/logger.js';

// CJS 导入
const path = require('path');
const fs = require('fs');

const message = 'Hello World'; // 这个注释会被移除
const welcome = 'Welcome to our app'; // 另一个注释

console.log(message);
console.log(welcome);

// 多行注释示例
/*
 * 这是一个多行注释
 * 它会被完全移除
 */

// 使用导入的模块
console.log('当前时间:', formatDate(new Date()));
logger.info('应用启动成功');

// ESM 导出
export default {
  message,
  welcome,
  formatDate,
  logger
};

// 命名导出
export const VERSION = '1.0.0';
export const AUTHOR = 'Webpack Learner';

// CJS 导出
module.exports.config = {
  name: 'webpack-demo',
  version: VERSION
};
javascript 复制代码
// src/utils/date.js (ESM 模块)
export function formatDate(date) {
  return date.toLocaleString('zh-CN');
}

export function getCurrentYear() {
  return new Date().getFullYear();
}

export default {
  formatDate,
  getCurrentYear
};
javascript 复制代码
// src/utils/logger.js (CJS 模块)
const colors = require('colors');

function info(message) {
  console.log(colors.green(`[INFO] ${message}`));
}

function error(message) {
  console.log(colors.red(`[ERROR] ${message}`));
}

function warn(message) {
  console.log(colors.yellow(`[WARN] ${message}`));
}

module.exports = {
  info,
  error,
  warn
};
html 复制代码
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Webpack 学习示例</title>
</head>
<body>
    <div id="app">
        <h1>Webpack 自定义 Loader 和 Plugin 示例</h1>
        <p>打开控制台查看构建信息</p>
    </div>
</body>
</html>

5.3 运行和测试

bash 复制代码
# 安装依赖
npm install webpack webpack-cli html-webpack-plugin style-loader css-loader --save-dev

# 构建项目
npx webpack

# 启动开发服务器
npx webpack serve

6. 最佳实践

6.1 Loader 最佳实践

  1. 保持简单: 每个 Loader 只做一件事
  2. 链式调用: 利用 Loader 的链式特性
  3. 缓存友好: 确保转换结果可以被缓存
  4. 错误处理: 提供清晰的错误信息
  5. 文档化: 为 Loader 提供详细的使用文档

6.2 Plugin 最佳实践

  1. 事件驱动: 合理使用 webpack 的钩子系统
  2. 异步处理: 对于耗时操作使用异步钩子
  3. 资源管理: 正确管理构建资源
  4. 配置验证: 验证插件配置参数
  5. 性能优化: 避免在插件中执行耗时操作

6.3 调试技巧

javascript 复制代码
// 调试 Loader
module.exports = function(source) {
  console.log('Loader 输入:', source.substring(0, 100) + '...');
  
  const result = source.replace(/console\.log/g, 'console.warn');
  
  console.log('Loader 输出:', result.substring(0, 100) + '...');
  
  return result;
};

// 调试 Plugin
class DebugPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('DebugPlugin', (compilation) => {
      console.log('编译开始');
      
      compilation.hooks.optimize.tap('DebugPlugin', () => {
        console.log('优化阶段');
      });
      
      compilation.hooks.afterOptimize.tap('DebugPlugin', () => {
        console.log('优化完成');
      });
    });
  }
}

6.4 性能优化

  1. 并行处理 : 使用 thread-loader 进行并行处理
  2. 缓存: 合理使用 webpack 的缓存机制
  3. 代码分割 : 使用 splitChunks 进行代码分割
  4. Tree Shaking: 启用 Tree Shaking 移除无用代码
  5. 压缩 : 使用 terser-webpack-plugin 进行代码压缩

6.5 常见问题解决

6.5.1 Loader 顺序问题

javascript 复制代码
// 正确的 Loader 顺序
{
  test: /\.scss$/,
  use: [
    'style-loader',     // 最后执行
    'css-loader',       // 中间执行
    'sass-loader'       // 最先执行
  ]
}

6.5.2 Plugin 执行时机

javascript 复制代码
// 选择合适的钩子时机
compiler.hooks.beforeCompile.tap('MyPlugin', () => {
  // 编译前执行
});

compiler.hooks.afterCompile.tap('MyPlugin', (compilation) => {
  // 编译后执行
});

compiler.hooks.emit.tap('MyPlugin', (compilation) => {
  // 输出前执行
});

总结

通过本文的学习,你应该已经掌握了:

  1. Webpack 基础概念和核心原理
  2. 模块化支持 (CJS/ESM) 的实现方式和转换工具
  3. 自定义 Loader 开发的方法和最佳实践
  4. 自定义 Plugin 开发的技巧和钩子使用
  5. 实战案例的完整实现
  6. 性能优化和调试技巧

Webpack 的 Loader 和 Plugin 系统提供了强大的扩展能力,特别是对模块化的支持,让开发者可以灵活地在 CommonJS 和 ES Modules 之间进行转换和兼容。通过合理使用这些功能,可以大大提高开发效率和构建质量。记住要遵循单一职责原则,保持代码的简洁性和可维护性。

模块化支持要点

  • CJS (CommonJS): 适用于 Node.js 环境和传统项目
  • ESM (ES Modules): 现代 JavaScript 标准,支持 Tree Shaking
  • 双模块化支持: 通过 Loader 和 Plugin 实现自动转换
  • 兼容性处理: 确保在不同环境下的正常运行

参考资料

相关推荐
北京_宏哥12 小时前
《刚刚问世》系列初窥篇-Java+Playwright自动化测试-38-屏幕截图利器-上篇(详细教程)
java·前端·面试
子兮曰12 小时前
🚀别再被JSON.parse坑了!这个深度克隆方案解决了我3年的前端痛点
前端·javascript·全栈
R瑾安12 小时前
VUE基础
前端·javascript·vue.js
无敌爆龙战士12 小时前
一文搞懂pnpm+monorepo的原理
前端
艾小码12 小时前
告别无效加班!这4个表单操作技巧,让你效率翻倍
前端·javascript·html
TimelessHaze12 小时前
面试必备:深入理解 Toast 组件的事件通信与优化实现
前端·trae
zayyo12 小时前
从 Promise 到 Generator,再到 Co 与 Async/Await 的演进
前端·javascript
我的写法有点潮12 小时前
这么全的正则,还不收藏?
前端·javascript
XiaoSong12 小时前
React 表单组件深度解析
前端·react.js