likes
comments
collection
share

当Vite遇到Commonjs, 项目中遇到的问题简析但是, 事情总是会有但是, 现阶段Vite在开发时使用ESBuil

作者站长头像
站长
· 阅读数 22

我们在去年把一个项目从Webpack迁移到了Vite, 主要是想要提高开发效率, Vite在开发时也确实是快, 项目组的小伙伴都表示用着很爽。

但是, 事情总是会有但是, 现阶段Vite在开发时使用ESBuild对项目依赖的第三方库(包括ES Module/CJS Module)进行预构建, 到编译时则是用rollup打包, (使用@rollup/plugin-commonjs处理CJS Module), 这也就使得同一份代码, 在开发时和编译时的行为可能会不一致。

最近我就在项目中遇到了两个问题, 都是使用CJS模块, 开发时没有问题, 但编译后在线上生产环境出现了问题, 这里简单记录下调试解决的过程。


案例1

项目中有个模块用了jspdf这个库来在前端生成PDF并提供下载, 并且使用了jspdf-autotable来在PDF内生成表格:

import JSPDF from 'jspdf';
import 'jspdf-autotable';

const doc: any = new JSPDF('p', 'pt');
...
...

doc.autoTable(...);

基本上就是jspdf-autotable作为一个副作用引入, 它内部应该会把自己挂载到主库(jspdf)上去, 让JSPDF的实例可以调用autoTable。 这个模块一直没有什么问题, 但是最近QA反馈PDF生成功能失效了。

线上打断点调试后发现doc上不存在autoTable方法了, 但是开发环境下打印是存在的, 那就可以大概圈定BUG是在构建过程中产生的。

本地搭了一个最小复现后发现, build后jspdf-autotable的代码是这样的:

...
module.exports = e(function() {
    try {
        return require("jspdf");
    } catch (t4) {
}}());
...

这个require...那就知道了, 打包的时候不知道为啥@rollup/plugin-commonjs没有处理这个文件, 原样输出了, 那当jspdf-autotable执行的时候找不到jspdf 自然就没有办法挂载api了。

BUG定位了之后开始准备解决, 这时又想起一个线索: 这块之前是通过测试的, 看git记录这个文件也很久没动了, 现在出现问题会不会是构建工具版本升级导致的呢?

因为@rollup/plugin-commonjs是Vite依赖的, 所以在最小实现上试Vite的不同版本的编译结果, 当前(2022.2)最新版本是2.8.0, 往前试了几个版本最终发现, 2.6.2是对的:

var require$$0 = /* @__PURE__ */ getAugmentedNamespace(jspdf_es_min);
...
...
module.exports = e(function() {
    try {
        return require$$0;
      } catch (t4) {
}
}());

到了2.6.3就错了, 比较这两个版本, 其中有一条跟 @rollup/plugin-commonjs 相关的vitejs/vite#5173嫌疑最大。

这个PR更新了@rollup/plugin-commonjs的大版本, 从20.0.0到21.0.0, 再到@rollup/plugin-commonjs的仓库比较了两个版本之间的差异后 基本确定是 use safe default value for ignoreTryCatch 这个PR:

它修改了@rollup/plugin-commonjs配置里的ignoreTryCatch默认值, 现在默认为true, 而这个配置的作用就是, 为true时忽略在try..catch中的require。而jspdf-autotable恰好是在try..catch中引用了jspdf, 在转换的时候就被忽略了...

至于为什么要修改这个默认值, 首先, @rollup/plugin-commonjs在ignoreTryCatch的文档中提到:

In most cases, where `require` calls are inside a `try-catch` clause, they should be left unconverted as it requires an optional dependency that may or may not be installed beside the rolled up package.
Due to the conversion of `require` to a static `import` - the call is hoisted to the top of the file, outside of the `try-catch` clause.

大意就是, try..catch中的依赖是可选的, 转换成静态的import后会被提升到顶层, 不在try..catch内了。 但为了更好的兼容性在之前这个值被默认设置成了false

这个BUG到这里可以解决了, 要么向jspdf-autotable提PR, 改掉这个代码, 要么在项目中显示的设置ignoreTryCatchfalse

案例2.0

项目依赖了Antd, 近期发现打开某个页面直接报错崩溃, 同样是开发环境无法复现, 并且这个因为打开页面就崩溃, 断点都不好打...在报错栈里看应该是Antd的Upload组件的渲染方法报的错, 到项目里搜了一下, 不少地方用到这个组件, 但有个地方有点奇怪, 大概是这样的:

import Upload from "antd/lib/upload";
function Test() {
  return <Upload.Dragger>...</Upload.Dragger>;
}

直接引入了CJS版本, 刚经历过上面那个问题的摧残我表示对CJS很怀疑...但是项目内在有ESM的情况下是不推荐引用CJS版本的, 这可能是哪次写快了, 编辑器自动导入的(之前有发生过这种问题)。

尝试性的把这里换成 import Upload from "antd/es/upload"; 提交CI, 上个厕所回来线上刷新一看, 嘿, 真是因为这个, 现在已经没有报错了。

好, 下班走人。

案例2.1

上次虽然修复了BUG, 但是一直有亿点好奇...虽然说不推荐用CJS了, 虽然说Antd提供了ESM版本, 但是Vite也没说不支持吧, rollup也没说不能用呀, 要是我就想用这个cjs版本, 为啥就是不行呢?

趁项目上有点时间, 我还是觉定挖一挖这个bug。 还是起了个最小环境测试,使用上面的代码, build之后进去看了dist的代码, 简化后是这样的:

// 若存在__esModule字段就原样返回, 否则包一层default
function _interopRequireDefault2(obj) {
  return obj && obj.__esModule
    ? obj
    : {
        default: obj,
      };
}

var upload$1 = {};

// 这里定义了 Upload$2 
var Upload$2 = {};
var Dragger = {};

// Dragger文件
(function (exports) {
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  // 引用了Upload$2, 此时Upload$2没有__esModule属性, 也没有default, 被包了一层default返回
  // 此时, _Upload = {default: Upload$2}
  var _Upload = _interopRequireDefault2(Upload$2);
  var InternalDragger = function InternalDragger2(_a, ref2) {
    // _Upload 被闭包捕获
    //...
    //渲染时, Upload$2在下面才被赋值default, 也就是Upload$2 = {default: ...}
    //那么这里_Upload["default"] = {default: ...}, 显然不是一个合法的type
    return /* @__PURE__ */ React2.createElement(_Upload["default"],...)
  };
  var Dragger2 = InternalDragger;
  exports["default"] = Dragger2;
})(Dragger);


// Upload文件
(function (exports) {
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  
  // 这里也引用了Dragger
  var _Dragger = _interopRequireDefault2(Dragger);
  
  var InternalUpload = function InternalUpload2(props, ref2) {
    //...
  };
  var Upload2 = InternalUpload;
  Upload2.Dragger = _Dragger["default"];
  
  // 这里Upload$2才被赋值default, 现在是Upload$2 = {default: ...}
  exports["default"] = Upload2;
})(Upload$2);


// index文件
(function (exports) {
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  var _Upload = _interopRequireDefault2(Upload$2);
  var _Dragger = _interopRequireDefault2(Dragger);
  _Upload["default"].Dragger = _Dragger["default"];
  exports["default"] = _Upload["default"];
})(upload$1);

var Upload = /* @__PURE__ */ getDefaultExportFromCjs(upload$1);

function App() {
  return /* @__PURE__ */ jsx("div", {
    className: "App",
    children: /* @__PURE__ */ jsx(Upload.Dragger, {
      children: "...",
    }),
  });
}

看这种代码其实挺头大的...但是也想不出其他好的办法了, 打了注释之后应该就比较清楚了, 这里有个循环引用的问题:

  • Dragger引用了Upload, 在渲染函数内会使用Upload
  • Upload也引用了Dragger, 但只是把Dragger挂载到Upload上
  • index引用了两者, 并也把Dragger挂载到了Upload上

@rollup/plugin-commonjs 在转换这个CJS模块的时候先进入了index, 然后深度优先的进入了Upload文件, 再进入Dragger文件, Dragger文件也引用了Upload, 但此时Upload文件还没有被解析(是一个空Object),就被错认为是一个CJS模块包了一层default, 并被Dragger的render方法通过闭包捕获了。

后续正常解析Upload, 最后的Upload是这样的: {default: ..., __esModule: true};, 而Dragger的render方法内捕获的Upload就变成了{default: {default: ..., __esModule: true}}

总结

可能有些标题党, 这俩问题跟Vite其实都没啥关系, 严格来说都是Rollup领域的问题😅。

期望后面可以在生产环境下也用上ESBuild(Why Not Bundle with esbuild?), 目前这种差异性导致的问题比较难以排查和解决, 目前我遇到的问题都CJS引起的, 但项目依赖的三方库很多, 短时间也无法确保他们全部都有ESM版本。

转载自:https://juejin.cn/post/7063389293351272478
评论
请登录