likes
comments
collection
share

Webpack 2024 前前端架构老鸟的分享(总篇)

作者站长头像
站长
· 阅读数 1

Webpack 2024 前前端架构老鸟的分享(总篇)

简述

嘿,各位!作为一个Webpack的老用户,我得吐槽一句,这玩意儿一开始真是够折腾人的!别说配置文件了,就像是在写某种古老的神秘仪式的咒语。刚开始的时候,我简直是摸不着头脑啊!但是别慌,慢慢来,慢慢琢磨,你会发现Webpack这个家伙,虽然有点拗口,但一旦掌握了它的精髓,就像是驾驭了一匹烈马一样,带你疾驰在前端的征途上!

你可能收货

关于webpack暂时计划是出三期,你将会学到基本的配置,和常用的插件的使用,以及一些基础原理和生命周期的流转,第三篇会教大家如何编写一个自己的loader,以及深入理解loader的原理。

读白

当然说到这里,很多人会说:“现在vite这么火为什么我不直接去用Vite呢?🙄”

统一回复:“新鲜事物赞成积极尝试,且Vite后面我们也会单独说,但是旧的事物必然有存在的意义,😄且企业并不希望你用什么新技术炫技或者变动太大,所有我们温故,遂而知新,认同这种观点你可以继续看下去 第一章也比较简单,但是如果你的动手能力强直接去官网研究是最好的,好了废话有点多,不影响大家继续学习🌹。”

一、Webpack 概念和基础知识

1.1 Webpack 简介

Webpack 不仅是一个模块打包工具,更是一个强大的构建工具。它可以充分利用现代 JavaScript 的优势,并将其内置的丰富功能发挥到极致。Webpack 的主要应用场景有:

  • 单页面应用程序(SPA)
  • 与框架如 React、Vue 等配合使用
  • 适用于需要对代码、资源进行复杂处理的大型项目

Webpack 优势:

  • 代码拆分 自动化资源文件的拆分,有利于减少首次加载时间和提高文件缓存利用率。
  • Loader Webpack 本身只能处理 JavaScript 模块,通过 Loader 可以使它支持其他格式的文件。
  • 智能解析 能够解析常用的模块化方案,例如 CommonJS、AMD、ES Module。
  • 插件系统 通过插件系统可以实现更加复杂、自动化的功能。
  • 环境支持 既可以运行在开发环境,又可以供生产环境使用。

1.2 Webpack 核心概念

  • 入口(Entry) 指定 Webpack 构建的入口模块文件,可以是单个文件,也可以是多个文件组成的数组或对象。

示例:

module.exports = {
  entry: './src/index.js' // 单入口
  entry: ['./src/index.js', './src/app.js'] // 多入口数组
  entry: {
    main: './src/index.js', 
    app: './src/app.js'
  } // 多入口对象
}
  • 输出(Output) 指定 Webpack 构建后的资源输出位置和输出文件名称。

示例:

const path = require('path');
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出目录
    filename: 'bundle.js' // 输出文件名
  }
}
  • Loader Loader 用于对模块的源代码进行转换,扩展 Webpack 的能力。

示例:

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'] 
      }
    ]
  }
}
  • 插件(Plugins) 插件用于执行更广泛的任务,例如打包优化、资源管理等。

示例:

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

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

二、Webpack 配置文件详解

2.1 入口(Entry)

入口是 Webpack 构建的起点,可以按需配置单个或多个入口。

单入口语法

module.exports = {
  entry: './src/index.js'  
}

多入口语法

module.exports = {  
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'
  }
}

以上示例中,Webpack 会为每个入口构建一个 Chunk(输出文件)。

动态入口语法

什么是动态入口?

动态入口是指在 Webpack 构建过程中动态生成入口模块路径的一种机制。与传统的静态入口不同,动态入口可以根据不同的条件或环境生成不同的入口模块路径,从而实现更灵活的构建配置。

基础配置:

entry: () => './src/index.js'

动态入口的优势

动态入口具有以下优势:

  • 提高可扩展性:  可以根据不同的需求或环境动态生成入口模块路径,从而提高 Webpack 配置的可扩展性。
  • 简化配置:  可以避免重复的配置,使 Webpack 配置更加简洁易懂。
  • 提高代码复用:  可以将通用的代码提取到单独的模块中,并在不同的入口模块中进行复用。

动态入口的实现

动态入口可以通过以下两种方式实现:

  • 使用函数:  可以使用函数返回一个入口模块路径字符串或数组。
  • 使用 webpack.DefinePlugin 插件:  可以使用 webpack.DefinePlugin 插件定义一个全局变量,并使用该变量来动态生成入口模块路径。

使用函数实现动态入口

entry: () => {
  const env = process.env.NODE_ENV;
  if (env === 'development') {
    return './src/index.dev.js';
  } else {
    return './src/index.prod.js';
  }
}

上述代码根据 NODE_ENV 环境变量来动态生成入口模块路径。在开发环境下,入口模块路径为 ./src/index.dev.js;在生产环境下,入口模块路径为 ./src/index.prod.js

使用 webpack.DefinePlugin 插件实现动态入口

module.exports = {
  entry: './src/index.js',
  plugins: [
    new webpack.DefinePlugin({
      'process.env.ENTRY_MODULE': JSON.stringify('./src/index.dev.js'),
    }),
  ],
};

上述代码使用 webpack.DefinePlugin 插件定义了一个全局变量 process.env.ENTRY_MODULE,并将其值设置为 ./src/index.dev.js。然后,可以在入口模块中使用该变量来动态生成入口模块路径。

小结

动态入口是一种灵活的 Webpack 构建机制,可以根据不同的需求或环境生成不同的入口模块路径,从而提高 Webpack 配置的可扩展性、简化配置和提高代码复用。

2.2 输出(Output)

output 选项用于控制 Webpack 构建后的资源输出位置和文件名称。

const path = require('path');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出路径
    filename: 'bundle.js', // 输出文件名
    publicPath: '/', // 公共路径 
    chunkFilename: '[name].bundle.js', // 代码分割后的命名
    libraryTarget: 'var', // 库导出方式
    library: 'MyLibrary' // 库名称
  }
}

2.3 模式(Mode)

mode 用于指定当前的构建环境,可选值有 production、development 或 none。

module.exports = {
  mode: 'production' // 生产环境
  mode: 'development' // 开发环境
}

2.4 模块(Module)

module 选项用于配置模块的解析规则,例如使用哪些 Loader。

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader' 
      }
    ]
  }
}

2.5 解析(Resolve)

resolve 选项用于设置模块解析规则,例如别名、扩展名等。

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    },
    extensions: ['.js', '.jsx', '.json'], 
    modules: ['node_modules']
  }
}

2.6 插件(Plugins)

plugins 选项用于应用各种 Webpack 插件。

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

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin(['dist'])
  ]
}

2.7 优化(Optimization)

optimization 选项用于优化构建结果。

module.exports = {
  optimization: {
    minimize: true, // 开启代码压缩
    splitChunks: {
      chunks: 'all' // 代码分割
    }
  }
}

2.8 devServer

devServer 选项用于配置 Webpack 开发服务器。

module.exports = {
  devServer: {
    contentBase: './dist', 
    hot: true, // 热更新
    open: true, // 自动打开浏览器
    port: 8080, // 端口号
    proxy: { // 代理设置
      '/api': 'http://localhost:3000'
    }
  }
}

基础小节

示例:

为了演示三级标题提到的Webpack配置,我们将创建一个简单的项目结构,并添加一个入口文件和一个HTML模板文件。然后,我们将使用Webpack进行打包,并通过Webpack DevServer在开发服务器上进行实时预览。

首先,创建一个名为webpack-demo的新目录,并在其中创建以下文件和目录结构:

webpack-demo/        // 项目根目录
  |- src/            // 存放项目的源代码
  |   |- index.js    // 项目的入口文件
  |- dist/           // Webpack 构建后的输出目录
  |- index.html      // 项目的 HTML 模板文件
  |- package.json    // npm 包的配置文件
  |- webpack.config.js   // Webpack 的配置文件

接下来,我们按照以下步骤编辑这些文件:

  1. src/index.js - 这是我们的入口文件,内容可以是简单的JavaScript代码,作为演示,我们将在这里输出一条简单的日志信息:
console.log("Hello from webpack!");
  1. index.html - 这是我们的HTML模板文件,Webpack会根据配置在dist目录中生成一个新的HTML文件,并注入打包后的JavaScript文件。这里我们简单写一个HTML框架:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Webpack Demo</title>
</head>
<body>
    <div id="app">Hello Webpack </div>
</body>
</html>
  1. webpack.config.js - 这是我们的Webpack配置文件,我们按照之前提到的配置内容来编写:
const path = require('path');  // 引入 Node.js 的 path 模块,用于处理文件路径
const HtmlWebpackPlugin = require('html-webpack-plugin');  // 引入 HtmlWebpackPlugin 插件,用于生成 HTML 文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');  // 引入 CleanWebpackPlugin 插件,用于清理构建目录
module.exports = {
  mode: 'development',  // 指定构建模式为开发模式
  entry: './src/index.js',  // 指定入口文件为 src 目录下的 index.js 文件
  output: {
    path: path.resolve(__dirname, 'dist'),  // 指定输出目录为当前目录下的 dist 文件夹
    filename: 'bundle.js',  // 指定输出文件名为 bundle.js
  },
  module: {
    rules: [  // 配置 Loader 规则
      {
        test: /.js$/,  // 匹配以 .js 结尾的文件
        exclude: /node_modules/,  // 排除 node_modules 目录下的文件
        use: {  // 使用 babel-loader 进行转译
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']  // 使用 @babel/preset-env 进行转译
          }
        }
      }
    ]
  },
  plugins: [  // 配置插件
    new CleanWebpackPlugin(),  // 在每次构建前清理 dist 目录
    new HtmlWebpackPlugin({  // 生成 HTML 文件
      template: './index.html'  // 指定 HTML 模板文件
    })
  ],
  devServer: {  // 配置开发服务器
    static: {
      directory: path.resolve(__dirname, 'dist'), // 指定服务器内容的基础目录
    },
      port: 8080,  // 指定端口号为 8080
     open: true,  // 自动在浏览器中打开
     hot:true //开启热更新
  }
};

  1. package.json - 添加必要的依赖和脚本:
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "A simple webpack demo",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --open",
    "build": "webpack"
  },
  "keywords": [
    "webpack",
    "demo"
  ],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.0",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.66.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.8.0"
  }
}

安装所需的依赖:

npm install //安装所有依赖  这里就不一一安装了直接使用我的package.json就好了

现在,我们已经准备好了我们的Webpack配置和项目文件。要执行打包并启动开发服务器,请运行以下命令:

npm start

Webpack将开始构建项目,并在浏览器中打开一个新的选项卡,显示我们的页面。你可以在控制台中看到来自Webpack的输出信息,以及我们在src/index.js中添加的日志信息。

这样就完成了简单的Webpack配置和项目设置。你自己可以根据需要扩展配置,并添加更多的功能和插件来满足项目的需求。

三、Webpack 生命周期和工作原理

3.1 生命周期概述

Webpack 的运行过程就是一个遵循生命周期的过程,在生命周期的各个阶段会执行对应的方法和插件,这些方法和插件就是生命周期事件。生命周期的主要事件有:

3.2 工作原理和流程

  1. 初始化(Initializing)

    • 读取并解析 Webpack 配置文件
    • 创建 Compiler 对象并装载所有配置项
  2. 编译(Compiling)

    • 创建 Compilation 对象
    • 构建模块依赖图谱(Module Graph)
    • 进行一系列编译
  3. 完成(Completion)

    • 对编译后的模块进行优化处理
    • 生成 Chunk 资源文件
  4. 发出(Emitting)

    • 将 Chunk 资源文件输出到指定位置
  5. 输出(Output)

    • 将资源文件输出到指定的输出路径
  6. 输出完成(Done)

    • 清理资源并输出统计信息

四、Webpack 高级概念和优化策略

4.1 Tree Shaking

优势

Tree Shaking 的优点包括:

  1. 减小文件大小,加快加载速度。
  2. 提高性能,降低资源消耗。
  3. 清除未使用的代码,优化代码质量。

其原理是基于静态代码分析和模块依赖图,识别未被实际使用的代码,并将其从最终生成的 bundle 中移除。

使用场景

  1. 在生产环境下自动开启
  2. 必须使用 ES6 Module 语法
  3. 需要配合 Terser 等压缩工具使用

最佳实践

一般情况下,生产环境下 Webpack 会自动开启 Tree Shaking,如果没有开启可以手动在配置文件中加入:

module.exports = {
  mode: 'production', 
  optimization: {
    usedExports: true
  }
}

示例

未使用 Tree Shaking:

// utils.js
export function add(a, b) {
  return a + b;
}

export function minus(a, b) {
  return a - b; 
}

// index.js
import { add } from './utils';

console.log(add(1, 2));

使用 Tree Shaking 后,minus 函数会被自动删除。

4.2 Code Splitting

优势

  • 提高资源加载速度
  • 提高缓存利用率
  • 并行加载资源

实现方式

  1. 入口点分割
  2. 动态导入(import())
  3. 按需加载

最佳实践

  • 入口点分割
// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  }
}
  • 动态导入
// index.js
import('./utils').then(utils => {
  utils.default();
});
  • 按需加载
// webpack.config.js 
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async'
    }
  }
}

4.3 缓存策略

  • 文件指纹

    通过为输出文件添加哈希值,可以有效防止浏览器缓存旧文件。

// webpack.config.js
module.exports = {  
  output: {
    filename: '[name].[chunkhash].js'
  }
}
  • 缓存清理 webpack5弃用了第三方的clean-webpack-plugin,取而代之的则是将清楚之前的打包缓存集成到out的属性中
module.exports = {

  // 出口
  output: {
    path: utils.resolve("../dist"),
    filename: "static/js/[name]." + ID + ".js",
    publicPath: "/", // 打包后的资源的访问路径前缀
    clean: true, //每次构建清除dist包
  }
}
  • 缓存压缩

    安装npm install terser-webpack-plugin启用资源压缩和缓存压缩文件可以减少传输体积,提高加载速度。

    const TerserPlugin = require("terser-webpack-plugin");
    module.exports = {
      optimization: {
       //打包的内容可在这里拆分文件和压缩css js 和拆分第三方的插件
        minimize: true, // 启用资源压缩
        minimizer: [
          new TerserPlugin({
            terserOptions: {
              compress: true, // 启用压缩
              mangle: true, // 启用混淆
            },
            extractComments: false, // 不提取注释
          })]
      }
    }
    

4.4 HMR(Hot Module Replacement)

可以在运行时更新各种模块,而无需进行完全刷新。webpack5简化了热更新的配置将其更简单的融合到了devServer的hot属性中

配置 HMR

// webpack.config.js
module.exports = {
  devServer: {
    hot: true//热更新
  }
}

4.5 多线程/多实例构建

通过 happypack或thread-loader 等工具可以启用多线程/多实例构建,提高构建速度。

HappyPack 示例

HappyPack 是一个能够通过多进程模型,来加速构建速度的工具。它可以将每个 Loader 的处理过程放到单独的 worker 池(worker pool)中,并行处理多个任务。使用 HappyPack,你需要对每个需要并行处理的 Loader 进行相应的配置。

// webpack.config.js
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配 JavaScript 文件
        use: 'happypack/loader?id=babel', // 使用 HappyPack 中的 babel loader
        exclude: /node_modules/ // 排除 node_modules 目录
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'babel', // 定义 HappyPack 的 id,用于区分不同的 loader
      threads: 4, // 启动 4 个线程来处理任务
      loaders: ['babel-loader'] // 使用 babel-loader 进行转译
    })
  ]
};

4.6 持久化缓存

webpack5弃用了cache-loader、hard-source-webpack-plugin 等插件。 通过内部的cache对象简单的配置即可开启持久化缓存,提高二次构建速度。

cache 示例

// webpack.config.js
module.exports = {
    // webpack5的缓存升级成内置的
  // 使用 webpack 5 内置的持久化缓存功能,你就不再需要手动安装和配置 cache-loader,Webpack 将会自  动处理构建过程中的缓存机制,从而提高构建性能。

  cache: {
    // 使用持久化缓存
    type: 'filesystem',
    // 可选的缓存目录,默认为 node_modules/.cache/webpack
    cacheDirectory: utils.resolve(__dirname, '.webpack_cache'),
  },
}

4.7 DllPlugin 和 DllReferencePlugin配合optimization的splitChunks对象拆分代码

DllPlugin 和 DllReferencePlugin 可以将常用的第三方库提取到单独的 dll 文件中,实现按需加载,提高构建速度。

DllPlugin 示例

// webpack.dll.config.js
const webpack = require('webpack'); // 导入 Webpack 模块

module.exports = {
  entry: { // 定义 DLL 包的入口点
    vendor: [ // DLL 入口点的键
      'react', // React 库
      'react-dom' // React DOM 库
    ]
  },
  output: { // 配置 DLL 包的输出设置
    filename: '[name].dll.js', // 使用模板生成 DLL 文件名(例如:vendor.dll.js)
    path: path.resolve(__dirname, 'dist'), // 输出目录的绝对路径
    library: '[name]_dll' // 加载 DLL 时暴露的全局变量名称
  },
  plugins: [
    new webpack.DllPlugin({ // 配置 DllPlugin
      path: path.resolve(__dirname, 'dist', '[name]-manifest.json'), // DLL 清单文件的路径
      name: '[name]_dll' // 全局变量和清单文件的名称
    })
  ]
};

DllReferencePlugin 示例

// webpack.config.js
const webpack = require('webpack'); // 导入 Webpack 模块

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dist/vendor-manifest.json') // 引用 DLL 清单文件
    })
  ]
};

optimization的splitChunks对象

const path = require('path');
const webpack = require('webpack');

module.exports = {
  optimization: {
    // 优化配置
    splitChunks: {
      // 使用 splitChunks 来拆分代码块
      cacheGroups: {
        // 缓存组配置
        react: {
          // 匹配规则,匹配 react 和 react-dom
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        reactRouter: {
          // 匹配规则,匹配 react-router
          test: /[\\/]node_modules[\\/](react-router)[\\/]/,
          name: 'reactRouter', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        axios: {
          // 匹配规则,匹配 axios
          test: /[\\/]node_modules[\\/](axios)[\\/]/,
          name: 'axios', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        common: {
          // 匹配规则,匹配其他 node_modules 中的模块
          test: /[\\/]node_modules[\\/]/,
          name: 'common', // 输出的 chunk 名称
          chunks: 'all', // 对所有类型的 chunk 生效
          minSize: 20000, // 模块的最小大小(字节)
          minChunks: 2, // 要生成的 chunk 的最小数量
          maxAsyncRequests: 30, // 按需加载时并行请求的最大数量
          maxInitialRequests: 30, // 入口点并行请求的最大数量
          enforceSizeThreshold: 50000 // 强制执行最小和最大大小限制
        },
        default: {
          // 默认配置
          minChunks: 2, // 最小 chunk 数量
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经存在的 chunk
        }
      }
    }
  }
};

4.8 分析构建性能

使用 webpack-bundle-analyzer、speed-measure-webpack-plugin 等工具可以分析构建性能瓶颈,优化构建速度。

webpack-bundle-analyzer 示例

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

五、Webpack 插件开发和实战

5.1 插件开发基础

Webpack 插件是一个具有 apply 方法的 JavaScript 对象,apply 方法会被 Webpack Compiler 调用,可以在不同生命周期钩子函数中执行相关任务。

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 发射资源前执行插件逻辑
      callback();
    });
}

module.exports = MyPlugin;

5.2 常用插件开发实战

  • 自动生成 HTML 文件插件
const fs = require('fs');
const path = require('path');
// 自动生成 HTML 文件插件
class HtmlGeneratorPlugin {
  apply(compiler) {
  // 注册 emit 钩子,该钩子在将资源输出到目标目录之前触发
    compiler.hooks.emit.tapAsync('HtmlGeneratorPlugin', (compilation, callback) => {
    // 生成 HTML 内容
      const htmlContent = `
        <!DOCTYPE html>
        <html>
          <head>
            <meta charset="UTF-8">
            <title>Webpack App</title>
          </head>
          <body>
            <script src="${compilation.assets['main.js'].publicPath}"></script>
          </body>
        </html>
      `;
// 将 HTML 内容添加到输出资源中
      compilation.assets['index.html'] = {
            source: () => htmlContent, // 返回 HTML 内容 
            size: () => htmlContent.length // 返回 HTML 内容的长度
      };
//回调函数
      callback();
    });
  }
}

module.exports = HtmlGeneratorPlugin;
  • 自动清理输出目录插件
const fs = require('fs');
const path = require('path');
// 自动清理输出目录插件
class CleanOutputPlugin {
  constructor(options) {
    this.outputPath = options.outputPath;
  }

  apply(compiler) {
  // 注册 done 钩子,该钩子在编译完成后触发
    compiler.hooks.done.tap('CleanOutputPlugin', (stats) => {
      const outputPath = this.outputPath || stats.compilation.outputOptions.path;// 获取输出路径
// 获取输出目录下的所有文件
      const files = fs.readdirSync(outputPath);
      // 遍历并删除每个文件
      for (const file of files) {
        fs.unlinkSync(path.join(outputPath, file));
      }
    });
  }
}

module.exports = CleanOutputPlugin;
  • Webpack Validator 插件

这是一个比较复杂的插件开发实例,用于校验 Webpack 配置文件的规则。包括检查配置项是否存在、类型是否正确、值是否在允许范围等。

// webpack-validator.js
const schema = require('./config-schema.json');
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true});
const validate = ajv.compile(schema);
// Webpack 配置校验插件
class WebpackValidator {
  apply(compiler) {
  // 注册 run 钩子,该钩子在开始编译前触发
    compiler.hooks.run.tap('WebpackValidator', () => {
    // 使用 Ajv 进行配置校验
      const valid = validate(compiler.options);
      // 如果配置不合法,则输出错误信息并退出进程
      if (!valid) {
        console.error('Webpack configuration is invalid:');
        validate.errors.forEach(error => {
          console.error(`${error.dataPath} ${error.message}`);
        });
        process.exit(1);
      }
    });
  }
}

module.exports = WebpackValidator;

使用

 // 引入自定义插件
const { HtmlGeneratorPlugin, CleanOutputPlugin, WebpackValidator } = require('./plugins');
module.exports = {
   ...
   plugins: [ new HtmlGeneratorPlugin(), // 自动生成HTML文件插件
   ...
}


六、Webpack 在项目中的实践和最佳实践

6.1 项目目录结构

  1. 创建项目目录结构
mkdir project
cd project
mkdir src dist src/components src/utils src/assets src/views src/router src/store
  1. 创建入口文件和 HTML 文件
touch src/index.js src/index.html
  1. 创建配置文件
touch .babelrc .eslintrc .gitignore webpack.config.js
  1. 初始化 npm 项目
npm init -y

现在,我们已经创建了项目所需的目录结构和配置文件。接下来,我们可以根据需要填充这些文件和目录,并配置 webpack 和其他工具。

project
├── dist
├── src
│   ├── components
│   ├── utils
│   ├── assets
│   ├── views
│   ├── router
│   ├── store
│   ├── index.js
│   ├── index.html
├── .babelrc
├── .eslintrc  
├── .gitignore
├── package.json
├── webpack.base.config.js
├── webpack.dev.config.js
├── webpack.prod.config.js

这里不用大家一步步安装了,直接复制我的package.json,动手能力强的还是希望大家自己一步步安装去遇见问题解决问题

{
  "name": "webpack-demo2",
  "version": "1.0.0",
  "description": "A webpack demo project",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --open",
    "build": "webpack --mode production"
  },
  "keywords": [
    "webpack",
    "demo"
  ],
  "author": "Alben",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.16.7",
    "@babel/preset-env": "^7.16.7",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "babel-loader": "^8.2.3",
    "css-loader": "^6.5.1",
    "html-webpack-plugin": "^5.5.0",
    "postcss-loader": "^6.2.1",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.2.5",
    "vue-loader": "^16.8.3",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.3",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "vue": "^2.6.14",
    "vue-router": "^3.5.3"
  }
}

6.2 环境配置

上面关于配置的废话优点多注释都在代码里,下面是个简单的示例大家自己酌情使用。 并没有完全使用到之前使用的所有插件,有需要的自己动手尝试。

  1. webpack.base.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // 入口文件配置
  entry: './src/index.js',
  // 输出文件配置
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出文件路径
    filename: '[name].[contenthash].js', // 输出文件名,[name] 会根据 entry 中的键名替换
    publicPath: '/' // 输出文件的公共路径
  },
  // 模块配置
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配规则,使用正则表达式匹配以 .js 结尾的文件
        exclude: /node_modules/, // 排除 node_modules 目录
        use: {
          loader: 'babel-loader', // 使用 babel-loader 处理匹配到的文件
          options: {
            presets: ['@babel/preset-env'] // 使用 @babel/preset-env 进行转译
          }
        }
      },
      {
        test: /\.css$/, // 匹配规则,使用正则表达式匹配以 .css 结尾的文件
        use: ['style-loader', 'css-loader', 'postcss-loader'] // 使用 style-loader、css-loader 和 postcss-loader 处理匹配到的文件
      },
      {
        test: /\.(png|jpg|gif)$/, // 匹配规则,使用正则表达式匹配以 .png、.jpg 或 .gif 结尾的文件
        use: [
          {
            type: "asset/resource", //webpack5 不再需要使用url-loader或者file-loader
            options: {
              name: '[name].[ext]', // 输出文件名,[name] 会替换为原文件名,[ext] 会替换为原文件扩展名
              outputPath: 'images/' // 输出文件的路径
            }
          }
        ]
      }
    ]
  },
  // 插件配置
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html', // 使用 ./src/index.html 文件作为模板
      filename: 'index.html' // 输出文件名
    }),
    new webpack.HashedModuleIdsPlugin() // 根据模块的相对路径生成一个四位数的 hash 作为模块 id
  ],
  // 优化配置
  optimization: {
    runtimeChunk: 'single', // 提取 webpack 运行时代码到单独的文件
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/, // 匹配规则,匹配 node_modules 目录下的文件
          name: 'vendors', // 输出 chunk 的名称
          chunks: 'all' // 在所有的 chunk 中使用这个缓存组
        }
      }
    }
  }
};
  1. webpack.dev.config.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'development', // 开发环境模式
  devtool: 'inline-source-map', // 使用 source map 提供源代码到构建后的代码的映射
  devServer: {
    contentBase: './dist', // 服务的根目录
    hot: true // 开启模块热替换
  }
});
  1. webpack.prod.config.js
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
// //css压缩 webpack5 弃用改成css-minimizer-webpack-plugin
// const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'production', // 生产环境模式
  devtool: 'source-map', // 使用 source map 提供源代码到构建后的代码的映射
  module: {
    rules: [
      {
        test: /\.css$/, // 匹配规则,使用正则表达式匹配以 .css 结尾的文件
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] // 使用 MiniCssExtractPlugin.loader、css-loader 和 postcss-loader 处理匹配到的文件
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css', // 输出文件名
      chunkFilename: '[id].[contenthash].css' // 用于按需加载的 chunk 的输出文件名
    })
  ],
  optimization: {
    minimize: true, // 开启代码压缩
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: true, // 开启压缩
          mangle: true // 开启代码混淆
        },
        extractComments: false // 不提取注释
      }),
      new CssMinimizerWebpackPlugin({ // 添加 CssMinimizerWebpackPlugin 实例到 minimizer 数组中   
              parallel: true, // 启用并行压缩 
              minimizerOptions: { 
                  preset: ['default', { discardComments: { removeAll: true } }] // 使用默认配置,并移除所有注释 
            }
      }) 
      ],
    splitChunks: {
      chunks: 'all', // 对所有类型的 chunk 生效
      minSize: 20000, // 模块的最小大小(字节)
      maxAsyncRequests: 30, // 按需加载时并行请求的最大数量
      maxInitialRequests: 30, // 入口点并行请求的最大数量
      automaticNameDelimiter: '~', // 文件名的连接符
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, // 匹配规则,匹配 node_modules 目录下的文件
          priority: -10 // 优先级
        },
        default: {
          minChunks: 2, // 最小 chunk 数量
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经存在的 chunk
        }
      }
    }
  }
});

小结

通过深入学习和实践,相信你对Webpack的使用和优化已经有了更全面的认识。未来,随着Webpack的持续演进,我们期待更多强大的功能和特性的加入。作为技术人员,我们需要保持对Webpack生态的持续关注,并不断学习和探索新的最佳实践,以确保我们始终保持在技术的前沿。

七、Webpack 自定义 Loader 的实践

7.1 Loader 基础

Loader 是一个导出为函数的 JavaScript 模块,用于对模块的源代码进行转换。 Loader 函数接受源码作为参数,可以通过 return 的方式将转换结果返回给 Webpack。 它可以实现各种功能,如代码压缩、代码转译、静态资源处理等,是 Webpack 构建过程中的重要组成部分。 通过编写自定义的 Loader,可以根据项目需求灵活地对源代码进行处理,从而满足特定的开发需求。 Loader 的执行顺序由配置中的顺序决定,可以通过配置文件或内联方式指定 Loader 的使用顺序和参数。

//basetransfer-loader.js
// 导出一个函数,该函数接收源代码作为参数
module.exports = function(source) {
  // 对源码进行转换,这里使用正则表达式将换行符替换为空字符串,实现去除换行的功能
  const transformedSource = source.replace(/\n/g, '');
  // 返回转换后的源码
  return transformedSource;
}

测试使用(下面的示例测试方式皆如此不再重复)

1、在项目的根目录下打开终端命令

2、使用 npm(Node.js 包管理器)或 yarn(另一种常用的包管理器)安装自定义 loader。假设你已经创建了一个名为 basetransfer-loader 的文件夹,并在其中包含了你提供的 loader 文件。使用以下命令安装:

npm install ./basetransfer-loader

或者使用 yarn 安装:

yarn add ./basetransfer-loader

这将会把 basetransfer-loader 安装到项目的 node_modules 文件夹中。 3、在 webpack 的配置文件中,像使用其他 loader 一样使用你的自定义 loader。假设你希望在处理 JavaScript 文件时使用这个 loader,在 webpack 配置文件中的 module.rules 部分添加如下配置:

module.exports = {
  // 其他配置...
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配 JavaScript 文件
        use: 'basetransfer-loader', // 使用 basetransfer-loader 进行处理
        enforce: 'pre' // 确保在其他 loader 之前执行
      },
      // 其他规则...
    ]
  }
};

这个配置会告诉 webpack 在处理 JavaScript 文件之前先使用 basetransfer-loader 这个 loader。

4、这样,你的自定义 loader 就会被安装到本地项目中,并且可以在 webpack 构建过程中使用了。

新手小白Q&A

webpack小白提问:"自定义loader需要手动引入到配置文件吗"

老鸟回答:"对于这个简单的自定义 loader,不需要显式地引入其他模块或文件,因为它只是对源码进行简单的处理,并没有依赖于其他模块。

所以在 webpack 配置文件中,只需要指定 loader 的名称即可,webpack 会自动从 node_modules 中找到对应的 loader 文件并加载。

因此,在 webpack 配置中直接指定 basetransfer-loader 即可,不需要额外的引入。"

7.2 常用 Loader 开发实战

通过上面的简单介绍想必你对webpack 自定义loader已经有一个基本的认知了,下面跟我这我一起来动手试试,定义自己的loader。

自动 import/require 转换 Loader

// import-export-loader.js

// 定义模块导入和导出数组
const imports = [];
const exports = [];

// 正则表达式匹配模块导入和导出语句
const importRegExp = /import\s+(.+)\s+from\s+['"](.+)['"]/g;
const exportRegExp = /export\s+(.+)/g;

// 替换源代码中的模块导入语句并将匹配结果存入imports数组
source = source.replace(importRegExp, (match, names, path) => {
  imports.push(`const { ${names} } = require('${path}');`);
  return '';
});

// 替换源代码中的模块导出语句并将匹配结果存入exports数组
source = source.replace(exportRegExp, (match, names) => {
  exports.push(`exports.${names} = ${names};`);
  return '';
});

// 返回处理后的源代码,包括模块导入、源代码主体、模块导出
return `${imports.join('\n')}\n\n${source}\n\n${exports.join('\n')}`;

说明:

相信你看到上面的内容感觉明白了但是又不是很通透下面我来给你讲解清楚每一步的含义和思考。

首先这个自定义 loader 的作用是将 ES6 的模块导入导出语法转换为 CommonJS 的 require 和 exports 语法:

    1. 定义模块导入和导出数组:
    const imports = []; // 用于存储模块导入语句
    const exports = []; // 用于存储模块导出语句
    

    这里创建了两个空数组,分别用来存储将要生成的模块导入和导出语句。

    1. 正则表达式匹配模块导入和导出语句:
    const importRegExp = /import\s+(.+)\s+from\s+['"](.+)['"]/g; // 匹配 import 语句
    const exportRegExp = /export\s+(.+)/g; // 匹配 export 语句
    

    这里定义了两个正则表达式,用于匹配 ES6 模块导入和导出语句。

    1. 替换源代码中的模块导入语句并将匹配结果存入 imports 数组:
    source = source.replace(importRegExp, (match, names, path) => {
      imports.push(`const { ${names} } = require('${path}');`); // 将匹配到的导入语句转换为 require 形式并存入数组
      return ''; // 将源代码中的 import 语句替换为空字符串
    });
    

    这里使用 source.replace() 方法,根据正则表达式匹配源代码中的 import 语句,将其转换为 CommonJS 的 require 形式,并存入 imports 数组中。

    1. 替换源代码中的模块导出语句并将匹配结果存入 exports 数组:
    source = source.replace(exportRegExp, (match, names) => {
      exports.push(`exports.${names} = ${names};`); // 将匹配到的导出语句存入数组
      return ''; // 将源代码中的 export 语句替换为空字符串
    });
    

    这里同样使用 source.replace() 方法,根据正则表达式匹配源代码中的 export 语句,将其转换为 CommonJS 的 exports 形式,并存入 exports 数组中。

    1. 返回处理后的源代码:
    return `${imports.join('\n')}\n\n${source}\n\n${exports.join('\n')}`;
    

    最后,将生成的模块导入、源代码主体和模块导出拼接起来,返回给 webpack 处理下一个 loader。

这样,当这个自定义 loader 被应用到 webpack 构建过程中时,它会将源代码中的 ES6 模块语法转换为 CommonJS 形式,使得代码可以在不支持 ES6 模块的环境中正常运行。

小结

因为本人比较辣第一个demo 和基础部分讲解会比较详细,下面demo注释基本上是逐行注释的,希望你带着上面学习的内容去理解下面的几个自定义loader,如果暂时不是很明白没关系,多看两遍上面的内容动手去尝试你就会明白下面的这些自定义loader的思路了,😄😄,🦀🦀。

7.3 更多的示例练习

自动样式前缀 Loader

// autoprefixer-loader.js
// 自动添加浏览器前缀的加载器

const autoprefixer = require('autoprefixer');
const postcss = require('postcss');

module.exports = function(source) {
  // 导入 autoprefixer 和 postcss 模块

  const options = {
    browsers: ['last 2 versions']
  };
  // 设置 autoprefixer 的选项,指定要兼容的浏览器版本为最近的两个版本

  const processed = postcss([autoprefixer(options)]).process(source, { from: undefined });
  // 使用 postcss 处理传入的源代码,并使用 autoprefixer 添加浏览器前缀

  return processed.css;
  // 返回处理后的 CSS 代码
}

自动添加 CSS Modules Loader

// css-modules-loader.js
// 导入必要的工具库
const loaderUtils = require('loader-utils');
const path = require('path');
const fs = require('fs');

// 导出 loader 函数,接受源代码 source 作为参数
module.exports = function(source) {
  // 获取 loader 的选项,如果没有则使用默认空对象
  const options = loaderUtils.getOptions(this) || {};
  // 根据资源路径获取模块名称,默认使用文件名(不带后缀)
  const moduleName = options.name || path.basename(this.resourcePath, '.css');

  // 创建空对象用于存储 CSS 类名映射关系
  const json = {};
  // 使用正则表达式替换源代码中的 CSS 类名,并生成对应的映射关系
  const replacedSource = source.replace(/.(\w+)(\s*{)/g, (match, className, brackets) => {
    // 根据选项中的前缀设置 CSS 类名前缀
    const classNamePrefix = options.prefix ? `${options.prefix}-${moduleName}` : moduleName;
    // 生成新的唯一 CSS 类名
    const newClassName = `${classNamePrefix}-${className}`;
    // 将原始 CSS 类名与新生成的 CSS 类名建立映射关系
    json[`.${className}`] = newClassName;
    // 返回替换后的 CSS 类名
    return `.${newClassName}${brackets}`;
  });

  // 根据模块名称生成对应的 JSON 文件路径
  const jsonPath = path.resolve(this.context, `${moduleName}.css.json`);
  // 将 CSS 类名映射关系写入 JSON 文件
  fs.writeFileSync(jsonPath, JSON.stringify(json));

  // 返回替换后的源代码
  return replacedSource;
}

自动添加 Base64 Loader

// base64-loader.js
// 导入必要的工具包
const loaderUtils = require('loader-utils'); // 从 loader-utils 中导入工具函数
const mime = require('mime'); // 导入 mime 库,用于获取资源的 MIME 类型

// 导出 loader 函数,接收源代码作为参数
module.exports = function(source) {
  // 获取 loader 的配置选项,若没有配置则使用空对象
  const options = loaderUtils.getOptions(this) || {};
  // 设置 base64 转换的字节限制,默认为 8192 字节
  const limit = options.limit || 8192;

  // 如果源代码长度超过了限制,直接返回源代码
  if (source.length > limit) {
    return source;
  }

  // 获取资源的 MIME 类型
  const mimetype = mime.getType(this.resourcePath);
  // 将源代码转换为 base64 编码格式,并拼接成 data URI
  const base64 = `data:${mimetype};base64,${source.toString('base64')}`;

  // 返回一个 JavaScript 模块,其中内容为 base64 编码后的资源
  return `module.exports = '${base64}'`;
}

自动添加 SourceMap Loader

// sourcemap-loader.js
// 这是一个自定义的webpack loader,用于处理JavaScript文件及其对应的源映射文件
const loaderUtils = require('loader-utils'); // 引入loader-utils模块,用于处理loader的参数
const path = require('path'); // 引入path模块,用于处理文件路径
const fs = require('fs'); // 引入fs模块,用于读取文件内容

// 导出一个函数作为loader的处理函数
module.exports = function(source, sourceMap) {
  // 获取loader的参数,如果没有参数则设为空对象
  const options = loaderUtils.getOptions(this) || {};
  // 获取当前处理的文件路径相对于webpack上下文的相对路径
  const sourcePath = path.relative(this.context, this.resourcePath);

  // 如果没有提供源映射,则从文件中读取源码内容,并创建一个源映射对象
  if (!sourceMap) {
    const sourceContent = fs.readFileSync(this.resourcePath, 'utf-8');
    sourceMap = this.sourceMap || {
      version: 3,
      sources: [sourcePath],
      sourcesContent: [sourceContent]
    };
  }

  // 创建一个新的源映射对象,确保其中的路径使用标准化的形式
  const newSourceMap = { ...sourceMap };
  newSourceMap.sources = newSourceMap.sources.map(source => path.normalize(source));

  // 调用webpack提供的callback函数,将处理后的源码及其源映射返回给下一个loader
  this.callback(null, source, newSourceMap);
}

总结

好了各位,关于Webpack的知识我就分享到这里。

无论你们对Webpack爱与恨,我都衷心希望它能在你们的项目中发挥应有的威力。

对了,最后给你们几个使用Webpack的小贴士:

第一,配置文件别看着就头疼,其实很简单;

第二,出了错别着急,stack trace很有用;

第三,永远相信那个绿色的进度条!

《Webpack 2024 前前端架构老鸟的分享(总篇)》方便有时间的同学通篇学习,三部曲是为了碎片学习的同学撰写。创作不易,希望看完留言点赞收藏🦀🦀。