2020-06-02

東京都 新型コロナウイルス感染症対策サイトを読む その2

これの続きをやっていく
知ってることでもちゃんと改めて自分の言葉で書く
ポート3000はRailsで使ってしまうので、ポート5000に変更してやっている

nuxt.config.ts

server: {
  port: 5000
}

トップページ

Nuxt.jsは pages ディレクトリ内のVueファイルの木構造に沿って、自動でルーティングを作成してくれる
ということでトップページは pages/index.vue が該当する
レイアウトは layouts/default.vue

単なるVue.jsのファイルなので、以下の3つのセクションに分かれている

<template>〜〜〜</template>
<script lang="ts">〜〜〜</script>
<style lang="scss" scoped>〜〜〜</style>
  • JSはTypescriptで書かれている
  • CSSはscssで書かれていて、Vue.jsのscoped CSSで書かれている
    • scoped CSSは意外とバッティングするとか何とか話があるが、CSS Modulesとどっちがいいんだろうね?
    • レイアウトはscopedになってなかった

ということがわかった

ローディング画面

アプリを起動して最初に目に付くのがロード画面なので、それがどうなっているのか確認する

通常ロード画面はレイアウトの最上段に配置されるのが普通だと思うので、 layouts/default.vue を見るといかにもそれっぽいものがある

<v-overlay v-if="true" color="#F8F9FA" opacity="1" z-index="9999">
  <div class="loader">
    <img src="/logo.svg" alt="東京都" />
    <scale-loader color="#00A040" />
  </div>
</v-overlay>
  • v-overlay は属性ではないのでディレクティブではなく、Vuetify が提供するコンポーネント
    • Vuetify はパッケージを入れて、 nuxt.config.tsbuildModules に突っ込めば即使えるようになるっぽい
    • Vue.use(Vuetify) 的なことはいらないんだー。へー。
  • 当たり前だけど、 v-overlay だけ置いても何も表示されないので、 loader クラスのdivタグが本体
  • scale-loadervue-spinner により提供されており、 'vue-spinner/src/ScaleLoader.vue' からimportしているのがわかる

default.vueでよくわからないところ

せっかくなので、このままデフォルトレイアウトファイルを眺めていく
そして現時点ではっきりと説明できないところをチェックしていく

  • LocalData のところ

この部分↓

type LocalData = {
  hasNavigation: boolean
  isOpenNavigation: boolean
  loading: boolean
}

data(): LocalData {
    let hasNavigation = true
    let loading = true
    if (this.$route.query.embed === 'true') {
      hasNavigation = false
      loading = false
    } else if (this.$route.query.ogp === 'true') {
      hasNavigation = false
      loading = false
    }

    return {
      hasNavigation,
      loading,
      isOpenNavigation: false
    }
  },

まず、 type LocalData = 〜 の部分だけど、これはTypescriptの機能を使っているだけ(参考)
なぜ interface ではないのかはわからない(勉強しておきます)

では data(): LocalData {〜} はというと、これは型を宣言的に書く手法のようだ(Vue.jsでの参考)
すなわちこいつもTypescriptの記法

通常 data は以下のように定義するが

data() {
  return {
    count: 'a'
  }
},

これを↓のように記述することができる
data は関数なので、その戻り値の型を定義しているということになる
で、上の例では { count: number } の部分が LocalData に置き換わっているということ

data(): {
  count: number
} {
  return {
    count: 0
  }
},

Typescriptだけで書くとこれと同じような感じ

class Hoge {
  test(): { count: number } {
    return { count: 123 }
  }
}

const a = new Hoge
console.log(a.test()) // => { count: 123 }

methods 内や head でも同じ記述が使われているが、これと同じ意味

なお、ここstrictもしくはnoImplicitThisをtrueにする必要あり と書かれているが、このアプリでも tsconfig.json でtrueが設定されている

  • headメソッド

いつのまにか新しいメソッドでも生えたのか!?とか思ったけどそんなわけなかった
こいつはNuxt.jsのAPIで、HTMLのheadタグを設定することができる便利なやつらしい

  • getMatchMediaメソッド

まず window.matchMedia('(min-width: 601px)') の部分で「画面の横幅が601px以上であるかどうか」を判定することができる
window.matchMedia('(min-width: 601px)').matches でbooleanが返るので、よくif文とかの条件に指定されている

ここでは window.matchMedia('(min-width: 601px)').addListener(this.hideNavigation) のようにチェーンされているが
こうすることで「指定した画面幅になったら指定された関数を実行する」ことができるようになる
すなわち「画面の横幅が601px以上になったら、hideNavigation 関数を実行する」ということ
スマホみたいに狭い画面からPC表示に切り替わったときとかに発動する

そして matchMedia の戻り値は MediaQueryList なので、 getMatchMedia 関数にはその型が指定されている
コロナ対策サイトは601pxを境にレイアウトが切り替わるようになっている模様

  • MetaInfo

vue-meta によって提供されている
このへんに合致するオブジェクトが返る必要がある

  • locale(i18n)周り

nuxt-i18n を使って多言語対応を行なっている
設定ファイルはルートにある nuxt-i18n.config.ts

  • strategy: 'prefix_except_default' が指定されているので、日本語の場合はURLに/jaなどのプレフィックスをつけなくてもOK
    • その他の言語の場合はURL末尾に /en などのプレフィックスを付与する必要がある
  • detectBrowserLanguage によって nuxt-i18n が勝手にブラウザの言語を検出して、いい感じにリダイレクトしてくれる(すごい)
    • useCookie をtrueにすることで、検出・設定した言語をCookieに保存してくれる
    • cookieKey はCookieのキー名
  • localeファイルは assets/locales に存在している(キーが日本語なの斬新だった)
  • $t は単なる変換で、 $tc複数形に対応することができる、 $te は定義のある・なしでboolが返る
    • 基本的に $t を使えばよさげ
    • 結構 $tc 使ってるところあるけど、複数形とか関係なさそうで謎(ただの使い方間違い?)

default.vueで読み込まれているコンポーネントを眺める

  • DevelopmentModeMark
    • process.env の値を見て開発画面かどうかを表示しているコンポーネント
    • コンポーネントオプションとして namepropscomputed がある(computed は特に書くことないのでスルー)
      • name
        • コンソールから console.log とかを使って、Vueコンポーネントを調べた際、 VueComponent というデフォルトの名前で表示されるが、 name を指定することで、指定した名前でわかりやすく表示してくれるオプション
      • props
        • 親から子へデータを渡す時に定義するオプション
        • DevelopmentModeMarkコンポーネントは value という名前で値(文字列)を受け取ることができる(必須ではない)
        • しかし、受け取った値を使っている箇所がないのでなんの為に存在するのか謎(誰か理由を教えてくれ)
    • どういうわけか手元の環境ではこのコンポーネントの表示がされない
  • ScaleLoader
    • ↑でも書いたけどローディング画面に出てるアニメーション
  • SideNavigation
    • サイドメニューのコンポーネント
    • 画面幅が601px未満だと(SideNavigation-Body の部分が)非表示になる
    • $emit で親(default.vue)のメソッドを呼んでいる
      <v-icon
        class="SideNavigation-OpenIcon"
        :aria-label="$t('サイドメニュー項目を開く')"
        @click="$emit('openNavi', $event)"
      >
      
    • この部分がクリックされると、 $emit('openNavi', $event) の部分が発火し、親である default.vue が子コンポーネント(SideNavigation)を描画している
      <side-navigation
        :is-navi-open="isOpenNavigation"
        :class="{ open: isOpenNavigation }"
        @openNavi="openNavigation"
        @closeNavi="hideNavigation"
      />
      
    • の部分の @openNavi="openNavigation" が呼ばれる
    • openNavigationdefault.vue のメソッドなので、その処理が行われる
    • なお、 @click="$emit('openNavi', $event)" の第二引数である $event は、親の openNavigation の引数として利用可能(ここでは使ってないけど)
    • 参考 - やわらかVue.js
    • 参考 - 親と子の引数を渡すこともできる
  • NoScript
    • JSがオフになっている人向けのコンポーネント

その他細かいこと

  • import XXX from '@/hoge/fuga.vue'@ はアプリルートを示している
  • :属性="〜〜〜" はVueの省略記法で、 v-bind:属性="〜〜〜" と等価
  • @イベント="〜〜〜" も省略記法で、 v-on:イベント="〜〜〜" と等価
    • 以下の部分を例にすると
    <side-navigation
      :is-navi-open="isOpenNavigation"
      :class="{ open: isOpenNavigation }"
      @openNavi="openNavigation"
      @closeNavi="hideNavigation"
    />
    
    • :is-navi-open は子コンポーネント(SideNavigation)に値を渡している(props)
    • :class は条件によって open クラスを付与したり、外したりしている(ただの属性)
      • 単にクラスを付与するだけなら class="hoge" とbindする必要はない
    • @openNavi は子コンポーネントによって呼び出される($emit("openNavi"))イベント

長くなってきたのでとりあえず今回はこんなところで