Configurator – 通过 postMessage 双向通信

简介

嵌入的 Configurator 使用与 Viewer 相同的安全传输机制:先进行 nonce 握手,再通过 ALTER_TOOL_INIT_SESSION 请求工具会话令牌。

设置:

  1. 在 Alter Product 应用中打开电商设置选项卡。
  2. 在集成部分设置将嵌入 iframe 的域名(例如 https://yourwebsite.comhttp://localhost:3000)。

流程:

  1. 网站加载 Configurator iframe。
  2. iframe 发送 ALTER_CHILD_HELLO,页面使用相同的 nonce 回复 ALTER_PARENT_ACK
  3. iframe 通过 ALTER_TOOL_INIT_SESSION 请求令牌,并将 payload.tool 设为 configurator
  4. 后端通过 Alter Product Public API 创建令牌,页面回复 ALTER_TOOL_SESSION_READY
  5. 设置 add_to_cart=1 后,Configurator 可发出 ALTER_CONFIGURATOR_ADD_TO_CART。宿主也可通过 ALTER_CONFIGURATOR_GET_PRODUCT_DATA 请求当前数据载荷,并接收 ALTER_CONFIGURATOR_DATA_RESPONSE

产品配置中设置的变体元数据会包含在数据载荷中。

Configurator 发送的消息

类型说明
ALTER_CHILD_HELLO初始化握手(包含 nonce)
ALTER_TOOL_INIT_SESSION向宿主网站请求会话令牌
ALTER_CONFIGURATOR_ADD_TO_CART用户在设置了 add_to_cart=1 的嵌入 Configurator 中点击了“加入购物车”
ALTER_CONFIGURATOR_DATA_RESPONSE产品数据请求的响应
ALTER_WP_RUNTIME_CONTEXT_REQUESTWordPress/WooCommerce 运行时上下文请求
ALTER_WP_LOCAL_DESIGN_REQUEST设置 wp_local_design=1 时的 WordPress 本地设计初始化请求

Configurator 接收的消息

类型说明
ALTER_PARENT_ACK握手确认(必须包含 nonce)
ALTER_TOOL_SESSION_READY提供会话令牌以授权 Configurator
ALTER_TOOL_SESSION_ERROR令牌请求失败(宿主端错误)
ALTER_CONFIGURATOR_GET_PRODUCT_DATA向 Configurator 请求配置后的产品数据
ALTER_WP_RUNTIME_CONTEXT_RESPONSEWordPress/WooCommerce 运行时上下文响应
ALTER_WP_LOCAL_DESIGN_RESPONSEWordPress 本地设计初始化响应

完整示例(HTML + JS)

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8" />
5  <title>Alter Product Configurator Communication (Handshake + Token + postMessage)</title>
6  <meta name="viewport" content="width=device-width, initial-scale=1" />
7  <style>
8    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9    iframe { width: 100%; height: 600px; border: 0; display: block; border-radius: 12px; }
10    .row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
11    button { padding: 10px 14px; border-radius: 10px; border: 1px solid #ddd; background: #fff; cursor: pointer; }
12    button:disabled { opacity: 0.5; cursor: not-allowed; }
13    pre { background: #0b1020; color: #d7e1ff; padding: 12px; border-radius: 12px; overflow:auto; }
14  </style>
15</head>
16<body>
17  <h1>Configurator — Two-Way Communication via postMessage</h1>
18
19  <div class="row">
20    <button id="getDataBtn" disabled>Request Current Payload</button>
21  </div>
22
23  <iframe
24    id="configuratorWidget"
25    src="https://alterproduct.com/app/configurator/1?nav=0&add_to_cart=1"
26    title="Alter Product Configurator"
27    allowfullscreen>
28  </iframe>
29
30  <h3>Logs</h3>
31  <pre id="log"></pre>
32
33  <script>
34    const IFRAME_ORIGIN = 'https://alterproduct.com';
35
36    const iframe = document.getElementById('configuratorWidget');
37    const getDataBtn = document.getElementById('getDataBtn');
38    const logEl = document.getElementById('log');
39
40    let handshakeOk = false;
41    let tokenReady = false;
42
43    function log(...args) {
44      const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' ');
45      logEl.textContent += line + '\n';
46    }
47
48    function postToIframe(payload) {
49      iframe.contentWindow.postMessage(payload, IFRAME_ORIGIN);
50    }
51
52    /**
53     * Your backend MUST request a session token (recommended).
54     * This endpoint is owned by YOU and should call Alter Product API using server-side credentials.
55     * Expected response: { sessionToken: "..." }
56     */
57    async function getSessionTokenFromYourBackend({ tool, mode, uiDesignId }) {
58      const url = new URL('/api/alter/session-token', window.location.origin);
59      url.searchParams.set('tool', tool);           // viewer | configurator | customizer
60      url.searchParams.set('mode', mode || 'design');
61      url.searchParams.set('uiDesignId', String(uiDesignId || 0));
62
63      const res = await fetch(url.toString(), { method: 'GET' });
64      if (!res.ok) throw new Error('Failed to get session token');
65      const data = await res.json();
66      if (!data || !data.sessionToken) throw new Error('Missing sessionToken');
67      return data.sessionToken;
68    }
69
70    // UI: request configured product data after the iframe has received a session token.
71    getDataBtn.addEventListener('click', () => {
72      if (!tokenReady) return;
73      log('[Host] -> Configurator: ALTER_CONFIGURATOR_GET_PRODUCT_DATA');
74      postToIframe({ type: 'ALTER_CONFIGURATOR_GET_PRODUCT_DATA' });
75    });
76
77    window.addEventListener('message', async (event) => {
78      // 1) Validate origin
79      if (event.origin !== IFRAME_ORIGIN) return;
80
81      // 2) Validate source (must be the embedded iframe)
82      if (event.source !== iframe.contentWindow) return;
83
84      const msg = event.data || {};
85      if (!msg.type || typeof msg.type !== 'string') return;
86
87      // -----------------------
88      // A) HANDSHAKE
89      // Configurator -> Host: ALTER_CHILD_HELLO { nonce }
90      // Host         -> Configurator: ALTER_PARENT_ACK { nonce }
91      // -----------------------
92      if (msg.type === 'ALTER_CHILD_HELLO') {
93        const nonce = msg.nonce;
94        if (!nonce || typeof nonce !== 'string') return;
95
96        handshakeOk = true;
97        log('[Configurator] -> Host: ALTER_CHILD_HELLO', { nonce });
98
99        log('[Host] -> Configurator: ALTER_PARENT_ACK');
100        postToIframe({ type: 'ALTER_PARENT_ACK', nonce });
101
102        return;
103      }
104
105      // Ignore everything until handshake is done
106      if (!handshakeOk) return;
107
108      // -----------------------
109      // B) TOKEN INIT (on-demand)
110      // Configurator -> Host: ALTER_TOOL_INIT_SESSION { payload: { tool, mode, uiDesignId } }
111      // Host         -> Configurator: ALTER_TOOL_SESSION_READY { token }
112      // -----------------------
113      if (msg.type === 'ALTER_TOOL_INIT_SESSION') {
114        try {
115          const payload = msg.payload || {};
116          const tool = String(payload.tool || 'configurator').toLowerCase();
117          const mode = String(payload.mode || 'design');
118          const uiDesignId = Number(payload.uiDesignId || 0);
119
120          log('[Configurator] -> Host: ALTER_TOOL_INIT_SESSION', { tool, mode, uiDesignId });
121
122          const token = await getSessionTokenFromYourBackend({ tool, mode, uiDesignId });
123
124          log('[Host] -> Configurator: ALTER_TOOL_SESSION_READY');
125          postToIframe({ type: 'ALTER_TOOL_SESSION_READY', tool, token });
126          tokenReady = true;
127          getDataBtn.disabled = false;
128        } catch (e) {
129          log('[Host] -> Configurator: ALTER_TOOL_SESSION_ERROR', String(e && e.message ? e.message : e));
130          postToIframe({ type: 'ALTER_TOOL_SESSION_ERROR', message: String(e && e.message ? e.message : e) });
131        }
132        return;
133      }
134
135      // -----------------------
136      // C) Configurator events
137      // -----------------------
138      if (msg.type === 'ALTER_CONFIGURATOR_DATA_RESPONSE') {
139        log('[Configurator] -> Host: ALTER_CONFIGURATOR_DATA_RESPONSE', msg.payload);
140        return;
141      }
142
143      if (msg.type === 'ALTER_CONFIGURATOR_ADD_TO_CART') {
144        log('[Configurator] -> Host: ALTER_CONFIGURATOR_ADD_TO_CART', msg.payload);
145        return;
146      }
147    });
148  </script>
149</body>
150</html>

数据载荷示例

{
  "userDesignId": 10,
  "designName": "Mug 450ml (15oz)",
  "totalPrice": {
    "value": 24.99,
    "currency": "EUR"
  },
  "productGroup": {
    "id": 1,
    "name": {
      "pl": "Kubek 450ml (15oz)",
      "en": "Mug 450ml (15oz)"
    }
  },
  "productItems": [
    {
      "model3d": {
        "id": 3
      },
      "size": {
        "id": 9,
        "name": {
          "pl": "450ml (15oz)",
          "en": "450ml (15oz)"
        },
        "measureSize": {
          "D": 8.65,
          "H": 11.95
        },
        "externalMapping": {
          "attribute": {
            "internalId": 123,
            "slug": "pa_size",
            "label": "Size"
          },
          "term": {
            "internalId": 456,
            "slug": "450ml-15oz",
            "label": "450ml (15oz)"
          }
        }
      },
      "material": {
        "id": 3,
        "name": {
          "pl": "Ceramika",
          "en": "Ceramic"
        },
        "externalMapping": {
          "attribute": {
            "internalId": 124,
            "slug": "pa_material",
            "label": "Material"
          },
          "term": {
            "internalId": 457,
            "slug": "ceramic",
            "label": "Ceramic"
          }
        }
      },
      "printingMethod": {
        "id": 5,
        "name": {
          "pl": "Sublimacja",
          "en": "Sublimation"
        },
        "externalMapping": {
          "attribute": {
            "internalId": 125,
            "slug": "pa_printing-method",
            "label": "Printing method"
          },
          "term": {
            "internalId": 458,
            "slug": "sublimation",
            "label": "Sublimation"
          }
        }
      },
      "color": {
        "id": 11,
        "name": {
          "pl": "Domyślny",
          "en": "Default"
        },
        "hex": "#FFFFFF",
        "customColor": false,
        "pickedColors": {},
        "patternId": null,
        "externalMapping": {
          "attribute": {
            "internalId": 126,
            "slug": "pa_color",
            "label": "Color"
          },
          "term": {
            "internalId": 459,
            "slug": "default",
            "label": "Default"
          }
        }
      },
      "variant": {
        "id": 11,
        "productGroupId": 1,
        "productModel3dId": 3,
        "sizeId": 9,
        "materialId": 3,
        "printingMethodId": 5,
        "colorId": 11,
        "metadata": {
          "sku": "MUG-450-WHITE"
        },
        "minOrderQuantity": 1,
        "processingTime": null,
        "stockQuantity": null,
        "volume": null,
        "weight": null
      },
      "unitPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "totalPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "quantity": 1
    }
  ]
}