Categories
open all | close allTags
Migration | Subversion | CSRF | ドキュメント | デュアル・コア | モデル | JustPosted | rake | フォーム | RESTful | Flash | 認証 | 名称 | アクセス制御 | タグ | 国際化 | スキンエンジン | Aptana | テスト | パソコンSearch
«Prev || 1 || Next»
スキン周りに関する仕様のまとめ
Nucleusのスキン周りについてTwitterでいろんな話が出ていたので,Foodyn CMSにおける仕様と実装についてまとめておきます。Foodynはスキン(他のCMSだとテンプレートといったほうがいいのかな?)の処理系(エンジン)を複数載せられますが,ここでは標準組み込みであるNucleus互換のスキン・エンジンについてだけまとめます。
まず,現状のNucleusのスキンには分かりやすさなどにおいていくつか問題があります。
①DBに保存されているスキンと,ファイル・ベースでインクルードする部分があり,編集画面もそれぞれ別になる
②さまざまなスキンタイプが分かりにくい
③スキンとテンプレートの関係が分かりにくい。テンプレートの中にはアイテムの表示に関係するもの,カテゴリーの表示に関係するもの,さらには日付表示に関係するものや,画像表示に関係するものなどが混在しており,どこで何が使われているのか理解するのが難しい
④特別なページを作る機能が少ない
Foodynではこれらについて次のような改善策を取ります。
①Nucleusのスペシャルスキンパーツをインクルードする機能を標準で持ちます(Nucleusでファイルをインクルードするのに使うparsedincludeを拡張)。基本的にDBベースの編集だけで済むようになります。
②「レイアウト」スキンを使うと,各スキンに固有な部分(例えばアイテムページにおける<%item%>スキン変数)はデフォルトのものが利用でき,何も作らなくて構わない(もちろんデフォルトで不満がある場合は作っても構わない)
③テンプレートにデフォルト機能があるので,意識してテンプレートを使わなくてもよくなります。例えば<%categorylist%>だけで動くようになります。また,インライン・テンプレート機能を使うことで,スキンの中にテンプレートを埋め込めます。
また,スキンに対してメインのテンプレートとアイテムスキン用のテンプレートという二つのテンプレートを属性として持つようにします(未実装)。標準ではスキン名に「/index」「/item」をつけたものがテンプレート名になります。これらはスキンのデフォルトテンプレートとして働きます(ない場合はシステムのデフォルトが使われます)。
これらのスキンに付属するテンプレートはスキン編集画面から直接パート(例えばカテゴリ表示関係)ごとに呼び出して編集できるようにします(未実装)。
④ブログに付属しないアイテムを作ることができ,このアイテム・ページに自由に名前を付けることによって,さまざまなページが作れます。
トップ・ページの機能があり,トップページだけ専用のスキンパートを利用できます。
クロージャを使うメソッドをかっこよくする
ツリー型カテゴリーの表示など,ロジックが必要な表示ルーチンで,ロジックと表示そのものの部分を分けるためにクロージャを使う話は以前に書きました。そのときのロジック側(Blogモデル)はこうなってました。
def category_tree
tempbuffer = ""
level = 0
self.categories.inject(nil) do |last_right, cat|
# cat is under the last leaf
if !last_right || (last_right > cat.cleft)
level += 1
tempbuffer += yield(:level_up, level)
#
elsif (last_right + 2 <= cat.cleft)
tempbuffer += yield(:item_close, level)
(last_right+2..cat.cleft).each do |i|
level -= 1
tempbuffer += yield(:level_down, level)
end
else
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:item, level, cat)
last_right = cat.cright
end
if (level > 1)
tempbuffer += yield(:level_down, level)
end
(level..1).each do |i|
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:level_down, level)
return tempbuffer
end
一方,呼び出し側(パーサ内のparse_categorylist)は
def parse_categorylist params
blog = params[2] ? Blog.find_by_bname(params[2]) : @controller.blog
template = _template(params[1])
bd = blog.basic_data
data = Hash.new
blog.category_tree do |type, level, cat|
data['level'] = level.to_s
case type
when :level_up
fill(template['CATLIST_HEADER'], bd.merge(data))
when :level_down
fill(template['CATLIST_FOOTER'], bd.merge(data))
when :item
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
when :item_close
fill(template['CATLIST_LISTITEM_END'], bd.merge(data))
end
end
end
これで実用上は不自由ないのですが,どうも呼び出し側があまりかっこよくない。case文で場合分けというのが「ださい」感じがします。yieldで呼び出されるたびにこのcase文を通るのが効率的にももう一つ。ここの部分を
blog.category_tree do |cat|
cat.level_up { |level|
data['level'] = level.to_s
fill(template['CATLIST_HEADER'], bd.merge(data))
}
cat.level_down { fill(template['CATLIST_FOOTER'], bd.merge(data)) }
cat.item { |cat|
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
}
cat.item_close { fill(template['CATLIST_LISTITEM_END'], bd.merge(data)) }
end
と書けたら,かなり「Railsっぽい」感じになります。で,やってみました。
» Read more
インライン・テンプレート
インライン・テンプレートの機能,動くようになったのですが,今の「ネストなしパーサー」だとインラインでテンプレート書くときに<%%>が使えないので<::>に書きなおす必要があります。やはりこれだと面倒なので,ネスト付きパーサーに切り替えることを画策中です。
ただ,単純に切り替えただけではさすがにエラーが…。ちゃんとテストしていないメソッドなので当然ですが。しばらくバグ取りに時間がかかりそうです。
それと,このネスト付きパーサーを考えたときは,ネストの内側も全部解釈実行することを前提にしていたのですが,考えてみたら内側は別のパーサー(例えばスキン変数の内側はテンプレートのパーサーが扱うべき)で実行するのが普通なので,そのあたりの処理も変えないといけません。例えばifの解釈もネストできるのですが,そこはオーバースペックでしょう。
インライン・テンプレートとヘッダー変数を実装
仕様として書いたからには実装しないと,ということでNucleus互換表示エンジンにヘッダー・スキン変数の機能とインライン・テンプレートの機能を実装しました。ヘッダーの方はごくごく簡単で,コントローラにヘッダを保持するハッシュを用意しておき,プラグインから読み書きできるようにして,スキン変数で書き出すだけです。文字列でなくハッシュにしたのは,同じものが既にないかどうか調べられるようにするため。インライン・テンプレートはスキン変数のパラメータに{}で挟まれた部分があると,そこをJSON(実際にはYAMLのライブラリを使っていますが)として解釈するもの。まだちゃんとデバッグはしていませんが,たぶんそんなに問題はないでしょう。
Nucleusの開発合宿に参加しました
Nucleusの開発合宿に行ってきました(詳しくはこちら)。とはいえ二泊三日のうち中日の日帰りという一部の参加でした。Foodyn CMSについてはこのブログにいろいろ書いてはいますが,見ていない人も多いだろうし,情報がまとまっていないので,この機会にどういうものだか理解してもらおうと,時間を取ってもらってプレゼンさせてもらいました。前の記事のプレゼンはそのための資料です。
午前中に1回と,見逃した人用に夕食後に1回,計1時間近く使わせていただきました。ありがとうございました。これが何かのきっかけになればと思います(Sourceforge.netのDevelopersは一人増えました。^^)。
また,ページスイッチの部分を実装しました。一つのページの中に複数の切り替え用スイッチ(例えば一つはメインブログ,もう一つはサブブログの切り替え用)を置けます。実装は案外苦労しましたが,割と面白い機能になったかと思います。
トップ・ページとレイアウト機能に対応しました
サイト・トップにアクセスしたときに「main」スキンがあったら,そちらを表示する機能と,レイアウト機能,デフォルト・スキン・パートの機能を実装しました。これらを使うと最低限のスキンを作ることがこれまでよりかなり簡単になるだろうと思います。
インライン・テンプレートの機能を加えれば,スキン編集一つだけでも済ませられるようになります。
そうか,クロージャって役に立つんだ
どうもいまいちクロージャの有用性が分かっていなかったのですが,階層カテゴリー部分の表示を作っていて,やっと役に立つことが分かったような気がしました。具体的に言うと,<%categorylist%>で表示する部分というのは既に作ってあったのですが,今回,アイテム投稿画面を作るときに,またカテゴリーのツリー表示が必要なことに気付いたわけです。最初はコピペして,同じようなことをやらせようとしたのですが,どうも効率が悪いし,アルゴリズムにバグがあったり,データ構造を変えたら両方直す必要があります。
そこで,Blogモデルの中に表示ルーチンを移し,そこからブロックを呼び出すような形に変えてみました。
def category_tree
tempbuffer = ""
level = 0
self.categories.inject(nil) do |last_right, cat|
# cat is under the last leaf
if !last_right || (last_right > cat.cleft)
level += 1
tempbuffer += yield(:level_up, level)
#
elsif (last_right + 2 <= cat.cleft)
tempbuffer += yield(:item_close, level)
(last_right+2..cat.cleft).each do |i|
level -= 1
tempbuffer += yield(:level_down, level)
end
else
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:item, level, cat)
last_right = cat.cright
end
if (level > 1)
tempbuffer += yield(:level_down, level)
end
(level..1).each do |i|
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:level_down, level)
return tempbuffer
endこれをスキン変数の実行部分では
def parse_categorylist params
blog = params[2] ? Blog.find_by_bname(params[2]) : @controller.blog
templatename = params[1]
template = Template.find_by_tdname(templatename)
bd = blog.basic_data
data = Hash.new
blog.category_tree do |type, level, cat|
data['level'] = level.to_s
case type
when :level_up
fill(template['CATLIST_HEADER'], bd.merge(data))
when :level_down
fill(template['CATLIST_FOOTER'], bd.merge(data))
when :item
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
when :item_close
fill(template['CATLIST_LISTITEM_END'], bd.merge(data))
end
end
endといった形でblog.category_treeにブロックを与える格好で呼び出します。かなりすっきりしたような気がします。管理画面でカテゴリーツリーが必要なときもblog.category_treeを使えばロジックは不要になります。
さらに,こういった構造に変えるメリットとして,スキン/テンプレートの実行部分とデータ構造を切り離せます。プラグインのイベント呼び出しを,これまではスキン/テンプレートの中からやる形だったのですが,モデル側に全部移せそうです。今後,スキン・エンジンを新たに作るときなどに,負担が大きく減るはずです。
Migrationをまじめに書く
このところ何をしていたかというと,スキン・エンジンの抽象化作業の残りを続けていました。具体的にはMigrationを書くところ。これまではNucleusのテーブルそのものしか使っていなかったので,実はMigrationを全然書いていなかったのですが,ここで新しいテーブルを導入するためにどうしても必要になったのです。
書いてみるとなかなか楽しい。Nucleusのアップグレードスクリプト相当のものが簡単にできてしまいます。
ちなみに,スキン・エンジンに関係するところは次のよう。
def self.up
create_table :skin_engines do |t|
t.string :klass_name
end
newengine = SkinEngines.new
newengine.klass_name = 'Skin'
newengine.save
create_table :skins do |t|
t.string :skin_name
t.integer :skin_engine
end
add_column :blog, :bskinid, :integer
engines = SkinEngines.find(:all)
blogs = Blog.find(:all)
engines.each do |engine|
engine.klass_name.constantize.find_skins.each do |skin|
s = Skins.new
s.skin_name = skin.name
s.skin_engine = engine.id
s.save
blogs.each do |b|
if b.oldskin.sdname == skin.name
b.bskinid = s.id
b.save
end
end
end
end
end
初めにスキン・エンジンのテーブルを作っていますが,ここは実際にパースを行うクラスをのフィールドだけというシンプルなものです。ついでにデフォルトのSkinクラスを最初のアイテムとして登録しています。
次のskinsのテーブルは抽象化されたスキンのクラス。つまり,いろいろなエンジンのスキンを一緒くたに登録するためのテーブルです。ここではスキンの名前とエンジンのIDのフィールドがあります。
次に,blogテーブルにbskinidというフィールドを加えています。これは今作った抽象化スキンのIDを入れるフィールドです。既にあるbdefskinフィールドを使ってもいいのですが,それだとNucleusとのテーブルの共存ができなくなるため,別フィールドを加えました。
次に,既にスキンの登録が各エンジンにある場合,これを新しいテーブルに登録していく作業をしています。ブログのデフォルトスキンを新しく作ったフィールドに加える作業もここでします。
Migrationは全部1つにまとめてしまうことも可能ですが,このように段階を追って作っておいたほうがメンテナンスは楽になりそうです。
スキン・エンジンの切り替え部分
今ここに手を入れるのが適切だったのかどうか分かりませんが,いつかはやらないといけないところですから,やってよかったのでしょう。まだ完全ではありませんが,思ったほどトラブルなしで動きそうです。最大の変更は,従来のスキンクラスとは別に様々なスキンを一括管理するモデルを加えたこと。SkinManagerとしています。登録内容はスキンの名前とそのクラスです。クラスが決まればそれに対してリクエストを投げることで,実際の描画などを行います。同時にBlogモデルとのリレーションは従来のSkinクラスに代わって,SkinManagerになります。互換性のために,Skinクラスの当初登録内容とSkinManagerのIDは同じにします。
class SkinManager < ActiveRecord::Base
set_base_name :skin_manager
has_many :blogs, :class_name => 'Blog', :foreign_key => 'bdefskin'
delegate :file, :fileurl, :to=>'@skin'
def parse controller
@skin = self.skinclass.constantize.findname(self.skinname)
@skin.parse controller
end
end
内容はこれだけ。実際のスキンのクラス側ではクラスメソッドとしてfindname(名前からスキンのオブジェクトを返す),インスタンスメソッドとしてparse(描画実行),file(ファイル名から内容を返す),fileurl(ファイル名からURLを生成する),part(スキンタイプからそのスキンの内容を返す)といったメソッドを用意しておく必要がありますが,クラスのヒエラルキーは問いません。
説明を補足するとparseメソッドの1行目にあるconstantizeというのがここのキモです。これはRailsがStringクラスを拡張して加えたメソッドの一つで文字列からそのクラスのオブジェクトを作ります。つまりskinclassが"Skin"だったら"Skin".constantizeはSkinクラスになります。これにfindnameというメソッドを呼び出すことでオブジェクトを返してくれます。同じようなことはevalを使ってもできますが,evalよりはこの方がきれいですし,セキュリティ上もベターです。
補遺:実は今回初めて新しくテーブルを作るためにMigrationの機能を使いました。予想はしていたけどテーブルのプレフィクスを見てくれません。どうしたものだか。いっそプレフィクスはあきらめて,nucleus_~~というテーブル名を標準にしてしまうという妥協案を取ってしまうという手もあるかも。
スキン・エンジンの仕様・実装メモ
現在のスキン・モデルはNucleusのDBベースのものしか取り扱えない。これをファイルベースのスキンなどにも利用できるようにする必要がある。ユーザーからするとスキンは名前で指定するだけだから,今のスキン・クラスとは別に統合的にスキンを管理するためのモデル・クラスが必要になる。そのクラスではスキン名とエンジン名のマッチングだけを行う。実際にスキンを読み込むところなどはスキン・エンジンの責任になる。今の実装ではスキン・データを読み込むところがコア側にあるので,責任範囲を変えないといけない。つまり,スキン・エンジンはスキン名を与えられたら,描画内容を返すこと,である。またスキン・エンジンは実際の描画を他のエンジンに任せることができる。すなわち,高級言語からアセンブラに変換するようなもので,結果を他のエンジンに引き渡して描画できる。
このようにするには,スキン・エンジンをモデルとして登録する際に,下位のエンジンが何かというフィールドを用意しておけばよい。
スキン・エンジン自体もRailsのプラグインとして登録可能であり,プラグインと同様にスキン・エンジン用のスタブ・クラスを用意する。後の実装はほとんど自由にできるはずだ。
«Prev || 1 || Next»