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 真被自動添加進去了:
查看自動添加邏輯:
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/scan.ts#L526
// bare imports: record and externalize ----------------------------------
build.onResolve({// avoid matching windows volumefilter: /^[\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 crawlingif (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 crawlingreturn {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
。
https://www.typescriptlang.org/tsconfig/#jsx
https://www.typescriptlang.org/tsconfig/#jsxFactory
https://www.typescriptlang.org/tsconfig/#jsxFragmentFactory
{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
來解決這種問題,但必須符合下面要求:
https://esbuild.github.io/api/#jsx-import-source
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
的配置:
https://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 的定義:https://esbuild.github.io/api/#define
在線編譯效果:https://esbuild.github.io/try/#YgAwLjE5LjIALS1pbmplY3Q6Li9wcm9jZXNzLWN3ZC1zaGltLmpzIC0tdGFyZ2V0PWVzNiAtLWRlZmluZTpwcm9jZXNzLmN3ZD1wcm9jZXNzQ3dkU2hpbQAAcHJvY2Vzcy1jd2Qtc2hpbS5qcwBleHBvcnQgbGV0IHByb2Nlc3NDd2RTaGltID0gKCkgPT4gJycAZQBlbnRyeS5qcwBjb25zb2xlLmxvZyhwcm9jZXNzLmN3ZCgpKQo
結合起來重寫配置:
// 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 官網中也講的不是很清楚,還是得摳源碼看具體實現細節。
另外對于庫的開發者來講,一定要提供編譯好后的代碼給開發者,包括腳本和樣式,默認美好。
微信搜索“好朋友樂平”關注公眾號。
github原文地址