问题
同事最近将某项目的构建工具从 webpack 迁移到了 Vite,开发和构建过程非常丝滑,但部署时发现构建出的静态文件在浏览器上运行报错:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
神奇的是原先使用 webpack 可以正确打包,唯独使用 Vite 有问题。他排查以后发现是项目中的 react-activation
依赖引起,这个库实现了类似 Vue 的 <keep-alive />
功能:
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import KeepAlive, { AliveScope } from 'react-activation'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
count: {count}
<button onClick={() => setCount((count) => count + 1)}>add</button>
</div>
)
}
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow((show) => !show)}>Toggle</button>
<div>without {`<KeepAlive>`}</div>
{show && <Counter />}
<div>with {`<KeepAlive>`}</div>
{show && (
<KeepAlive>
<Counter />
</KeepAlive>
)}
</AliveScope>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
报错的是 <KeepAlive></KeepAlive>
代码,但 KeepAlive
组件本身没有问题。
原因
要确定错误的原因很简单,从 React 报错的 Stack Trace 中点进去,设置断点,便可以发现:
如 React 的错误提示所说,这里的 KeepAlive
是个对象,不是 React 期望的函数或者类。
修改模块的引入方式,问题就可以解决(值得庆幸的是 react-activation
除了将 KeepAlive
作为默认导出,还进行了单独导出)。
import KeepAlive from 'react-activation'
// 改为
import { KeepAlive } from 'react-activation'
在 react-activation
的 issue 中原作者同样给出了这个解决方法,见 https://github.com/CJY0208/react-activation/issues/132。
重新梳理
问题虽然解决了,但借此可以详细写一写个中缘由。
react-activation 是如何导出模块的
首先看 react-activation
的导出(见 https://github.com/CJY0208/react-activation/blob/bd010077fdf1c20cbf45fea24fc0d25c048919b2/src/index.js#L12-L25):
// src/index.js
export default KeepAlive
export {
KeepAlive,
AliveScope,
withActivation,
fixContext,
autoFixContext,
useActivate,
useUnactivate,
createContext,
withAliveScope,
useAliveController,
NodeKey,
}
导出了所有对外 API,同时将 KeepAlive
作为默认导出,构建后变成(见 https://unpkg.com/browse/react-activation@0.9.5/lib/index.js):
// lib/index.js
Object.defineProperty(exports, '__esModule', { value: true });
exports.AliveScope = AliveScope;
exports.KeepAlive = KeepAlive$1;
exports.NodeKey = NodeKey;
exports.autoFixContext = autoFixContext;
exports.createContext = createContext;
exports.default = KeepAlive$1;
exports.fixContext = fixContext;
exports.useActivate = useActivate;
exports.useAliveController = useAliveController;
exports.useUnactivate = useUnactivate;
exports.withActivation = withActivation;
exports.withAliveScope = withAliveScope;
再结合 react-activation
的入口文件(见 https://github.com/CJY0208/react-activation/blob/bd010077fdf1c20cbf45fea24fc0d25c048919b2/index.js#L3-L7):
// index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./lib/index.min.js');
} else {
module.exports = require('./lib/index.js');
}
可以发现 react-activation
采用了和 react
一样的 CommonJS 模块导出方式。
Vite 是如何构建的
对于如下代码:
import KeepAlive from 'react-activation'
开发模式下,Vite 会将其处理成:
import __vite__cjsImport3_reactActivation from "/node_modules/.vite/react-activation.js?v=5ba8f290";
const KeepAlive = __vite__cjsImport3_reactActivation.__esModule
? __vite__cjsImport3_reactActivation.default
: __vite__cjsImport3_reactActivation;
这里的 KeepAlive
始终是函数。
但生产模式下,Vite 构建出的结果是:
import {
j as jsxRuntime, r as react, K as KeepAlive, R as ReactDOM, a as reactActivation
} from "./vendor.8e846e2e.js";
// ...
const jsx = jsxRuntime.exports.jsx;
const jsxs = jsxRuntime.exports.jsxs;
function Counter() {
const [count, setCount] = react.exports.useState(0);
return /* @__PURE__ */ jsxs("div", {
children: [/* @__PURE__ */ jsxs("p", {
children: ["count: ", count]
}), /* @__PURE__ */ jsx("button", {
onClick: () => setCount((count2) => count2 + 1),
children: "Add"
})]
});
}
function App() {
const [show, setShow] = react.exports.useState(true);
return /* @__PURE__ */ jsxs("div", {
children: [/* @__PURE__ */ jsx("button", {
onClick: () => setShow((show2) => !show2),
children: "Toggle"
}), show && /* @__PURE__ */ jsx(KeepAlive, {
children: /* @__PURE__ */ jsx(Counter, {})
})]
});
}
ReactDOM.render(/* @__PURE__ */ jsx(reactActivation.exports.AliveScope, {
children: /* @__PURE__ */ jsx(App, {})
}), document.getElementById("root"));
// vendor.8e846e2e.js
// ...
var reactActivation = { exports: {} };
// ...
var KeepAlive = reactActivation.exports;
// ...
export {
KeepAlive as K, ReactDOM as R, reactActivation as a, jsxRuntime as j, react as r
};
这里的 KeepAlive
实际是:
{
AliveScope: ƒ ()
KeepAlive: ƒ ()
NodeKey: ƒ ()
autoFixContext: ƒ ()
createContext: ƒ ()
default: ƒ ()
fixContext: ƒ ()
useActivate: ƒ ()
useAliveController: ƒ ()
useUnactivate: ƒ ()
withActivation: ƒ ()
withAliveScope: ƒ ()
__esModule: true
}
所以 React 才会报 Element type is invalid
错误。
另外,如果使用了 @vitejs/plugin-legacy
,对 import
/export
语法进行转义,降级成 SystemJS 效果一样(因为语义不变)。
System.register(["./vendor-legacy.6f14fe2e.js"], (function() {
"use strict";
var e, t, n, r;
return {
setters: [function(c) {
e = c.j,
t = c.r,
n = c.a,
r = c.R
}
],
execute: function() {
const c = e.exports.jsx
, o = e.exports.jsxs;
function i() {
const [e,n] = t.exports.useState(0);
return o("div", {
children: [o("p", {
children: ["count: ", e]
}), c("button", {
onClick: ()=>n((e=>e + 1)),
children: "Add"
})]
})
}
function s() {
const [e,r] = t.exports.useState(!0);
return o("div", {
children: [c("button", {
onClick: ()=>r((e=>!e)),
children: "Toggle"
}), e && c(n, {
children: c(i, {})
})]
})
}
r.render(c(n.AliveScope, {
children: c(s, {})
}), document.getElementById("root"))
}
}
}
));
其中的变量 n
即 KeepAlive
,它来源于 vendor-legacy.6f14fe2e.js
模块导出的 a
字段。
再看 vendor-legacy.6f14fe2e.js
,为了方便定位关键字 KeepAlive
,这里对 react-activation
做了改动,实际打包的是 lib/index.js
而不是 lib/index.min.js
,以下是节选代码:
// vendor-legacy.6f14fe2e.js
var Jc = {};
Object.defineProperty(Jc, "__esModule", {
value: !0
});
Jc.AliveScope = ih;
Jc.KeepAlive = Eh;
Jc.NodeKey = jd;
Jc.autoFixContext = Pp;
Jc.createContext = function() {...};
Jc.default = Eh;
Jc.fixContext = xp;
Jc.useActivate = Zp;
Jc.useAliveController = function() {...};
Jc.useUnactivate = Jp;
Jc.withActivation = Xp;
Jc.withAliveScope;
e("a", Jc);
可以看到除了使用 SystemJS 做降级,和未使用 @vitejs/plugin-legacy
之前的逻辑一样。
Vite 在生产模式下使用 Rollup 对应用进行打包,在处理 react-activation
模块时缺失了对 __esModule
的判断和对 default
属性的处理,导致开发和构建不一致。这究竟是 Vite 的问题还是 Rollup 的问题有待后续跟进。
Vite 与 webpack 的差别
webpack 会将 ES Module 和 CommonJS 全部统一处理成 ComomJS,最后的 bundle 文件中是级联的 CommonJS 模块,运行时根据 __esModule
判断原模块是否是 ES Module。Vite 和 webpack 走的是相反的一条路,它跟 Snowpack 一样选择了面向未来,将 CommonJS 转成 ES Module。
总结
export default
的问题,归根结底是 CommonJS 和 ES Module 的互操作性问题,二者不完全兼容。也就是 import KeepAlive from 'react-activation'
和 const reactActivation = require('react-activation')
不是直接对应关系。
二者实际关系,参考以下代码:
默认导入(default import):
import KeepAlive from 'react-activation'
// 近似于
const KeepAlive = require('react-activation').default
命名空间导入(namespace imports):
import * as reactActivation from 'react-activation'
// 近似于
const reactActivation = require('react-activation')
有名导入(named imports):
import { KeepAlive } from 'react-activation'
// 近似于
const { KeepAlive } = require('react-activation')
如今浏览器和 Node.js 都已原生支持 ES Module,但 npm 上依然有着存量巨大的 CommonJS 包,为了避免互操作时掉进默认导出的坑,日常开发更推荐使用有名导出。