LWRで構築する英国紅茶ブランドサイト 「The Royal Brew」 制作日誌、第3弾です。
前回は、Custom LWCで「究極の商品詳細ページ」を作り上げました。
見た目は完璧です。しかし、ECサイトとして決定的に足りないものがあります。 そう、「カート機能」 です。

画面中央にある [ADD TO CART] ボタンを押しても、現在は何も起きません。 理想は、ボタンを押した瞬間に、ヘッダー右上の「カートアイコン」の数字がポコッと増える ことです。
しかし、ここで技術的な壁にぶつかります。 「商品詳細コンポーネント」と「ヘッダーのカートアイコン」は、DOMツリー上で親子関係になく、赤の他人同士です。どうやって連携させればよいのでしょうか?
今回は、Salesforceの標準メッセージング技術 「Lightning Message Service (LMS)」 を使って、この問題を鮮やかに解決します。
今回の記事で作成したコンポーネントの全ソースコードをGitHubで公開しています。 ぜひ Clone して、あなたの組織で動かしてみてください。
今回のミッション:離れたコンポーネントを会話させる
- 送信側 (Publisher): 商品詳細ページ (Royal Product Detail)。「カートに追加」ボタンが押されたらメッセージを飛ばす。
- 受信側 (Subscriber): ヘッダー (Royal Cart Icon)。メッセージを受け取ったらバッジの数字を増やす。
- 放送局 (Channel): Lightning Message Channel。メッセージの通り道。
Step 1: 「放送局」を用意する (Message Channel)
まずは、会話のための専用回線(チャネル)を定義します。 これは LWC (JavaScript) ではなく、XMLファイルとして作成します。
ファイル: force-app/main/default/messageChannels/RoyalCartChannel.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel>RoyalCartChannel</masterLabel>
<isExposed>true</isExposed>
<description>商品がカートに追加されたことを通知するチャネル</description>
<lightningMessageFields>
<fieldName>productId</fieldName>
<description>追加された商品のレコードID</description>
</lightningMessageFields>
<lightningMessageFields>
<fieldName>productName</fieldName>
<description>追加された商品の名前</description>
</lightningMessageFields>
</LightningMessageChannel>
これを組織にデプロイすることで、Salesforce全体で使える「放送局」が開設されます。
Step 2: 「送信機」を作る (Product Detail)
前回作成した商品詳細コンポーネント royalProductDetail.js に、メッセージ送信機能を組み込みます。
ポイントは publish メソッドです。
// royalProductDetail.js
import { LightningElement, api, wire } from 'lwc';
// ... (既存のimport)
// ★ LMS関連モジュールのインポート
import { publish, MessageContext } from 'lightning/messageService';
import CART_CHANNEL from '@salesforce/messageChannel/RoyalCartChannel__c';
export default class RoyalProductDetail extends LightningElement {
// ...
// ★ LMSのコンテキストを取得(これがないと送れません)
@wire(MessageContext)
messageContext;
// ボタンが押された時の処理
handleAddToCart() {
const payload = {
productId: this.recordId,
productName: this.name
};
// メッセージ発射! (Fire!)
publish(this.messageContext, CART_CHANNEL, payload);
console.log('Published Add to Cart:', payload);
}
}
HTML側でボタンに onclick={handleAddToCart} を設定すれば、送信側の準備は完了です。
【技術コラム】実務では「裏側のロジック」をどう組む?
今回のコードでは、LMSの挙動を理解するために「メッセージ送信」のみを行っていますが、実際のECサイト構築では、同時にデータベースへの保存が必要です。
ボタンを押した瞬間の正しい処理フローは以下のようになります。
- Apexメソッドを呼び出す: 商品IDと数量をサーバーに送信し、
CartItem(またはカスタムカートオブジェクト)を作成・更新する。 - 成功レスポンスを受け取る: サーバーから「保存完了」の合図を待つ。
- LMSを送信する: 保存が確定して初めて、ヘッダーに「更新して!」とメッセージを送る。
このように、「サーバー処理(Apex)→ クライアント間通信(LMS)」 という順序を守ることで、データの整合性が取れた堅牢なアプリケーションになります。今回はLMSの紹介にフォーカスするため、このサーバー処理部分は省略しています。
Step 3: 「受信機」を作る (Cart Icon)
次に、メッセージを受け取るための新しいLWC、Royal Cart Icon を作成し、サイトのヘッダー部分に配置します。
ここでは subscribe メソッドを使って、チャネルを常時監視します。
クリックしてLWCのコードを展開する
royalCartIcon.js
import { LightningElement, wire } from 'lwc';
import { subscribe, MessageContext } from 'lightning/messageService';
import CART_CHANNEL from '@salesforce/messageChannel/RoyalCartChannel__c'
export default class RoyalCartIcon extends LightningElement {
cartCount = 0; // カートの中身の数
subscription = null;
@wire(MessageContext)
messageContext;
// コンポーネントが配置されたら購読開始
connectedCallback() {
this.subscribeToMessageChannel();
}
subscribeToMessageChannel() {
if (!this.subscription) {
this.subscription = subscribe(
this.messageContext,
CART_CHANNEL,
(message) => this.handleMessage(message)
);
}
}
// メッセージが届いた時の処理
handleMessage(message) {
console.log('Received:', message);
// カウントを増やす
this.cartCount++;
// ここで「ポコッ」と動くCSSアニメーションなどを発火させると最高です
}
}
royalCartIcon.html
<template>
<div class="cart-container">
<svg class="cart-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M19 6h-2c0-2.8-2.2-5-5-5S7 3.2 7 6H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-7-3c1.7 0 3 1.3 3 3H9c0-1.7 1.3-3 3-3zm7 17H5V8h14v12zm-7-8c-1.7 0-3-1.3-3-3H7c0 2.8 2.2 5 5 5s5-2.2 5-5h-2c0 1.7-1.3 3-3 3z" fill="currentColor"></path>
</svg>
<div class="cart-badge" lwc:if={cartCount}>{cartCount}</div>
</div>
</template>
royalCartIcon.css
.cart-container {
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: var(--royal-navy, #0F1F3D);
cursor: pointer;
}
/* ホバー時にゴールドに変化 */
.cart-container:hover .cart-icon {
fill: var(--royal-gold, #C5A059);
}
.cart-badge {
position: absolute;
top: -2px;
right: -2px;
background-color: var(--royal-gold, #C5A059);
color: #fff;
font-size: 0.75rem;
font-weight: bold;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
/* 数字が増えた時にポンと弾むアニメーション */
animation: pop 0.3s ease-out;
}
@keyframes pop {
0% { transform: scale(0); }
80% { transform: scale(1.2); }
100% { transform: scale(1); }
}
royalCartIcon.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Royal Cart Icon</masterLabel>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
</LightningComponentBundle>
完成! SPAのような滑らかな連携
これらを実装してデプロイし、エクスペリエンスビルダーで配置します。 プレビュー画面で [ADD TO CART] ボタンを押してみてください。
ページのリロードなしで、ヘッダーの数字が即座に反応します。 これこそが、LWRサイト(SPA: Single Page Application)ならではのユーザー体験です。
今回のまとめ
- DOMが離れていても大丈夫: LMSを使えば、どこにあるコンポーネント同士でも会話できる。
- 疎結合な設計: 送信側は「誰が聞いているか」を知る必要がなく、受信側も「誰が送ったか」を知る必要がない。これにより、コンポーネントの独立性が保たれる。
次回の制作日誌
次回は、画面遷移の仕上げです。 パンくずリストや「HOMEに戻る」リンクを、正しくLWRの作法で実装する方法(NavigationMixinの落とし穴)について解説します。





読者の声