从迁移至 Rsbuild 说起,前端为什么要工程化

从升级到 Rsbuild 说起,前端项目为什么需要工程化

这段时间,我陆陆续续完成了两个旧项目的构建工具的升级(从老版本的使用 Webpack 的 Create React App 升级到基于 Rspack 的 Rsbuild)。

关于具体如何升级,后续会再单独出文进行讲解。而本文,我则希望聚焦于解答 "为什么前端项目要工程化,优秀的工程化对前端项目到底意味着什么" 这一问题。

在我看来,前端项目实现并做好工程化,能带来以下几个方面的收益:

  • 开发效率的直接提升;
  • 适应各类需求的开发;
  • 更好地适应未来的前端技术发展;

在下文中,我将对这几个方面进行详细的说明。

升级前后对比

首先对这次升级的一个后台项目的成果进行一个总览:

指标 升级前 升级后 提升幅度
模块热替换(HMR) 无,修改代码后需要手动刷新页面和恢复页面状态,单次操作约15秒 有,修改代码后直接自动更新,并恢复页面状态,0.66s 22.72倍
开发服务器启动 132s 25.7s 5.14倍
生产打包 623.4s 25.7s 24.30倍
生产打包内存占用 13.7GB,需要关闭其他软件,腾出内存,否则可能内存不足导致打包失败 2034MB,影响极小(后续升级为流水线打包后则完全无影响) 6.76倍
依赖数量 98个 69个 缩减29.59%
配置文件和其他无用文件 31个 5个 缩减83.87%
生产打包和发布 需要本地打包和手动提交,产生大量打包产物的 git 历史,严重影响代码合并 流水线打包,仓库中不存储打包产物,只有源代码 减少1253个打包产物文件

从以上指标可以非常直观地看到,从原有的 CRA 升级到 Rsbuild 后,无论是开发效率,还是项目的可维护性,都有了大幅度的提升

这就是优秀的前端工程化所带来的直接效果。

开发效率的直接提升

从上面的成果总览中就可以直观地看到,升级后,各个环节的耗时都得到了大幅度的降低。这也是前端项目工程化最直接的目的------提升开发效率

前端经过这几十年的飞速发展,诞生了各种层出不穷的框架、工具库、设计模式,其目的都是为了提升开发效率。而工程化则是支撑这些技术发挥作用的基础设施,没有工程化,这些技术也难以整合到项目中,更无法发挥作用。

更别提还有 HMR 这种颠覆前端开发效率的技术,也是工程化带来的。

接下来,我就以这次升级为例,详细说一下工程化方面的优化对开发效率到底具体提升在哪。

各开发环节加速

首先是各开发环节的加速

先看一下旧的开发流程

一个需求的前端部分的开发和上线一般分为以下几个阶段:

  1. 开发;
  2. 联调;
  3. 测试;
  4. 上线;

而每个阶段都通常需要大量重复以下这两个子流程

  • 修改代码;
  • 部署代码到对应环境。

而旧的开发流程中,这两个子流程中就存在以下几个非常低效的步骤,根据是否可优化去掉和是否可提升速度,可以分为以下两类:

  • 可优化去掉的步骤:

    • 手动刷新页面;
    • 恢复页面状态;
    • 关闭其他软件,腾出内存;
    • 提交打包产物。
  • 可提升速度的步骤:

    • 修改代码后等待模块重新打包完成;
    • 生产打包;

通过将构建工具从 CRA 升级到 Rsbuild,几乎不进行额外配置的情况下,就可以将整个开发流程优化为以下这样子:

如果熟悉各类脚手架,其实会发现这里的 HMR 其实 Webpack 就支持,各种基于 Webpack 的脚手架也都默认开启,并不一定需要通过升级打包工具来实现。

而流水线打包就更是完全和打包工具无关的,只是把本地打包流程迁移到了线上。

这两个优化是否可以做到在升级打包工具之前就实现呢?

我的回答是,可以。但是原先项目的各种打包配置过于复杂 ,导致难以梳理出 HMR 被关闭的原因,以及开启的方法。同理,原先的打包时间过长、内存占用过高,也需要深度优化后才能将打包过程迁移到流水线上。

综合下来,基于原有技术栈进行优化的开发成本过高,不如直接推倒重来、升级打包工具来得简单。

降低工程化自身的开发和维护成本

从前一节末尾的分析中就可以知道,如果一个项目的工程化一开始设计得不好,那么后续的开发成本和维护成本都是非常高的。

原因无外乎以下两点:

  • 原打包工具、脚手架设计不够现代化,在长期开发中非常容易就导致项目工程化的配置变得过于复杂,无法维护;
  • 原打包工具本身就存在性能瓶颈,为了提升热更新、打包速度,需要付出大量的开发成本;

而这次升级的 Rsbuild 正是为了解决这两个问题而设计的。

首先,它可以开箱即用 ,内置并默认开启了语法降级、分包优化等各种功能,像是对 React 、Less 等框架的支持也只需要简单引入对应的插件即可,几乎不用进行额外配置。

同时,它有原生中文文档,可以非常方便地进行查阅和学习,各种配置介绍也非常详细,学习成本极低。

当然,更重要的是,因为底层基于 Rspack 开发(Rspack 是基于 Rust 开发的,底层语言层面上就远超 JS),它在性能上相比各种基于 Webpack 的脚手架有着巨大的优势

对各类需求的适应性

正如上文所说,这次升级的 Rsbuild 的设计更为现代化,同时其几乎兼容原 Webpack 生态,所以不管是重新开发一个 Rsbuild 插件,还是找到一个已有的插件来实现相关功能,都要更简单。

而且,由于使用 Rust 开发,未来接口稳定后,甚至可以直接使用 Rust 语言来开发插件,进一步提升插件性能。

也正是依托于 Rsbuild 这样的打包工具,很多现代前端技术理念才得以整合应用到项目中。比如现在习以为常的 语法降级、分包优化、各种框架的支持、CSS 预处理器 都是前端工程化的产物。

通过这次升级为 Rsbuild,未来还可以尝试和扩展以下功能:

  • unplugin-auto-import:实现各种框架和库的 API 的自动导入,无须手动导入;
  • 模块联邦:现代前端模块共享方案,降低项目复杂度,提升应用性能;
  • Rstest:Rspack 生态下的的测试框架,更优秀的性能和更丰富的功能; ......

示例:项目埋点插件

如果只是从社区中找插件,那显然不可能覆盖所有的项目需求,我这里也提供了一个示例,说明如何通过前端工程化解决一些个性化的需求。

项目地址:github.com/RJiazhen/rs...

CodeSandbox地址:codesandbox.io/p/github/RJ...

这是一个简单的 React 项目,只有一个示例页面,其滚动到底部有一个广告区域。

tsx 复制代码
const OriginalPage = () => {
  const [showBanner, setShowBanner] = useState(true);

  /** 点击广告回调 */
  const handleBannerClick = () => {
    alert('点击了广告');
  };

  /** 点击关闭按钮回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>原始页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          onClick={handleBannerClick}
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

现在,计划接入一个埋点服务,服务供应商提供了一个 js 文件,其作用是挂载两个埋点方法到全局,调用这两个方法即可发送埋点:

javascript 复制代码
window.tracking = {
  show: (name) => {
    console.log(`[Tracking] Show: ${name}`);
  },
  click: (name) => {
    console.log(`[Tracking] Click: ${name}`);
  },
};

如果按照常规的开发方式,就是直接在源代码中编写相关的调用方法逻辑:

tsx 复制代码
import 'http://localhost:3000/tracking.js'; // 引入埋点脚本

const TrackedPage = () => {
  const [showBanner, setShowBanner] = useState(true);
  const bannerRef = useRef<HTMLDivElement>(null);

  // 使用 IntersectionObserver 监听广告元素的可见性
  useEffect(() => {
    if (!showBanner || !bannerRef.current) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          window?.tracking?.show('ad'); // 发送显示埋点
          observer.disconnect();
        }
      },
      { threshold: 0.1 }, // 当广告元素可见度达到 10% 时触发
    );

    observer.observe(bannerRef.current);

    return () => {
      observer.disconnect();
    };
  }, [showBanner]);

  /** 点击广告回调 */
  const handleBannerClick = () => {
    window?.tracking?.click('ad'); // 发送点击埋点
    alert('点击了广告');
  };

  /** 点击关闭按钮回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    // 发送点击埋点
    if (window.tracking) {
      window.tracking.click('close-ad');
    }
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>埋点页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          ref={bannerRef}
          onClick={handleBannerClick}
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

可以看出来,这样加入埋点逻辑的维护成本非常大,埋点逻辑本身是和已有的业务逻辑无关的,但是相关的代码却和业务逻辑耦合在一起

即使进行二次封装,或者编写一个专门的高阶组件,其埋点代码都显然是对已有的业务逻辑代码的侵入

那有没有办法像 Vue 的自定义指令一样,只简单地添加一个属性,就实现埋点逻辑呢?

答案是可以的,只需要编写一个解析 JSX 节点的插件,然后在解析过程中,如果节点上存在相关的属性,就自动添加相关的埋点逻辑。

实现了该功能后,只需要在组件中添加一个属性,就可以实现埋点逻辑:

tsx 复制代码
const AutoTrackedPage = () => {
  const [showBanner, setShowBanner] = useState(true);

  /** 点击关闭按钮回调 */
  const handleBannerClick = () => {
    alert('点击了广告');
  };

  /** 点击广告回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>自动埋点页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          onClick={handleBannerClick}
          data-track-show="ad" // 添加显示埋点属性
          data-track-click="ad" // 添加点击埋点属性
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            data-track-click="close-ad" // 添加点击埋点属性
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

具体的插件实现代码,请查看 Github 仓库文件,这里也说明一下,示例代码非常简陋,仅作为示例,请勿直接用在生产环境

这个插件在实现自动添加埋点逻辑的同时,还支持了以下的特性:

  • 动态引入埋点文件,从而减少无埋点页面的加载资源;
  • 生成整个应用的埋点信息
  • 检查埋点是否重复,自动给出警告;

同时,还可以继续扩展以下功能:

  • 支持模拟挂载埋点文件,减少开发时对外部环境的依赖;
  • 在打包时,使用无头浏览器进行埋点测试,自动生成埋点标记示意图;

而这些都是基于打包工具提供的 API 实现的,也由于遵循了打包工具的插件规范,所以是可插拔的,可以非常方便地移除、替换和独立使用。

typescript 复制代码
export default defineConfig({
  plugins: [
    pluginReact(),
    // 不再需要自动添加埋点逻辑时,只需要移除这个插件就可以了
    pluginAutoTracking({
      outputTransformedFiles: true,
    }),
  ],
});

同时,也是基于 Rsbuild 自身的特性,甚至可以考虑使用 Rust 来重新编写成 SWC 插件(示例中的是使用 TS 编写的 Babel 插件),从而进一步提升插件的性能。

从这里也可以看到,该插件的实现是高度依赖于打包工具的,所以打包工具本身的上下限同时也决定了插件的上下限


总结地来说,当前前端工程化使用的各种打包工具,由于其确实给前端项目开发带来了巨大的效率提升,得到整个前端社区的广泛认可,所以发展到今日,依托于其的整个插件生态也非常繁荣,通过这些社区提供的插件,可以非常方便地实现各种需求。

而就算不使用已有的插件,因为现代打包工具提供了非常丰富的 API,自己开发插件也非常方便。

基于这一点,现代前端项目的工程化开发,基于这些打包工具可以说是必选项。

但同时,各类打包工具也决定了工程化的上限,轻则性能不好,影响开发效率,重则 API 不完善,连功能都无法实现。

比如最直接的性能问题,其直接影响到开发效率。过去的 Webpack 、Rollup 等打包工具都是基于 JS 开发的,受限于 JS 本身的特性和 JS 运行时的性能,在面对大型项目时,其性能已经捉襟见肘了。自身的性能不行,基于其开发的插件的性能自然也很难好到哪去。

这也是为什么现代的前端打包工具(Rspack,Rolldown等)都转向使用 Rust 来开发,也是为了给性能兜底。

所以,一个前端项目的迭代和维护不能只是业务功能方面的,也需要考虑到工程化方面的改进。对打包工具等工程化方面的优化,也是项目维护的重要一环。

前端技术的未来发展

基于构建工具的生态建设

当前前端发展的大趋势是以打包工具为核心,构建一整套完整的工具链。过去那种 webpack 官方基本只维护自己这一个工具,其他功能基本全靠社区各自维护的时代一去不复返了。

因为很多效率的提升需要深度介入底层的语言解析过程,如果打包工具不整合好解析工具,那很多想法也都只是空中楼阁。

Void Zero 的生态布局

而之前 Vite 团队专门成立的 VoidZero 公司,也是把统一整个前端工具链作为目标,从底层的语法解析器到打包工具、构建工具,甚至测试框架都由其亲自己进行开发和管理。

以 Rspack 为核心的官方生态系统

而这次升级的 Rsbuild 也同样如此,它就是作为 Rspack 官方生态系统的一部分诞生的,而官方生态系统中还包含静态博客框架 Rspress、测试框架 Rstest、包开发工具 Rslib 等。

那么为什么会呈现这样的趋势呢?

因为构建工具随着这几年前端的发展,已经不只是一个简单的前端文件的转换工具,而是前端工程的中枢管线。

构建工具也因为渗入到了前端项目开发的方方面面,自然可以从整个项目中解析出足够多的信息,这就使得各种工具自然而然地愿意依附于构建工具提供的 API 进行开发,以便更高效地消费这些内容。

基于构建工具的外围生态

正如前面所说,构建工具贯穿并整合整个前端项目,在一过程中会获取和产生非常多有价值的项目相关数据。

在使用这些数据的基础上,如果构建工具提供了相关的 API,那就不只可以做语法降级、分包优化这种直接影响最终生产打包产物的事情,还可以实现很多开发相关的功能,虽然对最后的打包产物没有影响,但是可以极大地提升开发效率。

例如现代前端开发中必备的 HMR 就是利用了构建工具解析得到的模块依赖关系 ,以及使用构建工具动态植入热更新逻辑

在过去 Webpack 这类构建工具,基本上只是提供相关的接口,而没有专门地进行设计和适配。

而近几年,基于过去开发配套的浏览器插件、VScode插件的经验,Vue 团队已经开始布局基于 Vite 为核心的开发者工具的生态

Nuxt DevTools

具体说明可以查看 Anthony Fu 在 ViteConf 2025 上的分享------Vite DevTools 前瞻介绍(幻灯片链接)。

相信如果接触过 Vue 和 Nuxt 开发,一定能感受过这种配套的外围开发工具对开发效率的提升。相信这也是未来构建工具的发展趋势。

AI 时代的构建工具

本质上来说,浏览器插件这类开发工具,就是把源码和框架运行时内部的信息进行整合转换,然后提供给开发者查看、阅读,并且通过图形界面提供对应的修改功能。

那么同样的,不仅开发人员可以阅读,AI 也可以阅读,并进行辅助开发。

像是现在已有的方案,比如 Google 提供的Chrome 开发者工具 MCP就是让 AI 直接阅读浏览器提供的信息。

但是直接阅读当前网页的 dom 结构和存储的变量,有几个问题,一是无效的信息太多 (比如只是使用列表渲染生成的 dom 结构,AI 需要就只是源代码和用来遍历的数据),二是这些信息不好映射到源代码中,不好作为 AI 自动修改源代码的依据。

所以,作为整个开发环节中"最了解项目"的构建工具,理论上是可以想办法提供这些信息的,从而实现 AI 更高效地辅助开发。

比如 Dart 团队提供的MCP 服务器,就可以让 AI 帮忙分析当前的布局问题,并进行修复

以此为出发,未来的前端构建构建工具会集成相关的特性,至少会出现相关的插件来实现这类功能。

总结

最后总结一下,做好前端工程化,并且持续迭代优化,已经不是可选项,而是必选项了。并不是说,最多项目热更新慢点,打包慢点,但只要开发没问题,就万事大吉了。

未来,如果是一个做得足够好的前端工程化的前端项目,在开发效率上,可以甩开其他项目一大截。甚至在 AI 快速发展的今天,还能有更多的想象空间。

而作为前端开发人员,同样也应该多关注这些底层的基建,并且沉淀出自己的一套关于构建工具在技术选型、管理维护和迭代的方法论,让自己的项目能够更好地吃到这些基建的红利。

相关推荐
小红3 小时前
网络通信核心协议详解:从ARP到TCP三次握手与四次挥手
前端·神经网络
影子信息4 小时前
uniapp 日历组件 uni-datetime-picker
前端·uni-app
俞凡4 小时前
UUID 替代方案详解
架构
D.eL4 小时前
深入解析 Redis 单线程 IO 模型:从架构到多路复用技术
数据库·redis·架构
向下的大树5 小时前
npm 最新镜像,命令导致下载错误
前端·npm·node.js
宁雨桥5 小时前
Service Worker:前端离线化与性能优化的核心技术
前端·性能优化
IT_陈寒5 小时前
SpringBoot实战:这5个隐藏技巧让我开发效率提升200%,90%的人都不知道!
前端·人工智能·后端
ObjectX前端实验室5 小时前
【图形编辑器架构】节点树与渲染树的双向绑定原理
前端·计算机图形学·图形学
excel5 小时前
Vue2 与 Vue3 生命周期详解与对比
前端