【教師オリジナルアプリを作ろう】はじめてのVue.js 英語・語句選択問題アプリを作ってみる

これまで,色々 web アプリを作る上でずっと jQuery を使用してきたのですが,やろうとすることが増えるにつれコードがひたすら煩雑化してきて,あとからコード見ても何をやっているのか理解し直すののにやたら時間がかかるようになり,新しいことやろうという気分になれない,という悪循環に陥っていました。

問題を解決するために,jQuery を捨てて他のフレームワークを使用することにしました。色々調べた結果,ニーズに最も合うものとして Vue.js を使ってみることにします。

Vue.js で何が実現できるのか?

jQuery から Vue.js に乗り換える利点についての詳細は他所に譲るとして,今回私が実際にコードを組んでみた感想は,コードが短くなって,何をやってるか分かりやすいということです。

実際,何がどう違うのかを論じるよりも,コードを組んでみた方が早いでしょう。ここでは,英文法の語句選択問題を実行する web アプリを実際に作って,Vue.js の使い方を習得していきます。

英文法・語句選択アプリ

今回作成するアプリの画面はおおむね上のようなものです。画面上に日本文,英文,択肢のボタンが表示され,ボタンをクリックすると正解・不正解を判定します。

今回は,問題文を 4 つ用意しました。それらの問題をすべて終えると,終了のメッセージが表示されるようにします。

Vue.jsの読み込み

実際のコードを見ていきましょう。

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

まず,<head></head>の中で,vue.jsを読み込みます。これで,Vue.jsが使えるようになります。

問題文のデータ

先に今回使用するデータを見ておきます。

	const all_texts = [
	{
	"ja_st": "私の父は私にあまりに働きすぎないように言った。",
	"select_st": "My father told me # work too hard.",
	"select_opt": ["not to","don't","not","without"],
	"select_ans": "not to"
	},
	{   
	"ja_st": "私は彼の新しい家を気に入ったが,それがあれほど小さいとは予想していなかった。",
	"select_st": "I liked his new house, but I hadn't expected it # so small.",
	"select_opt": ["be","of being","to be","to being"],
	"select_ans": "to be"
	},
	{
	"jp_st": "私たちはとても疲れていたので,ほとんど立ち上がれなかった。",
	"select_st": "We were # we could hardly stand up.",
	"select_opt": ["tired so that","tired so as","so tired as","so tired that"],
	"select_ans": "so tired that"
	},
	{
	"jp_st": "それはとても難しい作業だったので,実際に終わらせた人々はほとんどいなかった。",
	"select_st": "It was # that few people actually finished it.",
	"select_opt": ["so difficult task","such a difficult task","such difficult tasks","task so difficult"],
	"select_ans": "such a difficult task"
	}];

jp_stは日本文,select_stは問題として表示する英文です。文字列内の#の部分はあとから下線に置き換えます。

select_optは選択肢の配列です。配列の内容に応じて 4 つのボタンが表示されます。また,select_ansは答えです。ボタンをクリックしたとき,クリックした文字列とこの文字列が一致するかどうかで,正解・不正解を判定します。

Vueの構造

let id = 0;
let app = new Vue({
	el: '#root',
	data: {
		ja_st: all_texts[id].ja_st,
		en_st: all_texts[id].select_st.replace('#', '______'),
		select_opt: all_texts[id].select_opt,
		description: '',
		btn_next: false
	},

ここからJavaScriptの記述です。まず,new Vue()でVueをインスタンス化して,オブジェクトを送ります。

まず,el: '#root'として,Vue.js を使った表示を行いたいブロック要素を指定します。ここでは,<div id="root"></div>の中でコードが動くことになります。

次にオブジェクトdataを作ります。dataはキーと値がセットになったものを書き,あとでHTML側からキーを指定すると,それに応じた値が表示される仕組みです。

最初はid=0となっているので,ja_st: all_texts[id].ja_stは,ja_st: "私の父は私にあまりに働きすぎないように言った。"と同じことです。HTML側でja_stを指定するとその部分に,"私の父は私にあまりに働きすぎないように言った。"が表示されます。

次のen_stは英文です。.replace()を用いて文字列の#を下線に置き換えたものを格納します。

select_optには選択肢の配列を格納します。

descriptionは正解・不正解の判定を表示する部分です。初めは空にしておきます。

btn_nextは次の問題に進むボタンを指します。これがtrueの状態になると,ボタンが表示され,falseでは表示されません。初めは表示しないので,falseにしておきます。

Vue.jsの便利なところは,あとでこのオブジェクトの中身を書き換えるだけで,画面に表示される文字を切り替えることができる点です(DOM操作が不要)。今回のように文法問題を次々と表示していくような場合に役立ちます。

こうして作成したオブジェクトの要素をHTMLの側から呼び出して画面上に表示していきます。

ブロック要素

<body>
<div id="container">
	<p id="header">英文法トレーニングwebアプリ fasgram Vue.js 版</p>
	<div id="root" class="round-white">
		<p id="ja_st" v-html="ja_st"></p>
		<p id="en_st" v-html="en_st"></p>
		<button class="btn-option round-white"
			v-for="option in select_opt"
			@click="judgement(option)"
			v-html="option">
		</button>
		<p v-html="description"></p>
		<button id="btn_next" class="btn-option round-white" 
			v-if="btn_next"
			@click="go_next">
			次へ
		</button>
	</div>
</div>

<body>の内容です。一つずつ見ていきましょう。

<div id="container">

まず,表示する要素全体をdiv要素で囲みます。これは,画面の表示幅を指定するためで,スタイルシートで#containerの横幅を680pxにしています。

	<div id="root" class="round-white">

Vue.js で表示を制御する部分を#rootとして指定します。このように,Vue.js は「ここのブロック要素に対して操作を行うよ」という部分をあらかじめ決めておく必要があります。反対に言えば,このブロック要素以外の部分では Vue.js のコードは動きません。

		<p id="ja_st" v-html="ja_st"></p>
		<p id="en_st" v-html="en_st"></p>

日本文と英文を表示するブロック要素を作ります。idはスタイルシートを適用するために書いています。

次に v-html という見慣れない構文が出てきます。これが Vue.js のコードです。v-html="ja_st"と記述すると,dataオブジェクトの中にあるja_stに格納された文字列を表示します。

実は一般的には,<p> {{ ja_st }} </p>のように書きます。ただし,この書き方では文字列に<span></span>などのHTMLタグを使うことができません。v-htmlを使うと,文字列にHTMLタグを使うことができるためため,今回はv-htmlで統一しています。

		<button class="btn-option round-white"
			v-for="option in select_opt"
			@click="judgement(option)"
			v-html="option">
		</button>

選択肢のボタンを設置します。

v-forは繰り返し処理を行います。動きとしてはforeach()と同じような感じです。

select_optには選択肢の文字列が配列として格納されています。例えば,select_opt=["not to","don't","not","without"]のようになっています。

v-forは配列から値を一つずつ取り出してoptionに格納し,繰り返し処理を行います。

こうして取り出した値をv-html="option"で表示していきます。

上のサンプル画像を見ると,ボタンが 4 つ表示されているのが分かると思います。このように,配列の要素の数だけボタンを表示するというような処理が短いコードで書けるところがVue.jsの良さです。もちろん同じように,配列の要素をテーブルやリストに表示することもできます。

ボタンクリック時の処理をメソッドに書く

ボタン要素の中に,もう一つ@click="judgement(option)"という記述があります。これはボタンがクリックされたときにjudgementという正解・不正解の判定を行うメソッドを実行するという意味です。judgement(option)とすることで,optionに格納されている文字列をメソッドに渡します。

メソッドとは何か?について,実際に行っていることを見た方が早いでしょう。

	methods: {
		judgement: function(option) {
			this.en_st = all_texts[id].select_st.replace('#',
				'<span style="color:#ff6600">'+all_texts[id].select_ans+'</span>');
			if(option === all_texts[id].select_ans) {
				this.description = '<p style="color:#ff6600">正解</p>'
			} else {
				this.description = '<p style="color:#6699ff">不正解</p>'
			}
			this.btn_next = true;
		},

メソッドの中身です。この部分は先ほど書いたdataの下に書いていきます。結局行っていることは関数の呼び出しです。

		judgement: function(option) {

ボタンをクリックしたときにキーであるjudgementとボタンの文字列optionを指定しました。これを受け取ります。

			this.en_st = all_texts[id].select_st.replace('#',
				'<span style="color:#ff6600">'+all_texts[id].select_ans+'</span>');

en_stの中身は英文でした。最初は My father told me ______ work too hard. のように文の一部が下線で表示されています。ボタンをクリックすると,My father told me not to work too hard. のように,正解を赤文字で表示します。

ここでは,.replace()を用いて#の部分を正解の文字列.select_ansに置き換え,それを<span style="color:#ff6600"></span>で囲んで赤文字にしています。

このように,en_stに値を代入するだけで,画面上の表示に反映されます。いわゆるDOM操作を書かなくてもよいところが,Vue.jsを使う利点です。

en_stの前にthisがついているのは,en_stが同じオブジェクト内に格納されているキーだからです。Vue()の中で作ったキーを使うときには先頭にthis.を付けるものとして理解しておけば良いでしょう。

if(option === all_texts[id].select_ans) {
	this.description = '<p style="color:#ff6600">正解</p>'
} else {
	this.description = '<p style="color:#6699ff">不正解</p>'
}

正解・不正解の判定を行います。ボタンの文字列optionselect_ansが一致すれば,HTML側で<p v-html="description"></p>と書いた部分に正解の文字が表示されます。一致しなければ不正解と表示されます。

ここも上と同じように,オブジェクトdataの値を書き換えることで,画面上の表示も自動的に変更される仕組みです。

表示を次の問題に切り替える

			this.btn_next = true;

最後に,「次へ」のボタンを表示します。btn_nextの値がtrueのとき,ボタンが画面に表示され,falseのときには表示されません。ここで,ボタンを表示して次の問題に進みます。

		go_next: function() {
			id += 1;
			if(id === all_texts.length) {
				this.description = 'すべての問題が終了しました。';
				this.btn_next = false;
			} else {
				this.ja_st = all_texts[id].ja_st;
				this.en_st = all_texts[id].select_st.replace('#', '______');
				this.select_opt = all_texts[id].select_opt;
				this.description = '';
				this.btn_next = false;
			}
		}

HTML側で,次に進むボタンには@click="go_next"が書かれていました。ボタンをクリックすると,上の関数が実行されます。

			id += 1;

問題番号を一つ増やします。

if(id === all_texts.length) {
				this.description = 'すべての問題が終了しました。';
				this.btn_next = false;

all_texts.lengthは配列の大きさを表します。今回は問題を 4 つ用意したので,all_texts.length=4です。idは0から開始するので,id=3のときには 4 番目の問題が表示されています。したがって,id=4のときにはすべての問題を終えているので,その旨のメッセージを表示し,次に進むボタンをfalseにして消します。これで,アプリとしての動作は終了です。

			} else {
				this.ja_st = all_texts[id].ja_st;
				this.en_st = all_texts[id].select_st.replace('#', '______');
				this.select_opt = all_texts[id].select_opt;
				this.description = '';
				this.btn_next = false;
			}

問題番号が最後まで進んでいない時点では,こちらが実行されます。

idを一つ増やして,次の問題の日本文,英文,選択肢をオブジェクトdataのキーに格納していきます。先ほどと同じように,この時点で画面上の表示も書き換えられます。descriptionは再び空にして,正解・不正解の文字を消し,btn_next=falseで次に進むボタンを消します。

まとめ

コードの解説は以上です。Vue.jsを使うには新たな知識を学ぶ必要がありますが,jQueryで同じwebアプリを作ったときに比べて,JavaScript側のコードはかなり短くなりました。この状態であれば,ここから機能を増やしていってもコードが煩雑になるのを防ぐことができそうです。

最後にコード全体を示します。今回はサンプルなので,通常cssファイルに記述するスタイルシートをhtmlの中に直接記述しています。スタイルシートの中にある@importはGoogle Fontsを読み込んでいる部分です。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>fasgram 英文法トレーニングwebアプリ Vue.js 版</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap');
body{font-family:'Noto Serif','Kosugi Maru',serif;font-size: 24px;
	background-color: #fafafa;}
#header{font-size:0.75rem;}
#container{max-width:680px;}
#ja_st{font-size:1.0rem;}
#en_st{font-size:1.25rem;}
#btn_next{width:100%;}
.round-white{margin:5px 2px;padding: 1.25rem 1.25rem;
	border:1px solid #ccc;border-radius:0.5em;
	background:#fff;box-shadow:0px 1px 3px rgba(0, 0, 0, 0.16);
	outline: none;}
.btn-option{font-family:'Noto Serif','Kosugi Maru',serif;font-size:1.25rem;}
</style>
</head>
<body>
<div id="container">
	<p id="header">英文法トレーニングwebアプリ fasgram Vue.js 版</p>
	<div id="root" class="round-white">
		<p id="ja_st" v-html="ja_st"></p>
		<p id="en_st" v-html="en_st"></p>
		<button class="btn-option round-white"
			v-for="option in select_opt"
			@click="judgement(option)"
			v-html="option">
		</button>
		<p v-html="description"></p>
		<button id="btn_next" class="btn-option round-white" 
			v-if="btn_next"
			@click="go_next">
			次へ
		</button>
	</div>
</div>
<script>
const all_texts = get_texts();
let id = 0;
let app = new Vue({
	el: '#root',
	data: {
		ja_st: all_texts[id].ja_st,
		en_st: all_texts[id].select_st.replace('#', '______'),
		select_opt: all_texts[id].select_opt,
		description: '',
		btn_next: false
	},
	methods: {
		judgement: function(option) {
			this.en_st = all_texts[id].select_st.replace('#',
				'<span style="color:#ff6600">'+all_texts[id].select_ans+'</span>');
			if(option === all_texts[id].select_ans) {
				this.description = '<p style="color:#ff6600">正解</p>'
			} else {
				this.description = '<p style="color:#6699ff">不正解</p>'
			}
			this.btn_next = true;
		},
		go_next: function() {
			id += 1;
			if(id === all_texts.length) {
				this.description = 'すべての問題が終了しました。';
				this.btn_next = false;
			} else {
				this.ja_st = all_texts[id].ja_st;
				this.en_st = all_texts[id].select_st.replace('#', '______');
				this.select_opt = all_texts[id].select_opt;
				this.description = '';
				this.btn_next = false;
			}
		}
	}
});
//問題文データ
function get_texts() {
	const all_texts = [
	{
	"ja_st": "私の父は私にあまりに働きすぎないように言った。",
	"select_st": "My father told me # work too hard.",
	"select_opt": ["not to","don't","not","without"],
	"select_ans": "not to"
	},
	{   
	"ja_st": "私は彼の新しい家を気に入ったが,それがあれほど小さいとは予想していなかった。",
	"select_st": "I liked his new house, but I hadn't expected it # so small.",
	"select_opt": ["be","of being","to be","to being"],
	"select_ans": "to be"
	},
	{
	"jp_st": "私たちはとても疲れていたので,ほとんど立ち上がれなかった。",
	"select_st": "We were # we could hardly stand up.",
	"select_opt": ["tired so that","tired so as","so tired as","so tired that"],
	"select_ans": "so tired that"
	},
	{
	"jp_st": "それはとても難しい作業だったので,実際に終わらせた人々はほとんどいなかった。",
	"select_st": "It was # that few people actually finished it.",
	"select_opt": ["so difficult task","such a difficult task","such difficult tasks","task so difficult"],
	"select_ans": "such a difficult task"
	}];
	return all_texts;
}
</script>
</body>
</html>