# Vue 3 自定义组件 JS 操作 SOP ## 问题 Vue 3 自定义组件(如 OxdSelect)通过 `addEventListener` 绑定事件,JS `dispatchEvent` 产生的事件 `isTrusted: false`,组件不响应。 - `element.click()` 无效(组件可能绑定 mousedown 而非 click) - `dispatchEvent(new MouseEvent('mousedown'))` 无效(isTrusted:false) - `element.focus()` 不触发 Vue 绑定的 focus handler ## 解决方案:直接操作 Vue 组件实例 ### 1. 获取 Vue 3 根入口 ```javascript const rootVnode = document.getElementById('app')._vnode; ``` ### 2. 遍历 vnode 树匹配 DOM 元素 ```javascript function findCompByEl(vnode, targetEl, depth = 0) { if (depth > 50 || !vnode) return null; const comp = vnode.component; if (comp) { if (comp.vnode?.el === targetEl || comp.subTree?.el === targetEl) return comp; if (comp.vnode?.el?.contains?.(targetEl)) { const result = findCompByEl(comp.subTree, targetEl, depth + 1); if (result) return result; return comp; } const subResult = findCompByEl(comp.subTree, targetEl, depth + 1); if (subResult) return subResult; } if (vnode.children && Array.isArray(vnode.children)) { for (const child of vnode.children) { const result = findCompByEl(child, targetEl, depth + 1); if (result) return result; } } if (vnode.dynamicChildren) { for (const child of vnode.dynamicChildren) { const result = findCompByEl(child, targetEl, depth + 1); if (result) return result; } } return null; } ``` ### 3. 调用组件方法 ```javascript // 目标DOM的parentElement通常是组件根元素 const comp = findCompByEl(rootVnode, targetElement.parentElement); const ctx = comp.proxy; // 查看可用方法 Object.keys(ctx).filter(k => !k.startsWith('_') && !k.startsWith('$')); // Select 类组件:直接调用 onSelect ctx.onSelect({id: 'USD', label: 'United States Dollar'}); // 获取选项列表 ctx.computedOptions; // [{id, label, _selected}, ...] ``` ## 组件层级注意 - **展示层**(如 OxdSelectText):只有 onToggle/onFocus/onBlur,调用无实际效果 - **逻辑层**(如 OxdSelectInput,是展示层的父组件):有 openDropdown/onSelect/computedOptions/onCloseDropdown - 定位逻辑层:用 `targetElement.parentElement` 而非 targetElement 本身 ### 弹窗内 Select 同样纯 JS 优先(已验证) - 弹窗(`.oxd-dialog-sheet`)内的 `.oxd-select-text` 用循环向上查找同样能命中 `OxdSelectInput`,`onSelect` 正常工作。 - 不需要 CDP 兜底。仅当循环 8 层仍找不到组件时才考虑 CDP 打开+JS 点 option。 ### 循环向上查找模式(推荐) 单层 `parentElement` 可能不够,用循环更健壮: ```javascript function findSelectComp(selectTextEl) { for (let el = selectTextEl, up = 0; el && up < 8; el = el.parentElement, up++) { const comp = findCompByEl(rootVnode, el); if (comp?.proxy?.onSelect && comp.proxy.computedOptions?.length) return comp; } return null; // 找不到再考虑CDP兜底 } ``` ## 普通 Input/Textarea 操作(nativeSetter) Vue 3 的 `v-model` 监听 input 事件,直接 `el.value = x` 不触发响应式。需用原型 setter: ```javascript // Input const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(inputEl, '新值'); inputEl.dispatchEvent(new Event('input', {bubbles: true})); inputEl.dispatchEvent(new Event('change', {bubbles: true})); // Textarea const taSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set; taSetter.call(textareaEl, '内容'); textareaEl.dispatchEvent(new Event('input', {bubbles: true})); ``` ### Date Input 特殊处理 日期组件通常有 blur 校验,需要 focus→赋值→blur 完整链: ```javascript dateInput.focus(); setter.call(dateInput, '2026-08-05'); dateInput.dispatchEvent(new Event('input', {bubbles: true})); dateInput.dispatchEvent(new Event('change', {bubbles: true})); dateInput.dispatchEvent(new Event('blur', {bubbles: true})); ``` ### Button 普通 `.click()` 即可,Vue 3 不检查 button click 的 isTrusted。 ### File Upload (input[type="file"]) 浏览器安全模型禁止JS直接 `input.value='path'`,但可用 DataTransfer API 构造 FileList: ```javascript const fileInput = document.querySelector('input[type="file"]'); const content = '文件内容'; const file = new File([content], 'filename.txt', { type: 'text/plain', lastModified: Date.now() }); const dt = new DataTransfer(); dt.items.add(file); fileInput.files = dt.files; // Chrome 62+ 支持 fileInput.dispatchEvent(new Event('input', { bubbles: true })); fileInput.dispatchEvent(new Event('change', { bubbles: true })); ``` - 适用于任何框架(非Vue3特有),纯浏览器API - 可构造任意类型文件(Blob/ArrayBuffer均可传入File构造器) - ⚠ CDP `DOM.setFileInputFiles` 只设files属性不触发事件(Chrome通用行为),DataTransfer+dispatch是唯一纯JS方案 - ⚠ 确保弹窗/容器已打开再querySelector,否则input不在DOM中 ## 泛化到其他 Vue3 站点(未逐一验证,思路层面) 本 SOP 的核心方法(根 vnode → findCompByEl → proxy)是 Vue3 通用的,但具体方法名/属性名因 UI 库而异。 面对陌生 Vue3 站点的探测思路: 1. **确认是 Vue3** — `document.getElementById('app')?.__vue_app__` 存在即可 2. **定位目标 DOM** — 用选择器找到要操作的元素(如某个 select wrapper) 3. **从 DOM 反查组件** — 用 findCompByEl 从目标元素及其父级向上找,拿到 component 4. **探测组件能力** — 拿到 comp 后查看: - `Object.keys(comp.proxy.$options.methods || {})` → 组件方法名 - `Object.keys(comp.props || {})` → props - `Object.keys(comp.setupState || {})` → setup 暴露的响应式数据和函数 - 重点找类似 onSelect/handleSelect/select/setValue 的方法,以及 options/items/computedOptions 之类的选项列表 5. **试调** — 找到疑似选中方法后,传入选项对象试调,观察 DOM 是否更新 6. **选项格式** — 不同库的 option 结构不同(可能是 `{id, label}` 也可能是 `{value, text}` 或纯字符串),从选项列表数据中取一个完整对象传入即可 注意事项: - 有些库用 `emits` 而非 methods,选中逻辑可能在父组件而非子组件 - 有些库 prod build 会 minify 方法名,此时 setupState 里的 key 可能是短名,需结合行为猜测 - Composition API 组件的逻辑主要在 setupState 而非 $options.methods - 如果 proxy 上找不到方法,试试 `comp.exposed`(`