豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

コールバック地獄を体験したいんじゃ~Promiseへの道~(1)

日記

2018年明けましておめでとうございます。

更新が滞ってました。
年末にかけて仕事が多少忙しくなったこともあり、なかなかまとまって書く時間が取れなかったです。

新年は、39度の熱を出すという散々なスタートとなってしまいましたが、なんとか良い年にすべくがんばっていきたいと思います。

新年ということで、こっそり今年の目標を掲げることにしました。

年末にいくつクリアできたかな!とわくわくできるようにしたいです。

やることリスト

  • スキルセットにLaravel+Vue.jsと書けるようになる。
    どれくらいできれば書いていいのかわからないのですが。なんかそろそろ書いてもいいかなという自信を持てるようになりたいです。

  • C言語で簡易Webサーバーを構築する
    ずっと前からやろうやろうと思って、なかなか手がつけられてないです。 NginxとApacheであげられるC10k問題とかに対してもう少しちゃんと理解できるようにしておきたいです。

  • SPAで簡単なサイトをつくる SPAである必要ってなんもないかもしれないんですが、ちょっとやってみたいです。

  • ChefやらDockerやらをちょっと使えるようにしておく。 Vagrantで仮想環境構築が少しだけわかるようになったので、もう一歩先にいきたいところです。

  • Go言語をさわってみる
    理由はあんまりない。パラダイムシフトみたいなものを感じられらたらいいな。

  • ラズパイで2.4GHz帯のパケット解析
    ドローンも買ったんだけれども自律飛行を目指す企画が頓挫しちゃってる。
    これも進めたい。

  • ブログのアクセス数を倍にしたい 1日あたり200アクセスのところを目指せ400アクセスじゃ!

  • 個人で案件を請け負ってみたい
    一度くらいやってみたい。

  • LPICかAWSの資格あたりを一個とりたい

  • ピンキーと付き合いたい

本題

前回、オブジェクト指向フォームを作成しよう、でこんなかんじにaxiosを使って、データを送るコードを書きました。

www.tohuandkonsome.site

   /**
    * Formのデータをサーバーに送信するよ!
    */
   submit(){
        //プロパティの値を再設定する
        for(let field in this.originalData){
            this.originalData[field] = this[field];
        }
        axios.post('/thread', this.originalData)
        //HTTPリクエストが成功したとき
        .then(response => this.onSuccess(response.data))
        //HTTPリクエエストが失敗した時
        .catch(error => this.onFail(error.response.data.errors));
   }

このコードをベースにして、ファイルをサーバーに送る必要がでてきて、非同期やらコールバックやらPromiseやらaxiosやらasync/awaitやらでてきたので、備忘録がてら綴っていきたいと思います。


やりたいこと

  • ブラウザからファイルを選択して、ファイルを読み込む
  • ファイルの内容に問題がなければ、AWSのS3にアップロードする署名付きURLを発行する
  • 署名付きURLに対してアップロードを行う

こんな感じのことをする必要がでてきました。

PHPでAWSの署名付きURLを発行するのも、いろいろと調べたのでいずれ書いておきたいのですが、ここでは以下のように非同期処理を順次処理をする、という点に注目して書いていきたいと思います。

  • ブラウザからファイルを選択してファイルを読み込む
  • post処理を行う
  • 別のpost処理を行う


さっそくファイルを読み込む

まずは、ファイル読み込み処理を書いてみます。

ここでは、モジュールとして使いまわせるように`ReadFile.js'として切り出しておきます。

ReadFile.js

module.exports = function(file){
    let reader = new FileReader();

    //読み込み終わったあとのイベント
    reader.onload = function(){
        text = reader.result
        console.log('text:'+ text);
    }
    //読み込み開始
    reader.readAsText(file)
}

FileReaderを使った読み込みですね。
そして、このFileReaderですが当然のように非同期で読み込みを行います。
なので、読み込みが完了したときにやっておきたいことは、reader.onloadに関数として書いておけばいけますね!

ここでは、単純に読み込んだテキストの内容をコンソールに出力しているだけです。

では、実際にReadFile.jsを使ってファイルを読み込んでみます。
前回に引き続き、VueインスタンスからReadFile.jsを使うことにします。

こんな感じの画面で、ファイル選択を押下すると

f:id:konoemario:20180110224157p:plain
ファイル選択

以下のapp.jsのonDropイベントが呼ばれて、コンソールにファイルの内容が出力される流れになっています。

app.js

const ReadFile = require('./components/ReadFile');

window.Vue = require('vue');

const app = new Vue({
    el: '.simple-form',
    methods:{
        //ファイルを選択またはドロップ
        onDrop:function(event){
            //ファイルを取得
            let file = event.target.files[0];

            //読み込み
            ReadFile(file);
        }
    },
});


ファイルの内容をチェックする

ファイルを読み込めたので、ファイルの内容をチェックします。

まず、普通に考えるとReadFileが読み込んだテキストの内容を返してくれて、それをもとにチェックするのがわかりやすいですよね!

なんだけれども、ReadFileはtextも返すようにはなっていません。

app.js

const ReadFile = require('./components/ReadFile');

window.Vue = require('vue');

const app = new Vue({
    el: '.simple-form',
    methods:{
        //ファイルを選択またはドロップ
        onDrop:function(event){
            //ファイルを取得
            let file = event.target.files[0];

            //読み込み
           //読み込んだファイルの内容を取得したいんだけれども、、、
            let text = ReadFile(file);
        
            //textの内容を出力したりチェックしたり
            console.log(text);
        }
    },
});

ちょっと`ReadFile.js'に視線を戻して、こんな感じにtextを返してよ!ってやっても返してはくれません。

ReadFile.js

module.exports = function(file){
    let reader = new FileReader();

    //読み込み終わったあとのイベント
    reader.onload = function(){
        text = reader.result
        console.log('text:'+ text);
        //textをかえしてよ!
        return text
    }
    //読み込み開始
    reader.readAsText(file)
}

というのも、ReadFile.jsの中の関数は、reader.readAsText(file)'が終わったら処理を戻してしまって、reader.onload`は待たないからです。

(たぶんあってるんだけれども、シングルスレッドとかマルチスレッドとか、ちゃん理解できてないんだ。
reader.readAsText()はファイルIOを行うのでIOを行っている間CPUは、別の処理を行っていて、ファイルIOが終わったら割り込みが走って、処理していたことを止めるなりなんやらして、reader.onloadで書かれていることが動いて、それが終わったら元の処理に戻る、みたいな感じなのかな。 )

全てはFileReaderが非同期で実装されているせいで、いろいろと頭を悩ませることになっています。
javascript以外の言語、C言語とかCOBOLとかだと同期(直列)処理が当たり前で、効率的に非同期でやりたい、って話がでてきてあれこれと悩むことがあると思うのですが、javascriptなんかは、非同期で実装されているものを、順番に実行したいみたいな逆の悩みがあって、どうしてこう違いがでるんだろう、みたいに思ってます。
javascriptのイベント駆動というワードに引っかかっていますが、未だにイベント駆動の意味がしっくりきません。.NETをさわってるときもイベント駆動みたいな話があったんだけれどもイベントってなんだろうなぁと。。。

話がそれました。

とりあえず、ReadFile.jsが読み込んだ値を返してくれない!チェック処理が書けない!という状態になってしまいます。

ええい、ままよ!と以下のように書くこともできます。

ここでは、ファイルの中に特定の文字が含まれるかを検査する処理を追加しています。

ReadFile.js

module.exports = function(file){
    let reader = new FileReader();

    //読み込み終わったあとのイベント
    reader.onload = function(){
        text = reader.result
        
        //ええいままよ!とチェック処理を直に書く
        //ファイルの中の「おはんき」の出現回数を数える
        let count = (text.match(new RegExp('おはんき','g')) || []).length

        //チェックがOKだったら
        if(count > 0){
            console.log(count)

            //ファイルをサーバーに送る
            //送信がエラーだったらどうしよう
            //そういえばリクエストは2回なげるんだった
        }

    }
    //読み込み開始
    reader.readAsText(file)
}

途中までは、なんだいけるじゃん!と希望にあふれた船出でしたが、チェックがOKだった場合に続けざまに書いていく処理が、サーバー送信だったりして、ここに全部書くのか、、と不安になること間違いありません。

どうしよう、ということでまずとる手法がコールバック関数になります。

コールバック関数

コールバック聞いた当初は、やけに小難しいイメージがありましたが最近は少し慣れました。

単に関数を引数として渡すだけと思えば、そう大したことはありません。

さきほど、ReadFile.jsに書いた機能を関数として、app.jsに書いて、それを'ReadFile.js'に渡してあげます。

app.js

const ReadFile = require('./components/ReadFile');

window.Vue = require('vue');

const app = new Vue({
    el: '.simple-form',
    methods:{
        //ファイルを選択またはドロップ
        onDrop:function(event){
            //ファイルを取得
            let file = event.target.files[0];

            //ReadFile.jsの中でやってほしいことを書く
            const callback = function(text){
                //ファイルの中の「おはんき」の出現回数を数える
                let count = (text.match(new RegExp('おはんき','g')) || []).length

                //チェックがOKだったら
                if(count > 0){
                    console.log(count)
                }
            }

            //定義した関数を渡す
            let text =  ReadFile(file, callback);

    },
});

ReadFile.jsでは、受け取った関数を、reader.onloadが呼ばれるタイミングで実行してあげます。

ReadFile.js

module.exports = function(file){
    let reader = new FileReader();

    //読み込み終わったあとのイベント
    reader.onload = function(){
        text = reader.result
        //callbackを使う
        callback(text)  
        }

    }
    //読み込み開始
    reader.readAsText(file)
}

こうすることで、ReadFile.js'に書いていた処理を渡してあげることで、全部の処理をReadFile.js`に書く必要はなくなりましたね。


でも、ちょっとまってください。 これって結局、書く場所が変わっただけで、この後にサーバー送信をしたいってなったらどうなるんでしょう。

せっかくなんで書いてみることにしましょう。
AjaxでHTTPリクエストを行うxhr.jsを以下のように書いてみました。

xhr.js

module.exports = function(){
     var xhr= new XMLHttpRequest();
     xhr.open("GET","/sample");
     xhr.send(); 

     //リクエストを受信したときのイベント
     xhr.onload = function(){
         if(xhr.readyState === 4 && xhr.status === 0) {
             console.log(xhr.responseText);
           }
     };
}

本来はPOST処理を書くべきところなんですが、いろいろと面倒なのでシンプルにとあるAPIにGETリクエストを呼ぶだけになっています。

/samleをGETすると、ohanky!という文字列が返ってくるだけの素敵なAPIです。

xhrは例のごとく、当然のように非同期で処理がされるため、リクエストが受信し終わった場合の処理は、xhr.onloadに書いてあげます。

さて、こちらのサーバーリクエストは、さきほどのファイル読み込みのチェックが終わった場合に実行したい、としたとき、単純に考えるとこうなりますかね。

app.js

const ReadFile = require('./components/ReadFile');
const Xhr = require('./components/Xhr');

window.Vue = require('vue');

const app = new Vue({
    el: '.simple-form',
    methods:{
        //ファイルを選択またはドロップ
        onDrop:function(event){
            //ファイルを取得
            let file = event.target.files[0];

            //ReadFile.jsの中でやってほしいことを書く
            const callback = function(text){
                //ファイルの中の「おはんき」の出現回数を数える
                let count = (text.match(new RegExp('おはんき','g')) || []).length

                //チェックがOKだったら
                if(count > 0){
                    //サーバーにリクエストを飛ばす
                    Xhr();
                }
            }

            //定義した関数を渡す
            let text =  ReadFile(file, callback);

    },
});

callback関数の中に、サーバーリクエストを行うXhr()を追加しています。

これで、ファイルを読み込んで内容に問題がなかったらサーバーリクエストを行う、ことができるようになりました。


コールバック関数地獄を体験する

さて、

  • ファイルを読み込んで内容の確認を行う
  • 内容に問題がなければサーバーリクエストを行う

ときたので、さらに

  • ファイルを読み込んで内容の確認を行う
  • 内容に問題がなければサーバーリクエストを行う
  • サーバーリクエストに成功したら、別のサーバーリクエストを行う

といってみます。
サーバーリクエストの内容をファイルに書き込む、のほうがそれっぽいのですが、そもそもやりたかったことは、別のサーバーリクエストになるので、このままいきます。

今までのやり方を踏襲するのであれば、こんな感じでしょうか。

まずは、xhr.jsをcallback関数を受け取って実行するようにしときます。
(もちろんcallbackではなくxhr.onloadに書いてもいいんだけれども)
xhr.js

module.exports = function(callback){
     var xhr= new XMLHttpRequest();
     xhr.open("GET","/sample");
     xhr.send(); 

     //リクエストを受信したときのイベント
     xhr.onload = function(){
         if(xhr.readyState === 4 && xhr.status === 0) {
             console.log(xhr.responseText);

             //成功したらあとにコールバック関数を実行する
             callback();
           }
     };
}

次に、xhr.jsに処理させたい関数をcallbackAfterRequestという微妙な名前で作成しておき、Xhr()関数に渡してあげます。

app.js

const ReadFile = require('./components/ReadFile');
const Xhr = require('./components/Xhr');

window.Vue = require('vue');

const app = new Vue({
    el: '.simple-form',
    methods:{
        //ファイルを選択またはドロップ
        onDrop:function(event){
            //ファイルを取得
            let file = event.target.files[0];

            /// GET /sample1 した後の処理を書く
            const callbackAfterRequest = function(){
                var xhr= new XMLHttpRequest();
                xhr.open("GET","/other");
                xhr.send(); 

                //リクエストを受信したときのイベント
                xhr.onload = function(){
                    if(xhr.readyState === 4 && xhr.status === 200) {
                        console.log(xhr.responseText);
                      }
                };
            }

            //ReadFile.jsの中でやってほしいことを書く
            const callback = function(text){
                //ファイルの中の「おはんき」の出現回数を数える
                let count = (text.match(new RegExp('おはんき','g')) || []).length

                //チェックがOKだったら
                if(count > 0){
                    //サーバーにリクエストを飛ばす
                    Xhr(callbackAfterRequest);
                }
            }

            //定義した関数を渡す
            let text =  ReadFile(file, callback);

    },
});

これぐらいであれば、なんだ全然いけるじゃないか!と思うかもしれませんが、パッと見てどこがらどう処理が流れているのかがすごいわかりにくいです。

プログラムは上から下に流れると思いきや、後で動く処理が延々と書かれており、それがどのような順序で動くのかってなかなかわかりづらい印象を受けます。

ということで長くなったので次回Promise、そしてHTTPリクエストはaxiosを使って書いていきたいと思います。