初めてwebサービスを作ったので、学びを振り返ってみる。

はじめに

webサービスを1週間で作るイベント「web1week」で、とあるアプリを作成しました。
それが「SearchForPep(Pepを探して!)」です。

作ったサービス

watsumi.github.io

イベント

crieit.net

どうしてこのサービスを作ったのかということについては、 上の記事内の投稿で簡単に説明しているので、もし時間を持て余して仕方ないという方がいらっしゃれば読んでみてください。

この記事の目的

「SerchForPep」はステージを進むごとに増えていく人混みの中から、対象の人物(Pep)を探すという単純なゲームなのですが、 どうやって人を増やしていくか、どうやってPepを隠すか、正誤判定はどうやって行うかなどの機能を実装していく中で様々な学びがありました!

この記事では、実装していく中で詰まった箇所や、学んだことを振り返っていきたいと思います。

とはいっても、2日で作ったサービスなので、内容はかなり薄いです。長々と書いていきますが、ティッシュかってくらい薄いです。いやティッシュです。
では、早速ティッシュ、もといSearchForPepの機能要件から確認していきます。

Image from Gyazo

SearchForPepの機能要件

主な機能としては、こんな感じ。

  1. 問題を繰り返し表示する

  2. 正誤判定

  3. ステージごとに問題(人)を増やす

  4. ステージごとに問題(人)をシャッフルする

  5. TwitterにOGPを使って画像付きで結果を投稿する

うん!すぐ作れそう!って思ったら意外と時間溶かしポイントがあったので振り返っていきます。

振り返る前に

今回はweb1weekということでサクッと作ってしまいたかったため、DBを使わず、配列だけで実装していくことにしました。 配列は、「title」と「img」という2つのキーを持つシンプルな連想配列です。

questions:[ 
  {title: 'pep', img: require('/public/images/pep.svg')}
  .
  .
  .
  . 
],

ちなみにimgの値がrequire(...)という形になっているのは、問題画像を表示する際に、imgタグのsrcにこの値をモジュールとして読み込ませないとうまく表示できなかったからです。

それでは、機能を実装する際に詰まったポイントを確認していきます!

配列の重複するidの要素は上書きされる

実装したい機能:ステージごとに問題(人)を増やす

解決方法:ステージごとに新しい配列を生成する


正解した後、「NEXT」ボタンを押すことでステージ数のカウントが一つずつ増えていく(上記のGIF画像参照)のですが、

この数値(countNum)を利用して配列を追加できないかと考えました。

しかし、既存の配列に新しい配列を作って加えると重複するidの値が上書きされてしまうため、毎回配列ごと作り直してしまうことにしました。
その方がゲームの難易度も上がって面白いなとも思ったので。

具体的には、VuexのactionsでincreaseQuestionを定義し、「NEXT」ボタンを押すタイミングで呼び出すようにしました。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    questions:[
      {title: 'pep', img: require('/public/images/pep.svg')},
    ],
  },
  mutations: {
    addQuestion: (state, add_q) => {
      let tmp = state.questions
      state.questions = {...add_q, ...tmp};
    }
  },
  actions: {
    increaseQuestion(state,pow_num){
      let add_q = [];
      for (let i = 0; i < pow_num; i++){
        let num = Math.floor(Math.random() * 90 + 1 );
        if (num < 10) {num = '0'+num}
        add_q[add_q.length] = {title: i+92, img: require(`/public/images/${num}.svg`)};
      }
      return state.commit('addQuestion',add_q)
    }
  },
})
  • pow_numは算出プロパティ(Computed)で定義しています。
pow_num(){
      return Math.pow(1.5, this.countNum); //問題数を1.5のステージ数の累乗分増やすための数値
    },
  • public/imagesディレクトリに、タイトルが「01」~「91」までの画像を格納しているので、その中からランダムで画像をとってこれるようにしています。
let num = Math.floor(Math.random() * 90 + 1 );
  • 配列を結合する{...A, ...B}
  mutations: {
    addQuestion: (state, add_q) => {
      let tmp = state.questions
      state.questions = {...add_q, ...tmp};
    }
  },
  • state.questionsには、正解の配列(pepの情報)しか格納していないので、毎回生成される配列のid=1はpepになるように、生成した新しい配列のid=1を上書きしています

お前はいつObjectになったんだ?

実装したい機能:ステージごとに問題(人)をシャッフルする

解決方法:Object.keys()を使う(or 配列の長さを取得するのを諦める)


無事新しい問題(配列)が生成された後は、どうやってPepを隠すかを考え、配列をシャッフルすることにしました。

シャッフル自体は「フィッシャー–イェーツのシャッフル」というアルゴリズムを使えばできるようで、ググればたくさん参考になりそうな記事が出てきました。

例えばこちら。

shuffle: function(array) {
      for (let i = array.length - 1; i > 0; i--) {
        let r = Math.floor(Math.random() * (i + 1))
        let tmp = array[i]
        array[i] = array[r]
        array[r] = tmp
      }
      console.log(array)
      return array
    }

実際に紙と鉛筆を用意して確認してみると、確かに入れ替わる!
じゃあこれをquestions(配列)で置き換えて、新しい配列を取得した後にやってみようとした結果がこちら。

.
.
.
  methods:{
.
.
.
    async clickCountUp(pow_num){
      this.countNum++; //ステージ数をカウントする
      pow_num = this.pow_num; //問題数を取得
      await this.increaseQuestion(pow_num); //Vuexのactionsを呼ぶ
      this.handleCloseCorrectModal(); //正解時のモーダルを閉じる
      this.shuffle(this.questions);  //シャッフルする
    },
    shuffle(array) {
      console.log(this.questions) //確認用
      console.log(this.questions.length) //確認用
      for (let i = Math.floor(array.length)-1; i > 0; i--) { //フィッシャー–イェーツのシャッフルアルゴリズム
        let r = Math.floor(Math.random() * (i + 1))
        let tmp = array[i]
        array[i] = array[r]
        array[r] = tmp
      }
      return this.questions
    },

Image from Gyazo

入れ替わらへんのかーーーーい。
コンソールをみてみると、配列のところに{__ob__:Observer}って出てるし、配列の長さundifinedになってるし。
ってことはfor文の処理できてないやん。

Objectであるならば

とりあえずこいつ{__ob__:Observer}を調べた結果、どうも私が生み出したのは配列ではなくObjectである様子。 Objectであるならばとりあえずの解決策として、array.lengthObject.keys(array).lengthにすればいいはず。

    shuffle(array) {
      console.log(this.questions) //確認用
      console.log(Object.keys(this.questions).length) //確認用
      for (let i = Math.floor(Object.keys(array).length)-1; i > 0; i--) { //フィッシャー–イェーツのシャッフルアルゴリズム
        let r = Math.floor(Math.random() * (i + 1))
        let tmp = array[i]
        array[i] = array[r]
        array[r] = tmp
      }
      return this.questions
    },

Image from Gyazo

できた。

けど疑問が残る。

そうです。お前はいつObjectになったんだ?

Vue.jsのデータバインディング

配列を作っていたつもりでObjectを生み出していた原因を探るべく、データバインディングの仕組みについて調べました。
正直、理解し切れていないのですが、仮説として、ObjectになってしまったのはVueが配列の変化を検出できていないのが原因であると考えました。

そこで、mutationで定義していた配列の結合を{...A, ...B}これから、公式に定義されているメソッドsplice(...A, ...B)に変更してみることにしました。

.
.
.
  mutations: {
    addQuestion: (state, add_q) => {
      let tmp = state.questions
      state.questions.splice(...tmp,...add_q);
    }
  },

配列の長さのところも元に戻しておく。

.
.
.
    shuffle(array) {
      console.log(this.questions) //確認用
      console.log(this.questions.length) //確認用
      for (let i = Math.floor(array.length)-1; i > 0; i--) { //フィッシャー–イェーツのシャッフルアルゴリズム
        let r = Math.floor(Math.random() * (i + 1))
        let tmp = array[i]
        array[i] = array[r]
        array[r] = tmp
      }
      return this.questions
    },

Image from Gyazo

できた!!ちゃんとログも配列になってる!

でもこのままだと、既存の配列に新しい配列を加える形になってしまい、Pepが何度も登場してしまいます。
spliceするときに既存の配列を削除してから置き換えればいいのか?とか考えてみましたが、いい方法が見つからなかったので、結局、配列にするのは諦め、先述のObject.keysで実装しました。

配列の長さを取得するのを諦める

上記の方法で、シャッフルはできた訳ですが、そもそも配列の長さを取得しなくてもいいのでは?とも考えました。
今回は配列の長さ=問題数-Pepなので、array.lengthではなくpow_numに変えても同じことが実現できそうです。

結論だけ言うとできました。ただあまり綺麗な書き方ではないので、ここでの再現はやめておきます。

おわりに

ここまで読んでくださった方へ。
稚拙な記事に最後までお付き合いいただきありがとうございました。

「おわりに」を先に読もうとここまで飛ばれた方へ。
大したことは書かれてないので、人生でもうやることがないなって思ったら、読んでみてください。

Vueを勉強し始めたのが、ちょうど2週間前で、おそらく何言ってんだこいつってところが多々あるかと思います。
こうした方がいいよってところがあればご教示いただければ幸いです。

参考記事

【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う - Qiita

vuejsのv-forの中身をランダムにしたい – nozograph

Vue.js の observe あたり雑に追う - エンジニアですよ!

List Rendering — Vue.js

当サイトは、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。