【Html&Cssで作る四択クイズゲーム開発記録⑥】ネイティブなJavascriptスクリプトをフル活用してHTMLタグ要素(DOM)を自動生成する


※ 当ページには【広告/PR】を含む場合があります。
2021/01/14
【Html&Cssで作る四択クイズゲーム開発記録⑤】Sassリストでクイズの問題を動的に管理する
【Html&Cssで作る四択クイズゲーム開発記録⑦】Svelteアプリの作り方入門!HTML&CSSゲームをSvelteプロジェクトへ移行(マイグレーション)する
合同会社タコスキングダム|TacosKingdom,LLC.

前回までの解説記事の内容で、本四択クイズゲームが粗削りながらも実質の完成、ということに一応はなっておりました。

合同会社タコスキングダム|タコキンのPスクール
【Html&Cssで作る四択クイズゲーム開発記録⑤】Sassリストでクイズの問題を動的に管理する

Sassリストを使ってで四択クイズミニゲームのデータ構造を動的に管理する方法を検討します。

では今回から何をご説明させていただこうかしら?という話になります。

折角ですのでこれから、何かモダンなJS系フレームワークの一つを使って、四択クイズゲームの再構築をしてみる内容にシフトしていこうと画策中です。

とその前に、フレームワークを導入する前の足がかりとして、あえてよりJSフレームワークなしの不便さを知ってもらうべく、100%ピュアなHTML標準(ブラウザ内蔵)のネイティブJavascriptのコードを使って、HTML要素を操作しようとするとどうなるのか、実装しながらその大変さを実感して貰うための若干サディステックな記事になります。

「うわ...これはもう理解は無理だな〜時間の無駄かな〜」と思われた時点で、このブログの内容を切り上げ、
次回のブログ記事に進まれてもいいと思います。

とにかくHTMLの基礎的な部分から、しっかりと理解したい人は以降の話に宜しくお付き合いください。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

改良点の考察〜HTML要素を動的にスクリプトから生成する

特に前回までのコードに問題がある訳でも無いのですが、index.htmlから<form>タグを今一度眺めてみましょう。

            
            <!-- ...中略 -->
<form id="game-wrapper">
    <p class="game-header"><span id="gamestate">さあ4択クイズを始めましょう</span></p>
    <input type="reset" id="reset"/>
    <input type="checkbox" id="start"/>
    <input type="checkbox" id="stage1"/>
    <input type="checkbox" id="stage2"/>
    <input type="checkbox" id="stage3"/>
    <input type="checkbox" id="checkmark1"/>
    <input type="checkbox" id="checkmark2"/>
    <input type="checkbox" id="checkmark3"/>
    <div id="stage---op" class="stage stage--op">
        <label for="start">クリックしてスタート</label>
    </div>
    <div id="stage---1" class="stage stage--main stage--main--1">
        <label for="stage1"><ul class="flexlist"><li><li><li><li>
    </div>
    <div id="stage---2" class="stage stage--main stage--main--2">
        <label for="stage2"><ul class="flexlist"><li><li><li><li>
    </div>
    <div id="stage---3" class="stage stage--main stage--main--3">
        <label for="stage3"><ul class="flexlist"><li><li><li><li>
    </div>
    <div id="stage---reset" class="stage stage--end">
        <p class="score-board"><span id="score"></p>
        <label for="reset">もう一度トライ</label>
    </div>
</form>
<!-- ...中略 -->
        
これまでの一連のブログ記事の内容から、初期のコードの時と比較してもこれでも大分スリムな構成になったのですが、もっと効率的に簡単にこのHTMLの中身も改良しようと思うと、ReactやVueなどのフレームワークNode.jsなどの実行環境Typesciptなどを使えるようにするトランスパイラwebpackなどのリソースをまとめるバンドラーなどなど、覚えることが山ほど増えていくことでしょう。

もちろん、HTML/CSS/Javascriptをコードを純粋な標準構文やビルドイン関数で全て実装できるならこれらのツールを使う必要はありません。

JSフレームワーク無しのHTML要素の操作がどれくらい面倒なものなのか、実際に以降で体感してみましょう。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

最小の「JSフレームワーク」を自作する

そもそもフレームワークとは「枠組み」や「骨組み」という意味の単語です。

何かの言語でアプリケーションを作り始めたいと思っているソフトウェア開発者がソフトウェアを一から全て作り始めるのが大変な作業です。

なので、ソフトウェア開発の現場では、一から全てを作り始めるのではなく、アプリ開発の土台になるベース機能を提供しするソフトウェアを使って開発をスタートさせるのが普通になっています。

その土台となるソフトウェアを指す用語として、「アプリケーションフレームワーク」もしくは単に「フレームワーク」と呼ばれています。

ですので、最終的どのようなアプリケーションを作るかによって、適切なフレームワークを選ぶ必要があります。

とは言え全てのフレームワークで共通していえるのは、
「いかに開発者に楽をさせられて、プログラミング工数の削減・圧縮に貢献できるか」という思想のもとで作られています。

フレームワーク選びにおいて良く耳にする、「どのフレームワークが良いですか?」という質問が良くありますが、これには答えはそのご本人の中にしか存在せず、「自分はどんな感じに“楽”したいんだろう?」と自問自答する他ありません。

というのも、フレームワークには付き物のプログラミング言語とは別に、
「フレームワーク特有のラーニングコスト(学習コスト)」が大なり小なり存在します。

まずはフレームワークを使う上でのインストール方法や、コードを実装する作法を覚える必要があるのです。

よって、初学者が最初にフレームワーク選びを間違うと、楽な作業とは全く無縁の
「勉強のための苦行」に陥りやすく、ただただ苦い経験になってしまいます。

さて、概論はさておいて、著者が「最小のフレームワーク」と勝手に思っている、一つの関数を使ってHTMLを組み立てる方法で前回までのコードを書き直してみます。

この関数に関しては以前、別のブログにてJavascript側からHTML要素を操作するための概要を説明した記事を作成していました。

参考|【Javascript活用講座】もっとHTML要素を上手く操作するためのappendChildの使い方

ここで再度同じことを解説するのは手間がかかりますので、詳しくはそちらの記事に説明を見て頂くとして、最低限以下のような自作の関数を今回も利用します。

            
            //👇属性付きのDOM要素をレンダリングする関数
const createElement = (tagName, attrs) => {
    const _e = document.createElement(tagName);
    if (attrs) {
        for (let attr in attrs) { _e[attr] = attrs[attr]; }
    }
    return _e;
};

//👇使用例
document.addEventListener("DOMContentLoaded", () => {
    document.getElementById('wrapper')
        .appendChild(createElement('div', {
            innerHTML: 'これは最初のDIVです。'
        }))
        .appendChild(createElement('div', {
            innerHTML: 'これは2番めのDIVです。'
        }))
        .appendChild(createElement('div', {
            innerHTML: 'これは3番めのDIVです。'
        }))
        .appendChild(createElement('span', {
            innerHTML: 'これは3番めの中の最初のSPANです。'
        }));
});
        

使用例の通りで、この自作createElement関数一つあるだけで、スッキリと楽にHTML要素をベースのルートノード(親要素)に追加していくだけで簡単なWEBアプリケーションが作れます。

まさに、この関数(メソッドチェーン)は見方を変えると、
「最小のフレームワーク」とも言える(と勝手に思っている)機能を持っていると言えるのではないでしょうか。

前回までのindex.htmlからform要素だけを残し、後はゴッソリ中身を削除します。

            
            <!-- ...中略 -->
<form id="game-wrapper"></form>
<!-- ...中略 -->
        
多くのJSフレームワークは、レンダリング結果を何処に描けばいいかの基準にするため、最低一つのDOMノード(ルート要素)が必要になっていきます。

ここでのルート要素はform要素にして、この中身にJavascript側で処理した結果を出力することでアプリケーションとして機能します。

以下に、先程削除したDOM部分を動的に生成するスクリプトに書き換えます。

            
            document.addEventListener("DOMContentLoaded", () => {
    const stageLength = 6;
    const parent = document.getElementById('game-wrapper');

    parent.appendChild(createElement('p', {
        className: 'game-header'
    })).appendChild(createElement('span', {
        id: 'gamestate',
        innerHTML: 'さあ4択クイズを始めましょう'
    }));
    parent.appendChild(createElement('input', { id: 'reset', type: 'reset' }));
    parent.appendChild(createElement('input', { id: 'start', type: 'checkbox' }));
    parent.appendChild(createElement('div', {
        id: 'stage---op',
        className: 'stage stage--op'
    })).appendChild(createElement('label', {
        htmlFor: 'start',
        innerHTML: 'クリックしてスタート'
    }));

    for (let i=1; i<=stageLength; i++) {
        parent.appendChild(createElement('input', { id: `stage${i}`, type: 'checkbox' }));
        parent.appendChild(createElement('input', { id: `checkmark${i}`, type: 'checkbox' }));
        const ul_ = parent.appendChild(createElement('div', {
            id: `stage---${i}`,
            className: `stage stage--main stage--main--${i}`
        })).appendChild(createElement('label', {
            htmlFor: `stage${i}`
        })).appendChild(createElement('ul', {
            className: 'flexlist'
        }));
        for (let j=0;j<4;j++) { ul_.appendChild(document.createElement('li')); }
    }

    const childResetDiv = parent.appendChild(createElement('div', {
        id: 'stage---reset',
        className: 'stage stage--end'
    }));
    childResetDiv.appendChild(createElement('p', {
        className: 'score-board'
    })).appendChild(createElement('span', {
        id: 'score'
    }));
    childResetDiv.appendChild(createElement('label', {
        htmlFor: 'reset',
        innerHTML: 'もう一度トライ'
    }));

    //...略
});
        
するとどうでしょうか、コードの変更前と変更後で、「全然楽になった気がしない...」と思った方はとても正直な人です。

たった関数一つ程度ではアプリケーションの規模が少し大きくなったら、全然プログラマーは楽に出来ない、という教訓を与えてくれます。

アプリケーションの規模や機能を熟慮して、最適でバランスのとれたフレームワークを選ぶのはこの点で重要なことです。

繰り返しになりますが結論としては、全てのフレームワークは使ってくれるプログラマーの作業を楽にするために有志によって作成・開発が進められているものです。

ウェブ上で良く目にする「今年のフレームワークのトレンドはこれだ」とか、「最強のJSフレームワークまとめ」などの記事も溢れてはいますが、当のユーザー本人が「プログラミングが楽ちんだ」、「コーディングしてて楽しいし自分との相性抜群」などを信じて自身のスキルを磨いていかれると良いと思います。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

まとめ

以上、今回はやらなくても良かったのですが、これからフレームワークを導入してHTML&CSSアプリケーション開発を進めていく上での、フレームワークを使う意義、のような話を一度間に挟んでおきたく思いました。

敢えてHTMLネイティブの関数でDOM操作するとどのように辛いのかを具体例で確認することで、よりフレームワークの有り難みが分かるのではないでしょうか。

次回以降で、軽量フレームワーク・『Svelte』を使って、この四択クイズミニゲームを更に進化させる方法を紹介していこうかと思います。

付録〜改良前のソースコード(バックアップ)

今回も恒例の古くなったソースコード(バージョン0.6)のバックアップを付録として掲載しておきます。

index.html

            
            <!DOCTYPE html>
<html lang="ja">
<html>
    <meta charset="utf-8"/>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <form id="game-wrapper">
            <p class="game-header"><span id="gamestate">さあ4択クイズを始めましょう</span></p>
            <input type="reset" id="reset"/>
            <input type="checkbox" id="start"/>
            <input type="checkbox" id="stage1"/>
            <input type="checkbox" id="stage2"/>
            <input type="checkbox" id="stage3"/>
            <input type="checkbox" id="stage4"/>
            <input type="checkbox" id="stage5"/>
            <input type="checkbox" id="stage6"/>
            <input type="checkbox" id="checkmark1"/>
            <input type="checkbox" id="checkmark2"/>
            <input type="checkbox" id="checkmark3"/>
            <input type="checkbox" id="checkmark4"/>
            <input type="checkbox" id="checkmark5"/>
            <input type="checkbox" id="checkmark6"/>
            <div id="stage---op" class="stage stage--op">
                <label for="start">クリックしてスタート</label>
            </div>
            <div id="stage---1" class="stage stage--main stage--main--1">
                <label for="stage1"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---2" class="stage stage--main stage--main--2">
                <label for="stage2"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---3" class="stage stage--main stage--main--3">
                <label for="stage3"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---4" class="stage stage--main stage--main--4">
                <label for="stage4"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---5" class="stage stage--main stage--main--5">
                <label for="stage5"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---6" class="stage stage--main stage--main--6">
                <label for="stage6"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---reset" class="stage stage--end">
                <p class="score-board"><span id="score"></p>
                <label for="reset">もう一度トライ</label>
            </div>
        </form>
        <script>
        (function() {
            document.getElementById("start").addEventListener("change", (e) => {
                document.getElementById("gamestate").textContent = '成績: ';
            });
            function checkGameOver() {
                let isGameOver = true;
                document.querySelectorAll("input[id^='stage']").forEach((checkItem) => {
                    isGameOver = isGameOver && checkItem.checked;
                });
                return isGameOver;
            }
            function checkSum() {
                let result = 0, total = 0;
                document.querySelectorAll("input[id^='checkmark']").forEach((checkItem) => {
                    checkItem.checked && result++;
                    total++;
                });
                document.getElementById("score").textContent = result == total ? `全問正解🎉あなたは「タコマスター」の称号を得ました!!` : `正解率は ${result}/${total} でした。`;
            }
            [...Array(6).keys()].map(m => {
                return {stageName: `stage${m+1}`, checkbox: `checkmark${m+1}`};
            }).forEach(g => {
                document.getElementById(g.stageName).addEventListener("change", (e) => {
                    document.getElementById("gamestate").textContent += document.getElementById(g.checkbox).checked ? '⭕ ' : '❌ ';
                    checkGameOver() && checkSum();
                });
            });
            document.getElementById("reset").addEventListener("click", (e) => {
                document.getElementById("gamestate").textContent = 'さあ4択クイズを始めましょう';
            });
            document.querySelectorAll("ul.flexlist > li").forEach((userItem) => {
                userItem.addEventListener("click", (e) => {
                    const m_ = window.getComputedStyle(e.target,'::after').content.match(/(\d+)\/correct/);
                    m_ && (document.getElementById(`checkmark${m_[1]}`).checked = true);
                });
            });
        }());
        </script>
    </body>
</html>
        

styles.scss

            
            #game-wrapper {
    width: 100%;
    height: 300px;
    background-color: darkgray;
    box-sizing: border-box;
    position:relative;
    input { display: none; }
    .game-header {
        position: absolute;
        top: 0;
        left: 0;
        background: #220c0c;
        color: #ceeece;
        z-index: 1;
        margin: 0;
        font-size: 22px;
        padding: 6px 0 6px 0;
        width: 100%;
    }
    $stages: (
        'start' : ('op', ''),
        'stage1': ('1', 'タコの足は何本?', ('5本', '8本', '10本', '足はない'), 2, blue),
        'stage2': ('2', 'タコは何動物?', ('地球外生物', '緩歩動物', '実は植物', '軟体動物'), 4, rgb(231, 185, 99)),
        'stage3': ('3', 'タコの水揚げ量が世界一の国?', ('中国','カナダ','モーリタニア','日本'), 1, rgb(211, 51, 203)),
        'stage4': ('4', 'タコの心臓は何個?', ('1個','2個','3個','8個'), 3, rgb(68, 67, 8)),
        'stage5': ('5', 'タコの弱点は?', ('イソギンチャク','直射日光','真水','ウミウシ'), 3, rgb(179, 53, 15)),
        'stage6': ('6', 'タコの脳は何個?', ('3個','9個','12個','実は存在しない'), 2, rgb(79, 100, 4))
    );
    .stage {
        position: absolute;
        display: block;
        width: 100%;
        height: 100%;
        font-size: 24px;
        &--op {
            display: flex;
            align-items: center;
            justify-content: center;
            label {
                display: block;
                flex: 0 0 auto;
                margin: 0 auto;
                animation: flash 1s linear infinite;
            }
        }
        &--main {
            display: flex;
            align-items: center;
            justify-content: center;
            label {
                flex: 0 0 auto;
                margin: 0 auto;
            }
            @for $i from 2 through length($stages) {
                $item: nth($stages,$i);
                $item: nth($item,2);
                &--#{nth($item,1)} {
                    color: nth($item,5);
                    font-size: 22px;
                }
            }
        }
        &--end {
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            background: darkgray;
            label {
                flex: 0 0 auto;
                display: inline-block;
                font-weight: bold;
                padding: 0 0 0 0;
                text-decoration: none;
                color: #00BCD4;
                background: #ECECEC;
                margin: 0 auto;
                animation: flash 1s linear infinite;
            }
        }
        @keyframes flash {
            0%,100% { opacity: 1; }
            50% { opacity: 0; }
        }
    }
    .stage--main {
        ul.flexlist {
            border: 1px solid #666;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            list-style-type: none;
            margin: 10px auto 2px;
            padding: 0;
            width: 300px;
            li {
                border: 1px solid #aaa;
                line-height: 110%;
                margin: 5px 2px 5px 2px;
                padding: 8px;
                text-align: center;
                width: 35%;
                &:hover {
                    color: chartreuse;
                }
                &::after {
                    content: "wrong";
                    visibility: hidden;
                    display: block;
                    width: 0;
                    height: 0;
                }
            }
        }
    }
    #stage {
        &---op {display: flex}
        @for $i from 2 through length($stages) {
            $item: nth($stages,$i);
            $item: nth($item,2);
            &---#{nth($item,1)} { display: none; }
        }
        &---reset { display: none; }
    }
    @for $i from 1 through length($stages) {
        $item: nth($stages, $i);
        $item: nth($item,1);
        ##{$item}:checked ~ {
            $item: nth($stages, $i);
            $item: nth($item, 2);
            $item: nth($item,1);
            #stage---#{$item} {display: none}
            @if $i == length($stages) {
                #stage---reset {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                }
            } @else {
                $item: nth($stages, $i + 1);
                $item: nth($item,2);
                $item: nth($item,1);
                #stage---#{$item} {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                    $item: nth($stages, $i + 1);
                    $item: nth($item,2);
                    $item: nth($item,2);
                    label::before { content: '問題#{$i}:#{$item}'; }
                    $item: nth($stages, $i + 1);
                    $item: nth($item,2);
                    $quiz_option: nth($item,3);
                    $answer: nth($item,4);
                    @for $j from 1 through length($quiz_option) {
                        ul.flexlist {
                            li:nth-child(#{$j}) {
                                &::before {content: '#{nth($quiz_option,$j)}';}
                                @if $j == $answer {
                                    &::after { content: "#{nth($item,1)}/correct"; }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    @keyframes fadeIn {
        0% {
            display: none;
            opacity: 0;
        }
        1% {
            display: flex;
            opacity: 0;
        }
        100% {
            display: flex;
            opacity: 1;
        }
    }
}