webpack 面试宝典

62 阅读4分钟

1、基础

2、相关配置

3、手写 Loader 和 Plugin

如何编写一个自定义的 Webpack Loader?

案例:自动提取代码中的中文文案并生成多语言映射文件

/**
* webpack 中的配置
*/
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jsx|tsx)$/,
        use: [
          'babel-loader',
          {
            loader: './i18n-loader',
            options: { outputDir: 'src/locales' }
          }
        ]
      }
    ]
  }
};


/**
* i18n-loader
*/
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

module.exports = function(source) {
  // 异步
  const callback = this.async();
  const resourcePath = this.resourcePath;
  const outputDir = path.resolve(process.cwd(), 'locales');
  const messages = {};

  // 解析 JSX/TSX 文件
  const ast = parser.parse(source, {
    sourceType: 'module',
    plugins: ['jsx', 'typescript']
  });

  traverse(ast, {
    // 字符串字面量的节点:const str = "你好世界";
    StringLiteral(path) {
      const { value } = path.node;
      // value 如果为中文,存储在 messages 中
      if (/[\u4e00-\u9fa5]/.test(value)) {
        messages[value] = '';
      }
    },
    // 本节点的类型:<p>你好,世界!</p>
    JSXText(path) {
      const value = path.node.value.trim();
      if (value && /[\u4e00-\u9fa5]/.test(value)) {
        messages[value] = '';
      }
    }
  });

  // 生成语言包文件
  if (Object.keys(messages).length > 0) {
    const filename = path.basename(resourcePath, path.extname(resourcePath));
    const outputPath = path.join(outputDir, `${filename}.json`);
    
    fs.mkdirSync(outputDir, { recursive: true });
    fs.writeFileSync(outputPath, JSON.stringify(messages, null, 2));
  }

  callback(null, source);
};

自动为图片资源添加CDN前缀并生成WebP格式的Webpack

/**
* webpack 中的配置
*/
.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g)$/,
        use: [
          {
            loader: './cdn-webp-loader',
            options: {
              cdn: 'https://your-cdn.com/assets',
              quality: 80
            }
          }
        ]
      }
    ]
  }
};



/**
* cdn-webp
*/
const path = require('path');
const sharp = require('sharp');
const loaderUtils = require('loader-utils');

module.exports = async function(content) {
  const callback = this.async();
  const options = loaderUtils.getOptions(this) || {};
  const cdnBase = options.cdn || 'https://cdn.example.com';
  const quality = options.quality || 75;

  try {
    // 生成WebP格式
    const webpBuffer = await sharp(content)
      .webp({ quality })
      .toBuffer();

    // 生成CDN路径
    const filename = loaderUtils.interpolateName(
      this, 
      '[contenthash].[ext]', 
      { content: webpBuffer }
    );
    const publicPath = `${cdnBase}/${filename}`;

    // 输出文件到dist目录
    this.emitFile(filename, webpBuffer);

    callback(null, `module.exports = "${publicPath}";`);
  } catch (err) {
    callback(err);
  }
};

module.exports.raw = true; // 获取原始Buffer


如何编写一个自定义的 Webpack Plugin?

自动清理旧构建产物并保留指定版本

/**
* webpack 配置
*/
const CleanHistoryPlugin = require('./CleanHistoryPlugin');
module.exports = {
  plugins: [
    new CleanHistoryPlugin({
      maxVersions: 3,
      pattern: /^build-\d{8}/ // 匹配build-20250101格式
    })
  ]
};


/**
* CleanHistoryPlugin
*/
const fs = require('fs');
const path = require('path');
class CleanHistoryPlugin {
  constructor(options = {}) {
    this.maxVersions = options.maxVersions || 5;
    this.outputPath = options.outputPath || 'dist';
    this.pattern = options.pattern || /^app-\d+\.\d+\.\d+/;
  }
  apply(compiler) {
    compiler.hooks.afterEmit.tap('CleanHistoryPlugin', () => {
      const files = fs.readdirSync(this.outputPath);
      const matched = files.filter(f => this.pattern.test(f));
      
      if (matched.length > this.maxVersions) {
        matched
          .sort()
          .slice(0, matched.length - this.maxVersions)
          .forEach(f => {
            fs.rmSync(path.join(this.outputPath, f), { recursive: true });
            console.log(`Cleaned old version: ${f}`);
          });
      }
    });
  }
}
module.exports = CleanHistoryPlugin;

自动清除console.log的Webpack插件实现

/**
* webpack 配置
*/
ConsoleCleanPlugin = require('./ConsoleCleanPlugin');
module.exports = {
    mode: 'production',
    plugins: [
        new ConsoleCleanPlugin()
    ]
};


/**
* ConsoleCleanPlugin
*/
const { ConcatSource } = require('webpack-sources');
class ConsoleCleanPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('ConsoleCleanPlugin', compilation => {
      compilation.hooks.processAssets.tap({
        name: 'ConsoleCleanPlugin',
        stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS
      }, assets => {
        Object.keys(assets).forEach(filename => {
          if (/\.js$/.test(filename)) {
            let source = assets[filename].source();
            source = source.replace(/console\.log\(.*?\);?/g, '');
            compilation.updateAsset(
              filename,
              new ConcatSource(source)
            );
          }
        });
      });
    });
  }
}
module.exports = ConsoleCleanPlugin;

4、性能优化

  • 通用环境优化
    • include/exclude 限定 Loader 作用范围
    • 持久化缓存(Webpack 5 内置支持)
    • 多进程/多实例构建
    • 代码拆分与 Tree Shaking
    • 动态导入(懒加载)
  • 开发环境优化
    • 热更新(HMR)
    • 缓存
    • 模块热替换(HMR)
    • Source Map 优化
    • 优化 Source Map
    • 避免生产环境工具(移除 TerserPlugin[hash] 等生产环境专用配置)
    • DLL 预编译(大型项目)
    • 跳过大型库处理
  • 生产环境专项优化
    • 代码压缩
    • 长效缓存策略
    • 按需加载 Polyfill
    • 代码分割
    • 压缩
    • 长效缓
    • Tree Shaking。
    • 跳过解析与预编译
      • noParse 忽略已模块化库
      • DllPlugin 预编译静态库
    • 代码分割(Code Splitting)
    • 动态导入(懒加载)
      • 通过 import() 实现按需加载,减少首屏体积37。
  • Webpack 5 新特性优化
    • 模块联邦(Module Federation)
    • 资源模块(Asset Modules)
    • 构建性能提升
      • 持久化缓存
      • 改进的 Tree Shaking
      • 更优的 Chunk 分割算法

4.3 生产环境优化(开发/生产均适用)

代码分割 (Code Splitting)

Tree Shaking (移除未使用代码)

使用缓存 (cache-loader, hard-source-webpack-plugin)

缩小文件搜索范围 (resolve.modules, resolve.extensions)

多线程/并行构建 (thread-loader, happypack)

使用 DLLPlugin 预编译不常变化的模块

如何提高 Webpack 的构建速度?

  • 使用 speed-measure-webpack-plugin 分析构建速度
  • 减少 loader/plugin 的使用
  • 使用 cache-loader 或 hard-source-webpack-plugin 缓存
  • 使用 thread-loader 或 happypack 多线程构建
  • 使用 DllPlugin 预编译不常变化的模块

5、一些包的原理

Webpack 的 HMR 原理

1️⃣ 参考文档

image.png

6、源码

Webpack 的构建流程是一个串行的事件流,其核心阶段可分为以下几个关键环节

  • 初始化阶段(Preparation)‌

    • beforeRun‌:清除缓存,准备编译器环境。
    • run‌:启动编译,合并配置参数,创建 Compiler 实例‌。
    • initialize‌/environment:注册内置插件(如 EntryOptionPlugin),解析入口文件配置‌
    • 钩子作用‌:适合动态修改入口配置或注入全局变量
  • 编译阶段(Compilation)‌

    • compile‌:创建 Compilation 对象(管理模块依赖、资源生成的核心实例)。
    • make‌(‌核心依赖分析阶段‌)
      • 调用 ModuleFactory(如 NormalModuleFactory)创建模块对象‌
      • buildModule‌:对每个模块应用 Loader 转换源码,生成 AST 并解析依赖‌。
      • succeedModule/finishModules‌:模块构建完成后触发,标记依赖图完成‌。
    • afterCompile‌:模块依赖图构建完毕,进入优化阶段‌。
    • 性能瓶颈‌:此阶段最耗时,因需递归处理所有模块‌
  • 优化与封装阶段(Sealing)‌

    • seal‌:不可再修改模块依赖关系‌,执行 Chunk 优化:
      • 拆分代码块(Code Splitting)。
      • Tree Shaking 移除未用代码‌
      • 合并重复模块,生成 Chunk 对象和运行时(Runtime)逻辑‌
    • 钩子作用‌:适合代码压缩或自定义 Chunk 策略(如分包优化)‌
  • 输出阶段(Emission)‌

    • emit‌(‌生成资源到内存‌):生成资源文件(如 Bundle.js),可修改输出内容
    • afterEmit‌:资源已写入内存,准备输出到磁盘,适合清理临时文件‌
    • 钩子作用‌:插件可在此阶段干预最终输出(如生成报告)‌
  • 完成阶段(Completion)

    • done‌:编译成功时触发,生成 stats 对象(含耗时、资源大小等数据)‌
    • ‌failed‌:编译失败时处理错误‌
    • 钩子作用‌:适合分析构建报告或发送通知‌