🌵

Nuxt3+Vuetify+VueUseでフロントで完結するWebサービスを作った

2024/03/01に公開

概要

https://warframecustomtracker.vercel.app/
という,Warframeで遊んでいる全世界のテンノ[1]向けWebサービスを作りました。
ソースコードはこれ
https://github.com/YuyaItabashi3594/warframecustomtracker

package.jsonの中身
package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "devDependencies": {
    "nuxt": "^3.10.0",
    "vue": "^3.4.15",
    "vue-router": "^4.2.5"
  },
  "dependencies": {
    "@mdi/font": "^7.2.96",
    "@nuxtjs/i18n": "^8.0.1",
    "@nuxtjs/tailwindcss": "^6.7.0",
    "@pinia/nuxt": "^0.5.1",
    "@unhead/vue": "^1.7.4",
    "@vueuse/components": "^10.4.1",
    "@vueuse/core": "^10.4.1",
    "@vueuse/nuxt": "^10.4.1",
    "axios": "^1.6.2",
    "postcss": "^8.4.23",
    "sass": "^1.68.0",
    "tailwindcss": "^3.3.2",
    "vuetify": "^3.3.17"
  }
}

なぜ作ったか

  • Vue3/Nuxt3/Vuetifyの開発の練習兼ポートフォリオ
  • 既に似たようなトラッカーはあるものの,フィルター機能がない点や,廃テンノにとっては不必要な情報があるので作る余地があった
  • 単にモノを作りたかった

使用技術/フレームワーク/API

https://nuxt.com/
Vue3のフレームワーク
https://vuetifyjs.com/en/
UIコンポーネントのフレームワーク,流行りのデザインを学ぶため及びVuetifyを採用している企業を割と見かけることから役立つと考え採用
https://vueuse.org/
なんか色々と便利な機能が入っている,今回はuseStorageuseDateformatを使用
https://v8.i18n.nuxtjs.org/
色々とvuei18nのバージョン[2]はあるけれどnuxt3の場合はこれっぽい
https://docs.warframestat.us/
公式API これをfetchしてます
https://vercel.com/docs
デプロイ用 個人製作でサクっと公開したい時にはVercelが楽に感じる

機能紹介(ゲーム編) エンジニア要素がないのでスキップ可

  • 自分のやりたいミッションを選択出来る:レリック回したいから通常掃滅,SE集めたいから鋼耐久のみ など
  • ページを見なくても確認出来る:タブのtitleに希望のミッションが今あるかどうかを教えてくれる機能があるので,画面を開いて別タブで動画見ながらでもあるか分かる

機能紹介(技術編)

VueUseのuseStorageを使った,設定の記憶機能

https://vueuse.org/core/useStorage/
VueUseにはuseStorageという,ブラウザのLocalstorageに分かりやすくアクセスできる機能があります。

import { useStorage } from '@vueuse/core'
const local = useStorage('保存名','初期値')

という形で初回登録&参照が出来ます。
登録した後はv-modelを用いて煮るなり焼くなりします。

<script setup>
// 一部分を抜粋
const enemyTypes = ['Grineer', 'Corpus', 'Infested', 'Orokin']
const missions = ['Defense', 'Survival', 'Excavation', 'Interception', 'Defection', 'Infested Salvage', 'Disruption']

const selectedArbitrationEnemyType = useStorage('selected-arbitration-enemy-type', ['Grineer', 'Corpus', 'Infested', 'Orokin'])
const selectedArbitrationMission = useStorage('selected-arbitration-mission', ['Defense', 'Survival', 'Excavation', 'Interception', 'Defection', 'Infested Salvage', 'Disruption'])
</script>
    <v-expansion-panels class="mb-2">
      <v-expansion-panel title="Setting">
        <template v-slot:text> // v-slot:textを用いてtext部分の中身を上書きしています
          <div class="flex flex-col mt-2">
            <v-select v-model="selectedArbitrationEnemyType" :label="$t('Enemy')" :items="enemyTypes" multiple>
              <template v-slot:selection="{ item, index }"> //同様にselection部分をv-chipで上書き
                <v-chip>
                  <span>{{ $t(item.title) }}</span>
                </v-chip>
              </template>
            </v-select>
            <v-select v-model="selectedArbitrationMission" :label="$t('Mission')" :items="missions" multiple>
              <template v-slot:selection="{ item, index }">
                <v-chip>
                  <span>{{ $t(item.title) }}</span>
                </v-chip>
              </template>
            </v-select>
          </div>
        </template>
      </v-expansion-panel>
    </v-expansion-panels>


このようになる

使用感

全ユーザーの統計データを取る必要がなかったり,モバイル版と連携してデータを同期する必要がない場合で,何かしら保存機能を付けたい場合に気軽に使える機能だと思います。

useHeadを用いた動的title

Nuxt3にはUnheadがデフォルトで入っていて,<head>内にRefやcomputedを用いて動的な処理を書くことが可能です。
https://nuxt.com/docs/api/composables/use-head

構造としては各コンポーネント内の選択されたミッションの数が変化したかをwatchし,変化があった場合にemitで数を渡し,computed getter[3]で値を出しています。

child
watch(preferredFissure, () => {
  emit('preferred-fissure-changed', preferredFissure.value.length)
})
parent
<script setup>

const preferredArbitrationMissionCount = ref(0)
const preferredFissureMissionCount = ref(0)
const preferredMissionCount = computed(() => preferredArbitrationMissionCount.value + preferredFissureMissionCount.value)

const changeArbitrationCount = (count) => {
  preferredArbitrationMissionCount.value = count
}

const changeFissureCount = (count) => {
  preferredFissureMissionCount.value = count
}

const titleCount = computed(() =>
  preferredMissionCount.value == 0 ? 'No quests available,Tenno.' : `You have ${preferredMissionCount.value} quests`
);

useHead({
  title: () => titleCount.value, //computed getter
  meta: [{
    name: 'description',
    content: 'Warframe Custom Tracker'
  }]
})

useSeoMeta({
  description: 'Warframe Custom Tracker tells you Arbitration,Fissure,Eidolon etc. available and you can customize your preferred missions.'
})

</script>

<template>
  <div>
    <Navbar />
    <div class="grid grid-cols-4 mx-10 gap-4">
      <Arbitration @preferred-arbitration-changed="changeArbitrationCount" /> //emit
      <div class="col-span-2">
        <Fissure @preferred-fissure-changed="changeFissureCount" />
      </div>
      <Events />
    </div>
  </div>
</template>

後はnuxt.config.tsのappを以下のようにすれば

nuxt.config.ts
app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
      titleTemplate: '%s - ' + 'WarframeCustomTracker',
    }
  }


こうなる(Firefox Nightly)

使用感

Vue.jsを使っていて通知機能を持つようなものを作る場合は是非使うべきものだと思います。

今後こう改善したい

i18nの設定もuseStorageを使いたい

ライフサイクルの関係で単純なコーディングではうまくいかなかった。

Fissureのタイマーの挙動を直す

Fissureが空であることによる Uncaught TypeError: o.value.forEach is not a function,Fissure内のeta関連の処理をする時に毎秒発生していてsetIntervalで悪さをしているUncaught TypeError: proxy set handler returned false for property '"eta"'のせいで,APIで取得した配列の内index=0のものにしかタイマーが適応されてないバグが発生している。

Expireしたら配列から要素を削除してリストから消す

現状だとAPI叩いた時のみ配列を操作しているので,これをもうちょっと最適化する

Settingのドロップダウンの挙動を軽くする

Vuetify公式Docsで試せる挙動より明らかに重いので,処理を見直す

脚注
  1. Warframeではプレイヤーの事をこう呼ぶ ↩︎

  2. vue2向け,vue3向け ↩︎

  3. computedはパフォーマンスの関係で非推奨としています。 ↩︎

Discussion