旧gaaamiiのブログ

間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

SinatraとjQueryでおよそ100行で作るAjax掲示板アプリケーション

Web2.0と言われ始めた時代に、Ajaxという手法が広まりました。今ではAjaxを使っていないウェブアプリケーションを探すのが難しいくらい、Web開発の現場には浸透していると思います。もちろん、Ajaxを知らなくてもウェブアプリケーションは作れますが、これは使い心地を意識すれば自然と必要になってくるものです。

この記事では、実際に簡素な掲示板アプリを作りながら、現在ほとんどのウェブアプリケーションで使われているAjaxという手法を紹介していきます。ちなみに、タイトルの「100行で」というのは今回書いたapp.rb、index.erb、schema.sqlの3つのファイルのトータルがおよそ100行だという意味です。数字をタイトルに入れると読んでもらいやすいという話をどこかで聞いたので入れてみました。

ソースコードGithubにも置いておきました。

対象者としては「Ajaxって言葉は耳にするけど、どういうものかいまいちわからない」という方を想定しています。すでにウェブアプリの作法がわかっているような方には物足りないかもしれません。また、わかりやすさを優先しているため大雑把な説明が多くなっています。AjaxJavaScript周りの正確な説明を必要とする方には、「入門 モダンJavaScript 」という本がおすすめです。

できるだけ役立つ記事にしたいので、明らかにおかしい説明からささいな打ち間違いまで、なんでもご指摘ください。ブコメでもTwitter(@gaaamii)でもけっこうです。

目次

  1. Ajaxとは?
  2. 作ってみよう
    • 完成品
    • 環境
    • ファイル構成
    • ルーティング
    • DB設計
    • ビュー
    • jQueryAjax
  3. まとめ

Ajaxとは?

Ajaxという名前が付いているとプログラミング言語のようなものを想像してしまいますが、それは違います。Ajaxはあくまでもウェブアプリケーションにおいてサーバーと通信してデータを表示する際の手法の一つです。

噛み砕いて表現すると、Ajaxとは「ページの読み込みを減らす工夫」です。

使う場面

なぜAjaxを使うのか。これは多くの場合、アプリケーションの使い心地を高めるためです。ウェブアプリケーションはざっくり言うと「サーバーと通信してデータを取得してHTMLに表示する」仕組みと言えますが、サーバーから毎回全てのHTMLをダウンロードするのは無駄が多くなります。今見ているレイアウトのまま、新しいデータを表示したり、自分が送信したデータをすぐに反映させたりできたら便利ではないでしょうか。

これを実現するのがAjaxなのです。

大手ウェブサービスに使われているAjax

実例を見てみましょう。

などなど。

探してみればたいていのウェブサービスAjaxが使われています。 どれもなんとなく使っているものですが、逆に言うとなんとなくでも使えるような快適さを提供するための工夫としてAjaxという手法が使われていることがわかります。

作ってみよう

では、実際に掲示板アプリを作りながらAjaxに触れていきます。

完成品

完成品は、このようになります。簡単な掲示板アプリです。

テキストを入力して送信すると、ページを読み込み直すことなくそのテキストがすぐにビューに反映されます。これと全く同じことをjQueryだけで実装することもできますが、今回作るアプリでは、

サーバーにテキストを送信 -> データベースに保存 -> データベースからそのテキストを取得 -> ブラウザに表示

というところまでを(ページを読み込み直すことなく)行っています。これがAjax的な動作です。

環境

RubyフレームワークSinatraと、JavaScriptのライブラリjQueryを使ってアプリケーションを作っていきます。

ファイル構成

  • ajax_bbs
    • app.rb
    • views
      • index.erb
    • db
      • schema.sql
      • bbs.db
    • public

ルーティング

Sinatraではルーティングにgetメソッド、postメソッドを使います。メソッド名がhttpのそれと同じなのでとてもわかりやすいですね。

require 'sinatra'

get '/' do
  # コメント一覧を取得
  erb :index
end

post '/comment' do
  # コメントをDBに保存する
end

get '/comments/last' do
  # 最新のコメントをクライアントに返す
end

データベースが絡んでくる諸々は置いといて、ビューを先に作りましょう。

ビュー

ビューは、とりあえずこんな感じにしましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>掲示板アプリでAjax</title>
  <script type="text/javascript" src="/js/jquery-1.11.0.min.js"></script>
</head>
<body style="width:60%;margin:0 auto;text-align:center;">
  <h1>掲示板アプリでAjax</h1>
  <div class="comment-form">
    <input type="text" id="user-name” placeholder="名無し” /><br />
    <textarea id="comment-body" /></textarea><br />
    <input id="submit-comment" type="button" value="コメントする" />
  </div>
  <div class="comment-view">
    <div id="comments">
    </div>
  </div>
</body>

今回はAjaxがメインなのでCSSをしっかり整えたりはしないのですが、「デザインがキモいままだとやる気出ない」という方はしっかりレイアウトしてもいいかもしれません。

このビューを構成しているのはフォーム部分(comment-form)とコメントの表示部分(comment-view)です。

DB設計

掲示板アプリなので、データベースを使います。schema.sqlというファイルをdbディレクトリの下で作り、以下のように書きます。

create table comments(
  id integer primary key not null,
  user_name text,
  body text,
  created_at
);

見ての通りコメント本体とユーザーの名前、投稿日時の保存を期待しています。このスキーマをもとに実際にテーブルを作るには、データベースのファイル(bbs.db)を作成・指定して.readでsql文が書かれたファイル(schema.sql)を読み込んであげればOKです。

$ sqlite3 bbs.db
sqlite> .read schema.sql

次に、app.rbの方でActiveRecordの設定をします。

ActiveRecord::Base.establish_connection(
  "adapter" => "sqlite3",
  "database" => "./db/bbs.db"
)

class Comment < ActiveRecord::Base
end

after do
  ActiveRecord::Base.connection.close
end

これでDBの準備ができたので、Comment.メソッドという形でデータベースのデータにアクセスできるようになりました。インデックスページへアクセスした時にコメント一覧を返すようにします。

get '/' do
  @comments = Comment.order('id desc')
  index :erb
end

orderメソッドで、idの降順でデータを取ってきています。掲示板らしく、最新のコメントから表示するためにこうしています。

jQueryAjax

さて、ようやくAjaxです。Ajaxはサーバーと通信してクライアントにデータを表示する際の手法の一つなので、サーバーサイドとクライアントサイドのプログラミングを一緒に見ていきます。

必要なgemを読み込む
require 'sqlite3'
require 'active_record'
require 'json'
コメントをDBに保存する

今回はちょっと手抜きをしてindex.erbのbodyの閉じタグの直前に書いてしまいます。

<script type="text/javascript">
    $(function(){
      $("#submit-comment").click(function(){
        var comment = $("#comment-body").val();
        var userId = $("#user-id").val();
        if(userId.length == 0) userId = "名無し" ;
        var request = $.ajax({
          type: "POST",
          url: "/comment",
          data: {
            body: comment,
            user_id:  userId
          }
        });
      });
    });
</script>

送信ボタン(#submit-comment)をクリックすると、サーバーにリクエストが送られます。ご覧の通り、リクエスト部分の記述はこちらです。

var request = $.ajax({
  type: "POST",
  url: "/comment",
  data: {
    body: comment,
    user_id:  userId
  }
});

jQueryajax関数を使って、引数にjsonオブジェクトを渡すことでリクエストのオブジェクトを作り、送信しています。この辺は深く掘り下げませんが、ブラウザのコンソール画面やJavaScrpt,jQueryの本を参照して理解を深めておきたいですね。

そして、このリクエストが送られた時にサーバー側で実行される処理がこちらです。

post '/comment' do

  Comment.create({
    body: params[:body],
    user_id: params[:user_id]
  })
  
end

もらった値を、そのままデータベースに保存しています。これでbbs.dbにデータが書き込まれます。

最新のコメントをクライアントに返す

さて、ここが今回のチュートリアルの肝です。コメントをpostして、ページを読み込み直すことなくそれを反映します。

先ほどのリクエストが成功したら、最新コメント(つまり今まさにpostしたコメント)を返してもらうように要求します。そしてそれも成功したら、そのデータをHTMLに整形して表示します。

request.done(function(){
    $.ajax({
        type: "GET",
        url: "/comments/last”,
        dataType: “json”
    })
    .done(function(res){
        $("#comments").prepend('<p>' + escapeHTML(res.body) + ' by ' + escapeHTML(res.user_id) + ‘</p>');
    });
});

ここで、escapeHTML()という関数が使われています。これはコメントを表示する際にscriptタグなどを実行しないように自前で定義したエスケープのための関数です。

function escapeHTML(s){
  $('<div>').text(s).html();
}

これに対応するサーバー側の処理はこうなります。

get '/comments/last' do
  comment = Comment.last
  {comment_body: comment.body, user_id: comment.user_id}.to_json
end
まとめ

いかがでしたか。細かいセキュリティ面での配慮やデザインが不十分なので公開するにはもう少し作業が必要そうですが、今回のチュートリアルAjaxがどういうものかがなんとなく感じていただければ幸いです。

最初に書いたとおり、Githubにもソースコードを上げてあります。

また、説明のために多少前後したところがあったので、書いてきたコードを全てまとめて表示しておきます。

  • app.rb
require 'sinatra'
require 'sqlite3'
require 'active_record'
require 'json'

ActiveRecord::Base.establish_connection(
  "adapter" => "sqlite3",
  "database" => "./db/bbs.db"
)

after do
  ActiveRecord::Base.connection.close
end

class Comment < ActiveRecord::Base
end

get '/' do
  erb :index
end

post '/comment' do

  user_id = "名無し"

  if params[:user_id]
    user_id = params[:user_id]
  end

  Comment.create({
    body: params[:body],
    user_id: user_id
  })
end

get '/comments/last' do
  comment = Comment.last
  {comment_body: comment.body, user_id: comment.user_id}.to_json
end
  • index.erb
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>掲示板アプリでAjax</title>
  <script type="text/javascript" src="/js/jquery-1.11.0.min.js"></script>
</head>
<body style="width:60%;margin:0 auto;text-align:center;">
  <h1>掲示板アプリでAjax</h1>
  <div class="comment-form">
    <input type="text" id="user_id" placeholder="名無し" /><br />
    <textarea id="comment-body" /></textarea><br />
    <input id="submit-comment" type="button" value="コメントする" />
  </div>
  <div class="comment-view">
    <div id="comments">
      <% @comments.each do |comment| %>
        <p><%= escape_html "#{comment.body} by #{comment.user_id}" %></p>
      <% end %>
    </div>
  </div>
  <script type="text/javascript">
    function escapeHTML(s) {
      return $('<div>').text(s).html();
    }
    $(function(){
      $("#submit-comment").click(function(){
        var comment = $("#comment-body").val();
        var userId = $("#user_id").val();
        if(userId.length == 0) userId = "名無し" ;
        var request = $.ajax({
          type: "POST",
          url: "/comment",
          data: {
            body: comment,
            user_id:  userId
          }
        });
        request.done(function(msg){
          console.log(this);
          $.ajax({
            type: "GET",
            url: "/comments/last"
          })
          .done(function( res ){
            $("#comments").prepend('<p>' + escapeHTML(res.body + ' by ' + escapeHTML(res.user_id) + '</p>');
          });
        });
      });
    });
  </script>
</body>
</html>
create table comments(
  id integer primary key not null,
  user_id text,
  body text,
  created_at,
  updated_at
);

参考書籍