对前端工程化的理解

对前端工程化的理解

本文章仅个人对前端工程化的理解,仅供参考。

一、工程化衍生的背景

早期的前端开发更多是页面级开发:写 HTML 组织结构,写 CSS 处理样式,写 JavaScript 完成交互。页面数量少、业务逻辑简单时,这种开发方式足够直接,成本也不高。

但随着前端承担的职责越来越多,前端已经不只是"切页面"和"写交互"。现代前端通常需要处理路由、状态管理、接口请求、权限控制、组件复用、构建打包、代码分割、性能优化、环境配置、质量检测、自动化部署等一系列问题。前端项目逐渐从页面集合演变成一个完整的软件系统。

当项目复杂度提升后,单纯依赖个人习惯和手工维护会出现很多问题:

  1. 代码组织混乱,不同开发者的目录结构、命名方式和实现风格不一致。
  2. 重复代码增多,相似页面、相似组件、相似请求逻辑被反复编写。
  3. 项目构建依赖人工操作,开发、测试、生产环境之间容易出现配置差异。
  4. 代码质量缺少统一约束,问题往往要到运行时或者上线后才暴露。
  5. 多人协作成本上升,改动影响范围不清晰,维护成本越来越高。
  6. 发布流程不可控,依赖手工打包、手工上传、手工验证,容易出现遗漏。

因此,前端工程化的出现,本质上是为了解决前端项目在规模化、复杂化、多人协作场景下的可维护性和可控性问题。它不是单纯引入某个构建工具,也不是只配置一套脚手架,而是把前端开发从"手工作坊式开发"推进到"标准化、自动化、可持续维护的软件工程开发"。

二、如何进行工程化

1. 整体项目目录结构约定

一个项目,需要约定好以下规则:

  • 公共组件存放目录
  • 公共工具函数存放目录
  • 页面存放目录
  • 单个页面内,主页面以及子组件,类型文件,页面公共方法文件,hook文件,api文件存放在哪里,怎么命名
  • 路由全部采用懒加载配置
  • ... 以上都需要形成统一约定,从整体的目录规划上做好可维护性,实现开发者快速熟悉上手项目以及查找代码

2. 代码质量以及提交规范约束

在多人协作中,如果没有统一规范,每个人都会按照自己的习惯写代码。短期看问题不大,长期看会导致代码风格混乱、维护成本上升。

可以通过 ESLint、Prettier、Stylelint、TypeScript、Husky、Commitlint、lint-staged 等方式,把代码规范和质量检查前置到开发阶段。用工具把规范固化下来。

3. 沉淀通用能力

  • 把常见的业务沉淀成组件存放在全局
  • 把常用的工具函数沉淀成工具函数存放在全局
  • 统一封装好Http请求的工具方法

以上行为可以减少重复劳动性,这样后续开发同类需求时,不再从零开始写,而是基于已有能力组合和扩展。

4. 多环境配置管理

前端项目通常会区分本地、测试、预发、生产等环境。不同环境下接口地址、资源路径、埋点配置都可能不同。如果这些配置散落在代码中,极容易出现打错包、连错接口、发布错误环境的问题。

工程化会把环境变量和配置管理纳入统一体系,让不同环境使用不同配置,并且通过构建流程明确区分。例如:

text 复制代码
development -> 本地开发配置
test        -> 测试环境配置
production  -> 生产环境配置

这样可以让环境差异变得明确,而不是隐藏在业务代码里。

5. 构建打包配置(核心)

构建打包思路: 通过任意一个工具,从指定入口开始分析依赖,利用特定工具编译特定代码文件,将输出的产物落地磁盘,最终得到一个可发布的HTML/CSS/JS等文件资源。本次使用webpack作为构建工具举例

5.1 基础配置

作用:根据约定大于配置,动态加载多页面打包入口,根据不同类型的文件配置不同的编译loader转译,将编译后的文件根据各种分包配置进行拆分,最终输出到指定目录。

js 复制代码
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const webpack = require("webpack");
const glob = require("glob");
const Components = require("unplugin-vue-components/webpack");
const { AntDesignVueResolver } = require("unplugin-vue-components/resolvers");

const filePath = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
const templatePath = path.resolve(process.cwd(), "./app/view/entry.tpl");
const pageEntries = {};
const htmlWebpackPluginList = [];

/**
 * 定义页面输出的位置
 * @param {string} entryName
 * @returns
 */
const outputPage = (entryName) => {
  return path.resolve(process.cwd(), "./app/public/dist", `${entryName}.tpl`);
};

// 在约定大于配置的前提下,从指定目录中获取多页面的入口js
const entryList = glob.sync(filePath);

// 从fileList中动态生成entry入口和页面文件的生成插件配置
entryList.forEach((file) => {
  const entryName = path.basename(file, ".js");
  pageEntries[entryName] = file;
  // 对每一个入口都构造一个页面文件的生成,推进htmlWebpackPluginList数组中
  const item = new HtmlWebpackPlugin({
    template: templatePath,
    filename: outputPage(entryName),
    chunks: [entryName],
  });
  htmlWebpackPluginList.push(item);
});

module.exports = {
  // 入口配置
  entry: pageEntries,
  // 配置模块解析的方式,决定了要用什么方式去解析模块
  module: {
    rules: [
      // 对js文件配置解析
      {
        test: /\.js$/i,
        include: [path.resolve(process.cwd(), "./app/pages")],
        use: "babel-loader",
      },
      // 对css文件配置解析
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
      // 对less文件解析
      {
        test: /\.less$/i,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      // 对vue文件进行解析
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
      // 对图片解析
      {
        test: /\.(png|jpe?g|gif|webp)$/i,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: "static/images/[name].[contenthash:8][ext]",
        },
      },
      // 对字体图标进行解析
      {
        test: /\.(woff|woff2|eot|ttf|otf|svg)$/i,
        type: "asset/resource",
        generator: {
          filename: "static/fonts/[name].[contenthash:8][ext]",
        },
      },
    ],
  },
  // 输出配置
  output: {},
  // 配置模块解析的具体行为
  resolve: {
    // 当遇到没后缀的文件引入时,会自动添加后缀
    extensions: [".js", ".vue", ".less", ".css"],
    // 路径别名
    alias: {
      $page: path.resolve(process.cwd(), "./app/pages"),
      $common: path.resolve(process.cwd(), "./app/pages/common"),
      $service: path.resolve(process.cwd(), "./app/pages/service"),
      $widgets: path.resolve(process.cwd(), "./app/pages/widgets"),
      $store: path.resolve(process.cwd(), "./app/pages/store"),
    },
  },
  // 优化配置
  optimization: {
    // 将webpack运行时代码抽离成单独的chunk
    runtimeChunk: "single",
    splitChunks: {
      chunks: "all", // 同步或者异步模块
      maxAsyncRequests: 10, // 最大异步加载并行请求数
      maxInitialRequests: 10, // 入口点最大加载并行请求数
      cacheGroups: {
        // 缓存组,将第三方库单独打包
        vendor: {
          test: /[\\/]node_modules[\\/]/, // 匹配node_modules中的模块
          name: "vendor", // chunk名称
          priority: 20, // 优先级
        },
        // 缓存组,将超过2个地方引用的单独打包
        common: {
          name: "common",
          priority: 10,
          minChunks: 2, // 最小引用次数
          minSize: 1, // 最小大小 为了试验效果改成1kb
          reuseExistingChunk: true,
        },
      },
    },
  },
  plugins: [
    /**
     * 处理.vue文件,这个插件是必须的
     * 将定义过的解析规则应用到vue文件中
     */
    new VueLoaderPlugin(),
    // 把第三方库暴露在window 上下文中
    new webpack.ProvidePlugin({
      Vue: "vue",
    }),
    // 定义全局常量
    new webpack.DefinePlugin({
      // 默认兼容vue3语法,纯vue2语法可以选择不开这个全局控制变量,减小包体积
      __VUE_OPTIONS_API__: "true",
      // 是否开启 Devtools 调试
      __VUE_PROD_DEVTOOLS__: "false",
      // SSR 场景下,是否输出水合不匹配详细日志
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
      "process.env.APP_ENV": JSON.stringify(process.env.NODE_ENV),
    }),
    Components({
      resolvers: [
        AntDesignVueResolver({
          importStyle: false, // css由mini-css单独抽离,不内联
          resolveIcons: true, // 图标也按需引入
        }),
      ],
    }),
    // 创建页面文件,自动注入chunk
    ...htmlWebpackPluginList,
  ],
};
5.2 开发环境

在开发环境里,可以基于《基础配置 + 热更新》技术实现修改代码后,快速本地预览效果,提高开发效率

5.2.1 热更新原理

热更新原理:

  • 1.在本地客户端中起一个node的服务器,当服务器启动的时候,将webpack配置文件传入complie中编译(编译产物时,会在产物中注入一段sse或者webstock的代码以及热更新的api实现),编译的产物会落地到内存里,减少IO操作。
  • 2.webpack编译完成后会调用done的广播,订阅了done钩子的函数会向浏览器发送一个消息,告诉浏览器,有新的文件更新了,请刷新页面。
  • 3.浏览器收到消息后,会向服务器请求新的更新清单,根据清单内容请求资源文件重新执行。
  • 4.当文件系统发生变化后会重复做上面的动作
js 复制代码
const express = require("express");
const app = express();
const { webapckDevConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpack = require("webpack");
const webpackHotMiddleware = require("webpack-hot-middleware");
const compiler = webpack(webapckDevConfig);
const cors = require("cors");

// 跨域中间件
app.use(cors());

// 负责监听文件系统的改动,根据webpack配置重新编译,将tpl文件落地到磁盘,其他文件落地到内存中
// 配置publicPath,可以拦截对应的文件请求,从内存中返回出去
app.use(
  webpackDevMiddleware(compiler, {
    writeToDisk: (filePath) => /.tpl$/.test(filePath),
    publicPath: webapckDevConfig.output.publicPath,
  }),
);

// webpack编译完成,webpack-hot-middleware中间件会监听到done钩子,通过sse或者webstock通知客户端,重新请求资源清单进行热更新
app.use(
  webpackHotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
  }),
);

// 服务启动
app.listen(DEV_SERVER_CONFIG.PORT, DEV_SERVER_CONFIG.HOST, () => {
  console.log(`listening on http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}`);
});
5.3 生产环境

对于落地生产的包,需要尽可能的减少单个资源的体积,减少初始资源的加载数量,尽可能利用浏览器的并发请求,缓存等优化手段,提高用户体验与访问速度

  1. 静态资源优化:如果资源很小的话,可以直接打进chunk里,如果体积过大,可以直接打成单独的资源文件
  2. 所有资源文件都要使用哈希,利用浏览器的缓存机制,可以有效避免重复资源请求
  3. 对于css资源或者js资源需要做以下优化
  • 将公共部分提取成单独的chunk
  • 进行多线程压缩(提高构建速度以及降低体积)
  • 去掉无用的注释
  • 执行树摇
  1. 给资源文件添加crossorigin="anonymous",避免跨源资源请求携带用户凭证
  2. 关闭 sourcemap,避免生产环境暴露源码、源码路径和调试信息
  3. 抽离构建工具注入的运行时代码
  4. 根据框架提供的全局常量,控制开关,可以影响到框架编译时的代码判断,提高代码运行速度
  5. 抽离第三方库
js 复制代码
const webpackConfig = require("./webpack.base");
const { mergeWithRules } = require("webpack-merge");
const path = require("path");
const os = require("os");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizer = require("css-minimizer-webpack-plugin");
const HtmlWebpackInjectAttributesPlugin = require("html-webpack-inject-attributes-plugin");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = mergeWithRules({
  module: {
    rules: {
      test: "match",
      use: "replace",
    },
  },
})(webpackConfig, {
  mode: "production",
  devtool: false,
  output: {
    // 产物输出的目标目录
    path: path.resolve(process.cwd(), "./app/public/dist/prod"),
    // 打包的js文件的命名
    filename: "js/[name].[contenthash:8].bundle.js",
    publicPath: "/dist/prod/",
    // 异步模块的命名
    chunkFilename: "js/async.[name].[contenthash:8].chunk.js",
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "thread-loader",
            options: {
              workers: os.cpus().length,
            },
          },

          "css-loader",
        ],
      },
      {
        test: /\.less$/i,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "thread-loader",
            options: {
              workers: os.cpus().length,
            },
          },
          "css-loader",
          "less-loader",
        ],
      },
      {
        test: /\.js$/i,
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: os.cpus().length,
            },
          },
          "babel-loader",
        ],
      },
    ],
  },
  plugins: [
    // 抽离css成独立文件
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:8].css",
      chunkFilename: "css/async.[name].[contenthash:8].css",
    }),
    new HtmlWebpackInjectAttributesPlugin({
      crossorigin: "anonymous",
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      // 压缩js
      new TerserPlugin({
        // 多进程压缩
        parallel: true,
        // 提取注释到单独文件(生产关闭)
        extractComments: false,
      }),
      new CssMinimizer({
        parallel: true,
      }),
    ],
  },
});

6. CI/CD部署

前端项目从开发到上线会经历很多重复流程,例如安装依赖、启动本地服务、编译代码、压缩资源、生成产物、上传部署等。如果这些流程都依赖人工操作,就容易产生环境差异和操作失误。

工程化通过脚本、构建工具和 CI/CD 流程,把这些重复动作自动化。例如:

text 复制代码
本地开发 -> 代码检查 -> 单元测试 -> 构建打包 -> 部署发布

这样可以减少人为操作的不确定性,让每一次构建和发布都尽量可复现。

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒10 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端