フロントエンド開発Blog

オレには鈍器がある

このエントリーをはてなブックマークに追加

JavaScript , 検索

JavaScriptを書いてて「文字列の中に特定の文字が含まれるか否か」を判定したいとき、皆さんだったらどうやって書きますか?私は長いこと「ただの文字列ならindexOf」「正規表現なら.test」を使ってました。

これは5年位前に計測したときにindexOfが早かったのを覚えていてそのときの経験則からそうしていただけに過ぎません。

ここらでもう一回計測してみようと思い立って試験的にコードを書いてみました。

performance.nowを使って計測しています。なお、計測結果はconsoleに出力されます。

ES2015 , JavaScript , Material-ui , React , Redux , babel , webpack

以前、LAMP環境で動作するcakePHP製の家計簿アプリのフロントエンドを刷新しました。(以前の記事:WEB上で出費を管理「ふたりの家計簿」を公開しました

掲題の通りReact + Redux + Material-UIを組み合わせてみました。コンパイルにはWebpack, Babel、文法チェックはESLintを使用しました。

コンパイル環境にはtakanabe様の「Material UIを使ってカッコいいUIのReactアプリケーションを作ってみた」をベースにしました。1から自分で作っていたらもっと大変だったと思います。この場を借りて謝辞とさせていただきます。ありがとうございます!

少し解説

Reduxということで

  1. action creatorがactionを発行
  2. reducerがactionから新しいstateを発行し、containerに渡す
  3. containerから各componentにpropsとしてstate情報を配給

という基本型を意識して作りました。かいつまんで説明していきます。

エントリーポイント

src/index.jsx

webpackのエントリーポイントです。reactのマウント、routerの設定、初回アクセス時に発行するアクションの設定を行っています。

ページにアクセスしたばかりの状態だとstateは空の状態なので、AJAXで現在年月データを取得し、stateに反映する必要があります。

import { fetchMonthlyData } from '../actions/monthly';
const pageHistory = syncHistoryWithStore(browserHistory, store);
pageHistory.listen(function(location){
  // 月別データ表示
  if(location.pathname.match(/monthly/)){
    store.dispatch(fetchMonthlyData({start: location.pathname.slice(-6)}));
  }
  // インデックスに戻ってきた際、PHP側でtplに吐き出された現在年月から該当月データを取得
  if(location.pathname === '/kakeibo/forms/index'){
    store.dispatch(fetchMonthlyData({start: startYear + startMonth}));
  }
});

このように、/kakeibo/forms/indexというトップページにアクセスしたら月データ取得AJAXを走らせるようにしています。

このlistenというのが肝になりますね。特定のURLパターンにマッチしたら最短で実行したい処理をここに書きます。

今回は月別アーカイブ、トップの現在月データ取得の2つのパターンでlistenを用意しています。

エントリーポイント配下に複数のコンポーネントがぶら下がっている形になりますが、全てを説明しているときりがないのでTOPでも使っている月間データ取得部分に焦点を絞って解説します。

action

actions/monthly.jsx

(これがベストな方法かはさておき)非同期処理はActionで書く方針にしました。取得中、取得成功、取得失敗の3パターンのアクションクリエイターを用意し、reducerにactionを渡します。

/**
 * 月別データ取得AJAXの通信アクション
 * @return {Object} 削除通信中アクション->通信完了アクション
 */
export function fetchMonthlyData(data) {
  let params = new URLSearchParams();
  let i;

  // AJAXパラメータオブジェクトを生成
  for(i in data){
    params.append(i, data[i]);
  }

  return dispatch => {
    // 通信中アクションを実行
    dispatch(fetchMonthlyDataRequest(data));

    // AJAX
    Axios.post(
      ajaxUrl.getMonthData,
      params,
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    ).then(
      response => dispatch(fetchMonthlyDataResult(response.data))
    ).catch(
      () => dispatch(fetchMonthlyDataResult({status: "0", msg: "月別データの取得に失敗しました。通信状況を確認してください"}))
    )
  }
}

exportするのはこの関数だけです。AxiosというAJAXライブラリを使用しています。Axios自体の使い勝手は賛否あるようですが、単純にPOSTするだけなら問題は・・・ひとつだけありました。

headersを指定しないとpayloadとして送信するため、jQuery感覚で送信してもだめでした。PHP側での値の取得部分に手を加える必要がでてしまい、今回のガワ変更という趣旨から外れます。(PHPに手を入れるのは最低限にしたかった)

そこでheadersにContent-Typeを指定したのですが、そうすると今度は送信データをObject作って渡してもString型になってしまいました。URLSearchParamsを使うと解決できるようですがmobileブラウザでまったく動きません。

最後まで悩みましたがpolyfillを使わせていただきました。

これであっさり解決しちゃいました!

monthly以外にもAJAXを絡めたaction creatorはありますが、どれも構造自体は一緒です(del.jsx、edit.jsx、year.jsx)

reducer

reducers/monthly.jsx

超完結です。action.typeに応じて決まったstateを返すだけです。

FETCHED_POSTS_SUCCESSとFETCHED_POSTS_FAILUREにはAJAXで取得したdataをぶら下げてstateとして発行しています。reducerの処理はシンプルなのでコードは割愛します。

Container

containers/App.jsx

コンポーネントを包んでいるコンテナです。routerなどで直接マウントされる対象、ともいえます。

function mapStateToProps(state, ownProps) {
  return {
    monthly: state.monthly,
    drawmenu: state.drawmenu,
    del: state.del
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actionsMonthly: bindActionCreators(Monthly, dispatch),
    actionsDrawMenu: bindActionCreators(DrawMenu, dispatch),
    actionsDel: bindActionCreators(Del, dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

先ほどreducerで発行したstateは各コンポーネントに渡す必要があります。mapStateToPropsでオブジェクト形式でstateをpropsに変換して返します。こうすることでAppコンテナ内ではstateをthis.propsの形で参照できるようになります。

action creatorも同様に、コンポーネントに渡すことが可能です。mapDispatchToPropsの部分が該当部分になります。

reduxのデータの流れは以上になります。コンポーネント以下はviewの機能を担うreactがメインになります。コンポーネント全てを解説していると長大になるので割愛しますが、基本的には上記のconnectで割り当てたstateを使ってrenderを制御し、ユーザー操作を受けたらaction creator経由でstateを更新する流れになります。

Container

components/DetailTable.jsx

DetailTableを例にしてみてみると、renderの中で先ほどconnectで割り当てたstateをprops経由で表示制御に使用しています。また、ボタンイベントにはaction creatorを叩く処理が割り当てられています。

基本的なComponentの作りは皆一緒です。

実際に使ってみて

実は今回reactもreduxもmaterial-uiも初めてでした。とっつきやすくはありませんが、action、reducer、container、componentのように役割がしっかり分かれているため、コードを読むときに便利だなーと思います。特に人のソースコードを見るときはいいかもしれません。お互いが同じルールブックを元にコードを書いたり読んだりできるから。

reduxやreact自身のパワーもさることながら、統一化・画一化された規約という意味でもreduxやreactは有益かもしれません。

FIleAPI , JavaScript

フォルダをドラッグアンドドロップするとファイル一覧を出力するだけのWEBアプリです。
ロジック的には単純で、

  1. dataTransfer.itemsから最初のFileをエントリーポイントと定義
  2. エントリーポイントの該当階層を探査し、ファイルならパスを取得、ディレクトリなら更にエントリーポイントとして再帰処理
  3. ディレクトリが見つからなくなるまで処理を続け、最後に出力

という感じです。

ただ、全ファイルの探査が完了したか否かのロジックのためにPromiseを入れ子にしている点がちょっと変わっているなーと自分事ながら思います。母数が不明な非同期処理における終端を判定するひとつの解としてみていただければと。。

docker , dockerfile , frontend , gulp , webpack

前回、VagrantとAnsibleでフロントエンド開発環境を作るで既に仮想マシン上に開発環境を整えることができました。

今度はDockerで同じようなことができないか試してみました。

参考にさせていただきました

ぱいそんにっきさんの「Docker でフロントエンド開発も楽できるか?」を超参考にさせていただきました。ありがとうございます!とても分かりやすくて助かりました!

Dockerfile

ほぼほぼぱおそんにっきさんのDockerfileまんまです。とりあえず、Ruby2.1が元々入っているimageをcloneして作りました。rubyのバージョンは使いたいバージョンなら何でもOKなはずです。マシンの設定はこざっぱりとDockerfileだけで完結させました。

### Dockerfile

# Pull base image.
FROM ruby:2.1

# Timezone Setting
RUN echo "Asia/Tokyo" > /etc/timezone

# Install Debian
RUN apt-get update
RUN apt-get dist-upgrade -y

# Install Ruby.

# Install Gems
RUN gem install compass

# Install Node.js
RUN \
  cd /tmp && \
  wget http://nodejs.org/dist/v4.4.5/node-v4.4.5.tar.gz && \
  tar xvzf node-v4.4.5.tar.gz && \
  rm -f node-v4.4.5.tar.gz && \
  cd node-v* && \
  ./configure && \
  CXX="g++ -Wno-unused-local-typedefs" make && \
  CXX="g++ -Wno-unused-local-typedefs" make install && \
  cd /tmp && \
  rm -rf /tmp/node-v* && \
  npm install -g npm && \
  echo -e '\n# Node.js\nexport PATH="node_modules/.bin:$PATH"' >> /root/.bashrc

# Install Node modules
RUN \
  npm install -g grunt-cli gulp webpack bower

# Define working directory.
WORKDIR /data

# set up


# Define default command.
CMD ["bash"]

### IMPORTANT !! ###
# you can build image this command
# docker build -t nodejs:v4.4.5 ./

# you can run this
# docker run -itd -v /c/Users/{username}/works/:/data/ nodejs:v4.4.5 /bin/bash

# if you run npm i on docker container on windows host, you must choose option --no-bin-links
# https://docs.npmjs.com/cli/install

vagrant + ansibleのときと比べると、rbenvを使っていないからかもしれませんが随分と短くてすみました。

使ってみた所感としては、Dockerの場合は基本的にhostとの共有フォルダをuserフォルダ以下にしか作れない点が大きな制約になりますね。Dockerは作っては壊す、というルーチンがカンタンで起動や破棄が高速な点が良く、トライアルアンドエラーがvagrantほどしんどくないのが最大の強みかなと思います。

AI , ES2015 , JavaScript , Mithril , babel , docomoAPI , webpack , チャット , 罵り

Mithril.jsの勉強を始めて5日くらいですこんにちは。

勉強がてらに何か一本、WEBアプリを作ってみようと思い立って作りましたのはこちら、「罵りAIチャット」です。

※このサービスに関するお問い合わせは twitter または facebookページ からご連絡ください。

Mithril.jsの勉強をしていく中で実践経験を詰みたくなり、そんなときにdocomo APIの「雑談API」を見てふと思いつきで作りました。

フロントエンド

バックエンド

バックエンドは至ってシンプルでクロスドメインを解決するため、中継処理をPHPスクリプトで行っています。

フロントエンドは今回メインとなるMVCフレームワークMithrilをはじめ、コンパイル環境にwebpack、記法にES2015を選択するなどモダンな開発スタイルを目指しました。以前このブログでもご紹介した「Mithrilスケルトンキット」をベースに開発しました。

手前味噌ですがMithrilキットはナイスでした。最初に開発環境をしっかり整備しておいたことで開発スピードは劇的に速くなりました。着想からプロトタイプまで1日半程度。全体で2日強でこの形になりました。しかも半分はTOPページのモーションに変にこだわったせいです。アプリ部分自体は込みこみで1日くらいですね。Mithrilの書きやすさ、習得しやすさ、スケールのしやすさの恩恵をうまく享受できた結果かなと思います。

Mithril部分は愚直にModel, ViewModel, Componentの形式に則って記述しました。controllerに何でもかんでも集約しそうになる点が自分にとって課題だと痛感させられました。また、うまく分離できていないとmochaでのテストも不可能になるため疎結合は今後も意識して設計する必要がありそうです。また、mithril-queryを使った、Mithrilならではのテスト方法も勉強になりました。

TOPページはそこまで凝るつもりはなかったのですが、思いつきでパララックスがしたくなって今のスクロールするとアニメーションが展開されていく形に落ち着きました。これは完全に趣味です。いや、このアプリや企画自体がもはや趣味以外の何者でもないんですが・・・(笑)

そんなこんなで作り上げたアプリですが、結構頻繁にdocomoAPIが503になったりするので(特に午前)気長に、気楽に罵られにきていただければと思います(何

HTML5AUDIO , WEBAUDIO , boombox , howler , soundjs

Xperia Z1, Z2だとブラゲーとかでサウンドがまるで再生されない、なんてことありませんか?私は割りと遭遇します。

なぜだろうとぐぐってみても同じ症状を訴える人も少なければ、それについて対処している技術者も見当たらない。

ということでガッツリ調べてみました。

何はなくとも調査ツールがないと。ということでさくっと作ってみました。

国産のブラゲで良く使われているboombox.js、海外のサイトでよく見かけるhowler.js、国内外で見かけるけど評判がいまいちなSound.jsの3つでベンチマークとりました。

オーディオ形式はAndroid全般で再生可能なoggとiosで再生可能なaacの2種をベースに、念のためmp3も用意しました。基本、oggとaacで事足りるみたいですがmp3なら鳴る、というケースも考えられるので用意しました。

ライブラリに処理を委譲する前に、canPlayTypeでoggとaacで再生可能なファイルタイプを調べます。(前述の通り、Androidならほぼ間違いなくoggになります)そして決定したファイルタイプにマッチした音声ファイルをpreloadし、ライブラリに音声ファイルを登録して再生待ちの状態にします。ただしse2だけは強制的にmp3を使うようにしてあります。

canPlayTypeでのオーディオファイル形式振り分け処理


  var _au = new Audio;
  var _ogg = _au.canPlayType("audio/ogg");
  var _aac = _au.canPlayType("audio/aac");
  var _mp4 = _au.canPlayType("audio/mp4");
  var playType = "";
  var canPlayType = "";
  if (_ogg) {
      playType = "audio/ogg";
      canPlayType = "ogg";
  } else if (_aac) {
      playType = "audio/aac";
      canPlayType = "aac";
  } else if (_mp4) {
      playType = "audio/mp4";
      canPlayType = "aac";
  }

よくある感じですね。ここらへんの振り分けに関してはTM Life - HTML5 Audio を JavaScript で使う方法を参考にしてみました。

手元の端末とか借りた端末で色々みてみました。

  • ios全般はboombox,howlerではいずれも再生可能、SoundJSはmp3タイプのみ再生可能
  • Android2.3はboombox,howlerではいずれも再生可能、SoundJSはまるでだめだった
  • XperiaZ1, Z2以外のAndroid4.x~はboombox,howlerではいずれも再生可能、SoundJSはmp3タイプのみ再生不可

boomboxとhowlerは実力ともに互角ですね。さすが国内外の有力候補ライブラリ。SoundJSェ・・・

DeviceInfoではWebAudioが再生可能になってました、が、再生されず。うんともすんともいいません。boomboxやhowlerのonEndコールバックは正常に作動するため「あたかも鳴っている」かのような挙動。

Android SDKのlogcatで監視してもエラーは検出できません。

だめもとでHTML5AUDIO強制モードでboomboxを使ったら。。。鳴りました。完璧なほどに。

AudioContextは有効だけど実際は再生できない、という端末側の仕様か何かでしょうか。

腑に落ちませんがとにかくWEBAUDIOを使わずに再生すればいけました。まぁ機種依存といわれればそれまでですが、意外とはまってる人はいるんじゃないでしょうか。だめもとでHTML5AUDIOモードで再生してみてはいかがでしょうか。

JavaScript , unity , webview

前回の記事「Unity3Dのgree webviewでローカルHTMLを表示し、JSからUnityのコードを実行してファイル読み書きする」でなんとなくwebviewからUnityコードをたたくところまで試してみました。

今日はもう少し突っ込んでやってみました。

  • セーブデータはローカルに保存
  • データ形式はJavaScriptオブジェクトをJSON形式に変換してからbase64にしてテキストとして保存
  • ロード時に上記を逆変換してJavaScriptオブジェクトにする

C#スクリプトは前回とほぼ同じ

using UnityEngine;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

public class attachWevView : MonoBehaviour {
	//private string url = "file:///android_asset/streamingAssets/index.html";
	WebViewObject webViewObject;
	
	// Use this for initialization
	void Start () {
		webViewObject = (new GameObject("WebViewObject")).AddComponent();
		webViewObject.Init((msg) =>
        {
            Debug.Log("------------------------------------------");
            Debug.Log(msg);

            string savePattern = "save__";
            Match match = Regex.Match(msg, savePattern);

            if (match.Success)
            {
                Debug.Log("persistentDataPath:" + Application.persistentDataPath);
                StreamWriter sw = new StreamWriter(Application.persistentDataPath + "/savefile.txt", false); //true=追記 false=上書き
                sw.WriteLine(msg);
                sw.Flush();
                sw.Close();
            }
            else if(msg == "load")
            {
                FileInfo fi = new FileInfo(Application.persistentDataPath + "/savefile.txt");
                using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8))
                {
                    string loadedText = sr.ReadToEnd();
                    Debug.Log("LOG: " + loadedText);
                    webViewObject.EvaluateJS(
                        "Unity.updatePlayerStatusByUnity('" + loadedText + "');"
                    );
                }
            }
        });
        webViewObject.LoadURL("file:///android_asset/index.html");

        webViewObject.SetMargins(0, 0, 0, 0);
		webViewObject.SetVisibility(true);
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

変わったところだけかいつまんでみていくと、まず正規表現によるmatchが使いたいのでSystem.Text.RegularExpressionsをインポートしました。

WebViewのUnity.call()save__から始まるメッセージが送られてきたらセーブ書き込みとして処理したいため、Regex.Match(msg, savePattern)でマッチングチェックをします。StreamWriterを使った書き込み処理自体は前回と同じです(Android実機のファイルマネージャー内部ストレージの./Android/data/{アプリケーションドメイン}/files配下にファイルを書き出しできます)

続いてload処理ですが、Unity.call("load")が呼ばれたらStreamReaderでテキストファイルから文字情報を読み込みます。ここでひとつ注意ですが、ストリームリーダーは一度endまで読み込んだら次回以降は空文字列がかえってくる点です。

FileInfo fi = new FileInfo(Application.persistentDataPath + "/savefile.txt");
using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8))
    Debug.Log("LOG: " + sr.ReadToEnd());
    Debug.Log("LOG2: " + sr.ReadToEnd());
}

こちらの例でいうと、一回目のLOGは正常に文字列が書き出されますが二回目のLOG2は空になります。sedとかになじみのない方は戸惑うかもしれませんので念のため。

load側では読み込んだテキストをWebView側に渡すために「EvaluateJS」を追加しています。これはUnityからWebViewのJSを実行するメソッドになります。updatePlayerStatusByUnityというJS側のメソッドにloadした文字列を渡して実行しています。

JavaScriptでセーブファイルを作ってUnityに投げる

base64化はMasanao Izumo様のbase64.jsを使うと古いブラウザでもbase64化ができます。モダンブラウザの場合はatobメソッドでエンコード、btoaメソッドでデコードできますが念のためライブラリを使わせていただきます。

function getSaveObj(datas){
  return base64encode(escape(JSON.stringify(datas)));
}

日本語が含まれていてもbase64化できるように念のためescapeしておきます。あとはbase64化したテキストにsave__文字列を追加してUnityに投げます。

Unity.call("save__" + getSaveObj({a:1,b:2}))

これでローカルにsave__2upoifds;lakfds;lafjdsaoiみたいなテキストファイルが保存されたと思います。

JavaScriptでセーブファイルをloadする

先ほど、Unity側でload時にWebViewの「Unity.updatePlayerStatusByUnity」を実行するようにしました。JS側にこのメソッドを作ります。

Unity.updatePlayerStatusByUnity = function(data){
  data = data.replace(/^save__/,"");
  var obj = JSON.parse(unescape(base64decode(data)));
  
  //復元されたセーブデータから状態を復元
  player.status = obj;
}

saveのときと逆順に、bse64デコードし、unescapeしてJSON.parseしてオブジェクト化します。これだけでOKです。

ページトップへ