経緯
- 我がチームが忘年会の幹事に任命される
- まじろう「出し物、、、なんかします?」
- 先輩「オールスター感謝祭・・・」
- 一同「は?」
- 先輩「オールスター感謝祭!!」
ということで、即興でnuxtによる四択クイズウェブシステムを作ったお話です。
ソースはgithubに設置しました。
https://github.com/majirou/allstar
また、開発にあたり以下の記事とソースを参考にさせていただきました。
オール◯ター感謝祭もどきアプリで社内イベントを乗り切る
私自身はVue派なので、Reactソースを眺めながら勉強になりました。
#基盤となる技術
websocket
Slackなどのチャット系サービスなどに使われている技術とのことで、
nodejsでは、socketioというライブラリを用います。
wiki からの引用ですが、メリットはありそうです。
従来の手法に比べると、新たなコネクションを張ることがなくなる・HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減る、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがある
実装概要
- フレームワークは Nuxt.js
- Nuxt上のExpress(
server/index.js
)にて、上記の socketio によるコネクション確立と各イベント定義+もろもろ - クライアントは三種類を用意しました
- admin: 問題やランキング表示をコントロール
- client: ユーザーが解答などを行うUI(主にスマホ想定)
- monitor: アナウンスやランキングを表示するための中央モニター
- 音源は鳴らせる仕組みにしましたが、あくまで個人的なお遊びの範囲で行うため音楽ファイルそのものはgithubにあげていません。
- 尚、CMから番組に戻った際の「でっで、でっででっでで、ででん!」というお馴染みの音は、荻野目洋子の「千年浪漫」という曲のラストです。
実装の肝
- サーバー側は、「受信するイベント」を
on(イベント名, 実行する関数)
にて定義する。 - クライアント側にイベントとデータを送るには、
emit(イベント名, データ)
にて実行する。 - クライアント側でも、「受信するイベント」を
on(イベント名, 実行する関数)
にて定義する。 - サーバー側へイベントとデータを送るには、
emit(イベント名, データ)
にて実行する。
と、双方向で「受け」と「送り」の「イベント名称」と「その処理」を定義していくことで、通信と処理が行えます。
クイズで必要となるイベント
- 司会「問題です」
- (サーバー) 「問題」イベントを送信
- (クライアント) 「問題」イベントを受信
- 司会「Ready Go」
- (サーバー) 「カウントダウン」イベントを送信
- (クライアント) 「カウントダウン」イベントを受信
- (クライアント) 解答をタップしたら、サーバーに解答内容を送る「解答」イベントを送信
- (サーバー) 「解答」イベントを受信 → 集計
に加え、
- Answer Check → 解答数表示
- 正解者はこちら → 解答表示、正解不正解の判定
- 予選落ち → 解答が遅かった順ランキングの生成と表示
- 早押しランキング → 上の逆
- 全員スタンドアップ → 解答権を復活させる
などなどが必要で、加えて音源と連動させたり、不正防止や成績管理など考えねばならない機能があります。
サーバー
コネクション確立時on("connection")
内にて、各種イベントを定義することで、該当するイベントに対するサーバー側の処理を実行します。
以下の場合、コネクション確立時に、readyGo
イベントを受け取った時、コンソール上に書き出し、その後クライアントら接続対象にstartQuetion
イベントを発行する処理となります。
当然、クライアント側にはstartQuetion
を受けた時の処理を書く必要があります。
const socketio = require('socket.io')
const io = socketio.listen(server)
io.on('connection', socket => {
socket.on('readyGo', text => {
console.log('readyGo', text )
io.emit('startQuetion', text)
})
})
クライアント
基本はサーバー同様ですが、クライアント側では、socketio-client
を使います。
以下の場合、マウント時に、socket.on
にて各イベントの処理を定義しています。
startQuestion
を受けたら、カウントダウンを開始=問題の始まりとなります。
逆に、クライアント側で解答をしたら、それをサーバー側に送るという処理は、socket.emit(イベント名, データ)
という形で実施します。
import ioClient from "socket.io-client"
export default {
data() {
return {
socket: ioClient('localhost'),
(中略)
mounted() {
this.socket.on("startQuetion", () => {
// カウントダウン開始
this.countDown()
(中略)
methods: {
answer(event) {
this.socket.emit("answer", { answer: event.target.value })
},
画面と機能の抜粋
基本的には、ユーザーがスマホ、モニターは中央の大画面、管理者はノートPCなどに映すことを想定しています。
ログイン画面
回答者を識別するために、ログインを行わせます。
今回は、番号とアカウントを用意して、それらをサーバー側で生存フラグと共に管理します。
あくまで余興なので、ガチガチのセキュリティはしません。
スマホが基本対象機器なので、QRコードを用意して入力の手間などを省くようにしました。
忘年会なので、各席にQRコードを印字しておくなどの演出しようかと思います。
不正防止
一度ログインした場合、再ログインなどは認めない方向にしています。
localstorageに記憶させ、ログイン情報があれば前回のログイン者の名前などを表示し、問題解答画面を表示します。
ただし、ログアウト(/logout)はできるURLは用意しています。
問題解答画面 /client
ユーザーのメインとなる画面です。
- 「Ready Go」時に、タイマーはカウントダウン開始。
- 「アンサーチェック」で回答数を表示
- 「正解はこちら」で、正解不正解の判定
- 四択のいずれかをタップ時に解答をサーバーに送る
などを、websocketイベントで処理をします。
管理画面 /admin
スタンドアップ、問題の送信、音源などの制御を行います。
進行上操作しやすいように、一画面に収まるよう調整をした・・・つもりです。
ユーザー側のUIと同様に、アクションはWebsocketにて、サーバーに送られ処理されます。
基本的にトリガーとなるアクションが多いため、司会との阿吽の呼吸が試されます。
全体向けモニター /monitor
大画面に映す、ユーザー全体が見る画面です。基本的にはクライアントに準じますが、ログインなどは当然不要ですし、回答する機能などは省いています。
感想
Websocketでの通信は非常に軽く、通常のウェブページの遷移のような画面の切り替えが発生しないのは非常にありがたく、
また、スマホを何台か並べてテストした際、同一のタイミングで画面が切り替わることが、当然といえば当然なのですが、不思議な面白さがありました。
そして、思いつくがままに、部活動のようなノリで作り上げたので、仕事とは異なる面白さがありました。
実際に、忘年会でお披露目するまでデバッグと演出をチームで練り上げ、無事終わらせられるかを後ほどご報告できたらと思います。