Visualforce に埋め込まれた LWC との通信

# 概要

Visualforce ページに埋め込まれた Lightning Web コンポーネント (LWC) との通信には、以下の方法があります。

- JavaScript イベントを使用
- Lightning Message Service (LMS) を使用

これらの実装方法をサンプルコード付きで説明します。

:::note warn
前提条件
- 本記事では説明に必要なソースコードのみ記載します。サンプルコードの全メタデータは GitHub リポジトリ『[ykars/sf-visualforce-and-lwc-integration](https://github.com/ykars/sf-visualforce-and-lwc-integration)』をご参照ください。
- Visualforce ページに LWC を埋め込む方法として Lightning Out を使用しています。Lightning Out の使用方法については『[Visualforce ページ | Salesforce 対象でのコンポーネントの使用 | Salesforce 対象でのコンポーネントの使用 | Lightning Web Components 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-visualforce.html)』をご参照ください。
- Visualforce タブに配置された Visualforce ページを想定しています。Lightning ページに配置された Visualforce の挙動は検証できていないことをご了承ください。
:::

# JavaScript イベントを使用する方法

## サンプルコードの画面構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950738/2a871afc-6e60-bc1d-becc-948088687dbf.png)

## Visualforce ページ

LWC の親となる Visualforce ページです。
- LWC からのイベントを受信するために、`addEventListener` 関数を使用してイベントリスナーを追加します。
- LWC にデータを渡す際は LWC の `addVfMessage` 関数を呼び出して渡します。

```html:JsEventParent.page
<apex:page>
    <!-- Lightning 関連のライブラリをインポート -->
    <apex:includeLightning />

    <apex:form>
        <apex:pageBlock title="Visualforce">
            <!-- LWC にメッセージを送信するボタン -->
            <apex:pageBlockButtons>
                <apex:commandButton
                    value="LWC にメッセージを送信"
                    onclick="handleSendMessageToLwc();"
                    oncomplete="return null;"
                ></apex:commandButton>
            </apex:pageBlockButtons>

            <apex:pageBlockSection columns="2">
                <!-- LWC からのメッセージ表示領域 -->
                <apex:pageBlockSectionItem>
                    <h2>LWC からのメッセージ</h2>
                    <div class="lwc-message-list"></div>
                </apex:pageBlockSectionItem>

                <!-- LWC 埋め込み領域 -->
                <apex:pageBlockSectionItem>
                    <div id="lwc-container"></div>
                </apex:pageBlockSectionItem>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>

    <script>
        // Lightning Out を使用して LWC をページに埋め込む
        $Lightning.use('c:jsEventChildContainer', function() {
            $Lightning.createComponent(
                'c:jsEventChild',
                {
                    vfMessages: [{ key: 0, value: 'Hello!', timestamp: new Date().toLocaleTimeString() }]
                },
                'lwc-container',
                function(cmp) {
                    console.log('Visualforce ページに LWC が追加されました:' + cmp);

                    // LWC にイベントリスナーを設定
                    const lwc = document.querySelector('c-js-event-child');
                    lwc.addEventListener('message', handleLwcMessage);
                }
            );
        });

        /**
         * LWC からのメッセージ受信時ハンドラ
         * @param {Event} event イベント
         */
        function handleLwcMessage(event) {
            const lwcMessage = event.detail.message;

            const lwcMessageElement = document.createElement('p');
            lwcMessageElement.textContent = lwcMessage;

            const lwcMessageListElement = document.querySelector('.lwc-message-list');
            lwcMessageListElement.appendChild(lwcMessageElement);
        }

        /**
         * LWC にメッセージを送信する
         */
        function handleSendMessageToLwc() {
            // LWC の @api 関数を呼び出す
            const lwc = document.querySelector('c-js-event-child');
            lwc.addVfMessage('Hello from Visualforce!');
        }
    </script>
</apex:page>
```

## Lightning Web コンポーネント

Visualforce ページに埋め込む LWC です。

- `addVfMessage` 関数を Visualforce ページから呼び出せるようにするために `@api` デコレータを付けます。
- Visualforce にデータを送信するには、CustomEvent をディスパッチします。

```javascript:jsEventChild.js
import { LightningElement, api } from 'lwc';

export default class JsEventChild extends LightningElement {
    /**
     * Visualforce からのメッセージ
     * @type {{ key: number; value: string; timestamp: string; }[]}
     */
    @api
    vfMessages = [];

    /**
     * Visualforce からのメッセージを追加する
     * @param {string} message Visualforce メッセージ
     */
    @api
    addVfMessage(message) {
        const newVfMessage = {
            key: this.vfMessages.length,
            value: message,
            timestamp: new Date().toLocaleTimeString()
        };
        this.vfMessages = [...this.vfMessages, newVfMessage];
    }

    /**
     * 「Visualforce にメッセージを送信」ボタン押下時ハンドラ
     */
    handleSendMessageToVf() {
        // CustomEvent を発行
        const event = new CustomEvent("message", {
            detail: {
                message: 'Hello from LWC!'
            }
        });
        this.dispatchEvent(event);
    }
}
```

# Lightning Message Service を使用する方法

## サンプルコードの画面構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950738/15bb0a77-3c43-41f5-6b1c-317cb9287055.png)

## Lightning Message Channel

Visualforce ページと LWC 間で通信するためのメッセージを定義します。

- `message` 項目にメッセージ内容を含めます。
- Visualforce ページと LWC のそれぞれで公開と購読の両方が行われるため、自分自身が公開したメッセージも受信することになります。自分自身が公開したメッセージは読み飛ばす処理を実装するために、「どこから公開されたのか」を表す `publisher` 項目を用意しています。

```xml:Message.messageChannel-meta.xml
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>Message</masterLabel>
    <isExposed>true</isExposed>
    <description>Visualforce に埋め込まれた LWC との通信に使用するメッセージチャネル</description>
    <lightningMessageFields>
        <fieldName>publisher</fieldName>
        <description>公開者。VF:Visualforce, LWC:Lightning Web コンポーネント</description>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>message</fieldName>
        <description>メッセージ</description>
    </lightningMessageFields>
</LightningMessageChannel>
```

## Visualforce ページ

LWC の親となる Visualforce ページです。

- `$Lightning.createComponent` 関数の第2引数で、LWC にメッセージチャネル、`sforce.one.publish`、`sforce.one.subscribe` を渡します。LWC 側ではこれらを使用して LMS を公開・購読します。

```html:LmsParent.page
<apex:page>
    <!-- Lightning 関連のライブラリをインポート -->
    <apex:includeLightning />

    <apex:form>
        <apex:pageBlock title="Visualforce">
            <!-- LWC にメッセージを送信するボタン -->
            <apex:pageBlockButtons>
                <apex:commandButton
                    value="LWC にメッセージを送信"
                    onclick="handleSendMessageToLwc();"
                    oncomplete="return null;"
                ></apex:commandButton>
            </apex:pageBlockButtons>

            <apex:pageBlockSection columns="2">
                <!-- LWC からのメッセージ表示領域 -->
                <apex:pageBlockSectionItem>
                    <h2>LWC からのメッセージ</h2>
                    <div class="lwc-message-list"></div>
                </apex:pageBlockSectionItem>

                <!-- LWC 埋め込み領域 -->
                <apex:pageBlockSectionItem>
                    <div id="lwc-container"></div>
                </apex:pageBlockSectionItem>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>

    <script>
        // メッセージチャネルを購読する
        const MESSAGE_CHANNEL = "{!$MessageChannel.Message__c}";
        let subscription = sforce.one.subscribe(MESSAGE_CHANNEL, handleInboundMessage, { scope: 'APPLICATION' });

        // Lightning Out を使用して LWC をページに埋め込む
        $Lightning.use('c:lmsChildContainer', function() {
            $Lightning.createComponent(
                'c:lmsChild',
                {
                    messageChannel: MESSAGE_CHANNEL,
                    publish: sforce.one.publish,
                    subscribe: sforce.one.subscribe
                },
                'lwc-container',
                function(cmp) {
                    console.log('Visualforce ページに LWC が追加されました:' + cmp);
                    publishMessage('Hello!');
                }
            );
        });

        /**
         * メッセージチャネル受信時ハンドラ
         * @param {MessageChannel} message メッセージチャネルレコード
         */
        function handleInboundMessage(message) {
            if (message.publisher === 'LWC') {
                const lwcMessage = message.message;

                const lwcMessageElement = document.createElement('p');
                lwcMessageElement.textContent = lwcMessage;

                const lwcMessageListElement = document.querySelector('.lwc-message-list');
                lwcMessageListElement.appendChild(lwcMessageElement);
            }
        }

        /**
         * LWC にメッセージを送信する
         */
        function handleSendMessageToLwc() {
            publishMessage('Hello from Visualforce!');
        }

        /**
         * メッセージチャネルを公開する
         */
        function publishMessage(message) {
            const payload = {
                publisher: 'VF',
                message: message
            };
            sforce.one.publish(MESSAGE_CHANNEL, payload);
        }
    </script>
</apex:page>
```

## Lightning Web コンポーネント

Visualforce ページに埋め込む LWC です。

- Visualforce から受け取った `messageChannel`、`publish`、`subscribe` を使用して LWS を公開・購読します。

:::note alert
LWC で LMS を使用する一般的は方法は `lightning/messageService` モジュールをインポートして使用することですが、この方法では Visualforce ページと LWC 間で通信できなかったため、Visualforce ページからメッセージチャネル、`sforce.one.publish`、`sforce.one.subscribe` を受け取り使用しています。
しかし、この方法は Salesforce のドキュメントに載っていないため、サポートされていない可能性があります。
:::

```javascript:lmsChild.js
import { LightningElement, api } from 'lwc';

export default class LmsChild extends LightningElement {
    /**
     * メッセージチャネル
     */
    @api
    messageChannel;

    /**
     * メッセージチャネルを公開する
     * @type {(messageChannel: any, payload: any) => void}
     */
    @api
    publish;

    /**
     * メッセージチャネルを購読する
     * @type {(messageChannel: any, listener: (message: any) => void) => any}
     */
    @api
    subscribe;

    /**
     * Visualforce からのメッセージ
     * @type {{ key: number; value: string; timestamp: string; }[]}
     */
    vfMessages = [];

    /**
     * 初期処理
     */
    connectedCallback() {
        // メッセージチャネルを購読する
        if (this.messageChannel && this.subscribe) {
            this.subscribe(this.messageChannel, this.handleInboundMessage.bind(this));
        }
    }

    /**
     * 「Visualforce にメッセージを送信」ボタン押下時ハンドラ
     */
    handleSendMessageToVf() {
        // メッセージチャネルを公開する
        const payload = {
            publisher: 'LWC',
            message: 'Hello from LWC!'
        };
        this.publish(this.messageChannel, payload);
    }

    /**
     * メッセージチャネル受信時ハンドラ
     * @param {MessageChannel} message メッセージチャネルレコード
     */
    handleInboundMessage(message) {
        // 受信したメッセージを「Visualforce からのメッセージ」に表示する
        if (message.publisher === 'VF') {
            const newVfMessage = {
                key: this.vfMessages.length,
                value: message.message,
                timestamp: new Date().toLocaleTimeString()
            };
            this.vfMessages = [...this.vfMessages, newVfMessage];
        }
    }
}
```

# 参考資料

- [Visualforce ページ | Salesforce 対象でのコンポーネントの使用 | Salesforce 対象でのコンポーネントの使用 | Lightning Web Components 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-visualforce.html)
- [Lightning Message Service を使用した DOM 間の通信 | Visualforce 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.252.0.pages.meta/pages/message_channel_intro.htm)
- [Lightning Message Service | Salesforce 対象でのコンポーネントの使用 | Salesforce 対象でのコンポーネントの使用 | Salesforce 対象でのコンポーネントの使用 | Lightning Web Components 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-message-channel.html)
- [visualforce-to-lwc/force-app/main/default/pages/interoperability.page at main · trailheadapps/visualforce-to-lwc · GitHub](https://github.com/trailheadapps/visualforce-to-lwc/blob/main/force-app/main/default/pages/interoperability.page)
- [Vue3で作っているVisualforceページでLWCを使う](https://zenn.dev/cariot_dev/articles/51a46bf88e3fa6)