l'essentiel est invisible pour les yeux

Saturday, November 11, 2006

[rails] 実践RSpec on Rails - コントローラとモデルのBehaviourを書く

先日RSpec on Rails0.7(0.7.1も)が出ました。
まだまだ枯れていないので実際のプロジェクトで採用するのは難しい面もありますが、isoration from Databaseを支えるmock/stub frameworkや、isoration from viewsは強力なので、それほど影響の無い作成済みの社内アプリにRSpec on Railsを適用してみました。

maihaさんがやっておられるように、自作で作るのもすごく楽しそうなのですがとりあえずは使ってみます。mock/stub frameworkの実装の詳細をあとでみる。

BBDでは、Behaviourを書いてからコードの実装を行うのですが、今回は以前にうみがめで作った社内用情報共有ツールBasecamp(某signalsのパクリ)にBehaviourを書いていくことになります。

BasecampのSpec

  • 社内でブログをベースに情報共有を行うグループウェア
  • 認証は、Ajaxによるワンタイムパスワード認証
  • 3日以内で作るために、BBDもTDDをしていなかった。
  • はてな記法が使える。はてな日記のインポートが可能
rake stats

[4141]% rake stats [/var/www/sankhon]
(in /var/www/sankhon)
+----------------------+-------+-------+---------+---------+-----+-------+
| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Helpers | 37 | 33 | 0 | 5 | 0 | 4 |
| Controllers | 620 | 480 | 9 | 54 | 6 | 6 |
| Components | 0 | 0 | 0 | 0 | 0 | 0 |
| Models | 82 | 75 | 12 | 3 | 0 | 23 |
| Libraries | 574 | 472 | 6 | 15 | 2 | 29 |
| Model specs | 16 | 13 | 0 | 0 | 0 | 0 |
| View specs | 0 | 0 | 0 | 0 | 0 | 0 |
| Controller specs | 54 | 42 | 0 | 0 | 0 | 0 |
| Helper specs | 0 | 0 | 0 | 0 | 0 | 0 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total | 1383 | 1115 | 27 | 77 | 2 | 12 |
+----------------------+-------+-------+---------+---------+-----+-------+
Code LOC: 1060 Test LOC: 55 Code to Test Ratio: 1:0.1

You have new mail.
[4146]% [/var/www/sankhon]


セットアップ

% sudo gem install rspec
% sudo gem install zentest -v 3.4.1
% sudo gem install diff-lcs
% ./script/plugin install svn://rubyforge.org/var/svn/rspec/tags/REL_0_7_0/vendor/rspec_on_rails/vendor/plugins/rspec


AccountController#loginがログインに関する実装を提供するコントローラである。
GETの場合は、ワンタイムパスワードのためのトークンを埋め込んだログイン画面を表示し、POSTの際は認証を行い成功すると200を返し失敗すると400を返す。
ログインはXMLHttpRequestで行う。

BBDではBehaviourを書いてから実装するのがセオリーだが、ここでは既にあるコードにBehaviourを書いて必要に応じてリファクタリングを施していく。

旧AccountsController#login

def login
case request.method
when :get
@challenge_code = [rand(64), rand(64)].pack("C*").tr("\x00-\x3f", "A-Za-z0-9./").crypt([rand(64), rand(64)].pack("C*").tr("\x00-\x3f", "A-Za-z0-9./"))
session[:challenge_code] = @challenge_code
when :post
if user = User.find_by_username(params[:username])
hashed_password = Digest::MD5.new(user.password+session[:challenge_code]).to_s
if params[:hashed_passwd] == hashed_password
session[:challenge_code] = nil
session[:user] = user
render :text => "true"
else
render :text => "パスワードが間違っています", :status => 401 # Unauthroized
end
else
render :text => "入力されたユーザは存在しません", :status => 401 # Unauthroized
end
end
end

当初実装していたコードでは、認証の成功/失敗に関するBehaviourを書きづらいので、認証部分のロジックをモデルに移して再実装する。始めに、モデルに実装する認証メソッドのBehaviourを定義する。

[4138]% ./script/generate spec_model user

UserモデルのBehaviourを定義する。
spec/model/user_spec.rb
require File.dirname(__FILE__) + '/../spec_helper'
require 'digest/md5'

context "User class with fixtures loaded" do
fixtures :users

specify "should count one Users" do
User.should_have(1).records
end

specify "生のパスワード+トークンをMD5ハッシュ化した文字列をワンタイムパスワードとする" do
user = users(:junkonno)
challenge_code = [rand(64), rand(64)].pack("C*").tr("\x00-\x3f", "A-Za-z0-9./").crypt([rand(64), rand(64)].pack("C*").tr("\x00-\x3f", "A-Za-z0-9./"))
user.certify(challenge_code, Digest::MD5.new(user.password+challenge_code).to_s).should_eql true
end
end

Behaviourに基づきUser#certifyを実装する。(関連部分のみ)
app/model/user.rb

require 'digest/md5'

class User < ActiveRecord::Base
def certify(code, hashed_passwd)
hashed_passwd == Digest::MD5.new(self.password+code).to_s
end
end

fixturesを定義し検証を行う。
spec/controller/users.yml

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
junkonno:
id: 1
username: 'junkonno'
password: 'junkonno'

User#certifyを検証する。

[4138]% ./script/rails_spec spec/models/user_spec.rb [/var/www/sankhon]

..

Finished in 0.102308 seconds

2 specifications, 0 failures
[4141]% [/var/www/sankhon]


検証クリア。
User#certifyを使用して認証を行うようにAccountsController#loginを書き直す。
app/controller/accounts_controller.rb

when :post
user = nil
if (user = User.find_by_username(params[:username])) && user.certify(session[:challenge_code], params[:hashed_password])
session[:user] = user.dup
session[:challenge_code] = nil
render :text => "true"
else
render :text => "ユーザが存在しないかパスワードが間違っています", :status => 401 # Unauthroized
end
end

書き直したAccountsControllerに関するBeahviourを定義する。

spec/controller/accounts_controller_spec.rb

require File.dirname(__FILE__) + '/../spec_helper'

context "The AccountsController" do
# fixtures :accounts
controller_name :accounts

setup do
@user = mock('user')
@user.stub!(:new_record?).and_return(false)
User.stub!(:new).and_return(@user)
end

specify "should be a AccountsController" do
controller.should_be_an_instance_of AccountsController
end

specify "/accounts/loginへGETした際は、チャレンジコードを設定する" do
get 'login'
assigns[:challenge_code].should_match /[\w\d]+/
end

specify "/accounts/logoutへPOSTした際は、認証を行い成功したらtrueを出力し200を返す" do
User.should_receive(:find_by_username).with('junkonno').and_return(@user)
@user.should_receive(:certify).and_return(true)
controller.should_render :text => "true"
post 'login', :username => 'junkonno'
end

specify "/accounts/logoutへPOSTした際は、認証を行い失敗したら401を返す" do
User.should_receive(:find_by_username).with('junkonno').and_return(@user)
@user.should_receive(:certify).and_return(false)
controller.should_render :status => 401, :text => "ユーザが存在しないかパスワードが間違っています"
post 'login', :username => 'junkonno'
end
end
RSpec on Rails0.7の強力な機能である、mock/stub frameworkを使用する。
Mock/stubを使用することで、モデルとDBを切り離しBehaviourを書くことが出来る。

次のコードは、User#newでインスタンスが作成される際に、実際のインスタンスではなくSpec::Mocks::Mockクラスのインスタンスを返すようにスタブを定義する。コントローラ内でUser.newが実行された場合は、Mockオブジェクトのインスタンスである@userが返される。

setup do
@user = mock('user')
@user.stub!(:new_record?).and_return(false)
User.stub!(:new).and_return(@user)
end
setupのブロック内でスタブを定義し、specifyのブロック内でshould_receiveによってスタブメソッドを再定義するのが推奨されているやり方です。二つのメソッドは期待される結果は同じだが、stub!が検証(例外とか)されないのに対して、should_receiveは検証が行われる。
Behaviourは、プログラマでなくとも普通の英文として読むことができます。美しい。

assingsオブジェクトでは、コントローラ内で設定されたクラス変数を確認することが出来る。

specify "/accounts/loginへGETした際は、チャレンジコードを設定する" do
get 'login'
assigns[:challenge_code].should_match /[\w\d]+/
end
'junkonno'が引数に与えられて、User.find_by_usernameが呼び出されたときには、@userを返すようにする。@user.certifyが呼びださされた際には、trueを返し認証が成功した際のBehaviourを検証する。

User.should_receive(:find_by_username).with('junkonno').and_return(@user)
@user.should_receive(:certify).and_return(true)

検証を行う。
[ERROR:1]% ./script/rails_spec spec/controllers/accounts_controller_spec.rb                                                           [/var/www/sankhon]

....

Finished in 0.233141 seconds

4 specifications, 0 failures
[4148]% [/var/www/sankhon]


まとめ
今回は、ログインの実装に関するBehaviour(コントローラとモデル)を定義してリファクタリングを行った。
BBDを進める際は、ロジックを可能な限りモデルに定義しBehaviourを書いていくほうがよい。既存のソースに適用することでソースコードが綺麗になる。
次回は、controllerとviewsのBehaviourの定義。

Rspecのサイトにも書かれているように、RSpec0.7以上ではControllerとViewsのBehaviourを完全に分離して定義できます。これによりViewによりControllerのBahaviourが失敗するといったことがなくなります。そして、Views単体のBBDはselenium等のほかのフレームワークを利用して進める方法が推奨されています。

では、ふるたにでした。

参考
Mock Objects
Mock API
RSpec on Rails – Specifying Controllers
RSpec on Rails – Specifying Models

Wednesday, November 08, 2006

My Stiq(という表現は(ry)をブログに表示してみた

Be Stiqというサービスで好きなページにStiqをはって緩いコミュニケーションができるのだが、自分が張ったStiqをブログに表示してみた。右のバー。

UIE Japanはよく英語がとびかうのだが、英語に精通している人にとって"MyStiq"という表現はアレらしいが、他の表現も見つからなかったのでそうした。

データは全てAPIで取得できる。callbackパラメータを渡す事でJSONPで取得できるので、scriptタグを埋め込めればどこでも表示できる。


function showMyStiqs(json) {
//var data = eval(json);
for(var i=0,l=data.length;i<l;++i) {
try {
document.write('<li><a href="'+data[i].url+'">'+data[i].title+' ('+data[i].replies.length+')</a></li>');
}catch(e){}
}
}


<script type="text/javascript" src="http://be.stiq.net/apis/stiq?username=rakuto&callback=showMyStiqs" charset="UTF-8"></script>

毎回、ソースコードの色付けが面倒だけど、ハイライトのないソースコードは見るのも苦手なので頑張ってつけている。よしソースコードのハイライトをつけるGreaseMonkey script、VimColor at Bloggerつくろう。

Tuesday, November 07, 2006

[Javascript] iTunes7のように画像を水面に反射させたように表示する

賛否両論ある、iTunes7のアートワークの3D表示
でも、画像を水面反射したような見せ方をするだけでCool!と思わせれるかも?しれない。
意外と簡単に作れるので自作のフレームワークに組み込むのもよさそうです。

で、一日一つ新しい事を習得する(と決めた)ための、今日のネタはCanvasタグ。
ネタ自体は既出ですがCanvasタグの練習がてら作ってみました。

デモ

画像のURLを直接入力するか、URLをリストから選んでCreateボタンを押すとアルバムのジャケットのアートワークが水面反射付きで表示される。

現在

  • Saffariでは動作未確認。(動作未確認。Canvasタグの実装にバグがある様子)

ソースはシンプル。

var UA = navigator.userAgent.toLowerCase();
var isIE = (UA.indexOf('msie') != -1) &&amp;amp; !window.opera;

function addImageAndReflect(url, opacity) {
var opacity = opacity || 0.7;

/* image and reflection wrap element */
var imgWrapElm = document.createElement('div');
imgWrapElm.className = 'imgWrap';
var image = new Image;
image.src = url;
image.style.display = 'block';

if(isIE) {
var reflectImgElm = document.createElement('img');
reflectImgElm.src = url;
reflectImgElm.style.filter = "flipv progid:DXImageTransform.Microsoft.Alpha(opacity="+(1-opacity)*100+",finishopacity=0,style=1,startx=0,starty=0,finishx=0,finishy="+image.height+")"; // ⑤
imgWrapElm.appendChild(image);
imgWrapElm.appendChild(reflectImgElm);
document.body.appendChild(imgWrapElm);
} else {
/* canvas element */
var canvas = document.createElement('canvas');
image.onload = function() {
canvas.width = image.width;
canvas.height = image.height;
var ctx = canvas.getContext('2d');
with(ctx) {
save();
translate(0, image.height-1);
scale(1,-1);
drawImage(image, 0, 0);
restore();
var gradient = createLinearGradient(0, 0, 0, image.height);
gradient.addColorStop(1, "rgba(0, 0, 0, 1)");
gradient.addColorStop(0, "rgba(0, 0, 0, "+opacity+")");
globalCompositeOperation = 'destination-out';
fillStyle = gradient;
fillRect(0, 0, image.width, image.height);
}
imgWrapElm.appendChild(image);
imgWrapElm.appendChild(canvas);
document.body.appendChild(imgWrapElm);
}
}
}


①でcanvasタグとImageオブジェクトを作成する。
IEなどcanvas.getContextに対応していないブラウザのために処理を条件分岐。

②の解説。
Canvasへの画像の描画は、drawImageを使用する。drawImageをコールする前に画像の読み込みを完了している必要があるため、onloadイベントハンドラとしてCanvasタグの描画を行う。読み込みが完了していないのに、drawImageを呼び出すとExceptionを吐いてスクリプトが停止する。

③の解説。
原点を移動して、グリッドの座標を変換することで画像をY軸に対して反転させる。原点移動の際に-1をしているのは、オリジナルの画像との隙間を無くすため。

④の解説。
グラデーションをかけるための矩形図形を作成する。
画像のサイズと同じ長方形を作成し塗りつぶしを作成したグラデーションに指定する。

rgbaは、RGB値に加えて不透明度(0~1)を指定する。
上記のグラデーションは、黒色の完全に不透明の状態から、不透明度0.7の黒色までのグラデーションを作成している。
var gradient = createLinearGradient(0, 0, 0, image.height);  // ④
gradient.addColorStop(1, "rgba(0, 0, 0, 1)");
gradient.addColorStop(0, "rgba(0, 0, 0, 0.7)");

グラデーションを塗りつぶしに指定した矩形図形を画像の上に表示するように指定している。
参考: Canvas tutorial:Compositing

globalCompositeOperation = 'destination-out';

塗りつぶしを作成したグラデーションに指定してから、画像と同じサイズの図形を描画する。

fillStyle = gradient;
fillRect(0, 0, image.width, image.height);

と、意外と簡単に水面反射を作成することが出来る。

⑤はIE対応。
IEにはCanvasが実装されていないため、フィルタを用いて同様の効果を実現する。
flipvは画像を反転させるフィルタ。

参考
WHATWGのCanvas仕様
Canvas tutorial:Drawing shapes
Canvas tutorial:Compositing
Canvas tutorial:Transformations
Canvas tutorial:Applying styles and colors
Reflection.js

Monday, November 06, 2006

Canvas.getContext.drawWindow

Firefox 1.5以上 only.

現在のウインドを描画する 削除




var canvas = document.getElementById('c1');
var ctx = canvas.getContext('2d');
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
with(ctx) {
translate(canvas.width/2, 0);
scale(0.6, 0.6);
rotate(Math.PI/4, Math.PI/4);
drawWindow(window, 0, 0, 600, 600, "#000000");
lineWidth = 2.0;
strokeRect(0, 0, 600, 600);
}

[Rails] RSpec on Railsを始めてみた - インストールと実行

RSpec on Railsのさわりだけ。

おそばせながら今頃、BDD(Behaviour Driven Development)を知りました。。。
これからはきちんとBDD or TDDで開発を進めます。

BDDはテスト駆動開発と言葉の言い回しが大きく異なっている。
「ベヒイビアを書いて仕様を設計する。」これが大きなポリシー。
BDDでは必ず仕様コード(spec)を書いてから実際のコーディングを行う。


Rubyには、RSpecというツールがありこれを利用する。
gemパッケージが用意されているので簡単。


$ gem install rspec


次にRSpec on Railsプラグインをインストール。
REL_X_Y_Zの部分をrspecのバージョンとあわせる必要がある。

$ cd RAILS_ROOT
$ ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/REL_0_6_4/vendor/rspec_on_rails/vendor/plugins/rspec


RSpecでは、spec/ディレクトリ以下にspec, fixturesファイルなどが展開される。
始めにこのディレクトリを作成する。


$ ./script/generate rspec


インストール終わり。

テストの実行方法がいくつか用意されている。

rakeを使用する場合
  1. 全てのビヘイビアを実行
  2. コントローラのベヘイビアを実行
  3. モデルのビヘイビアを実行
それぞれ、

$ rake spec
$ rake spec:controllers
$ rake spec:models

で実行可能。

単体でも仕様を検証可能
単体で仕様(ビヘイビア)を満たすか検証することも可能。
先にscript/rails_spec_serverというプログラムを起動しておく。(Railsの環境設定が読み込まれるため実行が早くなる)

次の例では、personモデルの仕様を検証している。

$ ./script/rails_spec_server
$ ./script/rails_spec spec/models/person_spec.rb


コントローラー・モデルの作成
RSpecには、Specも同時に生成してくれるジェネレータが定義されている。
それぞれ、

$ ./script/generate rspec_controller index
$ ./script/generate rspec_model person

として利用でき、


create spec/controllers/
create spec/views/index
create spec/controllers/index_controller_spec.rb
create spec/models/
create spec/fixtures/
create spec/models/person_spec.rb
create spec/fixtures/people.yml

といった感じで通常のジェネレータに加えて、(モデル名 | コントローラ名)_spec.rbというファイルなどを生成してくれる。

仕様(べヘイビア)を書く
Personモデルを作成して、spec/models/person_spec.rbを編集して仕様を定義した。

require File.dirname(__FILE__) + '/../spec_helper'

context "Person class with fixtures loaded" do
fixtures :people

specify "should count two People" do
Person.should_have(2).records
end

specify "should full name is 'first_name last_name'" do
person = Person.find(1)
person.full_name.should_equal "#{person.first_name} #{person.last_name}"
end
end


app/model/person_spec.rbにメソッドを定義
class Person < ActiveRecord::Base
def full_name
"#{self.first_name} #{self.last_name}"
end
end

テスト実行
別ターミナルでscript/rails_spec_serverを実行しておく。

[4246]% ./script/rails_spec spec/models/person_spec.rb [/var/www/test]

..

Finished in 0.114999 seconds

2 specifications, 0 failures
[4248]%

この二つの仕様は正しいのでピリオドが二つ出力された。
仕様(spec)の数だけピリオド(またはF)が出力される。

should_equalをshould_not_equalに書き換えて、意図的に失敗させた場合は、次のように出力される。

[4237]% ./script/rails_spec spec/models/person_spec.rb [/var/www/test]

.F

1)
Spec::Expectations::ExpectationNotMetError in 'Person class with fixtures loaded should full name is 'first_name last_name''
"rakuto furutani" should not equal "rakuto furutani"
./spec/models/person_spec.rb:12:in `should full name is 'first_name last_name''

Finished in 0.103591 seconds

2 specifications, 1 failure
You have new mail.
[4250]%

二つ目のSpecが正しくないことがわかる。

簡単だが今日はここまで。
次回、RSpec on RailsとRSpecの中身を詳しくみていきたい。


参考
RSpec on Rails
RSpec
RSpec Documentation

Sunday, November 05, 2006

[javascript] IEとFirefox2.0以上で動作するClient Side Storage

Firefox2.0では、WHATWGのWeb Application1.0仕様の一部である、Client Side Storageを実装しています。FirefoxのClient Side Storageには、セッション中のみデータが有効で同一ドメイン間のみアクセスできるSession Storageと永続データ管理が可能でクロスドメインでデータにアクセスすることもできるGlobal Storageがあります。

これにより、IE5以上とFirefox2.0以上で永続データ管理が可能になりました。どうせなら同じAPIで使用したいのでFacadeデザインパターンを使用し、カプセル化したいと思います。

Client Side Storageデモ


その前に、Firefox2.0sessionStorageとglobalStorageについて。

sessionStorage
sessionStorageはセッションが切れるとデータも削除されます。また同じドメイン間でしか同じストレージにアクセスすることは出来ない。
Web Application1.0仕様そのままですが。
同一サイトの複数のウインド間でチェックボックスの値を共有することができる。


<label>
input type="checkbox" onchange="sessionStorage.insurance = checked">
I want insurance on this trip.
</label>
if (sessionStorage.insurance) { ... }


今回使用したのは、globalStorage.

globalStorage
globalStorageはセッションをまたがって永続的にデータを格納することが出来ます。データの保存方法によっては、クロスドメインからアクセスすることもできるのでセキュリティ上注意が必要です。

  • globalStorage[''] は全てのドメインからアクセス可能
  • globalStorage['com'] は.comドメインならアクセス可能
  • globalStorage['example.com'] は.example.comドメインならアクセス可能
  • globalStorage['www.example.com'] はwww.example.comとexample.comからアクセス可能


IE5以上に実装されている、UserData BehaviourとglobalStorageを共通のAPIで使えるようなクラスを書いてみた。

Client Side Storageデモ
複数のウインドを開いて片一方のウインドでTEXTAREA内を編集し保存→もう一方で復帰すると保存したデータが表示される。もちろんブラウザを閉じてもデータは有効。
// UserAgent
var UA = navigator.userAgent.toLowerCase();
var isIE = ((UA.indexOf('msie') != -1) && (UA.indexOf('opera') == -1))? true : false;
var isGecko = (UA.indexOf('gecko') != -1)? true : false;

// Client Side Storage Facade pattern
var ClientSideStorage = Class.create();
ClientSideStorage.prototype = {
_storage: null,
_sStoreName: 'ClientSideStorage',
initialize: function(options) {
options = options || {};
this._namespace = options.namespace || ((location.port == "")? location.host :
location.host.substr(0, location.host.length - location.port.length - 1)); // ポート番号を取り除く
this._expires = options.expires || new Date(2085, 3, 12);
if(isGecko && globalStorage) this._storage = globalStorage[this._namespace];
else if(isIE){
this._storage = document.createElement('div');
this._storage.style.behavior = "url(#default#userData)";
this._storage.expires = this._expires.toUTCString();
(document.getElementsByTagName('body')[0] || document.documentElement).appendChild(this._storage);
this._ie = true;
} else {
throw "Client side storage is supported at only since IE 5.0 and Firefox 2.0";
}
},
setItem: function(key, value) {
if(!this._ie) this._storage[key] = value;
else {
this._storage.setAttribute(key.camelize(), value);
this._storage.save(this._sStoreName);
}
},
getItem: function(key) {
if(!this._ie) return this._storage[key];
else {
this._storage.load(this._sStoreName);
return this._storage.getAttribute(key.camelize());
}
},
removeItem: function(key) {
if(!this._ie) this._storage.removeItem(key.camelize());
else {
this._storage.load(this._sStoreName);
this._storage.setAttribute(key.camelize(), '');
}
}
}

で使ってみる。

var storage = new ClientSideStorage;
storage.setItem('data', 'えいぞくでーた');
console.log(storage.getItem('data')); // => "えいぞくデータ"
storage.removeItem('data'); // => 削除
console.log(storage.getItem('data')); // => undefined

動いた。

一点気をつける必要があるのは、globalStorage['domain']に与えるドメイン名にポート番号を含んでいるとSecurity Errorが発生するのでポート番号は取り除いている。

参考
Firefox, JavaScript and the Web
5.3. [SCS] Client-side session and persistent storage
[MSDN] Introduction to Persistence

Saturday, November 04, 2006

[javascript] オフライン/オンライン切り替え時に呼び出すイベントハンドラを定義する

少し前にうみがめで自社用の社内ブログシステムを作ったのですが、最近naotakeさんにAny devices, Any Networkどうよ!とアドバイスをもらった。

これはつまり、どんなデバイスからでもアプリケーション利用することができ、オンライン/オフライン関係無しにサービスを利用できるというものだ。クロスデバイスの話はUIEngineでさくっとてことでひとまず置いておいて、ここではオンライン/オフラインの話。

Youtubeによるデモムービーが話題になっている、Scrybeもオフラインでも利用できるカレンダーアプリケーションです。詳しく見ていませんが、Google Docsもローカルで動作することを感じさせるコードが埋め込まれています。

http://docs.google.com/javascript/Utilities.js?v=cnz
のGetMashedURLあたり。

オフラインでもアプリケーションを利用でき、オンラインになったらサーバと同期するスタイルは、WebベースのOfficeアプリケーションには必須となりそうです。

となると、オンライン/オフライン切り替え時のイベントハンドラを定義したくなる。
オフライン時は、データストア先をローカルのストレージに切り替え、オンラインになったらサーバと同期を取る。そこで、イベントハンドラをアタッチできるかどうか調べてみた。

動作デモ

まず始めに、ブラウザがオフラインモードかどうか調べるには、


navigator.onLine // #=> true or false

で調べることが出来る。
(注意) 正常にネットワークにつながっているかどうかを検出できるわけではない。

windowエレメントかHTMLエレメントにそれらしきイベントハンドラをアタッチできないだろうか?という疑問がわく。

window.addEventListener('online', function(){alert('オンラインになりました。')}, false);
window.addEventListener('offline', function(){alert('オフラインになりました。')}, false);

残念ながらonline/offlineイベントに対するイベントリスナを定義することは現在の実装ではできない。

Web Applications 1.0 Working Draft — 4 November 2006に提案されているように、近いうちに実装されるかもしれない。

そこで、Object#watchを使用してnavigator.onLineを監視しようと考えた。
Firefox 1.5, 2.0で動作を確認。(IE未対応)


window.addNetoworkEventListener = function(type, listener, useCapture){
if(type == 'online') {
navigator.watch('onLine', function(id, oldval, newval){
if(newval) listener();
return newval;
});
} else if(type == 'offline') {
navigator.watch('onLine', function(id, oldval, newval){
if(!newval) listener();
return newval;
});
}
}
window.addNetworkEventListener('online', function(){alert('オンラインになりました。')}, false);
window.addNetworkEventListener('offline', function(){alert('オフラインになりました。')}, false);

これは正しく動作しなかった。
navigator.onLineの値はファイル→オフライン作業を選択することでtrue or falseに切り替わる。ブラウザ内部からプロパティが変更された場合は、watch式は動作しないようだ。

そこで、navigator.onLineの値を定期的に監視し変更があった場合は別に用意したオブジェクトのプロパティを変更する。そのプロパティをObject#watchによって監視するというアプローチを取ってみた。

Firefox 1.5, 2.0 限定 (IE未対応)
デモ

var Status = {
_onLine: navigator.onLine,
observers: {online: [], offline: []},
_update: function() {
if(Status._onLine != navigator.onLine) {
Status._onLine = navigator.onLine;
}
},
isOnline: function() { // wrapper of navigator.onLine
return navigator.onLine;
},
observe: function(type, listener, useCapture){
type == 'online'? Status.observers.online.push(listener) : Status.observers.offline.push(listener);
Status.watch('_onLine', function(id, oldval, newval) {
if(newval) for(var i=0,l=Status.observers.online.length;i<l;++i) Status.observers.online[i]();
else for(var i=0,l=Status.observers.offline.length;i<l;++i) Status.observers.offline[i]();
return newval;
});
},
checkInterval: function(delay/*=1000*/) {
setInterval(Status._update, delay || 1000);
}
}
Status.checkInterval(1000);
Status.observe('online', function() {alert('オンラインになりました');});
Status.observe('offline', function() {alert('オフラインになりました');});

これは正しく動作する。
厳密には、オフライン/オンラインイベントハンドラをアタッチしていることにはならないが、"それっぽく"動作する。早くonline/offlineイベントを実装してほしい。

参考
Core JavaScript 1.5 Reference:Global Objects:Object:watch
5.2.1. [SCS] Offline Web applications

[Firefox] Live Http HeadersをFirefox2.0にインストール

ブログにするほどのネタではない。
Live Http Headers プラグインが2.0に対応するバージョンが配布されていないので、手動でmaxVersionを書き換えてインストールする必要がある。

Application Dataフォルダから
\Application Data\Mozilla\Firefox\Profiles\pysto7f5.default\extensions
というようにディレクトリ(多少異なるかも)をたどり、install.rdfを


<em:maxversion>2.*</em:maxVersion>


に書き換えて、7-ZIPを使用して、全てのファイルを選択し、ZIPで圧縮し拡張子を.xpiにする。出来たファイルをFirefoxにドラップ&ドロップしてインストールする。

ざっと使った感じでは、2.0でも問題なく動作する。

[Javascript] 履歴を残さずページ遷移

技術メモ

location.replaceを使ってページ遷移させるとブラウザのヒストリに残らない。


var new_url = "http://www.google.co.jp/";
location.replace(new_url);

Sunday, October 22, 2006

Be Stiqに便利なスクリプト

Be Stiq
というサービスに関連した便利なスクリプトを紹介します。


Greasemonkey user script

まずは二つ
Firefox + Greasemonkeyをインストールすることで使用できるスクリプトです。

Stiq Notifier


















一つ目は、sfukunagさんが作ったStiq Notifierです。
画像が小さく見えにくいのですが、右下のBoxに同じタグがつけられたStiqが表示されています。
また、このNotifierは以下のような機能を実装しています。

  1. 今見ているページにStiqが張っている際は、右下のSの文字が赤く点滅。クリックするとStiqのタイトル表示。
  2. Be Stiq経由のページを閲覧している際は、そのページに張られたStiqと同じタグが貼られているページを表示。
  3. 右下の青色状態のSボタンを押すことで、Stiqページ(実際のページの裏のようなイメージ)に切り替え。
右下のSボタンが赤色のときは、Stiqが存在するページ。クリックするとStiqのタイトル等を表示。
青色のときは、Stiqが存在しない。クリックするとStiqページへ。
となっています。

WEBに書かれている情報は、決して正しいわけではないので、一ユーザがそのページの本当の情報や、またより有益な情報を書き込むことで、WEBサイト全体がCGMとなるのではないか?という考えです。また、現在見ているサイトと同じカテゴリを持つサイトを知れるのは、便利です。


二つ目は、完全にローカルで動作するBe Stiqです。


Be Stiq Annotation


















現在、セキュリティ上の理由から認証が必要なページやSSLのページにStiqを張ることが出来ません。
こちらのStiqでは、ローカル上で動作するため、ログインが必要なページにもStiqを張ることができます。また、データはローカルに保存されるため、自分用のStiqです。

Be Stiqとは少し違ったUIを試していて、Be Stiqの張り方が少し異なっています。
右上のONボタンを押して、範囲をドラッグ&ドロップで選択することでその場所にStiqが張られます。

List機能を備えており、「あ、あとで読もう」と思った際に、とりあえず一言書いてStiqを張っておき、Listボタンをクリックすると、Stiqを張ったページの一覧をサムネイルで表示することが出来ます。

サムネイル作成には、Simple APIを利用させていただいています。ありがとうございます。

Stiq内のキーワードをインクリメンタル検索することが可能なので、とりあえずキーワードをつけてStiqをはって後でまとめ読みという使い方もできます。

pretty cool.

Stiqはダブルクリックすることで編集可能です。×ボタンを押すと削除できます。


IE Bookmarklet 版

IEの悪口を言うと、実際にIEを作った人たちがいるので「いや、IEは凄いんだよ。」と宗教論争が勃発するのがうちの会社...

Local Be Stiq

リンクをお気に入りに追加して、追加されたリンクをリンクバーにドラッグ&ドロップしてください。
クリックすると、いくつかの制限を除いてGreasemonkey版ローカルBe Stiqと同じように、動作します。

制限
Greasmonkey版と違い、Userdata Behaviourを利用して、永続的にデータを使用します。
クロスドメインにまたがって、永続的なデータにアクセスする方法がない?のでList機能がありません。

ブックマークレットなので、リンクをクリックしないとStiqが表示されません。


Be Stiqモードに切り替えるブックマークレット

Be Stiq
今見ているページをBe Stiqモードに切り替えるブックマークレットです。
IEの場合は、上記のブックマークレットど同様にセットアップし、Firefoxの場合は、リンクバーにドラッグ&ドロップしてください。

Sunday, September 17, 2006

パビリオン山椒魚でドーン


















はーい、息吸ってー、とめて!ドーン!
本物とか、偽者とかどっちでもいいの。

前作「亀虫」をみていないので、富永ワールドを始めて見ることになった。

山椒魚の名前は、キンジロー。生誕150年でパリの万国博覧会にも出されたすごい歴史をみてきた山椒魚だそうだ。キンジローが、すげーかわいい。

そのキンジローを盗んでレントゲンをとり、本物かどうか確かめるようにオダギリジョー演じる天才レントゲン医師の飛鳥が依頼をうける。

・・・んな、まともな説明は必要ないような・・正直わからない!!

なぜ山椒魚なのか?なぜレントゲン医師の次は山賊なのか?第二農響って何なんだよ!!
まったくわからない(笑
そんな理屈は必要ないんだろうな。でも、もう1回見ないと多分満足できない。
ジャジーな音楽と映像がカッコよく、キャストが豪華で、色んな人間を演じている映像ががたまらなくカッコよくて面白かった。左上の写真は、購入したサントラ。

メゾンド・ヒミコの時のオダギリジョーも最高にセクシーだったけど、レントゲン医師のオダギリジョーもぬめぇっーとした色気がでていて、やっぱカッコいいししかも笑わせてくれる。

女優役は、最近注目の若手女優(らしい。最近テレビを見なくなったので全くしらなかった。)香椎由宇。プロフィール見たら、1987年生まれ!いや、ビビッった。。

最近大切なものを失って、ずっと低空飛行している気分だったが、そんなわかっていながらもださださだった俺もぶっとんだ。






















話は変わるけど、東京人は祭り好きなの?シネセゾン渋谷を出ると、ドーン!ドーン!太鼓の達人の10倍くらいの音が聞こえてきた。

って出ようと思ったら、ムキムキのお兄ちゃん達が扉の前に座っていて開かない。おみこしが通り、太鼓が鳴り響き、道玄坂は祭りムードになっていた。

この前原宿でも、表参道の通りを全部車とめて祭りをやっていた。あんなでかい通りよくとめるよなとふと思った。東京ではこういう祭りをよく開催しているのかな?

Tuesday, September 05, 2006

国分寺のパスタ屋 せもりな

京都に帰ってきた。
3週間東京にいると結構疲れるので、2週間滞在→京都→2週間滞在。

東京では、グルメな友達を持つにかぎる!情報がないと、なかなか美味しいお店までたどりつけません。
そこで、先日のフットサルでの筋肉痛をこらえ、日本一うまいと書いている人もいるぐらいの国分寺のパスタ屋 せもりなまで足を運んでみました。

井の頭線で吉祥寺まで出て、JR中央線乗り換えで国分寺へ。
駅から歩くこと10分。

昔からそこにあるような小さな食堂らしき家を見つけた。
看板をみると、せもりなでした。


















これは、よさそうな感じ。
期待を抱いて店内へ。

メニューを見せてもらうと、パスタの種類もピザの種類も豊富でかなり悩む。

ワインのおすすめを聞くと、白ワインのGaviというワインを勧められたので、
とりあえず、それをハーフボトルでお願いしますで。

アラカルトが300円で超おすすめ!
とブログに書いている人がいたことが頭によぎったが、ここは最近ずっと食べたかった牡蠣を注文。


















大きな岩かきがそのまま出てきて、身がぷるぷる言ってる・・・。
レモンをしぼり牡蠣から出ているスープと一緒に食べた。実がぷりぷりしていて、美味しかったです。

次は、パスタ。
最低2種類食べたかったので、一人前の量を聞くと100g.
二つ頼むと、「少し一皿の量を減らしましょうか?」とシェフの人が気をつかってくれました。

一皿目は、フレッシュトマトのカペリーニ。



トマトとカペリーニのパスタは冷性パスタとしてにんにくとバジルで食べるとうまくて、よく食べるけど、これは温かいパスタ。

トマト・にんにく・パセリがカペリーニと絡んで美味しかったけど、トマトから水分が出て少し大味になっていた感じがした。


二つ目のパスタは、クリームパスタと決めていたので、
ほたてとうにのクリームソース 生パスタを注文。




これは、うまい!!
ほたてからこんなに濃厚な味が出るとは驚き。生パスタがいい感じに絡んですごくおいしかったです。
クリームソースも一滴たりとも残さず完食。

ハーフボトルを飲み干したので、2本目を注文。
何にしようか迷ったので、となりの常連さんぽい夫婦にオススメしていた、
Gavi di Gaviというワインをまたボトルで注文。

こういうとき、「いつも大丈夫ですか?」と聞かれるが、「いいんです。大丈夫なんです。」と答える。

こちらのワインは、おいしい!一気に飲み干してしまいそうなのを抑えて、ワインのあてになるようなメニューを物色。後で調べると、「Gavi中のGavi」という意味らしく、そらGaviよりもおいしいはずだな。

生ハムは残念ながら、置いてなかった。
で、前を見ると、ピザのメニューが目に留まる。

迷わず、アンチョビピザを注文。



チーズ・アンチョビ・バジルのみのシンプルな薄生地のピザ。
その分、アンチョビのうまみが引き立っていて最高でした。
右手に見えるのは、タバスコではなく、唐辛子をつけたオリーブオイル。

で、ピザを食べながら、Gavi di Gaviも一気に飲み干して大満足。

お店の人に「インターネットでブログで日本一のパスタ屋って書かれているのを見て来たんですよ。」と言ったら、著作権の了解も無しにと笑っていました。店に来ていた人も毎週のように来ているという感じの人が一杯で、よそ者がたくさん来てお店の感じがかわると嫌かなとも思ったのですが、おいしかったのでブログに書いてしまいました。すみません。。

是非また、行きたい。
国分寺の少し手前の武蔵小金井あたりは、再開発中で今なら土地も安く結構おすすめだと、hoge山さんに教えてもらったので今度チャリンコのって見に行きます。

Sunday, September 03, 2006

Rubyのexpect.rbの使い方

SSHでつないで何か操作を行ったり、パスワードの入力を自動化したり、対話形式のプログラムをプログラム中から利用する場合は、expectモジュールを使用するのが一般的です。

Rubyは標準ライブラリ中にexpect.rbが存在していますが、IO#exceptとしてIOのメソッドとして定義されていて、このままでは扱いにくいので、仮想端末を扱うPTYライブラリとあわせて使用します。


require 'pty'
require 'expect'

module Expect
def spawn(cmd)
  puts "CMD: #{cmd}" if $expect_verbose
PTY.spawn(cmd) do |r,w,pid|
@input_stream = r
@output_stream = w
@child_pid = pid
PTY.protect_signal do
yield
end
end
end

def expect(pat, timeout=10)
ret = @input_stream.expect(pat, timeout) do |match|
raise "expect %s timeout " % (pat.kind_of?(Regexp)? pat.source : pat) unless match
put_cmd(yield(match))
end
end

private
def put_cmd(cmd)
@output_stream.puts(cmd)
end
end
PTY#protect_signalでブロックを実行しないと、SSHで接続後、exitしたときなど子プロセス終了時に例外が発生します。

例えば、rootにログインしてホームディレクトリに移動して、lsコマンドを実行する。

def root_ls
include Expect
spawn("su root") do
expect(/\$$/) {|match| "su root"}
expect(/Password:/) {|match| "myrootpass"}
expect(/\$$/) {|match| "cd"}
expect(/\$$/) {|match| "ls"}
end
end

Saturday, September 02, 2006

Prototype.jsのCHANGELOGまとめと自分なり拡張

Prototype.jsが Updateされいくつかの機能が追加されました。
メソッドチェーンによるメソッドの呼び出しが完全にサポートされたのは大きな変更かと思います。

CHANGELOG

Element, Form, Form.Elementメソッドの仕様変更
Elment, Form, Form.Elementが配列ではなく、一つの要素(or ID)を引数に取るように仕様変更され、返り値として引数で渡したオブジェクトを返すようになりました。

この修正により、

$('div').each(function(val) {Element.show(val);}); // Before
$('div').each(Element.show); // After

$('input[type="text"]').each(function(val) {Field.clear(val);}); // Before
$('input[type="text"]').each(Form.Element.clear); // After
eachメソッドにElement.*関数を高階関数と渡すだけで全要素に処理を適用できます。

ただし、この仕様変更により次のようなレガシーなコードは変更が必要になります。


Element.show('page', 'sidebar', 'content');// Before
['page', 'sidebar', 'conent'].each(Element.show); // After



Field.*はForm.Methodsに統一
Fieldオブジェクトに定義されていたメソッドは、Form.Elementに統一されました。FieldはForm.Elementへの参照として定義されているため、後方互換性は保たれています。


Form中の要素を全て有効化/無効化するメソッドの追加
Form中の要素全てを有効化/無効化するためのメソッドとして、Form.enable, Form.disableが追加されました。


$('form').disable();
$('form').enable();


$(), $()で返される要素にForm.Methods, Form.Elementがmixinされるようになった
Element.extendメソッドに、FORM要素にはForm.Methodsが、INPUT, TEXTAREA, SELECTの場合には、Form.Element.Methodsがmixinされるようになった。
この変更により、$()の結果に対しての完全なメソッドチェーンでのメソッド呼び出しがサポートされた。


$('input1').clear();
$('input2').focus();



Objectのコピーを生成する、Object.cloneの追加
オブジェクトのコピーを生成し返すメソッドが追加されました。

clone: function(object) {
return Object.extend({}, object);
}


Object中に定義されているキーと値を取得するメソッド
Object.keys, Object.valuesメソッドが追加され、それぞれ引数に渡したオブジェクト中に定義されているキーとその値を取得して配列で返します。

Object.keys(Array); // => ["from","bind","bindAsEventListener"]
Object.values(Array); // => [function (iterable) {...},function () {...},function (object) {...}]



$()関数で参照した要素の子要素に対してのクラス名・CSSセレクタでの参照が可能に。
Element.Methods.getElementByClassName, Element.Methods.getElementsBySelectorが追加され、$()で参照した要素の子要素に対してクラス名・CSSセレクタでの参照が可能になりました。

$('form').getElementsByClassName('item');
$('form').getElementsBySelec('input[type="text"]'); // => テキスト入力フォームのみ取得


Array.reduceメソッドの追加
配列中の要素が一つの場合は、要素を返し、複数要素を含む場合には配列自身を返すメソッドです。
配列内の要素が1つかどうかいちいち確認するのをDRYにします。


[1, 2].reduce() // [1, 2]
[1].reduce() // 1
[].reduce() // undefined


PUT/DELETEメソッドをPOSTメソッドでエミュレートする
DHHによる変更です。Ajax.Requestに渡すオプション中で、method: put, method: deleteと指定している場合は、methods: postに置き換え、_method: put, _method: _deleteを定義して、PUT/DELETEをエミュレートします。
ActiveResourceをJavascriptからも利用しやすくするための変更だと思われます。

個人的に機能拡張しているところ。
prototype.jsは、便利で活用しているのですが、細かいところで「こんな機能あったらなぁ・・」と思って、自分で拡張して実装しています。そのいくつかを書いておきます。

Hash.mapの変形バージョン
HashクラスもEnumerableクラスをmixinしているため、Hash.mapが使えるのですが、ハッシュに高階関数を適用すると次のように適用した結果の値を取り出した配列が返されます


Hash.collect2 = function(iterator) {
var result = $H({});

this.each(function(value, index){
result[value.key] = iterator(value, index);
});
return result;
}
Hash.map2 = Hash.collect2;
function inc_value(val){return val.value+1;};

$H({fruits: 2, apple: 3, melon: 4}).map(inc_value);
//=> [3,4,5]

console.log($H({fruits: 2, apple: 3, melon: 4}).map2(function(val){return val.value+1;}).inspect());
// =>
#<hash:{'fruits':3, apple: 4, melon: 5}>


Hash.toQueryStringの拡張
prototyp.js中に定義されている、Hash.toQueryStringはハッシュをURLパラメータ形式の文字列にして返してくれます。しかし、ハッシュ中に配列が含まれたり、ハッシュがネストしている場合には、次のようになります。

$H({fruits: ["apple", "banana", "melon"]}).toQueryString();
// => "fruits=apple%2Cbanana%2Cmelon"
$H({fruits: {name: "apple", size: 3, color: "red"}}).toQueryString();
// =>
"fruits=%5Bobject%20Object%5D"


人により差異はあるかもしれませんが、これは意図した通りにサーバ側でパースされません。
そこで、Hash.toQueryStringを変更し、次のように返すようにしています。

$H({fruits: ["apple", "banana", "melon"]}).toQueryString();
// =>"fruits[0]=apple&fruits[1]=banana&fruits[2]=melon"
$H({fruits: {name: "apple", size: 3, color: "red"}}).toQueryString();

// => "fruits[name]=apple&fruits[size]=3&fruits[color]=red"


と返されるように、Hash.toQueryStringを次のように定義しています。


Hash.toQueryString = function(){
return this.map(function(pair){
if(pair.value instanceof Array) {
var key = pair.key;
return pair.value.length == 0 ? "" : $A(pair.value).map(function(val){return key+"[]="+val}).join('&');
} else if(pair.value instanceof Object) {
var key = pair.key;
return $H(pair.value).map(function(_pair){return key+"["+_pair.key+"]="+_pair.value}).join('&');
} else {
return pair.map(encodeURIComponent).join('=');
}
}).compact2().join('&');
};


Array.compact2については下記で説明します。

Array.compact2-NULLとundefinedと空文字列(==falseと評価される値)を配列から取り除く
Array.compactの変形バージョンです。


Array.prototype.compact2 = function(){
return this.select(function(value) {
return value != undefined && !(value == false);
});
};



Array.unique - 重複した要素を取り除いたユニークな配列を返す。
Array.prototype.unique = function(){
var ret = [], sorted = this.sort(), cur;
for(var i=0,l=this.length;i<l;++i) i="" return="" ret="">


String関連の拡張
全角スペースを半角スペースにとか。

String.prototype.empty = function(){
return (this == "");
};
String.prototype.full2harf = function(){
return this.gsub("\u3000", "\u0020");
};



書式付き日付出力
そういえば、Prototype.jsには日付関連の拡張は一切無い。
/**
* Date.strftime
*
* Inspired by
* http://www.mattkruse.com/javascript/date/date.js
* Author: Matt Kruse <matt@mattkruse.com>
*/

Date.MONTH_NAMES = 'January February March April May June July August September October November December Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
Date.DAY_NAMES = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sun Mon Tue Wed Thu Fri Sat'.split(' ');
Date.prototype.strftime = function(format) {
function LZ(x) {return(x<0||x>9?"":"0")+x}
format=format+"";
var result="";
var i_format=0;
var c="";
var token="";
var y=this.getYear()+"";
var M=this.getMonth()+1;
var d=this.getDate();
var E=this.getDay();
var H=this.getHours();
var m=this.getMinutes();
var s=this.getSeconds();
var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,H,KK,K,kk,k;
// Convert real date parts into formatted versions
var value=new Object();
if (y.length < y="" h="=">12){value["h"]=H-12;}
else {value["h"]=H;}
value["hh"]=LZ(value["h"]);
if (H>11){value["K"]=H-12;} else {value["K"]=H;}
value["k"]=H+1;
value["KK"]=LZ(value["K"]);
value["kk"]=LZ(value["k"]);
if (H > 11) { value["a"]="PM"; }
else { value["a"]="AM"; }
value["m"]=m;
value["mm"]=LZ(m);
value["s"]=s;
value["ss"]=LZ(s);
while (i_format < c="format.charAt(i_format);" token="" result="result" result="result">


タグベースのイテレータ

ネタ元は、Enctymedia blog.

/**
* Tag base iterator
*/
(function() {
/* Tag base iterator */
var tags = "div p span ul ol li span form input select textarea h1 h2 h3 h4 h5 h6 dl dt em strong a";
var methods = {};
$A(tags.split(' ')).each(function(tag){

methods["each"+tag.charAt(0).toUpperCase()+tag.substring(1)] = function(element, iterator) {
element = $(element);
element.cleanWhitespace();
$A((element).getElementsByTagName(tag)).each(iterator);
}
});
Object.extend(Element, methods);
})();


正規表現文字のエスケープ

RegExp.quote = function(str){
return str.replace(/([\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:\\\-])/g, '\\$1');
};

Thursday, August 31, 2006

PHPのevalは遅くない (require vs eval)

今日の昼飯は、とんかつ 武信分点行ってきました。
ロースカツ膳 竹を食べて、かにクリームコロッケ単品を一つで注文。値段は、1600円ほど。
米油であげているからか、ジューシュにかりっとあがっていて、食べた後も意外とあっさり。肉厚でなかなか美味でした。

夜は焼酎と日本酒を取り揃えているみたいなので、焼酎を飲みながら特選醤油ダレとすだちでとんかつを食べてみたい。


最近、理由あってRuby, PHP, Javascript3つを同時に書かなければいけなくて頭が混乱しそう。

PHPのスクリプトをWEBサーバ経由で返すサーバからPHPファイルを取得し、キャッシュし(ファイルを作成して)実行するプログラムを書いてると、ふと気になった。

* ファイル作ってファイルシステム経由で読み込むよりも共有メモリでやるほうが断然早いよな。
* 共有メモリ上のデータをevalするほうが遅いか?ファイルシステムから読み込む方が遅いか?

ファイルシステムとか使うよりもダイナミックにロードする方がカッコイイが速度が気になる。。

で、作った。

SPEC
* eAcceleratorが入っている場合には共有メモリつかいますよ。
* 入っていない場合には、ファイルシステム使いますよ。
* 作成途中のファイルにアクセスが来ても不整合なファイルにアクセスすることが無いように、ロックかけるか作成してからリネームしましょう。
* ソースコード中の最初にある定数宣言でスクリプトレポジトリを設定してくださいよ。

ソースコード


// ScriptLoaderに関する設定
define('SC_UPDATE_SERVER_PATH', 'http://yourserver/');
define('SC_CACHE_DIR', 'caches/');
define('SC_CACHE_EXPIRED_MINUTES', 10);

/**
* ネットワーク越しにスクリプトをロードしキャッシュする
*/
class ScriptCache
{
/* 強制的にネットワークから読み込む */
var $_forceLoad = false;

function ScriptCache()
{
$this->_enableEAccelerator = ! is_callable("eaccelerator_load");
}

/**
* ファイルを読み込む
* キャッシュが有効な場合はキャッシュを読み込む
*
* @access public
* @param $file string 読み込むファイル名
*/
function fetch($file, $force_load=false)
{
$cach_file_name = $this->_getCachedFileName($file);
if($this->_enableEAccelerator) {
$cached_contents = eaccelerator_get($file);
if(!$cached_contents || $force_load) $this->_load($file);
eval($cached_contents);
} else {
if($force_load || !file_exists($cach_file_name) || (mktime() - filemtime($cach_file_name) > SC_CACHE_EXPIRED_MINUTES * 60)) $this->_load($file);
require_once $cach_file_name;
}
}

/**
* キャッシュファイルを削除する
*
* @access public
* @param $file string ファイル名
*/
function clear($file)
{
$cach_file_name = $this->_getCachedFileName($file);
if($this->_enableEAccelerator) {
eaccelerator_rm($file);
} else {
file_exists($cach_file_name) &&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; unlink($cach_file_name);
}
}

/**
* ガベージコレクション
* 不要となった共有メモリ領域の開放
*/
function gc()
{
if($this->_enableEAccelerator) eaccelerator_gc();
}

/**
* ネットワーク経由でファイルを読み込みキャッシュする。
*
* @param $file string ファイル名
* @return boolean 失敗したらfalse
*/
function _load($file)
{
if($res = $this->_networkFetch(SC_UPDATE_SERVER_PATH. $file)) {
$tmp_filename = tempnam('','');
$cache_file_name = $this->_getCachedFileName($file);
if($this->_enableEAccelerator) {
eaccelerator_lock($file);
eaccelerator_put($file, preg_replace('/<\?(php:?)?|\?>/ims', '', $res['body']), SC_CACHE_EXPIRED_MINUTES * 60);
eaccelerator_unlock($file);
} else {
if($fp = fopen($tmp_filename, 'w')) {
if(fwrite($fp, $res['body']) === FALSE) {
return false;
}
fclose($fp);
rename($tmp_filename, $cache_file_name);
chmod($cache_file_name, 0644);
}
}
return true;
} else {
return false;
}
}

/**
* キャッシュファイル名の取得
*
* @access private
* @param $file string ファイル名
* @return string キャッシュファイル名
*/
function _getCachedFileName($file)
{
$cwd = getcwd();
return $cwd. '/'. SC_CACHE_DIR. $file;
}

function _networkFetch($url, $initheaders=array(), $timeout=10, $limit=10)
{
!is_array($url) && ($aUrl = parse_url($url));
$client =& new HTTP_Client();
$client->setDefaultHeader($initheaders);
$client->setMaxRedirects($limit);
$client->get($url, null, false);
if(PEAR::isError($res = $client->currentResponse())) {
return false;
}
return $res;
}
}
?>

で早速ベンチとってみた。

キャッシュせずに常にネットワーク上から読み込む。

ソースコード

start();
$sl->fetch('test.php', true);
$timer->stop();
ob_end_clean();

print '<pre>';
print_r($timer->getOutput());
print '</pre>';

$sl->gc();
?>

time indexex time%
Start1157032411.98688100-0.00%
Stop1157032412.023001000.036120100.00%
total-0.036120100.00%


ファイルシステムでキャッシュしてrequire
キャッシュタイプを切り替えるオプションは用意していないので、

$this->_enableEAccelerator = ! is_callable("eaccelerator_load");

のようにしてファイルキャッシュするようにする。


time indexex time%
Start1157032411.98688100-0.00%
Stop1157032412.023001000.036120100.00%
total-0.036120100.00%

で、次。



eAcceraletaor APIを使いメモリ上にキャッシュしてevalする。

time indexex time%
Start1157033202.28823800-0.00%
Stop1157033202.290016000.001778100.00%
total-0.001778100.00%

結果はファイルシステム経由で読み込んだ場合とほとんど同じ。


結論
PHPのevalは遅くない。
eAcceralerator API使ったが思ったほどパフォーマンスが出なかったのが気になる。

今日の昼飯は特選醤油トンカツ

東京-京都を交互に行き来する生活も4ヶ月目。
東京は、知り合いや友達からの情報があらかじめないと、美味しいものを食べれる店にたどりつくのが難しいと聞いていたのですが、納得。

今日の昼はとんかつ 武信分店に行ってきます。
すだちで食べる、特性醤油とんかつがうまそう。。

後、せもりなという国分寺のパスタが気になる。。
HPが無いのに数々のブログで紹介されていて、高い評価を受けている。
今週末あたり行ってみようかな。

せもりな
紹介されているブログ
http://usalog.cocolog-nifty.com/usalog/2004/09/post_28.html
http://oisiine.exblog.jp/3485243/

Tuesday, August 22, 2006

VMWareでホストOSと時刻の同期

備忘録。簡単な方法。


$ cd /usr/local/src/
# mkdir vmw
# wget http://chitchat.at.infoseek.co.jp/vmware/vmw.c http://chitchat.at.infoseek.co.jp/vmware/vmw_door.s
# gcc -O -DHAVE_CLOCK_SETTIME -o vmw vmw.c vmw_door.s -lrt
# ./vmw -t
# ln -s /usr/local/src/vmw/vmw /usr/bin/vmw
# vim /etc/crontab
0-59 * * * * root vmw -t 2>&1 /dev/null
を追加

Saturday, August 19, 2006

CSS3について覚え書き

CSS3についてメモ。

Border 関連のプロパティ

15. The 'border-color' properties
グラデーションの掛かったボーダーを実現する。

対応ブラウザ: Mozilla
div.box {
height: 15px;
padding: 10px;
width: 200px;
text-align: center;
}
div.border-color {
border: 10px solid #000;
-moz-border-bottom-colors: #000000 #003F09 #005F0D #008F13 #00CF1C #00EF20 #3FFF59 #6FFF82 #AFFFBA #DFFFE3;
-moz-border-top-colors: #000000 #003F09 #005F0D #008F13 #00CF1C #00EF20 #3FFF59 #6FFF82 #AFFFBA #DFFFE3;
-moz-border-left-colors: #000000 #003F09 #005F0D #008F13 #00CF1C #00EF20 #3FFF59 #6FFF82 #AFFFBA #DFFFE3;
-moz-border-right-colors: #000000 #003F09 #005F0D #008F13 #00CF1C #00EF20 #3FFF59 #6FFF82 #AFFFBA #DFFFE3;
}


-moz-border-<position>-colors:
プロパティでボーダーの色を複数指定できる。順に色合いを変えることでグラーデーション調の線を引くことが可能。


<div class="box border-color">
UIEvolution Japan.
</div>

サンプル

19. The 'border-radius' properties

CSSで角を丸くする

対応ブラウザ: Mozilla, Webkit 最新版

div.border-radius {
border:1px solid #000;
/* Mozilla */
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
-moz-border-radius-bottomleft: 10px;
-moz-border-radius-bottomright: 10px;

/* Safari */
-webkit-border-left-radius: 10px;
-webkit-border-right-radius: 10px;
-webkit-border-top-radius: 10px;
-webkit-border-bottom-radius: 10px;
}


<div class="box border-radius">
UIEvolution Japan.
</div>

サンプル

3.4.1 The 'border-image' properties
通常のボーダーの変わりに画像を使用することが出来る。
コーナーと、コーナーとコーナーを結ぶ線の画像をそれぞれ指定することが出来る。

border-image: uri;
  • border-top-image
  • border-right-image
  • border-left-image
  • border-bottom-image
border-corner-image: uri;
  • border-top-left-image
  • border-top-right-image
  • border-bottom-left-image
  • border-bottom-bottom-image
ボーダー・コーナーともに個別に指定することが出来る。

最新版のWebkitでは、
-webkit-border-image:
でサポートしている様子。

3.26. Media Queries
Media Queryを使用すればより細かくデバイスに適用するスタイルを選択することが出来る。
media属性の指定とスタイルに対する指定でスタイルを適用するかどうか判定する。

対応ブラウザ: Operaと最新版のWebkit


/* Media query */
@media all and (min-width:650px){
div.box2#min-width {
background-color: #0300FF;
}
}
@media all and (max-width:1000px){
div.box2#max-width {
background-color: #0300FF;
}
}



<div class="box2 media-query" id="min-width">
650pxより大きいと青色
</div>
<div class="box2 media-query" id="max-width">
1000pxより小さいと青色
</div>


OperaとWebkitでウインドサイズを色々変えて表示してみてください。
サンプル

このMedia Queryを使用すれば、PCと携帯両方で動作するHPを簡単に作ることが出来そう。
スタイル属性にはディスプレイのサイズ・ディスプレイの高さ・ディスプレイの色の濃さを指定できる。
To describe in more detail what type of devices a style sheet applies to, this document proposes media queries. A media query consists of a media type and one or more expressions to limit the scope of a certain style sheet. Among the proposed media features that can be used in expressions are "width", "height" (size of the display), and "color" (color depth of the display).
とWorking Draft中に書かれている。

CSS3 module: Multi-column layout
対応ブラウザ: Mozillaのみ

3カラムレイアウトや2カラムレイアウトを簡単に作ることが出来ます。

div.multi-column{
-moz-column-with: 100px;
-moz-column-gap: 1em;
-moz-column-count: 3;
}


<div class="multi-column">In preparation for the release of Mozilla
Firefox 1.5 Beta 1, the tree will be locked down tonight at 11:59pm Pacific
Daylight Time (UTC -0700). After this point, no more checkins will be accepted
for Firefox 1.5 Beta 1, which is set for release on Thursday.

In preparation for the release of Mozilla
Firefox 1.5 Beta 1, the tree will be locked down tonight at 11:59pm Pacific
Daylight Time (UTC -0700). After this point, no more checkins will be accepted
for Firefox 1.5 Beta 1, which is set for release on Thursday.
</div>

一つのカラムの幅を100px、カラム間の空きを1em, カラム数を3つに指定します。

サンプル


7. Box Model addition
長い間、多くのWeb開発者やデザイナが苦しめられてきたボックスモデルの違い。
CSS2.1勧告のボックスモデルでは内容領域の高さと幅をあらわしますが、IEなどの一部のブラウザはボーダーやパディングも含めた領域を高さと幅とします。
CSS3で勧告されている、box-sizingプロパティは、どちらのボックスモデルを適用するかを指定することが出来ます。

box-sizing: content-box | border-box
content-box
要素の高さ(height)と幅(width)はCSS2.1勧告の視覚整形モデルに従う。指定された高さと幅は内容領域の高さと幅に適用される。パディングと高さは内容領域の外側に描かれる。

border-box
指定した幅と高さは、パディングとボーダーを含めた値になる。

9.1. 'resize' property
対応ブラウザ: 最新版のWebkitのみ

CSSのみで要素のリサイズを可能にします。
resize: both | horizontal | vertical;
bothを指定すると両方向にリサイズ可能。
horizontalを指定すると水平方向にリサイズ可能。
verticalを指定すると垂直方向にリサイズ可能。

サンプル

9.8. Text shadows: the 'text-shadow' property
対応ブラウザ: 最新版のWebkitのみ

Photoshopで作成できるような影付き文字の簡易版をCSSだけで作成することが出来ます。

text-shadow: 2px 2px 2px #ff0000;


CSS2から存在するが現在実装されているブラウザはWebkitだけなようです。
上記の設定では、右側に影を2px, 下側に影を2px、ぼかしを2pxで色は赤色。

サンプル


6.3.2. Substring matching attribute selectors
部分文字列でマッチ可能なセレクタが追加される。

[att^=val]
attがvalから始まる要素
[att$=val]
attがvalで終わる要素
[att*=val]
attが少なくとも一回はvalを含む要素

拡張子が.htmlのアンカ要素のみ取得するセレクタ
a[href$=".html"]


疲れたので、とりあえずこのへんで。