Chapter 12: コンポーネントとプラグイン
コンポーネントとプラグイン
コンポーネントとプラグインは比較的新しいweb2pyの機能です。それが何なのか、どうあるべきかに関して開発者の間では意見の相違があります。混乱のほとんどは、他のソフトウェアプロジェクトにおける、これらの用語のさまざまな用途に由来することと、開発者がまだ仕様を完成させるために働いているという事実に由来しています。
しかしながら、プラグインのサポートは重要な機能であり、我々はいくつかの定義を提供する必要があります。これらの定義は最終的なものではなく、単に、本章で説明するプログラミング・パターンに従ったものです。
ここでは次の2つの問題を扱いたいと思います。
- サーバー負荷を最小化しコード再利用を最大化するような、モジュール化されたアプリケーションをどのように構築できるのか?
- 程度の差はあれ流行のプラグイン・アンド・プレイ形式のようなコード断片を、どのように配布することができるか?
コンポーネントでは第1の問題を、プラグインでは第2の問題を扱います。
コンポーネント
コンポーネントは、ウェブページの自律的な機能部品です。
コンポーネントは、モジュール、コントローラ、ビューから構成されています。しかしウェブページに埋め込まれた時、HTMLのタグ(例、DIV、SPAN、IFRAME)中に局所化しなければいけないこと、ページの残り部分とは独立してタスクを実行しなければならないこと以外には厳密な要求はありません。ここでは特にページ内でロードされ、Ajaxを介してコンポーネントのコントローラ関数と交信を行う、コンポーネントに注目します。
コンポーネントの1つの例は、DIVに含まれている"コメント・コンポーネント"です。これはユーザーコメントと新規投稿コメントフォームを表示します。フォームをサブミットする時、フォームをAjaxを介してサーバーに送ります。さらにリストを更新し、コメントをサーバーサイドのデータベースに保存します。そして、ページの残り部分をリロードすることなく、DIVの中身を更新します。
web2pyのLOAD関数は、明示的なJavaScript/Ajaxの知識やプログラミンなしに、容易にそれを行うことを可能にします。
私たちの目標は、ページレイアウトにコンポーネントを組み込むことで、Webアプリケーションを開発できるようにすることです。
デフォルトのひな形アプリを拡張子した、"test"という簡単なweb2pyのアプリを考えます。これは、"models/db_comments.py"ファイルにおいて、次のようなカスタムモデルを持ちます。
db.define_table('comment_post',
Field('body','text',label='Your comment'),
auth.signature)
"controllers/comments.py"コントーローラのコードです。
@auth.requires_login()
def post():
return dict(form=SQLFORM(db.comment_post).process(),
comments=db(db.comment_post).select())
対応する、"views/comments/post.html" ビューです。
{{extend 'layout.html'}}
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <span class="post_body">{{=post.body}}</span>
</div>
{{pass}}
{{=form}}
通常と同じようにアクセス可能です。
http://127.0.0.1:8000/test/comments/post
ここまでの機能では、特別なことは行っていません。しかしレイアウトを拡張しない、".load"拡張子が付いた新しいビューを定義することにより、コンポーネントに切り替えることが可能です。
このため、"views/comments/post.load" を作成します。
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <blockquote class="post_body">{{=post.body}}</blockquote>
</div>
{{pass}}
{{=form}}
次のURLで、アクセス可能です。
http://127.0.0.1:8000/test/comments/post.load
このコンポーネントは、次のように任意のページに埋め込むことができます。
{{=LOAD('comments','post.load',ajax=True)}}
例えば、"controllers/default.py" を次のように編集します。
def index():
return dict()
さらにビューについても、コンポーネントを加えます。
{{extend 'layout.html'}}
{{=LOAD('comments','post.load',ajax=True)}}
ページにアクセスします。
http://127.0.0.1:8000/test/default/index
通常のコンテンツとコメントのコンポーネントが表示されます。
{{=LOAD(...)}}
コンポーネントは次のようにレンダリングされます。
<script type="text/javascript"><!--
web2py_component("/test/comment/post.load","c282718984176")
//--></script><div id="c282718984176">loading...</div>
(実際に生成されるコードは、LOAD関数に渡すオプションに依存します)
web2py_component(url,id)
関数は、"web2py_ajax.html"に定義されています。これが、すべての魔法を叶えます。つまり、Ajaxを介してurl
を呼び出し、対応するid
のDIVにそのレスポンスを埋め込みます。これはDIVに対する全てのフォーム送信を捕捉し、Ajax経由でこれらのフォームを送信します。Ajaxのターゲットは、常にDIVそのものになります。
LOAD関数のすべての引数は以下の通りです。
LOAD(c=None, f='index', args=[], vars={},
extension=None, target=None,
ajax=False, ajax_trap=False,
url=None,user_signature=False,
timeout=None, times=1,
content='loading...',**attr):
解説
- 最初の2つの引数
c
とf
は、それぞれ、呼び出すコントローラ及び関数です。 args
とvars
は関数に渡したい引数と変数です。前者はリスト型、後者は辞書型です。extension
は省略可能な拡張子です。なお、拡張子は、f='index.load'
のように、関数の一部としても渡すことができます。target
はターゲットとなるDIVのid
です。指定していない場合、ランダムなターゲットid
を生成します。ajax
は、DIVがAjax経由で書き込まれる場合にTrue
にします。現在のページが返される前に、書き込む必要がある場合はFalse
にします(Ajax呼び出しを回避します)。ajax_trap=True
とした場合、DIV内のどのフォームの送信も捕捉され、Ajax経由で送信します。そして、レスポンスはDIV内でレンダリングされることが必要です。ajax_trap=False
ではフォームは通常通り送信するため、ページ全体がリロードされます。ajax=True
の場合は、ajax_trap
の値が何であっても無視されてTrue
として扱われます。url
の指定がある場合、c
、f
、args
、vars
、extension
の値を上書きし、url
のコンポーネントをロードします。これは他のアプリケーション(自身もしくはweb2pyで作成されていないかもしれません)によって供給されるコンポーネントページとして、ロードするために使用されます。user_signature
はデフォルトは False です。しかしログインしている場合、True に設定すべきです。これは ajaxのコールバックが、デジタル署名されていることを確認します。この件は4章で触れています。times
はコンポーネントに何度リクエストするかを指定します。継続してコンポーネントをロードする場合は"infinity"を使用します。与えられたドキュメントリクエストに対して通常のルーティンを実行するのに有効なオプションです。timeout
はリクエストが実行されるまでの時間をミリ秒でセットします。times
に1より大きい数字が指定されている場合はそれぞれを実行する間隔になります。content
は ajax 呼び出し実行中に表示されるコンテンツです。これはcontent=IMG(..)
のようにヘルパーになります。- オプション引数の
**attr
(属性) は、DIV
へ属性値を渡すことが可能です。
.load
ビューが指定されていない場合、処理から返される辞書型データをレイアウトなしにレンダリングする generic.load
が使用されます。これは、単一のアイテムを保持する辞書型データの場合、最も上手く動作します。
.load
拡張子と、他の機能(例えばログインフォーム)へリダイレクトするコントローラを持つコンポーネントをLOADで使用する場合、.load
拡張子は伝搬し、新しいURL(リダイレクト先の一つ)にも .load
拡張子でロードされます。
Ajax経由でアクションを実行し、強制的に親ページにリダイレクトさせたい場合は以下のように実現できます。
redirect(url,type='auto')
Ajaxのpostは、マルチパートフォーム(multipart forms)、すなわちファイルアップロードをサポートしていません。このためアップロードフィールドは、LOADコンポーネントでは機能しません。個別コンポーネントの.loadビューからPOSTした場合、アップロードフィールドは通常通り機能するので、これが機能するかのように錯覚するかもしれません。代わりに、ajax互換サードパーティ製ウィジットと、web2pyの手動アップロードを行うstoreコマンドによって、アップロードを行います。
クライアント・サーバー コンポーネント通信
コンポーネントの機能がAjaxを経由して呼ばれるとき、web2pyはリクエストに2つのHTTPヘッダを渡します。
web2py-component-location
web2py-component-element
これは次の変数を介して、アクセスできます。
request.env.http_web2py_component_location
request.env.http_web2py_component_element
後者はまた次のようにアクセスできます。
request.cid
前者は、コンポーネント機能を呼び出したページのURLが含まれています。 後者は、レスポンスを含んだDIVの id
が含まれています。
コンポーネント機能はまた、2つの特別なHTTPレスポンスのヘッダにデータを格納します。これらは、レスポンス時にページ全体で解釈されます。次の通りです。
web2py-component-flash
web2py-component-command
これらは、次のものを介して設定可能です。
response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'
または(機能がコンポーネントから呼び出されたなら)、自動的に次のようにすることもできます。
response.flash='...'
response.js='...'
前者は、レスポンス時にフラッシュさせたいテキストを含みます。 後者は、レスポンス時に実行させたいJavaScriptを含みます。改行文字を含むことはできません。
例として、ユーザーが質問できるようなコンタクトフォームのコンポーネントを"controllers/contact/ask.py"に定義します。コンポーネントは、システム管理者に質問をメールし、"thank you"メッセージをフラッシュし、ページからそのコンポーネントを取り除きます。
def ask():
form=SQLFORM.factory(
Field('your_email',requires=IS_EMAIL()),
Field('question',requires=IS_NOT_EMPTY()))
if form.process().accepted:
if mail.send(to='admin@example.com',
subject='from %s' % form.vars.your_email,
message = form.vars.question):
response.flash = 'Thank you'
response.js = "jQuery('#%s').hide()" % request.cid
else:
form.errors.your_email = "Unable to send the email"
return dict(form=form)
最初の4行はフォームを定義し、それを処理します。送信用のメールオブジェクトは、デフォルトのひな形アプリケーションで定義されています。最後の4行は、HTTPリクエストヘッダからデータを取得し、HTTPレスポンスヘッダにそれを設定する、コンポーネント固有のロジックを実装しています。
これで次のように、このコンタクトフォームを任意のページに埋め込むことができます。
{{=LOAD('contact','ask.load',ajax=True)}}
ask
コンポーネントに対して、.load
ビューを定義しなかったことに、注目してください。これは単一のオブジェクト(form)を返すため、"generic.load" で十分なためです。汎用ビューは開発ツールであることに注意してください。本番においては、 "views/generic.load" を "views/contact/ask.load" にコピーする必要があります。
user_signature
引数を使用し、URLをデジタル署名することにより、Ajaxを介して呼び出される関数へのアクセスをブロックすることができます。{{=LOAD('contact','ask.load',ajax=True,user_signature=True)}}
URLに対するデジタル署名を有効にしています。 デジタル署名ではコールバック関数に、デコレータを使った認可を必要とします。
@auth.requires_signature()
def ask(): ...
Ajax トラップリンク
通常、リンクはトラップされません。このため、コンポーネント内のリンクをクリックすると、リンクページ全体がロードされます。しかし、リンクしたページがコンポーネント内にロードするようにしたい場合があります。これは次のようにA
ヘルパを用いて、実現することができます。
{{=A('linked page',_href='http://example.com',cid=request.cid)}}
cid
が指定された場合、リンクページはAjaxを経由してロードされます。 cid
は、設置するロードページ・コンテンツのhtml要素のid
です。この例ではrequest.cid
、すなわちリンクを生成したコンポーネントのid
を設定しています。リンクページは通常、URLコマンドを用いて生成した内部URLにします。
プラグイン
プラグイン は、アプリケーションファイルのサブセットです。
そして次のような、いくつかの現実的な特徴があります。
- プラグインはモジュールでなく、モデルでもなく、コントローラやビューでもありません。むしろモジュール、モデル、コントローラを含んでおり、ビューは含まれたり、含まれなかったりします。
- プラグインは機能上、自律性がある必要はありません。他のプラグインや特定ユーザのコードに依存していても構いません。
- プラグイン は プラグインシステム ではありません。ある程度の独立性を実現するためのルールは決めようとしていますが、登録や独立といった概念は持っていません。
- プラグインは、アプリケーションのためであり、web2py(フレームワーク)のためのプラグインではありません。
それでは、なぜプラグインと呼ぶのでしょうか?。それはアプリケーションのサブセットをパッキングし、他のアプリケーション上でアンパッキング(つまりプラグイン)するメカニズムを提供するからです。この定義に基づき、アプリケーション中のいくつかのファイルはプラグインとして扱われます。
アプリケーションが配布される時、プラグインはパックされ、アプリケーションと一緒に配布されます。
実際に admin はアプリケーションから独立して、パッキングやアンパッキングするためのインターフェイスを提供します。接頭語 plugin_
name の名前をもつアプリケーションのファイル及びフォルダは、次のファイルにまとめることができます。
web2py.plugin.
name.w2p
そして一緒に配布されます。
admin が一緒に配布されることを名前から判断し、さらに別ページに表示することを除き、web2pyはプラグインの構成するファイルを別に扱うことはしません。
まだ実際のところ、上記の定義に従い admin よって承認されたようなプラグインよりも、次のようなプラグインが一般的です。
現実的には、私たちは2つのタイプのプラグインだけ考えることにします。
- コンポーネントプラグイン。これは前セクションで定義したようなコンポーネントを含んだプラグインです。コンポーネントプラグインは、1つ以上のコンポーネントを含めることがで可能です。上記のcommentsコンポーネントを含んだ、plugin_commentsを例として考えることができます。他の例は、taggingコンポーネントと、プラグインによって定義したいくつかのデータベーステーブルを共有するtag-cloudコンポーネントを含む、
plugin_tagging
です。 - レイアウトプラグイン、これはレイアウトビューと必要な静的ファイルを含んだプラグインです。プラグインを適用するとアプリケーションに新しい、ルック&フィールを提供します。
上記の定義に従って、前セクションで製作したコンポーネントがあります。例えば、"controllers/contact.py"は既にプラグインです。あるアプリケーションで定義したコンポーネントを、他のアプリケーションに持って行き使用することが可能です。しかしまだ、プラグインとしてのラベルが無いため、admin から承認されていません。これには、解決すべき2つの問題があります。
- プラグインファイルを規定に従って命名することで、admin は同じプラグインの属するものとして承認できます。
- モデルファイルがプラグインにある場合、規定を設定することで名前空間を汚さず、互いの衝突も避けることをできます。
nameという名前のプラグインを仮定とします。以下のような従うべきルールがあります。
ルール 1: プラグインのモデルとコントローラは、それぞれ次のように呼ばれる必要があります。
models/plugin_
name.py
controllers/plugin_
name.py
フオルダ内にあるプラグインのビュー、モデル、静的ファイル、プライベートファイルは、それぞれ次のように呼ばれる必要があります。
views/plugin_
name/
modules/plugin_
name/
static/plugin_
name/
private/plugin_
name/
ルール 2: プラグインのモデルは、次の名前で始まるオブジェクトを定義するだけです。
plugin_
namePlugin
Name_
ルール 3: プラグインモデルは、次の名前で始まるセッション変数を定義するだけです。
session.plugin_
namesession.Plugin
Name
ルール 4: プラグインはライセンスとドキュメントを含める必要があります。設置は次の場所です。
static/plugin_
name/license.html
static/plugin_
name/about.html
ルール 5: プラグインはひな形の"db.py"で定義された、次のグローバルオブジェクトだけに依存できます。
db
というデータベースのコネクションauth
というAuth
インスタンスcrud
というCrud
インスタンスservice
というService
インスタンス
いくつかのプラグインはより高度です。複数のdbインスタンスが存在する場合、これらのプラグインはパラメータ設定を持っています。
ルール 6: プラグインにパラメータ設定が必要な場合、以下で説明するPluginManagerを使用して設定してください。
上記のルールに従うことによって、次のことが確実になります。
- admin は全ての
plugin_
nameファイルとフォルダーを、単一エンティティの一部として承認します。 - プラグインは互いに干渉しません。
上記のルールはプラグインのバージョンと依存関係の問題は解決しません。それは機能の範囲を超えています。
コンポーネントプラグイン
コンポーネントプラグインはコンポーネントを定義するプラグインです。コンポーネントは通常、データベースにアクセスし独自モデルを定義します。
以前記述したコードと同じですが、前述のルールに従い、comments
コンポーネントを comments_plugin
に変更します。
ステップ1、"models/plugin_comments.py" モデルを作成します。
db.define_table('plugin_comments_comment',
Field('body','text', label='Your comment'),
auth.signature)
def plugin_comments():
return LOAD('plugin_comments','post',ajax=True)
(プラグイン組み込みを単純化する関数定義がある、最後の2行を注意のこと)
ステップ2、"controllers/plugin_comments.py"を定義します。
def post():
if not auth.user:
return A('login to comment',_href=URL('default','user/login'))
comment = db.plugin_comments_comment
return dict(form=SQLFORM(comment).process(),
comments=db(comment).select())
ステップ3、"views/plugin_comments/post.load"ビューを作成します。
{{for comment in comments:}}
<div class="comment">
on {{=comment.created_on}} {{=comment.created_by.first_name}}
says <span class="comment_body">{{=comment.body}}</span>
</div>
{{pass}}
{{=form}}
admin を使って配布用プラグインをパックできます。Adminはこのプラグインを次のように保存します。
web2py.plugin.comments.w2p
admin の edit ページでプラグインをインストールし、ビューに追加するだけで、どのビューでもプラグインを使用することが可能になります。
{{=plugin_comments()}}
もちろん、パラメータを取得しオプションを設定したコンポーネントを用いることで、より高度なプラグインの作成が可能です。コンポーネントがより複雑になれば、名前の衝突を避けるのが難しくなります。以下で説明するプラグインマネージャーは、この問題を回避するように設計されています。
プラグインマネージャー
PluginManager
は、gluon.tools
で定義されているクラスです。内部でどの様に動作するかを説明する前に、使用方法を説明します。
ここでは前述の comments_plugin
を改良します。また、プラグインのコード変更なしで、カスタマイズを行なってみます。
db.plugin_comments_comment.body.label
プラグインのコード自体を編集せずに
次のように改良します。
最初に、次のように"models/plugin_comments.py"を書き換えます。
def _():
from gluon.tools import PluginManager
plugins = PluginManager('comments', body_label='Your comment')
db.define_table('plugin_comments_comment',
Field('body','text',label=plugins.comments.body_label),
auth.signature)
return lambda: LOAD('plugin_comments','post.load',ajax=True)
plugin_comments = _()
テーブル定義を除くすべてのコードが、グローバル名前空間を汚染しないように、単一関数_
内にカプセル化されていることに、注意してください。また関数が、 PluginManager
のインスタンスを作成することについても、注意してください。
アプリケーションの他のモデルファイルに、例えば"models/db.py"で、次のようにプラグインの設定をしてください。
from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comments.body_label = T('Post a comment')
plugins
オブジェクトのインスタンス化は、ひな形アプリケーションの"models/db.py"で既に設定されています。
PluginManagerオブジェクトは、Storageオブジェクトのスレッドレベルのシングルトン(singleton)Storageオブジェクトです。これは、同じアプリケーション内で好きなだけインスタンスを作成できますが、しかし(同じ名前を持っていようがいまいが)これらは1つのPluginManager インスタンスであるかのように振る舞います。
特に各プラグインファイルは、独自にPluginManagerオブジェクトを作成登録し、デフォルトパラメータを設定できます。
plugins = PluginManager('name', param1='value', param2='value')
他の場所(例えば "models/db.py")で、これらのパラメータの上書きが可能です。
plugins = PluginManager()
plugins.name.param1 = 'other value'
一箇所で複数のプラグインの設定も可能です。
plugins = PluginManager()
plugins.name.param1 = '...'
plugins.name.param2 = '...'
plugins.name1.param3 = '...'
plugins.name2.param4 = '...'
plugins.name3.param5 = '...'
プラグインが定義される時、PluginManagerは引数を取る必要があります。プラグイン名と、デフォルトパラメータであるオプションの名前付き引数があります。しかし、プラグインが設定されている場合、PluginManagerコンストラクタは引数を取りません。設定はプラグイン定義の前に行う必要があります(つまり、名前がアルファベット順で先頭のモデルファイルで行う必要があります)。
レイアウトプラグイン
レイアウトプラグインは通常コードは含まず、ビューと静的ファイルだけで構成されるため、コンポーネントプラグインより単純です。とはいえ、次のような良いプラクティスに従うべきです。
最初に、"static/plugin_layout_name/"(nameはレイアウトの名前です) フォルダを作成し、そこに必要な静的ファイルを配置します。
2番目に、レイアウトを含む "views/plugin_layout_name/layout.html" レイアウトファイルを作成します。これには "static/plugin_layout_name/" の画像、CSS、JavaScriptファイルをリンクします。
3番目に、"views/layout.html"を次のように簡単に読み込むように修正します。
{{extend 'plugin_layout_name/layout.html'}}
{{include}}
この設計の利点は、このプラグインのユーザが複数レイアウトをインストールできることと、"views/layout.html"を編集するだけで、どのデザインを適用するか選択できることです。その上、"views/layout.html"はプラグインと一緒に admin によりパックされませんので、インストール済みレイアウトのユーザコードをプラグインが上書きする心配がありません。
プラグインレポジトリ
web2pyのプラグインレポジトリは一箇所だけではありません。以下のサイトにてweb2pyプラグインを見つけることができます。
http://dev.s-cubism.com/web2py_plugins
http://web2py.com/plugins
http://web2py.com/layouts
s-cubismのレポジトリのスクリーンショットです。