[JavaScript] 別ウインドウ間でイベントごとに値(データ)のやりとりをしたい

2018/08/5

こんにちは。きんくまです。

画面Aがあります。そこからポップアップで画面Pを開きます。
このときAとPの間でデータのやりとりがしたいです。

さらに、画面Aから画面Bにページを切り替えます。ポップアップ画面Pは開いたままです。
このときでもBとPの間でデータのやりとりがしたいです。

というときのやり方です。

window.open, window.openerのやりかたもあるが、、、

ポップアップするときに、window.openして、その参照をもっておきます。
子供側から親側への参照は window.opener でできます。

でもこのやり方だと、親画面が遷移したあとにポップアップ画面と通信できないので、今回は候補からはずれました。
で、調べてみたところlocalStorageを使うことにしました。

参考)
>> Communication between tabs or windows

localStorageを使った通信

本来localStorageはcookieよりも大容量のデータをローカルに保存する仕組みです。
今回はlocalStorageに値が書き換わったときに、storageイベントを発行するので、それを利用する方法になります。

将来的には上のページにもある Broadcast Channel というAPIでできるようになるみたいですが、残念ながら現時点ではIE11やSafariで使えないので見送りです。

localStorage自体はIE11も含めて最近のブラウザでは全てサポートされているので大丈夫です。

簡単に扱えるようにする

そのままでもよいのですが、簡単に扱えるようにしたいのでヘルパークラスを作りました。

// original idea from
// https://stackoverflow.com/questions/28230845/communication-between-tabs-or-windows

let storageEventRef = null;

export default class LocalStorageMessenger {

    eventCallbackCollection = {};

    constructor(){
        if(storageEventRef === null){
            storageEventRef = window.addEventListener('storage', (e)=>{
                this._onStorageEvent(e);
            });
        }
    }

    _onStorageEvent(e){
        if(this.eventCallbackCollection.hasOwnProperty(e.key)){
            const collection = this.eventCallbackCollection[e.key];
            let newValue = JSON.parse(e.newValue);
            collection.map((callback)=>{
                callback(newValue.data);
            });
        }
    }

    /**
     * localStorageを空にします
     */
    clear(){
        localStorage.clear();
    }

    /**
     * イベントを登録します
     * @param eventName イベント名
     * @param callback 登録したいコールバック関数
     */
    on(eventName, callback){
        if(this.eventCallbackCollection.hasOwnProperty(eventName) == false){
            this.eventCallbackCollection[eventName] = [];
        }
        this.eventCallbackCollection[eventName].push(callback);
    }

    /**
     * イベントを解除します
     * @param eventName イベント名
     * @param callback 解除したいコールバック関数。
     * 何も指定しないときはイベント名に登録された全ての関数が解除されます
     */
    off(eventName, callback = null){
        if(!eventName){
            throw new Error(eventName + ' is not set');
        }
        if(callback === null){
            this.eventCallbackCollection[eventName] = [];
        }else{
            let callbacks = this.eventCallbackCollection[eventName];
            if(callbacks){
                for(let i = 0, len = callbacks.length; i < len; i++){
                    if(callback === callbacks[i]){
                        callbacks.splice(i, 1);
                        break;
                    }
                }
                this.eventCallbackCollection[eventName] = callbacks;
            }
        }
    }

    /**
     * イベントを発行します
     * @param eventName イベント名
     * @param data 送りたいデータ
     */
    emit(eventName, data){
        let savedata = {
            data: data,
            uid: (new Date()).getTime() + Math.random()
        };
        localStorage.setItem(eventName, JSON.stringify(savedata));
    }
}

使い方

on(イベント名, コールバック) でイベント登録します。
emit(イベント名、送るデータ) でイベント発行します。

サンプルです。

親ページ側

const messenger = new LocalStorageMessenger();
//イベント監視
messenger.on('popup.update', (data)=>{
    console.log('data =>', data);
});


const button = document.getElementById('emitButton');
button.addEventListener('click', ()=>{
    //イベント発行してデータを送る
    messenger.emit('parent.update', {
        message:'hello popup!',
        someFlag:true
    });
});

子供ページ側

const messenger = new LocalStorageMessenger();
//イベント監視
messenger.on('parent.update', (data)=>{
    console.log('data =>', data);
});


const button = document.getElementById('emitButton');
button.addEventListener('click', ()=>{
    //イベント発行してデータを送る
    messenger.emit('popup.update', 'text only');
});

送るデータの形式はJSONにシリアライズできる形だったら文字列でもオブジェクトでも大丈夫です。

それでイベント名のところだけ注意が必要です。

サンプルでは 'popup.update' のように、'送り元.イベント名' というふうにしています。
本当だったらイベント名だけでよいのですが、テストしていたところIE11だと、自分で更新したlocalStorageのstorageイベントが自分自身のウインドウにも発行されてしまいます。(Chromeとか他のブラウザは発行されなかった)

そのため、送り元とイベント名をつなげて書いてイベント名がかぶらないように書いています。
形式的にわかりやすいかと思ってドットでつなげていますが、文字列がかぶらないイベント名であればよいので、大文字小文字だけで作ったり、'parent-create'とハイフンでつなげたり、'ACBDE1345SPEOIJ'のようなデタラメな文字列のイベント名でも問題ないです。

※2018/08/05 前は'送り先.イベント名'と書いていたのですが、イベント発行するときは送り元の方が良いと思い修正しました

ではでは。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

LINEスタンプ作りました!
毎日使える。とぼけたウサギ。LINEスタンプ販売中! 毎日使える。とぼけたウサギ

ページトップへ戻る