跳转到内容

Electron 如何调用 Windows 原生 API

编辑此页
HagiCode for Windows Microsoft Store artwork
HagiCode for Windows 已上线 Microsoft Store
HagiCode for Windows 现已正式登陆 Microsoft Store,Windows 用户可以直接从商店完成安装并接收渠道更新。现在就打开商店页看看吧。
打开 Microsoft Store

Electron 如何调用 Windows 原生 API

在 Electron 应用里调用 Windows 原生 API,就像想看海却只能看地图。不过折腾了一阵,总算摸索出几条路,写下这篇文章算是留个纪念,也给后来者指个方向。

背景

做 Electron 桌面应用的时候,难免要和操作系统打打交道。在 Windows 上,这些需求说起来也不少:

  • 调用 Microsoft Store API 搞应用内购买
  • 处理 Microsoft 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 来访问 Microsoft Store API,使用原生 Node.js 扩展来处理高性能的 Store 购买操作,同时用 Node.js 原生 fs 和 path 模块处理 Microsoft Store 应用特有的文件系统虚拟化。能简单就简单,这也是我们的原则。

方案一:使用 dynwinrt 调用 WinRT API

dynwinrt 是 Microsoft 提供的一个工具链,可以基于 Windows SDK 的 metadata 文件自动生成 JavaScript 绑定。它专门用于调用 WinRT API,比如 Microsoft Store API。

安装依赖:

{
"optionalDependencies": {
"@microsoft/dynwinrt": "0.1.0-preview.6",
"@microsoft/dynwinrt-codegen": "0.1.0-preview.6"
}
}

生成 WinRT 绑定:

scripts/generate-store-bindings.js
const { 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.h>
using namespace v8;
using namespace Windows::Services::Store;
NAN_METHOD(QueryStoreStatus) {
auto async = new Nan::AsyncWorker(
[]() {
// 调用 Microsoft Store API
auto 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::New<FunctionTemplate>(QueryStoreStatus)).ToLocalChecked());
}
NODE_MODULE(windows_store_addon, InitModule)

编译和使用:

Terminal window
node-gyp rebuild
import addon from './build/Release/windows-store-addon.node';
const result = addon.queryStoreStatus({
storeId: 'your-store-id',
productKinds: ['Subscription', 'Durable']
});

原生扩展的性能是最好的,但开发成本也高。需要懂 C++,还要处理跨平台兼容问题。如果你的团队有 C++ 经验,或者性能要求特别高,这个方案值得投入。只是这条路走起来,终究是辛苦一些。

方案三:处理 Microsoft Store 应用虚拟化

Microsoft Store 应用运行在虚拟化环境中,路径映射需要特殊处理。HagiCode Desktop 用下面的函数来处理这个问题:

src/main/windows-store-path-display.ts
export 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',
'Roaming',
path.win32.relative(options.env.APPDATA, logicalPath)
);
}
return null;
}

虚拟化这东西,说起来挺复杂的。简单理解就是,Microsoft Store 应用看到的文件路径和实际存储位置不一样,需要做一个翻译。上面的代码就是在做这个翻译工作。就像记忆和现实,有时候也不重合,需要一点耐心去分辨。

实践经验

平台检测

始终检查 process.platform === 'win32',避免在非 Windows 平台执行 Windows 特定代码。这是一个好习惯,就像出门前看看天气一样,免得淋了雨还要怪天气不好。

if (process.platform !== 'win32') {
return { availability: 'not-supported' };
}

错误处理

Windows API 调用可能失败,需要妥善处理错误。这个坑我们踩过,没有完善的错误处理,用户遇到问题时根本不知道发生了什么。其实代码写多了就知道,错误处理不是为了别的,只是为了让自己少点麻烦。

function normalizeThrownError(error: unknown): { errorCode: string | null; errorMessage: string | null } {
if (error instanceof Error) {
const errorWithCode = error as Error & { code?: unknown };
return {
errorCode: normalizeErrorCode(errorWithCode.code) ?? error.name,
errorMessage: error.message,
};
}
return { errorCode: null, errorMessage: error == null ? null : String(error) };
}

异步处理

Microsoft Store API 大部分是异步的,使用 Promise 或 async/await。写异步代码的时候,记得处理好边界情况,比如超时、取消什么的。毕竟等待的滋味,谁都不想多尝。

async function queryStatus(): Promise<RawStoreLicenseState> {
try {
const result = await storeContext.getAssociatedStoreProductsAsync(productKinds);
return buildSupportedStateFromProductQueries(result);
} catch (error) {
return buildUnavailableState(error);
}
}

资源清理

确保在不需要时释放原生资源。C++ 资源不会自动回收,手动释放是个好习惯。就像有些东西,放下了才能轻装上阵。

class MicrosoftStoreSubscriptionBroker {
private broker: StoreLicensePlatformBroker | null = null;
dispose(): void {
this.broker?.dispose();
this.broker = null;
}
}

时间戳转换

Windows 使用 1601-01-01 作为纪元,需要转换到 Unix 时间戳。这个细节很容易被忽略,但如果处理不对,日期就会全错。时间这东西,差一点就差很多。

const WINDOWS_EPOCH_OFFSET_MILLISECONDS = 11644473600000n;
const HUNDRED_NANOSECONDS_PER_MILLISECOND = 10000n;
function toIsoDate(value: unknown): string | null {
const universalTime = (value as { universalTime?: unknown } | null)?.universalTime;
const ticks = typeof universalTime === 'bigint' ? universalTime : null;
if (ticks == null) return null;
const unixMilliseconds = ticks / HUNDRED_NANOSECONDS_PER_MILLISECOND - WINDOWS_EPOCH_OFFSET_MILLISECONDS;
return new Date(Number(unixMilliseconds)).toISOString();
}

最佳实践

根据我们在 HagiCode 项目中的经验,这里有几条建议:

  • 优先使用 dynwinrt:对于 WinRT API,dynwinrt 提供了类型安全和现代化的 JavaScript 绑定
  • 最小化原生扩展:只在确实需要高性能或 dynwinrt 不支持的功能时使用原生扩展
  • 跨平台兼容:使用条件编译或运行时检测来处理不同平台
  • 测试覆盖:在 Windows 上充分测试原生 API 调用,包括错误场景
  • 文档记录:清晰记录每个原生 API 调用的用途和可能的副作用

写代码的时候,能简单就不复杂。如果 dynwinrt 能解决问题,就不要去写 C++ 扩展。维护成本会少很多。这也是一点小心得,也不算什么高深的道理。

总结

调用 Windows 原生 API 是 Electron 应用在 Windows 平台上实现高级功能的重要手段。本文分享了 HagiCode Desktop 项目中使用的几种技术方案:dynwinrt 用于 WinRT API、原生 Node.js 扩展用于高性能场景、虚拟化路径处理用于 Store 应用文件访问。

选择哪种方案,取决于你的具体需求。如果只是调用 WinRT API,dynwinrt 是最简单的选择。如果需要高性能或者传统 Win32 API,原生扩展是必须的。临时性的操作,用 child_process 调用 PowerShell 也可以。条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点罢了。

不管用哪种方案,记住这些原则:做好平台检测、完善错误处理、处理好异步、及时清理资源。这些细节决定了代码的健壮程度。代码写久了就会明白,细节往往比大框架更重要。

如果你也在做类似的开发,希望这些经验能帮到你。技术这东西,踩过的坑多了,自然就有经验了。就像人生,跌得多了,也就学会怎么走路了…

参考资料

总结

围绕“Electron 如何调用 Windows 原生 API”,更稳妥的推进方式是先把关键配置、依赖边界和落地路径逐步跑通,再补齐优化细节。

当目标、步骤和验收点都明确之后,这类方案通常就能更顺畅地进入实际交付。

开始使用 HagiCode

一次安装,几分钟上手

HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。