はらへり日記

腹に弾丸

これからのJSの非同期処理関数は全てPromiseを返させるべき

はじめに

JSで非同期関数を書く時、個人的に意識してる話です。

別にTipsとかじゃないです。

要するにポエムです。

あしからず(´・ω・`)

非同期関数を使いこなす

JSを書いたことがある人なら知ってるであろう非同期処理ですが、僕は非同期処理はなるべくPromiseオブジェクトを返す関数に切り分けていくべきだと思っています。

極端な例として「APIその1を叩いて、その結果からAPIその2を叩いて、その結果からAPIその3を叩いて、それを画面に描画するぞい!」というコードを書いてみます。

悪い例:関数で処理をラップしない

APIが綺麗に整っていれば最高ですが、現実は得てしてあまくありません。

3種類のAPIを叩くわけですがそれらのレスポンススキーマは全て違うとしましょう。

const firstApiUrl = 'http://api1.com';
const secondApiUrl = 'http://api2.com';
const thirdApiUrl = 'http://api3.com';

// 自前で定義したajax()関数をがあるとします
ajax(firstApiUrl, { data: 'data' }, (err, res) => {
  if (err) {
    alert('APIその1のエラーだよ');
    return;
  }
  
  if (res.status) {
    // APIその1から返ったデータを使う
    ajax(secondApiUrl, { data: res.body.data }, (err, res) => {
      if (err) {
        alert('APIその2のエラーだよ');
        return;
      }
      
      if (res.result) {
        // APIその2から返ったデータを使う
        ajax(thirdApiUrl, { data: res.count }, (err, res) => {
          if (err) {
            alert('APIその3のエラーだよ!');
          } 
          
          $('body').append(res.html);
        });
      }
    });
  }
});

はい。いわゆるネスト地獄ですね。

このコードの悪いところをざっとあげてみると

  • ネストが深すぎて可読性が低い
  • レスポンススキーマAPIごとに違うので同じような処理なのにデータの取り出し方が違ってなんか気持ち悪い(res.body.data, res.result, res.count.....)
  • 『上司「APIその2のレスポンススキーマ変わるから対応してよ」』って時にその処理部分を探すのが大変(grepかけますか???)

悪い例:コールバック関数を引数に取る関数を書く

先ほどあがっていた悪い点のうち、2点目を解決してみましょう。

処理を関数でわけてみます。

/**
 * @description APIその1と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxFirstApi = (data, callback) => {
  let baseUrl = 'http://api1.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその1の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.body.data);
  })
};

/**
 * @description APIその2と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxSecondApi = (data, callback) => {
  let baseUrl = 'http://api2.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその2の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.result);
  })
};

/**
 * @description APIその3と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxThirdApi = (data, callback) => {
  let baseUrl = 'http://api3.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその3の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.count);
  })
};

ajaxFirstApi('data', (err, result) => {
  if (result) {
    ajaxSecondApi(result, (err, result) => {
      if (result) {
        ajaxThirdApi(result, (err, result) => {
          if (result) {
            $('body').append(result);
          }
        }
      }
    });
  }
});

コード量が増えたものの、先程よりは多少よくなったのではないでしょうか。

最初の例と比べると以下の点が改善されたかと思います。

  • それぞれのAPIへのアクセスを関数化したので使い回しがきく/修正が容易
  • レスポンススキーマについては関数の中で完結してるので実際に関数を使う時に意識しなくてよい

しかし、以下の問題については未だ未解決です。

  • ネストが深くて可読性が低い

また、このアプローチだと以下のような関数が混ざった時に混乱します。

/**
 * @description APIその4と通信
 * @param {mixed} data
 * @param {Function} calllback
 */
const ajaxForthApi = (data, callback) => {
  ajax('http://api4.com', { data: data }, (err, res) => {
    callback(res, err); // ここに注目!!!
  });
};

先ほど定義したajaxFirstApi(), ajaxSecondApi(), ajaxThirdApi()ではcallbackに渡される変数の順番が最初にerr, ついでresとなっていました。

しかしここで定義されている関数ではその順番が逆になっています。

これには2つの問題があり、

  • 関数の使用方法が統一されない コーディングルールで決めることは可能だがLint等の自動ツールでのチェックはほぼ不可能であり、実装者を完全に信用しなければいけない

  • コールバック関数に渡される引数の順番を知らないと関数が使えない ajaxFirstApi('data', (err, res) => {});というように使いたい時、第二引数の関数に最終的に何が渡されるかを実装者は知る必要があります。

丁寧にJSDoc等でコメントが書かれていればそれで済みますが、世のコードの9割はそんなに親切ではなく結局実装を追う羽目になることが多いと思います。(せっかく関数化したのに…)

良い例:全ての関数にPromiseを返させる

待たせたな!ここでPromiseを使ってみましょう!

/**
 * @description APIその1と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxFirstApi = (data) => {
  let baseUrl = 'http://api1.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその1の通信エラーです');
        reject(err);
      }
    
      resolve(res.body.data);
    });
  })
};

/**
 * @description APIその2と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxSecondApi = (data) => {
  let baseUrl = 'http://api2.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその2の通信エラーです');
        reject(err);
      }
    
      resolve(res.result);
    });
  })
};

/**
 * @description APIその3と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxThirdApi = (data) => {
  let baseUrl = 'http://api3.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその3の通信エラーです');
        reject(err);
      }
    
      resolve(res.count);
    });
  })
};

ajaxFirstApi('data') 
  .then((result) => {
    return ajaxSecondApi(result);
  })
  .then((result) => {
    return ajaxThirdApi(result);
  })
  .then((result) => {
    $('body').append(result);
  })
  .catch((err) => {
    alert('通信エラーです');
  });

// 省略してこう書くことも可能

ajaxFirstApi('data') 
  .then(ajaxSecondApi)
  .then(ajaxThirdApi)
  .then((result) => {
    $('body').append(result);
  })
  .catch((err) => {
    alert('通信エラーです');
  });

さぁ!前回の例での問題が解決されました。

前回の問題として以下のものがありました。

  • ネストが深い
  • コールバック関数に渡される引数の順番を知る必要がある

コード量は増えましたが、Promiseを返す関数はthen()catch()を使ってつなげることができます。

最新のJSではAPIでもこのthenableの考え方が浸透しており、これを使って非同期処理の順番をネストの深さを変えずにつなげることが可能になります。

コールバック関数に渡される引数の順番についても知る必要はなくなりました。

Promiseを返す関数ではresolve()reject()の2種類に縛られます。

前者は成功時の返り値、後者はエラー時の返り値となります。

そしてそれらをthen()catch()で捉えて、行いたい処理を書きます。

ね、簡単でしょう?

何が嬉しいのか

嬉しさポイントは見た目が綺麗になることもありますが、個人的にはコールバック関数の形式が強制的にresolve(), reject()で縛られるのがいいかなと思います。

今まではコールバック関数に何が返ってくるのかドキュメントを読む必要がありましたが、Promise化されていれば成功か失敗かで考えるだけで済みます。

あるAPIを叩く関数をPromise化しておけば内部実装がどうなっていようと使う側はthen()で結果を受け取り、catch()でエラーハンドリングを行えばよいのです。

ある程度冗長には見えますがこれからはPromiseが常識になっていきますし非同期関数は全部Promiseでいいんじゃないかなと思ってます。

実際に使いたいけど動かないんでしょう?

いいえ!動きます。

IE8まではpolyfillライブラリであるes6-promiseを使用することで問題なく使うことができます。

もっと知りたい

Promiseの本がおすすめです。

azu.github.io

まとめ

みんなPromiseで幸せになろう!!!!!!!!!必ずなろう!!!!!!!

Babel + Browserifyで環境変数を使用する

前提

Babel6系でES2015のJSをBrowserifyを使用してコンパイルします。

願い

JSをコンパイルする際、Ajax通信で使用するURIを開発と本番で分けたい場面がありました。

なので以下の様なことがしたい。

let uriPrefix = '/api';

if (process.env.APP_ENV === 'production') {
  uriPrefix = '/api/production';
}

export const apiUri = `${uriPrefix}/get/sushi`;

ただ、普通にやると process.env.APP_ENVなんてブラウザに存在しないのでundefinedとなってしまう。

やり方

Babelのプラグインであるbabel-plugin-transform-inline-environment-variables を使用します。

babel/packages/babel-plugin-transform-inline-environment-variables at master · babel/babel · GitHub

プラグインの導入方法はよしなに。

ひとまず必須なのはプラグインのインストール。以下のコマンドを叩く。

$ npm i babel-plugin-transform-inline-environment-variables --save-dev

その後、それぞれのコンパイル方法でプラグインを指定する。

以下はGulpを使用した場合の例です。

import gulp       from 'gulp';
import browserify from 'browserify';
import source     from 'vinyl-source-stream';

gulp.task('js', () => {
  browserify
    .transform(babelify, {
      entries: ['js/app.js'],
      presets: ['es2015'],
      plugins: ['transform-inline-environment-variables']
    })
    .bundle()
    .pipe(source('main.js'))
    .pipe(gul.dest('./public/js'));
});

こうすることでprocess.envが参照できるようになります。

ぐう便利!!!

Laravel5.1でsuperagentを使用する際の注意点

環境

ajax通信にsuperagentを使用したい

JS弱者なりに「jQueryから自立したい…!」と感じ、Ajax通信を$.ajaxでなくsuperagentというものを採用しました。

かのexpressやstylusを開発したTJ作ということもあり、とても使いやすい!

import request from 'supreagent';

request.get('/api/hoge')
  .query({ num: 5 })
  .end((err, res) => {
    if (err) console.error(err);
    console.log(res.body);
  });

ただ、コイツをLaravel5.1で使った時にハマりました。

Request::ajax()の罠

LaravelではHTTPリクエストが¥Illuminate¥Http¥Requestインスタンスとしてアプリケーションに渡されます。

また、その内容であらかじめ処理を分けるためにミドルウェアを使用することができます。

その中で「Ajax通信の場合、エラーメッセージを返す」といった処理をしたい場合は下記のようなミドルウェアを書くことで実現できます。

// 前後略

    /**
     * @param ¥Illuminate¥Http¥Request  $request
     * @param ¥Clouser  $next
     * @return mixed
     */
    public function handle($request, Clouser $next)
    {
        // リクエストがAjaxの場合、エラーメッセージを返す
        if ($request->ajax()) {
            return response()->json([ message => 'Server error, ajax is not allowed...' ]);
        }
        
        return $next($request);
    }

このajax()部分で少しハマりました。

superagentのリクエストはajax()に引っかからない

このajax()関数を使用してsuperagentのAjaxリクエストを判別しようとしたところ、どうやっても$request->ajax()falseを返していました。

どういうことだと思いコードを読むと\Symfony\Component\HttpFoundation\RequestisXmlHttpRequest()を使用していることが分かりました。

ここだけ抜粋すると以下の処理を行っています。

    public function isXmlHttpRequest()
    {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

X-Requested-Withヘッダーの中身を見て真偽値を返しているだけですね。

つまりはこのヘッダーが無いとajax()がきちんと動きません。

ここでおもむろにsuperagenthistory.mdを読んでみます。

そう、3年も前に廃止されています。

該当のIssueを見てみると「jqueryのヘッダだしセンスないよ」とだけTJが言ってます。

remove x-requested-with · Issue #189 · visionmedia/superagent · GitHub

このヘッダの出処をきちんと調べられてないですが、いろいろ調べてみるとこのヘッダがHTTP公式のものでなく独自ヘッダであることは確かなようです。

解決策

とはいえ、独自ヘッダだから使わないというわけにもいきません。

解決策はいくらでもあると思いますが、ヘッダが必要なら足せばよいのです。

私の場合、以下の様なsuperagent用ヘルパーメソッドを1つ用意し、それを全てのリクエストに噛ませるようにしています。

(ついでにCSRFトークンも渡せるようにしてたりします。)

/**
 * @description Laravelに送信するためのヘッダーを付与したsuperagentを返す
 * @method addLaravelHeader
 * @param {object} request - superagent object
 * @param {string} csrfToken
 * @return {object} superagent
 */
export function addLaravelHeader(request, csrfToken) {
  return request
    .set('X-CSRF-TOKEN', csrfToken)
    .set('X-Requested-With', 'XMLHttpRequest');
}

こうすることで$request->ajax()した時にAjaxリクエストだと認識してくれるようになります。

使い方は以下のとおり。ファイル名はよしなに。

import request from 'superagent';
import { addLaravelHeader } from './superagent-helper';

let csrfToken = 'csrf-token'; // CSRFトークンをよしなに取得
addLaravelHeader(request.get('/api/hoge'), csrfToken)
  .query({ num: 3 })
  .end((err, res) => {
    // 煮るなり焼くなり
  });

以上!

雑兵MeetUp #3でLTしました

雑兵MeetUpに行って来た

詳しくはイベントページをどうぞ。

zohyo.connpass.com

第一回目でもLTしたのだけど、2回目飛ばしての参加でした。

LTをした

年末ポエムでも言ってたが人前で喋る機会を増やしたく、LTした。

スライドはこちら。

speakerdeck.com

DBガチ勢が見たらもしかしたら突っ込みどころがあるのではと思いつつ発表。

個人的な信条である時間厳守を外れてしまったので本当に反省。。。

スライドの捕捉

ひとことで言うと「正規化大事だからDB設計する時は正規化しような!」って話でした。

ただ、そもそもなんで正規化する必要があるのか。という部分を掘り下げて考えるきっかけになればと思いお話しました。

そこらへんは社内勉強会でお話したのでよければそちらもどうぞ。

www.slideshare.net

要するに

LT後の懇親会で飲んだ酒が最高に美味かった。

雑兵同士今後も仲良くしてください。

2015年振り返り

振り返る。

卒業・入社

4年間お世話になった大学を卒業しました。

卒業してからもなんだかんだ友人と飲んでる気がするので引き続きオナシャス!!

勉強したこと

一部抜けてるが、読んだ本達はこちら

主に以下のものたち。

基礎力が圧倒的に足りなかったのでそこらへんを中心に勉強してた。

特にデータベース周りが本当にできなくてつらいので引き続き勉強する。

来年中は無理だが、再来年までにスペシャリスト3つを取りたい。

作ったもの

hubot-reviewer-choice

会社でレビュアー選びやすくするために作った。

結構使われてて、作ってよかった。

詳しくはこちら。

sota1235.hatenablog.com

Party

React.js + Flux製、オールスター感謝祭風クイズアプリ。(宴会用)

くわしくはこちらのエントリ。

sota1235.hatenablog.com

初めてリポジトリ複数スターが付いて嬉しかったです

owl

これは作ったのではなく、コントリビュートさせてもらったOSSです。(@fortkle氏作の情報共有アプリです)

コツコツプリリクを送っていたらコントリビューターとしてチームに加えてもらったのが嬉しかった。

社内でも使っているしまだまだ改善の余地があるので来年も引き続き開発したい。

GitHub - owl/owl

トライしたこと

初めてLTした

雑兵MeetUp #1で初めてLTした。

www.slideshare.net

正直、大学生の頃から強い人達に対する劣等感とか強くて対外発表に対するハードルが高かったのだが、今年はめでたく小さな一歩を踏み出せたので来年は積極的に発表していきたい。

なるべく社内勉強会で多く発表した

今年振り返ると以下のテーマでやっていた。

見ての通り、誰もが知る基礎の話を多く発表した。

自分の勉強はもちろん、自分の姿勢を社内にアピールしたり間違ってることを指摘してもらったりするために頑張った。

来年は少しずつレベルと頻度をあげてお届けしていきたい。

アドベントカレンダーをやった

会社でアドベントカレンダーをごり押しして、やらせてもらった。

直前にも関わらずみんな執筆を快諾してくれて無事全部埋まりました。

(アドベントカレンダーこちら)

来年はもう少し早く準備し始めて人被り減らしたい。

新入社員っぽいことをした

新卒1年目なので宴会芸を夏と冬の2回準備した。

徹夜ギリギリのことをしたり、映像制作をしてる時期はコードが全然書けなくて苦しかったがなんとか乗り越えられた。

(映像作ったのにGitHubみたいに成果を対外に発表できないのが悔しいのでここに冬のオープニング映像だけしれっと載せておく)

冬納会オープニング

www.youtube.com

冬はコーディングで宴会を作ることもできたし、いろいろあったけど社員の方にはそれなりに楽しんでもらえたと思う。

来年の目標

来年書けよって感じだけど、来年は以下を目標に頑張りたい。

  • OSS活動の加速

今、作りたいと思ったものがあった時にそれを形にする能力が低すぎるのでもっとスピードと技術力を磨いていきたい。

そして願わくばみんなに使われるものをどんどん生み出したい。

  • 対外発表

外部で発表し、自分自身を発信していきたい。

願わくばPHPカンファレンスに登壇したい。(!!!)

  • お賃金アップ

というのは冗談だが、まず社内でスペシャリスト方面のエンジニアとして仕事、エンジニアリングともども上位を目指していきたい。

よいお年を

プライベートもお仕事も割りとつらいことの方が多かったかもしれない今年でした。(凶も2回引いたからね!!)

でもそれ相応の断捨離はできたつもりなので来年はさっぱり改めて頑張りたいと思います。

引き続きコーディングかお散歩かお酒という日々になる気がするのでみなさん飲みましょう。

良いお年を。