目次
Ajax(Asynchronous Javascript + XML)とは、Javascriptを用いたサーバとの非同期通信のことで、何が非同期なのかというとHTTPプロトコルを使うとサーバの応答を待ってページが再ロードされるが、それを待たない、という意味で非同期なのである。
非同期の処理を行う場合、気を付けないとならないのは、「その処理はすぐには実行されないよ」という点である。
例えば、async()という命令が非同期実行の命令だとして、
async(function(){ // 非同期でasync()して終わったら関数function()を実行してね!
// 処理A
});
// 処理B
というコード(疑似コード)があったとしよう。
この場合(おそらくは)「処理A」より「処理B」が先に実行される。
なぜなら、関数async()は非同期処理を開始するとすぐに戻ってくるからである。その処理の終了を待たないのである。 したがって、すぐに次の処理にうつり、結果として処理Bが実行される。 処理Aは、非同期処理が終わった後で関数function()の中で初めて実行されるのである。
このことは、非同期処理を行う機能を別な関数として実装している場合などに(慣れないうちは)よく見落とされる。
例えば、非同期にサーバ上のデータベースからデータを読み込む処理readData()と、読み込んだデータを画面にリスト表示する処理listData()があったとして、以下のコードは間違いである。
readData(); listData();
一見すると、データを読み込んでからリスト表示しているように見えるが、readData()は非同期処理であるので、この関数が呼び出されて戻ってきたとしても、まだデータは読みだされてはいないのである。単に読み出し処理が開始されたに過ぎない。 したがって、すぐ直後にlistData()を呼び出しても、その時にはまだデータは読み込まれておらず、結果として何も表示されないかエラーになるだろう。
このコードの正しい実装は、readData()の中の終了したときの関数の中でlistData()を呼び出すことである。
HTML5アプリにおけるブラウザとサーバとの通信関係は次の図のようになる。
Javascriptはウェブサーバを介さず、直接サーバ上のPHPコードと通信し、その結果を得る。(例えばデータベースのデータを得る)
この処理は非同期で行われ、Javascript側は通信が終わるのを待たない。
通信が終わったときは、あらかじめ準備してあった関数が呼び出されることで、Javascript側で通信が終わったことを知ることができ、それに応じた処理を行うことができる。
JavascriptでAjax通信を行うためにはXMLHttpRequestというものを使う。
が、このオブジェクトがブラウザによって取得方法が異なり、いわゆるクロスブラウザ問題の1つを引き起こした。
この違いを吸収するため、jQueryが使えるときはjQueryのAjax用の関数を使うのが良い。
$.ajax({
url : サーバURL,
type : メソッド名,
data : 与えるデータ,
dataType : 戻りデータのタイプ,
timeout : 待ち時間[ms],
success : 成功時の処理,
error : 失敗時の処理
});
jQueryのセレクタで得られる要素は「jQuery要素」と呼ばれる。この要素に対して、jQueryの強力な機能が使用できる。
一方、jQuery関数で取り出される要素は、一般にはDOM要素である。DOM要素とはHTMLの文書を構成している要素のことである。
これら2つの要素は、それぞれ以下のようにして相互に変換できる。
$(DOM要素) -> jQueury要素 jQuery要素[0] -> DOM要素
jQueryが持つ機能は強力なものが多いが、その分、無駄なことをしている場合もある。 その点、DOMの機能は(それでも十分に強力なのだが)シンプルな場合が多い。
使用する状況や目的に応じて使い分けるのが良いと思う。
Ajaxでデータベースにアクセスするとき、任意のクエリを実行してくれるPHPコードがあればとても便利である。
ただ、このようなコードは便利さとは裏腹の危険性をはらんでおり、実際のアプリにおいては使用するべきではない。
ここでは、データベースへのより安全なアクセスについては次で対応するとして、とりあえず任意のクエリを実行できるPHPコードを使ってみよう。
それは、次のコードである。
try {
$query = $_POST['query'];
$pdo = new PDO("mysql:host=HOST; dbname=DBNAME; charset=utf8", "username", "password");
$stmt = $pdo->prepare($query);
$stmt->execute();
header('Content-type: application/json; charset=utf-8');
print json_encode($stmt->fetchAll());
} catch (PDOException $e) {
var_dump($e->getMessage());
}
$pdo = null;このコードは、実行されるとPOSTメソッドで渡されてきた"query"というデータを実行し、その結果をJSON形式で返す。
$_POSTグローバル変数に直接アクセスしていたり、エスケープ処理やクォート処理を全くしていないという点で危険極まりないコードだが、クエリを試すにはとても便利である。
Ajaxでこのコードにアクセスするには、例えば次のようにする。
// クエリ発行
function query(q, sf) {
$.ajax({
url: 'http://URL/query.php',
type: 'POST',
dataType: 'json',
data: {query: q},
timeout: 5000,
success: sf,
error: function () {
alert("Error in query()");
}
});
}
query("select * from member;",function(json){
alert("成功: 結果="+json);
});
データベースアプリケーションは、外部の攻撃者から狙われている。
SQLインジェクションへの対策が必要。
基本的に、MySQLに渡すSQL文と、その中で与える引数(データ)をと分離する方式。
こうすることで、データに対して自動的にエスケープ処理や引用符処理が施されるので、SQLインジェクションは原則的に発生しない。
もう一つの利用法として、同じSQL文を何度も使う場合、最初に1回だけ準備して、データを変えつつ繰り返すことで利用効率を高めるという利点もある。
HTMLとJavascriptとCSSとの役割をうまく説明しているページが以下にある。
これを読むとわかるが、CSSを使うには画面の見た目を制御するためである。
HTML5のアプリケーションはブラウザの画面を1画面として、HTTPによる画面遷移を行わず、Ajaxだけで画面を更新する形で作られることが多い。
そのためにも、画面構成はCSSで指定しておくことが求められる。
セレクタとは、画面を構成している要素を限定するものである。
jQueryでもセレクタを使用するが、これはもともとCSSのセレクタがベースになっている。
例えば、
p { color:red; }
と書くと、文書中のすべての<p>...</p>で囲まれた文字が赤色になる。
p#aaa { color:red; }
と書くと、<p id="aaa">...</p>で囲まれた文字は赤くなるが、その他の<p>...</p>の文字は変化しない。
p.aaa { color:red; }
と書くと、<p class="aaa">...</p>で囲まれた文字は赤くなるが、その他の<p>...</p>の文字は変化しない。
これらの例の中で、最初に書かれている文字はセレクタ、中カッコ { } の間の文字は属性と呼ばれる。
属性は、
属性名:属性値;
の形で表され、複数の属性をつなげて指定することができる。例えば、
p {
color:red;
background-color:blue;
margin:10px;
padding:5px;
}
のように書くことができる。改行は任意だが、見やすいようにした方が良い。
Javascriptはオブジェクト指向言語として使うことができる。
というか、一応、オブジェクト指向の考え方をサポートしている。
ただし、一般的なクラスベースのオブジェクト指向ではなく、プロトタイプベースのオブジェクト指向と呼ばれるものである。
Javascriptのクラスは関数である。
まずこれがなかなかなじめないが、そういうものである。
つまり、会員クラスとしてMemberを定義する場合、次のようにする。
var Member=function(){};
文法的には単なる関数オブジェクトMemberを宣言しただけであるが、これでMemberというクラスが定義されたことになる。
ただし、このままでは中に何も入っていないクラスなので、例えばIDと氏名、年齢、メールアドレスを入れてみると、次のようになる。
var Member=function(){
this.id;
this.name;
this.age;
this.mail;
};
ここで定義されているクラスMemberは、実は関数であり、それはコンストラクタと呼ばれるクラスインスタンスの生成メソッドである。
Javascriptでは、関数のオーバーロードはサポートされていないので、コンストラクタは1つだけしか持てない。
例えば、ID、氏名、年齢、メールアドレスを与えて会員を生成するようにするには、次のようにする。
var Member=function(i,n,a,m){
this.id=i;
this.name=n;
this.age=a;
this.mail=m;
};
ちなみに、Javascriptでは、クラス内のthis.は省略できない。JavaやC++に慣れている人にとってはとても煩わしいが、仕方がない。
さてこれで、次のようにして会員「山田太郎」を生成できる。
var taro=new Member(1,"山田太郎",55,"taro@aaaa.jp");
Javascriptにおいては、関数もオブジェクトである。したがって変数に入れることができ、
var func=function(){ ... };
のように書くことができた。
したがって、クラスのメソッドも同様にしてクラス内の変数として宣言する。
var Member=function(i,n,a,m){
this.id=i;
this.name=n;
this.age=a;
this.mail=m;
// 文字列化
this.toString=function(){
this.id+":"+
this.name+"("+this.age+") Mailto:"+
this.mail;
};
};
これを使うと、
taro.toString();
で太郎君の文字列化した表現を得ることができる。
しかし、この方法は1つ問題がある。
それは、会員を1人作るごとに、文字列化するメソッドも1つずつ作られてしまう、という点である。
一般的なオブジェクト指向言語においては、クラスのメンバー変数は各インスタンスごとに持つが、メソッドは共通で持つようになっている。 太郎君も次郎君も花子さんも、文字列化するときは全部同じ処理で済むからである。
しかし、上記のJavascriptのコードは、全く同じことをする関数が、それぞれのインスタンスごとに作成されるので、メモリの無駄遣いになってしまうのである。
これを回避するために、Javascriptでは「プロトタイプ」というものを使用する。
次のように書く。
var Member=function(i,n,a,m){
this.id=i;
this.name=n;
this.age=a;
this.mail=m;
};
// 文字列化
Member.prototype.toString=function(){
this.id+":"+
this.name+"("+this.age+") Mailto:"+
this.mail;
};
すべてのクラスはprototypeという名前の暗黙のメンバー変数(プロパティと呼ぶ)を持っており、もしメソッドが見つからない場合はこの変数の関数を探す、という決まりがあるのである。
実はこのprototypeというプロパティには、クラスのインスタンスを生成するために使ったひな形オブジェクト(=親オブジェクト)が格納されている。 したがって、自分を探しても見つからないときは親を探す、というオブジェクト指向ではごく当たり前の仕組みが実現できている、というわけなのである。
さらにこの仕組みは、クラスの継承関係を実現するうえでも便利に使えるのだが、ここでは、
Javascriptでは、メソッドはプロトタイプに宣言する
と覚えておこう。
Ajaxを使った会員管理システムを構築する。
<!DOCTYPE html>
<html>
<head>
<title>会員管理システム</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="js/libs/jquery/jquery.js"></script>
<script type="text/javascript" src="js/my.js"></script>
<link type="text/css" rel="stylesheet" href="style/my.css">
</head>
<body>
<div id="header">
<p id="title">会員管理システム</p>
<p id="last_modified">最終更新日:2015年9月12日</p>
</div>
<div id="list">
<table id="member_list">
<tr>
<th>ID</th><th>氏名</th><th>年齢</th><th>メール</th>
</tr>
</table>
</div>
<div id="body">
<div id="member_edit">
<form>
<p id="member_id">ID:</p>
<p>氏名:<input id="member_name" type="text" size="20"/></p>
<p>年齢:<input id="member_age" type="text" size="3"/></p>
<p>メール:<input id="member_mail" type="text" size="30"/></p>
<p><input id="bt_regist" type="button" value="登録"/>
<input type="reset" value="リセット"/></p>
</form>
<div id="debug"></div>
</div>
</div>
</body>
</html>
html,body{
margin:0px;
width:100%;
height:100%;
}
#header{
position:absolute;
color:#ff0000;
background-color:#ccccff;
width:100%;
height:60px;
}
#title{
position: absolute;
top:4px;
left:4px;
font-size: 32px;
margin:0px;
color:#000055;
}
#last_modified{
position: absolute;
bottom:4px;
right:4px;
font-size: 14px;
}
#list{
position:absolute;
top:60px;
bottom:0px;
left:0px;
right:auto;
width:50%;
overflow: auto;
}
#member_list{
margin:10px;
border: solid 1px;
width:90%;
}
#member_list th{
border:solid 1px;
}
#member_list td{
border:solid 1px;
}
#member_list tr.ind:hover{
background-color:#cccccc;
}
#member_list tr.ind_selected{
background-color: #ff0000;
color:white;
}
#body{
position:absolute;
top:60px;
bottom:0px;
left:auto;
right:0px;
width:50%;
overflow: auto;
}
#member_edit{
margin:10px;
}
var selected_member_tr=null; // 選択されている行オブジェクト
$(function () {
// 任意のクエリの実行
// q : 実行するクエリ文字列
// sf: 成功時に実行される関数
function query(q, sf) {
$.ajax({
url: "http://seminar-junior.jeez.jp/fuchida/MemberManager/query.php",
type: "POST",
dataType: "json",
data: {query: q},
timeout: 5000,
success: sf,
error: function () {
alert("クエリ失敗");
}
});
}
// 全員のデータを持ってくる
function listAllMembers() {
$('#member_list tr:has(td)').remove();
var q = "select * from member order by id;";
query(q, function (data) {
var tbl=$('#member_list');
for (var i = 0; i < data.length; i++) {
var id = data[i].id;
var name = data[i].name;
var age = data[i].age;
var mail = data[i].mail;
var h="<tr class='ind'><td>"+id+"</td><td>"+
name+"</td><td>"+
age+"</td><td>"+
mail+"</td></tr>";
tbl.append(h);
}
});
}
// 登録処理
$('#bt_regist').click(function(){
var n=$('#member_name').val();
var a=$('#member_age').val();
var m=$('#member_mail').val();
if(n=="" || a=="" || m==""){
alert("正しく入力してください。");
return;
}
var q="insert into member (name,age,mail) values ('"+
n+"','"+a+"','"+m+"');";
query(q,function(data){
listAllMembers();
});
});
// 選択処理
$('#member_list').on('click','tr.ind',function(){
if(selected_member_tr!=null){
$(selected_member_tr).addClass('ind')
.removeClass('ind_selected');
}
selected_member_tr=this;
$(selected_member_tr).addClass('ind_selected')
.removeClass('ind');
var i=selected_member_tr.childNodes[0].innerHTML;
var n=selected_member_tr.childNodes[1].innerHTML;
var a=selected_member_tr.childNodes[2].innerHTML;
var m=selected_member_tr.childNodes[3].innerHTML;
$('#member_id').text("ID:"+i);
$('#member_name').val(n);
$('#member_age').val(a);
$('#member_mail').val(m);
});
// 起動時の処理
listAllMembers();
});
<?php
try {
$query = $_POST['query'];
$pdo = new PDO("mysql:host=mysql514.db.sakura.ne.jp; dbname=forcreate_fuchida; charset=utf8", "forcreate", "junior2015");
$stmt = $pdo->prepare($query);
$stmt->execute();
header('Content-type: application/json; charset=utf-8');
print json_encode($stmt->fetchAll());
} catch (PDOException $e) {
var_dump($e->getMessage());
}
$pdo = null;