WebView2 原生拖放的桥接之道:拆解 yyzTools 的 drop-zone.js

yyzTools 是 C++(Win32 + WebView2)加 Vite 前端的混合架构:原生层负责窗口、系统钩子、文件操作,UI 全在 WebView2 里跑。这种架构有个绕不开的痛点------你想让用户从资源管理器把文件拖进网页,可 Chromium 沙箱下的 Web 端收不到系统级的 OLE 拖放(CF_HDROP)。这活只能原生层干,再桥给前端。

1.0.7.1700 把这套桥接做实了:六个批量模块、程序坞、右键菜单都接上了拖放。本文只拆前端这边看到的那一层------web/lib/drop-zone.js,以及它背后的注册时序。

整体链路

注意 ExecuteScript 这一步:原生层不解析文件、不过滤类型,只负责把绝对路径数组塞进页面全局函数。业务逻辑全留在前端,原生层保持"哑"。

全局入口与单 handler 设计

drop-zone.js 顶部就这么几行:

ini 复制代码
let handler = null;

window.__zenDropFiles = paths => {
    if (Array.isArray(paths) && handler) handler(paths);
};

export function registerDropFiles(cb) {
    handler = cb;
}

两个设计点值得说。

一是 window.__zenDropFiles 作为原生注入锚点。 C++ 那边硬编码调这个函数名,前端把它挂在 window 上,原生层不需要知道页面是哪个模块、用的什么框架。耦合点只有这一个字符串。

二是 registerDropFiles 是单 handler(后注册覆盖前者)。 宿主窗口只有一个拖放目标,同一时刻也只有一个模块在前台接收拖入,所以不需要事件总线、不需要多订阅。模块挂载时 registerDropFiles(myCb),卸载或切换时再注册新的即可。简单,且不会有两份回调同时触发导致的双处理。

expandDropPaths:一级展开 + accept 过滤

真正有意思的是展开逻辑:

ini 复制代码
export async function expandDropPaths(paths, accept = null) {
    const out = [];
    for (const p of paths) {
        let isDir = false;
        try {
            const info = await ZenAPI.getFileInfo(p);
            isDir = ZenAPI.isOk(info) && info.isDirectory === true;
        } catch (e) { /* ignore */ }

        if (!isDir) {
            if (!accept || accept(p)) out.push(p);
            continue;
        }

        try {
            const r = await ZenAPI.readDir(p);
            if (ZenAPI.isOk(r) && Array.isArray(r.files)) {
                const dir = p.replace(/\/g, '/').replace(//+$/, '');
                for (const name of r.files) {
                    const f = dir + '/' + name;
                    if (!accept || accept(f)) out.push(f);
                }
            }
        } catch (e) { /* ignore */ }
    }
    return out;
}

几个关键点:

判目录用原生布尔,不是字符串比较。 注释里写了"自研 ptree 已输出原生布尔,直接 === true 判断"。很多桥接方案图省事用字符串 "true" 或 1,一旦后端序列化出错就翻车。这里 info.isDirectory === true 是强类型判断,拿不到明确布尔就当文件处理,安全。

只展开一级。 目录走 readDir 拿子项,但子项不再递归判断。前面提过,整树递归既卡又容易误吞。一级是刻意的产品取舍。

readDir 返回的是文件名,不是全路径。 所以代码里做了拼接:dir + '/' + name,并把反斜杠统一成斜杠。这个细节前端同学容易忽略------原生层返回的是裸名,路径组装得自己补。

accept 过滤放在最后。 文件直接过 accept(p),目录展开后每个子文件再过 accept(f)。传 null 则不过滤。图片模块就传一个按扩展名判的函数,文件夹里混着的 txt、docx 自动被滤掉。

pathExt 这个小工具也顺带导出了,给 accept 实现复用:

bash 复制代码
export function pathExt(p) {
    return p.replace(/\/g, '/').split('/').pop().split('.').pop().toLowerCase();
}

抢注时序:为什么不是"注册一下就完事"

原生层还有个反直觉的细节。WebView2 本身能收 OLE 拖放,但你要抢在它之前拦截 CF_HDROP,就得自己 RegisterDragDrop。可 WebView2 的 AllowExternalDrop 必须保持 TRUE------置 FALSE 会让 Chromium 拒绝一切 OLE 拖放,你自己的目标也收不到。

于是做法是:全局低级鼠标钩子在左键按下 时才 RegisterDragDrop,抢在 WebView2 之前;左键松开 后延迟 150ms 才 RevokeDragDrop。为什么要等 150ms?因为低级钩子的松键回调早于 OLE 的 Drop 派发,你要是立刻撤销,拖放在最后一刻会落空,文件就"掉"不进去了。

这 150ms 是桥接层最隐蔽的一个坑,纯前端视角的同学看到 drop-zone.js 会觉得"不就是个回调吗",但真正让这个回调能稳定被调到,靠的是原生层这套钩子时序。

另一条路径:程序坞的文件夹拖入不走这个桥

有个对照值得记一下:程序坞(tabdock)导航条的"拖文件夹进来建分类",没有 走 drop-zone.js。

它在 native 侧就结束了------WinDockNav.cpp 把 AcceptFileDrop 打开,OnFilesDropped 收到路径后直接 DockDrop::ListFolderEntries 列目录、MakeId("category_") 建分类、重排 order、写回配置 CONF_DOCK_NAV,最后只把结果脚本(BuildDropResultScript)丢给页面渲染。单个分类最多收 30 条(DockDropHelper.h 的 MAX_ITEMS_PER_CATEGORY),超出截断。

区别在哪?批量模块要的是"这些文件的内容",得交给页面去处理,所以走桥接把路径递进去;程序坞要的是"这个目录变成一条配置",本质是配置写回,native 干完就行,页面只负责显示。判断标准其实很简单:谁消费数据,就由谁接这一棒。

顺带说下这个能力对用户意味着什么:把一堆软件快捷方式丢进一个文件夹,整个拖到导航栏上,一次就变成一个分类,桌面就此清空------整理成本从"一个一个加"变成"先归堆,再拖一次"。

小结

drop-zone.js 的代码量不大,但把混合架构里最别扭的一块(系统拖放 → 网页)收得很干净:原生层只传路径,不做业务;前端用单 handler + 一级展开 + accept 过滤,把"拖文件夹进来意味着什么"定义清楚。下次你要在 WebView2 里接原生拖放,这套"哑原生 + 薄桥 + 聪明前端"的分层值得直接抄。

相关推荐
朔北之忘 Clancy20 小时前
2025 CSP-J 第二轮真题解析
c++·青少年编程·题解·noip·csp·信奥赛·noi
longlongzihan20 小时前
LeetCode 17电话号码的字母组合:回溯算法(DFS)详解
c++·算法·leetcode·深度优先
yolo_guo20 小时前
调试mysql延迟与libevent回调实际发送回复时机问题
c++·mysql·libevent
前端snow20 小时前
ai agent --- postgreSQL 关系型数据库
前端
卷无止境21 小时前
ECharts:把数据变成故事的可视化利器
前端
今晚打老虎21 小时前
c++之提高A(前缀和)(第三课)
数据结构·c++·算法
Eric_见嘉21 小时前
在职前端 Skill 和 MCP 分享
前端·后端·agent
liangshanbo121521 小时前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
wengad21 小时前
从 wangEditor 迁到 wangEditor-next
前端·javascript·vue.js
by209991 天前
从结构体到对象:正式学习C++类的骨架、封装与this指针
c++·经验分享·学习