SeezooをIE6で動かしてみた

OSS界隈ではあんまり需要がなさそうで、かつ時代に逆行するようなカタチですが、とりあえず動かしてみました。

そもそもの発端

巷では「IE6のシェアがウン%を切った」と良く聞くのですが、実際私が関わった案件では90%位の確率で

「え〜IE6で動かないの〜?」

と言われます。これって私の周りだけなのでしょうか。
ともあれ、「対応してません」と言い続けるのも悔しいので、やってみました。
JS,CSS界では強敵のIE6との戦い。その戦果を報告します。*1


まずは結果から

とりあえず、結果から。


もう使われないであろう手法(ロストテクノロジー)を使って結構頑張って動かしてます。

IE6対応にあたって

私がWebを始めた頃はIE6が最新のブラウザでした。
なので、IE6に対するノウハウはそれなりにあると思っていたのですが…甘かった。
JavaScript(JScript)の方は特に問題なく。でも問題はCSS

CSSでの既知の問題として、以下のようなことが一般的ですね。

  1. position:fixedに対応していない。
  2. PNGに対応していない。
  3. width[height]:100%のレンダリングがおかしい。
  4. float+margin横方向は2倍計算。
  5. float合計が100%になるとカラム落ち。
  6. コンテンツが増えると、それに合わせてカラムが拡大する。

既知の問題であるが故に、対応策もちょっとググれば出てくるわけですが、
これらの問題はSeezooにとってはどれもクリティカルで、一筋縄ではいかず。
さらに、

  • iframeの挙動がおかしい

という問題にさんざん悩まされました。Concrete5がIE6に対応していないのは正しい判断だと思います。

まぁでもせっかくなのでSeezooはやろうかな。と、覚書も含めて対応したものを書いてみようかと思います。
今更感が漂いますが、再確認、再勉強の意味も込めて。

position:fixedの対応

この対策はもう出尽くしている感がありますね。

  • behaviorを使う。
  • expressionを使う。
  • onscrollイベント毎にFIX。

こんな感じですかね。(他あれば教えてください)で、色々考えましたが、

  • behaviorを使う
    • 高度なJSライブラリはbehaviorを使うケースがあるので、できれば開けておきたいから☓。
  • expressionを使う
    • 処理が重すぎる。要素が1個くらいならいいけど、今回は複数個あるのでブラウザが落ちるかも。なので☓。
  • onscrollイベント毎にFIX。
    • こちらも処理が重くなりがち。あと動きがカクカクするので☓。


どれもダメかなぁと思ったのですが、

よく考えれば、このボックスたちは編集時にしか現れないよね?

ということに気付き、cover-body法(勝手に命名)で再現することにしました。簡単に言うと、

JScriptでDOMツリーを改変して、強制的にposition:absolute = position:fixedな状態を作る方法

です。とってもイレギュラーな方法なので、あとが大変ですが、まぁそこは多めに見てもらいたいところです。
実際の表示では問題ないですし、そのうち消えていくブラウザだと思うので。これで解決。


PNGの表示対応

filter:progid:DXImageTransform.Microsoft.AlphaImageLoaderで対応。
あと、処理を振り分けてアイコンとかはGIFにコンバートしたものを表示させるように。


width[height]:100%のレンダリング対応

面倒ですが、ブロック移動時に横幅を再計算させてます。
あと初期化の時点でレイヤーの高さとかも再計算。


float+margin横方向は2倍計算。

これはテンプレートを書く方に任せました。


float合計が100%になるとカラム落ち。

エリア分割ブロックなどでこの問題が発生。
見た目は美しくないけど、IE6はMAX99%で計算するように調整。


コンテンツが増えると、突き抜けず、カラムが拡大する。

編集モードの時は赤い点線を付けるのですが、このボーダー分だけカラムの横幅が拡大されて、
floatが落ちるという何とも厄介な問題が発生しました。
結局、これも初期化の時点でカラム幅を再計算+ボーダー縦横分をマイナスするようにして回避しました。


iframeの挙動制御

今回、これで大きくつまづきました。
一般的なブラウザは[iframe].onload = function(){};が出来るのですが、IE(6だけかな?)はcreateElementで作ってsrcをセットすると、
onloadが完了する前までは

typeof [iframe] === 'unknown'

というおかしなことになり、しかもそのままappendChildするとsrc先がおかしなことになりました…。
しかもonloadはダメみたいです。

色々試した結果、[iframe].onreadystatechange = function(){};が使えることが判明し、
内部で[iframe].readyState === 'complete'で判定することで事なきを得ました。*2

結果(反省)

まだ一部レイアウトが完全じゃない部分があります(全く使えない、というほどではないですが)。
あと、画像編集ツールはIE6ではサポートしません(限界)
機能を落として完全互換か、古いブラウザは切って新しい機能をふんだんに使うかが悩ましいところですね。

もっとJSとCSSが上手く書けるようにならなければ、と改めて思いました。
JSのコードが汚くなりすぎたので、次バージョンに入れるかは考え中です。

さっそくの追記

これでOK…かと思いきや、他にも問題がありました。
後日別で書きます。

*1:どちらかというとJSは素直ですね。CSSの方が強敵でした。

*2:完了後にイベントを解除することも忘れずに。

CodeIgniterにオレオレmatchboxを実装してみる、の巻。

Concrete5のカスタマイズをしていて、クラスのオーバーライドやパッケージ管理が本当に良くできてるなぁ、と感心したところで、
せっかくなのでSeezooにも同じような仕組みが作れないかな?と試行錯誤しました。

そこでMatchbox

CodeIgniterにはMatchboxという便利なものがあります。

これは各機能をモジュールとして開発ができるようになり、とても便利です。

これ使ってもいいなぁ、と思っていましたが、


Loaderクラスが既に拡張されている
アドレス/modules/モジュール名/ とかURIを占有するかもしれない


以上から、何となく相性が悪いかもしれないなぁということで、
勉強も含めて自分で作ってみることにしました。

まずパッケージディレクトリを探索し、無ければコアを見るという挙動

CodeIgniterでモデルやヘルパーをロードするには、


// モデルのロード
$this->load->model('モデル名');

// ヘルパのロード
$this->load->helper('ヘルパ名')

と書きますが、結局独自にこれらの挙動を制御するとなると、Loaderクラスの拡張が必須なわけです。

Seezooは既にSZ_Loaderと拡張済みなので、これらのメソッドもオーバーライドして拡張してみます。


// 拡張後のmodel()メソッド。CI_Loaderをオーバーライドしてます
function model($model, $name = '', $db_conn = FALSE)
{
if (is_array($model))
{
foreach($model as $babe)
{
$this->model($babe);
}
return;
}

if ($model == '')
{
return;
}

// 以下の処理でファイル名とパスが正規化されるので、一時変数に退避
$orig_model = $model;
$orig_name = $name;

// Is the model in a sub-folder? If so, parse out the filename and path.
if (strpos($model, '/') === FALSE)
{
$path = '';
}
else
{
$x = explode('/', $model);
$model = end($x);
unset($x[count($x)-1]);
$path = implode('/', $x).'/';
}

if ($name == '')
{
$name = $model;
$orig_name = $name;
}

if (in_array($name, $this->_ci_models, TRUE))
{
return;
}

$CI =& get_instance();
if (isset($CI->$name))
{
show_error('The model name you are loading is the name of a resource that is already being used: '.$name);
}

$model = strtolower($model);

// 追加部分:拡張先モデルファイルが無ければ、親クラスのmodel()メソッドに移る
if ( ! file_exists(SZ_EXT_PATH.'models/'.$path.$model.EXT))
{
parent::model($orig_model, $orig_name, $db_conn);
return;
}

if ($db_conn !== FALSE AND ! class_exists('CI_DB'))
{
if ($db_conn === TRUE)
$db_conn = '';

$CI->load->database($db_conn, FALSE, TRUE);
}

if ( ! class_exists('Model'))
{
load_class('Model', FALSE);
}

require_once(SZ_EXT_PATH.'models/'.$path.$model.EXT);

$model = ucfirst($model);

$CI->$name = new $model();
$CI->$name->_assign_libraries();

$this->_ci_models[] = $name;
}

追加部分で、もしも拡張先ファイルが無ければ、通常のCI_Lorder::model()に移行する、という単なるフックですね。
SZ_EXT_PATHは別でdefineした拡張ディレクトリ配置先のパスが入ります。

ヘルパも同じ手法で対応できました。*1

モデルやヘルパとかはこれでいいのですが・・・

Controllerはそれだけじゃない

コントローラは同じ方法では上手くいきません。当たり前なのですが。
コントローラはCIのルーティングとも密接に関連しているので、Routerも拡張しないといけません。

Seezooはこれまた既にRouterクラスを拡張していて、Controllerを3階層まで辿ってルーティングするようにしています。

ここまできて諦めるのも悔しいので、頑張って再拡張。以下、SZ_Routerのコードです。


// 拡張後の_validate_requestメソッド。CI_Router::_validate_requestをoverride
function _validate_request($segments)
{
// 拡張ロード先のコントローラがあるかどうかをフックする
$customed_segments = $this->_custom_validate_request($segments);
if (is_array($customed_segments))
{
            // もしフックルーティングでコントローラがセットされていれば、ここで終了
return $customed_segments;
}

        // 以下、今までのコード

_validate_request()で使用するコントローラをとメソッドを決めるのですが、その処理の前に_custom_validate_request()というメソッドを呼んでフックします。

で、フック先で拡張ディレクトリからコントローラを検索します。


    // フックして呼び出すメソッド。拡張先からコントローラのロードを試みる
function _custom_validate_request($segments)
{
if ( file_exists(SZ_EXT_PATH . 'controllers/' . $segments[0].EXT))
{
$this->set_directory_ext();
return $segments;
}

if ( is_dir(SZ_EXT_PATH . 'controllers/' . $segments[0]))
{
$this->set_directory_ext($segments[0]);
$segments = array_slice($segments, 1);

if (count($segments) > 0)
{
if (is_dir(SZ_EXT_PATH . 'controllers/' . $this->fetch_directory() . $segments[0]))
{
// Set temp directory_name
$tmp_dir = $this->fetch_directory() . $segments[0];
$deep_segments = array_slice($segments, 1);

if (count($deep_segments) > 0)
{
if ( file_exists(SZ_EXT_PATH . 'controllers/' . $tmp_dir . '/' . $deep_segments[0] . EXT))
{
$this->set_directory_ext($tmp_dir);
return $deep_segments;
}
else
{
$this->directory = '';
return FALSE;
}
}
else
{
// Does the requested controller exist in the sub-folder?
if ( ! file_exists(SZ_EXT_PATH.'controllers/'.$this->fetch_directory().$segments[0].EXT))
{
$this->directory = '';
return FALSE;
}
}
}
}
}
$this->directory = '';
return FALSE;
}

// コントローラを拡張先ディレクトリにセットする
function set_directory_ext($dir = FALSE)
{
$this->directory = '../' . SZ_EXT_DIR . 'controllers/';

if ($dir !== FALSE)
{
$this->directory .= $dir . '/';
}
}


やっていることは_validate_request()と同じですが、大事なのは戻り値です。

拡張コントローラが見つかれば、返却値はそのパスを取った配列、見つからなければFALSEを返します。
これにより、フックメソッドの結果が配列なら、そのままreturnしてルーティング完了。
FALSEなら、CIのデフォルトルーティングを再開する方式です。

あと、set_directory_ext()というメソッドも追加してますが、これにも理由があるのです。

CodeIgniterが最終的にコントローラをロードするのはコアファイルで、


system/codeigniter/CodeIgniter.php:line150くらい

// Load the local application controller
// Note: The Router class automatically validates the controller path. If this include fails it
// means that the default controller in the Routes.php file is not resolving to something valid.
if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().EXT))
{
show_error('Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.');
}

としています。$RTRはRouterクラスのインスタンスで、それぞれfetch_directory()とfetch_class()でコントローラを決めています。
つまり、


この時点までに、Routerクラスに読み込めるコントローラのパスがセットされていればいい

わけです。そこで、set_directory_ext()でしかるべきパスをセットしておけば、コアをハックせずに拡張コントローラを読める設計です。

あとは、SZ_EXT_PATHを上手くdefineしてやれば、拡張パッケージだけ外に置くこともできます。

多分Matchboxも同じような事をしてるんだと思う。ソース読んでませんが。

と、いうわけで

プラグインはこれらの拡張ディレクトリに設置することで、CI、さらにはSeezooのコアにも影響を与えず、独自の開発ができるようになるはずです。
開発者も楽ですし、コアのバージョンアップも楽で、一石二鳥ですね。

まだベンチマークを取って無いのですが、体感速度はさほど変わらないような気がします。
メモリ使用量も問題なさそうです。


もう少し動作検証して、いけそうなら次のバージョンに含めます。

総括とか

ちょっとOverRideから外れてしまったけど、コアとプラグインの切り分けができたのでよしとしましょうか。


多分普通に実装するならこうなるだろう、という想像でやりました。
もっと良い方法がありそうな気もするけど、これくらいが自分の限界ですかね。


今まで色々拡張してきましたが、CodeIgniterはどんな要望にも応える仕組みが提供されていて、やっぱりすごいですね。
しかもコアをまったくハックしない。うん、素晴らしい。

*1:ビューはもうちょっと特殊なことをしてます。

betaを取るべくブランチを切りました

リリース後、他の案件の都合でさっそく頓挫してましたが、ようやくbetaを取るべく動き出しました。

次のリリースで実装予定の機能を簡単にリストアップします。

大体実装予定の機能

  • PHP5.1.x系の対応
    • CentOS5.1だとPHPのバージョンがが5.1.6だったと記憶しているので、今回サポートバージョンを拡大します。Jsphonを教えてくださった方がいらっしゃったのですが、ライセンスの関係でPEAR::Services_JSONになりそうです。すみません。
  • バックエンドプロセスの実装
    • cronで色々実行できる機能を追加します。Concrete5のJobみたいなものですね。
    • sitemap.xmlとか自動生成できるようになる予定です。
  • 新着情報管理機能(プラグイン
    • おそらく部分CMSでやる場合の実装率No.1の機能かな?と思って実装してます。
    • 管理画面で登録・アーカイブ化の選択ができる予定です。
    • フロント側ではブロック化、静的ページへの埋め込み、一覧表示とアーカイブ表示システムといった感じです。
  • パッケージ化・プラグイン化しやすいような構成へ
    • CodeIgniterのコントローラをSeezooへインストールする際に、必要なDBを定義ファイルに基づいてCREATEする機構をつけます。
    • コントローラ単位でプラグインが作れるようになると思います。
  • デフォルトテンプレートの変更
    • 「味気ない」という意見があったので変更しようかな、と思います。デザインセンス無いんですけどね。
    • 「テンプレート作るよ」という方がもしもいらっしゃったら是非お願いします。

あとはドキュメントの整備とかも着手しようと思います。もっと先にやれよと言われそうですが。

おまけ

twitterなどで「あれができないなぁ」等指摘を頂いた部分をちょこっと変更しました。

  • sz_includeをSZ_Loaderに実装。
    • blogコントローラからのテンプレート呼び出しでも使えるようにしました。
  • Google Analyticsの埋め込みタグはタグ内じゃないとダメらしい。
    • 仕様が変わったようですね。直前に配置していたものを移動させました。
  • タグに属性をつけるとメニューが出ないよ。
    • replace位置をに変更したのでOKです。
  • Routerレベルでのエラーメッセージが意味不明。
    • 404エラーのような感じに整形しました。翻訳もちょこっと追加。
  • 404ページで「あのコマンド」を実行すると・・・?
    • 次バージョンでは削除します。運用では必要ないので。というかソース見りゃわかりますね。
  • twitterガジェットの設定変更時、設定画面が消えてほしい
    • 消しましょう。


全部できるかは分かりませんが、暫定ということで、できるだけ頑張って実装したいと思います。

PEAR::Services_JSONとJsphonの検証してみた

PHP5.1.x系対応でjson_encode()を何とかしたいので、JSONエンコード系のライブラリの候補として、

PEAR::Services_JSON

Jsphon

を候補としているのですが、一体どっちが使い勝手がいいんだろうか?ということで、検証してみました。

テストケース

簡単な配列をそれぞれのライブラリでエンコードするコードです。

encode($data));
$bench = (time() - $st) + (microtime() - $stm);
var_dump($bench);


// Jsphonの場合
require_once('Jsphon-1.0.1/Jsphon.php');
var_dump(Jsphon::encode($data));
$bench = (time() - $st) + (microtime() - $stm);
var_dump($bench);

// メモリ使用量計測
echo memory_get_usage();

両方のベンチとメモリ使用量を簡単に計測してみました。
環境:Lampp (Ubuntu) PHP 5.3.1

比較と考察

結果はこんな感じでした。

PEAR::Services_JSONJsphon-1.0.2
エンコード結果"[1,2,3,4,5,6,7,8,9,10]""[1,2,3,4,5,6,7,8,9,10]"
実行時間(ms)0.0029170.012867
メモリ使用量(byte)436904657276
備考特に無しStrictエラー発生
Jsphonの方が少しだけ遅いですね。json_encodeは頻繁に実行するわけではないのでそれほど問題ないレベルかもしれません。 が、予想通りメモリ使用量に差が出ました。Services_JSONが約426KBに大してJsphonは約641KB。 マルチバイトが混じるともう少し差がでるかもしれません。 Jsphonは内部でエンコーダ・デコーダ・エラーハンドルファイルをrequireしているので、その分メモリを使うみたいですね。 あと、マルチバイト関連の処理が内部で走っているからかもしれないです。 さらに、JsphonはPHP5.3.1では Strict Errorが出ました。う〜ん、これはちょっと厳しいです。*1 $json = new Jsphon()としても、内部で「Jsphon_Error::singleton()」とやっているので、Static呼び出しは少しハックしないと回避できなさそうです。 1ファイルで全てまかなえるPEAR::Serveices_JSONか、ちょっと遅いけどマルチバイトに強いJsphonか。 個人的には日本製なのでJsphon押しなのですが、速度面とハックの手間を考えると さらに悩ましい問題になってしまいました><

*1:error_reportingを変更すれば回避できますが、CodeIgniterの設定を変更する必要があるんですよね。できればネイティブで動かしたいところです。

1.0.0-beta3 のリリース

1.0.0-beta3のリリースとなります。

修正内容は以下の通りです。

以下のURLから、リリースアーカイブまたはパッチのダウンロードができます。

seezoo.org ダウンロード

MAMPなどの80番ポート以外のインストールもサポートしました

MAMPで8888番ポートを使っている環境にインストールしたいけど、対策方法は?」という要望があり、急遽インストーラを更新しました。
正式リリース版から有効になっています。

変更内容

インストーラが起動する際、80番ポートでなければ


http://localhost:8888/install_directroy

のようにポート番号をつけてインストールする事ができます。
XAMPPやLAMPPのようなデフォルト80番ポートの場合はそのままでOKです。

deployするときは?

テスト環境なら大丈夫ですが、本番にアップする際には問題があるので、設定ファイルを書き換えて設置します。
具体的には、



# root/system/application/config/config.phpの最初の方

$config['base_url'] = 'http://localhost:8888/your/local/settings/';

を、


$config['base_url'] = 'http://ドメイン名/'; // 最後の「/」を忘れないでください


と書き換えれば、システム全体のパスが書き換わります。*1

さらに言えば、データベースの設定も、


# root/system/application/config/database.phpの下の方

$db['default']['hostname'] = 'なんとかかんとか'
.
.
.

と書かれているので、こちらも本番の環境に合わせて書き換えてあげればそのまま動作します。
普段WindowsLinuxで開発しているのでこのあたりの対応を忘れていました。申し訳ありません><

*1:mod_rewriteを動かしていると上手くいかないかもしれません。

PHP5.1.x系でseezooを動かしたい場合は?

おそらくサーバー要件できついかもしれないのがjson_encode関数のサポートだと思います。

SimpleXML関数系はPHP5組み込みですが、json_encode関数のサポートはPHP5.2.0以上となっています。

よって、


PHP5.0 < 導入サーバーのPHPバージョン < PHP5.2

の場合、json関連の処理がまったくできない事になってしまいます。

その場合、例外的ではありますが、PEAR::Services_JSONを入れたら何とかなります。

Servieces_JSONのDLとインストール


PEAR::Serveice_JSONからダウンロードできます。

PEARのパッケージマネージャがあれば、コマンドからインストールできます。


# pear install Services_JSON-1.0.2

幸いにしてPEARの依存関係がないので単体で使用できます。

ダウンロードした場合は、解凍してできるJSON.phpをsystem/application/libraries/以下に設置します。

設置方法


この関数はグローバルで使いたいので、CodeIgniterのヘルパーに関数を追加します。
自作ヘルパーを作ってロードしても良いですし、
分からない方はsystem/application/helpers/seezoo_helper.phpに以下のコードを追加すればOKです。


// 念のために関数が存在するかチェック
if ( ! function_exists('json_encode') )
{
 // JSON.phpをincludeしておく
 require_once(APPPATH . 'libraries/JSON.php');

function json_encode($data)
{
// Services_JSONインスタンス生成
$json = new Services_JSON();
// encodeして値を返却
return $json->encode($data);
}
}

perr installコマンドでインストールした場合はinclude_pathはセットされているので、上記requireは


require_once('JSON.php');

でOKとなります。
これでjson_encode関数が擬似的にサポートされます。*1

もしもPHP5.1.xとかでseezooを動かしたい!ということがあればお試しくださいませ。

あ、あとインストーラではPHP5.2以上のチェックを行っているので、そこをスキップしないといけないですね。
後日パッチ配布予定です。


要望が多ければ、次回バージョンアップではService_JSONをデフォルトで組み込んだPHP5.1.x系対応版とする予定です。*2

*1:※Services_JSONと組み込みのjson_encode関数は結果が若干違うようですが、JavaScript側でJSONを扱う際にはハッシュのキーの順は特に関係しないので、問題ないと思います

*2:もしjson_encodeをサポートさせる他のライブラリがあれば教えてください><