Webpack 工程化实现

775 阅读12分钟

Webpack 工程化

一、引言

Webpack 是一个强大的模块打包工具,在前端开发中,它能够帮助我们处理各种资源,如 JavaScript、CSS、图片等,还能进行代码分割、优化和压缩等操作,提升项目的开发效率和性能。本文将详细介绍今天实现的 Webpack 工程化开发相关功能。

功能概述

基于 Webpack 的工程化配置,主要包括以下内容:

  • 开发环境与生产环境的分离:通过 webpack.base.js 提供基础配置,分别创建 webpack.dev.js 和 webpack.prod.js 实现开发和生产环境的差异化配置。
  • 热更新(HMR)支持:在开发环境中引入 webpack-hot-middleware,实现代码修改后的自动刷新功能。
  • 模块化配置管理:通过 webpack-merge 插件合并基础配置与环境特定配置,提升代码可维护性。

插件详解

  • html-webpack-plugin:是 Webpack 中最核心的插件之一,专门用于动态生成 HTML 文件并自动注入打包后的资源(CSS/JS)。
  • happypack:基于 ​多进程并行处理 的 Webpack 插件,专为优化 JavaScript 打包性能设计。它通过将 Loader 的处理任务分配到多个子进程中执行,充分利用多核 CPU 资源,显著提升构建速度。
  • mini-css-extract-plugin:用于 ​CSS 提取 的核心插件,专为生产环境优化设计。它将 CSS 代码从 JavaScript bundles 中分离到独立的 CSS 文件,提升缓存复用率并减少 JS 文件体积。
  • clean-webpack-plugin:用于 ​自动清理构建目录 的核心插件,有效避免旧文件残留导致的部署错误或缓存问题。
  • css-minimizer-webpack-plugin:用于 ​CSS 文件压缩优化 的核心插件,专为生产环境设计。它通过移除冗余代码、合并重复规则、压缩关键属性值等方式显著减小 CSS 文件体积,提升加载性能。
  • html-webpack-inject-attributes-plugin:专注于 ​动态注入 HTML 属性 的 Webpack 插件,允许开发者在不修改模板文件的情况下,基于构建配置或环境变量向生成的 HTML 注入自定义属性(如 data-* 属性)。
  • terser-webpack-plugin:用于 ​JavaScript 代码压缩与优化 的核心插件,通过并发和缓存提升压缩阶段性能,​移除冗余代码,自动删除 console.logdebugger、未使用函数等。

二、 核心配置详解

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

基础配置文件包含所有环境通用的配置项,例如入口文件、输出路径、加载器(loaders)等。

// webpack.base.js
const glob = require("glob");
const path = require("path");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");

// 动态构造 pages 入口文件
const pageEntries = {};
// 动态构造 htmlWebpackPluginList 渲染模板
const htmlWebpackPluginList = [];
// 获取 app/pages 下所有的入口文件 (entry.xx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((file) => {
  const entryName = path.basename(file, ".js");
  // 构造 entry
  pageEntries[entryName] = file;
  htmlWebpackPluginList.push(
    // html-webpack-plugin 插件 辅助注入打包后的 boundle 文件到tpl文件中
    new HtmlWebpackPlugin({
      // 产物 最终模板 输出路径
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist/",
        `${entryName}.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/views/entry.tpl"),
      // 注入的代码块
      chunks: [entryName],
    })
  );
  // 构造最终渲染的页面文件
});

/**
 * webpack.base.js 基础配置
 * */

module.exports = {
  // 入口文件
  entry: pageEntries,
  // 出口文件 产物输出路径,开发和生产环境输出不一致,须在各自环境中配置
  output: {},
  // 模块加载解析器
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: "vue-loader",
      },
      {
        test: /\.js$/,
        include: [path.resolve(process.cwd(), "./app/pages")], //只针对 业务代码 进行babel转换 加快webpack打包速度
        use: { loader: "babel-loader" },
      },
      {
        test: /\.(png|jpe?g|gif)(\?.+)?$/,
        use: {
          loader: "url-loader",
          options: {
            limit: 1024, // 1024 以下的图片打包成 base64 格式
            esModule: false,
          },
        },
      },
      {
        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",
      },
    ],
  },
  // 模块解析具体行为
  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-loader 插件 处理.vue文件必须配置
    // 将定义过的其他规则复制并应用到.vue文件里  /\.js$/ 他会将该规则定义到.vue中的<script>模块内
    new VueLoaderPlugin(),
    // 配置 ProvidePlugin 插件 将第三方库暴露到window全局 contenxt下
    // 这样就可以在业务代码中直接使用 window.Vue 来使用Vue
    new webpack.ProvidePlugin({ Vue: "vue" }),
    // 配置 DefinePlugin 插件 定义全局变量
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: "true", // 支持vue解析optionsApi
      __VUE_PROD_DEVTOOLS__: "false", // 禁用Vue调试工具
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false", // 禁用Vue生产环境下显示 水合 信息
    }),
    ...htmlWebpackPluginList,
  ],
  // 配置 打包输出优化 (代码分割|模块合并|缓存|TreeShaking|压缩等优化策略)
  optimization: {
    /**
     * 代码分割 配置
     * 把 js 文件打包成3中类型
     * 1. vendor: 第三方lib库,基本不会改动,除非依赖版本升级
     * 2. common: 业务组件代码公共部分抽离,改动较少
     * 3. entry.{page}: 不同页面 entry 里面业务组件代码差异部分,会经常改动
     * 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存效果
     */
    splitChunks: {
      chunks: "all", //对同步和异步模块都进行分割
      maxAsyncRequests: 10, // 按需加载时最大并行请求数
      maxInitialRequests: 10, // 一个入口最大并行请求数
      cacheGroups: {
        vendor: {
          // 打包第三方依赖库
          name: "vendor", // 模块名称
          test: /[\\/]node_modules[\\/]/, //打包 node_modules下的模块
          priority: 20, // 优先级,值越大优先级越高
          enforce: true, // 强制打包
          reuseExistingChunk: true, // 复用已存在的模块
        },
        common: {
          // 公共模块
          name: "common", // 模块名称
          minChunks: 2, // 被引用次数大于等于2次的模块
          minSize: 1, // 最小分割文件大小(1 byte)
          priority: 10, // 优先级,值越大优先级越高
          reuseExistingChunk: true, // 复用已存在的模块
        },
      },
    },
    // 将 webpack 运行时生产的代码打包到 runtime.js 中,减少 main.js 的体积
    runtimeChunk: true,
  },
};

2.2 开发环境配置 (webpack.dev.js)

基于 Express 的本地开发服务器,配合 Webpack 进行前端项目的开发。该服务器支持热更新功能,能够在代码修改后自动更新浏览器页面,提高开发效率。同时,它还配置了跨域请求的支持,方便前后端分离开发。

开发环境配置主要关注开发效率,包括热更新、Source Map 等功能。

// webpack.dev.js
const path = require("path");
const mrege = require("webpack-merge");
const webpack = require("webpack");

// 基类配置
const baseConfig = require("./webpack.base.js");
// dev-server 配置
const DEV_SERVER_CONFIG = {
  HOST: "127.0.0.1",
  PORT: 9002,
  HMR_PATH: "__webpack_hmr", //官方规定配置
  TIMEOUT: 20000,
};

// 开发阶段 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((entryKey) => {
  // 第三方包不作为 hmr入口
  if (entryKey !== "vendor") {
    baseConfig.entry[entryKey] = [
      // 主入口文件
      baseConfig.entry[entryKey],
      // 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`,
    ];
  }
});

// 开发环境配置
const webpackConfig = mrege.smart(baseConfig, {
  mode: "development", // 指定开发环境
  // source-map 配置 开发工具,呈现代码映射关系,便于开发环境的调试代码
  devtool: "eval-cheap-module-source-map", // 开发环境 配置
  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", // 全局对象
  },
  // 开发阶段插件
  plugins: [
    // 用于实现热模块替换 (Hot Module Replacement 简称 HMR)
    // 模块热替换允许在应用程序运行时更新部分模块
    // 极大的提升开发效率,能让应用程序一直保持运行状态
    new webpack.HotModuleReplacementPlugin({
      multiStep: true, // 热更新 多步模式
    }), // 热更新插件
    new webpack.DefinePlugin({
      // 全局变量
      __DEV__: true,
    }),
  ],
});

module.exports = {
  // webpackConfig 配置
  webpackConfig,
  // devServer 配置 暴露给dev.js使用
  DEV_SERVER_CONFIG,
};

开发环境 启动文件 (dev.js)

// dev.js 本地开发启动 devServe
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配置 和 devServer配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");

const app = express();

const compiler = webpack(webpackConfig);

// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));
// 引用 devMiddleware 中间件(监控文件改动)
app.use(
  devMiddleware(compiler, {
    // 落地文件路径
    writeToDisk: (filePath) => filePath.endsWith(".tpl"),
    // 资源路径
    publicPath: webpackConfig.output.publicPath,
    // headers 配置
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "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;

// 启动 webpack-dev-middleware,将 webpack 编译打包后的产品文件挂在到 express 服务上
app.listen(port, () => {
  console.log(`express as app listening on port ${port}`);
});



2.3 生产环境配置 (webpack.prod.js)

生产环境下使用 Webpack 对项目进行打包构建。它会读取 Webpack 生产环境配置文件,然后执行打包操作,并将打包过程中的统计信息输出到控制台,方便开发者了解打包情况。

生产环境配置注重性能优化,包括代码压缩、Tree Shaking 等。

// webpack.prod.js
const path = require("path");
const mrege = require("webpack-merge"); // 基类配置
const baseConfig = require("./webpack.base.js");

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 happypackCommonConfig = {
  debug: false,
  threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};

// 生产环境配置
const webpackConfig = mrege.smart(baseConfig, {
  mode: "production", // 指定生产环境
  output: {
    filename: "js/[name]_[chunkhash:8].bundle.js",
    path: path.join(process.cwd(), "./app/public/dist/prod"),
    publicPath: "/dist/prod",
    crossOriginLoading: "anonymous",
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "happypack/loader?id=css"],
      },
      {
        test: /\.js$/,
        include: [path.resolve(process.cwd(), "./app/pages")], //只针对 业务代码 进行babel转换 加快webpack打包速度
        use: { loader: "happypack/loader?id=js" },
      },
    ],
  },
  // webpack 插件 不会有大量 hints 信息 默认为 warning
  performance: {
    hints: false,
  },
  plugins: [
    // 每次 build 前删除 pubilc/dist 目录
    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",
    }),
    // 优化并压缩 css 文件
    new CSSMinimizerPlugin(),
    // 多线程打包 js,提高打包速度
    new HappyPack({
      id: "js",
      ...happypackCommonConfig,
      loaders: [
        `babel-loader?${JSON.stringify({
          presets: ["@babel/preset-env"],
          plugins: ["@babel/plugin-transform-runtime"],
        })}`,
      ],
    }),
    // 多线程打包 css,提高打包速度
    new HappyPack({
      id: "css",
      ...happypackCommonConfig,
      loaders: [
        {
          path: "css-loader",
          options: { importLoaders: 1 },
        },
      ],
    }),
    // 浏览器在请求资源时不发送用户的身份凭证
    new HtmlWebpackInjectAttributesPlugin({ crossorigin: "anonymous" }),
  ],
  optimization: {
    // 使用 TerserPlugin 的并发和缓存 提升压缩阶段性能
    // 清除 console.log 等代码
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true, // 利用多核cpu优化来加快压缩速度
        cache: true, // 启用缓存加速构建过程
        terserOptions: {
          compress: {
            drop_console: true, // 清除 console.log 等代码
          },
        },
      }),
    ],
  },
});

module.exports = webpackConfig;

生产环境 启动文件 (prod.js)

// prod.js
const webpack = require("webpack");
const webpackProdConfig = require("./config/webpack.prod.js");

console.log("now in produce building... ");

webpack(webpackProdConfig, (err, stats) => {
  if (err) {
    console.log(err);
    return;
  }
  process.stdout.write(
    `${stats.toString({
      colors: true, // 增加控制台颜色信息
      modules: false, // 不显示内置模块打包信息
      children: false, // 不显示子级编译任务信息
      chunks: false, // 不显示每个代码块的信息
      chunkModules: true, // 显示代码块中模块信息
    })}`
  );
});


三、 关键点解析

3.1 热更新(HMR)
  • 在开发环境中,通过 webpack-hot-middleware 实现热更新功能。
  • 配置中将 HMR 入口文件添加到主入口文件中,确保页面修改后能够实时刷新。
3.2 模块化配置管理
  • 使用 webpack-merge 插件合并基础配置与环境特定配置,避免重复代码。
  • 通过 merge.smart 方法智能合并数组和对象,确保配置项不会被覆盖。
3.3 性能优化
  • 代码压缩:使用 TerserPlugin 对生产环境代码进行压缩,减少文件体积。
  • Tree Shaking:通过 splitChunks 配置对第三方库进行单独打包,提升加载速度。

四、 配置启动脚本

在 package.json 中添加启动打包脚本:

package.json
{
  "scripts": {
    "build:dev": "node --max_old_space_size=4096 ./app/webpack/dev.js",
    "build:prod": "node ./app/webpack/prod.js"
  },
}

  • --max_old_space_size=4096:这是 Node.js 的一个选项,用于设置 Node.js 进程的最大堆内存大小。max_old_space_size 表示 V8 引擎的老生代堆内存的最大容量,单位是 MB。这里将其设置为 4096MB,意味着 Node.js 进程在运行过程中,老生代堆内存最多可以使用 4GB 的空间。在处理大型项目或进行复杂的构建任务时,可能会消耗大量的内存,通过设置这个选项可以避免因内存不足而导致的程序崩溃。
4.1 启动打包编译开发环境

运行以下命令启动开发服务器:

npm run build:dev
npm run dev
4.2 启动打包编译生产环境

运行以下命令生成生产环境代码:

npm run build:prod

五、 总结

通过本次开发,我们实现了 Webpack 的工程化配置,涵盖了开发与生产环境的分离、热更新支持以及性能优化等内容,在 webpack.dev.js 所配置的开发环境中,热更新(Hot Module Replacement,简称 HMR)和打包是紧密相关且相互协作的,它们共同服务于提高开发效率这一目标。下面详细阐述二者的关系:

1. 打包是热更新的基础

  • 资源整合与处理:Webpack 的核心功能是打包,它会从项目的入口文件开始,递归地分析项目中的所有依赖关系,将各种类型的资源(如 JavaScript、CSS、图片等)进行处理和整合。在 webpack.dev.js 中,通过配置 entry 指定入口文件,output 指定输出路径和文件名,以及 module.rules 配置不同类型文件的加载器,完成对项目资源的打包。例如:
webpack.dev.js
Apply
const path = require("path");
// ...
const webpackConfig = mrege.smart(baseConfig, {
    // ...
    entry: {
        main: './src/index.js' // 入口文件
    },
    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/` // 外部资源公共路径
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: 'babel-loader', // 处理 JavaScript 文件
                exclude: /node_modules/
            },
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader'] // 处理 CSS 文件
            }
        ]
    }
    // ...
});
  • 为热更新提供基础结构:打包过程会生成一个包含所有模块信息的文件结构,热更新正是基于这个结构来实现的。每个模块在打包后都有唯一的标识符,热更新利用这些标识符来识别和替换发生变化的模块。

2. 热更新是打包在开发环境的优化

  • 避免全量打包:在开发过程中,如果每次修改代码都进行全量打包,会消耗大量的时间,降低开发效率。热更新机制允许在不重新加载整个页面的情况下,只更新发生变化的模块。例如,当修改了某个 JavaScript 模块或 CSS 文件时,Webpack 会监测到文件的变化,只对该模块进行重新打包,并将更新后的模块发送到浏览器,浏览器会自动替换旧的模块,从而实现页面的局部更新。
  • 提高开发体验:热更新能够保留应用的当前状态,如表单输入、滚动位置等,让开发者可以更流畅地进行开发和调试。在 webpack.dev.js 中,通过配置 HotModuleReplacementPlugin 来启用热更新功能:
webpack.dev.js
Apply
const webpack = require("webpack");
// ...
const webpackConfig = mrege.smart(baseConfig, {
    // ...
    plugins: [
        new webpack.HotModuleReplacementPlugin({
            multiStep: true // 热更新多步模式
        })
    ]
    // ...
});

3. 二者协同工作流程

  1. 启动开发服务器:运行 npm run build:dev 启动 Webpack 开发服务器,服务器会根据 webpack.dev.js 的配置进行首次打包,并将打包后的文件输出到指定的目录。
  2. 监听文件变化:开发服务器会监听项目文件的变化,当检测到某个文件发生修改时,Webpack 会重新打包该文件及其依赖的模块。
  3. 发送更新信息:Webpack 会将更新后的模块信息发送到浏览器,浏览器根据这些信息找到对应的旧模块,并进行替换。
  4. 应用更新:浏览器应用更新后的模块,页面会自动更新,展示最新的代码效果。

综上所述,打包是热更新的基础,为热更新提供了必要的模块结构和资源整合;而热更新是打包在开发环境的优化,通过局部更新模块,避免了全量打包的时间消耗,提高了开发效率和体验。二者相互配合,共同为开发者提供了一个高效的开发环境。