JS 非同期 POST 〜非同期通信でのPOSTリクエスト〜

イントロダクション

前回KMLファイルがなんたら。。。と記載しましたが、結局のところはGoogle Mapの選択した位置にピンを落とす処理を追加しました。

現在の作成物はこちらのURL

https://zenryokuservice.com/sample/js/SampleMap.html

非同期通信処理(Ajax)

今回は、ここから非同期で記載した情報をサーバーに送信、JSONファイルを出力する処理を実装しようと思います。
早速ですが、非同期通信について触れておきます。

非同期通信とは

<同期通信>
HTMLフォームを使用して画面を遷移したり画面をリロードしたりするようなURLを更新するような通信のことを同期通信と言います。まぁ通常のリクエスト送信だと思っていただいて間違い無いと思います。

<非同期通信>
それに対して、非同期は何が違うのかというと画面の一部分を更新するときによく使います。
画面を表示したときに「loading ....」なんて表示されているのがこれです。
<同期通信の場合>
リクエスト送信した後に「レスポンス(次のページを受け取る)を待ちます。
<非同期通信>
リクエスト送信した後に「レスポンス」を待ちません。
なので「loading ...」なんて処理を行いながら「レスポンスが帰ってくる」のを待っています。

ポイント

レスポンスを待たなくて良いので次の処理が実行できるというわけです。「待ち」の時間が減るのでユーザーに優しい実装になります。

今回の用途

地図上の情報を取得、写真をサーバーに送信しJSONという形で保存しようという処理を実装しよう、というお思惑です。
前回記載しました内容の処理を実装します。

JavaScript

最近ではAngluarとかJQueryとかいろんなもの(フレームワーク?)がありますが、面倒なので使用しません。HTML5の仕様読みましたが、かなりの範囲を網羅する形でAPIが提供されているので、わざわざ他のJSライブラリなどを使用しなくて良いと思った次第です。
→話(使用する部品)がややこしくなるのであまり使いたくありません(笑)。

XmlHttpRequest

久しぶりに使用します。非同期通信を行うときに使用するオブジェクトです。他のライブラリなどはこいつを使用しているのでハナっからこの部品を使ってしまいます。APIドキュメントはこちらにあります。ドキュメントページからコードを失敬。。。
こいつをベースにして、ファイルなどのデータを送信する処理を実装しようと思います。

あとは、ソースを眺めながら考えます(笑)。

実装の流れ

先にサーバー側の実装を行います。これはクライアント(画面)からAjaxのリクエストを飛ばし、サーバー側で受けるので、受け口を先に作る…というわけです。ファイルの受け取りを行うので、とりあえず受信が出来ているか確認するものを作ります。

サンプル(クライアント)は下のリンクからアクセス出来ます。

https://zenryokuservice.com/sample/js/SampleMap.html

でわでわ。。。


/*  https://developer.mozilla.org/ja/docs/DOM/XMLHttpRequest#sendAsBinary() */

if (!XMLHttpRequest.prototype.sendAsBinary) {
  XMLHttpRequest.prototype.sendAsBinary = function(sData) {
    var nBytes = sData.length, ui8Data = new Uint8Array(nBytes);
    for (var nIdx = 0; nIdx < nBytes; nIdx++) { ui8Data[nIdx] = sData.charCodeAt(nIdx) & 0xff; } /* send as ArrayBufferView...: */ this.send(ui8Data); /* ...or as ArrayBuffer (legacy)...: this.send(ui8Data.buffer); */ }; } /* https://developer.mozilla.org/ja/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest * This framework is released under the GNU Public License, version 3 or later. * https://www.gnu.org/licenses/gpl-3.0-standalone.html * */ var AJAXSubmit = (function () { function ajaxSuccess () { /* console.log("AJAXSubmit - Success!"); */ console.log(this.responseText); /* you can get the serialized data through the "submittedData" custom property: */ /* console.log(JSON.stringify(this.submittedData)); */ } function submitData (oData) { /* the AJAX request... */ var oAjaxReq = new XMLHttpRequest(); oAjaxReq.submittedData = oData; oAjaxReq.onload = ajaxSuccess; if (oData.technique === 0) { /* method is GET */ oAjaxReq.open("get", oData.receiver.replace(/(?:\?.*)?$/, oData.segments.length > 0 ? "?" + oData.segments.join("&") : ""), true);
      oAjaxReq.send(null);
    } else {
      /* method is POST */
      oAjaxReq.open("post", oData.receiver, true);
      if (oData.technique === 3) {
        /* enctype is multipart/form-data */
        var sBoundary = "---------------------------" + Date.now().toString(16);
        oAjaxReq.setRequestHeader("Content-Type", "multipart\/form-data; boundary=" + sBoundary);
        oAjaxReq.sendAsBinary("--" + sBoundary + "\r\n" +
            oData.segments.join("--" + sBoundary + "\r\n") + "--" + sBoundary + "--\r\n");
      } else {
        /* enctype is application/x-www-form-urlencoded or text/plain */
        oAjaxReq.setRequestHeader("Content-Type", oData.contentType);
        oAjaxReq.send(oData.segments.join(oData.technique === 2 ? "\r\n" : "&"));
      }
    }
  }

  function processStatus (oData) {
    if (oData.status > 0) { return; }
    /* the form is now totally serialized! do something before sending it to the server... */
    /* doSomething(oData); */
    /* console.log("AJAXSubmit - The form is now serialized. Submitting..."); */
    submitData (oData);
  }

  function pushSegment (oFREvt) {
    this.owner.segments[this.segmentIdx] += oFREvt.target.result + "\r\n";
    this.owner.status--;
    processStatus(this.owner);
  }

  function plainEscape (sText) {
    /* How should I treat a text/plain form encoding?
       What characters are not allowed? this is what I suppose...: */
    /* "4\3\7 - Einstein said E=mc2" ----> "4\\3\\7\ -\ Einstein\ said\ E\=mc2" */
    return sText.replace(/[\s\=\\]/g, "\\$&");
  }

  function SubmitRequest (oTarget) {
    var nFile, sFieldType, oField, oSegmReq, oFile, bIsPost = oTarget.method.toLowerCase() === "post";
    /* console.log("AJAXSubmit - Serializing form..."); */
    this.contentType = bIsPost && oTarget.enctype ? oTarget.enctype : "application\/x-www-form-urlencoded";
    this.technique = bIsPost ?
        this.contentType === "multipart\/form-data" ? 3 : this.contentType === "text\/plain" ? 2 : 1 : 0;
    this.receiver = oTarget.action;
    this.status = 0;
    this.segments = [];
    var fFilter = this.technique === 2 ? plainEscape : escape;
    for (var nItem = 0; nItem < oTarget.elements.length; nItem++) { oField = oTarget.elements[nItem]; if (!oField.hasAttribute("name")) { continue; } sFieldType = oField.nodeName.toUpperCase() === "INPUT" ? oField.getAttribute("type").toUpperCase() : "TEXT"; if (sFieldType === "FILE" && oField.files.length > 0) {
        if (this.technique === 3) {
          /* enctype is multipart/form-data */
          for (nFile = 0; nFile < oField.files.length; nFile++) {
            oFile = oField.files[nFile];
            oSegmReq = new FileReader();
            /* (custom properties:) */
            oSegmReq.segmentIdx = this.segments.length;
            oSegmReq.owner = this;
            /* (end of custom properties) */
            oSegmReq.onload = pushSegment;
            this.segments.push("Content-Disposition: form-data; name=\"" +
                oField.name + "\"; filename=\"" + oFile.name +
                "\"\r\nContent-Type: " + oFile.type + "\r\n\r\n");
            this.status++;
            oSegmReq.readAsBinaryString(oFile);
          }
        } else {
          /* enctype is application/x-www-form-urlencoded or text/plain or
             method is GET: files will not be sent! */
          for (nFile = 0; nFile < oField.files.length;
              this.segments.push(fFilter(oField.name) + "=" + fFilter(oField.files[nFile++].name)));
        }
      } else if ((sFieldType !== "RADIO" && sFieldType !== "CHECKBOX") || oField.checked) {
        /* NOTE: this will submit _all_ submit buttons. Detecting the correct one is non-trivial. */
        /* field type is not FILE or is FILE but is empty */
        this.segments.push(
          this.technique === 3 ? /* enctype is multipart/form-data */
            "Content-Disposition: form-data; name=\"" + oField.name + "\"\r\n\r\n" + oField.value + "\r\n"
          : /* enctype is application/x-www-form-urlencoded or text/plain or method is GET */
            fFilter(oField.name) + "=" + fFilter(oField.value)
        );
      }
    }
    processStatus(this);
  }

  return function (oFormElement) {
    if (!oFormElement.action) { return; }
    new SubmitRequest(oFormElement);
  };

})();