Zum Inhalt springen

Wie Electron-Desktopanwendungen Microsoft Store-Abonnements und permanente Lizenzen integrieren

Seite bearbeiten
HagiCode for Windows Microsoft Store artwork
HagiCode for Windows is now on Microsoft Store
HagiCode for Windows is officially live on Microsoft Store. Windows users can install it directly from the storefront and stay on the store-managed update path. Open the listing and take a look.
Open Microsoft Store

Wie Electron-Desktopanwendungen Microsoft Store-Abonnements und permanente Lizenzen integrieren

Wenn Ihre Electron-Anwendung Abonnements und permanente Lizenzen über den Microsoft Store verkaufen soll – wie werden die kommerziellen WinRT-APIs sauber in die Geschäftslogik integriert? Das ist eigentlich eine alte Geschichte. Wir sind bei HagiCode Desktop über Stolpersteine gestolpert und haben geschwitzt, aber am Ende haben wir diese Schichtenarchitektur entwickelt. Ich schreibe sie hier auf, um den Nachfolgern einen Wegweiser zu hinterlassen.

Hintergrund

HagiCode Desktop ist eine Electron-Anwendung, die über den Microsoft Store vertrieben wird. Kommerziell gesehen gibt es eigentlich nur zwei Produktarten:一种是 Sponsor Plan(Sponsor-Abonnement, Store-ID 9N0BTGWV23M1),按月或按年续费,就像一段需要不断浇灌的感情;另一种是 TurboEngine(永久授权的 DLC,Store-ID 9NSD809W18Z6),一次性买断,就像那本放在书架上再也不翻开的旧书,但终究是你的。

Das Problem besteht darin, dass die Electron-Laufzeitumgebung selbst nicht direkt auf die kommerziellen APIs des Microsoft Store zugreifen kann. Store-Käufe und Lizenzabfragen sind vollständig von der WinRT-Namespaces Windows.Services.Store abhängig. Diese APIs können nur in nativem Code verwendet werden. Der Electron-Hauptprozess ist jedoch eine Node.js-Umgebung, und Sie können dort keinen WinRT-Typ import – so wie Sie versuchen möchten, das Mondlicht zu greifen, aber die Handfläche immer leer bleibt.

Noch problematischer ist, dass der kommerzielle Status nicht durch eine einzige Abfrage beruhigt werden kann. Benutzer können im Store-Client Abonnements kündigen oder verlängern oder Geräte wechseln, und die Funktionsschalter in der Anwendung müssen sich entsprechend ändern. Wenn jedes Mal darauf gewartet wird, dass der Benutzer selbst auf “Aktualisieren” klickt, ist das Erlebnis natürlich hässlich. Wenn aber zu häufig abgefragt wird, stößt man auf die Rate-Limitierung des Stores. Bei einem Netzauswackeln wird ein ansonsten gültiges Abonnement plötzlich als “nicht abonniert” erkannt, und die Funktionen des zahlenden Benutzers werden deaktiviert – so etwas zu machen, würde man wirklich lachen, um die gefallenen Tränen zu verbergen.

Es gibt noch eine leicht zu übersehende Ecke: Das Verhalten verschiedener Vertriebskanäle ist nicht gleich. Nicht-Store-Versionen (wie die portable Version) haben überhaupt keine Store-Laufzeit, und der Aufruf von StoreContext schlägt direkt fehl. In diesem Fall darf die Anwendung nicht abstürzen und auch nicht so tun, als hätte der Benutzer ein Abonnement; es muss ein klarer “nicht unterstützt”-Status gegeben werden. Schließlich ist vorgeben zu haben, schlimmer als ehrlich zuzugeben, dass man nicht hat.

Aus diesem Grund haben wir eine Schichtenarchitektur entwickelt. Später wurde dieses Schema zu zwei OpenSpec-Vorschlägen von HagiCode: desktop-subscription-entitlements(Persistenz, Standardisierung und Ableitung von Abonnementlizenzen)和 desktop-turboengine-msstore-license(Kauf, Aktualisierung und DLC-Injektion von TurboEngine-Dauerlizenzen)。下面的慢慢说。

Über HagiCode

Das in diesem Artikel vorgestellte Schema stammt aus unserer Praxis im HagiCode-Projekt. HagiCode ist ein KI-Codierassistent-Projekt, das mehrere Endpunkte wie Web, Desktop und CLI umfasst. Die Desktop-Produktlinie HagiCode Desktop ist der Gegenstand dieses Artikels, und der vollständige Quellcode ist unter HagiCode-org/site verfügbar.

Schichtung ist der Schlüssel

Store-Aufrufe direkt im Electron-Hauptprozess zu schreiben, wäre sehr chaotisch. WinRT-Asynchronobjekte, COM-Threadmodelle, Fensterhandle-Übergabe – wenn diese Dinge mit der Geschäftslogik vermischt werden, ist es fast unmöglich zu warten. Unser Ansatz besteht darin, die gesamte Kette in vier Schichten zu unterteilen, wobei jede Schicht nur eine Verantwortung trägt:

Renderprozess (React)
↕ IPC bridge
Electron-Hauptprozess (TypeScript)
↕ broker-Schnittstelle
Native Node-Addon (C++)
↕ WinRT
Windows.Services.Store

Ganz unten befindet sich ein C++- natives Addon namens hagicode_store_purchase_addon.node. Es macht eigentlich nur zwei Methoden verfügbar: requestPurchase(storeId, windowHandle)queryStoreStatus(storeId, productName, productKinds)。Diese entsprechen WinRTs RequestPurchaseAsync und GetAssociatedStoreProductsAsync / GetUserCollectionAsync。Die gesamte Arbeit des Addons besteht lediglich darin, die asynchronen WinRT-Ergebnisse in JSON umzuwandeln und sie über Napi::ThreadSafeFunction an den JavaScript-Thread zurückzusenden.

In der Mitte befindet sich ein TypeScript-StoreLicenseService。Er kümmert sich nicht um WinRT, sondern nur um die Geschäftssemantik: Aktualisierung, Wiederholung, Caching, Rechteableitung, Statusübertragung. Er kommuniziert mit der unteren Ebene über eine StoreLicensePlatformBroker-Schnittstelle, die auch nur drei Methoden hat: queryStatus(), purchase(), dispose()

Oben befinden sich SubscriptionService und TurboEngineLicenseService,die eigentlich nur eine dünne Verkapselung von StoreLicenseService sind, jeweils mit spezifischen Produktkonfigurationen gebunden(Store-ID, Produktname, Rechtemame)。

Eine direkte Folge dieser Schichtung ist, dass Abonnements und Dauerlizenzen dieselbe Engine gemeinsam nutzen können. StoreLicenseService ist eine generische Klasse, die den Snapshot-Typ und den Rechtemamen parametrisiert. Um ein neues Produkt hinzuzufügen, muss nur ein weiteres StoreLicenseProductConfig geschrieben werden, ohne den gesamten Service kopieren und einfügen zu müssen. Wenn HagiCode in Zukunft macOS StoreKit oder andere kommerzielle Kanäle integrieren möchte, müsste theoretisch nur die Broker-Implementierung ausgetauscht werden, und die Geschäftsebene müsste keine Zeile ändern – das ist wahrscheinlich die Sanftheit der Schichtung.

Standardisierung: Die schmutzigen Store-Daten sauber machen

Die von WinRT zurückgegebenen Daten sind sehr “ursprünglich”. In StoreProductQueryResult sind IVectorView und IMap verschachtelt, die CollectionData.EndDate der SKU sind Windows DateTime-Ticks(Startpunkt 1601, Einheit 100 Nanosekunden),und die Fehlercodes sind HRESULTs. Wenn diese Dinge direkt an den Renderprozess weitergegeben werden, würde der Frontend-Code wahrscheinlich zusammenbrechen.

Daher hat die Broker-Ebene eine Standardisierung vorgenommen, bei der die ursprünglichen WinRT-Objekte zu RawStoreLicenseState abgeflacht werden:

export interface RawStoreLicenseState {
fetchedAt: string;
availability: 'supported' | 'store-unavailable' | 'error';
appLicenseActive: boolean;
product: RawStoreLicenseProduct | null;
sku: RawStoreLicenseSku | null;
license: RawStoreLicense | null;
purchaseEligibility: 'licensable' | 'not-licensable' | 'license-action-not-applicable' | 'network-error' | 'server-error' | 'unknown';
errorCode: string | null;
errorMessage: string | null;
}

Hier gibt es ein Detail, das erwähnenswert ist: Die Abfrage verwendet tatsächlich zwei Store-Aufrufe. Einer ist GetAssociatedStoreProductsAsync(Produkte, die mit der aktuellen Anwendung verknüpft sind),und der andere ist GetUserCollectionAsync(Produkte, die der Benutzer bereits besitzt)Der Grund ist einfach: Abonnementprodukte können in der Verknüpfungsliste erscheinen, aber noch nicht gekauft sein, oder sie können bereits in der Benutzersammlung sein. Nur durch den Vergleich der beiden Ergebnisse kann genau beurteilt werden, ob “besitzt” – so wie man aus der Entfernung auf eine Person schaut und aus zwei Winkeln blickt, um nicht falsch zu sehen.

Der Code zur Umrechnung von Ticks in ISO-Datum ist beachtenswert:

const WINDOWS_EPOCH_OFFSET_MILLISECONDS = 11644473600000n;
const HUNDRED_NANOSECONDS_PER_MILLISECOND = 10000n;
// ticks sind 100-Nanosekunden-Einheiten ab 1601, zuerst in Millisekunden umrechnen, dann die Windows/Unix-Epoche-Differenz abziehen
const unixMilliseconds =
ticks / HUNDRED_NANOSECONDS_PER_MILLISECOND - WINDOWS_EPOCH_OFFSET_MILLISECONDS;

11644473600000 ist die Anzahl der Millisekunden zwischen 1601-01-01 und 1970-01-01. Diese Umrechnung wird auch im C++-Addon durchgeführt(mit FileTimeToSystemTime),und die Ergebnisse müssen auf beiden Seiten übereinstimmen; sonst kann es zu einer seltsamen Fehlplatzierung kommen wie “Hauptprozess sieht heute, Addon sieht gestern” – Zeit und Gefühle sind ähnlich: Wenn sie fehlplaziert sind, lässt sich nichts mehr erklären.

Zustandsautomat: Von “ursprünglichen Daten” zu “Geschäftsstatus”

Nach der Standardisierung muss noch eine weitere Abstraktionsebene hinzugefügt werden. Der Geschäftscode muss eigentlich nicht wissen, was purchaseEligibility ist; er kümmert sich nur darum, “ob das Abonnement gültig ist”. Die Funktion deriveStatus in normalize.ts macht genau diese Übersetzung:

function deriveStatus(
raw: RawStoreLicenseState,
productConfig: StoreLicenseProductConfig
): StoreLicenseStatus {
if (raw.availability !== 'supported') {
return 'unknown';
}
const expirationDate =
raw.license?.expirationDate ?? raw.sku?.collectionEndDate ?? null;
const expirationTime = expirationDate ? Date.parse(expirationDate) : Number.NaN;
const hasExpired = Number.isFinite(expirationTime) && expirationTime < Date.now();
const isOwned = Boolean(
raw.license?.isActive ||
raw.sku?.isInUserCollection ||
raw.product?.isInUserCollection
);
if (isOwned && !hasExpired) {
return 'active';
}
if (hasExpired) {
return 'expired';
}
// ...andere Zweige: inactive / canceled / grace-period / pending
}

Der endgültige Geschäftsstatus hat sieben Werte: activeinactiveexpiredcanceledgrace-periodpendingunknown。Der Renderprozess schaut nur auf dieses eine Feld und berührt die ursprünglichen Daten nicht mehr.

Hier gibt es eine Entwurfsentscheidung: Die Bestimmung von active schaut nicht, ob expirationDate existiert. Der Grund ist einfach – Dauerlizenzen(TurboEngine)haben von Hause aus kein Ablaufdatum, und der von Store zurückgegebene license.isActive ist true, was reicht. Wenn man hart “hat Ablaufdatum才算 active” fordert, würde man Kaufnutzer fälschlicherweise als nicht abonniert beurteilen, was zu verletzend wäre. Dieses Detail ist im Spec klar formuliert: Dauerlizenzen bleiben auch ohne Ablaufmetadaten aktiv.

Fehlertoleranz: Verlieren Sie bei schlechtem Netzwerk nicht das Abonnement

Die Store-API gibt bei Netzauswackeln Fehler oder Timeout zurück. Wenn bei jedem Fehler der Status geleert wird, fallen die Berechtigungen zahlender Benutzer häufig ab – das ist im Grunde eine Binsenweisheit, aber es passiert tatsächlich. Die Strategie von HagiCode ist “bei Fehlern den letzten bekannten Status beibehalten und als stale kennzeichnen”.

Innerhalb von StoreLicenseService.refresh gibt es eine Wiederholungsschleife(Standardmäßig 3 Mal, Intervall 350ms),und es wird auch eine “Statusregression”-Erkennung durchgeführt: Wenn der letzte Status active war, diesmal aber nicht active ist, wird es als temporärer Fehler behandelt und wiederholt, anstatt das degradierte Ergebnis direkt zu akzeptieren.

private getRetryReason(
snapshot: TSnapshot,
recoverySnapshot: TSnapshot | null
): 'store-unavailable' | 'status-regression' | null {
if (snapshot.availability !== 'supported') {
return 'store-unavailable';
}
if (recoverySnapshot?.status === 'active' && snapshot.status !== 'active') {
return 'status-regression';
}
return null;
}

Erst wenn alle Wiederholungen fehlschlagen, wird mit createStaleSnapshot der letzte gute Status als stale gekennzeichnet und zurückgegeben, zusammen mit einer Diagnose von store-refresh-failed。Der Renderprozess kann selbst entscheiden, ob er Funktionen im stale-Status deaktivieren soll – üblicherweise wird er weiter durchgelassen und dem Benutzer ein Puffer gegeben. Schließlich möchte niemand an einem Tag mit schlechtem Netzwerk nicht einmal die Dinge nutzen können, für die er bezahlt hat.

Ein weiteres Detail ist die Deduplizierung von refreshInFlight。Wenn eine Aktualisierung bereits läuft, wird der neue refresh-Aufruf dasselbe Promise wiederverwenden, um zu vermeiden, dass gleichzeitige Anfragen den Store überlasten – das Prinzip ist wie das Anstehen: Wenn sich alle zusammendrängen, kommt niemand durch.

Rechteableitung: Status und Funktionsschalter entkoppeln

Der Abonnementstatus antwortet auf “ob das Abonnement gültig ist”, aber Funktionsschalter kümmern sich darum, “ob der Benutzer eine bestimmte Funktion nutzen kann”. Diese beiden entsprechen nicht unbedingt eins zu eins. Ein aktives Abonnement kann mehreren Rechten entsprechen(Sponsor-Abzeichen, Premium-Funktionsschalter),und in Zukunft könnte es nach Stufen unterschieden werden.

Daher gibt es eine weitere Schicht EntitlementEvaluator

evaluate(snapshot: TSnapshot): TEntitlement[] {
if (snapshot.availability !== 'supported' || snapshot.status !== 'active') {
return [];
}
return [...this.activeEntitlements];
}

In der Konfiguration des Abonnementprodukts wird deklariert, welche Rechte es bei Aktivierung gewährt:

export const subscriptionEntitlementNames = [
'sponsorBadge',
'premiumFeatureGate',
] as const;

Auf diese Weise hängt der Funktionscode nur vom Array entitlements ab und liest nicht mehr direkt status。Wenn Sie später Stufen hinzufügen oder Rechte aufteilen, müssen Sie nur die Konfiguration und den Evaluator ändern, ohne die Verbraucher ändern zu müssen. Diese Entkopplung ist besonders wichtig in Projekten wie HagiCode mit mehreren Produktlinien – Abonnements und Dauerlizenzen teilen sich dasselbe Rechtemodell, und das Frontend muss nur ein Array abfragen, und die Welt wird auf einmal viel klarer.

Laufzeitdegradierung: Was tun ohne Store

Nicht-Store-verteilte Versionen(portable Version, Entwicklungsumgebung)können das Addon nicht aufrufen, und es wird fehlschlagen. HagiCode hat mit MicrosoftStoreSubscriptionBroker eine verzögerte Initialisierung und Degradierung durchgeführt:

private async initializeBroker(): Promise<StoreLicensePlatformBroker> {
try {
return this.setBroker(
await this.adapterFactory(this.windowHandle, this.productConfig)
);
} catch (error) {
// Wenn keine Store-Laufzeit gefunden wird, auf einen "nichts unterstützt"-Broker degradieren
return this.setBroker(new UnavailableSubscriptionPlatformBroker(error));
}
}

UnavailableSubscriptionPlatformBroker implementiert dieselbe Schnittstelle, nur dass sein queryStatus immer store-unavailable zurückgibt und purchase immer not-supported。Der obige Code ist völlig unempfindlich, nur der Status wird zu “nicht unterstützt”, und der Renderprozess zeigt basierend darauf einen Hinweis “Bitte über Microsoft Store erwerben” an.

Dieses Design ermöglicht es, dass das gesamte kommerzielle Modul in jedem Vertriebskanal sicher ausgeführt werden kann, ohne aufgrund fehlender Store-Laufzeit abzustürzen. Wenn Sie auch eine Electron-Anwendung mit mehreren Vertriebskanälen entwickeln, ist dieser Punkt besonders值得抄一份——lassen Sie “Umgebung nicht unterstützt” nicht zu einem Absturz werden; schließlich ist es manches Mal würdevoller, zuzugeben, als vorzugeben.

Startprozess und IPC-Kanäle

Beim Start der Anwendung entscheidet main.ts anhand des Parameters --desktop-subscription-enabled=1,ob der Abonnementdienst initialisiert werden soll. Dieser Parameter wird nur im Startbefehl der Store-Version übergeben, um zu vermeiden, dass nicht-Store-Versionen ihn unnötig laden – was an Kraft gespart werden kann, sollte gespart werden.

function initializeSubscriptionService(): void {
if (!subscriptionFeatureEnabled || subscriptionService) {
return;
}
subscriptionService = new SubscriptionService({
broker: new MicrosoftStoreSubscriptionBroker({
windowHandle: mainWindow?.getNativeWindowHandle() ?? null,
}),
entitlementEvaluator: new EntitlementEvaluator(),
});
registerSubscriptionHandlers({
subscriptionService,
getWindows: () => ElectronBrowserWindow.getAllWindows(),
});
}

windowHandle stammt von mainWindow.getNativeWindowHandle(),dieser Buffer wird als bigint analysiert und an das native Addon übergeben, das Addon nimmt es dann, um IInitializeWithWindow::Initialize aufzurufen. Dies ist ein notwendiger Schritt für die Store-API, um ein Kaufdialogfeld in Desktopanwendungen(nicht UWP)aufzurufen; andernfalls hat das Kauffenster keinen Besitzer, und das Verhalten ist abnormal – wenn ein Mensch keine Zugehörigkeit hat, ist sein Handeln insgesamt unstet, und Fenster auch.

Der Renderprozess ruft den Hauptprozess über den durch preload freigelegten Bridge auf:

const subscriptionBridge: SubscriptionBridge = {
getSnapshot: (options) => ipcRenderer.invoke(subscriptionChannels.getSnapshot, options),
verifyStartup: () => ipcRenderer.invoke(subscriptionChannels.verifyStartup),
refresh: () => ipcRenderer.invoke(subscriptionChannels.refresh),
purchase: () => ipcRenderer.invoke(subscriptionChannels.purchase),
onDidChange: (callback) => {
const listener = (_event, snapshot) => callback(snapshot);
ipcRenderer.on(subscriptionChannels.changed, listener);
return () => ipcRenderer.removeListener(subscriptionChannels.changed, listener);
},
};

Statusänderungen werden über broadcastSnapshotChanged an alle Fenster übertragen. Nach Abschluss des Kaufs löst completePurchase eine refresh('purchase') aus, und der neue Status wird automatisch übertragen, sodass die Abonnement-Benutzeroberfläche des Renderprozesses in Echtzeit aktualisiert wird.

Außerdem gibt es in main.ts ein setInterval,das im Hintergrund still synchronisiert(subscriptionService?.refresh('scheduled'))。Dies ermöglicht es, während die Anwendung geöffnet ist, Veränderungen zu erkennen, die der Benutzer im Store-Client heimlich vornimmt(Verlängerung, Kündigung)。Die Frequenz darf natürlich nicht zu hoch sein(der Store ist begrenzt),im Code wird ein minutenbasierter Intervall verwendet – nicht zu fern, nicht zu nah, genau richtig.

Einige leicht zu übersehende Fallstricke

Erstens: Thread-Sicherheit des nativen Addons. Nachdem die asynchronen WinRT-Operationen abgeschlossen sind, befindet sich der Rückruf nicht auf dem JavaScript-Thread. Wenn Sie im Rückruf direkt die Napi-API aufrufen, stürzt es ab. Das Addon verwendet Napi::ThreadSafeFunction::BlockingCall,um die Ergebnisse an den JS-Thread zu senden:

auto const status = threadsafeFunction_.BlockingCall(
payload,
[self](Napi::Env env, Napi::Function, PurchaseCompletion* data) {
std::unique_ptr<PurchaseCompletion> ownedData{ data };
self->ResolveOnJs(env, *ownedData);
});

BlockingCall blockiert den WinRT-Rückruf-Thread, bis der JS-Thread fertig verarbeitet hat. In diesem Modus darf der Rückruf-Thread nicht der JS-Thread selbst sein, sonst ist es ein Deadlock. Zum Glück ist der WinRT-Completed-Rückruf normalerweise auf STA oder Threadpool, was diese Bedingung erfüllt.

Zweitens: COM-Initialisierung. Der Electron-Hauptthread hat möglicherweise bereits COM initialisiert. Im Addon ist winrt::init_apartment von einer try-catch-Schicht umgeben, bei Fehlern wird ignoriert:

try {
winrt::init_apartment(winrt::apartment_type::single_threaded);
} catch (...) {
// Electron hat möglicherweise bereits COM für diesen Thread initialisiert, ignorieren
}

Ohne dies zu behandeln, wird die wiederholte Initialisierung eine Ausnahme auslösen, und das Laden des Addons schlägt fehl. Einige Fehler sind besser ignoriert.

Drittens: Genauigkeit des Fensterhandles. getNativeWindowHandle() gibt einen Buffer zurück, dessen Länge 4(32 位)oder 8(64 位)sein kann. Dann wird er im Addon als hexadezimale Zeichenfolge mit 0x-Präfix formatiert, und auf der C++-Seite wird er mit std::stoull wieder zu HWND analysiert. Warum eine Zeichenfolge statt einer direkten Zahl? Weil die JS-Zahlgenauigkeit nur 53 Bit beträgt und 64-Bit-Zeiger an Genauigkeit verlieren. Dieser Fallstrick ist schwer zu entdecken, wenn man ihn nicht einmal berührt – wie einige Dinge, die man nicht erklären kann, wenn man sie nicht einmal erlebt.

Viertens: Statusisolation. Die Status von Abonnements und Dauerlizenzen müssen getrennt gespeichert werden. Der Spec von HagiCode erfordert ausdrücklich, dass die Persistenz von TurboEngine den Sponsor-Status nicht überschreiben darf. Die beiden Snapshots sind durch unterschiedliche productKeysubscription und turboengine)getrennt, um zu vermeiden, dass die Aktualisierung eines Produkts den Cache eines anderen Produkts überschreibt. Wenn jeder seine eigenen Dinge verwaltet, ist die Welt friedlich.

Fünftens: Nach dem Kauf muss aktualisiert werden. Nach Abschluss des Kaufs muss einmal aktualisiert werden, um zu übertragen. In completePurchase lösen sowohl die Fälle succeeded als auch already-purchased refresh('purchase') aus, weil das Kaufergebnis des Stores nur den Transaktionsstatus mitteilt, aber nicht die Details der aktuellen Lizenz. Der Lizenzstatus muss erneut abgefragt werden – zwischen Versprechen und Realität liegt immer noch eine Bestätigung.

Zusammenfassung

Diese Implementierung läuft schon eine Weile und ist insgesamt ziemlich stabil. Das wirklich übernahme Würdigste ist nicht irgendein kleiner Trick, sondern diese Schichtungsweise: die schmutzige Arbeit “mit dem Store sprechen” vollständig in Broker und Addon zu isolieren, und die oberen Schichten kümmern sich nur um reine Geschäftssemantik.

Einige Kernerfahrungen hier aufgezeichnet:

  • WinRT nur im C++-Addon berühren, das Addon macht nur “asynchron zu JSON”, Geschäftssemantik gar nicht.
  • Standardisierung und Zustandsautomat in zwei Schichten, ursprüngliche Daten und Geschäftsstatus nicht vermischen.
  • Bei Netzwerkfehlern den letzten guten Status behalten und als stale kennzeichnen, die Berechtigungen zahlender Benutzer nicht löschen.
  • Rechte und Status entkoppeln, Funktionscode schaut nur auf das entitlements-Array.
  • Nicht-Store-Umgebung verwendet Degradierungs-Broker, niemals “nicht unterstützt” zu einem Absturz werden lassen.

Wenn Sie auch Store-Kommerzialisierung für Electron-Anwendungen machen, hoffe ich, dass diese Schichtung Ihnen einige Fallstricke erspart.

Das in diesem Artikel vorgestellte Schema ist genau das, was wir im Entwicklungsprozess von HagiCode tatsächlich über Fallstricke gestolpert und tatsächlich optimiert haben. Wenn Sie denken, dass es noch etwas wert hat, bedeutet das, dass unsere Ingenieurskompetenz noch akzeptabel ist –这样一来,HagiCode selbst ist auch einen zweiten Blick wert……

Referenzen

开始使用 HagiCode

一次安装,几分钟上手

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