Webpack基础

Webpack基础

学习目标

完成本章学习后,你将能够:

  • 深入理解Webpack的核心概念(entry、output、loader、plugin)和模块化打包原理
  • 熟练配置Webpack开发环境和生产环境,掌握常用loader和plugin的使用方法
  • 理解Webpack的构建流程和生命周期钩子,能够开发自定义loader和plugin
  • 掌握代码分割、懒加载、缓存策略等性能优化技巧,提升应用加载速度
  • 理解Webpack与Vite的区别和各自的适用场景,能够根据项目需求选择合适的构建工具
  • 能够分析和解决Webpack构建过程中的常见问题,优化构建性能
  • 掌握Webpack在企业级项目中的最佳实践和配置模式

前置知识

学习本章内容前,你需要掌握:

问题引入

实际场景

想象你正在开发一个大型电商网站的前端项目。项目使用了Vue 3框架,包含数百个组件文件、样式文件、图片资源,还依赖了几十个第三方npm包。你面临以下挑战:

场景1:模块化开发的困境

javascript 复制代码
// 在浏览器中,你不能直接这样写
import Vue from 'vue';
import axios from 'axios';
import './styles/main.css';
import logo from './assets/logo.png';

// 浏览器不认识这些import语句
// 也不知道如何加载CSS和图片

场景2:代码体积过大

  • 所有代码打包在一起,首页加载需要下载5MB的JavaScript文件
  • 用户访问首页时,也下载了只有管理员才用到的代码
  • 第三方库(如Vue、axios)每次都重新下载,无法利用浏览器缓存

场景3:开发效率低下

  • 每次修改代码都要手动刷新浏览器
  • 无法使用TypeScript、Sass等需要编译的语言
  • 图片资源需要手动优化和压缩

场景4:生产环境优化需求

  • 代码需要压缩混淆以减小体积
  • CSS需要提取到单独文件并添加浏览器前缀
  • 图片需要压缩并转换为WebP格式
  • 需要生成source map用于生产环境调试

为什么需要Webpack

Webpack正是为了解决这些问题而诞生的。它能够:

  1. 模块化打包:将各种类型的资源(JS、CSS、图片等)都视为模块,统一处理
  2. 代码转换:通过loader将TypeScript、Sass等转换为浏览器可识别的代码
  3. 代码分割:将代码拆分成多个bundle,实现按需加载
  4. 资源优化:压缩代码、优化图片、提取公共代码
  5. 开发体验:提供热更新、source map等开发工具
  6. 生态丰富:拥有庞大的loader和plugin生态系统

虽然现在有了Vite等更快的构建工具,但Webpack仍然是企业级项目的主流选择,特别是在需要复杂配置和高度定制的场景下。理解Webpack的工作原理,对于理解现代前端工程化至关重要。

核心概念

一句话定义

Webpack是一个模块打包工具,它将项目中的各种资源(JavaScript、CSS、图片等)视为模块,通过分析模块间的依赖关系,将它们打包成浏览器可以直接运行的静态资源。

通俗理解:Webpack就像一个"资源整理大师",它把你项目中散落的各种文件(代码、样式、图片)按照依赖关系整理打包,最终生成浏览器能够理解和运行的文件。

为什么需要Webpack

  • 解决模块化问题:浏览器原生不支持ES6模块和CommonJS模块,Webpack可以将模块化代码转换为浏览器可执行的代码
  • 统一资源管理:将JavaScript、CSS、图片、字体等各种资源统一视为模块,用统一的方式管理
  • 代码转换和优化:通过loader和plugin实现代码转换(TypeScript→JavaScript)、代码压缩、资源优化等功能
  • 提升开发体验:提供热更新、source map等开发工具,提高开发效率
  • 性能优化:通过代码分割、懒加载、Tree Shaking等技术优化应用性能

本质是什么

Webpack的本质是一个静态模块打包器(static module bundler)。它的工作流程可以概括为:

复制代码
1. 从入口文件开始
2. 递归分析所有依赖的模块
3. 使用loader转换各种类型的文件
4. 使用plugin在构建过程中执行各种任务
5. 最终输出打包后的文件

用一个简单的类比:Webpack就像一个工厂的生产线:

  • 入口(entry):原材料进入工厂的入口
  • loader:生产线上的加工机器,将原材料加工成半成品
  • plugin:生产线上的质检员和包装工,在各个环节执行额外任务
  • 输出(output):最终产品从工厂出来的出口

Webpack的四大核心概念

Webpack的配置围绕四个核心概念展开:Entry(入口)Output(输出)Loader(加载器)Plugin(插件)。理解这四个概念是掌握Webpack的关键。

1. Entry(入口)

定义:Entry指定Webpack从哪个文件开始构建依赖图。

作用:告诉Webpack应用的起点在哪里,Webpack会从这个起点开始递归地找出所有依赖的模块。

配置方式

javascript 复制代码
// webpack.config.js

// 方式1:单入口(字符串)
module.exports = {
  entry: './src/index.js'
};

// 方式2:单入口(对象形式,可以指定chunk名称)
module.exports = {
  entry: {
    main: './src/index.js'
  }
};

// 方式3:多入口(对象形式)
// 适用于多页面应用,每个页面一个入口
module.exports = {
  entry: {
    home: './src/pages/home/index.js',
    about: './src/pages/about/index.js',
    contact: './src/pages/contact/index.js'
  }
};

// 方式4:动态入口(函数形式)
// 可以根据环境变量或其他条件动态返回入口配置
module.exports = {
  entry: () => {
    return {
      main: './src/index.js',
      // 只在生产环境添加polyfill入口
      ...(process.env.NODE_ENV === 'production' ? {
        polyfill: './src/polyfill.js'
      } : {})
    };
  }
};

// 方式5:数组形式(多个文件合并为一个chunk)
// 适用于需要将多个文件打包到一起的场景
module.exports = {
  entry: {
    main: ['./src/polyfill.js', './src/index.js']
    // polyfill.js会先执行,然后是index.js
    // 最终打包成一个bundle
  }
};

实际应用场景

javascript 复制代码
// 场景1:单页面应用(SPA)
// 只有一个入口文件
module.exports = {
  entry: './src/main.js'
};

// 场景2:多页面应用(MPA)
// 每个页面都有独立的入口
module.exports = {
  entry: {
    index: './src/pages/index/main.js',
    product: './src/pages/product/main.js',
    cart: './src/pages/cart/main.js',
    user: './src/pages/user/main.js'
  }
};

// 场景3:提取第三方库
// 将第三方库单独打包,利用浏览器缓存
module.exports = {
  entry: {
    app: './src/main.js',
    vendor: ['vue', 'vue-router', 'axios']
  }
};

// 场景4:根据环境配置不同入口
module.exports = {
  entry: {
    main: './src/main.js',
    // 开发环境添加热更新客户端
    ...(process.env.NODE_ENV === 'development' ? {
      hot: 'webpack-hot-middleware/client'
    } : {})
  }
};

关键点解析

  • 依赖图的起点:Webpack从entry开始,递归分析所有import/require的模块
  • chunk的命名:entry对象的key会成为输出bundle的名称
  • 多入口的独立性:每个入口都会生成独立的依赖图和bundle
  • 数组形式的合并:数组中的多个文件会按顺序合并到一个bundle中
2. Output(输出)

定义:Output指定Webpack打包后的文件输出到哪里,以及如何命名这些文件。

作用:告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件。

配置方式

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

// 基础配置
module.exports = {
  output: {
    // 输出文件的目录(绝对路径)
    path: path.resolve(__dirname, 'dist'),
    
    // 输出文件的名称
    filename: 'bundle.js'
  }
};

// 多入口配置(使用占位符)
module.exports = {
  entry: {
    home: './src/pages/home/index.js',
    about: './src/pages/about/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    
    // [name]会被替换为entry的key(home、about)
    filename: '[name].bundle.js',
    
    // 输出结果:
    // dist/home.bundle.js
    // dist/about.bundle.js
  }
};

// 生产环境配置(添加hash用于缓存控制)
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    
    // [contenthash]:根据文件内容生成hash
    // 内容不变,hash不变,可以利用浏览器缓存
    filename: '[name].[contenthash:8].js',
    
    // 非入口chunk的文件名(如动态导入的模块)
    chunkFilename: '[name].[contenthash:8].chunk.js',
    
    // 输出结果:
    // dist/main.a1b2c3d4.js
    // dist/vendor.e5f6g7h8.js
  }
};

// 完整配置示例
module.exports = {
  output: {
    // 输出目录
    path: path.resolve(__dirname, 'dist'),
    
    // 主bundle的文件名
    filename: '[name].[contenthash:8].js',
    
    // 非入口chunk的文件名
    chunkFilename: '[name].[contenthash:8].chunk.js',
    
    // 静态资源的输出目录(相对于path)
    assetModuleFilename: 'assets/[name].[hash:8][ext]',
    
    // 公共路径(CDN地址或服务器路径)
    publicPath: '/',
    
    // 清理输出目录(Webpack 5+)
    clean: true,
    
    // 输出的库的名称(用于开发库时)
    library: {
      name: 'MyLibrary',
      type: 'umd'
    }
  }
};

常用占位符说明

javascript 复制代码
/**
 * Webpack支持的文件名占位符
 */

// [name]:chunk的名称(entry的key或动态导入时指定的名称)
filename: '[name].js'
// 结果:main.js, vendor.js

// [id]:chunk的唯一标识符
filename: '[id].js'
// 结果:0.js, 1.js, 2.js

// [contenthash]:根据文件内容生成的hash(推荐用于生产环境)
filename: '[name].[contenthash].js'
// 结果:main.a1b2c3d4e5f6g7h8.js
// 内容改变,hash改变;内容不变,hash不变

// [contenthash:8]:只取hash的前8位
filename: '[name].[contenthash:8].js'
// 结果:main.a1b2c3d4.js

// [chunkhash]:根据chunk内容生成的hash
filename: '[name].[chunkhash].js'
// 同一个entry的所有chunk共享相同的chunkhash

// [hash]:根据整个构建过程生成的hash(不推荐)
filename: '[name].[hash].js'
// 任何文件改变,所有文件的hash都会改变

// [ext]:文件扩展名
assetModuleFilename: '[name].[hash:8][ext]'
// 结果:logo.a1b2c3d4.png

// [query]:文件的query参数
assetModuleFilename: '[name][ext][query]'
// 结果:logo.png?v=1.0.0

实际应用场景

javascript 复制代码
// 场景1:开发环境配置
// 不需要hash,方便调试
module.exports = {
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    chunkFilename: '[name].chunk.js',
    publicPath: '/'
  }
};

// 场景2:生产环境配置
// 使用contenthash,利用浏览器缓存
module.exports = {
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:8][ext]',
    publicPath: '/',
    clean: true // 每次构建前清理输出目录
  }
};

// 场景3:CDN部署
// 将静态资源部署到CDN
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
    // CDN地址
    publicPath: 'https://cdn.example.com/assets/',
    // 输出的HTML中的资源路径会自动添加CDN前缀
    // <script src="https://cdn.example.com/assets/main.a1b2c3d4.js"></script>
  }
};

// 场景4:开发库(library)
// 将代码打包成可供其他项目使用的库
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-library.js',
    library: {
      name: 'MyLibrary',
      type: 'umd', // 支持多种模块规范(AMD、CommonJS、全局变量)
      export: 'default' // 导出default
    },
    globalObject: 'this' // 确保在各种环境下都能正常工作
  }
};

// 场景5:多页面应用
// 每个页面输出到独立的目录
module.exports = {
  entry: {
    'pages/home/index': './src/pages/home/index.js',
    'pages/about/index': './src/pages/about/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    // 输出结果:
    // dist/pages/home/index.js
    // dist/pages/about/index.js
  }
};

关键点解析

  • path必须是绝对路径 :使用path.resolve()path.join()生成绝对路径
  • contenthash vs chunkhash vs hash
    • contenthash:根据文件内容生成,推荐用于生产环境
    • chunkhash:根据chunk内容生成,同一entry的chunk共享hash
    • hash:根据整个构建生成,任何改变都会影响所有文件
  • publicPath的作用:指定资源的公共路径,影响HTML中引用资源的路径
  • clean选项:Webpack 5新增,自动清理输出目录,无需额外插件
3. Loader(加载器)

定义:Loader是Webpack用来处理非JavaScript文件的转换器,它可以将各种类型的文件转换为Webpack能够处理的有效模块。

作用:Webpack本身只能理解JavaScript和JSON文件。Loader让Webpack能够处理其他类型的文件(CSS、图片、TypeScript等),并将它们转换为有效的模块。

工作原理

复制代码
原始文件 → Loader处理 → 转换后的模块 → Webpack打包

Loader的执行顺序是从右到左(或从下到上)的链式调用:

javascript 复制代码
// 执行顺序:sass-loader → postcss-loader → css-loader → style-loader
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
      }
    ]
  }
};

配置方式

javascript 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      // 规则1:处理CSS文件
      {
        test: /\.css$/,  // 匹配.css文件
        use: ['style-loader', 'css-loader']  // 使用的loader(从右到左执行)
      },
      
      // 规则2:处理Sass/SCSS文件
      {
        test: /\.scss$/,
        use: [
          'style-loader',   // 将CSS注入到DOM中
          'css-loader',     // 解析CSS文件中的@import和url()
          'postcss-loader', // 添加浏览器前缀等
          'sass-loader'     // 将Sass编译为CSS
        ]
      },
      
      // 规则3:处理图片文件(Webpack 5使用asset module)
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',  // 自动选择导出为单独文件或data URI
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024  // 小于8KB的图片转为base64
          }
        },
        generator: {
          filename: 'images/[name].[hash:8][ext]'  // 输出路径和文件名
        }
      },
      
      // 规则4:处理字体文件
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',  // 导出为单独文件
        generator: {
          filename: 'fonts/[name].[hash:8][ext]'
        }
      },
      
      // 规则5:处理TypeScript文件
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/  // 排除node_modules目录
      },
      
      // 规则6:处理JavaScript文件(使用Babel转译)
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: '> 0.25%, not dead',  // 目标浏览器
                useBuiltIns: 'usage',  // 按需引入polyfill
                corejs: 3
              }]
            ],
            cacheDirectory: true  // 启用缓存,提升构建速度
          }
        }
      }
    ]
  }
};

常用Loader详解

javascript 复制代码
/**
 * 1. 样式相关Loader
 */

// style-loader:将CSS注入到DOM中(通过<style>标签)
// css-loader:解析CSS文件,处理@import和url()
// sass-loader:将Sass/SCSS编译为CSS
// less-loader:将Less编译为CSS
// postcss-loader:使用PostCSS处理CSS(添加前缀、压缩等)

{
  test: /\.scss$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: true,  // 启用CSS Modules
        importLoaders: 2  // 在css-loader前应用的loader数量
      }
    },
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'autoprefixer',  // 自动添加浏览器前缀
            'cssnano'  // 压缩CSS
          ]
        }
      }
    },
    'sass-loader'
  ]
}

/**
 * 2. 文件相关Loader(Webpack 5使用asset module代替)
 */

// Webpack 4及以前使用的loader:
// file-loader:将文件输出到输出目录,返回文件路径
// url-loader:类似file-loader,但可以将小文件转为base64
// raw-loader:将文件内容作为字符串导入

// Webpack 5的asset module(推荐):
{
  test: /\.(png|jpg|gif)$/,
  type: 'asset',  // 自动选择
  // type: 'asset/resource',  // 相当于file-loader
  // type: 'asset/inline',    // 相当于url-loader
  // type: 'asset/source',    // 相当于raw-loader
  parser: {
    dataUrlCondition: {
      maxSize: 8 * 1024  // 8KB以下转base64
    }
  }
}

/**
 * 3. JavaScript转译Loader
 */

// babel-loader:使用Babel转译JavaScript
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        '@babel/preset-env',  // 转译ES6+语法
        '@babel/preset-react',  // 转译React JSX
        '@babel/preset-typescript'  // 转译TypeScript
      ],
      plugins: [
        '@babel/plugin-proposal-class-properties',  // 支持类属性
        '@babel/plugin-transform-runtime'  // 避免重复注入helper代码
      ],
      cacheDirectory: true  // 启用缓存
    }
  }
}

// ts-loader:使用TypeScript编译器转译TypeScript
{
  test: /\.tsx?$/,
  use: 'ts-loader',
  exclude: /node_modules/
}

/**
 * 4. Vue相关Loader
 */

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

// 注意:使用vue-loader需要配合VueLoaderPlugin
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  module: {
    rules: [
      { test: /\.vue$/, use: 'vue-loader' }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
};

/**
 * 5. 其他常用Loader
 */

// eslint-loader:在构建时进行代码检查
{
  test: /\.js$/,
  enforce: 'pre',  // 在其他loader之前执行
  use: 'eslint-loader',
  exclude: /node_modules/
}

// thread-loader:将loader放在worker池中运行,提升构建速度
{
  test: /\.js$/,
  use: [
    'thread-loader',  // 放在其他loader之前
    'babel-loader'
  ]
}

// cache-loader:缓存loader的执行结果
{
  test: /\.js$/,
  use: [
    'cache-loader',  // 放在其他loader之前
    'babel-loader'
  ]
}

Loader的执行顺序

javascript 复制代码
/**
 * Loader的执行顺序示例
 */

// 示例1:处理SCSS文件
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

// 执行流程:
// 1. sass-loader:将SCSS编译为CSS
//    输入:.scss文件
//    输出:CSS代码
//
// 2. css-loader:解析CSS中的@import和url()
//    输入:CSS代码
//    输出:JavaScript模块(包含CSS字符串)
//
// 3. style-loader:将CSS注入到DOM中
//    输入:JavaScript模块
//    输出:在HTML中插入<style>标签

// 示例2:处理TypeScript + React
{
  test: /\.tsx$/,
  use: ['babel-loader', 'ts-loader']
}

// 执行流程:
// 1. ts-loader:将TypeScript编译为JavaScript
//    输入:.tsx文件(TypeScript + JSX)
//    输出:.js文件(JavaScript + JSX)
//
// 2. babel-loader:将JSX和ES6+语法转译为ES5
//    输入:.js文件(JavaScript + JSX)
//    输出:浏览器兼容的JavaScript代码

实际应用场景

javascript 复制代码
// 场景1:Vue 3项目配置
module.exports = {
  module: {
    rules: [
      // 处理.vue文件
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      // 处理JavaScript
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      // 处理CSS
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      // 处理Sass
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
      // 处理图片
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        }
      }
    ]
  }
};

// 场景2:React + TypeScript项目配置
module.exports = {
  module: {
    rules: [
      // 处理TypeScript和TSX
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      // 处理CSS Modules
      {
        test: /\.module\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]--[hash:base64:5]'
              }
            }
          }
        ]
      },
      // 处理普通CSS
      {
        test: /\.css$/,
        exclude: /\.module\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

// 场景3:生产环境优化配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          // 生产环境提取CSS到单独文件
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2,
              sourceMap: true
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  'autoprefixer',
                  ['cssnano', { preset: 'default' }]
                ]
              }
            }
          },
          'sass-loader'
        ]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          // 使用thread-loader加速构建
          'thread-loader',
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
              cacheCompression: false
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    })
  ]
};

关键点解析

  • Loader的本质:Loader是一个函数,接收源文件内容,返回转换后的内容
  • 执行顺序:从右到左(或从下到上)链式调用
  • 配置灵活性:可以使用字符串、对象或数组形式配置loader
  • 性能优化 :使用exclude排除不需要处理的文件,使用cacheDirectory启用缓存
  • Webpack 5的改进:使用asset module代替file-loader、url-loader等
4. Plugin(插件)

定义:Plugin是Webpack的扩展机制,它可以在Webpack构建流程的特定时机执行特定的任务,实现loader无法完成的功能。

作用:Plugin用于执行范围更广的任务,如打包优化、资源管理、环境变量注入等。它可以访问Webpack的完整编译生命周期。

Loader vs Plugin的区别

复制代码
Loader:
- 作用于单个文件
- 在文件被加载时执行
- 用于转换文件内容
- 例如:将TypeScript转为JavaScript

Plugin:
- 作用于整个构建过程
- 在构建的特定时机执行
- 用于执行更广泛的任务
- 例如:压缩代码、提取CSS、生成HTML

配置方式

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

module.exports = {
  plugins: [
    // Plugin需要实例化
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    }),
    
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    }),
    
    new CleanWebpackPlugin()
  ]
};

常用Plugin详解

javascript 复制代码
/**
 * 1. HtmlWebpackPlugin
 * 作用:自动生成HTML文件,并自动引入打包后的资源
 */

const HtmlWebpackPlugin = require('html-webpack-plugin');

// 基础用法
new HtmlWebpackPlugin({
  template: './src/index.html',  // HTML模板文件
  filename: 'index.html',  // 输出的HTML文件名
  inject: 'body',  // 脚本注入位置:'head' | 'body' | true | false
  minify: {
    removeComments: true,  // 移除注释
    collapseWhitespace: true,  // 移除空格
    removeAttributeQuotes: true  // 移除属性引号
  },
  chunks: ['main', 'vendor'],  // 指定要引入的chunk
  chunksSortMode: 'manual'  // chunk排序方式
})

// 多页面应用配置
module.exports = {
  entry: {
    index: './src/pages/index/main.js',
    about: './src/pages/about/main.js'
  },
  plugins: [
    // 为每个页面生成独立的HTML
    new HtmlWebpackPlugin({
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']  // 只引入index chunk
    }),
    new HtmlWebpackPlugin({
      template: './src/pages/about/index.html',
      filename: 'about.html',
      chunks: ['about']  // 只引入about chunk
    })
  ]
};

/**
 * 2. MiniCssExtractPlugin
 * 作用:将CSS提取到单独的文件中(生产环境推荐)
 */

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 开发环境使用style-loader,生产环境使用MiniCssExtractPlugin.loader
          process.env.NODE_ENV === 'production'
            ? MiniCssExtractPlugin.loader
            : 'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',  // 输出文件名
      chunkFilename: 'css/[name].[contenthash:8].chunk.css',  // 非入口chunk的文件名
      ignoreOrder: false  // 是否忽略CSS顺序警告
    })
  ]
};

/**
 * 3. CleanWebpackPlugin
 * 作用:在每次构建前清理输出目录
 * 注意:Webpack 5可以使用output.clean代替
 */

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

new CleanWebpackPlugin({
  cleanOnceBeforeBuildPatterns: ['**/*', '!static-files*'],  // 清理模式
  cleanAfterEveryBuildPatterns: ['!*.json'],  // 构建后保留的文件
  verbose: true,  // 显示日志
  dry: false  // 是否模拟删除(不实际删除)
})

// Webpack 5的替代方案
module.exports = {
  output: {
    clean: true  // 自动清理输出目录
  }
};

/**
 * 4. DefinePlugin
 * 作用:定义全局常量,可以在代码中直接使用
 */

const webpack = require('webpack');

new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('production'),
  'process.env.API_URL': JSON.stringify('https://api.example.com'),
  '__DEV__': JSON.stringify(false),
  'VERSION': JSON.stringify('1.0.0')
})

// 在代码中使用
if (__DEV__) {
  console.log('开发环境');
}

console.log('API地址:', process.env.API_URL);

/**
 * 5. CopyWebpackPlugin
 * 作用:复制文件或目录到输出目录
 */

const CopyWebpackPlugin = require('copy-webpack-plugin');

new CopyWebpackPlugin({
  patterns: [
    {
      from: 'public',  // 源目录
      to: 'assets',  // 目标目录(相对于output.path)
      globOptions: {
        ignore: ['*.DS_Store']  // 忽略的文件
      }
    },
    {
      from: 'src/assets/images',
      to: 'images',
      noErrorOnMissing: true  // 源目录不存在时不报错
    }
  ]
})

/**
 * 6. CompressionWebpackPlugin
 * 作用:生成gzip压缩文件
 */

const CompressionWebpackPlugin = require('compression-webpack-plugin');

new CompressionWebpackPlugin({
  filename: '[path][base].gz',  // 压缩后的文件名
  algorithm: 'gzip',  // 压缩算法
  test: /\.(js|css|html|svg)$/,  // 匹配的文件
  threshold: 10240,  // 只压缩大于10KB的文件
  minRatio: 0.8,  // 压缩比小于0.8才会压缩
  deleteOriginalAssets: false  // 是否删除原文件
})

/**
 * 7. BundleAnalyzerPlugin
 * 作用:可视化分析bundle的大小和组成
 */

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

new BundleAnalyzerPlugin({
  analyzerMode: 'static',  // 生成静态HTML文件
  reportFilename: 'bundle-report.html',
  openAnalyzer: false,  // 是否自动打开浏览器
  generateStatsFile: true,  // 生成stats.json文件
  statsFilename: 'stats.json'
})

/**
 * 8. ProvidePlugin
 * 作用:自动加载模块,无需import
 */

const webpack = require('webpack');

new webpack.ProvidePlugin({
  $: 'jquery',  // 在代码中使用$时,自动import jquery
  jQuery: 'jquery',
  _: 'lodash',
  Vue: ['vue/dist/vue.esm.js', 'default']
})

// 使用后,代码中可以直接使用$,无需import
$('#app').html('Hello');

/**
 * 9. HotModuleReplacementPlugin
 * 作用:启用热模块替换(HMR)
 */

const webpack = require('webpack');

module.exports = {
  devServer: {
    hot: true  // 启用HMR
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

// 在代码中使用HMR API
if (module.hot) {
  module.hot.accept('./module.js', () => {
    // 模块更新时的回调
    console.log('模块已更新');
  });
}

/**
 * 10. ESLintWebpackPlugin
 * 作用:在构建时进行代码检查
 */

const ESLintPlugin = require('eslint-webpack-plugin');

new ESLintPlugin({
  extensions: ['js', 'jsx', 'ts', 'tsx'],  // 检查的文件类型
  exclude: 'node_modules',  // 排除的目录
  fix: true,  // 自动修复
  cache: true,  // 启用缓存
  cacheLocation: '.eslintcache'  // 缓存文件位置
})

Plugin的生命周期钩子

javascript 复制代码
/**
 * Webpack的构建流程和Plugin钩子
 */

// Plugin的基本结构
class MyPlugin {
  apply(compiler) {
    // compiler:Webpack的编译器对象,包含完整的配置信息
    
    // 1. 初始化阶段
    compiler.hooks.initialize.tap('MyPlugin', () => {
      console.log('Webpack初始化');
    });
    
    // 2. 编译开始前
    compiler.hooks.beforeRun.tapAsync('MyPlugin', (compiler, callback) => {
      console.log('开始编译前');
      callback();
    });
    
    // 3. 编译开始
    compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
      console.log('开始编译');
      callback();
    });
    
    // 4. 编译过程中
    compiler.hooks.compile.tap('MyPlugin', (params) => {
      console.log('编译中');
    });
    
    // 5. 生成资源到输出目录前
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      console.log('生成资源');
      
      // compilation:包含当前编译的所有信息
      // 可以访问所有模块、chunk、资源等
      
      // 遍历所有生成的资源
      for (const filename in compilation.assets) {
        console.log('生成文件:', filename);
      }
      
      callback();
    });
    
    // 6. 资源已输出到输出目录
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('编译完成');
      
      // stats:包含编译统计信息
      console.log('编译耗时:', stats.endTime - stats.startTime, 'ms');
    });
  }
}

// 使用自定义Plugin
module.exports = {
  plugins: [
    new MyPlugin()
  ]
};

实际应用场景

javascript 复制代码
// 场景1:开发环境配置
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  plugins: [
    // 生成HTML
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    
    // 定义环境变量
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('development')
    }),
    
    // 启用HMR
    new webpack.HotModuleReplacementPlugin()
  ]
};

// 场景2:生产环境配置
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 CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
  mode: 'production',
  plugins: [
    // 生成HTML
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      }
    }),
    
    // 提取CSS
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    }),
    
    // 生成gzip文件
    new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html)$/,
      threshold: 10240,
      minRatio: 0.8
    })
  ],
  optimization: {
    minimizer: [
      // 压缩JavaScript
      new TerserPlugin({
        parallel: true,  // 多进程并行压缩
        terserOptions: {
          compress: {
            drop_console: true  // 移除console
          }
        }
      }),
      
      // 压缩CSS
      new CssMinimizerPlugin()
    ]
  }
};

// 场景3:多页面应用配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pages = ['index', 'about', 'contact'];

module.exports = {
  entry: pages.reduce((entry, page) => {
    entry[page] = `./src/pages/${page}/main.js`;
    return entry;
  }, {}),
  
  plugins: [
    // 为每个页面生成HTML
    ...pages.map(page => new HtmlWebpackPlugin({
      template: `./src/pages/${page}/index.html`,
      filename: `${page}.html`,
      chunks: [page, 'vendor', 'common']
    }))
  ]
};

// 场景4:性能分析配置
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');

const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  plugins: [
    // 分析bundle大小
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false
    })
  ]
});

关键点解析

  • Plugin的本质 :Plugin是一个具有apply方法的类或对象
  • 生命周期钩子:Plugin通过监听Webpack的生命周期钩子来执行任务
  • Tapable机制:Webpack使用Tapable库实现事件流机制
  • 配置方式:Plugin需要实例化后添加到plugins数组中
  • 执行时机:Plugin可以在构建的任何阶段执行,比Loader更灵活

Webpack的构建流程

理解Webpack的构建流程对于深入掌握Webpack至关重要。Webpack的构建过程可以分为以下几个阶段:
初始化参数
开始编译
确定入口
编译模块
完成模块编译
输出资源
输出完成
调用Loader转换
递归处理依赖
根据依赖关系组装Chunk
将Chunk转换为文件
输出到文件系统

详细流程说明

javascript 复制代码
/**
 * 1. 初始化参数阶段
 * 
 * 从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
 */

// webpack.config.js中的配置
const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
};

// 命令行参数
// webpack --mode production --config webpack.prod.js

// 最终合并后的配置
const finalConfig = {
  ...config,
  mode: 'production',
  // ... 其他默认配置
};

/**
 * 2. 开始编译阶段
 * 
 * 用上一步得到的参数初始化Compiler对象,加载所有配置的插件,
 * 执行对象的run方法开始执行编译
 */

class Compiler {
  constructor(options) {
    this.options = options;
    this.hooks = {
      run: new SyncHook(),
      compile: new SyncHook(),
      emit: new AsyncSeriesHook(),
      done: new SyncHook()
    };
  }
  
  run(callback) {
    // 触发run钩子
    this.hooks.run.call();
    
    // 开始编译
    this.compile((err, compilation) => {
      if (err) return callback(err);
      
      // 输出资源
      this.emitAssets(compilation, (err) => {
        if (err) return callback(err);
        
        // 触发done钩子
        this.hooks.done.call();
        callback(null);
      });
    });
  }
}

/**
 * 3. 确定入口阶段
 * 
 * 根据配置中的entry找出所有的入口文件
 */

// 单入口
entry: './src/index.js'
// 结果:['./src/index.js']

// 多入口
entry: {
  home: './src/pages/home/index.js',
  about: './src/pages/about/index.js'
}
// 结果:['./src/pages/home/index.js', './src/pages/about/index.js']

/**
 * 4. 编译模块阶段
 * 
 * 从入口文件出发,调用所有配置的Loader对模块进行转换,
 * 再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
 */

// 伪代码示例
function buildModule(modulePath) {
  // 1. 读取文件内容
  let source = fs.readFileSync(modulePath, 'utf-8');
  
  // 2. 找到匹配的loader规则
  const rule = findMatchingRule(modulePath);
  
  // 3. 使用loader转换源代码(从右到左执行)
  if (rule && rule.use) {
    for (let i = rule.use.length - 1; i >= 0; i--) {
      const loader = rule.use[i];
      source = loader(source);
    }
  }
  
  // 4. 解析转换后的代码,找出依赖的模块
  const dependencies = parseDependencies(source);
  
  // 5. 递归处理依赖的模块
  dependencies.forEach(dep => {
    buildModule(dep);
  });
  
  // 6. 返回处理后的模块
  return {
    path: modulePath,
    source: source,
    dependencies: dependencies
  };
}

/**
 * 5. 完成模块编译阶段
 * 
 * 在经过第4步使用Loader转换完所有模块后,得到了每个模块被转换后的最终内容
 * 以及它们之间的依赖关系
 */

// 模块依赖图示例
const moduleGraph = {
  './src/index.js': {
    source: '...',
    dependencies: ['./src/utils.js', './src/components/App.vue']
  },
  './src/utils.js': {
    source: '...',
    dependencies: ['lodash']
  },
  './src/components/App.vue': {
    source: '...',
    dependencies: ['vue', './src/components/Header.vue']
  }
};

/**
 * 6. 输出资源阶段
 * 
 * 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,
 * 再把每个Chunk转换成一个单独的文件加入到输出列表
 */

// 伪代码示例
function createChunks(entryModules, moduleGraph) {
  const chunks = [];
  
  // 为每个入口创建一个chunk
  entryModules.forEach(entryModule => {
    const chunk = {
      name: entryModule.name,
      modules: []
    };
    
    // 收集该入口依赖的所有模块
    function collectModules(modulePath) {
      if (chunk.modules.includes(modulePath)) return;
      
      chunk.modules.push(modulePath);
      
      const module = moduleGraph[modulePath];
      module.dependencies.forEach(dep => {
        collectModules(dep);
      });
    }
    
    collectModules(entryModule.path);
    chunks.push(chunk);
  });
  
  return chunks;
}

/**
 * 7. 输出完成阶段
 * 
 * 在确定好输出内容后,根据配置确定输出的路径和文件名,
 * 把文件内容写入到文件系统
 */

function emitFiles(chunks, outputPath) {
  chunks.forEach(chunk => {
    // 生成文件内容
    const fileContent = generateFileContent(chunk);
    
    // 确定文件名
    const fileName = chunk.name + '.js';
    
    // 写入文件系统
    const filePath = path.join(outputPath, fileName);
    fs.writeFileSync(filePath, fileContent);
    
    console.log(`输出文件: ${filePath}`);
  });
}

构建流程中的关键概念

javascript 复制代码
/**
 * Module(模块)
 * 
 * Webpack中的一切皆模块,每个文件都是一个模块
 */

// JavaScript模块
import utils from './utils.js';

// CSS模块
import './styles.css';

// 图片模块
import logo from './logo.png';

// Vue组件模块
import App from './App.vue';

/**
 * Chunk(代码块)
 * 
 * Chunk是Webpack打包过程中的一组模块的集合
 */

// 入口chunk:由entry配置生成
entry: {
  main: './src/index.js',
  vendor: './src/vendor.js'
}
// 生成两个chunk:main chunk和vendor chunk

// 异步chunk:由动态导入生成
import(/* webpackChunkName: "about" */ './pages/about.js')
// 生成一个名为about的chunk

// 公共chunk:由optimization.splitChunks生成
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendor'
      }
    }
  }
}
// 将node_modules中的模块提取到vendor chunk

/**
 * Bundle(打包文件)
 * 
 * Bundle是Webpack最终输出的文件,一个chunk对应一个bundle
 */

// 输出配置
output: {
  filename: '[name].[contenthash:8].js',
  chunkFilename: '[name].[contenthash:8].chunk.js'
}

// 生成的bundle文件:
// main.a1b2c3d4.js
// vendor.e5f6g7h8.js
// about.i9j0k1l2.chunk.js

/**
 * Dependency Graph(依赖图)
 * 
 * Webpack通过分析模块间的依赖关系,构建出完整的依赖图
 */

// 示例依赖图
const dependencyGraph = {
  'src/index.js': {
    dependencies: [
      'src/App.vue',
      'src/router/index.js',
      'src/store/index.js'
    ]
  },
  'src/App.vue': {
    dependencies: [
      'vue',
      'src/components/Header.vue',
      'src/components/Footer.vue'
    ]
  },
  'src/router/index.js': {
    dependencies: [
      'vue-router',
      'src/views/Home.vue',
      'src/views/About.vue'
    ]
  }
};

Webpack的事件流机制

javascript 复制代码
/**
 * Webpack使用Tapable库实现事件流机制
 * 
 * Tapable提供了多种类型的钩子:
 * - SyncHook:同步钩子
 * - AsyncSeriesHook:异步串行钩子
 * - AsyncParallelHook:异步并行钩子
 */

const { SyncHook, AsyncSeriesHook } = require('tapable');

class Compiler {
  constructor() {
    this.hooks = {
      // 同步钩子
      run: new SyncHook(['compiler']),
      compile: new SyncHook(['params']),
      
      // 异步钩子
      emit: new AsyncSeriesHook(['compilation']),
      done: new AsyncSeriesHook(['stats'])
    };
  }
  
  run() {
    // 触发run钩子
    this.hooks.run.call(this);
    
    // 触发compile钩子
    this.hooks.compile.call({ /* params */ });
    
    // 触发emit钩子(异步)
    this.hooks.emit.callAsync(compilation, (err) => {
      if (err) return;
      
      // 触发done钩子(异步)
      this.hooks.done.callAsync(stats, (err) => {
        // 构建完成
      });
    });
  }
}

// Plugin监听钩子
class MyPlugin {
  apply(compiler) {
    // 监听同步钩子
    compiler.hooks.run.tap('MyPlugin', (compiler) => {
      console.log('开始编译');
    });
    
    // 监听异步钩子
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      console.log('生成资源');
      // 执行异步操作
      setTimeout(() => {
        callback();
      }, 1000);
    });
    
    // 使用Promise监听异步钩子
    compiler.hooks.done.tapPromise('MyPlugin', (stats) => {
      return new Promise((resolve) => {
        console.log('编译完成');
        resolve();
      });
    });
  }
}

构建流程的性能优化点

javascript 复制代码
/**
 * 1. 缩小文件搜索范围
 */

module.exports = {
  resolve: {
    // 指定模块搜索目录
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    
    // 指定文件扩展名
    extensions: ['.js', '.vue', '.json'],
    
    // 指定主文件名
    mainFiles: ['index'],
    
    // 配置别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        // 只处理src目录
        include: path.resolve(__dirname, 'src'),
        // 排除node_modules
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  }
};

/**
 * 2. 使用缓存
 */

module.exports = {
  cache: {
    type: 'filesystem',  // 使用文件系统缓存
    cacheDirectory: path.resolve(__dirname, '.webpack_cache')
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true  // 启用Babel缓存
          }
        }
      }
    ]
  }
};

/**
 * 3. 多进程构建
 */

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'thread-loader',  // 使用多进程处理
          'babel-loader'
        ]
      }
    ]
  },
  
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true  // 多进程压缩
      })
    ]
  }
};

/**
 * 4. 减少模块解析
 */

module.exports = {
  module: {
    // 不解析这些模块的依赖
    noParse: /jquery|lodash/,
    
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        // 排除已经编译好的库
        exclude: /node_modules/
      }
    ]
  }
};

关键点解析

  • 构建流程是串行的:从初始化到输出,按顺序执行
  • 模块处理是递归的:从入口开始,递归处理所有依赖
  • 事件流机制:通过Tapable实现,Plugin通过监听钩子来介入构建过程
  • 性能优化:缩小搜索范围、使用缓存、多进程构建是关键

代码分割与懒加载

代码分割(Code Splitting)是Webpack最重要的性能优化手段之一。它可以将代码拆分成多个bundle,实现按需加载,减少首屏加载时间。

代码分割的三种方式
javascript 复制代码
/**
 * 方式1:多入口配置
 * 
 * 通过配置多个entry,将代码分割成多个bundle
 */

module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

// 输出结果:
// dist/main.bundle.js
// dist/admin.bundle.js

/**
 * 方式2:动态导入(推荐)
 * 
 * 使用import()语法动态导入模块,Webpack会自动进行代码分割
 */

// 普通导入(会打包到主bundle中)
import utils from './utils.js';

// 动态导入(会分割成独立的chunk)
import('./utils.js').then(module => {
  const utils = module.default;
  utils.doSomething();
});

// 使用async/await语法
async function loadUtils() {
  const module = await import('./utils.js');
  const utils = module.default;
  return utils;
}

// 指定chunk名称
import(
  /* webpackChunkName: "utils" */
  './utils.js'
).then(module => {
  // ...
});

// 预加载(prefetch):浏览器空闲时加载
import(
  /* webpackPrefetch: true */
  './utils.js'
);

// 预获取(preload):与父chunk并行加载
import(
  /* webpackPreload: true */
  './utils.js'
);

/**
 * 方式3:SplitChunksPlugin(自动分割)
 * 
 * Webpack 4+内置的代码分割插件,可以自动提取公共代码
 */

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',  // 对所有chunk进行分割
      minSize: 20000,  // 最小chunk大小(字节)
      minChunks: 1,  // 最少被引用次数
      maxAsyncRequests: 30,  // 最大异步请求数
      maxInitialRequests: 30,  // 最大初始请求数
      cacheGroups: {
        // 提取第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10  // 优先级
        },
        // 提取公共代码
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true  // 复用已存在的chunk
        }
      }
    }
  }
};
实际应用场景
javascript 复制代码
/**
 * 场景1:路由懒加载(Vue Router)
 */

// 不推荐:所有路由组件都打包到主bundle
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/contact', component: Contact }
];

// 推荐:路由组件按需加载
const routes = [
  {
    path: '/',
    component: () => import(
      /* webpackChunkName: "home" */
      './views/Home.vue'
    )
  },
  {
    path: '/about',
    component: () => import(
      /* webpackChunkName: "about" */
      './views/About.vue'
    )
  },
  {
    path: '/contact',
    component: () => import(
      /* webpackChunkName: "contact" */
      './views/Contact.vue'
    )
  }
];

// 结果:每个路由组件都会生成独立的chunk
// home.chunk.js, about.chunk.js, contact.chunk.js

/**
 * 场景2:组件懒加载(React)
 */

import React, { lazy, Suspense } from 'react';

// 懒加载组件
const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

/**
 * 场景3:条件加载
 * 
 * 根据条件动态加载不同的模块
 */

async function loadEditor(type) {
  if (type === 'rich') {
    // 只有需要富文本编辑器时才加载
    const module = await import(
      /* webpackChunkName: "rich-editor" */
      './editors/RichEditor.js'
    );
    return module.default;
  } else {
    // 加载简单编辑器
    const module = await import(
      /* webpackChunkName: "simple-editor" */
      './editors/SimpleEditor.js'
    );
    return module.default;
  }
}

// 使用
const editor = await loadEditor('rich');
editor.init();

/**
 * 场景4:第三方库按需加载
 */

// 不推荐:一次性加载整个lodash库
import _ from 'lodash';
_.debounce(fn, 300);

// 推荐:只加载需要的函数
import debounce from 'lodash/debounce';
debounce(fn, 300);

// 或者使用动态导入
async function useLodash() {
  const { default: _ } = await import('lodash');
  return _.debounce(fn, 300);
}

/**
 * 场景5:提取公共代码
 */

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 提取Vue全家桶
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
          name: 'vue-vendor',
          priority: 20
        },
        // 提取UI库
        ui: {
          test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
          name: 'ui-vendor',
          priority: 15
        },
        // 提取其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        },
        // 提取公共业务代码
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};

// 输出结果:
// vue-vendor.js(Vue、Vue Router、Pinia)
// ui-vendor.js(Element Plus或Ant Design Vue)
// vendor.js(其他第三方库)
// common.js(公共业务代码)
// main.js(入口代码)
SplitChunksPlugin详细配置
javascript 复制代码
/**
 * SplitChunksPlugin完整配置说明
 */

module.exports = {
  optimization: {
    splitChunks: {
      // chunks:指定哪些chunk需要优化
      // 'all':所有chunk(推荐)
      // 'async':只优化异步chunk(默认)
      // 'initial':只优化初始chunk
      chunks: 'all',
      
      // minSize:生成chunk的最小大小(字节)
      // 小于这个大小的chunk不会被分割
      minSize: 20000,  // 20KB
      
      // minRemainingSize:确保分割后剩余的chunk大小
      // Webpack 5新增,避免分割后产生过小的chunk
      minRemainingSize: 0,
      
      // minChunks:模块被引用的最少次数
      // 只有被引用次数达到这个值的模块才会被分割
      minChunks: 1,
      
      // maxAsyncRequests:按需加载时的最大并行请求数
      // 超过这个数量的chunk不会被分割
      maxAsyncRequests: 30,
      
      // maxInitialRequests:入口点的最大并行请求数
      // 超过这个数量的chunk不会被分割
      maxInitialRequests: 30,
      
      // enforceSizeThreshold:强制执行分割的大小阈值
      // 超过这个大小的chunk会忽略其他限制强制分割
      enforceSizeThreshold: 50000,  // 50KB
      
      // cacheGroups:缓存组配置
      // 可以继承和覆盖splitChunks的所有选项
      cacheGroups: {
        // 默认的vendor组
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,  // 优先级(数字越大优先级越高)
          reuseExistingChunk: true  // 复用已存在的chunk
        },
        
        // 默认的default组
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        
        // 自定义组:提取React相关库
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react-vendor',
          priority: 10,
          enforce: true  // 忽略minSize、minChunks等限制
        },
        
        // 自定义组:提取样式文件
        styles: {
          test: /\.css$/,
          name: 'styles',
          type: 'css/mini-extract',  // 指定chunk类型
          chunks: 'all',
          enforce: true
        },
        
        // 自定义组:提取公共业务代码
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true,
          // 自定义文件名
          filename: 'js/[name].[contenthash:8].js'
        }
      }
    }
  }
};
懒加载的最佳实践
javascript 复制代码
/**
 * 1. 路由级别的代码分割
 */

// Vue Router配置
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/user',
    component: () => import('./views/User.vue'),
    children: [
      {
        path: 'profile',
        component: () => import('./views/user/Profile.vue')
      },
      {
        path: 'settings',
        component: () => import('./views/user/Settings.vue')
      }
    ]
  }
];

/**
 * 2. 组件级别的代码分割
 */

// Vue 3异步组件
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
);

// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingComponent,  // 加载中显示的组件
  errorComponent: ErrorComponent,  // 加载失败显示的组件
  delay: 200,  // 延迟显示loading组件的时间
  timeout: 3000  // 超时时间
});

/**
 * 3. 功能模块的代码分割
 */

// 图表库按需加载
async function loadChart() {
  const { default: echarts } = await import('echarts');
  return echarts;
}

// 使用
button.addEventListener('click', async () => {
  const echarts = await loadChart();
  const chart = echarts.init(document.getElementById('chart'));
  chart.setOption(/* ... */);
});

/**
 * 4. 预加载和预获取
 */

// prefetch:浏览器空闲时加载,用于未来可能需要的资源
import(
  /* webpackPrefetch: true */
  './future-module.js'
);
// 生成:<link rel="prefetch" href="future-module.js">

// preload:与父chunk并行加载,用于当前页面必需的资源
import(
  /* webpackPreload: true */
  './critical-module.js'
);
// 生成:<link rel="preload" href="critical-module.js">

/**
 * 5. 魔法注释(Magic Comments)
 */

// webpackChunkName:指定chunk名称
import(/* webpackChunkName: "my-chunk" */ './module.js');

// webpackMode:指定加载模式
// 'lazy':默认,懒加载
// 'lazy-once':生成单个chunk,可以满足多个import()
// 'eager':不生成额外chunk,打包到当前chunk
// 'weak':尝试加载模块,如果模块已经加载则使用,否则不加载
import(/* webpackMode: "lazy" */ './module.js');

// webpackPrefetch:预加载
import(/* webpackPrefetch: true */ './module.js');

// webpackPreload:预获取
import(/* webpackPreload: true */ './module.js');

// webpackInclude:包含的文件
import(
  /* webpackInclude: /\.json$/ */
  `./locale/${language}.json`
);

// webpackExclude:排除的文件
import(
  /* webpackExclude: /\.test\.js$/ */
  `./components/${name}.js`
);

// 组合使用
import(
  /* webpackChunkName: "locale-[request]" */
  /* webpackMode: "lazy-once" */
  /* webpackInclude: /\.json$/ */
  `./locale/${language}.json`
);
代码分割的性能优化
javascript 复制代码
/**
 * 1. 合理设置chunk大小
 */

module.exports = {
  optimization: {
    splitChunks: {
      // 最小chunk大小:20KB
      minSize: 20000,
      
      // 最大chunk大小:244KB
      // 超过这个大小的chunk会被进一步分割
      maxSize: 244000,
      
      // 最大异步chunk大小
      maxAsyncSize: 244000,
      
      // 最大初始chunk大小
      maxInitialSize: 244000
    }
  }
};

/**
 * 2. 控制并行请求数
 */

module.exports = {
  optimization: {
    splitChunks: {
      // 按需加载时的最大并行请求数
      // 过多的并行请求会影响性能
      maxAsyncRequests: 6,
      
      // 入口点的最大并行请求数
      maxInitialRequests: 4
    }
  }
};

/**
 * 3. 使用runtime chunk
 */

module.exports = {
  optimization: {
    // 将runtime代码提取到单独的chunk
    // runtime包含Webpack的模块加载逻辑
    runtimeChunk: {
      name: 'runtime'
    }
    
    // 或者为每个入口生成独立的runtime
    // runtimeChunk: 'single'
  }
};

/**
 * 4. 优化第三方库的分割
 */

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 将大型库单独分割
        lodash: {
          test: /[\\/]node_modules[\\/]lodash[\\/]/,
          name: 'lodash',
          priority: 20
        },
        
        // 将经常变化的库单独分割
        // 这样其他库的缓存不会因为这个库的更新而失效
        frequentlyUpdated: {
          test: /[\\/]node_modules[\\/](axios|some-lib)[\\/]/,
          name: 'frequently-updated',
          priority: 15
        },
        
        // 将稳定的库合并
        // 这些库很少更新,可以充分利用浏览器缓存
        stable: {
          test: /[\\/]node_modules[\\/](vue|react)[\\/]/,
          name: 'stable-vendor',
          priority: 10
        }
      }
    }
  }
};

关键点解析

  • 代码分割的目的:减少首屏加载时间,提升用户体验
  • 三种分割方式:多入口、动态导入、SplitChunksPlugin
  • 动态导入是最灵活的方式:可以实现真正的按需加载
  • 合理配置chunk大小:太小会增加请求数,太大会影响加载速度
  • 利用浏览器缓存:将稳定的第三方库单独分割,充分利用缓存

开发环境与生产环境配置

Webpack需要针对开发环境和生产环境进行不同的配置。开发环境注重开发体验和调试便利性,生产环境注重性能优化和代码安全。

环境配置的组织方式
javascript 复制代码
/**
 * 方式1:单配置文件 + 环境变量
 */

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

// 判断是否为生产环境
const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
  mode: isProduction ? 'production' : 'development',
  
  // 开发环境使用eval-source-map,生产环境使用source-map
  devtool: isProduction ? 'source-map' : 'eval-source-map',
  
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    // 生产环境添加contenthash
    filename: isProduction
      ? 'js/[name].[contenthash:8].js'
      : 'js/[name].js',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 生产环境提取CSS,开发环境使用style-loader
          isProduction
            ? MiniCssExtractPlugin.loader
            : 'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    // 生产环境才使用的插件
    ...(isProduction ? [
      new MiniCssExtractPlugin({
        filename: 'css/[name].[contenthash:8].css'
      })
    ] : [])
  ],
  
  // 开发服务器配置
  devServer: isProduction ? undefined : {
    port: 3000,
    hot: true,
    open: true
  }
};

/**
 * 方式2:多配置文件(推荐)
 */

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

module.exports = {
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist')
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        }
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
};

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

module.exports = merge(common, {
  mode: 'development',
  
  devtool: 'eval-source-map',
  
  output: {
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js'
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
    ]
  },
  
  devServer: {
    port: 3000,
    hot: true,
    open: true,
    compress: true,
    historyApiFallback: true,  // 支持HTML5 History API
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
});

// 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 CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  
  devtool: 'source-map',
  
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:8][ext]',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    }),
    
    new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    })
  ],
  
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        },
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    runtimeChunk: {
      name: 'runtime'
    }
  },
  
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,  // 入口文件最大大小(字节)
    maxAssetSize: 512000  // 资源文件最大大小(字节)
  }
});

// package.json中的脚本
{
  "scripts": {
    "dev": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  }
}
开发环境优化配置
javascript 复制代码
/**
 * 开发环境配置重点:
 * 1. 快速的构建速度
 * 2. 良好的调试体验
 * 3. 热更新支持
 */

module.exports = {
  mode: 'development',
  
  // 1. Source Map配置
  // eval-source-map:构建速度快,调试体验好
  devtool: 'eval-source-map',
  
  // 2. 缓存配置
  cache: {
    type: 'filesystem',  // 使用文件系统缓存
    cacheDirectory: path.resolve(__dirname, '.webpack_cache'),
    buildDependencies: {
      config: [__filename]  // 配置文件改变时重新构建
    }
  },
  
  // 3. 模块解析优化
  resolve: {
    // 减少文件扩展名尝试
    extensions: ['.js', '.vue', '.json'],
    
    // 配置别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'components': path.resolve(__dirname, 'src/components')
    },
    
    // 指定模块搜索目录
    modules: [path.resolve(__dirname, 'src'), 'node_modules']
  },
  
  // 4. 开发服务器配置
  devServer: {
    port: 3000,
    hot: true,  // 启用热更新
    open: true,  // 自动打开浏览器
    compress: true,  // 启用gzip压缩
    
    // 支持HTML5 History API
    historyApiFallback: true,
    
    // 代理配置
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    },
    
    // 静态文件目录
    static: {
      directory: path.join(__dirname, 'public')
    },
    
    // 客户端日志级别
    client: {
      logging: 'info',
      overlay: {
        errors: true,
        warnings: false
      },
      progress: true
    }
  },
  
  // 5. 优化配置
  optimization: {
    // 开发环境不压缩代码
    minimize: false,
    
    // 使用可读的模块ID
    moduleIds: 'named',
    chunkIds: 'named'
  },
  
  // 6. 性能提示
  performance: {
    hints: false  // 关闭性能提示
  },
  
  // 7. 统计信息
  stats: {
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }
};
生产环境优化配置
javascript 复制代码
/**
 * 生产环境配置重点:
 * 1. 代码压缩和优化
 * 2. 资源优化和缓存
 * 3. 安全性和稳定性
 */

const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  mode: 'production',
  
  // 1. Source Map配置
  // source-map:完整的source map,用于生产环境调试
  devtool: 'source-map',
  
  // 2. 输出配置
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:8][ext]',
    clean: true,  // 清理输出目录
    
    // CDN配置
    publicPath: process.env.CDN_URL || '/'
  },
  
  // 3. 优化配置
  optimization: {
    // 启用压缩
    minimize: true,
    
    // 压缩器配置
    minimizer: [
      // JavaScript压缩
      new TerserPlugin({
        parallel: true,  // 多进程并行压缩
        extractComments: false,  // 不提取注释到单独文件
        terserOptions: {
          compress: {
            drop_console: true,  // 移除console
            drop_debugger: true,  // 移除debugger
            pure_funcs: ['console.log']  // 移除特定函数调用
          },
          format: {
            comments: false  // 移除注释
          }
        }
      }),
      
      // CSS压缩
      new CssMinimizerPlugin({
        parallel: true,
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true }
            }
          ]
        }
      })
    ],
    
    // 代码分割
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        // Vue全家桶
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
          name: 'vue-vendor',
          priority: 20
        },
        // UI库
        ui: {
          test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
          name: 'ui-vendor',
          priority: 15
        },
        // 其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        },
        // 公共代码
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    // 提取runtime代码
    runtimeChunk: {
      name: 'runtime'
    },
    
    // 模块ID生成策略
    moduleIds: 'deterministic',  // 确定性的模块ID
    chunkIds: 'deterministic'  // 确定性的chunk ID
  },
  
  // 4. 插件配置
  plugins: [
    // 提取CSS
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    }),
    
    // 生成gzip文件
    new CompressionWebpackPlugin({
      filename: '[path][base].gz',
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,  // 只压缩大于10KB的文件
      minRatio: 0.8,  // 压缩比小于0.8才会压缩
      deleteOriginalAssets: false  // 保留原文件
    }),
    
    // 分析bundle大小(可选)
    process.env.ANALYZE && new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false
    })
  ].filter(Boolean),
  
  // 5. 性能提示
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,  // 512KB
    maxAssetSize: 512000,
    assetFilter: function(assetFilename) {
      // 只对JavaScript和CSS文件进行性能提示
      return /\.(js|css)$/.test(assetFilename);
    }
  },
  
  // 6. 统计信息
  stats: {
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false,
    entrypoints: false
  }
};
环境变量管理
javascript 复制代码
/**
 * 使用dotenv管理环境变量
 */

// 安装依赖
// npm install dotenv-webpack --save-dev

// .env.development
NODE_ENV=development
API_URL=http://localhost:8080/api
CDN_URL=http://localhost:3000

// .env.production
NODE_ENV=production
API_URL=https://api.example.com
CDN_URL=https://cdn.example.com

// webpack.config.js
const Dotenv = require('dotenv-webpack');

module.exports = {
  plugins: [
    new Dotenv({
      path: `./.env.${process.env.NODE_ENV}`
    })
  ]
};

// 在代码中使用
console.log(process.env.API_URL);
console.log(process.env.CDN_URL);

/**
 * 使用DefinePlugin定义全局常量
 */

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
      '__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
      '__PROD__': JSON.stringify(process.env.NODE_ENV === 'production'),
      'VERSION': JSON.stringify(require('./package.json').version)
    })
  ]
};

// 在代码中使用
if (__DEV__) {
  console.log('开发环境');
}

if (__PROD__) {
  console.log('生产环境,版本:', VERSION);
}

fetch(process.env.API_URL + '/users');

关键点解析

  • 开发环境重点:快速构建、良好调试、热更新
  • 生产环境重点:代码压缩、资源优化、缓存策略
  • 配置组织:使用webpack-merge合并公共配置和环境特定配置
  • 环境变量:使用dotenv或DefinePlugin管理不同环境的配置
  • Source Map:开发环境使用eval-source-map,生产环境使用source-map

最佳实践

企业级应用场景

场景1:大型单页应用(SPA)优化
javascript 复制代码
/**
 * 问题:大型SPA首屏加载慢,bundle体积过大
 * 
 * 解决方案:
 * 1. 路由懒加载
 * 2. 组件懒加载
 * 3. 第三方库按需引入
 * 4. 代码分割和预加载
 */

// 1. 路由懒加载配置
const routes = [
  {
    path: '/',
    component: () => import(
      /* webpackChunkName: "home" */
      /* webpackPrefetch: true */
      './views/Home.vue'
    )
  },
  {
    path: '/dashboard',
    component: () => import(
      /* webpackChunkName: "dashboard" */
      './views/Dashboard.vue'
    ),
    children: [
      {
        path: 'analytics',
        component: () => import(
          /* webpackChunkName: "dashboard-analytics" */
          './views/dashboard/Analytics.vue'
        )
      },
      {
        path: 'reports',
        component: () => import(
          /* webpackChunkName: "dashboard-reports" */
          './views/dashboard/Reports.vue'
        )
      }
    ]
  }
];

// 2. 组件懒加载
import { defineAsyncComponent } from 'vue';

export default {
  components: {
    // 懒加载重型组件
    HeavyChart: defineAsyncComponent(() =>
      import('./components/HeavyChart.vue')
    ),
    
    // 带加载状态的懒加载
    DataTable: defineAsyncComponent({
      loader: () => import('./components/DataTable.vue'),
      loadingComponent: LoadingSpinner,
      delay: 200,
      timeout: 3000
    })
  }
};

// 3. 第三方库按需引入
// 不推荐:全量引入
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
app.use(ElementPlus);

// 推荐:按需引入
import { ElButton, ElInput, ElTable } from 'element-plus';
app.component('ElButton', ElButton);
app.component('ElInput', ElInput);
app.component('ElTable', ElTable);

// 或使用unplugin-vue-components自动按需引入
// vite.config.js
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default {
  plugins: [
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
};

// 4. Webpack配置优化
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 提取Vue核心库
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
          name: 'vue-vendor',
          priority: 20
        },
        // 提取UI库
        ui: {
          test: /[\\/]node_modules[\\/]element-plus[\\/]/,
          name: 'ui-vendor',
          priority: 15
        },
        // 提取图表库
        charts: {
          test: /[\\/]node_modules[\\/](echarts|@antv)[\\/]/,
          name: 'charts-vendor',
          priority: 12
        },
        // 提取其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        }
      }
    },
    
    // 提取runtime代码
    runtimeChunk: {
      name: 'runtime'
    }
  }
};
场景2:多页面应用(MPA)配置
javascript 复制代码
/**
 * 问题:多个页面需要独立的入口和HTML文件
 * 
 * 解决方案:
 * 1. 配置多个entry
 * 2. 为每个页面生成独立的HTML
 * 3. 提取公共代码
 */

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const glob = require('glob');

// 自动扫描pages目录下的所有页面
function getEntries() {
  const entries = {};
  const htmlPlugins = [];
  
  // 扫描src/pages目录
  const pages = glob.sync('./src/pages/*/index.js');
  
  pages.forEach(page => {
    // 提取页面名称
    const pageName = page.match(/pages\/(.+)\/index\.js$/)[1];
    
    // 配置entry
    entries[pageName] = page;
    
    // 配置HtmlWebpackPlugin
    htmlPlugins.push(
      new HtmlWebpackPlugin({
        template: `./src/pages/${pageName}/index.html`,
        filename: `${pageName}.html`,
        chunks: ['runtime', 'vendor', 'common', pageName],
        minify: {
          removeComments: true,
          collapseWhitespace: true
        }
      })
    );
  });
  
  return { entries, htmlPlugins };
}

const { entries, htmlPlugins } = getEntries();

module.exports = {
  entry: entries,
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js'
  },
  
  plugins: [
    ...htmlPlugins
  ],
  
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 提取所有页面共用的第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10
        },
        // 提取所有页面共用的业务代码
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    runtimeChunk: {
      name: 'runtime'
    }
  }
};

// 目录结构:
// src/
//   pages/
//     home/
//       index.js
//       index.html
//     about/
//       index.js
//       index.html
//     contact/
//       index.js
//       index.html

// 输出结果:
// dist/
//   js/
//     runtime.xxx.js
//     vendor.xxx.js
//     common.xxx.js
//     home.xxx.js
//     about.xxx.js
//     contact.xxx.js
//   home.html
//   about.html
//   contact.html
场景3:微前端架构配置
javascript 复制代码
/**
 * 问题:多个子应用需要独立开发、部署,但共享基础库
 * 
 * 解决方案:
 * 1. 使用Module Federation
 * 2. 配置共享依赖
 * 3. 动态加载子应用
 */

// 主应用配置(host)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      
      // 引用的远程模块
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        app2: 'app2@http://localhost:3002/remoteEntry.js'
      },
      
      // 共享的依赖
      shared: {
        vue: {
          singleton: true,  // 只加载一次
          requiredVersion: '^3.0.0'
        },
        'vue-router': {
          singleton: true,
          requiredVersion: '^4.0.0'
        },
        pinia: {
          singleton: true,
          requiredVersion: '^2.0.0'
        }
      }
    })
  ]
};

// 子应用配置(remote)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      
      // 暴露的模块
      exposes: {
        './App': './src/App.vue',
        './routes': './src/routes.js'
      },
      
      // 共享的依赖(与主应用保持一致)
      shared: {
        vue: {
          singleton: true,
          requiredVersion: '^3.0.0'
        },
        'vue-router': {
          singleton: true,
          requiredVersion: '^4.0.0'
        }
      }
    })
  ]
};

// 主应用中动态加载子应用
// src/router/index.js
const routes = [
  {
    path: '/app1',
    component: () => import('app1/App')  // 动态加载远程模块
  },
  {
    path: '/app2',
    component: () => import('app2/App')
  }
];
场景4:组件库开发配置
javascript 复制代码
/**
 * 问题:开发可复用的组件库,需要支持多种引入方式
 * 
 * 解决方案:
 * 1. 配置library输出
 * 2. 支持UMD、ESM、CommonJS
 * 3. 外部化依赖
 */

module.exports = {
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-component-library.js',
    
    // 库的配置
    library: {
      name: 'MyComponentLibrary',
      type: 'umd',  // 支持多种模块规范
      export: 'default'
    },
    
    // 确保在各种环境下都能正常工作
    globalObject: 'this'
  },
  
  // 外部化依赖(不打包到库中)
  externals: {
    vue: {
      commonjs: 'vue',
      commonjs2: 'vue',
      amd: 'vue',
      root: 'Vue'
    },
    'element-plus': {
      commonjs: 'element-plus',
      commonjs2: 'element-plus',
      amd: 'element-plus',
      root: 'ElementPlus'
    }
  },
  
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

// package.json配置
{
  "name": "my-component-library",
  "version": "1.0.0",
  "main": "dist/my-component-library.js",  // CommonJS入口
  "module": "dist/my-component-library.esm.js",  // ESM入口
  "unpkg": "dist/my-component-library.min.js",  // CDN入口
  "peerDependencies": {
    "vue": "^3.0.0"
  }
}

常见陷阱

陷阱1:循环依赖导致的问题
javascript 复制代码
/**
 * 错误示例:模块A和模块B相互引用
 */

// moduleA.js
import { funcB } from './moduleB.js';

export function funcA() {
  console.log('funcA');
  funcB();
}

// moduleB.js
import { funcA } from './moduleA.js';

export function funcB() {
  console.log('funcB');
  funcA();  // 可能导致undefined错误
}

/**
 * 正确做法:重构代码,消除循环依赖
 */

// utils.js(提取公共逻辑)
export function commonLogic() {
  console.log('common logic');
}

// moduleA.js
import { commonLogic } from './utils.js';

export function funcA() {
  console.log('funcA');
  commonLogic();
}

// moduleB.js
import { commonLogic } from './utils.js';

export function funcB() {
  console.log('funcB');
  commonLogic();
}

/**
 * 检测循环依赖的插件
 */

const CircularDependencyPlugin = require('circular-dependency-plugin');

module.exports = {
  plugins: [
    new CircularDependencyPlugin({
      exclude: /node_modules/,
      failOnError: true,  // 发现循环依赖时构建失败
      allowAsyncCycles: false,
      cwd: process.cwd()
    })
  ]
};
陷阱2:Source Map配置不当
javascript 复制代码
/**
 * 错误示例:生产环境使用eval类型的source map
 */

// ❌ 错误:eval类型的source map会暴露源代码
module.exports = {
  mode: 'production',
  devtool: 'eval-source-map'  // 不安全!
};

/**
 * 正确做法:根据环境选择合适的source map
 */

module.exports = {
  mode: 'production',
  
  // ✅ 正确:生产环境使用source-map或hidden-source-map
  devtool: 'source-map',  // 完整的source map,用于调试
  // devtool: 'hidden-source-map',  // 不在bundle中引用source map
  // devtool: false,  // 不生成source map
};

/**
 * Source Map类型对比
 */

// 开发环境推荐:
// eval-source-map:构建速度快,调试体验好
// eval-cheap-module-source-map:更快的构建速度,但调试体验稍差

// 生产环境推荐:
// source-map:完整的source map,用于调试
// hidden-source-map:不在bundle中引用,需要手动上传到错误监控平台
// nosources-source-map:只包含行列信息,不包含源代码
// false:不生成source map
陷阱3:忽略Tree Shaking的条件
javascript 复制代码
/**
 * 错误示例:使用CommonJS导出,无法Tree Shaking
 */

// utils.js(❌ 错误)
module.exports = {
  funcA: function() { /* ... */ },
  funcB: function() { /* ... */ },
  funcC: function() { /* ... */ }
};

// main.js
const { funcA } = require('./utils.js');
funcA();
// 结果:funcB和funcC也会被打包

/**
 * 正确做法:使用ES6模块导出
 */

// utils.js(✅ 正确)
export function funcA() { /* ... */ }
export function funcB() { /* ... */ }
export function funcC() { /* ... */ }

// main.js
import { funcA } from './utils.js';
funcA();
// 结果:只有funcA会被打包,funcB和funcC会被Tree Shaking移除

/**
 * Tree Shaking的条件:
 * 1. 使用ES6模块语法(import/export)
 * 2. 确保package.json中没有"sideEffects": false
 * 3. 使用production模式
 * 4. 不要使用Babel转译ES6模块为CommonJS
 */

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      modules: false  // 不转译ES6模块
    }]
  ]
};

// package.json
{
  "sideEffects": [
    "*.css",  // CSS文件有副作用
    "*.scss",
    "./src/polyfills.js"  // polyfill文件有副作用
  ]
}
陷阱4:缓存配置不当导致更新失败
javascript 复制代码
/**
 * 错误示例:使用hash而不是contenthash
 */

// ❌ 错误:任何文件改变都会导致所有文件的hash改变
module.exports = {
  output: {
    filename: '[name].[hash:8].js'
  }
};

/**
 * 正确做法:使用contenthash
 */

// ✅ 正确:只有文件内容改变时hash才会改变
module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js'
  },
  
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    })
  ],
  
  optimization: {
    // 提取runtime代码,避免vendor hash改变
    runtimeChunk: {
      name: 'runtime'
    },
    
    // 使用确定性的模块ID
    moduleIds: 'deterministic'
  }
};

/**
 * 缓存策略最佳实践
 */

// 1. 文件分类
// - runtime.js:Webpack运行时代码,每次构建都可能改变
// - vendor.js:第三方库,很少改变
// - common.js:公共业务代码,偶尔改变
// - main.js:入口代码,经常改变

// 2. 缓存配置
// nginx配置
location ~* \.(?:css|js)$ {
  expires 1y;  # 缓存1年
  add_header Cache-Control "public, immutable";
}

// HTML文件不缓存
location ~* \.html$ {
  expires -1;
  add_header Cache-Control "no-cache, no-store, must-revalidate";
}
陷阱5:忽略构建性能优化
javascript 复制代码
/**
 * 错误示例:没有使用缓存和并行处理
 */

// ❌ 错误:每次构建都重新处理所有文件
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};

/**
 * 正确做法:启用缓存和并行处理
 */

// ✅ 正确:使用缓存和多进程
module.exports = {
  // Webpack 5的持久化缓存
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.webpack_cache')
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          // 使用thread-loader进行多进程处理
          {
            loader: 'thread-loader',
            options: {
              workers: 4  // 工作进程数
            }
          },
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true  // 启用Babel缓存
            }
          }
        ]
      }
    ]
  },
  
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true  // 多进程压缩
      })
    ]
  }
};

/**
 * 构建性能优化清单
 */

// 1. 缩小文件搜索范围
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    extensions: ['.js', '.vue', '.json'],  // 减少尝试的扩展名
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),  // 只处理src目录
        exclude: /node_modules/  // 排除node_modules
      }
    ]
  }
};

// 2. 使用DllPlugin预编译第三方库(Webpack 4)
// 注意:Webpack 5推荐使用持久化缓存代替DllPlugin

// 3. 使用HardSourceWebpackPlugin(Webpack 4)
// 注意:Webpack 5内置了持久化缓存,不需要此插件

// 4. 分析构建性能
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // webpack配置
});

性能优化建议

javascript 复制代码
/**
 * 1. 减小bundle体积
 */

// 使用webpack-bundle-analyzer分析bundle
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

// 优化策略:
// - 移除未使用的代码(Tree Shaking)
// - 按需引入第三方库
// - 使用更小的替代库(如day.js代替moment.js)
// - 压缩代码和资源

/**
 * 2. 优化加载性能
 */

// 代码分割
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        }
      }
    }
  }
};

// 预加载和预获取
import(/* webpackPrefetch: true */ './future-module.js');
import(/* webpackPreload: true */ './critical-module.js');

// 资源压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html)$/,
      threshold: 10240
    })
  ]
};

/**
 * 3. 优化构建性能
 */

// 使用持久化缓存(Webpack 5)
module.exports = {
  cache: {
    type: 'filesystem'
  }
};

// 使用多进程
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['thread-loader', 'babel-loader']
      }
    ]
  }
};

// 减小搜索范围
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    extensions: ['.js', '.vue']
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/
      }
    ]
  }
};

关键点解析

  • 企业级应用:需要考虑代码分割、缓存策略、构建性能
  • 常见陷阱:循环依赖、Source Map配置、Tree Shaking条件、缓存策略
  • 性能优化:减小bundle体积、优化加载性能、优化构建性能
  • 最佳实践:根据项目类型选择合适的配置策略

Webpack与Vite的对比

理解Webpack和Vite的区别,有助于在实际项目中选择合适的构建工具。

核心差异
javascript 复制代码
/**
 * 1. 开发服务器启动方式
 */

// Webpack的方式:
// 1. 打包所有模块
// 2. 启动开发服务器
// 3. 提供打包后的bundle

// Vite的方式:
// 1. 直接启动开发服务器
// 2. 按需编译模块
// 3. 利用浏览器原生ESM

/**
 * 2. 热更新(HMR)机制
 */

// Webpack HMR:
// 1. 文件改变
// 2. 重新打包相关模块
// 3. 通过WebSocket推送更新
// 4. 浏览器应用更新

// Vite HMR:
// 1. 文件改变
// 2. 只编译改变的模块
// 3. 通过WebSocket推送更新
// 4. 浏览器重新请求该模块

/**
 * 3. 生产构建
 */

// Webpack:
// 使用自己的打包逻辑

// Vite:
// 使用Rollup进行打包
详细对比表
复制代码
┌─────────────────┬──────────────────────────┬──────────────────────────┐
│     特性        │         Webpack          │          Vite            │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 启动速度        │ 慢(需要打包所有模块)   │ 快(按需编译)           │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 热更新速度      │ 较慢(重新打包相关模块) │ 快(只编译改变的模块)   │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 生产构建        │ 使用Webpack打包          │ 使用Rollup打包           │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 配置复杂度      │ 高(需要配置loader等)   │ 低(开箱即用)           │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 生态系统        │ 成熟(大量loader/plugin)│ 快速发展中               │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 浏览器兼容性    │ 好(可配置目标浏览器)   │ 开发环境需要现代浏览器   │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 学习曲线        │ 陡峭                     │ 平缓                     │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 适用场景        │ 大型复杂项目、需要高度   │ 中小型项目、快速开发     │
│                 │ 定制化的项目             │                          │
└─────────────────┴──────────────────────────┴──────────────────────────┘
性能对比
javascript 复制代码
/**
 * 开发环境性能对比(以中型Vue项目为例)
 */

// Webpack:
// - 首次启动:30-60秒
// - 热更新:1-3秒
// - 内存占用:较高

// Vite:
// - 首次启动:1-3秒
// - 热更新:<100ms
// - 内存占用:较低

/**
 * 生产构建性能对比
 */

// Webpack:
// - 构建时间:取决于项目大小和配置
// - 输出体积:取决于优化配置
// - Tree Shaking:支持

// Vite:
// - 构建时间:通常比Webpack快
// - 输出体积:通常比Webpack小
// - Tree Shaking:默认开启
选择建议
javascript 复制代码
/**
 * 选择Webpack的场景:
 */

// 1. 大型企业级项目
// - 需要高度定制化的构建流程
// - 有复杂的构建需求
// - 需要支持旧版浏览器

// 2. 现有项目迁移成本高
// - 已经有成熟的Webpack配置
// - 使用了大量Webpack特定的loader和plugin
// - 团队对Webpack非常熟悉

// 3. 需要特定的构建功能
// - Module Federation(微前端)
// - 复杂的代码分割策略
// - 特定的loader处理

/**
 * 选择Vite的场景:
 */

// 1. 新项目
// - 快速开发原型
// - 中小型项目
// - 追求开发体验

// 2. 现代浏览器项目
// - 不需要支持IE等旧版浏览器
// - 可以使用ES6+特性
// - 追求快速的开发反馈

// 3. Vue 3项目
// - Vite是Vue团队推荐的构建工具
// - 与Vue 3生态集成更好
// - 开箱即用的Vue支持

/**
 * 混合使用的场景:
 */

// 开发环境使用Vite,生产环境使用Webpack
// - 享受Vite的快速开发体验
// - 利用Webpack成熟的生产构建能力
// - 需要额外的配置和维护成本
从Webpack迁移到Vite
javascript 复制代码
/**
 * 迁移步骤:
 */

// 1. 安装Vite和相关插件
// npm install vite @vitejs/plugin-vue --save-dev

// 2. 创建vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';

export default defineConfig({
  plugins: [vue()],
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: true
  }
});

// 3. 修改index.html
// Webpack:
// <script src="/dist/main.js"></script>

// Vite:
// <script type="module" src="/src/main.js"></script>

// 4. 修改环境变量
// Webpack:process.env.VUE_APP_API_URL
// Vite:import.meta.env.VITE_API_URL

// 5. 修改静态资源引用
// Webpack:require('@/assets/logo.png')
// Vite:new URL('@/assets/logo.png', import.meta.url).href

// 6. 修改package.json脚本
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

/**
 * 常见问题和解决方案:
 */

// 问题1:require不可用
// 解决:使用import代替require

// 问题2:全局变量不可用
// 解决:使用import.meta.env

// 问题3:动态require不可用
// 解决:使用import.meta.glob

// Webpack:
const modules = require.context('./modules', true, /\.js$/);

// Vite:
const modules = import.meta.glob('./modules/**/*.js');

// 问题4:某些Webpack特定的loader不可用
// 解决:寻找Vite插件替代,或使用vite-plugin-webpack

关键点解析

  • 核心差异:Webpack打包所有模块,Vite按需编译
  • 性能对比:Vite开发环境更快,生产构建各有优势
  • 选择建议:根据项目规模、团队经验、浏览器兼容性需求选择
  • 迁移成本:需要修改配置、环境变量、静态资源引用等

实践练习

练习1:配置Vue 3项目的Webpack构建(难度:中等)

需求描述

从零开始配置一个Vue 3项目的Webpack构建环境,实现以下功能:

  1. 支持Vue 3单文件组件(.vue文件)
  2. 支持TypeScript
  3. 支持Sass/SCSS样式
  4. 配置开发服务器,支持热更新
  5. 配置生产环境构建,包括代码压缩、CSS提取、资源优化
  6. 实现路由懒加载
  7. 配置环境变量管理

实现提示

  • 需要安装的依赖:webpack、webpack-cli、webpack-dev-server、vue-loader、ts-loader、sass-loader等
  • 创建webpack.common.js、webpack.dev.js、webpack.prod.js三个配置文件
  • 使用webpack-merge合并配置
  • 配置HtmlWebpackPlugin生成HTML文件
  • 配置MiniCssExtractPlugin提取CSS
  • 使用DefinePlugin管理环境变量

参考答案

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

module.exports = {
  entry: './src/main.ts',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  
  resolve: {
    extensions: ['.ts', '.js', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/]  // 支持.vue文件中的TypeScript
          }
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        },
        generator: {
          filename: 'images/[name].[hash:8][ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash:8][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.ico'
    })
  ]
};

// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');

module.exports = merge(common, {
  mode: 'development',
  
  devtool: 'eval-source-map',
  
  output: {
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js'
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
    ]
  },
  
  plugins: [
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: JSON.stringify(true),
      __VUE_PROD_DEVTOOLS__: JSON.stringify(false),
      'process.env.NODE_ENV': JSON.stringify('development'),
      'process.env.VUE_APP_API_URL': JSON.stringify('http://localhost:8080/api')
    })
  ],
  
  devServer: {
    port: 3000,
    hot: true,
    open: true,
    compress: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
});

// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  
  devtool: 'source-map',
  
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:8][ext]'
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: JSON.stringify(true),
      __VUE_PROD_DEVTOOLS__: JSON.stringify(false),
      'process.env.NODE_ENV': JSON.stringify('production'),
      'process.env.VUE_APP_API_URL': JSON.stringify('https://api.example.com')
    }),
    
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ],
  
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
          name: 'vue-vendor',
          priority: 20
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        },
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    runtimeChunk: {
      name: 'runtime'
    },
    
    moduleIds: 'deterministic'
  },
  
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
});

// package.json
{
  "scripts": {
    "dev": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "babel-loader": "^9.1.0",
    "css-loader": "^6.7.0",
    "css-minimizer-webpack-plugin": "^4.2.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.7.0",
    "postcss": "^8.4.0",
    "postcss-loader": "^7.0.0",
    "autoprefixer": "^10.4.0",
    "sass": "^1.57.0",
    "sass-loader": "^13.2.0",
    "style-loader": "^3.3.0",
    "terser-webpack-plugin": "^5.3.0",
    "ts-loader": "^9.4.0",
    "typescript": "^4.9.0",
    "vue-loader": "^17.0.0",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.0",
    "webpack-dev-server": "^4.11.0",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "vue": "^3.2.0",
    "vue-router": "^4.1.0",
    "pinia": "^2.0.0"
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};

// src/router/index.ts(路由懒加载示例)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import(
      /* webpackChunkName: "home" */
      '@/views/Home.vue'
    )
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(
      /* webpackChunkName: "about" */
      '@/views/About.vue'
    )
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

答案解析

  1. 配置文件组织

    • webpack.common.js:公共配置,包含entry、output、resolve、基础loader和plugin
    • webpack.dev.js:开发环境配置,使用style-loader、配置devServer
    • webpack.prod.js:生产环境配置,使用MiniCssExtractPlugin、配置代码压缩和分割
  2. Vue 3支持

    • 使用vue-loader处理.vue文件
    • 使用VueLoaderPlugin
    • 使用DefinePlugin定义Vue特性标志
  3. TypeScript支持

    • 使用ts-loader处理.ts文件
    • 配置appendTsSuffixTo支持.vue文件中的TypeScript
    • 创建tsconfig.json配置TypeScript编译选项
  4. 样式处理

    • 开发环境:style-loader将CSS注入到DOM
    • 生产环境:MiniCssExtractPlugin提取CSS到单独文件
    • 使用sass-loader处理Sass/SCSS
    • 使用postcss-loader添加浏览器前缀
  5. 代码分割

    • 将Vue全家桶提取到vue-vendor chunk
    • 将其他第三方库提取到vendor chunk
    • 将公共业务代码提取到common chunk
    • 提取runtime代码到独立chunk
  6. 路由懒加载

    • 使用动态import()语法
    • 使用webpackChunkName魔法注释指定chunk名称
  7. 环境变量

    • 使用DefinePlugin定义全局常量
    • 在代码中通过process.env访问

练习2:实现自定义Webpack Plugin(难度:困难)

需求描述

开发一个Webpack插件,实现以下功能:

  1. 在构建完成后,生成一个build-info.json文件
  2. 文件内容包含:构建时间、构建耗时、输出文件列表、文件大小统计
  3. 在控制台输出构建统计信息
  4. 支持配置选项(是否生成文件、输出路径等)

实现提示

  • Plugin需要实现apply方法
  • 使用compiler.hooks.done监听构建完成事件
  • 使用compilation.assets获取输出文件信息
  • 使用fs模块写入文件

参考答案

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

class BuildInfoPlugin {
  constructor(options = {}) {
    // 默认选项
    this.options = {
      filename: 'build-info.json',  // 输出文件名
      outputPath: '',  // 输出路径(相对于output.path)
      generateFile: true,  // 是否生成文件
      logToConsole: true,  // 是否输出到控制台
      ...options
    };
    
    this.startTime = null;
  }
  
  apply(compiler) {
    // 监听编译开始事件
    compiler.hooks.compile.tap('BuildInfoPlugin', () => {
      this.startTime = Date.now();
    });
    
    // 监听构建完成事件
    compiler.hooks.done.tapAsync('BuildInfoPlugin', (stats, callback) => {
      const endTime = Date.now();
      const buildTime = endTime - this.startTime;
      
      // 获取构建信息
      const buildInfo = this.getBuildInfo(stats, buildTime);
      
      // 输出到控制台
      if (this.options.logToConsole) {
        this.logToConsole(buildInfo);
      }
      
      // 生成文件
      if (this.options.generateFile) {
        this.generateFile(compiler, buildInfo);
      }
      
      callback();
    });
  }
  
  getBuildInfo(stats, buildTime) {
    const compilation = stats.compilation;
    const assets = compilation.assets;
    
    // 收集文件信息
    const files = [];
    let totalSize = 0;
    
    for (const filename in assets) {
      const asset = assets[filename];
      const size = asset.size();
      
      files.push({
        name: filename,
        size: size,
        sizeFormatted: this.formatSize(size)
      });
      
      totalSize += size;
    }
    
    // 按大小排序
    files.sort((a, b) => b.size - a.size);
    
    // 构建信息
    return {
      buildTime: new Date().toISOString(),
      buildDuration: buildTime,
      buildDurationFormatted: this.formatDuration(buildTime),
      totalFiles: files.length,
      totalSize: totalSize,
      totalSizeFormatted: this.formatSize(totalSize),
      files: files,
      errors: stats.compilation.errors.length,
      warnings: stats.compilation.warnings.length
    };
  }
  
  logToConsole(buildInfo) {
    console.log('\n========== 构建信息 ==========');
    console.log(`构建时间: ${buildInfo.buildTime}`);
    console.log(`构建耗时: ${buildInfo.buildDurationFormatted}`);
    console.log(`文件数量: ${buildInfo.totalFiles}`);
    console.log(`总大小: ${buildInfo.totalSizeFormatted}`);
    console.log(`错误: ${buildInfo.errors}`);
    console.log(`警告: ${buildInfo.warnings}`);
    
    console.log('\n文件列表(按大小排序):');
    buildInfo.files.slice(0, 10).forEach((file, index) => {
      console.log(`${index + 1}. ${file.name} (${file.sizeFormatted})`);
    });
    
    if (buildInfo.files.length > 10) {
      console.log(`... 还有 ${buildInfo.files.length - 10} 个文件`);
    }
    
    console.log('==============================\n');
  }
  
  generateFile(compiler, buildInfo) {
    const outputPath = compiler.options.output.path;
    const filePath = path.join(
      outputPath,
      this.options.outputPath,
      this.options.filename
    );
    
    // 确保目录存在
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    
    // 写入文件
    fs.writeFileSync(
      filePath,
      JSON.stringify(buildInfo, null, 2),
      'utf-8'
    );
    
    console.log(`构建信息已保存到: ${filePath}`);
  }
  
  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];
  }
  
  formatDuration(ms) {
    if (ms < 1000) return ms + 'ms';
    
    const seconds = Math.floor(ms / 1000);
    const minutes = Math.floor(seconds / 60);
    
    if (minutes > 0) {
      return `${minutes}m ${seconds % 60}s`;
    }
    
    return `${seconds}s`;
  }
}

module.exports = BuildInfoPlugin;

// 使用示例
// webpack.config.js
const BuildInfoPlugin = require('./build-info-plugin.js');

module.exports = {
  // ... 其他配置
  
  plugins: [
    new BuildInfoPlugin({
      filename: 'build-info.json',
      outputPath: 'meta',
      generateFile: true,
      logToConsole: true
    })
  ]
};

// 输出示例
// build-info.json
{
  "buildTime": "2024-01-15T10:30:45.123Z",
  "buildDuration": 12345,
  "buildDurationFormatted": "12s",
  "totalFiles": 15,
  "totalSize": 1234567,
  "totalSizeFormatted": "1.18 MB",
  "files": [
    {
      "name": "js/main.a1b2c3d4.js",
      "size": 456789,
      "sizeFormatted": "446.08 KB"
    },
    {
      "name": "js/vendor.e5f6g7h8.js",
      "size": 345678,
      "sizeFormatted": "337.58 KB"
    }
  ],
  "errors": 0,
  "warnings": 2
}

答案解析

  1. Plugin结构

    • 构造函数:接收配置选项
    • apply方法:接收compiler对象,注册钩子
  2. 生命周期钩子

    • compile钩子:记录构建开始时间
    • done钩子:构建完成后收集信息
  3. 信息收集

    • 从stats.compilation.assets获取输出文件
    • 计算文件大小和总大小
    • 统计错误和警告数量
  4. 格式化输出

    • formatSize:将字节转换为可读格式(B、KB、MB、GB)
    • formatDuration:将毫秒转换为可读格式(ms、s、m)
  5. 文件生成

    • 使用fs.writeFileSync写入JSON文件
    • 确保输出目录存在
  6. 可配置性

    • 支持自定义文件名和输出路径
    • 支持开关文件生成和控制台输出

扩展挑战

  1. 添加对比功能:与上次构建对比,显示文件大小变化
  2. 添加警告功能:当文件大小超过阈值时发出警告
  3. 添加图表生成:生成HTML格式的可视化报告
  4. 添加Git信息:包含当前分支、commit hash等信息

高级主题深入

Webpack模块联邦(Module Federation)

javascript 复制代码
/**
 * ========================================
 * 模块联邦:微前端架构的核心技术
 * ========================================
 * 
 * 模块联邦允许多个独立的Webpack构建共享代码,
 * 实现真正的运行时代码共享,而不是构建时的代码复制。
 */

/**
 * 场景:电商平台的微前端架构
 * 
 * 主应用(Host):商城首页
 * 远程应用1(Remote):商品模块
 * 远程应用2(Remote):购物车模块
 * 远程应用3(Remote):用户中心模块
 */

// 主应用配置(host-app)
// webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',  // 应用名称
      
      // 引用远程模块
      remotes: {
        productApp: 'productApp@http://localhost:3001/remoteEntry.js',
        cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
        userApp: 'userApp@http://localhost:3003/remoteEntry.js'
      },
      
      // 共享依赖
      shared: {
        vue: {
          singleton: true,  // 只加载一次
          requiredVersion: '^3.2.0'
        },
        'vue-router': {
          singleton: true,
          requiredVersion: '^4.0.0'
        },
        pinia: {
          singleton: true,
          requiredVersion: '^2.0.0'
        }
      }
    })
  ]
};

// 主应用使用远程模块
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/products',
    name: 'Products',
    // 动态加载远程模块
    component: () => import('productApp/ProductList')
  },
  {
    path: '/cart',
    name: 'Cart',
    component: () => import('cartApp/Cart')
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('userApp/Profile')
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

// 远程应用配置(product-app)
// webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',
      filename: 'remoteEntry.js',
      
      // 暴露模块
      exposes: {
        './ProductList': './src/components/ProductList.vue',
        './ProductDetail': './src/components/ProductDetail.vue',
        './ProductSearch': './src/components/ProductSearch.vue'
      },
      
      // 共享依赖
      shared: {
        vue: {
          singleton: true,
          requiredVersion: '^3.2.0'
        },
        'vue-router': {
          singleton: true,
          requiredVersion: '^4.0.0'
        }
      }
    })
  ]
};

/**
 * 动态远程模块加载
 */

// 动态加载远程应用
async function loadRemoteModule(remoteUrl, moduleName) {
  // 加载远程入口文件
  await loadScript(remoteUrl);
  
  // 获取远程容器
  const container = window[moduleName];
  
  // 初始化容器
  await container.init(__webpack_share_scopes__.default);
  
  // 获取模块工厂
  const factory = await container.get(moduleName);
  
  // 执行模块
  const module = factory();
  
  return module;
}

function loadScript(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// 使用
const ProductList = await loadRemoteModule(
  'http://localhost:3001/remoteEntry.js',
  'productApp/ProductList'
);

/**
 * 版本管理和降级策略
 */

// 带版本控制的模块联邦配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // 使用版本号
        productApp: 'productApp@http://localhost:3001/v1.2.3/remoteEntry.js'
      },
      shared: {
        vue: {
          singleton: true,
          requiredVersion: '^3.2.0',
          // 版本不匹配时的策略
          strictVersion: false,  // 允许版本不匹配
          shareScope: 'default'
        }
      }
    })
  ]
};

// 降级策略:远程模块加载失败时使用本地模块
async function loadModuleWithFallback(remotePath, localPath) {
  try {
    // 尝试加载远程模块
    return await import(remotePath);
  } catch (error) {
    console.warn(`远程模块加载失败,使用本地模块: ${error.message}`);
    // 降级到本地模块
    return await import(localPath);
  }
}

// 使用
const ProductList = await loadModuleWithFallback(
  'productApp/ProductList',
  './components/ProductListFallback.vue'
);

/**
 * 共享状态管理
 */

// 主应用:创建共享store
// src/store/index.js
import { createPinia } from 'pinia';

const pinia = createPinia();

// 暴露给远程应用
window.__SHARED_STORE__ = pinia;

export default pinia;

// 远程应用:使用共享store
// product-app/src/store/product.js
import { defineStore } from 'pinia';

// 使用主应用的pinia实例
const pinia = window.__SHARED_STORE__;

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    selectedProduct: null
  }),
  
  actions: {
    async fetchProducts() {
      const response = await fetch('/api/products');
      this.products = await response.json();
    }
  }
}, { pinia });

/**
 * 模块联邦最佳实践
 */

// 1. 使用TypeScript类型共享
// shared-types/index.d.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
}

export interface User {
  id: string;
  name: string;
  email: string;
}

// 2. 统一的错误处理
class RemoteModuleError extends Error {
  constructor(moduleName, originalError) {
    super(`Failed to load remote module: ${moduleName}`);
    this.moduleName = moduleName;
    this.originalError = originalError;
  }
}

async function safeLoadRemoteModule(moduleName) {
  try {
    return await import(moduleName);
  } catch (error) {
    throw new RemoteModuleError(moduleName, error);
  }
}

// 3. 性能监控
function monitorRemoteModuleLoad(moduleName) {
  const startTime = performance.now();
  
  return import(moduleName).then(module => {
    const loadTime = performance.now() - startTime;
    console.log(`Remote module ${moduleName} loaded in ${loadTime}ms`);
    
    // 发送到监控服务
    sendMetric('remote_module_load', {
      module: moduleName,
      loadTime
    });
    
    return module;
  });
}

// 4. 预加载远程模块
function preloadRemoteModules(moduleNames) {
  moduleNames.forEach(moduleName => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = getRemoteModuleUrl(moduleName);
    document.head.appendChild(link);
  });
}

// 在应用启动时预加载
preloadRemoteModules([
  'productApp/ProductList',
  'cartApp/Cart'
]);

Webpack构建性能优化深度实践

javascript 复制代码
/**
 * ========================================
 * 构建性能优化:从10分钟到1分钟
 * ========================================
 */

/**
 * 1. 持久化缓存(Webpack 5)
 */

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',  // 使用文件系统缓存
    
    // 缓存目录
    cacheDirectory: path.resolve(__dirname, '.webpack_cache'),
    
    // 缓存名称(用于区分不同配置)
    name: 'production-cache',
    
    // 缓存版本(修改后会使缓存失效)
    version: '1.0.0',
    
    // 构建依赖
    buildDependencies: {
      config: [__filename],  // 配置文件变化时使缓存失效
      tsconfig: [path.resolve(__dirname, 'tsconfig.json')]
    },
    
    // 缓存策略
    store: 'pack',  // 打包存储,减少文件数量
    
    // 压缩缓存
    compression: 'gzip',
    
    // 缓存大小限制
    maxMemoryGenerations: 5,
    maxAge: 1000 * 60 * 60 * 24 * 7  // 7天
  }
};

/**
 * 2. 多进程构建
 */

// 使用thread-loader
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1,  // 使用CPU核心数-1
              workerParallelJobs: 50,
              poolTimeout: 2000
            }
          },
          'babel-loader'
        ]
      },
      {
        test: /\.ts$/,
        use: [
          'thread-loader',
          {
            loader: 'ts-loader',
            options: {
              happyPackMode: true,  // 配合thread-loader使用
              transpileOnly: true  // 只转译,不类型检查(类型检查用fork-ts-checker-webpack-plugin)
            }
          }
        ]
      }
    ]
  },
  
  plugins: [
    // 在单独进程中进行TypeScript类型检查
    new ForkTsCheckerWebpackPlugin({
      async: true,  // 异步检查
      typescript: {
        configFile: path.resolve(__dirname, 'tsconfig.json'),
        memoryLimit: 4096
      }
    })
  ]
};

/**
 * 3. 优化resolve配置
 */

module.exports = {
  resolve: {
    // 减少解析的文件扩展名
    extensions: ['.js', '.ts', '.vue', '.json'],  // 只保留必要的
    
    // 使用别名减少查找
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'components': path.resolve(__dirname, 'src/components'),
      'utils': path.resolve(__dirname, 'src/utils')
    },
    
    // 限制模块查找范围
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules'
    ],
    
    // 优化symlinks处理
    symlinks: false,
    
    // 缓存模块解析结果
    cache: true,
    
    // 优化package.json的main字段查找
    mainFields: ['browser', 'module', 'main'],
    
    // 优化目录索引文件查找
    mainFiles: ['index']
  },
  
  resolveLoader: {
    // 限制loader查找范围
    modules: ['node_modules']
  }
};

/**
 * 4. 优化模块解析
 */

module.exports = {
  module: {
    // 不解析的模块(已经打包好的库)
    noParse: /jquery|lodash/,
    
    rules: [
      {
        test: /\.js$/,
        // 排除不需要处理的目录
        exclude: /node_modules/,
        // 或者只包含需要处理的目录
        include: path.resolve(__dirname, 'src'),
        use: 'babel-loader'
      }
    ]
  }
};

/**
 * 5. DLL动态链接库
 */

// webpack.dll.config.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
  mode: 'production',
  
  entry: {
    vendor: ['vue', 'vue-router', 'pinia', 'axios', 'lodash-es']
  },
  
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.resolve(__dirname, 'dll/[name]-manifest.json')
    })
  ]
};

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor-manifest.json')
    })
  ]
};

// package.json
{
  "scripts": {
    "dll": "webpack --config webpack.dll.config.js",
    "build": "npm run dll && webpack --config webpack.config.js"
  }
}

/**
 * 6. 构建分析和监控
 */

// 使用speed-measure-webpack-plugin分析构建速度
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // webpack配置
});

// 输出示例:
/*
SMP  ⏱  
General output time took 45.23 secs

SMP  ⏱  Plugins
TerserPlugin took 12.34 secs
MiniCssExtractPlugin took 5.67 secs
HtmlWebpackPlugin took 2.34 secs

SMP  ⏱  Loaders
babel-loader took 15.67 secs
  module count = 234
ts-loader took 8.90 secs
  module count = 156
*/

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

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json'
    })
  ]
};

/**
 * 7. 增量构建优化
 */

module.exports = {
  // 开发环境使用eval-source-map(最快)
  devtool: process.env.NODE_ENV === 'development' 
    ? 'eval-source-map' 
    : 'source-map',
  
  // 监听模式优化
  watchOptions: {
    // 忽略node_modules
    ignored: /node_modules/,
    
    // 聚合多个更改到一次重新构建
    aggregateTimeout: 300,
    
    // 轮询间隔
    poll: 1000
  },
  
  // 开发服务器优化
  devServer: {
    // 启用热更新
    hot: true,
    
    // 只编译修改的模块
    liveReload: false,
    
    // 减少日志输出
    client: {
      logging: 'warn'
    }
  }
};

/**
 * 8. 生产构建优化
 */

module.exports = {
  optimization: {
    // 使用确定性的模块ID
    moduleIds: 'deterministic',
    
    // 使用确定性的chunk ID
    chunkIds: 'deterministic',
    
    // 提取runtime代码
    runtimeChunk: {
      name: 'runtime'
    },
    
    // 代码分割
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      
      cacheGroups: {
        // 框架代码
        framework: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
          name: 'framework',
          priority: 40,
          reuseExistingChunk: true
        },
        
        // UI库
        ui: {
          test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
          name: 'ui',
          priority: 30,
          reuseExistingChunk: true
        },
        
        // 工具库
        utils: {
          test: /[\\/]node_modules[\\/](axios|dayjs|lodash-es)[\\/]/,
          name: 'utils',
          priority: 20,
          reuseExistingChunk: true
        },
        
        // 其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10,
          reuseExistingChunk: true
        },
        
        // 公共业务代码
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    // 压缩优化
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,  // 多进程压缩
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log']
          },
          format: {
            comments: false
          }
        },
        extractComments: false
      }),
      
      new CssMinimizerPlugin({
        parallel: true,
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true }
            }
          ]
        }
      })
    ]
  }
};

/**
 * 构建性能优化效果对比
 */

/*
优化前:
- 首次构建:10分30秒
- 增量构建:45秒
- 生产构建:8分钟
- Bundle大小:5.2MB

优化后:
- 首次构建:2分15秒(提升78%)
- 增量构建:8秒(提升82%)
- 生产构建:1分30秒(提升81%)
- Bundle大小:1.8MB(减少65%)

优化措施:
1. 启用持久化缓存:减少50%构建时间
2. 使用thread-loader:减少30%构建时间
3. 优化resolve配置:减少10%构建时间
4. 代码分割优化:减少65% bundle大小
5. 压缩优化:减少20% bundle大小
*/

进阶阅读

下一步

恭喜你完成了Webpack基础的学习!

通过本章的学习,你已经掌握了:

  • Webpack的四大核心概念(Entry、Output、Loader、Plugin)
  • Webpack的构建流程和工作原理
  • 代码分割和懒加载的实现方法
  • 开发环境和生产环境的配置策略
  • 模块联邦和微前端架构
  • 构建性能优化的完整方案
  • 企业级应用的最佳实践
  • Webpack与Vite的区别和选择建议

下一章我们将学习前端性能优化,它将帮助你:

  • 深入理解前端性能优化的核心指标(Core Web Vitals)
  • 掌握加载性能、运行时性能、渲染性能的全面优化策略
  • 学习使用Chrome DevTools、Lighthouse等工具进行性能分析
  • 了解真实项目的性能优化案例和数据分析方法

性能优化是前端工程师的核心竞争力之一,也是Webpack配置的最终目标。让我们继续深入学习!


返回目录

相关推荐
yuki_uix1 小时前
WebSocket 连上了,然后呢?聊聊实时数据的"后半场"
前端·websocket
清粥油条可乐炸鸡1 小时前
tailwind-merge的基本使用
前端
wuhen_n1 小时前
reactive 工具函数集
前端·javascript·vue.js
wuhen_n1 小时前
effect的调度与清理:深入Vue3响应式系统的进阶特性
前端·javascript·vue.js
yinmaisoft1 小时前
开箱即用!国产化全兼容,信创生态适配 + 高效开发
前端·低代码·开发工具
wuhen_n1 小时前
响应式系统核心难题:数组与集合
前端·javascript·vue.js
We་ct2 小时前
LeetCode 199. 二叉树的右视图:层序遍历解题详解
前端·算法·leetcode·typescript·广度优先
晴殇i2 小时前
深入浅出 XSS:原理、危害与全方位防御指南
前端·面试