【javascript】非同期通信を同期処理する

フロントエンドで初期データを外部APIから取得したい場合があります。

データ通信はajax を用いるのは一般的です。
ajaxでデータを取得して画面内の要素を更新したり、スクリプト内の変数に変更を加えたりという事をします。

この通信をあらかじめ済ませておきたい場合はどうしたら良いでしょうか?
今回は、通信を待ってから(同期してから)処理を行う方法について触れます。

目次

「非同期通信を同期処理する」とは

外部APIデータをajax経由で取得する時のフロントエンドを例にします。
例えば下記の2つの要件を実装したいとします。

  • Youtubeプレイヤーをページ内に埋め込む
  • 5秒毎に外部データサーバからデータを取得する

同期通信と非同期通信について

Youtubeプレイヤーは、再生ボタンを押されたタイミングでYoutubeのサーバから動画データを取得します。

プログラムは上から下に処理する原則があり、通信も例外ではありません。
もし、通信の送受信が終わるまで次の通信ができないのであれば、外部データサーバとのやりとりは止める必要が出てきます。
この状態が同期通信です。

外部データサーバからデータを取得する処理も常に動かしておきたいのであれば、Youtubeのプレイーヤーの通信と並行して通信できるようにします。
これが非同期通信です。

f:id:nakahashi_h:20210515164444j:plain

同期処理と非同期処理について

f:id:nakahashi_h:20210515164220j:plain

ajax通信でデータを取得した後は、通信結果をもとにコールバック処理を実行します。
コールバックの中でページ内の要素の更新を行ったり、関数を実行する事で描画に変化を加えます。

この通信からコールバックまでの間、フロントエンドの処理は止まりません。
「ajaxで通信処理をする」という処理が終わったとみなされて、次の処理に移行します。
このようにajaxの通信を待たない状態を非同期処理と言います。

勿論、ajaxの通信結果をあえて待つこともできます。
これを同期処理と言います。

本件では、通信処理は非同期で行うが、通信結果を待って処理を行う状態の事を、
非同期通信を同期処理する」と言います。

今回の目標

今回はVue.jsを用いて、API用のURLからデータを取得するスクリプトを作成します。
データの通信はコンポーネントのmounted 時に行います。

使用するライブラリ

ajax通信はaxios を用います。

ライブラリ 用途
Vue.js フレームワーク
axios ajax通信用のライブラリ

ソース

処理の流れがわかるように、コンソールログのコメントを表示できるようにします。

コンポーネント

propsで設定されたkey 属性を元に、外部APIからjsonでデータを取得するようにします。

// example.vue
<script>
    import axios from "axios";
    export default {
        data: function() {
            return {
                 values: []
            };
        },
        props: [
            key
        ],
        async mounted() {
            console.log('start'); // 処理開始
            const apiUrl = 'https://example.com/keys/' + key;
            await axios.get(apiUrl)
                .then(function (response) {
                    this.values = response.data.questions;
                    console.log('got')  // 取得完了
                }.bind(this))
                .catch(function (error) {
                    console.log(error)
                });
            console.log('end'); // 処理終了
        },
    }
</script>

GETメソッドで使用したaxios.get() の戻り値は、Promise 型です。
developer.mozilla.org

そのため、awaitを指定するだけで通信の同期を待つことが出来ます。

エントリーポイント

先ほど作成したコンポーネントを読み込みむ設定を記述します。

// app.js
import Vue from 'vue';

import Example from './example';
Vue.component('example', Example);

let app = new Vue({
    el: '#app',
})

Dom

コンポーネントを呼び出し、key 属性に固有値を設定します。

<html>
<body>
     <div id="app">
          <example :key="1">
     </div>
     <script src="./bundle.js"></script>
</body>
</html>

この状態でブラウザのコンソールを確認すると次のように、同期してから処理できている事が確認できます。  

start
got 
end

外部ファイルに処理を切り出す

別のコンポーネントで使いたい場合、mixinを使ってmethod を共通化する方法があります。
v3.ja.vuejs.org

前項の処理で外部から取得していた処理を別ファイル分解していきます。

mixin用コンポーネント

新しくmixin.js ファイルを作成します。
作成したファイルに通信処理部分を切り出します。

// ./mixin.js
import axios from "axios";
export default {
    methods: {
        getValues: async function (key) {
            const apiUrl = 'https://example.com/keys/' + key;
            let tmpValue = [];
            await axios.get(apiUrl)
                .then(function (response) {
                    tmpValue = response.data.questions;
                }.bind(tmpValue))
                .catch(function (error) {
                    console.log(error)
                });
            return tmpValue;
        },

コンポーネントの設定

外部化したファイルをimport して、設定を上書き(マージ)します。

axiosの通信結果を待つ必要があるため、mountedのメソッドは asyncを対象にします。

// ./example.vue
<script>
    import CommonMixin from '../mixin';
    export default {
        data: function() {
            return {
                 values: []
            };
        },
        mixins: [CommonMixin], 
        props: [
            key
        ],
        async mounted() {
            this.values = await this.getValues(this.key);
        },
    }
</script>

Tips

babelでasync, awaitを使う時の注意

axiosの注意書きにもありますが、IEのような古いブラウザではasync, await を扱えません。

NOTE: async/await is part of ECMAScript 2017 and is not supported in Internet Explorer and older browsers, so use with caution.

github.com

babel を用いる事でIE用に変換します。
しかし、babel単体では解決しません。
async, await 用のモジュールとライブラリをインストールする必要があります。

導入しないでビルドを実行した場合

今回、webpack を使用しているのですが、特に対策をしていない場合そのままビルドすると、ブラウザのコンソール上で次のようなエラーが発生します。

Uncaught ReferenceError: regeneratorRuntime is not defined

ライブラリをインストール

$ npm install @babel/runtime
$ npm install --save-dev @babel/plugin-transform-runtime

webpack.configの設定

今回はwebpackでビルドしました。
babelのプラグインを導入します。

module.exports = {
    entry: './js/app.js',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    presets: [
                        "@babel/preset-env"
                    ],
                    plugins: [
                        "@babel/plugin-transform-runtime"
                    ]
                }
            },

        }
    }
}

エントリーポイントにライブラリ、プラグイン読み込み処理追加

導入したライブラリ、プラグインをスクリプト内でimport します。

import "core-js/stable"
import "regenerator-runtime/runtime";


以上です。

axiosについては公式ドキュメントを参考にしました。
今回取り上げませんが、複数の外部データ取得を全て同期するまで待つ方法についても説明されています。

github.com

©︎2017-2018 WebSandBag