React Iframeで埋め込んだWebアプリに情報を渡す

[前回](https://qiita.com/maika_kamada/items/231273fd6c58a96e6d12)、Reactで作ったWebアプリ（以下、React アプリ）に、Vueで作ったWebアプリ（以下、Vue アプリ）をReact Iframeを用いて合体させました。React アプリからVue アプリに値を渡す必要があったので、今回は値を渡す方法についてまとめました。

## やったこと

**URLパラメータ**と**postMessage**の2つの方法を試してみました。

今回は、ユーザー名の`aaa.bbb`をReact アプリからVue アプリに渡して、Vueのページで表示したいと思います。

![スクリーンショット 2021-05-23 17.26.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/d082e926-38ae-3099-2578-51d61147648a.png)


## URLパラメータ

URLの末尾にデータを付与して送ります。

### Reactプロジェクト
Iframeに指定するURLのパラメータとして、`?userName=aaa.bbb`を加えます。

```React
import React from 'react';
import Iframe from 'react-iframe'


export default function Page1(){

    return (
        <div>
            <Iframe id = 'page1'
                    url = '＜埋め込みたいサイトURL＞?userName=aaa.bbb'
                    position='absolute'
                    width='80%'
                    height='90%'/>
        </div>
    );
}
```

### Vueプロジェクト

#### Vue Routerのインストール

URLパラメータで送られた情報をVueで取得するには、Vue Routerを使います。Vue Routerの使い方は[こちら](https://qiita.com/hshota28/items/765cf903f055754f7557)を参考にさせていただきました。

VueプロジェクトにVue Routerをインストールします。

```
$ yarn add vue-router
```

#### router.jsの作成

ルーティングを管理するための`router.js`ファイルを作成します。デフォルトはhashモードになっているのですが、hashモードだと値が取れなかったので、historyモードを指定しています。

```router.js
import Vue from 'vue'
import Router from 'vue-router'
import App from './App';

Vue.use(Router);

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'App',
            component: App
        }
    ]
});
```

#### main.jsに追記

`main.js`にも前述で作成した`router.js`をインポートし、 Vue インスタンス作成処理に、`router`をインジェクトします。

```main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
```

#### 値を使う

URLパラメータの値は`this.$route.query.${キー}`で取得することができます。

```Vue
<template>
  <div id='app'>
    <h1>{{ userName }}</h1>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      userName: 'ユーザー名'
    }
  },

  mounted: function(){
    this.userName = this.$route.query.userName
  }
}
</script>
```

これで、`URLパラメータ`で情報を渡すことができました！


## postMessage
`postMessage`を用いてデータを送ります。

### Reactプロジェクト
Iframeの属性に`onLoad`を追加します。そうすると画像を含む画面の読み込みが完了した時に、指定している`loaded`関数が実行されます。

`postMessage`については[こちら](https://qiita.com/yasumodev/items/d339a875b4b9bf65d156)を参考にしました。今回第二引数には、送り先(埋め込みたいサイト)のURLを指定してましたが、送り先を限定したくない場合は`*`を指定することもできます。

```React
import React from 'react';
import Iframe from 'react-iframe'

export default function Page1(){

    const loaded = () => {
        const ifrm = document.getElementById('page1').contentWindow;
        ifrm.postMessage({
            userName: 'aaa.bbb',
        }, '＜埋め込みたいサイトURL＞');
    }

    return (
        <div>
            <Iframe id = 'page1'
                    url = '＜埋め込みたいサイトURL＞'
                    position='absolute'
                    width='80%'
                    height='90%'
                    onLoad={loaded}/>
        </div>
    );
}
```

（余談）
React Iframeのページには、`onLoad`の記載がなく、ここに辿り着くまで結構時間がかかってしまいました。

### Vueプロジェクト
メッセージ受信時に実行される`window.addEventListener`のタイプ`message`を使用して、Reactから送られてくる値をキャッチします。
また、送った値とともに送信元のオリジンも取得できるので、送信元オリジンのチェックも行っています。

```Vue
<template>
  <div id='app'>
    <h1>{{ userName }}</h1>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      userName: 'ユーザー名'
    }
  },

  mounted: function(){
    const self = this
    const accessURL = '＜送信元URL＞'
    window.addEventListener('message', function(event) {
      if(event.origin === accessURL){
        self.userName = event.data.userName
      }
    }, false);
  }
}
</script>
```

これで、`postMessage`で値を渡すことができました！

### つまずいたいたところ

React アプリからVue アプリに値を渡すための`postMessage`を実行するタイミングにつまずいたので、失敗した実装方法も残しておきます。

#### つまずき① React Routerの遷移では反応してくれない問題

```React
// 失敗例
window.onload = function() {
    const ifrm = document.getElementById('page1').contentWindow;
    ifrm.postMessage({
        userName: 'aaa.bbb',
    }, '＜埋め込みたいサイトURL＞');
};
```

`postMessage`について参考にした[サイト](https://qiita.com/yasumodev/items/d339a875b4b9bf65d156)に記載している値の受け取り方をそのまま適用してみました。リロードをすると意図した通りに動作しますが、React Routerの遷移では、`window.onload`は動きません。。

#### つまずき② 埋め込んだサイトの読み込みが終わっていない問題

```React
// 失敗例
useEffect(() => {
    const ifrm = document.getElementById('page1').contentWindow;
    ifrm.postMessage({
        userName: 'aaa.bbb',
    }, '＜埋め込みたいサイトURL＞');
}, []);
```

つまずき①の結果から、コンポーネントのマウント時に実行されるように`useEffect`を使用してみました。もちろんこれだと、React Routerの遷移でもリロードでも動きますが、おそらく動き出したタイミングでは埋め込んだサイトの読み込みが終わっておらず、Vue アプリ側で値を受け取ることができませんでした。
なので、例えば`useEffect`内で1秒待機をしてから`postMessage`の処理が動き出すように実装すると、問題なく動作はしますが、待機か・・という気持ちになります。

その他、`addEventListener`の`pageshow`や`popstate`を使ってみたりしましたが、ことごとく上手くいかなかったです。

## おわりに
埋め込んだサイトに値を渡す方法として`URLパラメータ`と`postMessage`という2つの方法についてまとめました。

実際に渡したかった値がセキュリティの観点からURLパラメータを使用するのは相応しくない値だったので、postMessageを使用するようにしました。ただ、同時に実行すると、URLパラメータのほうがややはやくReact アプリからVue アプリに値を渡せることがわかったので（後述）、セキュリティ的に問題ない値であれば、URLパラメータを使用するのもいいのかなと思いました。

また、今回はReact Routerを使ってVueで作ったWebアプリに飛びたいという要望があったこともあり、`postMessage`は実行タイミングを使いこなすのに時間がかかってしまいました。。
みなさん、こんなときは`onLoad`を使いましょう！他に良い方法がありましたら、コメントください！

### おまけ 実行速度について

同時にするとどっちがはやいか選手権もやってみました。

#### Reactプロジェクト

```React
import React from 'react';
import Iframe from 'react-iframe'

export default function Page1(){

    const loaded = () => {
        const ifrm = document.getElementById('page1').contentWindow;
        ifrm.postMessage({
            userName: 'aaa.bbb',
        }, '＜埋め込みたいサイトURL＞?userName=aaa.bbb');
    }

    return (
        <div>
            <Iframe id = 'page1'
                    url = '＜埋め込みたいサイトURL＞?userName=aaa.bbb'
                    position='absolute'
                    width='80%'
                    height='90%'
                    onLoad={loaded}/>
        </div>
    );
}
```

#### Vue プロジェクト

```Vue
// 省略

<script>
export default {
  name: 'App',
  data() {
    return {
      userNameUrl : '',
      userNamePost : ''
    }
  },

  watch: {
    userNameUrl: function () {
      console.log(this.getTime() + ' URLパラメータ')
    },

    userNamePost: function () {
      console.log(this.getTime() + ' postMessage')
    }
  },

  mounted: function(){
    const self = this
    const accessURL = '＜送信元URL＞'
    window.addEventListener('message', function(event) {
      if(event.origin === accessURL){
        self.userNamePost = event.data.userName
      }
    }, false);
    this.userNameUrl = this.$route.query.userName
  }
}

// 現在時刻の取得省略

</script>
```

#### 実行結果

```
22:32:05:763 URLパラメータ
22:32:05:778 postMessage
```

URLパラメータの方が若干はやいことがわかりました。URLパラメータだと値を送るのに、埋め込んだサイトの読み込みを待たなくていいからですかね。