1. 背景
众所周知,vite 在构建生态的位置,vue 与之更是密切,主流的 vue 库几乎都与 vite 捆绑。
但有些 UI 库 如 @private/ui 并没进行行编译,而是直接将源码发布到了 npm 中,无法实现兼容化,需要消费方去自行处理库中的环境问题,及额外的编译时间。
基于 vue 官方脚手架创建的项目也是捆绑的 vite,但在使用 @private/ui 组件时,开发环境一直编译报错,无法使用。
还得从 vite 下手,看下为什么它无法编译通过。
2. 问题现场
开发环境报错:
为什么会把 @private/ui 编译成了 React.createElement 去创建元素?
编译环境:
正常。
vite 是有两套构建环境的,这种不一致性很麻烦:
问题就出在开发环境的 esbuild 中。
3. vite optimizeDeps
从样是写 tsx,为什么项目中的可以正常执行,而 @private/ui 中的就编译错误?两者明显不在一个构建过程中。 vite 的 optimizeDeps 也没进行配置,怎么会出现预编译的效果。
debugger 编译过程发现,@private/ui 真被自动添加进去了:
查看自动添加逻辑:
// bare imports: record and externalize ----------------------------------
build.onResolve(
{
// avoid matching windows volume
filter: /^[\w@][^:]/,
},
async ({ path: id, importer, pluginData }) => {
if (moduleListContains(exclude, id)) {
return externalUnlessEntry({ path: id });
}
if (depImports[id]) {
return externalUnlessEntry({ path: id });
}
const resolved = await resolve(id, importer, {
custom: {
depScan: { loader: pluginData?.htmlType?.loader },
},
});
if (resolved) {
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id });
}
if (isInNodeModules(resolved) || include?.includes(id)) {
// dependency or forced included, externalize and stop crawling
if (isOptimizable(resolved, config.optimizeDeps)) {
depImports[id] = resolved;
}
return externalUnlessEntry({ path: id });
} else if (isScannable(resolved, config.optimizeDeps.extensions)) {
const namespace = htmlTypesRE.test(resolved) ? "html" : undefined;
// linked package, keep crawling
return {
path: path.resolve(resolved),
namespace,
};
} else {
return externalUnlessEntry({ path: id });
}
} else {
missing[id] = normalizePath(importer);
}
}
);
可以看到只要是项目源码直接引用的,js 类型的包就会被自动添加进去。
4. 解决
这里要注意所有在预处理过程中的 esbuild 配置,一定要在optimizeDeps.esbuildOptions
中配置,而不是esbuild
,两个流程读取的配置不一样,详情看源码。
4.1 解法一:esbuild jsx 重写
esbuild 提供了 jsx 相关的配置重写,可以直接将React.createElement
重写为 h
。
www.typescriptlang.org/tsconfig/#j…
www.typescriptlang.org/tsconfig/#j…
www.typescriptlang.org/tsconfig/#j…
{
jsxFactory: 'h',
jsxFragment: 'Fragment'
}
编译后:
// 最初编译结果
return React.createElement(React.Fragment, null, slots.handler && React.createElement(
GridItem,
{
row: props.row,
column: "1 / -1",
...bindings
},
slots.handler()
)
// 修改后编译结果
return h(Fragment, null, slots.handler && h(
GridItem,
{
row: props.row,
column: "1 / -1",
...bindings
},
slots.handler()
)
可以看到正常了,但又报错了:
esbuild 提供了 jsxImportSource
来解决这种问题,但必须符合下面要求:
esbuild.github.io/api/#jsx-im…
import { createElement } from "your-pkg";
import { Fragment, jsx, jsxs } from "your-pkg/jsx-runtime";
import { Fragment, jsxDEV } from "your-pkg/jsx-dev-runtime";
然而 vue 完全没这种包。
esbuild 还有一个 inject
的配置:
esbuild.github.io/api/#inject
不太好的方式是,直接把 React
定义到全局变量中:
// inject.js
const { h, Fragment } = require("vue");
window.React = {
createElement: h,
Fragment: Fragment,
};
// vite.config
inject: ["./inject.js"],
可以正常工作了。
esbuild 提供了另一种方式:
import { h, Fragment } from "vue";
export { h as "React.createElement", Fragment as "React.Fragment" };
但报错:
✘ [ERROR] Using a string as a module namespace identifier name is not supported in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)
看到 esbuild 的 define 的定义:esbuild.github.io/api/#define 在线编译效果:esbuild.github.io/try/#YgAwLj…
结合起来重写配置:
// inject.js
export { h, Fragment } from "vue";
// config
inject: ["./inject.js"],
define: {
"React.createElement": "h",
"React.Fragment": "Fragment",
}
可以正常工作。
4.2 解法二:移除 @private/ui 预编译
更快的方式是把 @private/ui 从预编译中移除,但会增加加载时长。
exclude: ["@private/ui"];
5. 其它
由于没编译,组件库内非 es 的模块还会出问题,还要项目上去做预编译才能正常使用:
export default defineConfig({
plugins: [vue(), vueJsx()],
optimizeDeps: {
include: [
"lodash.uniq",
"lodash.get",
"lodash.set",
// ...
],
esbuildOptions: {
inject: ["./inject.js"],
define: {
"React.createElement": "h",
"React.Fragment": "Fragment",
},
},
},
esbuild: {},
});
6. 总结
vite 固然好,但多编译环境还是会出现对不齐的问题,一些配置在 vite 官网中也讲的不是很清楚,还是得抠源码看具体实现细节。
另外对于库的开发者来讲,一定要提供编译好后的代码给开发者,包括脚本和样式,默认美好。
微信搜索“好朋友乐平”关注公众号。