Lightning Aura ComponentとCTIアダプタ(Visualforce)間での相互通信

# Lightning Aura ComponentとCTIアダプタ(Visualforce)間での相互通信

Lightning Aura ComponentとCTIアダプタ(Visualforce)でデータのやり取りを行う必要がありました。しかし、直接通信する手段がありません。そのため、JavaScriptのWeb Messaging API「postMessage」を使うことにより間接的にメッセージ通信することにより実現しました。

## 条件

* CTIアダプタにはVisualforceが設定されている
* (諸事情により)Salesforceの機能(Apexなど)が使えない

## 実現方法

Lightning Aura Componentは自身のiframe内のVisualforceとしか通信ができません。今回の場合、通信先VisualforceはLightning Aura Component外に存在します。よって、仲介用のVisualforceを用意し、そのVisualforceを介してCTIアダプタ内のVisualforceと通信を行います。

## 実装

以下に実装内容を記載します。

### Lightning Aura Componentと自信のiframe内のVisualforceで通信

@hrk623さんがすでに詳細な説明を[Qiitaの記事](https://qiita.com/hrk623/items/d86899218fe521570e4d "Lightning コンポーネントと iframe 内の Visualforce ページで相互通信する")に投稿しています。Lightning Aura Componentの実装はそちらを参考にしてください。Lightning Aura Component内に仲介Visualforceを埋め込みます。本記事内では埋め込むVisualforceを「proxy.vfp」とします。

### CTIアダプタ(Visualforce)と仲介用Visualforce間の通信

Salesforce Lightningのため、CTIアダプタとメッセージ交換するためのSalesforce API [^sfdc_classic_cti_method]が使用できません。JavaScriptのWeb Messaging APIを使用します。

[^sfdc_classic_cti_method]: Salesforce Classicの場合、[CTIアダプタとメッセージ交換するAPI](https://developer.salesforce.com/docs/atlas.ja-jp.api_console.meta/api_console/sforce_api_console_methods_cti.htm "コンピュータテレフォニーインテグレーション (CTI) のメソッド")が用意されています。

Salesforceは複数のiframeで構成されているようです。単純にpostMessageするだけではメッセージが届きません。まず親Windowオブジェクトを取得し、そこから子iframe全てに対してpostMessageを実行します。SalesforceのUIの実装が今後どうなるかわかりませんので、iframeがネストされていることも念の為考慮します。

以下はCTIアダプタ内のVisualforceとなります。

```html:ctiAdoptor.vfp
<apex:page>
<button type="button" onclick="sendMsgToProxyVF();">Send Message To Proxy VF</button>
<script>
console.log = console.log.bind(console, '[CTIAdaptor]'); // ログの見易さのため部分適用
let proxyVFPort;

function sendMsgToProxyVF() {
    proxyVFPort.postMessage("hello, I'm CTI Adaptor.");
}

function connectListener(e) {
    if (e.data === 'CONNECT') {
        console.log('Receive connect request message.');
        // 送信元でMessageChannelを生成しても可能、そちらの方が実装は楽です。
        // 今回はブロードキャストのように子iframe全てにメッセージが送信されますので、接続先で生成するようにしました。
        const msgChannel = new MessageChannel();
        proxyVFPort = msgChannel.port1;
        msgChannel.port1.onmessage = function(e) {
            console.log('Receive message');
        };
        e.source.postMessage('CONNECTED', e.origin, [ msgChannel.port2 ]);
        window.removeEventListener('message', connectListener);
    }
}

window.addEventListener('message', connectListener);
</script>
</apex:page>

```

以下は仲介用Visualforceとなります。Lightning Aura Componentのiframe内に埋め込まれます。

```html:proxy.vfp
<apex:page>
<button type="button" onclick="connectToCTIAdaptor();">Connect to CTI Adaptor</button>
<button type="button" onclick="sendToCTIAdaptor();">Send Message to CTI Adaptor</button>
<script>
console.log = console.log.bind(console, '[Proxy]'); // ログの見易さのため部分適用
let ctiAdaptorPort;

function sendToCTIAdaptor() {
    ctiAdaptorPort.postMessage("hello, I'm Proxy.");
}

function connectToCTIAdaptor() {
    const frameArr = window.top.frames;
    for (let i = 0; i < frameArr.length; i++) {
        console.log('connect to CTI Adaptor.');
        // サンプルのため、第3引数に送信先オリジンを指定しません。実際の実装ではきちんと設定してください。
        postMessageRecursive(frameArr[i], 'CONNECT', '*');
    }
}

// iframe内にネストされたiframeがある場合を考慮して、再帰的にメッセージ送信を行います
function postMessageRecursive(iframe, msg, targetOrigin) {
    iframe.postMessage(msg, targetOrigin);
    if (iframe.frames.length > 0) {
        // window.framesはiterableオブジェクトではないためfor...of構文は使用できません
        for (let i = 0; i < iframe.frames.length; i++) {
            postMessageRecursive(iframe.frames[i], msg, targetOrigin);
        }
    }
}

function connectedListener(e) {
    if (e.data === 'CONNECTED') {
        console.log('Success to connect.');
        ctiAdaptorPort = e.ports[0];
        ctiAdaptorPort.onmessage = function(e) {
            console.log('Receive message.');
        }
        window.removeEventListener('message', connectedListener);
    }
}

// 今回はMessageChannelは接続先で生成します。
// 接続先でMessageChannelを生成してそれを利用するため、メッセージを待ち受けます。
window.addEventListener('message', connectedListener);
</script>
</apex:page>

```

Lightning Componentとの通信も実装した仲介用Visualforceでは、以下のようにしてメッセージをバケツリレーするイメージです。

```js
class ProxySession {
    constructor(lcPort, ctiAdaptorPort) {
        lcPort.onmessage = function(e) {
            ctiAdaptorPort.postMessage(e.data);
        };
        ctiAdaptorPort.onmessage = function(e) {
            lcPort.postMessage(e.data);
        };
    }
}
```

## 参考

* [Lightning コンポーネントと iframe 内の Visualforce ページで相互通信する](https://qiita.com/hrk623/items/d86899218fe521570e4d)
* [Salesforce Developer Blog](https://developer.salesforce.com/blogs/developer-relations/2017/01/lightning-visualforce-communication.html "Communicating between Lightning Components and Visualforce Pages")