rollup进阶——为产物添加全局try-catch

背景

笔者参与开发的前端性能监控平台使用了OpenTelemetry作为监控日志的标准协议。而在使用过程中发现该协议提供的库中使用了较新的 ES6 特性,在低版本浏览器中不支持,触发了ReferenceError。从而中断了主线程,引发严重的页面白屏问题。

尝试解决

模拟问题

经过排查,发现是引用的OpenTelemetry里使用了较新的 ES6特性,该特性在版本较老的浏览器中不支持。为了复现问题,在sdk中创建一个不存在的类实例:

js 复制代码
// mock error
const mockError = new NotExistFeature();

class WebPerformanceSDK {

  constructor(callback: ReportCallback, config: SDKConfig = {}) {
    this.reportCallback = callback;
    this.config = config;
  }
  
  public init(): void {
    try {
      ...
    } catch (error) {
      ...
    }
  }
}

效果如下:

代码报了 ReferenceError 异常,并且中断了后续脚本执行,引发了页面白屏。

init时加try-catch

既然是sdk初始化引发的问题,自然而然想到的是在应用系统中给 sdk添加 try-catch防止异常影响到应用代码的执行。
文中的代码均为脱敏后的demo代码

js 复制代码
import { useState, useEffect, useRef } from "react";
import "./App.css";
import WebPerformanceSDK from "../../dist/index.esm.js";

  ...
  const init = useRef<boolean>(false);
  
  useEffect(() => {
    if (init.current) {
      return;
    }
    init.current = true;
    try {
      const sdk = new WebPerformanceSDK(
        (metric: { name: string; value: number }) => {
          setMetrics((prev) => [
            ...prev,
            { name: metric.name, value: metric.value },
          ]);
        },
        { debug: true }
      );
      sdk.init();
    } catch (e: any) {
      console.error(e.message);
    }
  }, []);
  
  ...

重新构建后再运行项目...依然白屏!

通过打印日志发现代码压根没有执行到 init(),在import WebPerformanceSDK from "../../dist/index.esm.js 时就已经出现问题了:
为了证明是 import 代码的问题,使用 import() 动态导入catch中间发生的异常

js 复制代码
import("../../dist/index.esm.js")
  .then((sdk) => {
    console.log("sdk", sdk);
  })
  .catch((e: any) => {
    console.error("import() error: ", e.message);
  });

异常日志:

问下 deepseek import 一个模块时会发生什么:

  • 加载 index.esm.js 后,会 立即执行其顶层代码(如变量初始化、函数调用)。
  • 若该文件包含 export 语句,导出的内容会绑定到模块命名空间,供外部调用。例如,若 index.js 导出 export default function() {},导入方可通过 import func from './index.js' 获取该函数。

好吧,怪不得在init方法里加try-catch没用,原来异常在import的时候就已经发生了。

对于应用侧来讲,上面例子里用到import()就可以作为临时解决方案,在import().then(...)中完成sdk的初始化。

但是对于sdk方来肯定不能偷懒说建议大家用动态加载的方式,那怎么办呢?

这里也不卖关子了,因为想让接入方尽可能简单而安全地使用sdk,所以需要在sdk内部添加全局的try-catch,提供代码的健壮性。

使用rollup添加try-catch

为产物添加try-catch还是得借助构建器的能力,我们的sdk使用的是rollup作为构建工具,rollup提供了输出插件选项,支持开发者自定义plugin去修改产物内容。

既然官方提供了这样一个入口,肯定有相应的plugin,所以先去社区寻找相应的rollup插件。找了一圈后还真的找到了一个npm包: @rollup/plugin-babel

@rollup/plugin-babel exposes a plugin-builder utility that allows users to add custom handling of Babel's configuration for each file that it processes.

createBabelInputPluginFactory accepts a callback that will be called with the loader's instance of babel so that tooling can ensure that it using exactly the same @babel/core instance as the loader itself.

It's main purpose is to allow other tools for configuration of transpilation without forcing people to add extra configuration but still allow for using their own babelrc / babel config files.

官方文档里只提供了 createBabelInputPluginFactory 方法的说明,但是这个包还提供了 createBabelOutputPluginFactory 方法,这个 Output 方法的用法跟 Input 是完全一样的。

createBabelOutputPluginFactory添加try-catch

为了添加全局try-catch,需要把sdk的初始化在内部提前完成,也就是说需要创建一个 IIFE,让sdk代码在IIFE中执行,然后return待导出的WebPerformanceSDK类。

export改为 return

为什么要做这样的修改?

上文说到我们将生成的code用IIFE包裹了起来,让其成为一个函数执行。因此需要在拿到IIFE的结果后,再自主export,导出真正的class。

原产物code:

js 复制代码
var WebPerformanceSDK = /** @class */ (function () {
    function WebPerformanceSDK(callback, config) {
        if (config === void 0) { config = {}; }
        this.reportCallback = callback;
        this.config = config;
    }
    WebPerformanceSDK.prototype.handleError = function (error, context) {
       ...
    };
    WebPerformanceSDK.prototype.safeCallback = function (metric) {
        ...
    };
    WebPerformanceSDK.prototype.init = function () {
        try {
            this.collectWebVitals();
            this.collectResourceTiming();
            this.collectNavigationTiming();
        }
        catch (error) {
            this.handleError(error, "init");
        }
    };
    WebPerformanceSDK.prototype.collectWebVitals = function () {
        ...
    };
    WebPerformanceSDK.prototype.collectResourceTiming = function () {
        ...
    };
    WebPerformanceSDK.prototype.collectNavigationTiming = function () {
        ...
        }
        catch (error) {
            this.handleError(error, "collectNavigationTiming");
        }
    };
    return WebPerformanceSDK;
}());

export { WebPerformanceSDK as default };

可以发现其导出语句为 export { WebPerformanceSDK as default }。可以直接采用字符串替换的方式将其改为 return WebPerformanceSDK

在catch中mock WebPerformanceSDK

要完整的try-catch还需要在catch中mock 出一个空的 WebPerformanceSDK,否则当导入sdk时,会出现 TypeError: WebPerformanceSDK is not a constructor 异常。mock代码其实就是在初步产物的基础上裁剪掉业务代码,只保留一个个空的Function。

js 复制代码
...
} catch (e) {
  console.warn('SDK 初始化异常,已启动mock逻辑', e.message);// 打印警告提示
  var WebPerformanceSDK = (function () {
    function WebPerformanceSDK(callback, config) {
      if (config === void 0) {
        config = {};
      }
      this.reportCallback = callback;
      this.config = config;
    }
    WebPerformanceSDK.prototype.handleError = function (error, context) {
      if (this.config.debug) {
        console.error(
          "[WebPerformanceSDK] Error in ".concat(context, ":"),
          error
        );
      }
    };
    WebPerformanceSDK.prototype.safeCallback = function (metric) {
      try {
        this.reportCallback(metric);
      } catch (error) {
        this.handleError(error, "reportCallback");
      }
    };
    WebPerformanceSDK.prototype.init = function () {
      try {
        this.collectWebVitals();
        this.collectResourceTiming();
        this.collectNavigationTiming();
      } catch (error) {
        this.handleError(error, "init");
      }
    };
    WebPerformanceSDK.prototype.collectWebVitals = function () {};
    WebPerformanceSDK.prototype.collectResourceTiming = function () {};
    WebPerformanceSDK.prototype.collectNavigationTiming = function () {};
    return WebPerformanceSDK;
  })();
  return WebPerformanceSDK;
}

plugin完整实现

这里贴出plugin的完整实现:

js 复制代码
// rollup-try-catch-babel.mjs
import { createBabelOutputPluginFactory } from "@rollup/plugin-babel";

// @desc: https://github.com/rollup/plugins/tree/master/packages/babel
export const babelOutputPluginWithESM = createBabelOutputPluginFactory(() => {
  function customPlugin() {
    return {
      visitor: {},
    };
  }

  // 添加try-catch,防止sdk初始化异常影响主线程
  function addTryCatch(code) {
    code = code.replace(
      "export { WebPerformanceSDK as default }",
      "return WebPerformanceSDK"
    );
 return `const result = (function () {
  try {
    ${code}
  } catch (e) {
    console.warn('SDK 初始化异常,已启动mock逻辑', e.message);// 打印警告提示
    var WebPerformanceSDK = (function () {
      function WebPerformanceSDK(callback, config) {
        if (config === void 0) {
          config = {};
        }
        this.reportCallback = callback;
        this.config = config;
      }
      WebPerformanceSDK.prototype.handleError = function (error, context) {
        if (this.config.debug) {
          console.error(
            "[WebPerformanceSDK] Error in ".concat(context, ":"),
            error
          );
        }
      };
      WebPerformanceSDK.prototype.safeCallback = function (metric) {
        try {
          this.reportCallback(metric);
        } catch (error) {
          this.handleError(error, "reportCallback");
        }
      };
      WebPerformanceSDK.prototype.init = function () {
        try {
          this.collectWebVitals();
          this.collectResourceTiming();
          this.collectNavigationTiming();
        } catch (error) {
          this.handleError(error, "init");
        }
      };
      WebPerformanceSDK.prototype.collectWebVitals = function () {};
      WebPerformanceSDK.prototype.collectResourceTiming = function () {};
      WebPerformanceSDK.prototype.collectNavigationTiming = function () {};
      return WebPerformanceSDK;
    })();

    return WebPerformanceSDK;
  }
})();

export { result as default };
`;
}

  return {
    // Passed the plugin options.
    options({ opt1, opt2, ...pluginOptions }) {
      return {
        // Pull out any custom options that the plugin might have.
        customOptions: { opt1, opt2 },

        // Pass the options back with the two custom options removed.
        pluginOptions,
      };
    },

    config(
      cfg /* Passed Babel's 'PartialConfig' object. */,
      { code, customOptions }
    ) {
      if (cfg.hasFilesystemConfig()) {
        // Use the normal config
        return cfg.options;
      }

      return {
        ...cfg.options,
        plugins: [
          ...(cfg.options.plugins || []),
          // Include a custom plugin in the options.
          customPlugin,
        ],
      };
    },

    result(result, { code, customOptions, config, transformOptions }) {
      return {
        ...result,
        // 使用 addTryCatch 方法为code添加全局的try-catch
        code: addTryCatch(result.code),
      };
    },
  };
});

使用plugin

使用起来也很简单,在rollup.config.mjs里配置一下就可以:

js 复制代码
import typescript from "@rollup/plugin-typescript";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { babelOutputPluginWithESM } from "./plugins/rollup-try-catch-babel.mjs";

export default {
  input: "src/index.ts",
  output: [
    {
      file: "dist/index.esm.js",
      format: "es",
      sourcemap: true,
      plugins: [babelOutputPluginWithESM()],
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({
      tsconfig: "./tsconfig.json",
      declaration: true,
      declarationDir: "./dist",
    }),
  ],
};

使用效果:

下图是最终构建产物代码,可以发现已经加上了 try-catch:\

在项目中引用新的sdk代码,可以页面的渲染已经不受sdk的异常影响了,可以正常执行:\

总结

  1. 开发js-sdk需要有完备的错误捕获机制,在高危操作外部,甚至sdk初始化方法里都加上异常捕获逻辑,保证sdk的异常不影响到业务系统(本文解决方案属于亡羊补牢,最好的时机仍然是问题没发生前);
  2. rollup产物支持通过自定义的plugin去修改,本文基于 @rollup/plugin-babel 实现了自定义的 rollup-try-catch-babel plugin;
  3. 修改打包产物应该是克制且谨慎的,因为这会造成打包产物与业务代码不一致问题,容易引起其他开发同学不必要的困惑。
相关推荐
CodexDave40 分钟前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
爱喝水的鱼丶5 小时前
SAP-ABAP:ALV通用封装实践——搭建可复用的ALV开发工具类,减少80%重复代码
开发语言·性能优化·sap·abap·erp·alv
饼干哥哥8 小时前
腾讯会议出AI同传了?这下跨境人开会不用担心哑巴英语了
人工智能·性能优化·腾讯
爱喝水的鱼丶9 小时前
SAP-ABAP:ALV跨版本兼容指南——适配ECC与S4 HANA系统的ALV开发注意事项
开发语言·性能优化·sap·abap·经验交流
世人万千丶12 小时前
鸿蒙Flutter Flex布局性能优化
学习·flutter·性能优化·harmonyos·鸿蒙·鸿蒙系统
糖果店的幽灵1 天前
【langgraph 从入门到精通graphApi 篇】生产部署与性能优化
人工智能·性能优化·langgraph
解局易否结局2 天前
鸿蒙新特性实战:ArkUI 渲染性能优化——从 LazyForEach 到页面级按需加载
华为·性能优化·harmonyos
●VON2 天前
鸿蒙 PC Markdown 编辑器工程基线:用 PRD、ADR 与阶段闸门控制技术风险
华为·性能优化·编辑器·harmonyos·鸿蒙
淡海水2 天前
06-04-YooAsset源码-Unity加密解密服务
前端·unity·性能优化·c#·游戏引擎·yooasset