跳至内容
Micro Frontend

设置用户和身份验证

微前端将票务商店本地渲染到您的页面中。用户是谁来自您的后端 - 主机页面负责对用户进行身份验证。会话token是强制性的:没有会话token,微前端将保持不活动状态。本页解释了原因以及服务器到服务器的会话流程。

微前端背后的技术

ADITUS 售票处,编织而不是传送。微前端将完整的购买流程本地渲染到您的页面中 - 没有 iframe,没有视觉异物。它的构建精益且主机原生,因此它看起来和感觉就像您自己的网站,而不是嵌入式小部件。

React 19 — 兼容到 React 18

完整的流程——事件、文章、购物车、注册、付款、完成——作为单个、完全类型安全的 React 流程运行。我们自己已经在 React 19 上运行,但故意保持对 React 18 的兼容性开放(peerDependency React >=18),以便微前端嵌入到尽可能多的主机页面中。该对等范围的字面意思是:适配器在内部仅使用 React 18 功能集(useState、useEffect、useMemo 等),并且不使用 React-19 专有的 API — 在 React 19 下构建和测试,保证在 React 18 下运行。React 只是一个 PeerDependency:适配器本身带来零运行时依赖项。没有 UI 库,没有数据获取框架,没有状态工具包——只有精心挑选的 React hooks。结果是一个很小的包,没有任何东西可以拖到你的页面上。

轻量 DOM 而不是 iframe

没有 iframe,没有 Shadow DOM,没有异物。该商店直接呈现到您页面的 DOM 中:它继承您的排版、适合您的布局、完全响应且可访问 - 并作为您网站的本机部分读取给您的访问者。

与框架无关:一个 mount(),无处不在

一次调用 — mount(container, config) — 将商店附加到任何 DOM 元素。无论是 React、Vue、Angular 还是纯 HTML:适配器都是框架中立的,并且在任何地方都以相同的方式运行。

造型是你的

每个元素都带有一个稳定的 CSS 钩子(BEM 类 aditus-shop__* 加上数据状态)。您可以通过自己的 CSS 自由设计样式,或者使用一些 --aditus-* 设计标记立即设置您的品牌颜色、半径和间距。可以选择通过 CSS @layer 使用完全隔离的基本样式,这些样式永远不会覆盖您的页面。

多语言、可切换的直播

DE/EN 是内置的。语言切换通过 handle.update() 直接推送到正在运行的实例中 - 无需重置用户的旅程。购物车和进度被保留。

设计安全

如果没有有效的会话token,适配器将处于非活动状态 - 然后它不会发出任何请求。会话是在服务器到服务器之间使用秘密创建的;没有key到达浏览器。所有调用都通过在服务器端注入凭据的代理运行。

主机环境要求

在集成层,微前端有意实现最大程度的兼容——任何框架、任何主机 CSS、React 18+。不可避免的固定点很少且无害:浏览器中的客户端、React 存在以及具有白名单公钥的会话。

需要什么

  • 主机包中的 React 18+ 和 React-dom 18+ - 声明为 PeerDependency React >=18; mount() 使用react-dom/client 中的createRoot。主持人决定React版本。
  • 具有 DOM(客户端)的浏览器。适配器安装到真实的 DOM 元素(轻量 DOM)中。只要 mount() 在客户端上运行,SSR 主机就可以。
  • 当前的常青浏览器 — 使用标准 Web API(fetch、AbortController、ResizeObserver、CSS @layer),基线自 2022 年左右开始。没有IE。
  • 会话加公钥 — 如果没有 sessionToken,微前端将保持不活动状态。使用key(具有后端的主机)或从浏览器通过域门控 /shop/embed-session(无后端主机)创建服务器到服务器。该域名必须在 publicKey 的白名单中。

我们故意不要求的

  • 没有特定的框架 - mount(container, config) 是框架中立的:React、Vue、Angular、纯 HTML。
  • 没有 iframe,没有 Shadow DOM——它会本地渲染到您的页面中。
  • 没有特定的主机 CSS — 基本样式存在于 @layer aditus-shop 中(任何未分层的主机规则获胜),具有单类特异性,加上针对主机的重置装甲 * { margin:0;填充:0 }。可以通过 baseStyles: false 完全关闭。
  • 没有运行时依赖项——只有 React peerDeps,主机包中没有任何冲突。
  • 除了 ESM 导入之外,没有特定的构建设置。

支付安全、PCI 和 CSP

传统上,集成票务商店等第三方组件会迫使用户在体验、性能和 IT 安全性之间进行权衡。微前端解决了与混合范式的冲突:交互流在页面的本机轻 DOM 中呈现,而支付步骤保持严格隔离 - 一切无害的事情都发生在您的页面中,支付本身永远不会发生。

light DOM 中没有支付数据

完整的购买准备工作——活动、门票、附加组件、购物车、注册——在您的页面中本地呈现。当用户开始重定向支付时,敏感流程将完全离开您的页面:正常的顶级导航移交给支付提供商专用的外部支付页面,回程将恢复旅程并完成订单。卡或银行数据仅在此处输入 - 微前端绝不会在您的页面中呈现、传输或存储支付凭证。

最小 PCI 范围

由于您的主机应用程序中不会处理、传输或存储任何信用卡或银行数据,因此您的环境停留在经典重定向模型的最小 PCI-DSS 范围内(通常为 SAQ A — 绑定分类始终由您的收单机构或 QSA 进行)。没有重定向的支付方式,例如发票,完全服务器端 - 浏览器中根本没有支付数据。

精益内容安全策略

除了现有策略之外,微前端本身只需要两个限额:用于 ADITUS API 源的 connect-src(每个商店调用都会经过它)和用于 ADITUS 资产 URL 的 img-src(如果您显示活动横幅或文章图像)。特别是,它不需要支付提供商的 script-src 或 frame-src 条目,因为支付步骤是一个普通的顶级重定向:没有支付脚本,也没有支付框架加载到您的页面内。

两全其美

通常,您必须选择:iframe(孤立的,但刚性的和视觉异物)或完全集成的 API 构建(无缝,但您的安全团队突然拥有完整的 PCI 范围)。微前端解决了这个困境:无害的交互——浏览、购物车、票证配置——原生地存在于你的页面中;关键的互动——收钱——被严格切断并与外部隔离。

它与市场替代品有何不同

与经典的 iframe 对比

由于严格的浏览器隔离,iframe 被认为是安全的,但它们的行为就像孤立的文档:它们既不继承全局样式(例如版式或响应式布局规则),也不能为爬虫可靠地索引其内容;对于屏幕阅读器来说,它们通常意味着媒体中断。微前端将整个购买准备过程(文章选择、购物车、注册)作为真实的 HTML 直接渲染到页面的 DOM 中,使其像您自己的标记一样可访问且响应迅速。 iframe 的隔离效果仅在付款时重新创建:关键数据流通过顶级重定向完全移交给外部专用付款页面。

灵活性与纯 Shadow DOM

许多 Web 组件将自身封装在影子 DOM 中,以避免 CSS 特异性冲突——实际上,这通常会导致品牌不完整,因为全局样式被阻止,并且必须通过部件属性或自定义属性费力地传递设计规则。微前端使用原生 CSS 级联:它的基本样式位于 @layer aditus-shop 中,这是一个故意较低的层 - 页面的每个非分层 CSS 规则都会自动获胜。您的企业设计是原生继承的,不会与商店的 UI 组件发生特殊性冲突。

资源效率和捆绑包大小

单体小部件经常提供自己的 HTTP 客户端、状态管理和 UI 库,并且可以强制浏览器并行加载冗余运行时,从而损害页面的加载时间(Core Web Vitals)。微前端将 React 和 React-DOM 专门声明为peerDependency,并且其自身的运行时依赖性为零:如果您的页面已经使用 React 18 或更高版本,它会直接挂钩到现有的运行时。对于非 React 环境,可选的 Web 组件将 React 依赖项封装在其自己的隔离包中,而不占用页面上的全局变量。

方法比较

标准经典 iframeShadow-DOM 小部件ADITUS 微前端
搜索引擎优化和可访问性内容存在于单独的文档中——爬虫通常看不到,而屏幕阅读器则更难看到。孤立;可发现性和可访问性通常需要额外的工作。DOM 的本机部分——可索引且可访问,就像您自己的标记一样。
造型与品牌刚性:没有 CSS 继承,只能通过 postMessage API 进行定制。全局样式无法到达内部;仅通过传递的属性和部分进行主题化。通过 @layer 定义你的 CSS 获胜;基础皮肤可覆盖或完全关闭。
捆绑与性能通常会加载完整的第二个应用程序,包括其在框架内的运行时。通常会提供自己的运行时和重复的依赖项。使用页面的 React;其自身的运行时依赖性为零。
支付安全和 PCI 范围高度隔离——但代价是用户体验、设计和响应能力。如果在主机 DOM 中捕获支付数据,则该数据将流经主机 DOM。轻DOM中选择,支付与外部支付页面完全隔离。

如何获取微前端

微前端不在公共 npm 上,也不在 CDN 上。它直接从您的 ADITUS 实例作为源包提供:TypeScript、ESM、完全类型化,其自身具有零运行时依赖性 - 您的捆绑程序(Vite、webpack、Next.js、Nuxt)将其与您的应用程序一起编译,就像您自己的代码一样。此页面片段中的导入名称 (@workspace/aditus-shop-embed) 是我们参考集成中的包名称;可下载的包名为@aditus/shop-embed — API 是相同的。

交货内容

完整的适配器作为可读的 TypeScript 源,带有每个配置选项、回调和事件的类型声明。 React 和 React-dom 保持对等依赖关系——你的页面提供了它们,所以你的包中不会发生任何冲突,也没有第二个 React。

获取包裹

每个主机的软件包都是相同的,并直接从此实例下载,无需凭据:{{API_BASE_URL}}/api/embed/v1/aditus-shop-embed.tgz。直接从该 URL 安装它(npm install 接受 tarball URL - 使用实例上的链接目标,路径 /api/embed/v1/aditus-shop-embed.tgz)或将其解压到您的存储库中。入职实际上提供的是您的公钥及其域白名单;如果没有它,微前端将保持不活动状态。联系方式:联系页面。没有自己的构建步骤(WordPress、纯 HTML)的主机根本不需要源包:它们使用下一节中描述的自托管 Web 组件包。

Web 组件:无需构建步骤

对于没有自己的捆绑器的页面——WordPress、Typo3、纯 HTML——微前端也作为预构建的、自托管的捆绑包提供:一个脚本标签注册 <aditus-shop> 元素,包括 React,没有 npm,没有构建。它是从版本化 URL (/api/embed/v1/…) 下的 ADITUS 实例交付的,而不是从公共 CDN 交付的 - 对于此演示系统,该演示系统为 https://developers.aditus.com/api/embed/v1/aditus-shop.js。 URL 是故意公开的,不需要凭据:捆绑包是纯发布的代码,实际的门是您的可发布key及其域白名单和会话。此路径是严格可选的并且纯粹是附加的:具有自己构建的主机通过 mount() 完全按照本页上记录的方式不断集成源包 - 相同的旅程,相同的 CSS 挂钩,相同的主题。

index.html
<!-- 1. Das selbst gehostete Bundle einbinden (einmal pro Seite). -->
<script src="https://ihre-aditus-instanz.de/api/embed/v1/aditus-shop.js" defer></script>

<!-- 2. Das Element platzieren — der Shop rendert nativ an dieser Stelle. -->
<aditus-shop
  public-key="pk_live_ihrkey"
  culture="de-DE"
></aditus-shop>

<!-- 3. Optional: Journey-Events als ganz normale DOM-Events konsumieren. -->
<script>
  document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
    console.log("Positionen im Warenkorb:", e.detail.itemCount);
  });
</script>
属性意义
public-key您的可发布key(域白名单适用)。仅设置此属性后,元素就会创建自己的浏览器会话——这对于没有后端的页面来说是正确的模式。
session-token由您的后端创建的会话(服务器到服务器,见下文)。如果设置,该元素永远不会自行创建 - 您的页面拥有会话生命周期。
api-baseADITUS API 的基本 URL。默认为加载捆绑脚本的来源 - 通常您从不设置它。
culture商店语言/区域设置,例如de-DE 或 en-GB。更改属性会重新定位 - 用户的旅程被保留。
article-layout / event-layout / article-select-mode / show-header平面呈现选项,与 mount() 配置相同的值(卡片、数量……)。入口点(事件选择器与固定事件)不是一个属性:它在创建会话时在服务器端固定。
survey-columnsRadioButtonList/CheckBoxList 调查问题答案选项的列数(1–4)——与 mount() 配置中的 surveyColumns 相同。未设置时保持单列默认值(或服务器端存储的客户端默认值);窄视口会折叠回单列。
base-styles设置为“false”可删除中性默认皮肤并自行设计每个钩子的样式。

会话和自我修复

本页面的会话规则保持不变。通过后端,在服务器到服务器之间创建并设置会话token——该元素永远不会自行创建。如果没有后端,则仅设置公钥:该元素通过域门控端点创建浏览器会话,并在过期时透明地重新创建,因此用户永远不会陷入死胡同。一如既往的 Light DOM:没有 iframe,没有 Shadow DOM——你的 CSS 到达每一个记录的钩子。

事件和版本控制

事件总线在元素上以普通 DOM CustomEvents 的形式呈现(冒泡,event.detail 中的有效负载)—— cart:update、checkout:complete 和分析镜像,与handle.on() 相同的名称和有效负载。 URL 携带主要版本:v1 就地接收兼容更新;重大更改以 /embed/v2/ 形式发布,因此您的页面下不会有任何未经通知的更改。除了平面属性之外的所有内容(主题对象、onUserRequired 等回调、卡片内容)在设计上仍然是源包功能。

属性,而不是属性

该元素仅通过 HTML 属性进行配置 - 并且属性始终是字符串。 base-styles="false" 是文字字符串“false”;元素会解析它,你永远不会分配 JavaScript 属性。框架无论如何都会将带有连字符的名称(如公钥)绑定为属性,因此不需要特殊的绑定语法。任何无法用平面字符串表示的内容(主题对象、回调、卡片内容)都故意不是属性:为此,请使用源包。

刻意重置旅程

“仅属性”的一个例外:该元素公开了一个clearJourney() 方法——源包的clearJourney() 导出的无构建版本。在重新添加或重新加载元素之前调用 document.querySelector("aditus-shop").clearJourney() (例如,从“重新开始”按钮),并且当前页面的本地持久旅程记录将被删除 - 下一次挂载将在入口点重新开始,而不是继续。仅移除本地恢复指针;任何服务器端购物车状态均保持不变。

无需重新安装即可更新

更改活动元素上的属性会更新正在运行的实例 - 与源包中的handle.update() 完全相同。呈现属性(文化、布局)保留用户的旅程;仅更改会话身份(公钥、会话token、api-base)即可重新建立会话。从 DOM 中删除元素会彻底卸载商店,包括所有侦听器。

SSR 主机:Next.js 和 Nuxt

微前端特意是仅限客户端的。它的内容是实时的并且受会话限制——可用性、价格和用户购物车仅在请求时经过身份验证的会话中存在,因此主机服务器可以预渲染没有任何有意义的内容。 mount() 创建一个新的客户端 React 根 (createRoot);容器内服务器渲染的标记未水化。这使得 SSR 集成变得简单且可预测:在服务器上渲染占位符,安装在客户端上,并保留空间,这样就不会发生任何跳跃。

ticketshop-section.tsx
// Next.js (App Router) — das Micro-Frontend ist bewusst client-only.
"use client";
import { useEffect, useRef } from "react";
import { mount, type ShopHandle } from "@workspace/aditus-shop-embed";

export function TicketshopSection({ sessionToken }: { sessionToken: string }) {
  const el = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!el.current) return;
    const handle: ShopHandle = mount(el.current, {
      publicKey: "pk_deine_seite",
      sessionToken, // server-seitig gemintet, als Prop an die Client-Komponente
    });
    return () => handle(); // Unmount beim Routenwechsel
  }, [sessionToken]);

  // Server und Client rendern DENSELBEN Platzhalter — kein Hydration-Mismatch.
  // min-height reserviert den Platz, damit die Seite beim Mount nicht springt (CLS).
  return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}

避免布局偏移 (CLS)

为安装容器指定一个与第一个商店视图大致匹配的最小高度,并可选择在其中渲染您自己的骨架 - 服务器和客户端生成相同的占位符,因此不会出现水合作用不匹配的情况。适配器在安装后立即渲染并替换一种绘制中的占位符。不要尝试在容器上使用 HydroRoot:没有服务器渲染的商店树可以附加。

SSR 主机中的会话

SSR 主机有一个天然的优势:呈现页面的服务器运行时也可以使用key创建会话 - 服务器到服务器,与下面的创建流程完全相同(Next.js:路由处理程序或服务器组件;Nuxt:服务器路由)。将生成的不透明token作为 prop 或有效负载传递给客户端组件。该秘密永远不会出现在客户端代码中,并且token保留在内存中。

Next.js 细节

将安装组件标记为“use client”并安装在 useEffect 中(参见代码片段)。对于页面路由器,使用 ssr: false 的 next/dynamic 可以实现相同的效果。从效果中返回句柄作为清理,以便客户端路由更改干净地卸载。正在运行的实例上的文化切换会通过handle.update()进行,而无需重置旅程;一个新的会话token重新安装(如代码片段中所示)——购物车也能幸存下来,因为微前端恢复了安装过程。

Nuxt / Vue 细节

将挂载目标包装在 <ClientOnly> 中或挂载在 onMounted 中 - 适配器需要浏览器中的真实 DOM 元素。请记住主机包要求:react 和react-dom 18+ 必须作为 Nuxt 应用程序的依赖项安装;商店渲染到自己的容器中,并且不会干扰 Vue 的虚拟 DOM。

微前端需要一个会话

无token

无会话token — 微前端保持不活动状态

如果没有会话token,微前端会呈现中立的通知,并且根本不会启动任何 API 调用。无法填充购物车、无法注册、无法结账。在商店激活之前,会话是强制性的 - 但它不必与已知用户绑定:访客会话足以浏览和填充购物车,并且可以在稍后结帐时建立真正的用户(请参阅下面的可选用户流程)。

mount.ts
import { mount } from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ohne sessionToken bleibt das Micro-Frontend inaktiv: es zeigt nur einen
// neutralen Hinweis und startet KEINE API-Calls — kein Warenkorb,
// keine Registrierung, kein Checkout. Erst eine Session aktiviert den Shop.
mount(el, {
  publicKey: "pk_deine_seite",
  // sessionToken fehlt -> Shop wird nicht aktiviert.
});
每主机用户

会话token——特定用户

您的后端会铸造一个与一个用户绑定的短期token。然后,微前端充当该用户:他们的购物车、他们的数据。该token是不透明的、过期的、单用户的,并且绑定到您的公钥 - 并且您的秘密永远不会到达浏览器。

mount.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

// getElementById kann null sein — in TypeScript sauber prüfen statt "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen — Autovervollständigung inklusive.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend gemintet, nur im Speicher halten
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

页面重新加载不会丢失旅程。微前端会在本地记住购物车和步骤,在安装时从服务器重新获取实时购物车,并准确地从用户离开的地方继续——即使您的页面在每次加载时都会生成一个新的会话token。如果购物车在服务器端已过期,则旅程将重新开始。您的页面无需为此执行任何操作。

想要一个刻意的新开始——一个真正的“重新开始”按钮?该包导出clearJourney():在挂载(或重新挂载)之前调用它,并且当前页面的本地持久旅程记录将被删除,因此下一次挂载将在入口点重新开始。仅移除本地恢复指针;任何服务器端购物车状态均保持不变。

可选:匿名浏览,结账时登录

您不必预先知道用户是谁。通过访客会话安装,微前端允许匿名访客浏览事件、选择文章并填充购物车。仅当他们经过购物车(进行注册和结帐)时,它才会要求您通过 onUserRequired 回调建立真正的用户。然后流程的其余部分将继续,并保留购物车。

  1. 1

    通过访客会话安装

    激活商店仍然需要会话token - 但它可以是访客会话:只需在没有电子邮件字段的情况下创建它,不需要占位符地址。访客浏览并填充其下方的购物车。

  2. 2

    离开购物车会触发 onUserRequired

    当访问者经过购物车时,微前端会调用您的回调一次并显示中立的待处理状态。对用户进行身份验证(登录/SSO),然后以与第一个会话完全相同的方式创建一个新的、用户绑定的会话:使用您的key进行服务器到服务器。后来的用户也是通过机器对机器的薄荷建立的——秘密永远不会到达浏览器,并且用户永远不会在客户端代码中设置。您的回调仅中继生成的不透明token。至关重要的是,您的后端从其自己的经过身份验证的会话(cookie/JWT)中获取该身份;它决不能接受来自浏览器的电子邮件或用户 ID,否则恶意访问者可能会请求其他人的会话并通过预填充读取他们的个人数据。

  3. 3

    返回新的token——购物车结转

    返回新的会话token,微前端将其交换并将已识别的用户绑定到现有的购物车 - 购物车、其文章和任何时间段都保持不变。然后就进入注册了。返回 null 以中止 - 访问者只是停留在购物车上,没有错误。

  4. 4

    对于固定用户省略 onUserRequired

    如果您忽略回调,则经典流程不会发生任何变化:使用用户绑定的会话进行挂载,微前端从第一步起就充当该用户。

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ein GAST-Session-Token reicht zum Mounten: server-zu-server gemintet wie
// gehabt, nur OHNE das Feld email (keine Platzhalter-Adresse nötig). Der
// Besucher darf browsen UND den Warenkorb füllen, ohne dass ein konkreter
// Nutzer feststeht.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite",
  sessionToken: guestSessionToken,

  // Wird EINMAL aufgerufen, sobald der Besucher den Warenkorb Richtung
  // Registrierung verlässt. Hier authentifizierst du den Nutzer (Login/SSO)
  // und mintest server-zu-server eine NEUE, nutzergebundene Session.
  // Gib den frischen Token zurück -> das Micro-Frontend übernimmt ihn und baut
  // den Warenkorb unter dem identifizierten Nutzer neu auf (Artikel bleiben
  // erhalten). Gib null zurück, um abzubrechen -> der Besucher bleibt im
  // Warenkorb, ohne Fehler.
  onUserRequired: async () => {
    // Ruft NUR dein Backend auf – ohne Identität im Payload. Dein Backend liest
    // den eingeloggten Nutzer aus SEINER eigenen Session (Cookie/JWT) und mintet
    // server-seitig für GENAU diese Identität. Niemals die E-Mail aus dem Browser
    // übergeben – sonst könnte ein Angreifer hier eine fremde Adresse einsetzen
    // und über den Prefill an deren personenbezogene Daten gelangen.
    const res = await fetch("/api/shop/my-session", { method: "POST" });
    if (res.status === 401) return null;        // nicht eingeloggt -> im Warenkorb bleiben
    const { sessionToken } = (await res.json()) as { sessionToken: string };
    return sessionToken;
  },
};

const handle: ShopHandle = mount(el, config);

主机页面对用户进行身份验证

ADITUS 不会对您的最终用户进行身份验证。您的网站是用户身份的唯一真实来源 - 您运行自己的登录、SSO 或会话。 ADITUS 仅信任后端使用key创建的会话token。信任链是:您的身份验证证明身份,您秘密铸造的token为其提供担保,微前端呈现它。

  1. 1

    您的网站对用户进行身份验证

    登录、SSO、会员会话 — 完全属于您自己。 ADITUS 在此不涉及。

  2. 2

    你的后端创建一个会话

    使用key的服务器到服务器。您传递微前端应充当的角色(电子邮件)并将其绑定到您的公钥。

  3. 3

    页面挂载微前端

    不透明token被传递给浏览器并作为 sessionToken 传递给 mount()。秘密保留在您的服务器上。

  4. 4

    每次调用都会携带会话

    微前端在每个请求上发送 X-Aditus-Session;代理从会话中解析购物车的用户。

  5. 5

    无token → 微前端保持不活动状态

    如果没有会话,微前端永远不会激活:它会显示中立的通知,并且不会发出任何呼叫 - 没有购物车,没有结账。创建一个会话以将其打开。

1. 在你的后端进行 Mint

server.ts
// SERVER-SEITE deines Hosts — der Secret-Key verlässt NIE den Browser.
// Dein Backend hat den Nutzer bereits selbst authentifiziert (Login/SSO/Session).
// WICHTIG: user stammt aus DIESER Server-Session, nie aus dem Request-Body des
// Browsers — sonst könnte ein Angreifer eine fremde E-Mail unterschieben.
const res = await fetch("https://<dein-host>/api/shop/session", {
  method: "POST",
  headers: {
    // Mint-Secret DEINES Clients (im ADITUS-Admin erzeugt), nur server-seitig.
    Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    publicKey: "pk_deine_seite",      // bindet die Session an deinen Key (Pflicht)
    email: user.email,                 // wer das Micro-Frontend sein soll (weglassen = anonyme Gast-Session)
    externalUserId: user.id,           // optionale Metadaten (deine User-ID)
    ttlSeconds: 1200,                  // optional: Lebensdauer (Default 20 min, max 24 h)
    eventSlug: "ExperienceDaysv52024", // optional: Einstieg direkt in dieses Event (sonst Eventübersicht)
  }),
});

// Antwort-Typ deiner Wahl — sessionToken an den Browser reichen (inline oder fetch).
const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();

2. 使用token挂载

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

// getElementById kann null sein — in TypeScript sauber prüfen statt "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen — Autovervollständigung inklusive.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend gemintet, nur im Speicher halten
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

3.注销时撤销

server.ts
// SERVER-SEITE — z. B. beim Logout deines Nutzers.
await fetch(`https://<dein-host>/api/shop/session/${sessionToken}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}` },
});
// { revoked: true }

完成的订单返回到您的页面

门票购买完成后,您不需要单独查找订单。因为微前端在您的页面中本地运行(轻量 DOM,而不是 iframe),所以它通过 onComplete 回调将完成的订单直接返回给您的代码 - 直接 JS 调用,没有 postMessage。微前端已经为您解决了订单,因此您收到订单号和票证链接,而不仅仅是 ID。

mount.ts
import {
  mount,
  type ShopHandle,
  type CompletedOrder,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // Wird GENAU EINMAL aufgerufen, sobald die Bestellung platziert ist.
  // Das Micro-Frontend hat die Bestellung bereits für dich aufgelöst — du bekommst
  // die aufgelöste Bestellung, nicht nur eine ID.
  onComplete: (order: CompletedOrder) => {
    order.number;   // Bestellnummer (Order-ID als Fallback) — dein Identifier
    order.status;   // Status der Bestellung
    order.buyer;    // Käufer (Name / E-Mail), falls vorhanden
    order.tickets;  // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]

    // -> Order-ID speichern, Bestätigung zeigen, DOI / Newsletter anstoßen ...
  },
  // Wird bei jedem nicht behebbaren Fehler im Ablauf aufgerufen.
  onError: (err: Error) => {
    console.error("Shop-Fehler:", err.message);
  },
});

您收到什么

每次成功结账时,onComplete 都会触发一次并处理已解决的订单:编号(订单 ID/订单号 - 您的标识符)、状态、买家(姓名和电子邮件(如果存在))和门票 - 每张门票都带有类型为“pdf”、“apple”或“google”的键入链接。 onError 在流程中发生任何不可恢复的错误时触发。

重定向付款方式

留在页面上的方法(发票、预付款、内联卡)会立即触发 onComplete。将浏览器发送到外部支付页面(例如 Saferpay)的方法由微前端端到端处理:它从其运行的页面派生返回 URL,将用户发送出去,并在返回时完成订单并触发 onComplete — 一个零代码的完整往返。仅当返回必须到达不同页面时,您才覆盖redirectUrls.successUrl / cancelUrl / errorUrl。

GA4/电子商务活动(可选)

微前端可以向您的分析报告购物渠道——但前提是您要求这样做。它不捆绑任何 gtag、GTM 和 GA SDK,不加载任何内容,不设置 cookie,也不自行发送任何内容。您添加一个可选回调 onEvent,并接收已在 Google 的 GA4 增强型电子商务架构中形成的类型化事件。省略 onEvent 则不会发生任何变化。您提供哪种工具以及您是否同意发送,完全取决于您。

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopAnalyticsEvent,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // OPTIONAL. Lässt du onEvent weg, ändert sich nichts: das Micro-Frontend
  // sendet nichts, lädt kein gtag/GTM/GA-SDK und setzt keine Cookies.
  // Tool, Consent und Mapping bleiben komplett bei dir — hier GA4 via gtag.
  onEvent: (event: ShopAnalyticsEvent) => {
    // Consent liegt bei dir: erst senden, wenn der Nutzer zugestimmt hat.
    if (!hasAnalyticsConsent()) return;
    // Die Events sind bereits im GA4-Schema (name + items/value/currency ...),
    // also 1:1 an gtag durchreichen.
    const { name, ...params } = event;
    window.gtag?.("event", name, params);
  },
});

已处于 GA4 形状

每个事件都带有 GA4 事件名称及其标准参数:商品(包含 item_id、item_name、价格、数量、item_category,以及折扣和优惠券(如果适用))、价值和货币; add_ payment_info 添加 payment_type ,purchase 添加 transaction_id 。解构 name 并将其余部分直接传递给 gtag("event", name, params) — 无需重新映射。

同意权归您所有

onEvent 只是我们直接调用的页面中的一个普通 JavaScript 函数。微前端本身不会在任何地方发送分析 - 在您的处理程序发送之前,没有任何内容离开页面,因此您的同意管理保持完全控制:同意转发,路由到 GTM 的 dataLayer 而不是 gtag、批处理或完全删除事件。您的处理程序抛出的任何错误都会被捕获并隔离,因此它永远不会中断用户流程 - 保持处理程序轻量级(gtag/dataLayer 推送),因为它内联运行。

事件触发时
view_item_list用户看到文章列表(每个显示的集合触发一次)。
add_to_cart文章或附加组件已添加到购物车。
remove_from_cart购物车系列或附加组件被删除。
view_cart显示购物车视图(每个购物车一次)。
begin_checkout用户离开购物车进行注册/结帐。
add_payment_info选择付款方式(携带 payment_type)。
purchase订单已下达(包含 transaction_id、value、items)。

事件总线:handle.on/handle.off(可选)

除了配置回调之外,mount() 返回的句柄还带有一个小型的、完全可选的事件总线。使用handle.on(event,listener)订阅——它返回匹配的取消订阅函数——并使用handle.off(event,listener)分离。如果您从不调用 on(),则不会发生任何变化:总线不会添加任何依赖项,不会发送任何内容,也不会产生任何费用。它是对您的页面的纯粹观察 - 迷你购物车徽章、确认横幅、您自己的日志记录 - 它补充了回调而不是取代它们。

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopCartUpdate,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
});

// OPTIONAL. Ohne on() verhält sich das Micro-Frontend exakt wie bisher —
// der Bus ist reine Beobachtung, kein Event ist Pflicht.
// on() liefert die Abmeldefunktion zurück; alternativ handle.off(name, fn).
const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
  // Bei jeder Warenkorb-Änderung: Anzahl, Summe, Positionen (GA4-Item-Form).
  updateMiniCartBadge(cart.itemCount); // z. B. Badge im Seiten-Header
});

handle.on("checkout:complete", ({ order }) => {
  // Bestellung platziert — dieselbe aufgelöste Bestellung wie in onComplete.
  showConfirmationBanner(order.number);
});

handle.on("analytics", (event) => {
  // Spiegel des GA4-Funnels (identische Payloads wie config.onEvent).
  console.debug("Funnel:", event.name);
});

// Später gezielt abmelden — der Rest bleibt aktiv:
offCart();
事件触发时
cart:update购物车发生变化:创建、添加或删除项目、或清空。包含 cartId、itemCount、价值、货币和行作为 GA4 形状的项目。
checkout:complete订单已下达 — 与 config.onComplete 相同的时刻和相同的解析订单对象。
analytics上面 GA4 漏斗的镜像:每个 onEvent 有效负载也会在总线上发出,保持不变。

监听器在 update() 中幸存

订阅存在于句柄上,而不是渲染上:handle.update({ ... }) 重新渲染微前端,但保持每个侦听器附加。通过handle()卸载会自动分离所有监听器——当你想在商店继续运行时停止监听时,你只需要off()(或返回的取消订阅)。

隔离,永不阻塞

抛出异常的监听器会被捕获并隔离——它不会破坏用户流程,也不会影响其他监听器。 “analytics”事件镜像 GA4 漏斗,其有效负载与 config.onEvent 相同,因此您可以通过总线、回调或两者使用漏斗;同样的同意规则适用:在您的代码发送页面之前,不会有任何内容离开页面。

公钥与会话token

将两者分开。他们回答不同的问题并且是配对的,不能互换。

publicKey — 起源信托

可发布。它包含在您的客户端代码中,因此它不是秘密,并且本身不会授予任何内容。后端强制执行每个key域白名单:key只能在其注册域中使用。它回答“这是哪个网站?”

sessionToken — 身份

在您的后端秘密铸造,不透明且短暂。它回答“用户是谁?”会话绑定到为其创建的公钥:会话请求也必须发送该key,并且不匹配将被拒绝(403)。

安全规则

  • key仅存在于您的服务器上,而不会存在于发送到浏览器的客户端代码、捆绑包或环境文件中。
  • 仅将token保留在内存中。不要将其持久化到localStorage;通过重新铸造来刷新它。
  • token的有效期很短(默认 20 分钟,最长 24 小时)并且是单用户的。
  • 注销时撤销会话,以便无法重播泄露的token。
  • 始终将会话与其公钥配对 - 丢失或不匹配的key将被拒绝 (403)。
  • 错误或过期的会话是一个硬性的 401——绝不是无声地退回到演示用户。

端点和响应参考

POST/api/shop/session薄荷 — 不记名秘密
DELETE/api/shop/session/:token撤销 — 持有者秘密

在此演示中,铸币通过上述路径上的代理运行。在生产中,会话是在 ADITUS 核心中铸造的——合约是相同的。这两个调用都是服务器到服务器的:您的后端使用客户端的薄荷秘密(在 ADITUS 管理控制台中生成)作为承载token进行身份验证。每次登录一枚薄荷币是正常模式;生成的token就是浏览器所看到的所有token。

您可以使用您自己的凭据尝试这个确切的流程集成测试仪—薄荷,安装,走上旅程。

创建会话——请求

标头价值意义
AuthorizationBearer <mint-secret>您客户的薄荷秘密,在 ADITUS 管理控制台中生成。仅限服务器端 - 绝不能将其发送到浏览器或应用程序包。
Content-Typeapplication/json正文是一个 JSON 对象。
体场类型意义
publicKeystring · 必填会话创建的可发布key。它必须与微前端安装的key相同——以后的每个商店调用都会根据它进行检查(如果不匹配则为 403)。承载key必须属于该key的客户端。
emailstring · 必填会话充当的商店用户。从您经过身份验证的服务器会话(登录/SSO cookie 或 JWT)中获取它 — 切勿从浏览器请求中获取,否则访问者可能会获取其他人的会话。省略它以创建匿名会话:访问者可以浏览并填充购物车,但微前端会阻止经过购物车的步骤,直到您的 onUserRequired 回调提供用户绑定会话(请参阅可选用户流程)。存在但格式错误的电子邮件仍会被拒绝 (400)。
externalUserIdstring · 必填您自己的用户 ID,作为元数据与会话一起存储(对于支持和日志关联很有用)。 ADITUS 不解释。
ttlSecondsstring · 必填会话生命周期(以秒为单位)。默认 20 分钟,上限为 24 小时。当它过期时,shop 调用返回 401 session_expired — 然后铸造一个新的token(例如通过 onSessionExpired 挂钩)。
eventSlugstring · 必填修复了旅程的ENTRY POINT服务器端:设置后,微前端直接在该事件的文章选择上启动;省略,从事件概述开始。因为它是创建会话的一部分,所以浏览器无法操作它。无效字符产生 400 invalid_event_slug;解析为没有实时事件的 slug 会回退到事件概述。

创建会话 - 响应 (200)

场地类型意义
sessionTokenstring不透明token(sess_…)。交给浏览器,作为sessionToken传给mount();微前端在每次商店调用时将其作为 X-Aditus-Session 发送。它不包含用户数据并且无法解码。
expiresAtnumber过期时间为 Unix 时间戳(以毫秒为单位)。纯粹为您自己的调度提供信息 — 微前端自行对 401 做出反应。

撤销会话

删除具有相同承载key的 /api/shop/session/:token — 该key必须属于为其创建会话的客户端。在注销时调用它,以便token随着您自己的会话而消失。响应:{ "revoked": true } (如果会话已过期,则为 false)。撤销是幂等的并且可以安全地“一劳永逸”。

错误是明确的

地位代码意义
503session_not_configured此客户端未配置嵌入会话。
401unauthorized用于创建会话的 secret 无效或缺失。
400invalid_public_key提供的 public key 无效。
400invalid_email提供的电子邮件地址无效。
400invalid_token提供的会话 token 无效。
429rate_limited请求过多,请稍后重试。
403public_key_invalid提供的 public key 格式无效。
403public_key_unknown无法识别提供的 public key。
403public_key_origin_unresolved无法确定此请求的来源。
403public_key_domain_not_allowed此 public key 不允许来自该域名的请求。
401invalid_session提供的会话无效。
401session_expired提供的会话已过期。
403session_requires_public_key此会话需要 public key。
403session_key_mismatchpublic key 与会话不匹配。
403anonymous_session匿名会话无法执行此操作。

现在让它成为你的

身份已确定——接下来,为微前端打造品牌。样式工具生成一个可立即粘贴的主题配置,并通过实时预览解释每个参数。

打开样式工具