Elpis Webpack工程化·自我学习总结

一.认识Webpack

webpack是一个现代JavaScript应用程序的静态模块打包器。它主要用于将多个模块(包括JavaScript、CSS、图片等)打包成一个或多个bundle文件,以便在浏览器中高效加载。

webpack的作用:

  1. 模块打包:将多个模块按照依赖关系打包成静态资源,减少HTTP请求次数。
  2. 编译转换:通过加载器(loader)将其他类型的文件(如TypeScript、CSS、图片、字体)转换成浏览器可识别的格式。
  3. 代码分割:将代码拆分成多个块,实现按需加载,提高首屏加载速度。
  4. 插件系统:通过插件(plugin)扩展功能,如优化、压缩、环境变量注入等。
  5. 开发服务器:提供热重载(hot reload)功能,提升开发效率。

二.Webpack构建流程

1.读取配置文件

Webpack会读取命令行传入的参数以及项目中的配置文件(webpack.config.js),合并成最终配置。

2.创建 Compiler 对象

根据上面的配置创建 Webpack 的核心对象 Compiler,并注册所有配置的插件,执行插件的 apply方法

3.解析Entry入口文件

为每个Entry 入口文件创建Module对象,将入口模块添加到编译依赖图中

4.Loader模块转换

根据模块文件类型(js,css,.vue,.ts)匹配对应的Loader,Loader从左到右(从上到下)顺序执行,将模块内容转化为JavaScript模块,Loader翻译完成后,Webpack将js代码转换成AST(抽象语法树)

5.分析依赖

在中AST遍历寻找import,require等依赖,针对每个模块进行"Loader转换 -> AST解析 -> 分析依赖"

6.封装模块

这里通过seal()所有模块编译完成,得到模块依赖图

Tree Shaking 优化(标记未使用的导出,减少无用的依赖):Webpack 依靠ES6 Module 的静态结构,分析出那些export 没被引用,再下一步的Chunk代码中进行移除

7.组装Chunk文件

根据入口文件(Entry)和代码分割(splitChunks)规则,Webpack合并成一个或多个Chunk 代码块

8.输出文件

Webpack 会根据配置的output.path,output.feilname,将文件写入磁盘中,一般生成一个新的dist文件夹

三.Webpack的核心概念

1. 入口文件(Entry)

Entry是webpack构建过程的起点,从入口开始递归地构建成一个依赖图,将每个模块打包到一起,最终生成一个xxxx.bundle.js文件产物。入口可以配置单入口或多入口。

  • 单入口: 只有一个入口文件的引入,只生成一个对应的bundle.js文件
  • 多入口: 拥有多个入口文件的引入,生成多个对应的bundle.js文件

不管单入口还是多入口的文件最终产物生成的是bundle.js文件,只是生成数量的问题,最终我们是需要将引入到页面中进行业务的呈现。此时Webpack中使用html-webpack-plugin插件会将我们的bundle.js文件自动引入到我们指定的页面中。

JavaScript 复制代码
// html-webpack-plugin 使用逻辑
//动态构造 entry 入口对象和 html-webpack-plugin 插件实例
const HtmlWebpackPlugin = require('html-webpack-plugin')

const pageEntries = {}
const htmlWebpackPluginList = []
//自动加载app/pages目录下的入口文件
const entryList = glob.sync(path.resolve(process.cwd(), './app/pages/**/entry.*.js'))
entryList.forEach((filePath) => {
  const entryName = path.basename(filePath, '.js')
  //构造entry对象
  pageEntries[entryName] = filePath
  //构造最终渲染的页面文件
  htmlWebpackPluginList.push(new HtmlWebpackPlugin({
    //产物 (最终末班) 输出路径
    filename: path.resolve(process.cwd(), './app/public/dist/', `${entryName}.tpl`),
    //指定要使用的模板文件
    template: path.resolve(process.cwd(), './app/view/entry.tpl'),
    //要注入的代码块
    chunks: [entryName],
  }))
})

module.exports = {
   //入口配置
  entry: pageEntries,
  plugins:[
     ...htmlWebpackPluginList
  ]
}

因为在多入口的配置过程中,我们需要将进行 new HtmlWebpackPlugin 该操作来将bundle.js自动注入页面模版中, 如果入口数量过多需要多次 new HtmlWebpackPlugin 操作,所以采用动态构建来完成js代码注入工作。当然项目是个单入口的配置时,按照Webpack的配置进行单独配置就行,没有多次编写的麻烦。

2. 加载器(Loaders)

将非 JavaScript 模块转换为 webpack 能够处理的模块,即 webpack 本身只支持 JavaScript,通过加载器可以将其他类型的资源(如 CSS、图片、TypeScript 等)转换为有效的模块。

常用Loaders:

  • babel-loader:将ES 6转换为向后兼容的JS版本,解决不同浏览器之间的兼容性问题
  • css-loader: 解析 CSS 文件中的 @import 和 url(),主要是分析css中模块依赖关系
  • style-loader :将CSS注入页面中去,一般配合着css-loader一起使用
  • sass-loader/less-loader: 将sass/less 编译为为CSS.
  • file-loader :将文件复制到输出目录并返回 public URL
  • url-loader: 类似于 file-loader,但可以设置文件大小限制,将小文件转换为Base64编码的Data url
  • vue-loader :用来解析和转换(模版,脚本,样式).vue文件

如何实现一个Loader 加载器:

JavaScript 复制代码
// my-loader.js 
// 自定义 loader: 去除.vue文件中所有的console.log 打印
module.exports = function (source) {
    // source: 文件内容(字符串)
    // this: loader 上下文对象
    console.log(this.query); //接受 options 配置参数
    const closeConSource=source.replace(/console\.log\(.+?\);/g, '')
    return closeConSource
}

//自定义loader 引入
//遵循由下往上的规则,先执行自定义的loader再去执行vue-loader
{
   test: /\.vue$/,
   use: [
     'vue-loader',
    {
      loader: path.resolve(__dirname, '../loaders/my-loader.js'),
      options: { name: 'my-loader' }
     }
   ]
}

在使用vue-loader 如何进行查找顺序(由近到远,优先到项目结构内查找,再到全局进行查找):

  • 项目本地的 node_modules (优先)
  • 上级目录的 node_modules (递归向上查找)
  • 全局安装的 node_modules

3. 插件(Plugins)

插件是 Webpack 的扩展功能,拥有强大的能力。要使用一个插件,你只需要 require()它,然后把它添加到 plugins数组中。例如打包优化、资源管理、环境变量注入 等都可以通过插件来完成,可以在Webpack 的整个编译生命周期中通过hook函数来处理各种复杂的任务。

常见的Plugins 插件:

  • HtmlWebpackPlugin :将构建好的bundle,自动注入到页面模版文件中
  • CleanWebpackPlugin :每次构建前将上一次的构建产物删除
  • MiniCssExtractPlugin :将 CSS 从主 JS 文件中提取出来,成为一个独立的 CSS 文件(而不是嵌在 JS 里),利于浏览器缓存和并行加载。
  • CopyWebpackPlugin: 通常用于复制静态文件(图标,字体)到输出目录
  • DefinePlugin :可在代码中定义全局变量,用于在开发和生产不同环境下配置使用
  • HotModuleReplacementPlugin: Webpack中热模块替换(HMR)功能,在开发阶段能实时查看代码更新变化

如何实现一个自定义的plugins

①.首先我们要了解在Webpack中,插件是一个具有apply方法的JavaScript对象。apply方法会被webpack compiler调用,并且在整个编译生命周期都可以访问compiler对象。

JavaScript 复制代码
class MyCustomPlugin {
  // 构造函数,用于接收配置选项
  constructor(options = {}) {
    this.options = options;
  }
  // apply 方法,Webpack 会自动调用并传入 compiler 对象
  apply(compiler) {
    // 在这里注册hook函数
  }
}
module.exports = MyCustomPlugin;

②.了解webpack不同Hook函数的调用时期,这样确认我们编写的自定义plugins功能在对应的Hook函数下面进行,但是Hook函数的类型不同(同步/异步)我们注册事件的方式也不一样,例如常见的有 tap, tapAsync, tapPromise等,根据命名我就可以推断tap处理同步,tapAsync处理异步,以及在部分场景必须返回Promise时使用tapPromise

Hook 名称 类型 调用时机
entryOption SyncBailHook 在 webpack 选项中的 entry 被处理之后
compile SyncHook 开始编译之前
compilation SyncHook 编译创建之后
emit AsyncSeriesHook 生成资源到 output 目录之前
afterEmit AsyncSeriesHook 生成资源到 output 目录之后
done SyncHook 编译完成
failed SyncHook 编译失败

③.来实现控制台打印输出打包构建时间、各个产物文件大小信息与携带新生成版本信息文件的plugins

JavaScript 复制代码
class MyCustomPlugin {
    // 构造函数,用于接收配置选项
    constructor(options = {}) {
        this.options = options
    }
    // apply 方法,Webpack 会自动调用并传入 compiler 对象
    apply(compiler) {
        // 插件逻辑
        // 1. 在编译开始前执行
        compiler.hooks.beforeRun.tap('MyCustomPlugin', (compilation) => {
            console.log(`🚀 开始编译...`);
        });
        // 2. 在编译完成后执行
        compiler.hooks.done.tap('MyCustomPlugin', (stats) => {
            const compilation = stats.compilation;
            const endTime = stats.endTime;
            const startTime = stats.startTime;
            console.log(`✅ : 编译完成!`);

            //输出构建时间
            const buildTime = (endTime - startTime) / 1000;
            console.log(`⏰ 构建时间: ${buildTime.toFixed(2)}s`);

            //输出资源大小信息
            const assets = compilation.assets;
            Object.keys(assets).forEach(assetName => {
                const size = assets[assetName].size();
                const sizeKb = (size / 1024).toFixed(2);
                console.log(`📦 ${assetName}: ${sizeKb} KB`);
            });
        });
        // 3. 在资源输出到目录前执行(可以修改输出内容)
        compiler.hooks.emit.tapAsync('MyCustomPlugin', (compilation, callback) => {
            // 创建一个版本信息文件
            const buildInfo = {
                buildTime: new Date().toLocaleString(),
                hash: compilation.hash,
                version: process.env.package_version || '1.0.0'
            };
            // 将信息添加到编译资源中
            compilation.assets['build-info.json'] = {
                source: function () {
                    return JSON.stringify(buildInfo, null, 2);
                },
                size: function () {
                    return JSON.stringify(buildInfo).length;
                }
            };
            callback();
        });
    }
}
module.exports = MyCustomPlugin;

4. 优化产物输出(Optimization)

optimization配置项的主要目的是控制打包产物的优化行为

它的核心目标可以归结为三点:

  1. 减小打包体积 :通过压缩、摇树、作用域提升等技术,移除无效代码,减小文件大小。
  2. 提升运行性能 :通过代码分割等方式,实现按需加载,减少初始加载时间。
  3. 优化缓存 :通过合理的哈希策略和代码分离,使得未更改的文件能够被浏览器有效缓存,提升二次加载速度。

4.1 代码分割与分块

  1. splitChunks :这是最核心的代码分割配置,它主动用于拆分公共模块与第三方。

工作原理:Webpack 会分析模块的引用情况,根据你设置的规则(如大小、被引用次数等)自动将符合条件的模块提取到独立的 chunk 中。

JavaScript 复制代码
optimization: {
  splitChunks: {
    //'async'(默认值):只对异步加载(dynamic import)​ 的模块进行分割
    //'initial':只对同步初始化的模块进行分割。
    //'all':最常用、最彻底的优化。对所有模块(同步和异步)都应用分割规则。这能最有效地提取公共依赖。
    chunks: 'all',
    maxAsyncRequests: 10, // 按需加载时的最大并行请求数。设置此值可以防止chunk过多导致网络请求爆炸。
    maxInitialRequests: 10, // 入口点的最大并行请求数
    cacheGroups: {
      // 1. 抽离第三方库(如react, lodash)
      vendor: {
        test: /[\\/]node_modules[\\/]/, // 匹配node_modules下的文件
        name: 'vendors', //  chunk 名称
        priority: 20, // 优先级,数字越大优先级越高
        reuseExistingChunk: true, // 如果当前chunk包含的模块已经被抽出,则直接复用
      },
      // 2. 抽离公共工具函数或组件
      common: {
        name: 'common',
        minChunks: 2, // 模块被至少2个chunk引用时才被抽离
        minSize: 0, // 生成的chunk最小大小(字节)
        priority: 10,
        reuseExistingChunk: true,
      },
      // 3. 单独抽离某个大型库,如echarts
      echarts: {
        test: /[\\/]node_modules[\\/]echarts[\\/]/,
        name: 'chunk-echarts',
        priority: 30, // 优先级高于vendor
      },
    },
  },
}
  1. runtimeChunk:将 Webpack 的运行时(runtime)代码提取到单独的文件中。

什么是运行时:Webpack 用来连接模块化应用程序所需的一系列辅助代码。它负责模块的加载和解析逻辑。

为什么要提取:运行时代码虽然小,但会随着模块关系的变化而变化。如果不提取,即使你的业务代码没变,但只要模块ID等元信息变了,包含业务代码的vendor chunk的哈希值也会变,导致浏览器缓存失效。提取后,运行时代码单独成一个很小的文件,经常变动的只是它,而庞大的vendor chunk和业务chunk可以保持缓存。

JavaScript 复制代码
optimization: {
  runtimeChunk: `single`, // 提取所有入口点的运行时代码到一个文件中,如 `runtime~main.js`
  // 或者
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`, // 为每个入口生成独立的runtime文件
  },
}

4.2 代码压缩

JavaScript 复制代码
const TerserWebpackPlugin = require('terser-webpack-plugin');
const cssMinimizerPlugin = require('css-minimizer-webpack-plugin');

optimization: {
        minimize: true, //是否启用代码压缩
        minimizer: [
            new cssMinimizerPlugin(),//优化并压缩css资源
            new TerserWebpackPlugin({
                cache: true, //启用缓存来加速构建构建过程
                parallel: true, //利用多核cpu 优势加快压缩速度
                terserOptions: {
                    compress: {
                        drop_console: true, ///去掉 console.log 等语句
                    }
                }
            }),
        ],
}

四.环境模式

通过mode配置项的参数来知道当前的Webpack的构建环境,并且会启动Webpack针对不同环境的内置优化。

三个可选值:

  • 'development'- 开发模式
  • 'production'- 生产模式
  • 'none'- 无默认优化

不管是development还是production模式,我们进行Webpack配置项编写时可通过基础通用的配置与两种不同环境下的配置文件进行merge合并,生成最终的配置文件。

因为我们在两种不同的环境中所要求目标不一样,通过划分配置文件来形成在两种不同环境下的需求目标:

特性 开发模式 (development) 生产模式 (production)
主要目标 开发体验和构建速度 代码优化和性能
代码压缩 ❌ 不压缩,保持可读性 ✅ 深度压缩和优化
构建速度 ⚡ 快速构建和重载 ⏳ 较慢,但只执行一次
webpack.base.js webpack.dev.js webpack.prod.js
主要功能 存放开发/生产公共配置 热更新开发配置 产物优化配置
入口文件动态构建 dev-serveer 配置 代码分割
基础loader处理加载 source-map 代码映射配置 代码压缩
公共插件加载 热更新映射/模块更新配置 多线程加速打包

1.1 webpack.base.js (基础配置)

JavaScript 复制代码
const glob = require('glob')
const path = require('path')
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')

//动态构造 entry 入口对象和 html-webpack-plugin 插件实例
const pageEntries = {}
const htmlWebpackPluginList = []
//自动加载app/pages目录下的入口文件
const entryList = glob.sync(path.resolve(process.cwd(), './app/pages/**/entry.*.js'))
entryList.forEach((filePath) => {
  const entryName = path.basename(filePath, '.js')
  //构造entry对象
  pageEntries[entryName] = filePath
  //构造最终渲染的页面文件
  htmlWebpackPluginList.push(new HtmlWebpackPlugin({
    //产物 (最终末班) 输出路径
    filename: path.resolve(process.cwd(), './app/public/dist/', `${entryName}.tpl`),
    //指定要使用的模板文件
    template: path.resolve(process.cwd(), './app/view/entry.tpl'),
    //要注入的代码块
    chunks: [entryName],
  }))
})

/*
 * @Description: webpack基础配置
 */
module.exports = {
  //入口配置
  entry: pageEntries,
  //模块解析配置(决定需要加载解析什么模块,以及用什么方式解析)
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: { loader: 'vue-loader' }
      },
      {
        test: /\.js$/,
        include: [
          //只对业务代码进行vavel, 加快 webpack 打包速度
          //表示只对 app/pages 目录下的js文件进行 babel 转换
          path.resolve(process.cwd(), './app/pages'),
        ],
        use: { loader: 'babel-loader' },
      },
      {
        test: /\.(png|jpg|jpeg|gif)(\?.+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 8192,  // 小于 8KB 的图片转为 base64
            name: '[name].[ext]',
            outputPath: 'images/',
            esModule: false // 控制导出格式, true 导出为 esModule 模块, false 导出为 commonjs 模块
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
        test: /\.(eot | svg |ttf |woff |woff2)(\?\S*)?$/,
        use: 'file-loader'
      }
    ]
  },
  //产物输出路径,因为开发和生产环境的输出路径不同,所以需要区分,各自环境中分别配置
  output: {},
  //配置模块解析的具体行为(定义 webpack 在打包时,如何找到具体模块并解析)
  resolve: {
    extensions: ['.js', '.vue', '.less', '.css'],//配置解析的文件扩展名
    alias: {
      //配置别名,方便在代码中引入模块  
      $pages: path.resolve(process.cwd(), './app/pages'),
      $common: path.resolve(process.cwd(), './app/pages/common'),
      $widgets: path.resolve(process.cwd(), './app/pages/widgets'),
      $store: path.resolve(process.cwd(), './app/pages/store'),
    },
  },
  //配置webpack插件
  plugins: [
    //处理.vue文件,将自定义的其他规则复制并应用到 .vue 文件的解析中
    new VueLoaderPlugin(),
    //把第三方库暴露到 Window context下
    new webpack.ProvidePlugin({
      Vue: 'vue',
      axios: 'axios',
      _: 'lodash',
    }),
    //定义全局变量
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: 'true', //支持 vue 解析 options API
      __VUE_PROD_DEVTOOLS__: 'false', //是否开启 vue 生产环境下的 devtools 功能
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false', //是否开启 vue 生产环境下的 hydration  mismatch('水合') 详情功能
    }),
    //构建最终渲染的页面模板
    ...htmlWebpackPluginList,
  ],
  //配置打包输出优化(配置代码分割,模块合并,缓存,TreeShaing,压缩等优化策略)
  optimization: {
    splitChunks: {
      chunks: 'all', //对同步和异步模块都进行分割
      maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
      maxInitialRequests: 10, // 入口点的最大并行请求数
      cacheGroups: {
        //第三方库依赖库
        vendor: {
          test: /[\\/]node_modules[\\/]/, //打包 node_modules 中的文件
          name: 'vendor',//模块名称
          priority: 20, //优先级, 数值越大,优先级越高
          enforce: true, //强制分割, 不考虑 minSize, maxSize 等配置
          reuseExistingChunk: true, //复用已用的公共 chunk
        },
        //公共模块部分
        common: {
          name: 'common', //模块名称
          minChunks: 2, //模块被引用次数超过 2 次,才会被分割
          minSize: 1,
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
    //将 Webpack 的运行时(runtime)​ 代码提取到单独的文件中。
    runtimeChunk: {
      name: 'runtime',
    },
  },
}

1.2 webpack.prod.js (生产配置)

JavaScript 复制代码
const merge = require('webpack-merge'); //webpack 配置合并插件
const path = require('path')
const os = require('os')
const HappyPack = require('happypack')

const miniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin');
const cssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');

//基类配置
const baseConfig = require('./webpack.base.js');
//多线程 build 设置
const happypackCommonConfig = {
    debug: false,
    threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
}
//生产环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
    //指定生产环境
    mode: 'production',
    //生产环境的 output 配置
    output: {
        filename: 'js/[name]_[chunkhash:8].bundle.js',
        path: path.join(process.cwd(), './app/public/dist/prod'),
        publicPath: '/dist/prod/',
        crossOriginLoading: 'anonymous',//跨域加载资源时,是否添加 crossorigin 属性
    },
    //生产环境的模块解析配置
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    miniCssExtractPlugin.loader,
                    'happypack/loader?id=css',
                ]
            },
            {
                test: /\.js$/,
                include: [
                    path.resolve(process.cwd(), './app/pages'),
                ],
                use: ['happypack/loader?id=js'],
            },
        ]
    },
    // webpack 不会有大量的 hints 信息 ,默认是 warning
    performance: {
        hints: false,
    },
    plugins: [
        //每次build 前,先删除上一次的 build 结果
        new CleanWebpackPlugin(['public/dist'], {
            root: path.resolve(process.cwd(), './app/'),
            exclude: [],
            verbose: true,
            dry: false,
        }),
        //提取 css 的公共部分,有效利用缓存
        new miniCssExtractPlugin({
            chunkFilename: 'css/[name]_[contenthash:8].bundle.css',
            filename: 'css/[name].[contenthash:8].bundle.css' // 输出到 单独 css 文件夹
        }),
        //多线程打包 js ,加快打包速度
        new HappyPack({
            ...happypackCommonConfig,
            id: 'js',
            loaders: [{
                path: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env'],
                    plugins: ['@babel/plugin-transform-runtime']
                }
            }],
        }),
        //多线程打包 css ,加快打包速度
        new HappyPack({
            ...happypackCommonConfig,
            id: 'css',
            loaders: [{
                path: 'css-loader',
                options: {
                    importLoaders: 1,
                }
            }],
        }),
        //浏览器在请求资源时,不发送用户的身份凭证
        new HtmlWebpackInjectAttributesPlugin({
            crossorigin: 'anonymous',
        }),
    ],
    optimization: {
        minimize: true,
        minimizer: [
            //优化并压缩css资源
            new cssMinimizerPlugin(),
            new TerserWebpackPlugin({
                cache: true, //启用缓存来加速构建构建过程
                parallel: true, //利用多核cpu 优势加快压缩速度
                terserOptions: {
                    compress: {
                        drop_console: true, ///去掉 console.log 等语句
                    }
                }
            }),
        ],
        splitChunks: {
            cacheGroups: {
                vue: {
                    test: /[\\/]node_modules[\\/]vue/, // 匹配 node_modules 中的 Vue
                    name: 'vue', // 输出文件名为 vue.[hash].js
                    chunks: 'all', // 处理所有引入方式(同步/异步)
                    priority: 30, // 优先级高于 vendor(确保先拆分 Vue)
                    enforce: true, // 强制拆分(忽略 minSize 限制,即使 Vue 很小也拆分)
                },
            },
        },
    }
});

module.exports = webpackConfig;

在Elpis 项目中我们是使用HappyPack来启用多线程加速打包的速度,我们也可以使用thread-loader来优化打包速度。

JavaScript 复制代码
module: {
  rules: [
    {
      test: /.js$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: 2, // 线程数量,  使用 os.cpus().length 自动根据cpu核心生成线程数
            workerParallelJobs: 50, // 每个线程并行任务数
            poolTimeout: 2000 // 超时时间
          }
        },
        'babel-loader'
      ]
    }
  ]
}

1.3 webpack.dev.js (开发配置)

JavaScript 复制代码
const merge = require('webpack-merge'); //webpack 配置合并插件
const path = require('path')
const webpack = require('webpack')
//基类配置
const baseConfig = require('./webpack.base.js');
//dev-serveer 配置
const DEV_SERVER_CONFIG = {
    HOST: '127.0.0.1',
    PORT: 9002,
    HMR_PATH: '__webpack_hmr', //官方规定
    TIMEOUT: 20000,
}
//开发阶段的 entry 配置需要加入hmr
Object.keys(baseConfig.entry).forEach((entryName) => {
    if (entryName !== 'vendor') {
        baseConfig.entry[entryName] = [
            //主入口文件
            baseConfig.entry[entryName],
            //hmr 更新入口 ,官方指定的 hmr 路径
            `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
        ]
    }
})
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
    //指定开发环境
    mode: 'development',
    //source-map 配置,呈现代码的映射关系,便于观察代码
    devtool: 'eval-cheap-module-source-map',
    //开发环境的 output 配置
    output: {
        filename: 'js/[name]_[chunkhash:8].bundle.js',
        path: path.resolve(process.cwd(), './app/public/dist/dev/'), //输出文件存储路径
        publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, //外部资源公共路径
        globalObject: 'this',//指定全局对象,默认是 window
    },
    //开发环境插件
    plugins: [
        // 热更新插件,模块热更新允许在应用运行时,替换、添加或删除模块,而无需刷新整个页面
        new webpack.HotModuleReplacementPlugin({
            multiStep: false,
        }),
    ],
});
module.exports = {
    webpackConfig, //webpack 开发环境配置
    DEV_SERVER_CONFIG, //dev-server 配置
}

1.4 Source Map 配置选项总览

该配置选项主要便利开发时快速定位代码错误位置

配置值 构建速度 重建速度 质量 生产环境适用 特点
false 最快 最快 不生成 source map
eval 很快 最快 每个模块用 eval 执行
eval-source-map 一般 每个模块生成 DataUrl 的 source map
eval-cheap-source-map 较快 只映射行,不映射列
eval-cheap-module-source-map 一般 一般 中高 包含 loader 转换前的代码
source-map 最高 生成独立的 .map 文件
hidden-source-map 最高 生成 .map 文件但不引用
inline-source-map 最高 将 source map 内联到文件中
cheap-source-map 一般 一般 只映射行,忽略 loader source map
cheap-module-source-map 一般 一般 中高 包含 loader 转换前的代码
nosources-source-map 生成 .map 但不包含源代码内容

1.5 热更新(HMR)

HMR 的核心思想是:在应用程序运行过程中,替换、添加或删除模块,而无需完全重新加载整个页面

可以概括为以下几个步骤:

1. 启动阶段
  1. webpack 编译 :首先,webpack 在启动开发服务器时,会对源代码进行编译,并生成 bundle 文件(文件都在内存中)。
  2. 注入 HMR Runtime:在编译过程中,webpack 会向 bundle 中注入 HMR Runtime 代码。这段代码将在浏览器中运行,负责与开发服务器的 HMR 服务进行WebSocket通信,并处理更新逻辑。
2. 文件监听与编译
  1. 监听文件变化:webpack 通过文件系统监听来监测项目中的文件变化。
  2. 增量编译 :当文件发生变化时,webpack 会重新编译这些变化的模块。注意,这里不是完全重新编译,而是增量编译,只编译变化的模块和受影响的模块,以生成更新的模块代码。
3. 生成更新包(Update)

编译完成后,webpack 会生成一个更新包(update),这个包包含了变化的模块的代码以及更新信息(通常是一个 JSON 文件,描述了哪些模块发生了变化以及新的模块代码)。

4. 通知客户端
  1. WebSocket 推送:开发服务器通过 WebSocket 向浏览器发送一个消息,通知浏览器有更新可用。这个消息通常包含一个 hash 值(本次更新的标识)和更新包的 URL。
  2. 下载更新包:浏览器端的 HMR Runtime 收到通知后,会通过 AJAX 请求更新包(通常是一个 JavaScript 文件,包含了更新的模块代码)。
5. 应用更新
  1. 检查更新是否可应用:HMR Runtime 会检查当前模块树中哪些模块需要更新。它根据更新包中的信息,找到需要更新的模块。
  2. 模块替换 :对于 JavaScript 模块,HMR Runtime 会移除旧的模块,并执行新的模块代码。如果模块支持 HMR(即模块代码中包含了 module.hot.accept等处理代码),那么会执行模块自身的更新逻辑(例如,重新执行模块代码,并调用回调函数)。对于样式模块,通常直接替换 style 标签即可。
  3. 回调与错误处理 :如果更新过程中有错误,HMR Runtime 会回退到完整页面刷新。如果更新成功,可能会触发一些回调函数(例如,module.hot.accept中注册的回调)。
6.构建HMR热更新服务

在前面的所讲的webpack.dev.js环境下的配置文件,已做了相关的HMR配置内容。通过dev.js来作为HMR开发的启动文件,Express框架作为搭建HMR服务,webpack-dev-middleware插件监听文件的改动完成增量编译,webpack-hot-middleware来实现与浏览器之间的通讯,最终实现HMR代码更新。

dev.js

JavaScript 复制代码
// 本地开发启动 devserber
const express = require('express');
const path = require('path');
const consoler = require('consoler');
const webpack = require('webpack');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');

//从 webpack.dev.js 中引入 webpack 配置
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev.js');

const app = express();

const compiler = webpack(webpackConfig);
//指定静态文件目录
app.use(express.static(path.join(process.cwd(), './app/public/dist/')));
//引用 devMiddleware 中间件 监控文件改动
app.use(devMiddleware(compiler, {
    //落地文件
    writeToDisk: (filePath) => filePath.endsWith('.tpl'),
    //资源路径
    publicPath: webpackConfig.output.publicPath,
    //head 信息
    headers: {
        'Access-Control-Allow-Origin': '*',// 允许所有域名跨域访问
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',// 允许的HTTP方法
        'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',// 允许的请求头
    },
    stats: {
        colors: true,
    },
}));
//引用 hotMiddleware 中间件 实现热更新通讯
app.use(hotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
    log: () => { },
}));

consoler.info('请等待webpack初次构建完成提示....');

//启动服务
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
    console.log(`dev-server 启动成功, http://${DEV_SERVER_CONFIG.HOST}:${port}`);
})

dedv.jsheaders配置项配置解释:

因为HMR可能涉及到跨域请求(例如,前端应用运行在另一个端口上,而开发服务器在另一个端口)。 所以在Webpack开发中间件中配置headers,主要是为了控制CORS(跨域资源共享)相关的行为,确保在开发过程中,前端应用能够正确地从开发服务器加载资源并进行HMR通信。

相关推荐
LYFlied8 小时前
浅谈前端构建工具核心理解&&主流工具对比
前端·webpack·软件构建·rollup·vite·开发工具·工程化
LYFlied9 小时前
Webpack详细打包流程解析
前端·面试·webpack·node.js·打包·工程化
蜗牛攻城狮9 小时前
前端构建工具详解:Vite 与 Webpack 深度对比与实战指南
前端·webpack·vite·构建工具
F2E_Zhangmo1 天前
pnpm如何对node_modules打补丁
webpack·npm·pnpm
Mintopia1 天前
🧭 2025 年「大前端 Monorepo 架构」最佳实践指南
前端·前端框架·前端工程化
快乐点吧1 天前
为啥不用Webpack
前端·webpack·node.js
光影少年2 天前
webpack和vite区别及原理实现
webpack·vite·掘金·金石计划
大布布将军2 天前
一种名为“Webpack 配置工程师”的已故职业—— Vite 与“零配置”的快乐
前端·javascript·学习·程序人生·webpack·前端框架·学习方法