Webアプリをいまどきの手法で爆速開発した

外道はるかぜちゃんジェネレータというWebアプリを いまどきな手法を用いて爆速で開発した話を紹介します。

先の3連休中、外道はるかぜちゃんジェネレータというWebアプリを開発&公開しました。ここで採用した開発手法がいまどきな爆速開発でしたのでちょっと紹介&ステマします。使った技術は以下の通りです。

  1. AngularJS: Googleが開発しているViewModelなWeb開発ライブラリ(MVW: Model View Whateverだったかな?w)
  2. Github pages: スタティックサイトのホスティングに最適
  3. Kii Cloud: mBaaS (mobile backend as a service) で共有データの保存に利用
  4. HTML5 Canvas: 画像生成に。サーバサイドではなにもしてない!

サービス概要

外道はるかぜちゃんジェネレータはベースとなる画像があり、そこに面白いセリフを考えてもらって合成し、URLや画像で共有するサービスです。テンプレート固定の大喜利共有系と言えば良いでしょうか。

AngularJS

ユーザに入力してもらうのはセリフだけです。逆に言うとセリフを入力したら即座に画面に反映されなきゃ面白くありません。ということはdata bindingが必要で、いまどきのdata bindingと言えばAngularJSです。

この手のアプリケーションを作るのにAngularJSは本当に便利で、JSのコードは一行も書かずに実現できてしまいました。HTMLとしてはこんな感じ。

<!DOCTYPE html>
<html ng-app>
<meta charset="utf-8">
<link rel="stylesheet" href="default.css">
<title>外道はるかぜちゃん</title>
<h1>外道はるかぜちゃんジェネレータ</h1>
<div id="screen">
  <img src="baby.jpg" alt="はるかぜちゃん/赤ん坊">
  <div id="script_first" class="script">
    <p>{{text1 || 'お前が買ってきたアポロチョコ'}}</p>
  </div>
  <div id="script_second" class="script">
    <p>{{text2 || 'いちごの部分だけ全部食った!'}}</p>
  </div>
</div>
<form id="cpanel">
  <input type="text" name="input1" ng-model="text1"
    placeholder="お前が買ってきたアポロチョコ">
  <input type="text" name="input2" ng-model="text2"
    placeholder="いちごの部分だけ全部食った!">
</form>
<script src="angular.min.js"></script>
</html>

さすがAngularJS! すごい簡単ですね。

時間がかかったのは狙った場所にセリフを表示するCSSのほうで、とはいえ単に「それなりに見られるようにするには調整がめんどう」ってだけなのでそれほどでもありません。風邪ひいてボーッとしてる頭でも2時間かからずに公開できるところまでできちゃいました。

Github Pages

こんなジョークWebアプリを公開するのにまじめにサーバ立てて運用するのも馬鹿らしいですよね。なので Github Pages を使って公開することにしました。 Github Pagesは github 上のレポジトリに gh-pages というブランチを作ればそれが

http://{username}.github.io/{projectname}/

というURLで公開されるというお手軽なサービスです。どうせ隠すようなソースもないので本Webアプリにはぴったりです。

以前から Github Pages はいろんな場所で使っていたのですが、しかし今回ちょっとハマりました。というのは index.html だけを置いた場合には、pushしてから実際に公開物に反映されるまでが、jekyllを使った時より遅いようなのです。

もしかしたらどっかでキャッシュされてそれが悪さをしてただけかもですが、jekyllを使うようにしたらそれが緩和されたように感じたので、途中でjekyllに切り替えました。が、これ自体はjekyll使う意味がまったくないWebサービスですので余計な手間でした。

ともあれこの段階でいったん公開。連休1日目です。

Kii Cloud

連休2日目は、作った作品の共有機能を作ります。とは言えGithub Pagesでデータベースを持つわけにもいきません。query stringでURLに埋め込んでしまおうかと実験もしましたが、ちょっとでもセリフが長くなると圧縮してもキツイかなという感じ。

そこで mBaaS である Kii Cloud を使ってみました。簡単に言うとデータベースを含むバックエンドを提供してくれるWebサービスです。

JSのライブラリもあるので保存や読み込みは楽々でした。参考までに当初の保存処理はこれだけでした。なお後に Safari との相性のために少し変わりました。

function save(callbacks) {
  var key = calcKey($scope.text1, $scope.text2);
  if (isEmpty(key)) {
    return null;
  }
  var uri = 'kiicloud://buckets/data1/objects/' + key;
  var obj = KiiObject.objectWithURI(uri);
  obj.set('text1', $scope.text1);
  obj.set('text2', $scope.text2);
  obj._setUUID(key);
  obj.saveAllFields({
    success: function (obj) {
      callbacks.success(key, obj);
    },
    failure: function (obj, errstr) {
      console.log('ERROR: save failed: ' + errstr);
      callbacks.failure(key, obj, errstr);
    }
  });
}

対応する読み込みコードはこんな感じでした。

function load(key) {
  var uri = 'kiicloud://buckets/data1/objects/' + key;
  KiiObject.objectWithURI(uri).refresh({
    success: function (kiiobj) {
      var t1 = kiiobj.get('text1');
      var t2 = kiiobj.get('text2');
      if (t1 && t2) {
        setText(t1, t2);
      } else {
        setEmptyText();
      }
    },
    failure: function (kiiobj, errstr) {
      console.log('ERROR: load failed: ' + errstr);
      setEmptyText();
    }
  });
}

うーん…簡単ですね。これだけで永続化できちゃうんですから。

ちょっとめんどうだったのは Kii Cloud でセリフデータの入れ物となるバケツの作成と ACL(アクセス権限)の設定で、ドキュメント読みながら curl でぽちぽちという感じです。大まかな手順はこんな感じ。

  1. Kii Cloudの開発者Webサイトでアプリ作成(IDとKey合わせて4つの文字列をメモ)
  2. 管理者トークンの取得 (上記の4つの文字列が必要)
  3. アプリスコープのオブジェクト作成(バケツを作るため)
  4. 3で作ったバケツにアクセス権限を設定(誰でもデータを作れるように)
  5. 3で作ったオブジェクトを削除

合わせていくつか管理用のシェルスクリプトも作りました。セリフの一覧を取得したり個別に消したりする奴です。

ほぼこれだけでURLを用いた作品共有はできました。連休2日目で、作業時間は実験も含めて3時間程度といったところ。自分でDBスキーマ作ってREST APIつないでサーバ管理・運用してって考えると、とっても簡単ですよね。良い時代になったものです。

HTML5 Canvas

連休3日目は、画像を出力&保存できるようにしてみました。これはまじめに解説すると長くなるんですがだいたいこんな感じ。

  1. Canvas 要素を作成
  2. 描画コンテキストを取得
  3. 描画
    1. ベースになる画像をimgからCanvasへコピー
    2. セリフを描画
  4. PNGとして保存: CanvasのtoDataURLを使ってURLを取得しnavigateしてダウンロード

コードとしてはこんな感じですね。

$scope.onclick_saveAsImage = function () {
  // Create a canvas and draw on it.
  var canvas = document.createElement('canvas');
  canvas.width = 640;
  canvas.height = 480;
  drawHarukazeChan(canvas.getContext('2d'));
  // Save canvas as a PNG image.
  var url = canvas.toDataURL();
  window.open(url, '外道はるかぜちゃん');
}

function drawHarukazeChan(ctx) {
  ctx.drawImage($('#bg_img')[0], 0, 0);
  ctx.font = 'bold 24px sans-serif';
  var t1 = safeString($scope.text1, 'お前が買ってきたアポロチョコ');
  var t2 = safeString($scope.text2, 'いちごの部分だけ全部食った!');
  drawTextVertical(ctx, t1, 425, 15, 200, 320);
  drawTextVertical(ctx, t2, 45, 50, 100, 200);
  return ctx;
}

drawTextVertical() は縦書きで文字を描画する関数で、ちょっと時間かかった上に微妙な出来なのでコードは載せません。興味がある方はGithubを直接みてください。自分で一文字ずつ描いています。

このHTML5 Canvasへの描画処理は、OpenGLをやったことあればすんなり理解できると思います。あとC#のGraphicsにも近いかも。

ローカルでの画像保存にはCanavsからData URLを取得してナビゲートしてますが、ちょっと普通だと思いつかない方法かもしれませんね。ただコレだけでサーバにGDやImageMagickを入れなくて済むって、サーバサイドで画像を取り扱ったことがある人たちには、ビビビっとくるものがあるんじゃないでしょうか。

コアの作業時間は…やはり3時間前後ってところでしょうか。

雑感

以上で要素技術の解説はおしまいです。残りはちょっと気づいたことの落穂ひろいをしておきます。

まず AngularJS について。

AngularJSはHTML/CSS/JSだけでちょっと書くには本当に強力です。ただしスクリプトの読み込みに時間がかかるとプレースホルダーが表示されてしまうみたいな、つまらない問題がないわけじゃありません。回避方法があるとは信じていますが調べていません。

あと AngularJS によって呼び出されたコンテキストで $scope を更新した時には即座に反映されましたが、そこからKii Cloudを呼び出したあとのコールバック、すなわち AngularJS ではないコンテキストでは更新されません。 $scope.$apply() を呼び出しましょう。これ、ちょっとわかりにくかった。

次に Kii Cloud というか有料サービスの利用について。

Kii Cloudは mBaaS ですから、無料枠(100万回/月)があるとはいえAPIのコール回数でお金がかかります。加えて今回作ったジェネレータはURLで共有した作品を表示する度に1回のAPIコールが発生することになります。初めはここかなり心配しました。

だってTwitterではかなりの人気を誇るはるかぜちゃんですよ。彼女が拡散したらどんなアクセスになるのか…心配するのも当然でしょう? 実際に、はるかぜちゃんにも RT してもらったり、まとめにまで入れてもらっちゃっいました。しかし蓋を開けてみれば、公開から約3週間でAPIコール数が3000回にも届かず、仮に無料枠がなくても…21セント…駄菓子の値段かよw

結論を言えば「おめーのアプリ、アクセスねーから!」です。「はるかぜちゃん」というブランド・広告媒体をもってしても、そう簡単には無料枠を超えるようなアクセスは発生しないのです。だから気にせず mBaaS でもなんでも使ってみれば良い、と思いました。

最後にスタティックページで作るサービスについて。

今回サーバサイドはスタティックなファイルだけで構成され、しかもゼロ・コーディングです。このスタイルでもそこそこのサービスは開発できるなという手応えです。ただし1つだけ困ったのはOpenGraphやTwitter Cardなどを扱えないってことです。作品を作って共有した時に、その概要をタイムライン上に表示したいじゃないですか。これがいまはまだできません。コンテンツのレンダリングをサーバでやってないからですね。

いくつか対策は考えられます。例えばクライアント側にHTMLをレンダリング、Kii Cloudへアップロード、その共有URLを払いださせるっていう方法も考えるには考えられますね。ただここまでお手軽にやってきたのに、ここでそうするのはちょっと違うよなって感じてます。


というわけで以上、Webアプリをいまどきの手法で爆速開発した記録でした。