かけるヒトからできるヒト

プログラムを書ける人からプログラムが出来る人へなるために個人的にまとめるブログ

AngularJSの$watchで複数の変数の変化を同時にチェックする

AngularJSを使っていると、非常にお世話になる$watch。

変数やオブジェクトが変化した事を読み取る事ができます。

データバインディングによってリアルタイムにページの変化を扱えるAngularJSではほぼ必須の機能と言っても過言ではないと思います。

僕が開発しているシステムでもあちこちで利用しています。

 

変数やオブジェクトの変化を簡単に検知できるのですが、今回は1つの変数と1つのオブジェクト、どちらかが変化した時にイベントを取得したかったので、その方法を備忘録に載せたいと思います。

 

やり方

通常では変数名を文字列で検知対象を指定します。

 

$scope.hoge = 'fuga';
$scope.$watch('hoge', function() {
    // 変数hogeがなんか変化した時の処理
});

$watchの第一引数は関数で書くことができます。

関数使って同じこと書くとこんな感じ。

$scope.hoge = 'fuga';
$scope.$watch(function() {
    return $scope.hoge;
} , function() {
    // 変数hogeがなんか変化した時の処理
});

つまりこの第一引数のreturnにオブジェクトを指定してやるとobjectの変化を検知できる。

 ただ、第3引数を省略した書き方だとオブジェクトのreferenceが変化しないと(中身だけ変わっても無理)検知できないので、中身だけ変化する場合も検知したい時は、第3引数にtrueを加えてやる必要があります。第3引数にtrueを入れるとオブジェクトの中身を毎回走査する事になったり、比較用に旧オブジェクトを持ったりする必要があるので、速度やメモリ消費的にはもちろん悪くなるので注意。

オブジェクトの参照先をまるごとすげ替える時はtrueを付けなくても検知可能。

$scope.hoge = {'fuga': 'fugafuga', 'hoge': 'hogehoge'};
$scope.$watch(function() {
    return $scope.hoge;
} , function() {
    // 変数hogeがなんか変化した時の処理
}, true);

ここまできたら後はわかるかもしれませんが、このreturnで検知したい変数とオブジェクトをオブジェクトにして指定しちゃえばいいのです。

$scope.hoge1 = 'hoge';
$scope.hoge2 = {'fuga': 'fugafuga', 'hoge': 'hogehoge'};
$scope.$watch(function() {
    return {
        'hoge1': $scope.hoge1,
        'hoge2': $scope.hoge2,
    };
} , function() {
    // 変数hogeがなんか変化した時の処理
}, true);

これで変数かオブジェクトが変化した時のどちらでも検知できるというわけですね。

オブジェクトの中身を走査するので、オブジェクトが大きくなる程加速度的に速度が悪化するのが悩みどころですね……

活用どころ

今回僕がこれを利用したシーンは検索のサジェストを表示する部分でした。

htmlの構造的に、この検索ボックスとサジェストの部分でcontrollerを分けたかったんですね。

f:id:koh110:20140615014050p:plain

検索ボックスの変数を取得するのはng-modelを使ってcontrollerから引っ張っていけばいいんですが、その入力値からデータを引っ張ってきてサジェストの内容を更新します。

自分の場合はテキストボックスの値の変化を$watchで検知して、変化のたびに$http(ajax)でデータベースから内容をひっぱてきてます。(リクエスト飛ばし過ぎなんでこの辺は色々考えた方がいいとは思います)

非同期通信なんで、入力速度が速いと検索ワードの変化とデータが返ってくるタイミングがずれて、検索ワードと結果が違うものになっちゃったりする訳です。

なので、検索ワードの変化と取得したデータを保持しておく変数の両方を検知して、現在の検索ワードに対応したサジェスト内容を表示するという使い方をしていました。

最後に

他にもいい使い方、お前の無駄なリクエスト送り続けるプログラムこうした方がいいだろ、みたいな話があったらぜひ教えてください。

 

phpunitでprivateなstatic関数をテストする

最近新規開発のシステムでテスト駆動開発を試しています。

テストは絶対書いた方がいいとか、同じ工数ならテストが少ないほうがいいとか、色々言われていますが、個人的には新規開発のような、開発中に頻繁に内部のアルゴリズムが変わる状態の時こそテストを書いておくと、一瞬で変更箇所によるミスが見つかるので、最終的には早く仕上がると感じています。

 

プログラムを書いていると、当然privateな関数がどんどん増えていって、それらの関数についてもテストを書いておいた方が安心できる、というのが人情ってものです。

特に先ほど述べたような、アルゴリズムが頻繁に変わる時にすぐ該当箇所に気づくためには、private関数へのテストは必須です。

 

そこで、phpunitでprivateな関数をテストする方法を調べてみました。

 

候補

PHP5.4時代のprivateメソッドテスト手法 #php5_4 - 泥のように

上記のサイトがわかりやすくまとまっていました。

 

テスト用にクラスを継承して、継承したクラスからメソッドのアクセス権を上書きする

単純に考えればこんな方法もありそう。だけど、テストのためだけにわざわざクラス作るのはちょっと面倒だなぁという感じ。

 

Reflectionクラスから呼び出す

検索するとたくさん出てくる方法。

staticな関数についてのやり方があまりなかったので、今回はその方法をまとめようと思います。

 

php5.4以降ならばClosureを利用する

Closureは無名関数の呼び出しで、無名関数から呼び出されている時はprivateなメンバであろうといくらでもアクセスできてしまうという、すごいような怖いような機能らしいです。

比較的新しい機能なので、中々対応している環境が少ないのが難点です。

今回の開発案件もphp5.2なので残念ながらこちらの方式は使えず。

 

方法

ReflecionClassのinvokeArgsメソッドを使ってテストする。

ReflectionClassのインスタンスを作成して各種invokeメソッドを呼ぶ。

毎回インスタンス作成する処理を書くのがめんどくさいので、クラス化しておいた。

class Test_Util 
{
  // $class_obj : テストするクラスのオブジェクト(new CLASS_NAME())
  // $method_name : 呼び出すメソッド名のstring
  // $args : 呼び出すメソッドの引数
  //    array('args1' => 'hogehoge', 'args2' => 'fugafuga', ...)
  public static function invokeStaticMethod($class_obj, $method_name, $args)
  {
    $test_class = new ReflectionClass($class_obj);
    $method = $test_class->getMothod($method_name);
    // メソッドを外からアクセス可能にする
    $method->setAccessible(true);
    // invokeArgsメソッドの第一引数をnullにするとstaticメソッドを呼び出せる
    return $method->invokeArgs(null, $args);
  }
}

この関数をテストクラスから呼び出すと、privateな関数の結果を取得できるので、その値をテストする

class Logic_Hogehoge
{
  private static function hoge_add($hoge, $piyo)
  {
    return $hoge + $piyo;
  }
}

-------
// phpunitテスト
public function test_private_func()
{
  args = array(
    'args1' => 2, 'args2' => 3
  );
  $result = Test_Util::invokeStaticMethod(new Logic_Hogehoge(), 'hoge_add', args);
  $this->asserEquals($result, 5);
}

こんな感じでstaticでprivateなメソッドでもテストを書くことができました。

ajaxを使った遷移のないページで戻るボタンを利用

ajaxを使ってページ遷移をしないシステムを扱っています。

ページ遷移をしない時に厄介になるのが、URLが変わらないゆえにブラウザの進む、戻るが効かない事。

そこで現在のシステムでは、進む、戻るを有効にするhashchangeというプラグインを導入しています。

Ben Alman » jQuery hashchange event

ハッシュが変わった時にイベントを追加できるというものです。

これを使うことで、ajaxや「http://url.html#hoge」のようにurl内でアクセスをした時にイベントが発生して、そのタイミングで進む、戻るが効くようになるということです。

 

以下のページに詳しくまとまっていますので、プラグインの詳細や使い方は以下参照。

jQueryのAjaxやタブ切替などでブラウザの「戻る」「進む」が有効になる「hashchangeプラグイン」(実装解説つき): 小粋空間

 

問題

先日、システムのjQueryを1.10に上げるという作業が発生しました。

テストしてみたところ、jquery.ba-hashchange.js内でエラーを起こしていたのを発見。

本家サイトをみてみると、そもそもjQuery 1.2.6, 1.3.2, 1.4.1, 1.4.2しかサポートしてませんでした。

しょうがないので、上記のサイトを参考に中身のコードを読んでいると、以下のようなコードを発見。

$.browser.msie && !supports_onhashchange && (function(){

ブラウザごとに動作が違うらしく、 IEの判定を行っていました。

この$.browserはjQuery1.3からサポート外、1.9で完全に削除となっていたので、使えなくなっています。

上記のサイトそのままのように$.support.msieとやろうとすると、supportの中にはIEを判定、といったようなブラウザを直接判定する機構が存在しないので、IEでは進む、戻るが使えなくなります。

解決

要はIEの判定ができればいいわけなので、ここを以下のようにかえます。

$.support.noCloneEvent && !supports_onhashchange && (function(){ 

とするとIEにはclone eventがないので、IEであるという判定ができます。

これでIEでも進むと戻るが有効になりました。

settingslogicが使えない

railsのアプリの設定を外部ファイルに書けるものはないかなーと探してたら、settingslogicというgemにたどり着いた。

Rails3で設定ファイルを作りたいとき - t-taira blog

 

gemファイルに以下のように書き込んでbundle installを実行

gem 'settingslogic'

したのに以下のようなエラーがでてSettings.hogehogeにアクセスができなかった。

NameError (uninitialized constant Settingslogic):
  app/models/settings.rb:1:in `'

 

解決

bundle installで失敗していたのに気付いていなかっただけだった。

$ bundle install
You are trying to install in deployment mode after changing
your Gemfile. Run `bundle install` elsewhere and add the
updated Gemfile.lock to version control.

If this is a development machine, remove the Gemfile freeze 
by running `bundle install --no-deployment`.

You have added to the Gemfile:
* settingslogic

そのまま解決方法も書いてあったので、ここに書いてあるコマンドを実行したら無事使えるようになりましたとさ。

bundle install --no-deployment

railsサーバ動かしてるとめっちゃ出てくる警告を消す

 

railsのサーバを起動すると以下のような警告がでてきてめっちゃうざい。

WARN  Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true

 

loggerで吐き出したログが見れないので、この警告を消し去る。

この警告はサーバにWEBricを使ってると絶対に出るもので、開発者の人も気にしなくていいよと言っているから、遠慮なく消す

 

環境

mac os lion

ruby 1.9.3p327(rbenvでインストール)

 

解決法

下のリンクにのっている通りに内容を書き換える

https://bugs.ruby-lang.org/attachments/2300/204_304_keep_alive.patch

 

rbenvでインストールしたので、そのディレクトリの下にありました

vim ~/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/httpresponse.rb

rubyのバージョン変えたらその都度書かないとだめぽそう

205行目の該当箇所を書き換え

 

if chunked? || @header['content-length']
->
if chunked? || @header['content-length'] || @status == 304 || @status == 204

 

これで警告はでなくなりました

railsでmodelに変更を加えたらNoMethodErrorが出るようになった

railsでアプリケーションを作っていたら、modelに他の変数が必要になったので追加したらNoMethodErrorが出るようになった。

初心者なので正しい手順じゃない可能性があるが、一応修正できたのでまとめ

 

問題

Userモデルに「screen_name」という変数を加えたかったので、一度Userを削除して

rails generate model user hogehoge:string fugafuga:string screen_name:string

として作りなおした。

その後、

rake db:migrate

でdbを作りなおしたが、userクラスのscreen_nameを使おうとするとNoMethodErrorに襲われた

 

解決

明らかに新しく追加した変数を使う時にエラーになっているので、db関連のエラーとあたりを付けて、そのへんのファイルをあさってみたら関係しているっぽいファイルを見つける。

schema.rb

ActiveRecord::Schema.define(:version => 20130808153613) do
     create_table "users", :force => true do |t|
         t.string "hogehoge"
         t.string  "fugafuga"
         t.datetime "created_at", :null => false
         t.datetime "updated_at", :null => false
     end
end

 

となっており、新しく作ったはずのscreen_nameという文字が見当たらない。

これがなんの時に作られるファイルなのか調べていたら、db生成時に自動的に作られるファイルらしいという事がわかる。

つまりdbを一度初期化してしまえばいいんじゃないか?ということで

 

rake db:migrate:reset

 

を実行してみたところ、scheme.rbのなかに変数が追加された事が確認された。

アプリケーションもばっちり動く。

 

結論

modelを作りなおしたら一度「rake db:migrate:reset」を行え

railsでauth認証をさせようと思ったらエラーがでる

railsアプリでauth認証させようと思ってプラグインを入れてたら全然違う所でエラーが出たので備忘録

 

問題

認証に使うのはOmniAuthというプラグイン。

twitterでもfacebookでもopenIDでも認証してくれるというすごいものらしい。

 

 

ここを参考にして色々と設定していって、いざ認証!としたらtwitterでcallbackしてきた時に

We're sorry, but something went wrong.

という謎のエラーに襲われた。

 

解答

logディレクトリにあるログをみてみると、

sessions_controller.rb:13: invalid multibyte char (US-ASCII)

というエラーが出ていた。

日本語が入ってるとダメらしい。

日本語が入る場合はファイルの一番最初に

# -*- coding: utf-8 -*-

と記入する事。

コメントが1行でもファイルの先頭に入っていると、この記述が有効にならないので必ず0行目に書く。ここで結構つまった