做 Electron 桌面应用的时候难免要和操作系统打打交道。在 Windows 上这些需求说起来也不少调用 Windows Store API 搞应用内购买处理 Windows Store 应用特有的文件系统虚拟化获取系统级别的权限和资源和 Windows Runtime (WinRT) 组件交互Electron 说到底还是 Node.js 环境而 Node.js 本来就不直接提供访问 Windows 原生 API 的能力。两者之间需要一座桥。这就像你想和不懂中文的朋友交流中间总得有个翻译官。Electron 是用 JavaScript 写的Windows API 是 C/C 写的语言不通得想办法搭个桥。代码世界的残酷就在这里没什么人情的。关于 HagiCode本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode Desktop 需要调用 Microsoft Store API 来处理订阅购买和许可证管理这便是我们摸索出一套技术方案的原因。毕竟有需求才有动力这话一点不假。技术方案对比在 Electron 中调用 Windows 原生 API有几种主流方案可以选择。每种方案都有其适用场景就像工具箱里的不同工具用对了地方才能发挥最大作用用错了也只是徒增麻烦。方案适用场景优点缺点dynwinrtWinRT API (如 Store API)类型安全、自动生成绑定、现代 JavaScript 支持只支持 WinRT API、需要 Windows SDK原生 Node.js 扩展高性能、任何 Windows API完全控制、性能最优需要 C 开发能力、跨平台复杂child_process PowerShell临时性、一次性调用简单快捷、无需编译性能差、错误处理复杂edge.js/ffi-napi调用现有 DLL可复用现有库兼容性问题、维护成本高HagiCode Desktop 采用了混合方案使用 dynwinrt 来访问 Windows Store API使用原生 Node.js 扩展来处理高性能的 Store 购买操作同时用 Node.js 原生 fs 和 path 模块处理 Windows Store 应用特有的文件系统虚拟化。能简单就简单这也是我们的原则。方案一使用 dynwinrt 调用 WinRT APIdynwinrt 是 Microsoft 提供的一个工具链可以基于 Windows SDK 的 metadata 文件自动生成 JavaScript 绑定。它专门用于调用 WinRT API比如 Windows Store API。安装依赖{optionalDependencies: {microsoft/dynwinrt: 0.1.0-preview.6,microsoft/dynwinrt-codegen: 0.1.0-preview.6}}生成 WinRT 绑定// scripts/generate-store-bindings.jsconst { execFileSync } node:child_process;function generateStoreNamespace(windowsWinmdPath) {execFileSync(npx, [dynwinrt-codegen,generate,--winmd, windowsWinmdPath,--namespace, Windows.Services.Store,--output, src/main/subscription/generated-js,--lang, js,]);}使用生成的绑定// 使用 dynwinrt 生成的 Store API 绑定import { Windows } from ../subscription/generated-js/index.js;async function queryStoreProduct(storeId: string) {const storeContext Windows.Services.Store.StoreContext.getDefault();const result await storeContext.getAssociatedStoreProductsAsync([Subscription, Durable]);if (result.extendedError ! 0) {throw new Error(Store API error: ${result.extendedError});}return result.products.get(storeId);}dynwinrt 的好处是类型安全生成的代码和现代 JavaScript 习惯一致。但它只能处理 WinRT API如果你需要调用传统的 Win32 API就得用别的方案了。工具就是这样各有所长。方案二原生 Node.js 扩展当需要高性能或者 dynwinrt 不支持的功能时原生 Node.js 扩展是最佳选择。这个方案需要用 C 写代码然后用 node-gyp 编译成 .node 文件。创建 binding.gyp{targets: [{target_name: windows-store-addon,sources: [src/windows-store-addon.cpp],include_dirs: [!(node -e \require(nan)\)],defines: [WIN32_LEAN_AND_MEAN]}]}C 原生模块示例// src/windows-store-addon.cpp#include nan.h#include windows.h#include wrl.h#include windows.services.store.husing namespace v8;using namespace Windows::Services::Store;NAN_METHOD(QueryStoreStatus) {auto async new Nan::AsyncWorker([]() {// 调用 Windows Store APIauto context StoreContext::GetDefault();auto products context-GetAssociatedStoreProductsAsync(...)-GetResults();// 处理结果});Nan::AsyncQueueWorker(async);}NAN_MODULE_INIT(InitModule) {Nan::Set(target, Nan::New(queryStoreStatus).ToLocalChecked(),Nan::GetFunction(Nan::NewFunctionTemplate(QueryStoreStatus)).ToLocalChecked());}NODE_MODULE(windows_store_addon, InitModule)编译和使用node-gyp rebuildimport addon from ./build/Release/windows-store-addon.node;const result addon.queryStoreStatus({storeId: your-store-id,productKinds: [Subscription, Durable]});原生扩展的性能是最好的但开发成本也高。需要懂 C还要处理跨平台兼容问题。如果你的团队有 C 经验或者性能要求特别高这个方案值得投入。只是这条路走起来终究是辛苦一些。方案三处理 Windows Store 应用虚拟化Windows Store 应用运行在虚拟化环境中路径映射需要特殊处理。HagiCode Desktop 用下面的函数来处理这个问题// src/main/windows-store-path-display.tsexport function resolveWindowsStorePackageFamilyName(executablePath: string): string | null {const WINDOWS_APPS_SEGMENT \\windowsapps\\;const windowsPath executablePath.replace(/\//g, \\);const markerIndex windowsPath.toLowerCase().indexOf(WINDOWS_APPS_SEGMENT);if (markerIndex 0) return null;const relativePath windowsPath.slice(markerIndex WINDOWS_APPS_SEGMENT.length);const packageFullName relativePath.split(\\, 1)[0]?.trim();return packageFullName || null;}export function resolveWindowsStoreVirtualizedPhysicalPath(logicalPath: string,options: ResolveWindowsStorePathDisplayOptions {}): string | null {const packageFamilyName options.packageFamilyName?? resolveWindowsStorePackageFamilyName(options.execPath ?? process.execPath);if (!packageFamilyName) return null;const packageStorageRoot path.win32.join(options.env.LOCALAPPDATA,Packages,packageFamilyName);// 将虚拟化路径映射到物理路径if (isPathWithinWindowsRoot(logicalPath, options.env.APPDATA)) {return path.win32.join(packageStorageRoot,LocalCache,