[学习]对前端工程化的理解

41 阅读10分钟

对前端工程化的理解

前端工程化不是某一个工具,而是 “规范 + 工具链 + 自动化流程” 的组合 。它的本质是让前端开发从 “单兵作战” 的作坊模式,转向 “团队协作” 的工业化模式,适配现代前端项目的复杂度和规模 **。

工程化的核心目标

  • 提升开发效率
  • 保障代码质量
  • 优化项目性能
  • 方便团队协助

在里程碑二中主要灌输一种工程化的思想和设计模型。在里程碑一中我们把业务文件通过解析器,也就是我们的服务引擎elpis-core,通过各种loader解析出我们的目标文件。在我们的前端工程化亦是如此,也是把业务文件通过解析引擎产出我们的目标文件。

前端工程化的解析引擎需要处理什么?

前端工程化的解析引擎主要做这三件事。

  1. 解析编译:源码的 “翻译与转换” 这是解析引擎的基础能力,核心是识别不同类型的文件并完成语法 / 格式转换,解决 “浏览器不认识的代码” 问题。 譬如:vue-loader 解析和编译 .Vue 文件
  2. 模块分包:代码的 “拆分与重组” 这是解析引擎处理大型项目的核心能力,目的是优化加载性能,实现按需加载,避免单文件体积过大。 譬如:公共模块common的抽离
  3. 压缩优化:产物的 “瘦身与提效” 是解析引擎的收尾优化环节,核心是减小产物体积,提升运行性能,面向最终上线的产物。 譬如:移除 console

前端工程化的解析引擎课程使用的是Webpack,当然你也可以用Vite等工具,但主要的思想还是没有变。还是那句话工具不是最重要的,只要适合你的项目就行,最重要的是工程化的思想和设计模型。

在webpack中,理论上,我们只需要配置这三个核心选项,webpack就能完成最基础的打包工作。

- entry:打包入口
- module:模块解析配置
- output:产物输出路径

“读入源码 → 转换处理 → 输出产物”。在这个流程骨架上,我们为了开发便利,提升开发效率,避免手写冗长路径,新增了resolve 配置模块解析的具体行为。譬如增加路径别名来简化业务代码中的导入路径。

为了实现更复杂的打包,实现自动化、性能优化,新增了 plugins 处理整个打包流程,提取公共部分、优化并压缩资源、生成HEML文件并自动注入资源、每次打包前自动清理输出目录、注入环境变量等。

为了解决打包的产物体积大、加载慢的问题,新增了 optimization 优化打包产物的性能。譬如代码分割,抽离第三方依赖库、抽离公共的js模块、使用多线程加速打包等

在webpack中拓展性最高的就是 plugins 模块,你可以引用其他的第三方库,来构建出你认为项目中最理想的打包流程

多页面开发

背景

因为我们项目是多页面构建,每一个页面都是一个独立的入口,因此我们不可能每创建一个页面就在entry中配置多一个入口和新增一个HtmlWebpackPlugin为每个页面生成最终的产物文件,这就违背了自动化的理念。

目的

所以为了实现页面新增的自动化配置,避免手动修改 Webpack 配置的重复劳动,我们可以基于前端工程化的自动化、规范化理念,制定一套标准方案:所有页面入口文件统一存放于 app/pages 目录下,并严格遵循 entry.xxx.js 的命名规范。

实现方案

通过 path 模块解析文件路径,结合 glob 库的文件匹配能力,自动扫描符合命名规范的入口文件,动态生成 Webpack 所需的 pageEntries 入口配置和 htmlWebpackPluginList 插件列表,最终注入 Webpack 核心配置中,实现新增页面 “零配置接入”。

前端内容基建

背景

上文提到本项目为 多页面Vue项目,每个页面都需要重复进行 createApp、注册插件Element PlusPinia 还有配置路由,挂载实例等操作,导致代码冗余度高,维护成本也高。 因此在基建中就需要把一些通用的启动逻辑抽取封装,形成统一的页面入口函数,实现 一处配置、多处复用。

目的

  • 所有页面的启动流程、插件注册方式保持一致,降低团队协作成本
  • 抽离公共逻辑,业务页面无需重复编写初始化代码,专注于核心功能开发
  • 支持个性化配置(路由模式、自定义插件),适配不同页面的差异化需求

实现方案

封装一个 Vue 页面通用启动函数,统一完成 Vue 应用的初始化工作,具体包含以下核心逻辑:

  1. 创建 Vue 应用实例,全局注册 Element Plus 组件库和 Pinia 状态管理;
  2. 引入全局自定义样式文件;
  3. 支持传入第三方插件列表,自动批量注册扩展能力;
  4. 支持传入路由配置,自动创建并挂载 Hash 模式路由,路由就绪后再挂载应用;无路由时直接挂载应用到 #root 节点;
  5. 业务页面只需传入根组件和个性化配置(路由、插件),即可一键启动,无需重复编写初始化代码。

分包策略配置的核心

背景

在开发中,随着业务迭代,项目体积会迅速膨胀。如果不进行合理的分包配置,Webpack 默认会将所有依赖(node_modules)和业务代码打包成一个巨大的 bundle.js 文件。这会导致以下两个核心问题: 1. 首屏加载慢(Load Performance):浏览器必须下载完整个巨大的 JS 文件,并进行解析(Parse)和编译(Compile)后,页面才能渲染。这会导致白屏时间过长,用户体验极差。 2. 缓存利用率低(Caching Efficiency):只要修改了一行代码(例如改了个 Bug),整个巨大的 bundle.js 的 Hash 就会变化,导致用户浏览器缓存完全失效,必须重新下载所有内容。

目的

实施分包策略的核心目标可以归纳为两个词:性能(Performance)缓存(Caching)

  • 减小首屏体积,按需加载
    • 将“首屏关键资源”与“非关键资源”分离。
    • 将“当前页面的代码”与“其他页面的代码”分离。
    • 实现并行加载:浏览器可以同时请求多个较小的文件,利用 HTTP 并发特性缩短加载时间。
  • 最大化长效缓存(Long-term Caching)
    • 业务与第三方分离:将业务代码(Business)和第三方库(Vendor)隔离。分离后,业务更新不影响 Vendor 的缓存。
    • 公共代码抽离:如果多个页面都用到了某个组件,将其抽离成 Common 包,避免重复打包,节省带宽。

实现方案

通过Webpack中的optimization.splitChunks配置项来实现,我们采用三级分包策略第三方lib库(vendor):基本不会改动的第三方库,除非是依赖升级,基本不改动模块 公共业务(common):业务组件代码中的公共部分,改动较少的模块 基础(entry.{page}):不同页面 entry 里的业务代码的差异部分,经常改动的模块

  // 配置打包输出优化(代码分割,模板合并,缓存,TreeShaing,压缩等优化策略)
  optimization: {
    splitChunks: {
      chunks: "all", // 对同步和异步模块都进行分割
      maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
      maxInitialRequests: 10, // 入口点的最大并行请求数
      cacheGroups: {
        vendor: {
          // 第三方依赖库
          test: /[\\/]node_modules[\\/]/, // 打包 node_module 中的文件
          name: "vendor", // 模块名称
          priority: 20, // 优先级, 数字越大, 优先级越高
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 复用已有的公共 chunk
        },
        common: {
          // 公共模块
          name: "common", // 模块名称
          minChunks: 2, // 被两处引用即被归为公共模块
          minSize: 1, // 最小分割文件大小(1 byte)
          priority: 10, // 优先级
          reuseExistingChunk: true, // 复用已有的公共 chunk
        },
      },
    },

    // 将 webpack 运行时生成的代码打包到 runtime.js
    runtimeChunk: true,
  },

热更新的理解

背景

在前端工程化体系中,与 elpis-core 一样,我们需要明确区分生产环境与开发环境,针对不同环境设计差异化的构建策略

  • 生产环境:目标是生成可部署的产物包,执行 build 命令后,所有文件会落地到磁盘(如 dist 目录),直接用于线上部署;
  • 开发环境:若每次修改代码都执行完整 build,会导致开发效率极低,因此需要实现无需手动构建、实时更新的开发体验。

目的

开发环境下,实现代码修改后自动编译 + 热更新,无需手动执行 build 命令,也无需刷新浏览器即可看到最新修改效果。

实现方案

基于 Express 框架搭建 devServer 开发服务器,配合 Webpack 的两个核心中间件完成热更新流程:

  1. webpack-dev-middleware(简称 devMiddleware
  • 核心作用:替代传统的 webpack build 命令,启动 Webpack 以监听模式编译代码;
  • 关键特性:编译后的产物不落地磁盘,而是直接存入内存,大幅提升编译速度;当监控到业务文件改动时,自动触发 Webpack 增量编译,更新内存中的产物。
  1. webpack-hot-middleware(简称 hotMiddleware
  • 核心作用:基于 WebSocket 协议建立浏览器与 devServer 的实时通信;
  • 工作流程:当 devMiddleware 完成内存产物更新后,hotMiddleware 会将更新通知发送到浏览器端,触发前端的热模块替换(HMR)逻辑,实现页面无刷新更新(如替换修改的组件、样式)。

整体流程:文件修改 → 监控工具检测变化 → devMiddleware 触发增量编译 → 产物存入内存 → hotMiddleware 通知浏览器 → 前端执行热更新。

const path = require("path");
const merge = 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,
  HMP_PATH: "__webpack_hmr", // 官方规定
  TIMEOUT: 20000,
};

// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((v) => {
  // 第三方包不作为 hmr 入口
  if (v !== "vendor") {
    baseConfig.entry[v] = [
      // 主入口文件
      baseConfig.entry[v],
      // hmr 更新入口, 官方指定的 hmr 路径
      `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMP_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",
  },
  // 开发阶段插件
  plugins: [
    // HotModuleReplacementPlugin 用于实现热模块替换(Hot Module Replacement 简称 HMR)
    // 模块热替换允许在应用程序运行时替换模块
    // 极大的提升开发效率,因为能让应用程序一直保持运行状态
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }),
  ],
});

module.exports = {
  // webpack 配置
  webpackConfig,
  // devServer 配置 暴露给 dev.js 使用
  DEV_SERVER_CONFIG,
};
// 本地开发启动 devServer
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, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": "X-Request-With, content-type, Authorization",
    },
    stats: {
      colors: true, // 打印的时候有颜色
    },
  })
);

// 引用 hotMiddleware 中间件 (实现热更新通讯)
app.use(
  hotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMP_PATH}`,
    log: () => {},
  })
);
consoler.info("请等待webpack初次构建完成提升...");
// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});