【教師オリジナルアプリを作ろう】英文法語句選択問題アプリの作成方法をはじめから(JavaScript)

ITCの普及に伴い,英語教育の現場でもコンピューターを使った教育の導入を検討する教員が増えてきました。さまざまなソフトウェアメーカーが教材の開発・販売をしており,その中から最も優れた選択肢を選ぶのも一つの方法です。

一方で,そうしたソフトウェアはあなたが生徒に提供したいものに完全に合致しているとは言えないでしょう。汎用性を考慮して作られた市販のソフトウェアは,それぞれの教員が生徒に提供したい教材のセットを100パーセント実現できるわけではありません。

この問題の解決策の一つは,教員が自分でオリジナルのアプリを作成して生徒に提供することです。

しかしながら,それを実現するにはプログラミングの知識が必要であり,またそれをまったくの白紙の状態から用意することはかなりの労力を伴います。

そこで,教員がオリジナルのアプリを提供するためのフレームワークを構築してみよう,というのがこの投稿の趣旨です。

今回紹介するアプリは,フレームワークの部分だけで構成される,最もシンプルなソースコードです。見た目は質素ですが,次の投稿で見た目をアプリらしく整えていきます。今回はJavaScriptを使って,オリジナルアプリの基本構造を学んでいきます。

開発環境

XAMPPはローカル環境でwebサーバーを動かすために必要です。JavaScriptを動かす上では必ずしも必要ではありませんが,インストールしておくとブラウザから localhost と入力するだけでページにアクセスすることができるので,何かと便利です。あとでphpを使って機能を追加することなどを考えるとインストールしておいたほうが良いでしょう。

また,コードを書くためにVisual Studio Codeもインストールしておいた方が良いでしょう。エディタの中では安定性が高く,おすすめです。

JavaScript

JavaScriptはブラウザ上で動作するプログラミング言語です。webアプリを作成する上で重要な言語です。この記事ではJavaScriptについての細かい説明は省きます。

また,JavaScriptの記述を簡略化するためのツールとしてjqueryを使用しています。使い方については日本語リファレンスを参照してください。

jqueryは使いすぎるとアプリの動作が遅くなる欠点がありますが,このくらいの規模のアプリでは気にする必要はありません。コードを見たときに動作が分かりやすい長所もあるので,今回はjqueryを使っていきます。

コードの解説

装飾の要素を取り払ったフレームワークとなるコードを解説していきます。今回のコードでは html ファイル内に直接 JavaScript のコードを挿入する形をとっています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>fasgram 英文法トレーニングwebアプリ最小版</title>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous"></script>
</head>

ヘッダー部です。<script> ~ </script> で jquery を読み込みます。

<body>
<div id="container">
    <header>
        <p>fasgram 英文法トレーニングwebアプリ動作サンプル最小版</p>
    </header>
    <article>
        <div id="display"></div>
        <div id="comment_box"></div>
    </article>
</div>

本体です。ブロック要素を指定しています。ブロック要素とは中身を入れるための箱であると考えてください。あとで JavaScript のコードによってこの箱の中に実際に中身を入れていきます。

ブロック要素は箱の中にさらに箱を入れた構造になっています。ここでは,containerという箱があり,そのなかにheaderarticleという箱が入っています。articleの中にはdisplaycomment_boxの箱が入っています。

containerはページ上に表示されるすべての文字などを格納する最も大きな箱です。containerを用意する理由は,あとでこのブロック要素の幅を操作することで,画面に表示されるページの幅を決めることができるなどの利点があるからです。

headerはページの最上部に表示されるタイトルなどを格納します。そして,アプリの主要な部分はarticleに格納します。

displayは問題文と選択肢のボタンを格納する部分です。そして選択肢のボタンを押したときに,正解や不正解,解説などを表示する部分をcomment_boxに格納します。

こうして,bodyでは中身を入れるための箱だけを用意して,中身自体はJavaScriptのコードによって表示していきます。

スタート画面

function start_display() {
    $('#display').empty();
    let content = '<p>ボタンを押して、トレーニングを開始しましょう。</p><button type="button" id="start-button">語句選択問題</button>';
    $('#display').append(content);
    $('#start-button').on('click', function() {
        grammar_test_start()
    });
}

まず,スタート画面を表示する関数start_display()を定義します。この関数を最後に呼び出すことによって,アプリを起動します。

中身を見ていきましょう。

    $('#display').empty();

これがjqueryのコードです。先頭に$の記号が付いていることで判別できます。括弧の中はセレクタです。#displayiddisplayとされている要素を指します。つまりhtmlの中で書いた<div id="display"> ~ </div>の部分のことです。

empty()は,<div id="display"> ~ </div>で示されたブロック要素の中身を空にします。初めにプログラミングを起動した段階ではブロック要素の中身は空の状態ですが,あとでそうではない状態が発生するので,箱の中身を空にする処理を書いておきます。

    let content = '<p>ボタンを押して、トレーニングを開始しましょう。</p><button type="button" id="start-button">語句選択問題</button>';

ブロック要素の中に入れるhtmlのコードを,いったん変数contentに格納します。ここでは,スタート画面のメッセージとボタンを書いています。

    $('#display').append(content);

ブロック要素の中に,contentの内容を追加します。append()はブロック要素に内容を追加します。

<div id="display"></div>

append(content) -->

<div id="display"><p>ボタンを押して、トレーニングを開始しましょう。</p><button type="button" id="start-button">語句選択問題</button></div>

append()によって html の中身が書き換えられ,メッセージとボタンが表示されます。これをDOM操作と呼びますが,このようにして画面に表示される内容を動的に書き換えることができます。

    $('#start-button').on('click', function() {
        grammar_test_start()
    });

詳しい説明は省きますが,このように書くことでidstart-buttonで指定されたボタンをクリックすると,関数grammar_test_start()を呼び出す仕組みになっています。grammar_test_start()は実際に文法問題を表示していく部分です。

問題文と選択肢の表示

async function grammar_test_start() {

問題文と選択肢を表示し,正解と不正解を判定する部分に移ります。asyncは,あとで登場するawaitとセットになっていて,プログラムの処理を直鎖状に実行していきます。

    //画面の初期化とレイアウトの設定
    let content;
    content = '<p id="question-number"></p>';
    content += '<p id="question"></p>';
    content += '<p id="option1" class="box-1"></p>';
    content += '<p id="option2" class="box-1"></p>';
    content += '<p id="option3" class="box-1"></p>';
    content += '<p id="option4" class="box-1"></p>';
    $('#display').empty();
    $('#display').append(content);

スタート画面のときと同様,いったん変数contentに表示したいhtmlコードを格納して,append()でブロック要素に追加します。ここでは,実際の内容は記述せずに,それぞれの内容を格納するブロック要素だけを用意しています。

それぞれのブロック要素に内容を追加していきましょう。

    //問題文データの読み込み
    const data = await fetch('./data/test01.json');
    obj = await data.json();
    obj_json = JSON.stringify(obj); //json文字列に変換
    texts = JSON.parse(obj_json); //配列に格納

問題文のデータを読み込みます。fetch()でファイルをリクエストして,.json()でjson形式のファイルとして読み込みます。さらに.stringify()でjson文字列に変換し,.parse()で配列に格納します。このあたりは実際に何をしているのかはあまり理解できなくても構いません。

test01.json
--------------------------------------------------
[["Look! There's a dog in the hall. Someone must have left the door open.","見て。ホールに犬がいるよ。誰かがドアを開けたままにしたに違いない。","Look! There's a dog in the hall. Someone must have left the door (  ).","be opened","open","opening","to open",2,"leave O C OをCのままにする。Cには補語が入ります。ここでは形容詞の open「開いている(状態)」を用います。① be opened のように動詞の原形 be や,④ to open のように不定詞を用いることはできません。また,③ opening とすると「ドアが何かを開いている」となり意味が成り立ちません。"],["Each of the sumo wrestlers weighs over 100 kg.","相撲取りはみんな体重が100キロ以上ある。","Each of the sumo wrestlers (  ) over 100 kg.","is weigh","is weight","weighs","weights",3,"動詞の weigh「~の重さがある」と名詞の weight「重さ」の違いを覚えましょう。① is weigh のように be 動詞に動詞の原形を続けることはできません。② は「相撲取りはみんな重さである」となり意味が成り立ちません。④ weights は名詞の複数形であり,ここは動詞を用いるべきところです。"],["After a lot of problems she managed to learn to drive a car.","多くの困難のあと,彼女はなんとか車を運転できるようになった。","After a lot of problems she (  ) to learn to drive a car.","gave up","managed","put off","succeeded",2,"② manage to ~「なんとか~する」 ① give up ~ing「~することをあきらめる」 ③ put off ~ing「~することを延期する」 ④ succeed in ~ing「~することに成功する 動詞のあとに動名詞~ingが続くもの,to不定詞が続くものを区別して覚えます。"]]

jsonファイルの中身です。ここでは,問題を3問用意しました。jsonファイルの書式については説明を省きますが,二次元配列を定義する要領で記述することができます。

これを,配列に格納すると以下のようになります。

texts[0][0] = "Look! There's a dog in the hall. Someone must have left the door open."
texts[0][1] = "見て。ホールに犬がいるよ。誰かがドアを開けたままにしたに違いない。"
texts[0][2] = "Look! There's a dog in the hall. Someone must have left the door (  )."
texts[0][3] = "be opened"
texts[0][4] = "open"
texts[0][5] = "opening"
texts[0][6] = "to open"
texts[0][7] = 2
texts[0][8] = "leave O C OをCのままにする。Cには補語が入ります。ここでは形容詞の open「開いている(状態)」を用います。① be opened のように動詞の原形 be や,④ to open のように不定詞を用いることはできません。また,③ opening とすると「ドアが何かを開いている」となり意味が成り立ちません。"
texts[1][0] = "Each of the sumo wrestlers weighs over 100 kg."
......

このように二次元配列の一つ目の値が問題の番号,二つ目の値が問題文や選択肢などを表していることが分かります。

二つ目の値について説明すると,0 は正解となる英文,1 は和訳,2 は問題文,3 ~ 6 は選択肢,7 は正解の番号,8 は解説となっています。

今回のコードでは 0 と 1 は使用していません。

jsonファイルの用意が少し手間がかかりますが,この部分を自分で用意することで,オリジナルの教材に差し替えることができます。

    //問題文をシャッフル
    for(let i = texts.length -1; i > 0; i--) {
        let rand = Math.floor(Math.random() * (i + 1));
        [texts[i], texts[rand]] = [texts[rand], texts[i]];
    }

問題文をシャッフルすることで,問題がランダムに表示されるようにします。問題を順番通り表示したい場合はこの部分は必要ありません。

Promise() async await

    for(let id = 0; id < texts.length; id++) {

for文によって,今回用意した3つの問題を順番に表示していきます。texts.lengthは問題数を表し,今回はtexts.length=3です。

        await new Promise((resolve) => {

問題文と選択肢を表示する部分をPromiseの中に記述していきます。

この部分に関しては,深く掘り下げても意味の分からない部分が多くなってくるかもしれません。

ざっくりと説明すると,Promise()はあとで登場するresolve()を実行することで処理を終了します。awaitPromise()が処理を終了するまで次の処理に進まずに待ちます。resolve()は選択肢をクリックする部分に記述されていて,つまり,選択肢がクリックされるまで次に進まないようになっています。

このあたりが,他の言語に比べてJavaScriptが分かりにくい部分かもしれません。

このような記述をしなかった場合,for文はクリックを待つことなく,次々と処理を実行していきます。つまり,問題に解答する前に次の問題に移ってしまい,最後の問題まで一気に進んでしまいます。

そこで,選択肢がクリックされるまで次の問題に進まないようにするために,Promise()asyncawaitを使用しているのです。

            $('#question-number').text('第 '+(id+1)+' 問');

.text()は指定されたブロック要素に文字列を指定します。最初はid=0から始まるので,ブロック要素question-numberの中身は第 1 問となります。

            $('#question').text(texts[id][2]);
            $('#option1').text('① '+texts[id][3]);
            $('#option2').text('② '+texts[id][4]);
            $('#option3').text('③ '+texts[id][5]);
            $('#option4').text('④ '+texts[id][6]);

同様に問題文と4つの選択肢のブロック要素に文字を加えていきます。

            //選択肢クリック時の処理
            $('#option1').on('click', function() {
                clicked = 1;
                resolve();
            });
            $('#option2').on('click', function() {
                clicked = 2;
                resolve();
            });
            $('#option3').on('click', function() {
                clicked = 3;
                resolve();
            });
            $('#option4').on('click', function() {
                clicked = 4;
                resolve();
            });

4つの選択肢それぞれをクリックした時の処理です。変数clickedに選択した番号を格納します。そして,上で説明したresolve()を実行します。これによって,選択肢をクリックしたときにはじめて次の処理に移るようにしています。

正解と解説の表示

        await new Promise((resolve) => {

次に,正解と不正解を判別し,解答と解説を表示する部分に進みます。上と同様にawaitPromise()を使って,勝手に次の問題に進まないようにします。

            const option_char = ['①','②','③','④'];
            const correct_answer = texts[id][7]; //答えの番号を格納

配列option_charに正解の番号を示す文字列を格納し,correct_answerに正解の番号を格納します。

            if(clicked == correct_answer) {
                //正解の場合の処理
                let content = '<p>〇 正解</p>';
                $('#comment_box').append(content);
                setTimeout(() => {
                    resolve();                    
                }, 1500);

正解の選択肢をクリックした場合の処理です。

まず,append()で正解であることのメッセージを表示します。

setTimeout()は,処理を一時的にストップする関数です。上のように書くことで,1500ミリ秒後,つまり1.5秒後にresolve()を実行し,次の問題に移ります。このようにしないと,メッセージが表示された瞬間に次の問題に移ってしまうので,待ち時間を設けています。

            } else {
                //不正解の場合の処理
                let content = '<p>× 不正解 ';
                content += '正解は '+option_char[correct_answer-1]+'</p>';
                content += '<p>'+texts[id][8]+'</p>';
                content += '<button type="button" id="confirm">OK</button>';
                $('#comment_box').append(content);
            }

次に,不正解の選択肢をクリックした場合の処理です。

append()で不正解であることのメッセージなどを表示し,またOKボタンを表示します。

            $('#confirm').on('click', function() {
                resolve();
            });

OKボタンがクリックされたら,resolve()を実行し,次の問題に移ります。

終了メッセージ

    await new Promise((resolve) => {
        let content;
        content = '<p>トレーニング終了です。</p>';
        content += '<button type="button" id="confirm">最初に戻る</button>';
        $('#display').empty();
        $('#comment_box').empty();
        $('#display').append(content);
        //OKボタンが押されたら次の問題へ
        $('#confirm').on('click', function() {
            start_display();
            resolve();
        });
    });

すべての問題が終了したときに表示されるメッセージです。これまで説明してきたコードの理解が進んでいれば,ここで行っていることも理解できると思います。

「最初に戻る」のボタンが押されたら,start_display()を実行し,初めのスタート画面に戻ります。

まとめ

ここでは,JavaScriptを使って英文法問題を表示するwebアプリを作成しました。

ブロック要素とDOMを使って,ページ内の情報を書き換え,asyncawaitPromise()を使って処理を逐次的に処理する方法を学びました。

ボタンを押す動作によって画面が順番に進んでいくようにすることで,アプリとして動作させることができるようになりました。

この考え方は,他のwebアプリを作成する際にも利用できそうです。

ここで,紹介したコードは商用・非商用を問わず著作権者の許諾無しに自由に改変・再配布することを許可します。紹介されたコードを利用して,教師オリジナルのアプリを生徒に提供して下さい。

最後にコード全体を示します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>fasgram 英文法トレーニングwebアプリ最小版</title>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous"></script>
</head>
<body>
<div id="container">
    <header>
        <p>fasgram 英文法トレーニングwebアプリ動作サンプル最小版</p>
    </header>
    <article>
        <div id="display"></div>
        <div id="comment_box"></div>
    </article>
</div>
<script>
//ここからJavaScript
let obj;
let texts;
//スタート画面
function start_display() {
    $('#display').empty();
    let content = '<p>ボタンを押して、トレーニングを開始しましょう。</p><button type="button" id="start-button">語句選択問題</button>';
    $('#display').append(content);
    $('#start-button').on('click', function() {
        grammar_test_start()
    });
}
//語句選択問題
async function grammar_test_start() {
    //画面の初期化とレイアウトの設定
    let content;
    content = '<p id="question-number"></p>';
    content += '<p id="question"></p>';
    content += '<p id="option1" class="box-1"></p>';
    content += '<p id="option2" class="box-1"></p>';
    content += '<p id="option3" class="box-1"></p>';
    content += '<p id="option4" class="box-1"></p>';
    $('#display').empty();
    $('#display').append(content);
    //問題文データの読み込み
    const data = await fetch('./data/test01.json');
    obj = await data.json();
    obj_json = JSON.stringify(obj); //json文字列に変換
    texts = JSON.parse(obj_json); //配列に格納
    //問題文をシャッフル
    for(let i = texts.length -1; i > 0; i--) {
        let rand = Math.floor(Math.random() * (i + 1));
        [texts[i], texts[rand]] = [texts[rand], texts[i]];
    }
    //テストを順番に行う
    let clicked;
    for(let id = 0; id < texts.length; id++) {
        //問題文と選択肢の表示
        await new Promise((resolve) => {
            $('#question-number').text('第 '+(id+1)+' 問');
            $('#question').text(texts[id][2]);
            $('#option1').text('① '+texts[id][3]);
            $('#option2').text('② '+texts[id][4]);
            $('#option3').text('③ '+texts[id][5]);
            $('#option4').text('④ '+texts[id][6]);
            $('#comment_box').empty()
            //選択肢クリック時の処理
            $('#option1').on('click', function() {
                clicked = 1;
                resolve();
            });
            $('#option2').on('click', function() {
                clicked = 2;
                resolve();
            });
            $('#option3').on('click', function() {
                clicked = 3;
                resolve();
            });
            $('#option4').on('click', function() {
                clicked = 4;
                resolve();
            });
        });
        //正解不正解の判定と解説の表示
        await new Promise((resolve) => {
            const option_char = ['①','②','③','④'];
            const correct_answer = texts[id][7]; //答えの番号を格納
            //正解不正解の判定
            if(clicked == correct_answer) {
                //正解の場合の処理
                let content = '<p>〇 正解</p>';
                $('#comment_box').append(content);
                setTimeout(() => {
                    resolve();                    
                }, 1500);
            } else {
                //不正解の場合の処理
                let content = '<p>× 不正解 ';
                content += '正解は '+option_char[correct_answer-1]+'</p>';
                content += '<p>'+texts[id][8]+'</p>';
                content += '<button type="button" id="confirm">OK</button>';
                $('#comment_box').append(content);
            }
            //OKボタンが押されたら次の問題へ
            $('#confirm').on('click', function() {
                resolve();
            });
        });
    }
    //全問終了のメッセージ
    await new Promise((resolve) => {
        let content;
        content = '<p>トレーニング終了です。</p>';
        content += '<button type="button" id="confirm">最初に戻る</button>';
        $('#display').empty();
        $('#comment_box').empty();
        $('#display').append(content);
        //OKボタンが押されたら次の問題へ
        $('#confirm').on('click', function() {
            start_display();
            resolve();
        });
    });
}
//スタート画面
start_display();
</script>
</body>
</html>