LoginSignup
47
46

More than 5 years have passed since last update.

Vue.js(Vuetify)とFirebaseで簡単なブログを公開して、ちょっとテストも書く

Posted at

はじめに

以前、Vue.jsとRailsでTODOアプリのチュートリアルみたいなものを作ってみたという記事を作成しました。
ただ、フロントエンドを完全に分離するなら、やはり単独で切り出した方が良いかなと悶々と考えておりました。
今回は、Vue.js と Firebase で簡単なブログを作成してみたので、手順をまとめておきたいと思います。

サンプルはこちらになります。
https://my-blogs-158b8.firebaseapp.com/

なお、実行環境は下記の通りです。

  • Node.js 10.6.0
  • npm 6.1.0

Node.js のインストールは下記の記事が参考になりました。
Node.js と npm インストールとアップデート

Vuetify

VuetifyはVue.jsのマテリアルデザイン用フレームワークです。
デザインで楽をしたいので、今回導入しました。
なお、最新の1.1.4では、vue-cli の3系が必要なようです。

プロジェクトの作成

Vuetify の QuickStartを見ながら進めます。

Vue CLI-3 のインストール

$ npm install @vue/cli -g

Vueプロジェクトの作成

$ vue create my-app
# 初期設定。こだわりがなければ default で
? Please pick a preset: default (babel, eslint)
# package manager npm or yarn
? Pick the package manager to use when installing dependencies: Yarn

パッケージマネージャーは以後、 npm ではなく yarn を使用していきます。

Vuetifyの導入

$ vue add vuetify
# 全部Enterで良さそう
? Use a pre-made template? (will replace App.vue and HelloWorld.vue) Yes
? Use custom theme? No
? Use a-la-carte components? No
? Use babel/polyfill? Yes

起動させてみる

ここまで行けば、とりあえずで確認してみます。

$ yarn serve
http://localhost:8080

http://localhost:8080 で下記のように画面が表示されるかと思います。

スクリーンショット 2018-07-08 21.56.18.png

Firebaseプロジェクトの作成

Firebaseに関しては、ドットインストールさんの動画が分かりやすいと思うので、もし見ていない方がいらっしゃれば先に見ておくと良いかと思います。
Firebaseでウェブサイトを公開してみよう | ドットインストール

Firebase にいき、ログインします。
Firebase は色々できるのですが、今回は Hosting サービスを使用します。

スクリーンショット 2018-07-08 22.08.38 (1).png

新しいプロジェクトを作成します。

プロジェクト名は my-blogs 、言語は日本語にします。

スクリーンショット 2018-07-08 22.07.37.png

作成が完了すると、ローカルに firebase-tools を入れるよう案内されるので、ローカル環境を作成します。

プロジェクトにFirebaseの設定を追加

こちらの記事を参考にさせていただきました。
Vue.js によるアプリを Firebase で Hosting する最短の道

$ npm install -g firebase-tools

firebaseコマンドが使用できると思うので、ログインします。

$ firebase login

Firebaseのロゴが出たら成功です。
プロジェクトディレクトリ内でFirebaseの設定を初期化します。

$ firebase init hosting
  • どのプロジェクトの設定か聞かれるので、上記のFirebaseコンソールで作成下プロジェクト名を選択します。( my-blogs
  • What do you want to use as your public directory? という質問には dist で回答すると良いようです。
  • 他の質問はデフォルトで良さそうです。 これでプロジェクトディレクトリに firebase.json ができていればOKです。

デプロイしてみる

Vue.jsを公開ようにするには、まずビルドする必要があります。

$ yarn build

そしてFirebaseにデプロイします。

$ firebase deploy

これで、FirebaseのURLにアクセスすればローカルで構築したような画面になっているかと思います。
今後ある程度開発がまとまったら、適宜デプロイできるようになりました。

必要なコンポーネントを作成、編集

表示されている画面は、 src/App.vue に記載されています。
ヘッダーやフッターを修正する場合は、その画面を編集します。
ヘッダー、フッターのVuetifyコンポーネントは下記です。

このあたりはお好みなので、ドキュメントを読みながら好きなように修正していただくのが良いかと思います。
参考までに、私がサンプルを作成した際の差分は下記になります。
https://github.com/naoki85/my-blogs/commit/1bf479bca2d205811645a5c49f553d641c3cd7d8

記事一覧画面用のコンポーネント

後々使用するので、 src/components ディレクトリを作成して、 Index.vue を作成します。
Cardコンポーネントを使用して作成します。
記事のサムネイルとタイトルが表示されるようにしますが、ちょっと困ったのは、 imgsrc にバインドしようとするとうまくいきませんでした。
下記の記事の方法を参考にさせていただきました。
Vue.jsでimgタグのsrcをバインドさせる際の注意点

src/components/Index.vue
<template>
  <div>
    <v-card>
      <v-container
        fluid
        grid-list-lg
      >
        <v-layout row wrap>
          <v-flex xs12>
            <v-card color="grey lighten-5">
              <v-container fluid grid-list-lg>
                <v-layout row>
                  <v-flex xs7>
                    <div>
                      <div class="headline">About Vuetify</div>
                      <div>2018-07-10</div>
                    </div>
                  </v-flex>
                  <v-flex xs5>
                    <v-card-media
                      :src="image_src"
                      height="125px"
                      contain
                    ></v-card-media>
                  </v-flex>
                </v-layout>
              </v-container>
            </v-card>
          </v-flex>
        </v-layout>
      </v-container>
    </v-card>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        image_src: require("@/assets/logo.png")
      }
    }
  }
</script>

記事詳細のコンポーネントを作る

今回は静的なブログなので、記事ごとにコンポーネントを用意することを想定しています。
Vuetify の記事を書いている想定で、 src/components/PostVuetify.js というファイルを作成します。 ぶっちゃけ template の中身はなんでも良いです。

<template>
  <div>
    This blog written about Vuetify!!
  </div>
</template>

VueRouterの追加

https://router.vuejs.org/ja/
SPAのためのルータを提供してくれます。

$ yarn add vue-router

plugins ディレクトリに router.js を作成します。

ここに、先ほど作成した Index.vuePostVuetify を登録することで、対応したパスの時にそのコンポーネントが呼ばれるようになります。

plugins/router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../components/Index'
import PostVuetify from '../components/PostVuetify'
Vue.use(VueRouter)
export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
    { path: '/vuetify', component: PostVuetify },
  ],
})

この router.jsmain.js に登録します。

src/main.js
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import Router from './plugins/router'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
  router: Router,
  render: h => h(App)
}).$mount('#app')

VueRouter用のリンクに差し替え

App.vue

App.vue をベースにコンポーネントを出し入れするようにします。

そのために、 router-view を使用します。
この部分がよしなに差し変わります。

src/App.vue
<v-content>
-  <HelloWorld/>
+  <router-view></router-view>
</v-content>

ここまで書いて、 yarn serve を実行したあと、 http://localhost:8080http://localhost:8080/vuetify にアクセスすると、画面が変わるかと思います。
さて、ただまだ画面のボタンから遷移はできないので、設定していきます。

Index.vueにrouter-linkを設定する

記事のカードが押されたら、記事詳細に差し代わるようにします。
その時は router-link を使用するのですが、 Vuetify 製のコンポーネントの多くは、 to という属性をつけると、 router-link として解釈してくれます。

そのため、 Index.vuev-card タグに以下のように設定します。

src/components/Index.vue
<v-card color="grey lighten-5" to="/vuetify">

これで、カードをクリックすると、記事詳細ページに画面が切り替わると思います。
ついでに、ヘッダーメニューの「Home」も、押されたら一覧に戻るようにします。
これもVuetifyのコンポーネントなので to が使えます。
下記の差分のように変更するのが良いかと思います。
https://github.com/naoki85/my-blogs/commit/51b0156b9e33caf54d66cad79148da42cbda2a73

ここまでで、とりあえず形はできました。

記事を追加する場合は、

  • 新しく PostVuetify.vue のようなファイルを作成する。
  • router.js に登録する。
  • Index.vue に同じようなカードコンポーネントを登録する。

ただ、 Index.vue にあのカードコンポーネントをツラツラ追記していくのは長ったらしいので、少しまとめてみます。

Cardコンポーネントを独自コンポーネントに

新しく、 Vue.js に関する記事を書いたとして、 PostVue.vue というコンポーネントを作成します。
router.js に登録し一覧に表示させるようにしますが、Cardコンポーネントはそのままだとかなり長くなってしまうので、新しくラッパーコンポーネントを作成します。
(ループ文を使えば良いと思いますが、練習ということで。)

新しく、 src/components/PostCard.vue を作成します。
ここには、Cardコンポーネントをそのまま移植しますが、親コンポーネントから props を使って値を受け取ります。
(よくあるコンポーネントもこんな感じで作成されていると思います。)

親コンポーネントから指示されたい項目としては、タイトル、公開日、リンクのパス、画像のパスかと思うので、それぞれ props に登録します。

src/components/PostCard.vue
<template>
  <v-flex xs12>
    <v-card color="grey lighten-5" :to="to">
      <v-container fluid grid-list-lg>
        <v-layout row>
          <v-flex xs7>
            <div>
              <div class="headline">{{ title  }}</div>
              <div>{{ date  }}</div>
            </div>
          </v-flex>
          <v-flex xs5>
            <v-card-media
              :src="src"
              height="125px"
              contain
            ></v-card-media>
          </v-flex>
        </v-layout>
      </v-container>
    </v-card>
  </v-flex>
</template>

<script>
  export default {
    props: {
      title: String,
      date: String,
      to: String,
      src: String,
    }
  }
</script>

このコンポーネントを Index.vue に取り込みます。

src/components/Index.vue
<script>
  import PostCard from './PostCard'
  export default {
    components: {
      'v-post-card': PostCard,
    },
    // ...
  }
</script>

v-post-card というタグを登録し、v-card だった部分を置き換えます。
その際、 title などをタグ内で指定するようにします。
差分表示だとGithubのリンクの方が見やすいかもしれません。
https://github.com/naoki85/my-blogs/commit/f3c531c43c36f1f7882a05229741c58f5191aedd#diff-be1bbdeb1231c484c6590c15778f98c6
あとは、記事を追加するたびにこのコンポーネントを増やして行けば、通常よりかは短くなるのではないでしょうか。

ただ、渡しているデータ以外は同じなので、ループ文も使用します。
最終的には Index.vue は下記のようになっています。

<template>
  <div>
    <v-card>
      <v-container
        fluid
        grid-list-lg
      >
        <v-layout row wrap>
          <v-post-card v-for="post in posts"
            :title="post.title"
            :date="post.date"
            :to="post.to"
            :src="post.src"
          />
        </v-layout>
      </v-container>
    </v-card>
  </div>
</template>

<script>
  import PostCard from './PostCard'
  export default {
    components: {
      'v-post-card': PostCard,
    },
    data() {
      return {
        posts: [
          {
            title: "About Vuetify",
            date: "2018-07-10",
            to: "/vuetify",
            src: require("@/assets/logo.png")
          },
          {
            title: "About Vue.js",
            date: "2018-07-12",
            to: "/vuejs",
            src: require("@/assets/logo_vue.png")
          }, 
        ]
      }
    }
  }
</script>

posts に各記事の情報を持たせ、それを post in posts で取り出しています。
少しは追加しやすくなったのではないでしょうか。

テストの導入

テストツールのインストール

普段はRSpecやPHPUnitを使ってAPIのテストなんかを書いていますが、恥ずかしながら、JS関連のテストは書いたことはありませんでした。

事前に調べていたところ、vue-test-utilsJest というテストフレームワークを使おうと決めていましたが、なかなか導入がうまくいきませんでした。

どうもVue CLI 3系だと何かしら変わっているのではないかと推測しました。(未確認の情報です。)
幸いなことに、Vue CLIのドキュメントを眺めていたところ、ユニットテストに関するモジュールの案内があったので、そちらを使用します。

テストツールは前置き通り、vue-test-utilsJest です。

$ vue add @vue/unit-jest

このコマンドを実行後、 いくつかのファイルが書き換わります。

初めてのテストを実行してみる

重要な設定は、 package.json や新しくできた jest.config.js に記載されていますが、詳細は割愛します。
jest.config.js を編集することでテストカバレッジの設定もできるようなので、試してみてください。
Configuring Jest

とにもかくにもテストが実行できるかどうかを確認できた方が良いと思います。
デフォルトで、 tests/HelloWorld.spec.js というファイルができているかと思います。
これは src/components/HelloWorld.vue 用のテストファイルです。

テストの実行には以下のコマンドを使用します。

$ yarn test:unit
# 実際は vue-cli-service test:unit というコマンドが実行されている

コマンドを実行すると、失敗したような感じになるでしょうか?
Vuetify をインストールしている場合、デフォルトのテストはコケるかと思います。
これは、Vuetify のインストール時に src/components/HelloWorld.vue を書き換えているので、テストと齟齬が生じています。

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        6.016s

このテストを通るように修正していきます。

HelloWorld.spec.js の修正

やることとしては、Vuetifyを使用する宣言をします。
この方法は下記のドキュメントを参考にしました。
localVue | Vue Test Utils

修正後のファイルは下記のようになります。

tests/unit/HelloWorld.spec.js
// mount の他に createLocalVue もインポートする
import { mount, createLocalVue } from '@vue/test-utils'
// Vuetify をインポートする
import Vuetify from 'vuetify'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    // Vueインスタンスを別で作成する
    const localVue = createLocalVue()
    // そのVueインスタンスにVuetifyを取り込む
    localVue.use(Vuetify)
    const msg = 'new message'
    const wrapper = mount(HelloWorld, {
      // マウントするVueインスタンスを指定
      localVue,
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

これでテストを実行しても、まだ失敗すると思います。
ここで無駄に悩んでしまったのですが、上記のテストは props.msg で渡した文字列がちゃんと描画されているのかを確認していますが、そもそも src/components/HelloWorld.vue では msg を表示していませんでした。

src/components/HelloWorld.vue の修正

<h1>{{ msg }}</h1> というタグを適当な場所に追加してください。

<template>
  <v-container fluid>
+    <h1>{{ msg }}</h1>
    <!-- ... -->

これでテストを実行すると、うまく行くはずです。

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        6.269s

ここまでの差分は下記になりますので、分かりづらければ参考にしてください。
https://github.com/naoki85/my-blogs/commit/f969910ac0c097615b0e0b7224cce4c8b72ff613

記事詳細のテストを作る

記事詳細はほぼHTMLのみのコンポーネントなので、サンプル同様、タイトルだけチェックするくらいで終わりにします。 PostVuetify.vuePostVue.vue は同じような感じなのでテストファイルは一つにまとめて、Post.spec.js とします。

tests/unit/Post.spec.js
import { mount, createLocalVue } from '@vue/test-utils'
import Vuetify from 'vuetify'
import PostVuetify from '@/components/PostVuetify.vue'
import PostVue from '@/components/PostVue.vue'

describe('PostVuetify.vue', () => {
  it('renders title', () => {
    const localVue = createLocalVue()
    localVue.use(Vuetify)
    const wrapper = mount(PostVuetify, {
      localVue
    })
    expect(wrapper.text()).toMatch('About Vuetify')
  })
})

describe('PostVue.vue', () => {
  it('renders title', () => {
    const localVue = createLocalVue()
    localVue.use(Vuetify)
    const wrapper = mount(PostVue, {
      localVue
    })
    expect(wrapper.text()).toMatch('About Vue.js')
  })
})

カードコンポーネントのテストを書く

一覧画面のカードコンポーネントのラッパー用に作成した PostCard.vue のテストを書きます。
props で渡した値が描画されているかのチェックかと思うので、ちょっとサボってタイトルだけチェックします。
(本当はDOMを取得して確認した方が良いのかな?)

なお、 PostCard.vueVueRouter を使用しています。
VueRouter を使用する場合は注意点があるようなので、下記の通りに実施します。
Vue Router と一緒に使用する | Vue Test Utils

tests/unit/PostCard.spec.js
import { mount, createLocalVue } from '@vue/test-utils'
import Vuetify from 'vuetify'
import VueRouter from 'vue-router'
import PostCard from '@/components/PostCard.vue'

const localVue = createLocalVue()
localVue.use(Vuetify)
localVue.use(VueRouter)
const router = new VueRouter()

describe('PostCard.vue', () => {
  it('renders props when passed', () => {
    const wrapper = mount(PostCard, {
      localVue,
      router,
      propsData: { title: 'About Ruby', date: '2018-07-14', to: '/ruby', src: '/img/logo.png' }
    })
    expect(wrapper.text()).toMatch('About Ruby')
  })
})

一覧画面のテストを書く

一覧画面のテストですが、 PostCard.vue は別でテストしているので、ここはスタブを返すようにします。
スタブを使用する | Vue Test Utils

ただ、上記をスタブにすると、描画関係で確認することがないような気がするので、Vueインスタンスが持つプロパティの確認をします。

tests/unit/Index.spec.js
import { mount, createLocalVue } from '@vue/test-utils'
import Vuetify from 'vuetify'
import VueRouter from 'vue-router'
import Index from '@/components/Index.vue'

const localVue = createLocalVue()
localVue.use(Vuetify)
localVue.use(VueRouter)
const router = new VueRouter()
const wrapper = mount(Index, {
  localVue,
  router,
  stubs: ['v-post-card']
})

describe('Index.vue', () => {
  it('count data.posts', () => {
    expect(wrapper.vm.$data.posts.length).toBe(2)
  })
})

とりあえずこのくらい書いて、全部テストが通ることを確認します。
ちなみに、それぞれのファイルだけテストを実行したい場合は、引数でファイル名を与えます。

$ yarn test:unit tests/unit/Index.spec.js

さいごに

こちらの記事は、以前個人ブログの方で書かせていただいたものをまとめたものになります。
よろしければそちらもご覧ください。
ブログ自体は Vue.js + Ruby on Rails で作成しています。

ブログに関するご意見もいただけると嬉しいです!
ありがとうございました!

47
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
46