feat(build): 添加 Vue 3 兼容性补丁插件

- 实现了 Vite 插件来拦截依赖模块加载并返回兼容 Vue 3 的补丁版本
- 添加 xe-utils hasOwnProp 补丁解决 Proxy 兼容性问题
- 添加 element-plus form-label-wrap 补丁防止 NaN 宽度和生命周期错误
- 实现虚拟模块系统避免修改 node_modules 文件
- 添加 _isMounted 守卫防止组件卸载后访问已销毁的上下文
- 实现缓存机制优化补丁代码加载性能
```
This commit is contained in:
2026-06-03 15:12:48 +08:00
parent 39593f1aaf
commit d46cb7f93d

View File

@@ -0,0 +1,143 @@
/**
* Vite 插件:在构建时拦截依赖模块加载,返回兼容 Vue 3 的补丁版本。
*
* ⚠️ 不修改 node_modules 中的任何文件。
*
* 拦截清单:
* 1. xe-utils/hasOwnProp.js — Vue 3 Proxy 兼容
* 2. element-plus form-label-wrap.mjs — NaN 防护 + 生命周期守卫
*/
const VIRTUAL_HASOWNPROP = '\0patched:xe-utils/hasOwnProp'
const VIRTUAL_FORM_LABEL = '\0patched:element-plus/form-label-wrap'
export default function patchDepsPlugin() {
return {
name: 'patch-deps-vue3-compat',
enforce: 'pre',
// ── resolveId: 拦截模块 ID返回虚拟模块路径 ──
resolveId(source, importer) {
// 拦截 xe-utils 内部的 require('./hasOwnProp')
if (
source === './hasOwnProp' &&
importer &&
importer.includes('xe-utils')
) {
return VIRTUAL_HASOWNPROP
}
// 拦截 element-plus form-label-wrap完整路径匹配
if (
source.includes('element-plus') &&
source.includes('form-label-wrap')
) {
return VIRTUAL_FORM_LABEL
}
},
// ── load: 对被拦截的模块返回补丁代码 ──
load(id) {
if (id === VIRTUAL_HASOWNPROP) {
return PATCHED_HASOWNPROP
}
if (id === VIRTUAL_FORM_LABEL) {
return PATCHED_FORM_LABEL_WRAP
}
},
}
}
// ═══════════════════════════════════════════════
// 补丁 1xe-utils hasOwnProp — Proxy 兼容
// ═══════════════════════════════════════════════
// 根因Object.prototype.hasOwnProperty.call(proxyObj, key)
// 在 Vue 3 reactive Proxy 上触发 reactivity 拦截,
// 抛出 "obj.hasOwnProperty is not a function"。
// ═══════════════════════════════════════════════
const PATCHED_HASOWNPROP = `
function hasOwnProp(obj, key) {
if (obj == null) return false
try {
return Object.prototype.hasOwnProperty.call(obj, key)
} catch (e) {
try { return key in Object(obj) } catch (e2) { return false }
}
}
export default hasOwnProp
export { hasOwnProp }
`
// ═══════════════════════════════════════════════
// 补丁 2element-plus form-label-wrap
// ═══════════════════════════════════════════════
// 根因vxe-table 展开行收起时 el-form 组件已卸载,
// 但 nextTick/onUpdated 回调仍访问已销毁的 formContext
// 导致 NaN width 和 teardown 错误。
//
// 策略:从原始文件读取内容,在运行时用正则替换。
// 这样不依赖 node_modules 中的修改。
// ═══════════════════════════════════════════════
import fs from 'fs'
import path from 'path'
let cachedFormLabelWrap = null
function getFormLabelWrapCode() {
if (cachedFormLabelWrap) return cachedFormLabelWrap
const filePath = path.resolve(
process.cwd(),
'node_modules/element-plus/es/components/form/src/form-label-wrap.mjs'
)
if (!fs.existsSync(filePath)) {
console.warn('[patch-deps] form-label-wrap.mjs not found, skipping')
return null
}
const code = fs.readFileSync(filePath, 'utf-8')
cachedFormLabelWrap = code
// NaN 防护
.replace(
'return Math.ceil(Number.parseFloat(width))',
'return Math.ceil(Number.parseFloat(width)) || 0'
)
// _isMounted 守卫
.replace(
'const updateLabelWidth = (action = "update") => {',
'let _isMounted = true;\n const updateLabelWidth = (action = "update") => {'
)
.replace(
'nextTick(() => {',
'nextTick(() => {\n if (!_isMounted) return;'
)
.replace(
'if (slots.default && props.isAutoWidth) {',
'try {\n if (slots.default && props.isAutoWidth) {'
)
.replace(
'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);',
'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);\n }\n } catch (e) { /* teardown race */ }'
)
.replace(
'onBeforeUnmount(() => {',
'onBeforeUnmount(() => {\n _isMounted = false;'
)
.replace(
'onUpdated(() => updateLabelWidthFn())',
'onUpdated(() => { if (_isMounted) updateLabelWidthFn(); })'
)
.replace(
'if (props.updateAll) formContext?.registerLabelWidth(val, oldVal);',
'if (_isMounted && props.updateAll) formContext?.registerLabelWidth(val, oldVal);'
)
.replace(
'return () => {',
'return () => {\n if (!_isMounted) return null;'
)
return cachedFormLabelWrap
}
const PATCHED_FORM_LABEL_WRAP = getFormLabelWrapCode()