読者です 読者をやめる 読者になる 読者になる

HackerNews翻訳してみた

HackerNewsを中心に、人気の英語記事を翻訳してお届けします。記事は元記事の著者に許可をとった上で翻訳・掲載をしています。

Webフレームワークとは何か

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: What is a Web Framework? by Jeff Knupp




Webアプリケーションフレームワーク、略して「Webフレームワーク」がWeb対応のアプリケーション構築に広く使われているのは、皆さんご存じですよね。単純なブログからAjax機能を搭載した複雑なアプリケーションまで、Web上のすべてのページはコードで記述されています。最近気になるのは、FlaskやDjangoのようなWebフレームワークに興味を持ってはいるけれど、実際にはWebフレームワークの目的や機能をちゃんと理解していない開発者が意外に多いということです。そこでこの記事では、ややもすれば見落とされがちなトピックであるWebフレームワークの基礎を取り上げることにしました。皆さんがこの記事を読み終える頃には、Webフレームワークとは何か、そしてそもそもなぜWebフレームワークが存在するのかという根本のところを、きちんと理解できるようになっているはずです。Webフレームワークの基礎知識があれば、新しいWebフレームワークの習得や、どのフレームワークを選ぶべきかの判断がずっと楽になるでしょう。

Webのしくみ

フレームワークの話に入る前に、まずWebの「しくみ」を理解しましょう。最初に、ブラウザにURLを打ち込んでEnterキーを押したら何が起こるのかを検証したいと思います。ブラウザの新しいタブを開いてhttp://www.jeffknupp.comに飛んでください。それでは、ページを表示するのにあなたのブラウザが取った手順を見ていきましょう(DNS検索の処理は除きます)。

"Webサーバ"と"Webサーバ"?

すべてのWebページはHTMLを用いてあなたのブラウザに送信されます。HTMLとはWebページの内容や構造を記述するために使われる、ブラウザ用の言語です。そしてそのHTMLをブラウザへ送るアプリケーションがWebサーバなのです。ただ紛らわしいことに、このアプリケーションを内臓するコンピュータも通常Webサーバと呼ばれています。 とにかく重要なのは「すべてのWebアプリケーションはブラウザにHTMLを送る」ということです。アプリケーションのロジックがどんなに複雑でも、Webアプリケーションの最終目的はブラウザにHTMLを送ることなのです(JSONCSSファイルのような異なるタイプのデータも送れますが、ここでは割愛します。コンセプトは同じです)。 では、Webアプリケーションはブラウザに何を送るかをどうやって判断しているのでしょうか。実はWebアプリケーションは、ブラウザに要求されたものすべてを送信するのです。

HTTP

ブラウザは、HTTPプロトコル(プログラミングの世界でのプロトコルとは既知の2者間通信を可能にするデータフォーマットとシーケンスのこと)を使って、Webサーバ(または「アプリケーションサーバ」) からWebサイトをダウンロードします。HTTPプロトコルrequest-response型をベースにしています。クライアント(あなたのブラウザ)が、物理的なコンピュータ上で稼動しているWebアプリケーションにデータを要求すると、Webアプリケーションはその要求に応答してブラウザが要求したデータを返します。

ここで重要なのは、この通信はクライアント(あなたのブラウザ)からしか始められないという点です。サーバ(Webサーバです)側から接続を開始したり、あなたのブラウザに不要なデータを送りつけたりすることは不可能です。もしWebサーバからデータを受信したとすれば、それはあなたのブラウザがそのデータを要求したからに他ならないのです。

HTTPメソッド

HTTPプロトコルのメッセージはすべて、それに相当するメソッド(動詞の形をとる)を持っています。各HTTPメソッドが、クライアントから送られてくるロジックの異なるリクエストに対応していて、クライアントサイドのさまざまな要求に応えてくれるのです。例えば、WebページのHTMLを要求することと、フォームを送信することは論理上異なるアクションととらえられます。従ってこの2つのアクションは、それぞれ異なるメソッドが使われるのです。

HTTP GET

GETメソッドとは、その言葉の示すとおり、Webサーバからデータを「得る(要求する)」メソッドです。GETリクエストは、HTTPリクエストの中でも間違いなく最もよく使われるリクエストでしょう。GETリクエストを受信したWebアプリケーションは、要求されたページのHTMLを返す以外のことはしません。つまりWebアプリケーションは、GETリクエストに応答しますが、アプリケーションの状態は変更しないのです(例えば、GETリクエストをもとに新規ユーザアカウントを作成するようなことはしません)。Webサイトの提供元であるアプリケーションを変更しないという点から、GETリクエストは通常「安全」なメソッドだと考えられています。

HTTP POST

当然ながらWebサイトは、単にページを眺めるだけのものではありません。入力フォームを用いてアプリケーションにデータを送信することもできます。そのためには、POSTと呼ばれるタイプのリクエストが必要です。POSTリクエストによってユーザが入力したデータがサーバに転送され、その結果Webアプリケーション側で何らかの処理が行われます。入力フォームに情報を入力してWebサイトにユーザ登録するというのは、フォームに含まれるデータがWebアプリケーションへPOSTされることで実現するのです。

GETリクエストと異なり、POSTリクエストは通常アプリケーション状態の変更を伴います。先ほどの例でいうと、フォームがPOSTされると新規ユーザアカウントが作成されます。またGETリクエストと違い、POSTリクエストでは必ずしも新しいHTMLページがクライアントに送信されるわけではありません。その代わりに、クライアントはレスポンスのステータスコードを使って、アプリケーションへの処理が成功したかどうかを判断します。

HTTPステータスコード

通常、Webサーバはステータスコード200を返します。これは「あなたの要求を実行しました。結果は成功です」という意味です。ステータスコードは常に3桁の数字で、Webアプリケーションは、要求に対して結果がどうなったかを示すステータスコードをレスポンスごとに返さなくてはなりません。「OK」を意味するステータスコード200は、GETリクエストに対して最も多く返されるコードです。一方、POSTリクエストに対してはステータスコード204(コンテンツなし)が返される場合があります。このコードは「要求は実行しましたが、提示するものが何もありません」という意味です。

ここで理解してほしいのは、POSTリクエストは、データを送信したページとは異なるURLへ送られるケースもあるということです。先ほどのユーザ登録の例で説明すると、入力フォームのあるwww.foo.com/signupでsubmit処理を行ったとしても、入力フォームの内容を保持したPOSTリクエストはwww.foo.com/process_signupにPOSTされる可能性があるということです。POSTリクエストの送り先は、入力フォームのHTMLに記述されています。

Webアプリケーション

HTTPのGETとPOSTが使えれば、ほとんどの処理が行えるようになります。なにしろ最もよく使われるHTTPメソッドの2トップですからね。では次にWebアプリケーションについてですが、これはHTTPリクエストを受け、HTTPレスポンスを返す役割を担っています。通常返す内容には、要求されたページのHTMLが含まれています。POSTリクエストはWebアプリケーションに何らかのアクションを起こさせます。例えば、データベースへの新規レコードの追加などです。他にも多くのHTTPメソッドがありますが、ここではGETとPOSTに焦点を絞りましょう。 最も単純なWebアプリケーションとはどんなものでしょうか。ここで、ポート80(よく使われるHTTPポート番号で、ほぼすべてのHTTPトラフィックがこのポートに送信されます)をリッスンするだけのアプリケーションを実装してみましょう。接続が確立したら、クライアントからのリクエストを待ち、リクエストを受け取ったらごく単純なHTMLを返します。 以下のようになります。

import socket

HOST = ''
PORT = 80
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
connection, address = listen_socket.accept()
request = connection.recv(1024)
connection.sendall("""HTTP/1.1 200 OK
Content-type: text/html


<html>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>""")
connection.close()

(うまくいかない場合は、PORTを8080などに変えてみてください) このプログラムでは、接続もリクエストも1つしか扱いません。どんなURLがリクエストされても、HTTP 200ステータスコードを返します(ですので実際はWebサーバとは言えません)。Content-type: text/htmlの行は、ヘッダフィールドを表しています。ヘッダには、リクエストやレスポンスに関するメタ情報が含まれています。上記の場合、送信するデータはHTMLである(実際にはJSONなのですが)という情報をクライアントに伝えています。

リクエストの構造

上記のコードで、テスト用のHTTPリクエストを送っている箇所を見てください。レスポンスとよく似ていますよね。リクエストの1行目には<HTTP Method> <URL> <HTTP version>が指定されます。今回だとGET / HTTP/1.1がそれに当たります。2行目以降には、Accept: */*(どんなタイプのコンテンツでも受け取りますという意味)のようなヘッダが記述されます。ざっくりですが、リクエストはそういう構造です。 一方のWebサーバからのレスポンスを見ると、リクエストの1行目とよく似ています。フォーマットは<HTTP version> <HTTP Status-Code> <Status-Code Reason-Phrase>で、今回は HTTP/1.1 200 OK となっています。それに続くヘッダのフォーマットはリクエストと同じです。最後に、レスポンスの具体的なコンテンツが含まれます。コンテンツは文字列やバイナリオブジェクトに変換されることを覚えておいてください(ファイル指定の場合)。レスポンスの変換方法は、ヘッダ内のContent-typeでクライアントに伝えられます。

Webサーバの負荷

上記のコーディングをベースにしてWebアプリケーションを構築しようとすれば、多くの問題に直面するでしょう。

  1. リクエストされたURLの内容をチェックして、適切なページをクライアントに送るにはどうしたらよいか
  2. 単純なGETリクエストはよいとして、POSTリクエストをどのように扱うのか
  3. セッションやCookieなどの高度な概念をどのように扱うのか
  4. 大量の同時アクセスを処理するために、アプリケーションの拡張性をどの程度見積もるか

Webアプリケーションを構築する度に、こうした問題に煩わされるのは避けたいですよね。だからこそパッケージが存在するのです。パッケージはHTTPプロトコルの核となる詳細部分に対処し、上記の問題をすっきり解決してくれます。とはいえ、その基本機能は、私たちが先ほど作ったサンプルとほとんど変わりません。つまり、リクエストを受け取りHTMLを含んだHTTPレスポンスを返すのが、パッケージの主な仕事です。 ただしこの説明は、"クライアントサイド"のWebフレームワークについては全くあてはまりませんので注意してください。

課題の2トップ: ルーティングとテンプレート

Webアプリケーションを構築するとき、特に避けて通れないのが次の2つです。

  1. どうやってクライアントがリクエストしたURLを適切なアプリケーションにマップするか
  2. どうやって計算結果やデータベースから取得した情報を使い、リクエストされたHTMLを生成するか

どのWebフレームワークも何らかの手法でこれらの問題に対処していますが、そのアプローチはさまざまです。実例を挙げると分かりやすいので、これからDjangoとFlaskがとっている解決策を紹介しようと思います。ただその前に、MVCモデルについて簡単な説明が必要かもしれませんね。

DjangoでのMVC

DjangoMVCパターンを利用しているため、このフレームワークを採用する場合はコードもそれに対応する必要があります。MVCとは「モデル・ビュー・コントローラ」の頭文字をとったもので、アプリケーションそれぞれの持つ機能範囲を論理的に分離する手法です。データベーステーブルなどのリソースはモデルが受け持ちます(Pythonでのclassがリアルワールドオブジェクトを扱うのに似ています)。コントローラはアプリケーションのビジネスロジックを担当し、モデルの上層で機能します。ビューはすべての必要な情報にもとづいて、ページのHTMLを動的に生成します。

少しややこしいのですが、Djangoでは、コントローラがビューと呼ばれ、ビューがテンプレートと呼ばれています。しかし名前のつけ方を除けば、DjangoMVCアーキテクチャをそのまま採用していると言っていいでしょう。

Djangoでのルーティング

ルーティングとは、リクエストされたURLを、それに紐づいたHTMLを生成するコードに割り当てるプロセスのことです。特にシンプルなケースでは、同一コードですべてのリクエストを処理します(先ほどの例もそうです)。もう少し複雑なケースでは、すべてのURLを1対1でview functionに割り当てることもあります。例えば、www.foo.com/barというURLがリクエストされたら、handle_bar()という関数にレスポンスを生成する役割を持たせる、ということをどこかに記述しておくのです。このように、アプリケーションがサポートするすべてのURLを、それと紐づく関数とともに列挙して、マッピングテーブルを組み立てていきます。

ただしURLがリソースのIDなど有用なデータを含んでいる場合(例えばwww.foo.com/users/3/のような場合)、このアプローチは通用しません。こうしたURLをビュー関数に割り当てて、IDが3であるユーザを表示したい場合はどうすればいいのでしょうか。

Djangoでは、正規表現を含むURLを引数を持ったビュー関数に割り当てて、この問題を解決しています。例えば^/users/(?P<id>\d+)/$にマッチするURLは、正規表現のグループidでキャプチャした値を引数idに代入し、display_user(id) を呼ぶのです。こうすれば、/users/<some number>/というURLがどんなIDを含んでいても、必ずdisplay_user関数に割り当てられます。これらの正規表現は任意の複素数を扱うことができ、キーワードと位置パラメータの両方を持つことができます。

Flaskでのルーティング

Flaskのアプローチは少し違います。一般的に、リクエストされたURLと関数を結びつけるにはroute()デコレータを用います。次のFlaskのコードは、前述の正規表現や関数と同様の機能を果たします。

@app.route('/users/<id:int>/')
def display_user(id):
    # ...

ご覧の通り、デコレータは非常に単純化された正規表現の形式を使って、URLと引数をマップしています(セパレータには暗黙的に/を使用)。route()関数に引き渡すURLに<name:type>の形式で記述して、引数をキャプチャします。ここまでくれば、/info/about_us.htmlのような静的URLへのルーティングがどうなるか分かりますよね。ご想像のとおり、@app.route('/info/about_us.html')のように処理されます。

テンプレートを使ったHTML生成

引き続き、先に挙げた例題をもとに考えていきます。適切なコードを正しいURLに割り当てて動的にHTMLを生成すると同時に、Webデザイナーが画面を手直しできる余地を残すには、一体どうすればいいのでしょう。DjangoとFlaskはどちらもHTMLテンプレートを用いています。

HTMLテンプレートはstr.format()関数を使う方法と似ています。つまり、動的な値に対応するプレースホルダーを使って、期待されたアウトプットを出力するのです。プレースホルダーは、後からstr.format()へ渡される引数によって書き換えられます。Webページ全体を1つの長い文字列として記述し、ブレースを使って動的データをマークしておいて、最後にstr.format()を呼ぶ、という流れです。DjangoテンプレートとFlaskのテンプレートエンジンjinja2は、両方ともこうした使い方を想定して設計されています。

もちろん両者には違いもあります。Djangoにはテンプレートを使って実装するための基本的なサポート機能がついていますが、Jinja2では基本的に自分で任意のコードを実行する必要があります(まあ、すべてではありませんが)。また、Jinja2はレンダリングテンプレートの結果を積極的にキャッシュするため、同じ引数を持つ後続のリクエストは、再度レンダリングされるのではなく、キャッシュから返されます。

データベースインタラクション

基本理念に「至れり尽くせり」を掲げるDjangoには、ORM(O/Rマッパー)も用意されています。ORMの目的は2つ。1つはPythonのクラスをデータベーステーブルに割り当ること、もう1つはさまざまなデータベースエンジンの差異を取り除くことです(前者が本来の役割ですが)。ORMを好きだという人は珍しいですが(ドメイン同士のマッピングが完ぺきではないからでしょう)、Djangoはとにかく「フル装備」が売りなので大目に見られています。一方のFlaskには「マイクロフレームワーク」を名乗るだけあって、ORMは用意されていません(それでもFlaskはSQLAlchemyとの互換性が高いので、DjangoのORMに対抗できる唯一で最大のライバルです)。

ORMが搭載されているおかげで、Djangoを使えばフル機能のCRUDアプリケーションを構築することができます。CRUD(Create・Read・Update・Delete)アプリケーションはWebフレームワークのキモと言えます(ただしサーバサイドに限る)。Django(と、FlaskとSQLAlchemyのコンビ)は、いろいろなCRUD処理を各々のモデルに沿った形で作成します。

Webフレームワークのまとめ

さて、これでWebフレームワークの目的を分かってもらえたと思います。HTTPリクエストやレスポンスを処理するひな型やインフラのコードを開発者の目から隠すことです。どの程度隠すかはフレームワークによりますが、DjangoとFlaskは両極端な例だと言えるでしょう。Djangoはうっとうしいと思えるほどに、すべての状況を想定して作られています。Flaskは自分で「マイクロフレームワーク」だと宣言しているとおり、Webアプリケーションとして最小限の機能を提供し、汎用度の低いWebフレームワークのタスクについてはサードパーティーのパッケージに任せる方式をとっています。

とはいえ繰り返しになりますが、PythonのWebフレームワークがすることは最終的にはみんな同じです。HTTPリクエストを受け取り、HTMLを生成するコードを特定し、その内容をもとにHTTPレスポンスを生成します。実際、メジャーなサーバサイドのフレームワークはすべて、このような動きをしています(JavaScriptフレームワークは除く)。これでWebフレームワークの役割が分かったと思います。どうか自分の目的にあったフレームワークを選んでくださいね。