敢えて素のJavaScriptのみで実装してAjaxを理解する

#目的
新人時代に「Ajaxって何？」と質問し、あのGoogle Mapとかでグルグルって回るアレだよ」と説明され、てっきりフロント側だけの技術だと思っていたあの頃を思い出し、今後自分が説明するときには実物のコード込みで説明できるようになりたいなという目的。


#そもそもAjaxとは？
[こちら](https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9)の方がすごくわかりやすくまとめられているので、
本記事では省略します。

#作るもの
こんなものを作ってみます。
![faa13fc5f997b0ce79b6f214ebaf3f6b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/ad3cc6d7-6aae-0364-8529-a8a9f593f915.gif)

#Webサーバを立てる
まずは、Webサーバを立てます。
今回はPythonとFlaskで行きます。

まずはflaskの機能を使い、/search宛にリクエストが来たらsearch.htmlを返すようにしておきます。
static_listは後ほど使います。

```python:app.py
from flask import *

app = Flask(__name__)
static_list = ('arnold', 'abel', 'abraham', 'alex', 'alice', 'bob', 'becky', 'christian', 'catherine', 'david', 'daniel', 'jessica', 'jones', 'james', 'jack', 'jason', 'jonathan','mickey', 'mike', 'nathan','oscar')

@app.route('/search')
def return_search_page():
    return render_template('search.html')

if __name__ == '__main__':
    port = int(5000)
    app.run(host="0.0.0.0", port=port)

```

上記で/searchと対応付けていたsearch.htmlはこんな感じです。

```html:search.html
<form action="/search_result" name="search_form">
    <input type="text" name="searchkey" id="searchkeyholder"
     placeholder="username" >
</form>
```
見た目はこんな感じで、簡単なユーザの検索フォームです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/d2adf473-4781-7490-cc4c-abfc7ee4ca96.png)

今回は、この検索フォームに「a」と入力したら、aで始まるユーザを拾ってきて表示するようにしようと思います。
ここまでで、Ajaxは一切出てきていません。

#Ajaxに必要な技術
Ajaxを理解する上では、まず以下のことに着目する必要があります。

* そもそも、Webページにアクセスできるのは人（ブラウザ）だけじゃなくて、プログラムからもいけるんだぜ？
* JavaScriptではHTMLの各要素を自由にいじくれるんだぜ？

この2点が重要です。
つまり、雑な図ですがこういうことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/b45b301c-4811-f2cd-5bf8-acaf64265ab4.png)

まず、検索フォームがあります。これは毎回固定です。
次に、**空っぽの要素**を作っておきます。当然何も表示されません。
同時に、別のページを用意しておきます。これが、実際に表示したいコンテンツになります。今回でいうと、ユーザ名の一覧ですね。
そして、ページにはJavaScriptコードを仕込んでおきます。このコードを特定のタイミングで動かし、
**別に用意しておいたページにアクセスしてページのデータをまるごと取得**します。
最後に、**JavaScriptコードが先程あけておいた空っぽの要素に別ページの情報を流し込んで終わり**です。

ユーザからすると、裏で通信を行っているので画面遷移が発生しなかったように見えます。


#検索フォーム画面の調整
さて、検索フォーム画面を調整していきましょう。
検索フォームはもうありますので、次に「空っぽの要素」を用意しておきましょう。
あとで別のページを埋め込む用のスペースです。もちろん、見た目は変わりません。

```search.html
<form action="/search_result" name="search_form">
    <input type="text" name="searchkey" id="searchkeyholder"
     placeholder="username">
</form>

<div id="result">
<!-- 空っぽの要素 -->
</div>
```


さて、埋め込まれる側のページを用意しましょう。
一部python風のコードがありますが、これはテンプレートファイルと呼ばれており、
後から値を注入する仕組みです。今回は、usersというリスト（配列）の内部を一覧で表示する繰り返し処理になっています。

```html:search_result.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        {% for user in users %}    
        <li>
            {{user}}
        </li>
        {% endfor %}
    </ul>
</body>
</html>
```

これでHTMLが揃いました。最終的には、search_result.htmlがsearch.htmlの空要素部分に埋め込まれるイメージです。


#JavaScriptで裏の通信を書く
では、裏の通信を書いていきましょう。最初の検索画面の末尾に、以下のようにscript要素を足しています。

```html:search.html
<script>
    // XMLHttpRequestオブジェクトを生成
    var request = new XMLHttpRequest();

    //リクエストの状態が変化したときの挙動を定義
    request.onreadystatechange = function(){
        var result = document.getElementById('result')

        if(request.readyState == 4){ //通信完了時
            if(request.status == 200){ //通信の成功時
                result.innerHTML = request.responseText;
            }
        }else{
            result.innerHTML = "データ取得中...";
        }
    }

    //oninput時の挙動（リクエストを投げる）ための関数
    function makeRequest(){

        //入力された情報を取得
        searchkey = document.getElementById("searchkeyholder").value;

        //リクエスト作成
        //引数：HTTPメソッド, URL, 非同期通信をするかどうか
        request.open('GET', ('search_result?searchkey=' + encodeURIComponent(searchkey)), true)

        //リクエストを送信
        //引数：GETはパラメータをURLに含めているので引数はnull。POSTの場合はリクエスト内容
        request.send(null);        
    }
</script>
```


まず、裏でリクエストを投げてくれるオブジェクトXMLHttpRequestを生成しています。
次にリクエストの状態（通信が終わった、うまくいった等）に応じた処理を書いています。
色々書いてありますが、ポイントはまずココです。

**同じページ内のIDがresultの要素を取得し、別のページにアクセスしてから
別のページの内容をまるごとresult要素の値として書き換えています**。

```javascript
    //リクエストの状態が変化したときの挙動を定義
    request.onreadystatechange = function(){
        var result = document.getElementById('result') //ココと

        if(request.readyState == 4){ //通信完了時
            if(request.status == 200){ //通信の成功時
                result.innerHTML = request.responseText; //ココ
            }
        }else{
            result.innerHTML = "通信中...";
        }
    }
```

さて、長かったので関数に分けたのですが以下の部分も忘れてはいけませんね。
これは、「特定のタイミングで」動かしたい処理です。
特定のタイミングで検索フォームに入力した値を取得し、別のページ（search_result）に**こっそり要求を出して**います。

```javascript
    function makeRequest(){

        //入力された情報を取得
        searchkey = document.getElementById("searchkeyholder").value;

        //リクエスト作成
        //引数：HTTPメソッド, URL, 非同期通信をするかどうか
        request.open('GET', ('search_result?searchkey=' + encodeURIComponent(searchkey)), true)

        //リクエストを送信
        //引数：GETはパラメータをURLに含めているので引数はnull。POSTの場合はリクエスト内容
        request.send(null);   
    }
```


#サーバ側の調整
そして、サーバ側のコードの調整です。
今回はリクエストパラメータから情報を受け取り、それを元に自分が持っている名前の一覧から検索した結果を別のページとして生成し、それを返しています。この結果そのものが、最初の検索フォームページに埋め込まれるデータになります。

```python:app.py
from flask import *

app = Flask(__name__)
static_list = ('arnold', 'abel', 'abraham', 'alex', 'alice', 'bob', 'becky', 'christian', 'catherine', 'david', 'daniel', 'jessica', 'jones', 'james', 'jack', 'jason', 'jonathan','mickey', 'mike', 'nathan','oscar')



@app.route('/search')
def return_search_page():
    return render_template('search.html')

@app.route('/search_result')
def return_search_result_page():
    searchkey = request.args.get('searchkey')

    new_list = []

    if searchkey != None and searchkey != '':
        new_list = [name for name in static_list if name.startswith(searchkey)]

    print(new_list)
    return render_template('search_result.html', users=new_list)


if __name__ == '__main__':
    port = int(5000)
    app.run(host="0.0.0.0", port=port)
```


#検索フォームの最終調整
最後の調整です。

現在、検索フォームページには検索フォームそのものと空っぽの要素、それから裏で別のページをとってきてくれるJavaScriptコードがあります。さていつこのJavaScriptコードをですが、今回の要件で言うと**「フォームに文字が入力されたとき」**でしょう。
というわけで色々やり方はあると思いますが、検索フォームのinput要素にoninput属性を足し、関数を動かすようにしておきます。
これで、入力があったときにmakeRequest関数が毎回動いてくれます。

```html:search.html
<form action="/search_result" name="search_form">
    <input type="text" name="searchkey" id="searchkeyholder"
     placeholder="username" oninput="makeRequest()">
</form>

<div id="result">
<!-- space for ajax -->
</div>

<script>
    // XMLHttpRequestオブジェクトを生成
    var request = new XMLHttpRequest();

    //リクエストの状態が変化したときの挙動を定義
    request.onreadystatechange = function(){
        var result = document.getElementById('result')

        if(request.readyState == 4){ //通信完了時
            if(request.status == 200){ //通信の成功時
                result.innerHTML = request.responseText;
            }
        }else{
            result.innerHTML = "データ取得中...";
        }
    }

    //oninput時の挙動（リクエストを投げる）ための関数
    function makeRequest(){

        //入力された情報を取得
        searchkey = document.getElementById("searchkeyholder").value;

        //リクエスト作成
        //引数：HTTPメソッド, URL, 非同期通信をするかどうか
        request.open('GET', ('search_result?searchkey=' + encodeURIComponent(searchkey)), true)

        //リクエストを送信
        //引数：GETはパラメータをURLに含めているので引数はnull。POSTの場合はリクエスト内容
        request.send(null);        
    }


</script>
```


このようになりました。
リアルタイムに検索ができているようです！
ログを見てみると、入力のたびにリクエストがきているのがわかると思います。

![faa13fc5f997b0ce79b6f214ebaf3f6b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/ad3cc6d7-6aae-0364-8529-a8a9f593f915.gif)

#流れの整理
このような流れで動いていることがわかります。
1.検索ページでユーザがアクションを取る（今回は入力）
2.JavaScriptの関数がキックされる
3.関数が別のページのデータを読みにいく
4.関数は検索ページに用意しておいた空欄に、別ページのデータを埋め込む

これで、ユーザからは画面遷移を伴わない通信ができているように見えるはずです。


#まとめ
素のJavaScriptで書くとかなりごちゃごちゃするので、普段はjQueryやAngular等のライブラリを使うとスッキリ書けます。
ただ、裏で何が起こっているのかはわかりにくくなってしまうので、今回は敢えて原始的なコードを使うことで何が起こっているのかを見てみました。


#参考（jQuery使うと...）
先のコードにあった長いJavaScriptは、これくらいスッキリします。

```js

    $("#searchkeyholder").on("input", function(){
        $("#result").text("通信中...");
        $.ajax({
            url: "search_result",
            type: "GET",
            dataType: "html",
            data: $("form").serializeArray(),
            timeout: 10000
        })
        .done(function(data){
            $("#result").html(data)
        });
    });
```