2010-10-28

リクエストの表現 (あるいは URI) の設計 - Blogger Glass

Blogger Glass では、リクエスト(特定の機能を実行するための URL)を表現するために「クエリ変数」を用いている。具体的には、個別の記事を見るためのリクエストは以下のようになる。

http://bloggerglass.appspot.com/post/?id=1234567890

Blogger Glass にとって(Blogger にとっても)、記事は最小のリソースだが、記事 ID はそれを特定するために必要十分な情報だ。言い換えると、/post/ リクエストに対する付加情報(引数と言っても良い)には記事の ID 以外にはない。このことを踏まえると、/post/?id=1234567890 という表現の中の ?id= の部分は余計なものだとわかる。これは以下のように書けばすっきりした表現になる。

http://bloggerglass.appspot.com/post/1234567890

同じことは他のリクエストに対してもあてはまるだろうか?

2 つの形式

(「RESTful Webサービス」p.126)
パス変数は階層をトラバースしているように見えるし、クエリ変数はアルゴリズムに引数を渡しているように見える。「検索」にはアルゴリズムのような響きがある。

以下では、「クエリ変数」を使ったリクエストの表現を「アルゴリズム形式」、パスにマップした表現を「パス形式」と呼ぶことにする。

プログラミング的に見れば、必要な情報をウェブアプリに渡すという意味で 2 つの形式は等価だと言える。ただし、アルゴリズム形式は、伝統的に CGI で使われてきたこともあり、パス形式より簡単に扱える(後述)。

ヒトが読むことを想定するなら、アルゴリズム形式よりもパス形式の方が見やすい。先の個別記事を見るためのリクエストの例のように、パスの階層の意味が明確ならなおさらだ。

とはいえ、ウェブサービスを作っているならともかく、ウェブアプリではトップの URL を別にして、リクエストを表現する URL はユーザの目に触れることを意図しない。であれば、どちらを使ったところで「ユーザ体験」に差が出るとは思えない。

試してみる

なにはともあれ、試してみよう。

Blogger Glass 全リクエスト形式 (省略形をのぞく)
リクエスト 機能
/?page=<number> number で指定したページの一覧を表示する。
/post/?id=<id> id で指定した記事の内容を表示する。
/search/?label=<string> ラベル string で記事を絞り込んむ。結果が複数ページにわたる場合は、最初のページ(ページ番号 0)を開く。
/search/?label=<string>&page=<number> ラベル string で記事を絞り込んだ結果のうち、number で指定したページを開く。
/settings/ 設定変更のための画面を開く。

これらアルゴリズム形式のリクエストをパス形式で表現すると以下のようになる(付加情報のない /settings/ を除く)。

アルゴリズム形式とパス形式の対応
アルゴリズム形式 パス形式
/?page=<number> /<number>
/post/?id=<id> /post/<id>
/search/?label=<string> /search/<label name>
/search/?label=<string>&page=<number> /search/<label name>/<number>

以下では、個別記事を見るためのリクエストをパス形式にするための実装上の変更点を見ていく。

追記@2010-10-31

以下の記述はほぼ無用のものだった。GAE の webapp フレームワークでは、リクエスト URL とリクエストハンドラを結びつける URL マッピングという機能を提供している。これにより、上述のパス形式で表現されたリクエスト URL の中から、その一部をパラメータとして切り出すことが簡単にできる。正規表現のグルーピングを使って切り出す部分を指定すると、get メソッド等の引数となってわたってくる。

具体的にはこうなる。

(src/postview.py より)
class PostViewHandler(webapp.RequestHandler):
    [...snip...]
    def get(self, post_id):
        [...snip...]

def main():
    application = webapp.WSGIApplication([('/post/(\d+)', PostViewHandler),
                                          ],
                                         debug=True)

Bloggger Glass のコードには、GAE の便利な機能を知らないために書いてしまっている無駄なコードが他にもまだあるんだろうな。

app.yaml への記述
(src/app.yaml より)
- url: /post/|/post/.*/?
  script: postview.py

省略形の /post/ や /post/1234567890/ のように末尾の余分なスラッシュに対応するために、URL パターンは少し複雑になっている。

リクエストハンドラの変更

変更点は主に 2 ヶ所。1 つは URL から付加情報を取り出す部分、もう 1 つは URL に対するハンドラの対応づけだ。

付加情報の取り出しは util モジュールに関数として置く。

(src/util.py より)
def get_params(uri, request_type):
    """This function assumes that a request uri must be written in
    one of the following forms:
    - 'http://<host>/<param_1>/<param_2>/.../<param_n>/'
    - 'http://<host>/<request_type>/<param_1>/<param_2>/.../<param_n>/'
    """
    uri_parts = uri.split('/')[3:]
    if len(uri_parts) < 1 or uri_parts[0] == '':
        params = None
    elif uri_parts[0] == request_type:
        params = uri_parts[1:]
    else:
        params = uri_parts
    return params

呼び出す側を以下に示す。

(src/postview.py より)
        post_id = util.get_params(self.request.uri, 'post')[0]

ハンドラの対応づけ部分はこうなる。

(src/postview.py より)
def main():
    application = webapp.WSGIApplication([('/post/', PostViewHandler),
                                          ('/post/.*/?', PostViewHandler),
                                          ],
                                         debug=True)

app.yaml 同様の指定になっている。2 つに分けているのは見易さのためだ。

参考文献

RESTful Webサービス
Leonard Richardson, Sam Ruby
オライリー・ジャパン ( 2007-12-21 )
ISBN: 9784873113531
おすすめ度:アマゾンおすすめ度

関連リンク

関連記事

2010-10-27

絞り込み検索とパンくずリスト

Blogger Glass を「iPhone アプリらしく」仕立てるためのアレコレを考えていて、ラベルによる絞り込み検索というモノを思い付いた(「iOS デバイスに対応する (基礎編) - Blogger Glass」の「もっと iPhone らしく」を参照)。

ユーザ体験としてはこんな感じになる。

  1. 「一覧画面」から「ラベル検索画面」を開く。
  2. 元の「一覧画面」に出ていた記事それぞれに付いていたラベルがまとめて表示されている。
  3. ラベルの中から 1 つ選ぶと、検索実行。
  4. 「検索結果画面」になる。 (*)
  5. 絞り込みをする場合、再度「ラベル検索画面」を開く。
  6. さきほどの「検索結果画面」の記事それぞれに付いていたラベルがまとめて表示されている。
  7. ラベルの中から 1 つ選ぶと、検索実行。このときは、先に選んだラベルとの複合検索(AND)になる。
  8. (*) に戻る。

最初に「一覧表示」から開いた「ラベル検索画面」では、すべての記事に付いているラベルを表示する方が良いかも。

パンくずリスト

日本語では「パンくずリスト」、英語では「breadcrumbs」あるいは「breadcrum trail」と呼ばれるモノがある。現在表示中のウェブページの位置を、たどってきたパスの階層構造等で表現するナビゲーションアイテムだ。Mac OS X の Finder で、表示中のフォルダの階層構造を示すために使われている「パスバー」もこの一種だ(Finder に表示されていなければ、メニューから「表示」>「パスバーを表示」を選ぶ)。

Finder のパンくずリスト

上述の「ラベル検索」では、検索に使われたラベルをユーザにフィードバックしなければならない。そのためにも「パンくずリスト」が適しているはず。これならユーザが選んだ順序をふくめて表現できる。検索結果はラベルを選んだ順序によらないが、ユーザにとっては自分の操作を正確にフィードバックしてくれる方が良い(少なくともわたしはそうだ)し、なにより「パンくずリスト」なら、逆にたどって以前の画面(検索結果)に戻っていける(という機能をわかりやすく提示できる)。

文字入力させたら負けだと思う

iPhone のように使用状況を選ばないデバイス(歩きながらでも、満員電車で吊り革につかまっているときでも使えるし、使いたい)では、文字入力のような複雑な操作はなるべく避けたい。項目をタップし、画面をドラッグそれにスワイプする。片手で操作できるなら歩きながらでも使える。

検索では、最初にラベルを複数選んでおいてから検索実行、という方式も考えられる。だが iPhone で実現するなら、操作が自ずと小さなステップに分割され、さらに個々のステップでフィードバックも得られる「順次絞り込み」タイプの方が適している。また、現在の状況と操作の履歴を同時に表現できる「パンくずリスト」はナビゲーションの仕組みとして最適だろう。

ま、実際に使ってみない間は、ただの一般論でしかないがな。

関連リンク

関連記事

2010-10-26

iPhone アプリらしく #1 - 画面デザイン (Blogger Glass)

ウェブアプリを iPhone アプリらしく仕上げるためには、画面のデザイン(とくに意匠)も iPhone 用のものが必要になる。単にスタイルシートの切り替えだけでは限界がある。さらに言えば、スタイルシートも共通部分と画面に特有のものとに分けるべきだ。

以下は、Blogger Glass の主要画面のデザインを iPhone 用に作り直したものだ。

Blogger Glass: iPhone 用画面デザイン
画面 外観 説明
一覧画面 記事の一覧をリストとして表示する。各項目をタップすることで個別記事の内容画面に移る。左上のボタンでメニューを表示する。
内容画面 ブログの個別記事の内容を表示する。左上のボタンでリスト表示に戻る。左↓のボタンで、記事に付けられたラベル一覧を表示する(→ ラベル検索画面)。
メニュー アプリメニューを表示する。「Back」でメニューを開く前の画面に戻る。
ラベル検索画面 記事に付けられたラベルをリストで表示する。各項目をタップすることで、ラベル検索を行う。下部にはラベルによる絞り込みの状況を表示する。メニューと同様、「Back」で 1 つ前の画面に戻る。

画面のデザインが変われば機能にも手直しが入る。

たとえば、iPhone の画面の狭さから、これまではアプリ画面上部に共通で表示させていたメニューは、別画面として独立させることにした。また、内容画面では一覧表示に戻るためのボタン(リンク)を用意する。メニューやラベル検索画面にも「Back」ボタンを用意して元の画面に戻れるようにする。

この「戻る」を実現するためには、元の画面(をリクエストする URL)をプログラム的に記憶しておく仕組みが必要になる。

変更するのはスタイルシートとテンプレートだけで済みそうにないな。

関連記事

2010-10-25

iOS デバイスに対応する (基礎編) - Blogger Glass

Blogger の提供するスタイルにあれこれ変更を加え始めたのは何より iPhone (と iPad) で見やすくしたかったからだ。それが高じてブログの記事を表示する GAE アプリを作るまでに至った。ある程度の実用性を備えた今こそ、iOS デバイスの対応を始めるときだ。

iOS デバイスへの対応と言っても、これまでのように単にスタイルを最適化する程度で終わりたくない。ブログコンテンツを表示するためのアプリと呼べるものに仕上げたいのだ。最終的には Blogger Glass を iOS デバイス用のウェブアプリと呼ぶにふさわしいモノにするつもり。何をどこまで作り込めばそう呼べるかはまだわからない。そもそも BG 自体まだ機能的に不足しているし、実装ずみの機能も洗練されているとは言えないしな。課題は(見えないものもふくめて)多いが、まずは一歩を踏み出そう。そうすれば次に進むべき方向も見えてくるから。

今回は基礎編。まずは以前の成果(スタイル定義)を流用して、現在、Blogger (の LOG+REPO) を iOS デバイスで開いたときと同等の外観を実現する。それには主に以下の 2 つの作業が必要だ。

  • Viewport の設定
  • デバイスごとの CSS ファイルの用意

それでは順番に見ていこう。

viewport の設定

(「iPhoneアプリケーション開発ガイド―HTML+CSS+JavaScript による開発手法」p.17)
特に指定がない場合、iPhone 版の Safari ではページの横幅が 980 ピクセルであるとみなされます(図2-3)。多くの場合はこの設定でも問題はありませんが、iPhone の小さな画面に特化したコンテンツを作成するためには横幅を明示する必要があります。

さて、この横幅の指定は meta タグによって「viewport」を設定することで行う。viewport の設定(の meta タグ)は(iOS デバイスの Safari)以外のブラウザでは無視されるということだ(→「iPhoneアプリケーション開発ガイド」p.18)が、そこはウェブアプリなのだからリクエストデバイスに応じて追加するようにしたい。

それが以下の部分だ。app オブジェクトの is_ios_device 属性が True のときだけ viewport 設定のmeta タグを追加している。

(src/base.html より)
<title>{{ app.title }}</title>
{% if app.is_ios_device %}
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
{% endif %}
<link type='text/css' rel='stylesheet' href="{{ app.stylesheet }}">

app オブジェクトは、各リクエストハンドラからわたってくる info.AppInfo クラスのインスタンスだ。このオブジェクトに情報を詰めているのが、util.fill_appinfo 関数で、リクエストデバイスの識別を行う util.get_device 関数を利用してデバイスごとの情報を生成している。それにはデバイス専用の CSS ファイルもふくまれている。

(src/util.py より)
def fill_appinfo(appinfo, request_url, user_agent):
    [...snip...]
    device = get_device(user_agent)
    if device == 'iphone':
        appinfo.is_ios_device = True
        appinfo.stylesheet = '/stylesheets/iphone.css'
    elif device == 'ipad':
        appinfo.is_ios_device = True
        appinfo.stylesheet = '/stylesheets/ipad.css'
    else:
        appinfo.is_ios_device = False
        appinfo.stylesheet = settings.get('stylesheet')

デバイスの識別はユーザエージェント文字列中のデバイス名によるもので、3 つのタイプを返す(iphone、ipad、そして mac)。iOS デバイス以外は mac 扱いにしている。

(src/util.py より)
def get_device(user_agent):
    if user_agent.find('iPad') > -1:
        device = 'ipad'
    elif user_agent.find('iPhone') > -1 or user_agent.find('iPod') > -1:
        device = 'iphone'
    else:
        device = 'mac'
    return device

デバイスごとの CSS ファイルの用意

デフォルトスタイルの時と同様に、Style Repository で LOG+REPO 用に提供している各デバイス用のスタイルをほぼそのまま流用した。

iPhone 用では実験的に少し色を変更してあるが、これはすぐに変更することになる(後述)。

Blogger Glass が生成する HTML は HTML5 に準拠することを目指している。header や section、そして footer のような区画分けのための要素を使っているのも、そのためだ。今回、iPad 用のスタイルを用意していて、現在の iPad 上の Safari (OS: 3.2.2 (7B500)) ではこれら区画分けの要素に単独で指定したスタイルが効かないことがわかった。具体的には以下のような定義が無効になる。

(区画分け要素のスタイル)
header {
    background-color: #eeffcc;
}

同じ定義が iPhone および iPod touch (どちらも OS 4.1 (8B117)) では有効になる。iPad の OS がアップデートされれば解決されるだろう。

ま、わたし自身が十分に HTML5 のスペックを読み込んでいないため、BG の HTML5 準拠も手探りだからな。BG が生成する HTML の構造もまだまだ変わる。スタイルもそれに合わせて変えなくてはならない。今のスタイルも暫定版でしかない。

もっと iPhone らしく

さて、以上が iOS デバイス対応の基礎編だ。ここから先は、デバイス対応というよりも、むしろ BG の iOS デバイス専用ウェブアプリ化になる。その手始めとして、今、実験中のスタイル定義によるスクショを示す。iPod touch による表示だ。

これは「iPhoneアプリケーション開発ガイド」の p.19 〜 23 の書かれている手法を適用したものだ。少しリストと見出しのスタイルをいじるだけで、ずいぶん iPhone アプリらしくなる。

ゴールを、iPhone でも使えるモノから iPhone で専ら使うモノに変えると、機能のデザインも(とくにユーザ体験に関して)いろいろと変わってくる。たとえば、ラベル検索による絞り込みも、Mac (上の Safari)で使うことが前提なら、詳細検索画面を作って複数のラベルを指定させて、なんていう拡張を考える。けれど iPhone に特化するなら、まずラベルを 1 つ選択して検索し、その結果表示画面でさらにもう 1 つラベルを選択して(2 つのラベルによる複合検索で)絞り込む、というような使い方を考える。

iPhone では一度に表示できる情報が限られているし、モバイル機器であるためユーザとの対話は短いほど望ましい(ユーザは椅子に座ってじっくりと iPhone の画面を眺めているとは限らない)。そういった機器の特性はユーザ体験のデザインに大きく影響する。ウェブアプリとして実現されていたとしても、iPhone 用を標榜するなら、iPhone アプリらしくあらねばならない。単にスタイルを変えるだけで済む問題ではないってことだ。

参考文献

関連リンク

関連記事

2010-10-24

ページ切り替え機構の切り替え - Blogger Glass

今回はちょっと実験。2 種類のページ切り替えの仕組みを用意し、どちらが使いやすいかを確かめる実験だ。

「ページ切り替え」と言うのは、Blogger Glass の一覧表示系の画面の下部に出てくるもののこと。リストする項目が多いときには複数のページに分割する。そのページを切り替えるための仕組みだ。

現状では、Google の検索結果に出てくるモノ(Goooooooooogle! のようになるアレだ)を真似ている。これをもっとシンプルな「Next」「Prev」ボタンのものと比較してみようと思ったのだ。

2 つの方式の比較

実験というからには、2 つを比較できなければ意味がない。旧来の方式と新しい方式を両方使えるようにしておいて、実際に使ってみて試すわけだ。

幸い、一覧表示系の画面(機能)は「一覧画面」と「検索結果画面」の 2 種類がある。このうち、「一覧画面」のみを新方式にして、「検索結果画面」の方は従来のままにしておく。そうすれば、どちらの方式も試すことができる。

まあ、配備ずみの正式版(appspot で稼動しているもの)と開発中のローカル版(SDK で動かしているもの)で比較するという手もあるんだがね。

リファクタリング

「ページ切り替え機構」を簡単に取り替えられるようにするため、src/info.py、さらに src/main.py と src/searchview.py にも手を入れる。

具体的には、「ページ切り替え機構」そのものを ViewInfo のインスタンスで抱え、表示対象のページ番号や全ページ数は「ページ切り替え機構」の属性に押し込める。リクエストハンドラは「ページ切り替え機構」を選び、ViewInfo のインスタンスにセットすることになる。

「ページ切り替え機構」は、util.Pager クラスを基底として派生させる。旧方式は util.GlassPager と名付けた。

(src/util.py より)
class Pager(object):
    PAGESIZE = 25
    def __init__(self, page_num, total_posts):
        self.current = page_num
        self.total_pages = 0
        if total_posts > 0:
            self.total_pages = (total_posts - 1) // Pager.PAGESIZE + 1
        else:
            self.total_pages = 1

class GlassPager(Pager):
    class Page(object):
        def __init__(self, number, current=False):
            self.number = number
            self.current = current

    def __init__(self, page_num, total_posts):
        Pager.__init__(self, page_num, total_posts)
        self.pages = []
        for p in range(self.total_pages):
            self.pages.append(GlassPager.Page(p, p == page_num))

src/main.py の MainHandler で「ページ切り替え機構」をセットする付近のコードを示す。

(src/main.py)
            total_posts = int(feed.total_results.text)
            self.viewinfo.pager = util.PrevNextPager(page, total_posts)
            self.viewinfo.start_index = start_index

結果として、ページ切り替えに関する情報の生成と詰め直しを追い出したことになり、少しすっきりした。

シンプルなページ切り替え

ごく単純な「Prev」「Next」の 2 つのボタン(リンク)のある方式で、最初と最後へのショートカットを持っている。つまり、こんな感じになる。

[Top] [Prev] <current page #> [Next] [Last]

また、現在のページが最初のときは「Top」と「Prev」は表示せず、最後のときは同様に「Next」と「Last」を表示しない。

これを実現するための util.Pager の派生クラスは以下のようになる。

(src/util.py より)
class PrevNextPager(Pager):
    def __init__(self, page_num, total_posts):
        Pager.__init__(self, page_num, total_posts)

        if self.total_pages > 0:
            self.last = self.total_pages - 1

        if self.current != 0:
            self.is_top = False
            self.prev = self.current - 1
        else:
            self.is_top = True
            self.prev = 0

        if self.current != self.last:
            self.is_last = False
            self.next = self.current + 1
        else:
            self.is_last = True
            self.next = self.last

で、これを表示するテンプレートはこう。

(src/listview.html より)
{% block view_footer %}
<nav>
  <div class="pager">
    {% if not view.pager.is_top %}
    <span class="l-button"><a href="/?page=0">Top</a></span>
    <span class="l-button"><a href="/?page={{ view.pager.prev }}">Prev</a></span>
    {% endif %}
    <span id="pager-current">{{ view.pager.current }}</span>
    {% if not view.pager.is_last %}
    <span class="r-button"><a href="/?page={{ view.pager.next }}">Next</a></span>
    <span class="r-button"><a href="/?page={{ view.pager.last }}">Last</a></span>
    {% endif %}
  </div>
</nav>
{% endblock %}

info.Menu クラスと同様に、util.Pager クラス(とそのサブクラス)は info モジュールに移すべきかな。

「Goooooooooogle!」方式と「前後」方式

実際に使ってみたところ、「Goooooooooogle!」方式にはとくにメリットがないと感じた。この方式では途中のページにダイレクトに飛べるわけだが、そこに何がリストされているかがわかっていない限りほとんど意味がない。リストを順にたどって見るようなときには「前後」方式の方が切り替えやすい(マウスを動かさなくても良いから)。まとめて何ページも先を開くなら「Goooooooooogle!」方式の方が便利だが、実用上そういう場面が思い浮かばない。

特定の時期に書かれた記事を探すなら大きく前後に移動できた方が便利だが、そういう便利さを実現したいなら月や週で検索できる画面(ビュー)を用意した方が良い。

iPhone のような画面の狭いデバイスの場合も「前後」方式が適している。特定のページを直接開くというユーザ体験を実現するなら、それ専用の画面(ビュー)を作るべきだ。

「一覧」や「検索」といった機能を実行した結果が複数のページに分かれる場合、シンプルな前後方式の方が汎用性が高い(画面のサイズを選ばないから)。そもそも(大量の)複数ページに分かれているという点でユーザ体験としては減点だろう。ユーザが意識して「全部を順番に見たい」というのでない限り、適切に絞り込む手段を提供すべき。そして、適切に絞り込めるなら(ページ数にしてせいぜい 2 〜 3)「前後」方式が(ユーザに余分なことを考えさせないという意味で)直感的だ。

関連リンク

関連記事