Stocks and Flows

やったこと、考えたことの記録

SNS利用3カ条、というのを考えた

SNSは素晴らしい。素晴らしいだけにそれに時間を使いすぎてしまいがちだ。

特に動画系サービスはやばい。関連動画を軽い気持ちで観ていってたら気づくと2時間過ぎていた、ということが結構ある。周りに人がいない時とかやばい。

ただ、SNSを見ないというのはそれはそれで問題だ。今やSNSは新鮮な情報を得るための最も効率的なインフラだ。

かといってどうでもいい情報に流され続けるのもアホらしいというか、エンジニアとしての生産性に直撃する。Youtube見てて開発遅れましたとかなったら笑えない。

自分は先日、「有吉ジャポン」の動画を観てたら5時間経ってた、という経験をした。ちなみに一応、土曜日なので勤務時間外ではあった。

おそらくこれは人類全体に共通する課題だろう。

この課題を解決するために、私は「SNS利用3カ条」を考案した。これを書き留めておくことで、無駄にSNSに時間を浪費することなく、リアルタイムに情報を集めるというメリットをも享受することができる。たぶん。

ちなみにこれは「仕事をしている時のみ」に使うルールだ。それ以外の時間はいくらでも耽溺して構わないとしている。

  1. Youtubeはあまりに危険なので原則として開かない。
  2. Facebookは通知がきたときだけ開き、5分経ったら閉じる。
  3. Twitterは呟くときだけ開き、5分経ったら閉じる。

自分の経験上、Instagramは情報やリンクがないためかあまり時間の無駄にならない。

AngularJSテストソンに参加してきた

AngularJSテストソン@mixiに参加してきた。

学んだことは即座に振り返るのが一番ハードルが低くて楽だ。今振り返るのが億劫だったら明日はもっと億劫になるにちがいない。ということで簡単に振り返る。

やったこと

集まった人たちで4人程度のチームを組み、MSakamakiさんが用意してくれた簡単なAngularJSアプリにテストを実装してみよう、という内容。リポジトリの自分が書いたブランチは以下。

yusuke-nozoe/AngularTestSonSample at nozoe · GitHub

自分のチームには本格的にAngularJS(とそのテスト)の経験がある人はおらず、手探りに試行錯誤しつつ進んだ。

アプリの内容は、商品をカートに追加して、チェックアウトして顧客の氏名などを入力して購入するという一般的なECサイトを想定したもので、顧客が自分の情報を入力して購入するページのテスト作成を担当した。

成果

Protractorを使って情報が自動で入力されてsubmitされるまでのテストを実装した。 Directiveのテストを書こうとしたが、時間内に実装することはできなかった。

反省

  • あまり頑張らなかった。リラックスして和気藹々と進んだ。
  • 目的を持とうという意識がなかった。あらかじめ「なんのために参加するのか」を言葉にしておけばもっと得るものがあったかもしれない。

  • 一方でこのテストソンに参加したことはとても良かった。現在会社を立ち上げたばかりで自分一人で開発しているということもあり、チームで何かに取り組むということが新鮮だった。リラックスしていたとはいえ、自宅でただ一人で勉強するよりはよほど身になった。

  • KarmaやJasmine、Protractorなどなんとなく知っていた知識が整理された。KarmaはJSテストの実行環境、Jasmineはテスティングフレームワーク、Protractorは受け入れテストを可能にするSeleniumベースのサムシングである。Protractorは正直よく理解できてない。とりあえず動くけど。

今後に向けて

  • 定期的にこのような勉強会やハッカソンには参加したいが、目的意識をもうちょい形にした上で参加する。
  • 「テストを書く習慣をつける」ことは若いエンジニアとして大きなテーマの一つだと思うので、これをとっかかりに引き続き取り組む。
  • 学んだことはその日のうちに振り返る。これが大事。

Rails x CoffeeScript x KnockoutJSでWebサービス作った

今年の10月からあるスタートアップの立ち上げに参加していて、先日ベータローンチした。

自分はプログラミング初心者というわけでは全くないが、大学の専攻は農学部だし、それまで一人でサービスを一から作ってリリースしたこともなかったので、完全に独力でサービスを作りあげリリースするというのはかなりタフで、学ぶところが多かった。

この記事はKnockoutJS Advent Calendarの中の一記事として作成しているが、KnockoutJS単体というよりもRails x KnockoutJSでWebアプリを作った過程の記録としたい。

何を作ったのか

今回立ち上げた会社は、投資銀行出身の外国人(アラン)が金融機関向けのツールを作りたいということから始まったBtoBである。 彼が、East Venturesの衛藤バタラさん(mixi作った人)に話を持ちかけたところから始まった。で、ちょうどいい所にいた自分に話がきた。

アランが言うには、投資銀行という場所はコミュニケーションの点で未だにイノベーションが進んでおらず、そこに最新の(?)Webアプリケーションを作れば必ずいける、という話だ。 アプリケーションとしてはそこまで高度なこと(大量のトラフィックとかなんとか処理とか)が要求されているわけではなく、だからこそ自分でも挑戦してみたいという気持ちになった。自分には一からサービスを作ってみたいという気持ちがとても強かったこともあり、新卒で入った会社を飛び出して参加させてもらった。

アプリケーションの機能としては、ディールの一覧を取得し、それをインタラクティブに投資家<=>投資銀行間でやりとりしたり、チャットしたりとコミュニケーションを円滑化する、というのが中心である。 Webアプリとしては一般的だと思う。

友達に使ってもらうようなものではないが。

なぜこの構成にしたのか

今回のサービス作りではAPIおよびHTMLのテンプレートを返すためにRails, フロントエンドのなんやかんやをやりくりするためにKnockoutJSを選択した。

Railsはとりあえず普通のWebアプリを作るためには安易すぎるくらいの選択で特に述べることはないが、KnockoutJSを選ぶというのはあまり多くないと思う。

最近はだいぶディスられてるみたいだけど、JavaScriptフレームワークとしてはちょっと前にAngularJSがもてはやされてて、自分も前職でそれに触れ、純粋に面白いと思い、今回もできれば使いたいなと思っていたが、考えた結果としてAngularJSは却下した。

その他にもBackboneやらEmberJSも(ちょっとだけ)検討したが、最終的にチュートリアル触った感じからKnockoutJSを選ぶことにした。自分が選んだ理由ははっきりしていて、

Internet Explorer 8などの古めのブラウザにも対応している

② 機能が少なくて学習コストが低い

Railsアプリとの相性がよい

この3つに集約される。

①については、金融機関向けのサービスということで、大変残念なことにメインのターゲットブラウザがIE8と9 ということになってしまった。 エンジニアとしてはちっとも面白くないしなんだかなーと思っているうえに、AngularJSがIE8のサポート次から切るよ、と言っててぐぬぬと思っていたところにKnockoutJSは「IE6から全部サポート!」などというソ○トバンクばりの大言壮語を吐くので、「これは心強い」と素直に感じてしまった。

②は後述するが、KnockoutJSは代表的なJavaScriptフレームワークの中では圧倒的に学習コストが低いと思う。Angularにちょっと触れた自分としては逆にそれが魅力的だった。data-bind=とやってほげほげすればたいていのことは間に合う。ドキュメントは充実してる気はあんまりしないが、チュートリアルはわりと親切で、これを組み合わせればなんとかなりそうだ、という実感がもてた。

③が結局は決定的だったが、AngularJSはRailsとの相性があまりよろしくないと思う。Rebuild.fmでも言われていたが、RailsとAngularは機能的にかぶっている部分がありすぎる。Gruntでビルドするとか、ルーティングとかうまく使いこなせれば強いのだろうが、それだったらRails使う意味があんまりないかなと思っていた。その一方でKnockoutJSは薄いフレームワークであるがゆえに、Ruby on Railsのapp/assets/javascripts以下にきれいに収まってくれる。

この透明感というか「全体を把握できている感」が一からサービスを作っていく上で安心できた。

全体の構成

Railsの役割としては大きく二つで、HTMLをほぼリソースとは関係なくレンダリングする部分と、リソースをAPIサーバとして適宜返す、という部分を/apiでネームスペースで区切って共存させることになった。 レンダリングしたHTMLやJavaScriptの中には当然ながらKnockoutJSで書いたコードが入っていて、その中でKnockoutJSがAPIを叩いてテンプレートをよろしく動かすという感じだ。

KnockoutJSはすべてCoffeeScriptで書いた。CoffeeScriptはクラスを定義できるので、そこでリソースが持っているデータ構造をほぼ完全に再現して、それをKnockoutJSでバインドして動かした。

フロントエンドの構成

ここからもうちょっと具体的な話を書きたい。Railsに導入するための参考になれば。

ディレクトリ構成は次のような感じで、

├── application.js
├── common
│   ├── bindingHandlers.js.coffee
│   ├── class.js.coffee
│   └── utils.js.coffee
├── calendar.js.coffee
├── deal_room.js.coffee
├── sessions.js.coffee
└── users.js.coffee

なんとなくこういうの載せるってどうなのかなーと思って本物とは少し変えてあるのだが、要はapplication.jsの下に、共通して使うメソッドとかクラスを定義するcommonというディレクトリがあって、それ以外が各テンプレートと対応するCoffeeScriptになっていて、それぞれがサーバーとデータをやりとりする、という感じ。

当然ながらチャットとかノーティフィケーション系のどこでも呼ばれないといけないデータはcommon/class.js.coffee内に入っていて、その中に共通機能をいれたViewModelを定義して、各テンプレートではそれを継承したViewModelをつくる、という感じだ。全く伝わる気がしない。

bindingHandlersには、要素をドラッグしたり、エフェクトをつけたりといったDOM操作系の機能が入っている。だから、DOM操作は末端のCoffeeScriptには書いてなくて、データとかロジックだけをうまく切りだせていると思う。

よく使ったコードパターン

自分が書いたコードを振り返ってみると、アプリケーションの機能としては分割されているものの、違うリソースに対して同じようなふるまいを提供している部分が多い。

class @BaseViewModel
  constructor: ->
    self = this
    self.currentUser = ko.observable()
    self.notifications = ko.observableArray([])
    self.message_status = ko.observable ''
    self.message        = ko.observable ''
    self.socket
    $.ajax({
      type: 'GET',
      url: '/current_user',
      success: (result) ->
        self.currentUser(new User(result.current_user))
        self.socket = io.connect(':[PORT]')
        self.socket.on('connected', -> self.socket.json.emit('init', { 'room': [room id] }) )
        self.socket.on('notification', (data) -> self.addChat(data.from_id, false ) )
    })
    self.loadNotifications = ->
      $.getJSON('/api/notifications', (result) ->
        self.notifications($.map(result.notifications, (item) -> return new Notification(item)))
      )
      return
    self.loadNotifications()

上がログイン下でのすべてのテンプレートのViewModelが共通して継承するクラス(の省略版)で、ログインしているユーザーの情報をAjaxでとってきたり、別に立ててあるSocketIOのサーバにつなぎに言ったりしている。 よく使ったのがself.loadNotifications()みたいにしてデータをロードして、$.map使って定義したクラスにデータを入れ籠む、という形。これも伝わる気がしない。

そして、各データを構成するクラスは以下のような感じ。

class @User
  constructor: (data) ->
    self = this
    self.id = data.id
    self.type = data.type
    self.email = ko.observable(data.email)
    self.first_name = ko.observable(data.first_name)
    self.surname = ko.observable(data.surname)
    self.full_name = data.first_name + ' ' + data.surname
    self.company_name = ko.observable(data.company_name)
    self.image_url = if data.image_url then data.image_url else 'default-user-image.jpg'
    self.attemptedEmail = ko.observable(data.email)
    self.attemptedFirstName = ko.observable(data.first_name)
    self.attemptedSurname = ko.observable(data.surname)
    self.editing = ko.observable(false)
    self.edit = -> self.editing(true)
    self.save = ->
      $.ajax({
        type: 'PUT', url: '/api/users/' + self.id, contentType: 'application/json',
        data: ko.toJSON({ user: {
            email: self.attemptedEmail(),
            first_name: self.attemptedFirstName(),
            surname: self.attemptedSurname(),
          }
        }),
        success: (result) ->
          if result.status == 'success'
            self.update(result.user)
            self.editing(false)
      })
      return true
    self.cancel = ->
      self.editing(false)
  update: (data) ->
    self = this
    self.email(data.email)
    self.attemptedEmail(data.email)
    self.first_name(data.first_name)
    self.attemptedFirstName(data.first_name)
    self.surname(data.surname)
    self.attemptedSurname(data.surname)

上はユーザークラスで、プロフィール画面にバインドさせて編集したり更新するのに使った。idとかfirst_nameとか一通りの情報は持たせた上で、saveみたいに情報をPOSTするためのメソッドを個別に持たせた。ここではPUTか。

情報を更新したら、サーバーから更新後のデータをそのまま返させて、それを使ってupdateメソッドを呼ぶことで自分自身を更新する。こうすることでデータをまるごとリロードしたりせずともSPA的な機能を表現できた。

リソースの更新にはこのように各オブジェクトが自分自身を更新できる場合もあってきれいな感じだが、実際にはその親クラスにメソッドを持たせる必要がある場合が多かった。

例えばそのオブジェクトがObservableArrayに入っている場合。

observableArrayは配列を監視して、データが更新されたらテンプレートも一緒に更新する、というデータバインディングの基本機能を実現するものだが、ただ自分を変えるだけじゃなく、削除したり、他のobservableArrayに移動したりという場合にはどうしても親から操作する必要があった。

まとめ

今回はRailsとKnockoutJSでそこそこの規模のアプリを作るのに使った大枠をざっくりと記録した。

以上の内容をまとめると、以下のようになる。

Railsにはテンプレートの描画とAPIの返し(?)を担当させ、両者を綺麗に分割する

② リソースをそのまま反映したクラスをフロントエンド側で用意しておき、ajaxで取り込んでself.list($.map(data, (item) -> new Class(item)))の形で一覧を取得

③ 各アイテムの更新の際には、一覧の数が関係なければオブジェクト自身に更新させ、数が変わるときには親のビューモデルからAPIを叩いて更新する

来週も書くつもりだが、次回はもっと具体的なTips的なのを書けたらと思う。

Yeomanで生成したAngularJSアプリをherokuでデプロイする

この投稿の目的は以下2つの記録。主に後者。

  1. Sass入れてYeoman使う場合に詰まったところ
  2. Yeomanで作ったAngularJSアプリをherokuにデプロイ

1. 前提+事前準備

$ gem update sass compass #=> これをしておかないとSass入れる場合につまづく
$ node -v #=> v0.10.30
$ yo -v #=> 1.2.1
$ bower cache clean #=> 一応念のため
$ npm cache clean

あとherokuアカウント+SDKも必要

2. Scaffolding

$ mkdir yo-angular-heroku && cd yo-angular-heroku
$ yo angular # 色々聞かれるのは全部YES/デフォルト

     _-----_
    |       |    .--------------------------.
    |--(o)--|    |    Welcome to Yeoman,    |
   `---------´   |   ladies and gentlemen!  |
    ( _´U`_ )    '--------------------------'
    /___A___\
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

Out of the box I include Bootstrap and some AngularJS recommended modules.

? Would you like to use Sass (with Compass)?: Yes
? Would you like to include Bootstrap?: Yes
? Would you like to use the Sass version of Bootstrap?: Yes
? Which modules would you like to include?: (Press <space> to select)
❯ⓧ angular-animate.js #=> デフォルトでEnter
 ⓧ angular-cookies.js
 ⓧ angular-resource.js
 ⓧ angular-route.js
 ⓧ angular-sanitize.js
 ⓧ angular-touch.js

3. ローカルで起動

$ npm install
$ grunt serve
Running "wiredep:app" (wiredep) task
Warning: ENOENT, no such file or directory '/Users/myname/Desktop/yo-angular-heroku/app/bower.json' Use --force to continue. #=> エラー

このエラーを解決するには、package.json内のgrunt-wiredepバージョン指定を"^1.7.0"から"1.8.0"に書き換え、再びnpm install。 終わったらサーバーを起動。

$ grunt serve #=> 開発中のファイルでサーバーを起動

または

$ grunt server:dist #=> コンパイルされたdist/以下でサーバーを起動

これでいつものこの画面が出てればとりあえずOK.

f:id:yusuke-nozoe:20140909082612p:plain

5. heroku deploy の準備(ビルド)

世の中はとても便利で、yeomanアプリをherokuにデプロイするためのnpm packageが既にある。

$ npm install -g generator-heroku
$ yo heroku 
[?] Do you want a separate git repository in dist/? No #=> 同じgitリポジトリのsubtreeとしてデプロイする
   create heroku/Procfile
   create heroku/server.js
   create heroku/distpackage.json
Please add this copy task rule to your Gruntfile:
    copy: {
        dist: {
            files: [{
                expand: true,
                dest: '<%= yeoman.dist %>',
                cwd: 'heroku',
                src: '*',
                rename: function (dest, src) {
                    var path = require('path');
                    if (src === 'distpackage.json') {
                        return path.join(dest, 'package.json');
                    }
                    return path.join(dest, src);
                }
            }]
        }
    }

You're all set! Now run
    heroku apps:create
and push your dist directory with
    git subtree push --prefix dist heroku master

基本的には出力された通りにやればいいが、Gruntfile.jsにはcopyタスクが既にあるので、ちゃんと合併させる必要がある。

// Gruntfile.js
...
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
            '.htaccess',
            '*.html',
            'views/{,*/}*.html',
            'images/{,*/}*.{webp}',
            'fonts/*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/images',
          dest: '<%= yeoman.dist %>/images',
          src: ['generated/*']
        }, {
          expand: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= yeoman.dist %>'
        }, {
          expand: true,
          dest: '<%= yeoman.dist %>',
          cwd: 'heroku',
          src: '*',
          rename: function (dest, src) {
          var path = require('path');
          if (src === 'distpackage.json') {
            return path.join(dest, 'package.json');
          }
            return path.join(dest, src);
          }
        }]
      },
...

そしてビルドする。ここでは確認のため、dist/に移動してnpm installし、node.jsを起動してみる。

$ grunt build
$ cd dist && npm install
$ node server.js #=> http://localhost:1203/

6. heroku deploy

herokuにデプロイする前に注意点が一つある。yeomanのデフォルトの.gitignoreではdist/とbower_componentsがignoreされている。yo herokuするとdist/のignoreが消されるが、bower_componentsはそのままになっており、dist/bower_componentsもignoreされてしまい、そのままpushするとbootstrapのアイコンが出ない。この辺はherokuがbowerに対応してくれれば解決するのかも。

そこで下記を追加。

# .gitignore
node_modules
.tmp
.sass-cache
bower_components
!dist/bower_components #=> これを追加!

これでcommit して、pushする。

$ heroku create <app name>
Creating <app name>... done, stack is cedar
http://<app name>.herokuapp.com/ | git@heroku.com:<app name>.git
Git remote heroku added
$ git subtree push --prefix dist heroku master
git push using:  heroku master
-n 1/       1 (0)
Initializing repository, done.
Counting objects: 30, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (22/22), done.
Writing objects: 100% (30/30), 214.86 KiB | 262.00 KiB/s, done.
Total 30 (delta 0), reused 0 (delta 0)

-----> Node.js app detected
...
-----> Launching... done, v3
       http://<app name>.herokuapp.com/ deployed to Heroku

To git@heroku.com:<app name>.git
 * [new branch]     jaogjraejgaeogjieaogjae -> master

$ heroku open #=> ブラウザが開かれる

DONE.

Yeoman x AngularJSの導入

PCを新しくしたので、またいちからYeoman x AngularJSを導入する。

まずはnvmを使ってnode.jsを入れる。

$ curl https://raw.githubusercontent.com/creationix/nvm/v0.13.1/install.sh | bash

...
...

=> Appending source string to /Users/yusuke.nozoe/.bash_profile
=> Close and reopen your terminal to start using nvm

ターミナルを開き直し、インストール。

$ nvm install 0.10
######################################################################## 100.0%
Now using node v0.10.30

npmを使ってYeomanを入れる。npm1.2.10以降であれば、Gruntとbowerも同時にインストールしてくれる。

$ npm install -g yo

AngularJSアプリのScaffoldingを行うには、次を実行しておく。

$ npm install -g generator-angular

以上で、次のコマンドでAngularJSをデフォルトで組み込んだScaffoldを自動で生成できようになる。

$ yo angular

簡単。

Pod Install周りでハマった

iOS複数人で開発するときは基本的にここに書いてある内容にそってgitignoreをしつつ、必要に応じてPods/以下もignoreする。

今回知り合いと作っているアプリでもPods/以下をignoreしているのだが、そうするとpod install時にしばしばエラーが出て怒られる。

昨日もとあるエラーが出たので「なんかSDWebImageのバージョン違うのかな?」と思って慌ててPods/以下をrm -rdf した後にpod installし直そうとした。そしたら今度は

[!] An error occurred while performing `git pull` on repo `master`.
[!] /usr/local/bin/git pull --ff-only

Cannot pull with rebase: You have unstaged changes.

Please commit or stash them.

という別なエラーが出て、cocoapods自体をgemで入れ直したり、git-configに--ff-onlyがデフォルトになるようになど色々やってみたもののどれもうまくいかず。

最終的にはこの記事に書いてある内容でうまくいった。

pod installがうまく動かなくなった時に試してみること

$ pod repo remove master
$ pod setup

なんでこうなるかは理解できていないが。 unstaged changesって何だろう。

ただ、こういうときは一旦削除して入れ直す、というアプローチは自然なのかもしれない。さすがにPods/以下を削除してもしょうがない感じはするが。。

そして、最初に述べた「とあるエラー」は、Xcode6(beta)で起動していたことが問題であるらしい。次のコマンドでXcode5を起動することにより解決。

$ open プロジェクト名.xcworkspace -a Xcode

詳細は触れないが、Xcode6に移行するときに捨て去るコードがあるのかもしれない

AngularJSを採用することのメリット

自分の理解をまとめただけだが、おおよそこういうことになるのかなと。

1. いい感じにMVCが書けて、全体のコード量が減らせる

そもそも自分は「フロントエンドでもMVCなんてことがあるのか」とか思ってしまうほど理解が浅かったのだが、考えてみれば仮にJavaScriptであろうと「データを用意して、それをなんやかんやしてテンプレートに反映したり、テンプレートから情報を受け取る」というアプリケーションである以上は、MVCであって然るべきだ、確かに。

他のJavaScriptフレームワークに対する知識がほとんどないので、比較することができないが、機能ごとにコントローラを生成して、コントローラに対して必要なコンポーネントを渡す、というのはいい感じにMVCを実現しやすいと思う。

そして、テンプレートに記載するのは ng-modelとかng-viewみたいなディレクティブと呼ばれるものだけで、とてもシンプル。jQuery みたいにid="hogehoge"をつけまくってそれを動かす、ということをする必要がない。

2. 動的なアプリケーションをシンプルに作れる

これは1. につながっている部分なのだと思うが、AngualrJSでは2way Data Bindingというスタイルをとっており、モデルの変更は即時的にテンプレートに反映され、逆もまた即時で反映される。

それによってアニメーションやインクリメンタル検索といった動的なUIを実装することが異常なほど簡単である(というかデフォルトでそうなってると言っていい)。

要するに$('#divElement').on('change', function() { ... }); といった状態をWatchするコードを書く必要が劇的に減るということだ。

3. テストしやすい

これは他のMVCフレームワークと同じであるが、機能が分割されているということは「テストしやすい」ということに直結する。 モデルはモデルで各メソッドの振る舞いをテストできるし、コントローラもそう。

開発をリードしているのがGoogleというだけあってそこらへんはデフォルトでのツールも一通り揃っている印象を受ける。モデル側のUnitTestはKarma、コントローラ側はProtractorが推奨ツールなのだろうか(この辺ちゃんと把握してない)。

他にもAngularJSにはDependency Injectionとかその他諸々の概念があるので、調べつつ自分の理解をまとめていきたい。