webpack前端基建+手动实现devServer热更新

134 阅读3分钟

前言

继上次分享了从抖音哲玄大佬《大前端全栈实践》课程中学到的 BFF 层框架封装思路后,我的学习之旅继续推进到了第二章——围绕 Webpack 的前端基建。这一部分同样干货满满。下面,我将继续分享这部分学习带来的收获与思考。

也许你会好奇,现在打包工具层出不穷,vite、esbuild等,速度都比webapck要好,为什么还用webpack?但在我看来,工具只是一个手段,构建等手段和优化等方法都是万变不离其宗的,而且webapck也能让我们更好的了解前端基建设的原理。

作为前端,想必大家对 webpack 都有所了解,下面我就挑一些重点的基建来展开说说。

自动多入口打包配置

从下图⬇️可以看出,我们的项目是要实现一套系统模板,一个模板页,可以延伸出多套系统,从而减少搭建一套系统80%的重复性工作,对剩下的20%工作进行自定义。而一个系统需要一个入口文件(如一个vue项目),多个系统,就需要对多个入口进行打包。 IMG_9285.jpeg 多系统框架: IMG_9286.jpeg 但如果每新增一个系统,都要修改一次webapck配置,开发体验就不太好,我们需要自动获取所有系统的入口文件并配置到webpack中:

const path = require("path");
const glob = require("glob");
const webpack = require("webpack");

// 多入口配置
const entries = {};
// 获取pages页面目录下的所有入口文件main.js
const pages = glob.sync(path.resolve(process.cwd(), "./app/pages/**/main.js"));
// 不同入口文件打包输出的html配置
const htmlWebpackPlugins = [];

// 遍历获取入口文件路径
pages.forEach((page) => {
  const len = page.split("/").length;
  const name = page.split("/")[len - 2];
  const entryName = `entry.${name}`;
  entries[entryName] = page;
  htmlWebpackPlugins.push(
    new HtmlWebpackPlugin({
      template: path.resolve(process.cwd(), `./app/view/entry.tpl`),
      filename: `${name}.tpl`, // 生成的html文件名
      chunks: [entryName],
    })
  );
});

module.exports = {
  // 入口文件配置
  entry: entries,
  plugins: [
    // 打包输出的html配置
    ...htmlWebpackPlugins,
    // ...其他配置
  ],  
  // ...其他配置
};

打包性能优化

代码拆分

为什么要拆分代码?

  1. 减少代码冗余:  通过将多个文件共用的代码提取到独立的公共 chunk 中,避免重复打包,有效减小最终产物的总体积。
  2. 提升缓存效率:  将变动频率较低的代码(如第三方库 node_modules)分离到单独的 chunk。只要这些库版本不变,其文件名(通常基于内容哈希)就能保持稳定,浏览器便可以长期缓存这些资源,减少不必要的网络请求,加快页面加载速度。
  3. 加快首屏加载速度: 从主包中将首屏加载不需要对非关键代码拆分出去,减少首屏加载资源体积,加快加载速度。
  4. 利用浏览器并发请求: 一个包被拆分成多个包时,浏览器就可以并发获取多个包,提升加载速度。
module.exports = {
  module: {
    rules: [
      // 通过 MiniCssExtractPlugin.loader 处理 css 文件,将 css 文件提取出去
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    // 将 css 从 js 中拆分出去,有效利用缓存
    new MiniCssExtractPlugin({
      filename: "css/[name]_[chunkhash:8].bundle.css",
    }),
  ],
  optimization: {
    // 代码拆分
    splitChunks: {
      chunks: "all", // 所有chunk类型
      maxAsyncRequests: 10, // 异步加载的chunk最大并行请求数量
      maxInitialRequests: 10, // 初始加载的chunk最大并行请求数量
      cacheGroups: {
        // 将node_modules第三方库拆分到vendor包
        vendor: {
          name: "vendor",
          priority: 20,
          test: /[\\/]node_modules[\\/]/,
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 重用已存在的chunk
        },
        // 将被引用两次以上的公共代码拆分到 common 包
        common: {
          name: "common",
          priority: 10,
          minChunks: 2, // 最小被引用次数
          minSize: 1024 * 5, // 最小分割文件大小 byte
          reuseExistingChunk: true, // 重用已存在的chunk
        },
      },
    },
  },
}

代码压缩

生产环境下,对js、css、图片等文件进行压缩,减少资源体积。

  • js:webpack5在mode为production时,自动使用terser插件对js进行压缩,无需配置;
  • css:使用 css-minimizer-webpack-plugin
module.exports = {
    plugins: [
        // 压缩css
        new CssMinimizerPlugin(),
    ],
    // ...其他配置
}

手动实现 HMR 热更新

webpack5 自带 devServer 功能,但是为了更好的理解热更新的原理,我们选择手动实现devServer

  1. 通过 webapck-dev-middleware 监听代码文件变化并触发重新编译
  2. 通过 webpack-hot-middleware 来通知浏览器请求获取最新代码

开发环境 webpack配置:

const { merge } = require("webpack-merge");
const webpack = require("webpack");

// devServer 配置
const DEV_SERVER_CONFIG = {
  PORT: 8080,
  HOST: "127.0.0.1",
  HMR_PATH: "/__webpack_hmr", // 官方规定
  TIMEOUT: 20000,
};

// 开发环境给entry添加热模块更新
Object.keys(baseConfig.entry).forEach((key) => {
  baseConfig.entry[key] = [
    baseConfig.entry[key],
    // 热模块更新入口,官方指定的 hmr 路径,注意path必须只想热更新所在服务路径
    `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 = merge(baseConfig, {
  mode: "development",
  output: {
    path: path.join(process.cwd(), "./app/public/dist"),
    filename: "js/[name].bundle.js",
    // 静态资源请求路径,请求到热更新服务中,服务会将内存中的资源返回
    publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/`,
    globalObject: "this", // 全局对象,默认是 window
  },
  plugins: [
    // 热模块更新
    new webpack.HotModuleReplacementPlugin(),
  ],
});

module.exports = {
  webpackConfig,
  DEV_SERVER_CONFIG,
};

热更新服务启动脚本:

const webpack = require("webpack");
const express = require("express");
const path = require("path");
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");

// 实例化 express
const app = require("express")();
// 解析 webpack 配置
const compiler = webpack(webpackConfig);

// 指定静态文件目录,让浏览器从这个目录获取入口 html
app.use(express.static(path.join(__dirname, "../public/dist")));

// 使用 devMiddleware 中间件监听文件变化并触发重新编译
app.use(
  devMiddleware(compiler, {
    // 指定静态文件目录
    publicPath: webpackConfig.output.publicPath,
    // 将入口html文件(tpl)落盘,输出到文件夹中,其他文件在内存中
    writeToDisk: (filePath) => filePath.endsWith(".tpl"),

    // 允许跨域,让浏览器可以跨域访问热更新服务
    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",
    },
  })
);

// 使用 hotMiddleware 热更新中间件,通知浏览器刷新
app.use(
  hotMiddleware(compiler, {
    path: DEV_SERVER_CONFIG.HMR_PATH,
    log: () => {}, // 不显示日志
  })
);

// 提示等待 webpack 打包完成
console.log("请等待webpack初次构建完成提示...");

// 启动热更新服务
app.listen(DEV_SERVER_CONFIG.PORT, () => {
  console.log(
    `Server is running on http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}`
  );
});

全文完,感谢观看。