Webpack[TBC]

77 阅读4分钟

核心概念

  1. 本质:模块打包器

    • 将各种资源(JS/CSS/图片等)视为模块
    • 通过依赖关系构建依赖图
    • 输出静态资源bundle
  2. 五大核心概念

    • Entry:打包入口起点,entry: './src/index.js'
    • Output:输出位置配置,path: path.resolve(__dirname, 'dist')
    • Loaders:处理非JS文件,{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
    • Plugins:扩展功能,new HtmlWebpackPlugin()
    • Mode:环境模式,mode:'development'

核心工作原理

打包流程

graph LR
    A[入口文件 Entry] --> B[解析模块依赖]
    B --> C[构建依赖图]
    C --> D[应用 Loaders 转换]
    D --> E[打包模块资源]
    E --> F[执行插件优化]
    F --> G[输出 Bundles]
    G --> H[完成打包]

    classDef default fill:#f9f,stroke:#333,stroke-width:2px;
    classDef process fill:#6cf,stroke:#333,stroke-width:2px;
    classDef output fill:#2ecc71,stroke:#333,stroke-width:2px;
    
    class A,H process;
    class B,C,D,E,F process;
    class G output;

关键过程

  1. 入口文件
    • 从配置的入口文件开始打包过程
    • 通常是index.js/main.js
  2. 解析模块依赖
    • 分析文件中的import/require语句
    • 递归查找所有依赖模块
  3. 构建依赖图
    • 创建模块依赖关系图谱
    • 确定模块加载顺序
  4. 应用Loaders转换
    • 使用配置的Loaders处理各类资源
    • 如:JS转译、CSS预处理、图片优化
  5. 打包模块资源
    • 将所有模块合并为代码块chunk
    • 应用代码分割规则
  6. 执行插件优化
    • 运行插件进行额外处理
    • 如:代码压缩、资源注入、生成html
  7. 输出bundles
    • 将最终结果写入文件系统
    • 生成JS/CSS/资源文件
  8. 完成打包
    • 输出构建统计信息
    • 触发回调通知

配置webpack.config.js

基础结构

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

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: 'babel-loader',
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({ template: './src/index.html' }),
    ],
    mode:'production',
};

Loaders

文件类型Loader配置
Javascriptbabel-loader, @babel/preset-env
CSS['style-loader', 'css-loader', 'postcss-loader']
SASS['sass-loader'] [需配合css-loader]
图片/字体type: 'asset/resource' [webpack5内置]
SVG@svg/webpack

Plugins

插件用途
HtmlWebpackPlugin生成html文件
MiniCssExtractPlugin提取CSS到单独文件
DefinePlugin定义环境变量
CleanWebpackPlugin清理构建目录
BundleAnalyzerPlugin包体积分析

高级优化策略

代码分割Code Splitting

将应用代码拆分为多个独立报chunk,按需加载或并行加载,减少初始加载体积。

目的

  1. 减少初始加载体积:避免用户下载整个应用代码
  2. 提高加载速度:并行加载多个小文件比单个大文件更快
  3. 按需加载:只加载当前视图需要的代码
  4. 缓存优化:第三方库单独打包,充分利用浏览器缓存

实现方式

  1. 入口点分割 \手动
// webpack.config.js
module.exports = {
    entry: {
        app: './src/app.js',
        vendor: ['react', 'react-dom']
    },
    output: {
        filename: '[name].bundles.js'
    }
};
  1. 动态导入
// 使用import语法
const loadComponent = () => import('./HeavyComponent');

button.addEventListener('click', () => {
    loadComponent().then(module => {
        module.default.render();
    });
});
  1. 智能分割SplitChunksPlugin
optimization: {
    splitChunks: {
        chunks: 'all',
        minSize: 20000,  // 将包拆分成不小于minSize的包
        maxSize: 0,  // 当一个包大于指定的大小时,才进行拆包
        minChunks: 1,
        
        // 将需要进行拆包的内容进行分包
        cachedGroups: {
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                priority: -10
            },
            commons:{
                name: 'commons',
                minChunks: 2,  // 被2个以上chunk引用
                priority: -20,
                reuseExistingChunk: true
            }
        }
    }
}

代码分割策略

策略类型适用场景示例
入口分割多页面应用entry: { home: './home.js', about: './about.js' }
动态分割路由组件/弹窗() => import('./UserProfile')
第三方库稳定不常更新的库cacheGroups.vendors
运行时代码Webpack运行时runtimeChunk: 'single'

Tree Shaking

移除JavaScript上下文中未使用的代码dead code

工作原理

  • ES6模块静态分析:
    • import/export是静态声明(编译时确定)
    • CommonJS是动态的(运行时确定)
  • 标记未使用代码
// math.js
export function square(x) { return x * x; } // 被使用 ✅
export function cube(x) { return x * x * x; } // 未被使用❌

// app.js
import { square } from './math.js';
  • 压缩阶段移除
    • 通过TerserPlugin等工具删除标记代码

启动Tree Shaking

  1. 基本配置
// webpack.config.js
module.exports = {
    mode: 'production',
    optimization: {
        usedExports: true,  // 标记未使用
        minimize: true  // 移除未使用
    }
}
  1. package.json配置
{
    "name": "my-demo",
    "sideEffects": [
        "*.css",
        "*.scss",
        "@babel/polyfill"
    ]
}
  • sideEffects: false:所有文件无副作用
  • sideEffects: [...]:列出有副作用的文件

常见问题及解决方案

  1. Babel转换导致失效
// .babelrc
{
    "presets": [
        ["@babel/preset-env", { "modules": false }] // 关键:不转换ES6模块
    ]
}
  1. 第三方库不支持
// 使用es模块版本
import { debounce } from 'lodash-es';

// 或使用插件
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
  1. CSS Tree Shaking[待深入]
// webpack.config.js ?
// 使用PurgeCSS
const PurgeCSSPlugin = require('purgecss-webpack-plugin');

{
    ...,
    plugins: [
        new PurgeCSSPlugin({
            paths: glob.sync(`${PATH.src}/**/*`, { nodir: true }),
        })
    ]
}

Code Splitting VS Tree Shaking

特性Code SplittingTree Shaking
目的拆分代码为多个包移除未使用代码
优化方向加载性能包体积优化
触发时机构建时+运行时仅构建时
主要技术import() + SplitChunksES6模块 + Terser
影响范围模块/文件级别函数/变量级别
依赖语法动态import()ES6 import/export

最佳实践组合

  1. 优化流程
graph LR
A[原始代码] --> B[Tree Shaking]
B --> C[移除未使用代码]
C --> D[代码分割]
D --> E[按需加载]
  1. 配置示例
// webpack.config.js
module.exports = {
    mode:'production',
    optimization: {
        usedExports: true,
        splitChunks: {
            chunks: 'all',
            cachedGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors'
                }
            }
        },
        runtimeChunk: 'single'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [['@babel/preset-env', { modules:false }]]
                    }
                }
            }
        ]
    }
}
  • 代码分割横向切割(将代码拆分成多个文件)

    • 适用:路由分割、第三方库分离、动态加载
    • 核心:import() + splitChunks
  • Tree Shaking纵向修剪(删除文件内未使用的代码)

    • 适用:工具函数库、组件类、工具类
    • 核心:ES6模块 + sideEffects标记
  • 最佳实践:两者结合使用,先通过Tree Shaking删除无用代码,再通过代码分割拆分剩余代码,可达到最佳优化效果。

缓存优化

output: {
    filename: '[name].[contenthash].js'
}

懒加载

// dynamic import
const loadModule  = () => import('./heavyModule.js');

自定义扩展

自定义loader

module.exports = function(source) {
    return source.replace(/console\.log\(.*\);?/g, '');
};

自定义plugin

class myPlugin {
    apply(complier) {
        commplier.hooks.done.tap('MyPlugin', stats => {
            console.log('编译完成!');
        });
    }
}

性能优化实践

构建速度优化

方法实现
缓存loadersuse: ['cache-loader', 'babel-loader']
多进程处理thread-loader
DLL预构建DllPlugin + DllReferencePlugin
缩小搜索范围resolve: { modules: [path.resolve('node_modules')] }

输出优化

方法实现
CDN加速output.publicPath: 'https://cdn.example.com/'
Gzip压缩compression-webpack-plugin
图片优化image-webpack-loader

原理深入

Tapable事件流机制

compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
    // 处理逻辑
    callback();
});

核心对象关系

classDiagram
  Compiler *-- Compilation
  Compilation *-- Module
  Module *-- Dependency
  Compiler : hooks
  Compilation : modules
  Compilation : chunks

HMR热更新原理

  1. 建立WebSocket连接
  2. 文件修改触发重新编译
  3. 服务端发生更新消息
  4. 客户端拉取更新模块
  5. 执行模块替换逻辑