当Vite遇到Commonjs, 项目中遇到的问题简析但是, 事情总是会有但是, 现阶段Vite在开发时使用ESBuil
我们在去年把一个项目从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, 改掉这个代码, 要么在项目中显示的设置ignoreTryCatch
为false
。
案例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