优化 Vite 项目中的 Lodash 引入:从 Tree Shaking 到自动化测试

948 阅读7分钟

前言

我想在公众号查看🚀🚀🚀

在现代前端开发中,优化代码的体积和构建速度是非常重要的。特别是在使用 Vite 作为构建工具时,如何正确地处理 Lodash 这种通用的工具库,变得尤为关键。在本文中,我将分享我们如何在 Vite 项目中优化 Lodash 的引入方式,并确保优化后的代码能够正常运行。

问题背景

在项目中,我们经常使用 Lodash 提供的各种实用函数。传统的方式是直接使用 import _ from 'lodash' 来引入整个 Lodash 库。然而,这种方式有几个明显的问题:

  1. 冗余模块加载:由于 import _ from 'lodash' 会加载整个库,即使我们只用了其中的几个函数,整个库的所有模块都会被加载。

  2. Tree Shaking 无效:Lodash 使用的是 CommonJS 模块系统,而 Vite 的 Tree Shaking 依赖于 ES Module(ESM)的静态分析。因此,Vite 无法有效地在构建时剔除未使用的 Lodash 模块。

解决方案概述

我们分析了几个优化 Lodash 引入方式的解决方案,并最终决定采用更适合我们项目需求的方案。

思路 1:使用 Lodash-ES 替代 Lodash

Lodash-ES 是一个 ES Module 版本的 Lodash,可以很好地与 Vite 的 Tree Shaking 配合。使用 lodash-es 可以确保未使用的模块在构建时被有效地剔除。

操作步骤

  1. 安装 Lodash-ES

    • 在项目中安装 lodash-es,以替代传统的 lodash
    npm install lodash-es
    
  2. 全局替换 import _ from 'lodash'import { functionName } from 'lodash-es'

    • 逐步将项目中的 import _ from 'lodash' 替换为按需引入的 lodash-es 版本。
    • 示例代码:
    // 原始代码
    import _ from 'lodash';
    
    const data = [1, 2, 3];
    const result = _.map(data, (item) => item * 2);
    
    // 替换后的代码
    import { map } from 'lodash-es';
    
    const data = [1, 2, 3];
    const result = map(data, (item) => item * 2);
    
  3. 批量替换工具

    • 可以使用 VSCode 的全局搜索和替换功能,或者其他代码重构工具,帮助批量替换 lodash 的导入方式。

挑战

  • 全局替换 import _ from 'lodash'import { functionName } from 'lodash-es' 风险较大,因为项目中存在大量的 Lodash 引入,替换成本和风险都较高。

解决办法

  • 可以逐步替换为按需加载的方式,例如 import { deepClone } from 'lodash-es',但这仍然需要全局修改代码,工作量较大。

思路 2:使用 Babel 插件 babel-plugin-lodash

为了在不修改现有代码的情况下实现按需加载,可以使用 babel-plugin-lodash 插件。该插件的原理是将 import _ from 'lodash' 转换为按需引入的方式,例如 import { deepClone } from 'lodash/deepClone'。这样既不需要修改现有代码,又能解决冗余模块加载的问题。

操作步骤

  1. 安装 Babel 及其相关插件

    • 你需要安装 @babel/core@babel/preset-envbabel-plugin-lodash。这些插件和预设将帮助你在不修改代码的情况下实现按需加载。
    npm install --save-dev @babel/core @babel/preset-env babel-plugin-lodash
    
  2. 配置 Vite 使用 Babel 插件

    • 在 Vite 的配置文件中(通常是 vite.config.js),你需要集成 Babel 插件。
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import babel from '@rollup/plugin-babel';
    
    export default defineConfig({
      plugins: [
        vue(),
        babel({
          babelHelpers: 'bundled',
          presets: ['@babel/preset-env'],
          plugins: ['babel-plugin-lodash'],
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
          exclude: 'node_modules/**', // 排除 node_modules 目录下的文件
        }),
      ],
    });
    

    配置解释

    • babelHelpers: 'bundled' 是指定 Babel 如何处理辅助函数的选项。bundled 选项告诉 Babel 将辅助函数包含在捆绑包中。
    • presets: ['@babel/preset-env'] 是 Babel 的预设配置,帮助处理 ES6+ 语法。
    • plugins: ['babel-plugin-lodash'] 是关键插件,负责将 Lodash 的全局引入转换为按需引入。
    • extensions: 指定需要被 Babel 处理的文件扩展名,包括 .js.jsx.ts.tsx.vue
    • exclude: 排除对 node_modules 文件夹的处理,以加快构建速度。
  3. 运行项目

    • 配置完成后,你可以像往常一样运行项目,babel-plugin-lodash 将自动处理 Lodash 的按需引入,无需手动修改代码。

挑战

  • 使用 Babel 插件会影响构建速度,构建时间明显增加。

解决办法

  • 在开发阶段保留原始的引入方式,避免构建速度下降。在生产环境中,通过 Babel 插件进行按需加载,以减小最终打包的体积。

思路 3:大规模替换后的验证与测试

经过权衡,我们决定采用思路 1,在全项目范围内逐步将 import _ from 'lodash' 替换为按需加载的方式。为了确保替换后的代码正常工作,我们采用了以下验证方法:

  1. TypeScript 静态检测

    • 开启 TypeScript 的严格模式,确保所有替换后的代码在构建时都能通过静态类型检查。这样可以在构建阶段捕获大多数潜在的代码错误。
  2. 自动化测试

    • 使用 Puppeteer 进行自动化测试,编写脚本模拟用户操作,点击项目中每个菜单项,确保页面加载正常,没有因 Lodash 替换而导致的功能异常。

Babel 的工作流程回顾

在采用思路 2 的过程中,我们再次复习了 Babel 的工作流程:

  • Parse:Babel 首先将代码解析为抽象语法树(AST)。
  • Transform:在这一步,Babel 的插件(如 babel-plugin-lodash)会根据预定义的规则对 AST 进行修改。对于 Lodash,它会将全量引入转换为按需加载。
  • Generate:最后,Babel 将修改后的 AST 生成新的代码,并将其输出到目标文件中。

虽然 babel-plugin-lodash 能很好地解决 Lodash 的引入问题,但在我们的场景下,由于构建速度的要求,我们最终选择了更具稳定性和可控性的替换方式。

最终选择及实践经验

经过权衡,我们决定使用 Lodash-ES 替换原有的 Lodash 引入方式,并通过手动逐步替换和严格的自动化测试,确保项目稳定性。虽然这个过程有一定的工作量,但它为我们带来了更快的构建速度和更小的打包体积。

自动化测试:使用 Puppeteer 验证替换后的代码

为了确保代码替换后的功能正常,我们使用 Puppeteer 编写了自动化测试脚本,模拟用户操作,自动点击页面中的每个菜单项,并检查页面是否加载正常。

Puppeteer 自动化测试代码

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // 设置页面的视窗尺寸
  await page.setViewport({ width: 1200, height: 800 });

  // 访问你的应用
  await page.goto('http://localhost:3000'); // 替换为你的实际应用 URL

  // 等待菜单加载完成
  await page.waitForSelector('ul.el-menu');

  // 获取所有菜单项的 `span` 元素
  const menuItems = await page.$$eval('ul.el-menu li.el-menu-item span', spans => spans.map(span => span.textContent.trim()));

  for (const itemText of menuItems) {
    console.log(`点击菜单项: ${itemText}`);

    // 点击菜单项
    await page.evaluate(text => {


      const elements = [...document.querySelectorAll('ul.el-menu li.el-menu-item span')];
      const targetElement = elements.find(el => el.textContent.trim() === text);
      if (targetElement) targetElement.click();
    }, itemText);

    // 等待页面加载完成(根据你的页面实际情况调整选择器)
    await new Promise(resolve => setTimeout(resolve, 1000)); // 适当等待时间,确保页面加载完成
  }

  await browser.close();
})();

结论

通过这次优化,我们不仅减小了最终的打包体积,还提升了项目的构建效率,并确保了项目的稳定性和可维护性。

在整个优化过程中,我们总结了以下几点经验:

  1. 逐步替换,分阶段实施:面对大规模的代码替换,不要一蹴而就,而是分阶段实施,并在每个阶段进行详细测试。
  2. 自动化测试的重要性:在大规模代码变更后,自动化测试能帮助我们快速验证功能的正确性,避免因手动测试不彻底而导致的上线问题。
  3. 工具链的理解与合理使用:深刻理解工具(如 Babel)的工作原理,有助于我们在做技术选型时做出更加合适的决定。

通过这次优化,我们不仅减小了最终的打包体积,还提升了项目的构建效率,并确保了项目的稳定性和可维护性。在未来的项目中,我们也会继续关注如何通过工具链的优化来提升开发效率和代码质量。