Help us understand the problem. What is going on with this article?

(強いて言うなら)サーバー・インフラ開発者の、初めてのWebページ開発 その2. HTMLとJavaScriptとの連携

More than 1 year has passed since last update.

はじめに

Webページ開発初心者のサーバー・インフラ開発者が簡単なページ開発を行った際の記録その2です。その1はこちら。(要約: CSS設計大事)
今回はJavaScript部分を作ってみます。

環境構築

Webサーバーとして動作させたい: Node.jsの利用

まずはjsで実際にWebサーバーとして動作させたいので、Webサーバーの用意をしましょう。私は自宅に立てたルーターをHTTPサーバー(lighttpd)にします。

…なんて「Webサーバー」とくくられるとハードルが高くなっちゃいますが、Node.jsというサーバーサイド向けのJavaScript実行環境を利用すると、比較的簡単に簡易的なWebサーバーが作れます。こんな感じで

(今回私が作ったページではlighttpdのプラグインを利用する為、上記ではwebサーバーでは一部機能が動作しませんが、index.htmlとcgiくらいならそれなりに動作すると思います。)

npmでのJQueryのインストール

まずはHTTP通信を行うためのajaxを利用する為JQueryをインストールします。公式サイトを見ると、Node.jsのパッケージ管理ツールであるnpmからインストールが出来るとのことなので、npmからインストールします。

npm install jquery

また、npmインストールの参考を見たところ、browserifyというコマンドでJavaScriptのコードをまとめられるというので、こちらも使ってみます。

npm i -D browserify

すると、コマンド実行環境下にnode_modulesというディレクトリが出来、色々なファイルがその中に出来る状態になります。これで環境準備は完了。
他にも便利なnpmのパッケージがあればinstallして追加してください。
rubyのgem, pythonのpip等、スクリプト言語ではパッケージ管理ツールは基本なんですね。

browserifyを利用したjsファイルのまとめ

jsファイル(jQuery含む)は以下のようにbrowserifyコマンドでまとめることが出来ます。

$(npm bin)/browserify ./js/controller.js -o ./bind.js

browserify は指定されたjsファイルが参照(require)しているファイルを検索して、-oで指定されたbind.jsファイルにまとめてくれます。
なので、html側は全てのjsファイルを読み込む必要がなく、こんな感じでbind.jsだけを読み込めば動作出来るようになります。これは楽ちんですね!

index.html
<script type="text/javascript" src="bind.js"></script>

requireについて。例えばcontroller.jsは以下のようにview.js, http_service.jsを参照しています。

controller.js
const CGIRequest = require('./http_service.js');
const View = require('./view.js');

さらに参照しているhttp_server.jsはjqueryを参照しています。これだけでバージョンも気にせず利用することが出来ます。

http_service.js
'use strict';
var $ = require('jquery');

module.exports = class CGIRequest {
...

このようにjsファイル内でrequireを指定しているファイルをまとめて1つのbind.jsファイルにまとめてくれるのがbrowserify。便利です。

HTMLとJavaScriptとの連携部分を作ってみる

作ってみた主な機能は以下です。

  1. ajaxでのHTTP通信(コマンドリストの取得)
  2. 1で取得したリストを利用して、クリックイベント付きリストを動的に作成する
  3. ajaxでの非同期通信(Transfer-encoding:chunked)

以下htmlのcommandlists部分にcgiで取得したリストを動的に追加。
コマンドリストのクリックに対応してHTTP requestを実行(Transfer-encoding:chunked), 結果をresponse_data部分に記載するような形にしました。

index.html
  <div id="main">
    <div id="content">
      <div id="content_inner">
        <!-- ページの中央部分 -->
        <h2 id="current_command" disabled="true"></h2>
        <div class="post">
          <pre id="response_data">
          </pre>
        </div><!-- / .post -->
      </div><!-- / #content_inner --><
    </div><!-- / #content -->

    <!-- ページの左部分 -->
    <div id="leftside">
      <table>
      <div id="commandlists" class="commandlists">
      </div>
      </table>
      <label for="uname">loop second:</label>
      <input id="loopcount" type="number" name="num1" min="0" max="60" value="0" step="1" required><br>
      <button id="loop_clear" type="button">stop</button>
    </div><!-- / #leftside -->
  </div><!-- / #main -->
</body>
</html>

機能実装の前に: html内容の書き換えはclassやidを利用

基本的な知識として。
htmlの中身を取得したりする場合はdocument.getElementById(id名)document.getElementsByClassName(class名)等でhtmlの中身を参照する事が出来ます。
例えば上記の2を実施したいならdocument.getElementById('response_data')でhtmlの中身を参照して中身を書き換えたり。
htmlとのやり取りはidやclassで行うのが基本のようです。

ということはJavaScriptのコード内でhtmlの構成と影響する部分が出てくるということになるので、MVCモデルのようにHTMLに依存する部分と他の部分を切り離すデザインが有効になるんですね。

ajaxでの通信

ajaxでのHTTP通信は$.ajaxで実現可能です。今回はhttp://IP/cgi-bin/コマンド名を実行する処理としました。

http_server.js
        onshot(cmd) {
                //cgiのget request
                return $.ajax({
                        type: 'GET',
                        url: '/cgi-bin/' + cmd,
                        timeout: 10000
                })
        }

$.ajaxの応答はPromiseというオブジェクトで、Promise.then(成功時に呼ばれる関数, 失敗時に呼ばれる関数)を指定することで、HTTPの応答を拾うことが出来ます。こんな感じで。

controller.js
//成功時に呼ばれる関数
function command_list_success(result) {
        //commandをリスト化してクリックイベント付きリストを動的に作成する
        view.update_commandlists(result.split('\n'), call_cmd)
}

//失敗時に呼ばれる関数
function command_list_failure(result) {
        console.log(result)
}

//コマンド一覧の追加 + 応答用のコールバック設定
cgireq.onshot('command_list.py').then(command_list_success, command_list_failure);

resultはreponse bodyの内容がそのまま入ってきます。
こちらは改行込みのコマンド一覧なので、配列にsplitを利用して配列に変更し、クリックイベント付きリスト作成用関数update_commandlistsに渡しています。

クリックイベント付きリストを動的に作成する

リストの動的作成

$('クラス名').append(html);(指定したクラスのhtml要素内にhtml文字列を追加する関数)を利用してhtml内に新しい要素を追加するようにしました。
getElementByIdで取得した要素に対してinsertAdjacentHTMLを利用するのも試してみたのですが、CSSの設定がうまく反映されず。
CSSにcommandlistsクラス定義を追加し、appendを利用して要素を追加したらうまくいったのでこちらを採用。(やっぱりちょっと触っただけではCSSの挙動が掴みきれない(-_-;))

view.js
        update_commandlists(cmd_list, cmd_callback) {
                var html;
                html = '<tr><th class="commandlists_th">cgi command</th></tr>'
                //class="commandlists"部分にhtmlの要素をそのまま追加
                $('.commandlists').append(html);
                for(let i = 0; i < cmd_list.length; i++) {
                        html = '<tr><td class="commandlists_td" id="'+cmd_list[i]+'">' + cmd_list[i] + '</td></tr>'
                        $('.commandlists').append(html);
                        //イベント設定
                        ...
                }
        }

クリックイベントの追加

各コマンドに追加するHTMLのタグにはCSS反映用のclass="commandlists_td"とイベント用のid="'+cmd_list[i]を指定。クリック追加したコマンド毎に別で発火出来るようにidを付けました。
クリックイベントはこんな感じでidと紐づけて$(document).on('click', '[id="XXX"]', 関数)で追加しています。

view.js
        update_commandlists(cmd_list, cmd_callback) {
...
                        //イベント設定
                        $(document).on('click', '[id="'+cmd_list[i]+'"]', function(){

                                console.log($(this).text())
                                var cmd_title = document.getElementById('current_command')
                                cmd_title.textContent = $(this).text()
                                cmd_title.disabled = false

                                //インターバル取得
                                var loopcount = Number(document.getElementById('loopcount').value)
                                cmd_callback($(this).text(), loopcount)

                        });
        }

クリックイベントの実体はcallback形式にしてhtmlと切り離ししてます。

ajaxでの非同期通信(Transfer-encoding:chunked)

通常のajaxのPromiseオブジェクトを利用したやり方では、GETのレスポンスが終わってからしか結果が返ってこないためGetのレスポンスを抜き出すような処理が必要になります。
今回はHTTPのレスポンスが開始した際に呼ばれるonloadstartを利用してsetIntervalでタイマーを設定。
定期的にajaxのresponseTextを抜き出してchunked_callbackを呼ぶようにしました。

また、キャンセル時にタイマーストップケアの為にHTTP処理終了時に呼ばれるonloadendも定義。
ajax内で使用するthisはajaxのインスタンスになるようで、this.timerの停止はこのような形にする必要がありました。

http_service.js
        //loopcgiの実行
        loopcgi(cmd, interval, chunked_callback) {
                return $.ajax({
                        type: 'GET',
                        url: '/loopcgi',
                        //Queryの指定
                        data: {
                                'cgi': cmd,
                                'interval': interval,
                        },
                        xhrFields: {
                                //response開始
                                onloadstart: function() {
                                        var xhr = this;
                                        this.timer = setInterval(function() {
                                                chunked_callback(xhr.responseText);
                                        }, interval*1000);
                                },
                                //終了時
                                onloadend: function() {
                                        clearInterval(this.timer);
                                }
                        },
                        success: function() {
                                // すぐにクリアしてしまうと最終的なレスポンスに対する処理ができないので
                                // タイマーと同じ間隔を空けてクリアする必要があるらしいです。
                                setTimeout('clearInterval(this.timer)', interval*1000);
                        },
                        error: function() {
                                console.log("Stop request " + cmd);
                        }
                })
        }

/loopcgiは自作lighttpdプラグイン向けのurlで、/loopcgi??cgi=cgiコマンド名&interval=秒数で定周期でcgi実行⇒結果をTransfer-encoding:chunked方式で順々に返すような仕様にしています。

サンプルコード

前回のhtml, cssも含めこちらに格納しています。

今回の感想~ 「機能を実現する」だけならなんとか。でもそれだけでは…

JavaScriptはpythonやrubyといったスクリプト言語を触ったことがある人なら、HTML/CSSと比較するとJavaScriptの方が理解がしやすい気がします。

ただツールやフレームワークによって書き方も変わりますし(今回もがっつりjQueryに依存しています)、HTMLと関連する部分と他のロジックをどう切り分けるか、業務レベルの規模で設計をするにはきちんと勉強する必要がありそうです。(やっぱりCSSの部分が掴み切れないし、ちょっとググって作るだけでは足りないすね(-_-;))

画面デザインはクライアントの要望でガンガン変わるのが想像できるので、前回のCSS設計も含め、画面側の設計は気を遣うことが多そうだな。

参考

環境構築:
 もう、jQueryはnpmで管理しようぜ

ajax周りの参考全般:
 はじめてajaxを使うときに知りたかったこと
ajax参考:
 JavascriptのAjaxについての基本まとめ
 Promiseを使う

クリックイベント追加の参考:
 jQueryで動的に追加した要素はクリックイベントが発火しない?いやそんなことはないぞ

Encoding-chunkedの参考:
 PHPとJavaScriptでHTTPストリーミングする話(Transfer-Encoding: chunked編)
 jQueryの$.ajaxで通信途中のresponseTextを取得する
 jQueryのajax()を中断する方法
Query指定:
 jQuery.ajax()のまとめ

developer-kikikaikai
元CのLinux組み込み開発者→201904からとある会社でGo言語バックエンドのアーキテクトとして活動しています。 組み込み時代はミドルウェアより上位層が主戦場でした。たまにRubyやpython、Java/Androidも若干触ります。 技術の幅を増やすのはもちろんだけど、それ以上にチーム構築・チーム開発への貢献力を磨きたい
https://github.com/developer-kikikaikai
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away