はじめに
以前、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
で下記のように画面が表示されるかと思います。
Firebaseプロジェクトの作成
Firebaseに関しては、ドットインストールさんの動画が分かりやすいと思うので、もし見ていない方がいらっしゃれば先に見ておくと良いかと思います。
Firebaseでウェブサイトを公開してみよう | ドットインストール
Firebase にいき、ログインします。
Firebase は色々できるのですが、今回は Hosting サービスを使用します。
新しいプロジェクトを作成します。
プロジェクト名は my-blogs
、言語は日本語にします。
作成が完了すると、ローカルに 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コンポーネントを使用して作成します。
記事のサムネイルとタイトルが表示されるようにしますが、ちょっと困ったのは、 img
の src
にバインドしようとするとうまくいきませんでした。
下記の記事の方法を参考にさせていただきました。
Vue.jsでimgタグのsrcをバインドさせる際の注意点
<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.vue
と PostVuetify
を登録することで、対応したパスの時にそのコンポーネントが呼ばれるようになります。
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.js
を 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 を使用します。
この部分がよしなに差し変わります。
<v-content>
- <HelloWorld/>
+ <router-view></router-view>
</v-content>
ここまで書いて、 yarn serve
を実行したあと、 http://localhost:8080
、 http://localhost:8080/vuetify
にアクセスすると、画面が変わるかと思います。
さて、ただまだ画面のボタンから遷移はできないので、設定していきます。
Index.vueにrouter-linkを設定する
記事のカードが押されたら、記事詳細に差し代わるようにします。
その時は router-link を使用するのですが、 Vuetify 製のコンポーネントの多くは、 to
という属性をつけると、 router-link
として解釈してくれます。
そのため、 Index.vue
の v-card
タグに以下のように設定します。
<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
に登録します。
<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
に取り込みます。
<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-utils と Jest というテストフレームワークを使おうと決めていましたが、なかなか導入がうまくいきませんでした。
どうもVue CLI 3系だと何かしら変わっているのではないかと推測しました。(未確認の情報です。)
幸いなことに、Vue CLIのドキュメントを眺めていたところ、ユニットテストに関するモジュールの案内があったので、そちらを使用します。
- Unit Testing | Vue CLI
- https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest
テストツールは前置き通り、vue-test-utils と Jest です。
$ 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
修正後のファイルは下記のようになります。
// 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.vue
と PostVue.vue
は同じような感じなのでテストファイルは一つにまとめて、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.vue
は VueRouter
を使用しています。
VueRouter
を使用する場合は注意点があるようなので、下記の通りに実施します。
Vue Router と一緒に使用する | Vue Test Utils
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インスタンスが持つプロパティの確認をします。
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 で作成しています。
ブログに関するご意見もいただけると嬉しいです!
ありがとうございました!