Webpack 面试题

22 阅读5分钟

Webpack 面试题

Webpack 是目前最主流的前端模块打包工具,以下涵盖核心概念、配置、性能优化、Loader/Plugin 开发等高频面试题。


1. Webpack 是什么?它的核心概念有哪些?

Webpack 是一个静态模块打包工具,它从一个或多个入口文件出发,递归地构建依赖图(dependency graph),然后将所有模块打包成一个或多个 bundle。

// webpack.config.js 最基本配置
const path = require('path')

module.exports = {
  // 1. 入口(Entry):打包的起点
  entry: './src/index.js',

  // 2. 输出(Output):打包结果的位置和文件名
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',  // 内容哈希,用于缓存
    clean: true  // 打包前清空 dist 目录
  },

  // 3. 加载器(Loader):处理非 JS 文件(CSS、图片、TS等)
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.tsx?$/, use: 'ts-loader' },
      { test: /\.(png|jpg|gif)$/, type: 'asset/resource' }
    ]
  },

  // 4. 插件(Plugin):扩展 Webpack 的能力
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' })
  ],

  // 5. 模式(Mode):决定内置优化策略
  mode: 'production',  // 'development' | 'production' | 'none'

  // 6. 解析(Resolve):配置模块解析规则
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

五大核心概念对比:

概念作用示例
Entry打包入口entry: './src/index.js'
Output输出配置output: { filename: 'bundle.js' }
Loader转换非 JS 模块css-loader、babel-loader
Plugin扩展功能(打包优化、资源管理)HtmlWebpackPlugin
Mode构建模式development / production

💡 面试加分点: Webpack 5 相比 4 的重大变化——内置了 Asset Modules(取代 file-loader/url-loader)、持久缓存(filesystem cache)、Module Federation(模块联邦)、不再自动 polyfill Node.js 核心模块。


2. Loader 和 Plugin 的区别?

// ========== Loader:文件转换器 ==========
// Loader 用于处理非 JS 文件,将其转换为 Webpack 能理解的模块
// 本质:一个函数,接收源文件内容,返回转换后的结果

// Loader 的执行顺序:从右到左,从下到上
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',      // 3. 将 CSS 注入 DOM 的 <style> 标签
          'css-loader',        // 2. 解析 CSS 中的 @import 和 url()
          'sass-loader'        // 1. 将 SCSS 编译为 CSS
        ]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true  // 开启缓存
          }
        }
      }
    ]
  }
}

// ========== Plugin:功能扩展器 ==========
// Plugin 可以在 Webpack 构建的各个生命周期钩子上执行操作
// 本质:一个具有 apply 方法的类

const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  plugins: [
    // 自动生成 HTML 并注入打包后的 JS/CSS
    new HtmlWebpackPlugin({
      template: './public/index.html',
      minify: { collapseWhitespace: true, removeComments: true }
    }),
    // 将 CSS 提取为独立文件(生产环境)
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    }),
    // 包体积分析
    new BundleAnalyzerPlugin()
  ]
}

Loader vs Plugin 对比:

对比LoaderPlugin
作用文件转换(编译/转译)功能扩展(优化/注入/清理等)
本质函数类(有 apply 方法)
执行时机在模块加载时在整个编译生命周期的各个钩子
配置位置module.rulesplugins
常见示例babel-loader, css-loaderHtmlWebpackPlugin, DefinePlugin

3. 常用 Loader 有哪些?

module.exports = {
  module: {
    rules: [
      // ===== JS/TS 处理 =====
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',       // 将 ES6+/JSX 转换为 ES5
          options: {
            presets: [
              ['@babel/preset-env', { targets: '> 0.25%, not dead' }],
              '@babel/preset-react'
            ],
            plugins: ['@babel/plugin-transform-runtime'],
            cacheDirectory: true
          }
        }
      },
      {
        test: /\.tsx?$/,
        use: 'ts-loader'               // 编译 TypeScript
        // 或者用 babel-loader + @babel/preset-typescript
      },

      // ===== CSS 处理 =====
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // 生产环境提取 CSS 文件
          // 'style-loader',            // 开发环境注入 <style> 标签
          {
            loader: 'css-loader',       // 解析 CSS
            options: { modules: true }  // 开启 CSS Modules
          },
          'postcss-loader'              // PostCSS 处理(自动前缀等)
        ]
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']  // Less 编译
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']  // Sass 编译
      },

      // ===== 资源处理(Webpack 5 内置 Asset Modules)=====
      {
        test: /\.(png|jpe?g|gif|webp|svg)$/,
        type: 'asset',                 // 自动选择 resource 或 inline
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024           // 小于 8KB 转为 base64
          }
        },
        generator: {
          filename: 'images/[name].[hash:8][ext]'
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',         // 字体文件
        generator: { filename: 'fonts/[name].[hash:8][ext]' }
      },

      // ===== Vue 单文件组件 =====
      {
        test: /\.vue$/,
        use: 'vue-loader'              // 解析 .vue 文件
      },

      // ===== 其他 =====
      {
        test: /\.md$/,
        use: ['html-loader', 'markdown-loader']  // Markdown 转 HTML
      }
    ]
  }
}

💡 面试加分点: Webpack 5 的 Asset Modules 取代了 file-loader、url-loader、raw-loader。四种 type:asset/resource(输出文件)、asset/inline(转 base64)、asset(自动选择)、asset/source(导出源代码)。


4. 常用 Plugin 有哪些?

const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const CompressionPlugin = require('compression-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
  plugins: [
    // 1. 自动生成 HTML,注入打包资源
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: '我的应用',
      minify: {
        collapseWhitespace: true,
        removeComments: true,
        removeRedundantAttributes: true
      }
    }),

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

    // 3. 定义环境变量
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
      __APP_VERSION__: JSON.stringify('1.0.0')
    }),

    // 4. Gzip 压缩
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,    // 大于 10KB 才压缩
      minRatio: 0.8
    }),

    // 5. 复制静态资源
    new CopyWebpackPlugin({
      patterns: [
        { from: 'public/favicon.ico', to: 'favicon.ico' }
      ]
    }),

    // 6. 包体积分析
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',  // 生成静态 HTML 报告
      openAnalyzer: false
    }),

    // 7. ESLint 检查
    new ESLintPlugin({
      extensions: ['js', 'jsx', 'ts', 'tsx'],
      fix: true
    }),

    // 8. TypeScript 类型检查(独立进程,不阻塞编译)
    new ForkTsCheckerWebpackPlugin(),

    // 9. 模块热替换(开发环境)
    new webpack.HotModuleReplacementPlugin(),

    // 10. 进度条
    new webpack.ProgressPlugin()
  ],

  optimization: {
    minimizer: [
      // JS 压缩
      new TerserPlugin({
        parallel: true,      // 多进程并行压缩
        terserOptions: {
          compress: { drop_console: true }  // 移除 console
        }
      }),
      // CSS 压缩
      new CssMinimizerPlugin()
    ]
  }
}

5. Webpack 的构建流程(原理)?

Webpack 构建流程(简化版):

1. 初始化(Initialization)
   ├── 读取并合并配置(webpack.config.js + 命令行参数 + 默认配置)
   ├── 创建 Compiler 对象
   └── 注册所有 Plugin(调用 plugin.apply(compiler))

2. 编译(Compilation)
   ├── 从 Entry 开始,调用对应 Loader 转换模块内容
   ├── 解析模块的依赖(import/require)
   ├── 递归处理所有依赖模块 → 构建完整的依赖图
   └── 每个文件只会被处理一次

3. 输出(Emit)
   ├── 根据依赖图和配置,组装 Chunk
   ├── 将 Chunk 转换为文件(Bundle)
   └── 写入文件系统(dist 目录)
// 简化版 Webpack 核心流程伪代码
class Webpack {
  constructor(config) {
    this.config = config
    this.modules = new Map()       // 所有模块
    this.chunks = []               // 代码块
    this.hooks = {                 // 生命周期钩子
      beforeRun: new SyncHook(),
      run: new AsyncSeriesHook(),
      compilation: new SyncHook(),
      emit: new AsyncSeriesHook(),
      done: new SyncHook()
    }

    // 注册所有插件
    config.plugins.forEach(plugin => plugin.apply(this))
  }

  run() {
    this.hooks.beforeRun.call()

    // 1. 从入口开始构建
    const entryModule = this.buildModule(this.config.entry)

    // 2. 递归解析依赖
    this.processDependencies(entryModule)

    // 3. 生成 Chunk
    this.chunks = this.generateChunks()

    // 4. 输出文件
    this.hooks.emit.callAsync(() => {
      this.emitFiles()
      this.hooks.done.call()
    })
  }

  buildModule(filePath) {
    // 读取文件内容
    let content = fs.readFileSync(filePath, 'utf-8')

    // 匹配并执行 Loader(从右到左)
    const loaders = this.getMatchingLoaders(filePath)
    for (const loader of loaders.reverse()) {
      content = loader(content)
    }

    // 解析 AST,提取依赖
    const ast = parse(content)
    const dependencies = extractImports(ast)

    return { filePath, content, dependencies }
  }
}

💡 面试加分点: Webpack 使用 Tapable 库管理插件的钩子系统。整个构建过程有上百个钩子,Plugin 通过订阅这些钩子来介入构建流程。


6. Webpack 的热更新(HMR)原理?

HMR(Hot Module Replacement)工作流程:

1. 文件修改 → Webpack 重新编译修改的模块
2. Webpack Dev Server 通过 WebSocket 通知浏览器
3. 浏览器收到通知后,通过 JSONP 请求获取更新的模块
4. 替换旧模块,局部刷新(不刷新整个页面)
// webpack.config.js - 开启 HMR
module.exports = {
  devServer: {
    hot: true,             // 开启 HMR
    port: 3000,
    open: true,
    historyApiFallback: true  // SPA 路由支持
  }
}

// 手动处理 HMR(一般框架会自动处理)
if (module.hot) {
  module.hot.accept('./App', () => {
    // App 模块更新后的处理逻辑
    const NextApp = require('./App').default
    render(NextApp)
  })
}

// HMR 原理示意:
// 1. 建立 WebSocket 连接
//    浏览器 ←WebSocket→ Dev Server
//
// 2. 文件变化时,服务端推送更新信息
//    Server → { type: 'hash', data: 'abc123' }
//    Server → { type: 'ok' }
//
// 3. 浏览器请求更新清单
//    GET /abc123.hot-update.json
//    → { c: { main: true }, r: [], m: [] }
//
// 4. 浏览器请求更新的模块
//    GET /main.abc123.hot-update.js
//    → 新的模块代码
//
// 5. 替换旧模块,执行 module.hot.accept 回调

HMR vs Live Reload 对比:

对比HMRLive Reload
更新方式局部替换模块刷新整个页面
状态保持✅ 保持页面状态❌ 状态丢失
速度快(只更新变化的模块)慢(重新加载整个页面)
CSS 更新无刷新即时生效需要刷新

7. Source Map 是什么?有哪些类型?

// Source Map 是一个映射关系文件,将打包后的代码映射回源代码
// 方便在浏览器中调试压缩/编译后的代码

module.exports = {
  // 开发环境推荐:有完整的源码映射,构建速度快
  devtool: 'eval-cheap-module-source-map',

  // 生产环境推荐:不生成 source map 或只生成 hidden
  // devtool: false,                    // 不生成(安全)
  // devtool: 'hidden-source-map',      // 生成但不关联(用于错误监控)
  // devtool: 'nosources-source-map',   // 有行列信息但无源码
}

常见 Source Map 类型对比:

devtool构建速度重构建速度质量适用场景
false最快最快无生产环境(安全)
eval快最快生成代码开发(速度优先)
eval-cheap-module-source-map较快快原始源码(行)开发(推荐)
eval-source-map慢较快原始源码(行+列)开发(质量优先)
source-map最慢慢原始源码(行+列)生产(调试需要)
hidden-source-map最慢慢原始源码生产(错误监控)
nosources-source-map慢慢无源码生产(只要行列号)

💡 面试加分点: 生产环境如果用了 source-map,一定要确保 .map 文件不会被公开访问(通过 Nginx 限制或只上传到 Sentry 等监控平台)。


8. Webpack 的 Tree Shaking 原理?

// Tree Shaking:移除未使用的代码(Dead Code Elimination)
// 依赖 ES Module 的静态结构(import/export 是静态的,编译时可确定依赖关系)

// ========== 生效条件 ==========
// 1. 必须使用 ES Module(import/export),不能用 CommonJS(require/module.exports)
// 2. mode: 'production'(自动开启 Tree Shaking)
// 3. package.json 中设置 sideEffects

// utils.js
export function add(a, b) { return a + b }       // ✅ 被使用,保留
export function subtract(a, b) { return a - b }   // ❌ 未使用,移除
export function multiply(a, b) { return a * b }   // ❌ 未使用,移除

// main.js
import { add } from './utils'  // 只导入了 add
console.log(add(1, 2))
// 打包后:subtract 和 multiply 的代码会被移除

// ========== sideEffects 配置 ==========
// package.json
{
  "name": "my-app",
  // false:表示所有模块都没有副作用,可以安全地 Tree Shake
  "sideEffects": false

  // 或者指定有副作用的文件(不会被 Tree Shake)
  // "sideEffects": [
  //   "*.css",
  //   "*.scss",
  //   "./src/polyfills.js"
  // ]
}

// ========== 什么是副作用? ==========
// 副作用:import 后会影响全局状态的代码

// ❌ 有副作用(即使没用到导出的值,导入本身就会执行代码)
import './polyfills'          // 全局 polyfill
import 'normalize.css'         // 全局样式
Array.prototype.myMethod = function() {}  // 修改原型

// ✅ 无副作用(纯函数/纯组件,不使用就可以安全移除)
export const add = (a, b) => a + b
export const PI = 3.14159

// ========== Webpack 配置 ==========
module.exports = {
  mode: 'production',  // 自动开启 Tree Shaking
  optimization: {
    usedExports: true,    // 标记未使用的导出
    minimize: true,       // 压缩时移除未使用代码
    sideEffects: true     // 读取 package.json 的 sideEffects 字段
  }
}

💡 面试加分点: Tree Shaking 只对 ES Module 有效,因为 import/export 是静态的(编译时确定),而 CommonJS 的 require() 是动态的(运行时确定),无法在编译阶段确定哪些代码被使用。


9. Webpack 的代码分割(Code Splitting)?

// 代码分割:将代码拆分成多个 bundle,按需加载,减少首屏加载量

// ========== 方式1:多入口(Multi Entry)==========
module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].[contenthash:8].js'
  }
}

// ========== 方式2:动态导入(Dynamic Import)==========
// 最常用,Webpack 自动将动态导入的模块打包为单独的 chunk
// React 懒加载
const UserPage = React.lazy(() => import('./pages/UserPage'))
// Vue 路由懒加载
const routes = [
  { path: '/about', component: () => import('./views/About.vue') }
]

// 自定义 chunk 名称(魔法注释)
import(
  /* webpackChunkName: "lodash" */
  /* webpackPrefetch: true */      // 预获取:浏览器空闲时加载
  'lodash'
).then(({ default: _ }) => {
  console.log(_.join(['Hello', 'webpack'], ' '))
})

// ========== 方式3:SplitChunksPlugin(提取公共模块)==========
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',        // 'async' | 'initial' | 'all'
      minSize: 20000,       // 最小 20KB 才分割
      minChunks: 1,         // 最少被引用 1 次
      maxAsyncRequests: 30,  // 按需加载时最大并行请求数
      maxInitialRequests: 30, // 入口最大并行请求数
      cacheGroups: {
        // 提取 node_modules 中的第三方库
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10        // 优先级
        },
        // 提取公共模块
        common: {
          minChunks: 2,       // 被至少 2 个 chunk 引用
          name: 'common',
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true  // 复用已存在的 chunk
        },
        // 单独提取大型库
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react-vendor',
          chunks: 'all',
          priority: 20
        }
      }
    },
    // 将 Webpack 运行时代码单独提取
    runtimeChunk: 'single'
  }
}

💡 面试加分点: webpackPrefetch(预获取)和 webpackPreload(预加载)的区别——Prefetch 在浏览器空闲时加载(<link rel="prefetch">),Preload 与父 chunk 并行加载(<link rel="preload">)。


10. Webpack 的持久化缓存(Persistent Caching)?

// Webpack 5 内置了持久化缓存,大幅加速二次构建

module.exports = {
  cache: {
    type: 'filesystem',            // 缓存到文件系统(默认是 'memory')
    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
    buildDependencies: {
      config: [__filename]         // 当 webpack.config.js 变化时,缓存失效
    },
    version: '1.0.0'               // 改变版本号可手动使缓存失效
  },

  // 输出文件使用 contenthash 实现浏览器缓存
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].js'
  }
}

// ========== 三种 hash 的区别 ==========
// [hash]:整个项目的 hash,任何文件变化都会改变
// [chunkhash]:基于 chunk 的 hash,同一 chunk 内文件变化才改变
// [contenthash]:基于文件内容的 hash,只有文件内容变化才改变(推荐)

// 场景:JS 文件用 contenthash,CSS 从 JS 中提取后也用 contenthash
// 这样修改 JS 不会导致 CSS 的缓存失效,反之亦然

构建速度对比(Webpack 5 持久缓存):

场景无缓存有缓存
首次构建30s30s(首次仍需完整构建)
无修改重建30s~1s(从缓存恢复)
小量修改30s~3s(增量编译)

11. Webpack 的 Module Federation(模块联邦)?

// 模块联邦:允许多个独立构建的应用之间共享模块
// 核心概念:应用既可以是"提供者"(expose),也可以是"消费者"(remote)

// ========== 应用 A(提供者):暴露模块 ==========
// app-a/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_a',                    // 应用名
      filename: 'remoteEntry.js',        // 远程入口文件
      exposes: {
        './Button': './src/components/Button',  // 暴露的模块
        './utils': './src/utils/helpers'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  ]
}

// ========== 应用 B(消费者):使用远程模块 ==========
// app-b/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_b',
      remotes: {
        app_a: 'app_a@http://localhost:3001/remoteEntry.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
}

// app-b/src/App.jsx 使用远程组件
const RemoteButton = React.lazy(() => import('app_a/Button'))

function App() {
  return (
    <React.Suspense fallback="加载中...">
      <RemoteButton text="来自 App A 的按钮" />
    </React.Suspense>
  )
}

💡 面试加分点: Module Federation 是微前端的一种实现方案。它的优势是运行时共享——不需要将所有依赖打包到一起,应用可以在运行时动态加载远程模块。shared 配置可以避免重复加载 React 等公共依赖。


12. 如何优化 Webpack 构建速度?

module.exports = {
  // ===== 1. 缩小搜索范围 =====
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],  // 减少扩展名尝试
    alias: { '@': path.resolve(__dirname, 'src') },
    modules: [path.resolve(__dirname, 'node_modules')]  // 指定模块目录
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),  // 只处理 src 目录
        exclude: /node_modules/,                    // 排除 node_modules
        use: 'babel-loader'
      }
    ]
  },

  // ===== 2. 持久化缓存(Webpack 5)=====
  cache: {
    type: 'filesystem',
    buildDependencies: { config: [__filename] }
  },

  // ===== 3. 多线程/多进程 =====
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        {
          loader: 'thread-loader',  // 将耗时 loader 放到 worker 池中
          options: { workers: 4 }
        },
        'babel-loader'
      ]
    }]
  },

  // ===== 4. DLL 预编译(Webpack 4 常用,5 中用缓存替代)=====
  // 将不常变化的库(React、Vue、lodash)预先打包

  // ===== 5. 使用 esbuild-loader 替代 babel-loader =====
  module: {
    rules: [{
      test: /\.[jt]sx?$/,
      use: {
        loader: 'esbuild-loader',   // 比 babel-loader 快 10-100 倍
        options: { target: 'es2020' }
      }
    }]
  },

  // ===== 6. 开发环境优化 =====
  devtool: 'eval-cheap-module-source-map',  // 快速 source map
  optimization: {
    removeAvailableModules: false,   // 开发环境关闭
    removeEmptyChunks: false,
    splitChunks: false                // 开发环境关闭代码分割
  },

  // ===== 7. 忽略大型库的解析 =====
  module: {
    noParse: /jquery|lodash/         // 不解析的库(已知没有依赖)
  },

  // ===== 8. TypeScript 类型检查放到独立进程 =====
  plugins: [
    new ForkTsCheckerWebpackPlugin()
  ]
}

构建优化策略速查:

策略效果适用阶段
缓存(filesystem cache)⭐⭐⭐⭐⭐开发+生产
esbuild-loader⭐⭐⭐⭐⭐开发+生产
缩小搜索范围⭐⭐⭐开发+生产
thread-loader 多线程⭐⭐⭐生产
关闭开发环境不必要的优化⭐⭐⭐⭐开发
noParse⭐⭐开发+生产

13. 如何优化 Webpack 打包体积?

module.exports = {
  mode: 'production',  // 自动开启 Tree Shaking + 代码压缩

  // ===== 1. 代码压缩 =====
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,   // 移除 console
            drop_debugger: true,  // 移除 debugger
            pure_funcs: ['console.log']
          },
          mangle: true            // 混淆变量名
        }
      }),
      new CssMinimizerPlugin()
    ]
  },

  // ===== 2. 代码分割 =====
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: { test: /node_modules/, name: 'vendors', priority: 10 },
        common: { minChunks: 2, name: 'common', priority: 5 }
      }
    }
  },

  // ===== 3. 外部依赖(CDN 引入)=====
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_'
  },

  // ===== 4. Gzip/Brotli 压缩 =====
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    })
  ],

  // ===== 5. 图片压缩 =====
  module: {
    rules: [{
      test: /\.(png|jpe?g|gif|webp)$/i,
      use: [
        { loader: 'image-webpack-loader', options: {
          mozjpeg: { progressive: true, quality: 65 },
          pngquant: { quality: [0.65, 0.9] },
          webp: { quality: 75 }
        }}
      ],
      type: 'asset'
    }]
  },

  // ===== 6. 分析工具 =====
  plugins: [
    new BundleAnalyzerPlugin()  // 可视化分析包体积
  ]
}

// ===== 7. 动态 polyfill =====
// 不在 bundle 中包含 polyfill,使用 CDN 按需加载
// <script src="https://polyfill.io/v3/polyfill.min.js?features=es2015"></script>

// ===== 8. lodash 按需引入 =====
// ❌ import _ from 'lodash'          // 引入整个库 ~70KB
// ✅ import debounce from 'lodash/debounce'  // 只引入需要的
// 或使用 babel-plugin-lodash 自动按需引入

14. 手写一个简单的 Loader?

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

// ========== 示例1:markdown-loader ==========
// markdown-loader.js
const marked = require('marked')

module.exports = function(source) {
  // source 就是文件的原始内容
  const html = marked.parse(source)

  // 返回一个 JS 模块字符串
  return `export default ${JSON.stringify(html)}`
}

// ========== 示例2:banner-loader(添加版权声明)==========
// banner-loader.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')

const schema = {
  type: 'object',
  properties: {
    author: { type: 'string' },
    date: { type: 'string' }
  },
  additionalProperties: false
}

module.exports = function(source) {
  const options = getOptions(this)

  // 验证配置
  validate(schema, options, { name: 'Banner Loader' })

  const banner = `/**
 * @author ${options.author || 'Unknown'}
 * @date ${options.date || new Date().toISOString()}
 */\n`

  return banner + source
}

// 使用:
// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader: path.resolve('./banner-loader.js'),
        options: { author: '张三', date: '2024-01-01' }
      }
    }]
  }
}

// ========== 示例3:异步 Loader ==========
module.exports = function(source) {
  const callback = this.async()  // 声明异步操作

  someAsyncOperation(source)
    .then(result => callback(null, result))     // 成功
    .catch(err => callback(err))                 // 失败
}

15. 手写一个简单的 Plugin?

// Plugin 本质:一个具有 apply 方法的类
// apply 方法接收 compiler 对象,通过订阅钩子来扩展功能

// ========== 示例1:打包完成通知插件 ==========
class BuildNotifierPlugin {
  constructor(options = {}) {
    this.title = options.title || 'Webpack'
  }

  apply(compiler) {
    // 订阅 done 钩子(编译完成时触发)
    compiler.hooks.done.tap('BuildNotifierPlugin', (stats) => {
      const time = ((stats.endTime - stats.startTime) / 1000).toFixed(2)
      if (stats.hasErrors()) {
        console.error(`❌ ${this.title} 构建失败!`)
      } else {
        console.log(`✅ ${this.title} 构建成功!耗时 ${time}s`)
      }
    })
  }
}

// ========== 示例2:生成文件清单插件 ==========
class FileListPlugin {
  constructor(options = {}) {
    this.filename = options.filename || 'filelist.md'
  }

  apply(compiler) {
    // 订阅 emit 钩子(输出文件到 dist 之前)
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
      let content = '# 打包文件清单\n\n'

      for (const filename in compilation.assets) {
        const size = compilation.assets[filename].size()
        const sizeKB = (size / 1024).toFixed(2)
        content += `- **${filename}** (${sizeKB} KB)\n`
      }

      // 将新文件添加到输出资源中
      compilation.assets[this.filename] = {
        source: () => content,
        size: () => content.length
      }

      callback()
    })
  }
}

// ========== 示例3:清除 console 插件 ==========
class RemoveConsolePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('RemoveConsolePlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'RemoveConsolePlugin',
          stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE
        },
        (assets) => {
          for (const name in assets) {
            if (name.endsWith('.js')) {
              const source = assets[name].source()
              const cleaned = source.replace(/console\.(log|warn|info)\([^)]*\);?/g, '')
              compilation.updateAsset(name, new webpack.sources.RawSource(cleaned))
            }
          }
        }
      )
    })
  }
}

// 使用
module.exports = {
  plugins: [
    new BuildNotifierPlugin({ title: '我的项目' }),
    new FileListPlugin({ filename: 'files.md' }),
    new RemoveConsolePlugin()
  ]
}

💡 面试加分点: Webpack 的钩子系统基于 Tapable 库,有同步钩子(tap)、异步串行钩子(tapAsync/tapPromise)、异步并行钩子等。常用钩子:beforeRun → run → compilation → make → emit → done。


16. Webpack 的 Proxy 代理配置?

// devServer.proxy 用于开发环境解决跨域问题
module.exports = {
  devServer: {
    port: 3000,
    proxy: {
      // 基本用法:将 /api 开头的请求代理到后端服务器
      '/api': {
        target: 'http://localhost:8080',  // 后端地址
        changeOrigin: true,                // 修改请求头中的 Origin
        pathRewrite: {
          '^/api': ''  // /api/users → http://localhost:8080/users
        }
      },

      // 多个代理
      '/auth': {
        target: 'http://localhost:9000',
        changeOrigin: true
      },

      // WebSocket 代理
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true
      },

      // 条件代理
      '/graphql': {
        target: 'http://localhost:4000',
        changeOrigin: true,
        // 自定义代理逻辑
        bypass: function(req, res, proxyOptions) {
          if (req.headers.accept?.includes('html')) {
            return '/index.html'  // 返回 HTML 不代理
          }
        }
      }
    }
  }
}

// 原理:
// 1. 浏览器请求 http://localhost:3000/api/users
// 2. Webpack Dev Server 拦截 /api 开头的请求
// 3. 使用 http-proxy-middleware 将请求转发到 target
// 4. 后端返回数据 → Dev Server → 浏览器
// 因为代理发生在服务端,不存在浏览器的同源策略限制

17. Webpack 5 相比 Webpack 4 有哪些重大变化?

特性Webpack 4Webpack 5
缓存需要 cache-loader内置持久化缓存
资源处理file-loader/url-loader内置 Asset Modules
模块共享无Module Federation
Tree Shaking基础支持嵌套 Tree Shaking + 内部模块
代码生成ES5 only可配置 ES6+
Node polyfill自动注入不再自动 polyfill
构建速度一般大幅提升(缓存)
包体积一般更好的优化
// Webpack 5 新特性示例

// 1. 持久化缓存
module.exports = {
  cache: { type: 'filesystem' }
}

// 2. Asset Modules(替代 file-loader/url-loader)
module.exports = {
  module: {
    rules: [
      { test: /\.png$/, type: 'asset/resource' },  // 替代 file-loader
      { test: /\.svg$/, type: 'asset/inline' },     // 替代 url-loader(base64)
      { test: /\.txt$/, type: 'asset/source' }      // 替代 raw-loader
    ]
  }
}

// 3. Top-level await
// 可以在模块顶层使用 await
const data = await fetch('/api/config').then(r => r.json())
export { data }

// 4. 不再自动 polyfill Node.js 模块
// 如果代码中用了 Buffer、process 等,需要手动处理
module.exports = {
  resolve: {
    fallback: {
      buffer: require.resolve('buffer/'),
      process: require.resolve('process/browser')
    }
  }
}

18. Webpack 的 Resolve 配置详解?

module.exports = {
  resolve: {
    // 1. 省略扩展名:import './App' 会依次尝试 .ts .tsx .js .jsx
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],

    // 2. 路径别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      // 精确匹配
      'lodash$': 'lodash-es'  // import 'lodash' → 实际引入 lodash-es
    },

    // 3. 模块搜索目录
    modules: [
      path.resolve(__dirname, 'src'),  // 先搜索 src
      'node_modules'                    // 再搜索 node_modules
    ],

    // 4. 主文件名(目录导入时的默认文件)
    mainFiles: ['index'],

    // 5. 条件导出(package.json 的 exports 字段)
    conditionNames: ['import', 'require', 'browser', 'default'],

    // 6. 符号链接
    symlinks: false  // 关闭符号链接解析(加速构建)
  }
}

19. Webpack 的 DevServer 常用配置?

module.exports = {
  devServer: {
    port: 3000,
    host: '0.0.0.0',         // 允许局域网访问
    open: true,               // 自动打开浏览器
    hot: true,                // 热更新
    compress: true,           // Gzip 压缩

    // SPA 路由支持
    historyApiFallback: {
      rewrites: [
        { from: /^\/admin/, to: '/admin.html' },
        { from: /./, to: '/index.html' }
      ]
    },

    // 静态资源目录
    static: {
      directory: path.join(__dirname, 'public'),
      publicPath: '/static'
    },

    // 自定义中间件
    setupMiddlewares: (middlewares, devServer) => {
      // 在其他中间件之前添加
      middlewares.unshift({
        name: 'custom-middleware',
        path: '/api/mock',
        middleware: (req, res) => {
          res.json({ message: 'mock data' })
        }
      })
      return middlewares
    },

    // HTTPS
    // server: 'https',

    // 客户端配置
    client: {
      overlay: {
        errors: true,
        warnings: false
      },
      progress: true
    }
  }
}

20. 在实际项目中如何配置 Webpack(完整生产配置示例)?

// webpack.common.js(公共配置)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    clean: true
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    alias: { '@': path.resolve(__dirname, 'src') }
  },
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'esbuild-loader',
          options: {
            target: 'es2020',
            jsx: 'automatic'
          }
        }
      },
      {
        test: /\.(png|jpe?g|gif|webp|svg)$/,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
        generator: { filename: 'images/[name].[hash:8][ext]' }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' })
  ]
}

// webpack.dev.js(开发配置)
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  output: { filename: '[name].js' },
  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true,
    proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }
  },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] },
      { test: /\.less$/, use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'] }
    ]
  }
})

// webpack.prod.js(生产配置)
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  devtool: false,
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }),
    new CompressionPlugin({ algorithm: 'gzip', threshold: 10240 })
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({ parallel: true, terserOptions: { compress: { drop_console: true } } }),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: { test: /node_modules/, name: 'vendors', priority: 10 },
        common: { minChunks: 2, name: 'common', priority: 5, reuseExistingChunk: true }
      }
    },
    runtimeChunk: 'single'
  },
  cache: { type: 'filesystem' }
})

💡 面试加分点: 实际项目中使用 webpack-merge 将配置拆分为公共/开发/生产三份,开发环境用 style-loader(快),生产环境用 MiniCssExtractPlugin.loader(提取 CSS 文件)。