簡易ブログ作ってみた!パート1(Nuxt.js & Firebase & Tailwind CSS)
はじめに
Nuxt.js
のアウトプットとしてFirebase
と連携し, 簡単なブログを作成する手順をまとめました。
この記事ではfirestoreにデータ保存, firestoreからのデータ取得この2点の達成を目指します!
今回の実例はgithub
にて管理しております。是非御覧になってください🙌
Firebaseとの連携
firebaseとの連携を図るために, 必要なモジュールを導入します。
yarn add firebase @nuxtjs/firebase
以下の画像に従って, FirebaseConfigをコピーします。
コピーした後, エディタに .env
ファイルを作成し貼り付けます。
API_KEY='~~~',
AUTH_DOMAIN='~~~',
PROJECT_ID='~~~'
STORAGE_BUCKET='~~~',
MESSAGING_SENDER_ID='~~~',
APP_ID='~~~',
MEASUREMENT_ID='~~~',
コピペした値をnuxt.config.ts
のmodules
に以下のように記述します。
import { NuxtConfig } from '@nuxt/types'
const config: NuxtConfig = {
...
modules: [
[
'@nuxtjs/firebase',
{
config: {
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID,
measurementId: process.env.MEASUREMENT_ID,
},
services: {
auth: true,
firestore: true,
},
},
],
],
...
}
export default config
以上で, Firebaseと連携が出来ました!
ベースとなるコード
Headerは常に最上部に置いておきたいので, layouts/default.vue
を作成します。
<template lang="">
<div>
<TheHeader @sidenavToggle="displaySidenav = !displaySidenav" />
<nuxt /> <!-- ここに各vueファイルが入ります -->
</div>
</template>
<script lang="ts">
import TheHeader from '@/components/Navigation/TheHeader.vue'
import Vue from 'vue'
export default Vue.extend({
components: {
TheHeader,
},
data() {
return {
displaySidenav: false,
}
},
})
</script>
TheHeader
やCustomButton
のコードはトグルボタンに置いておきます。
TheHeader.vue
<template lang="">
<div class="h-16">
<header class="the-header">
<TheSideNavToggle @toggle="$emit('sidenavToggle')" />
<div class="ml-10 font-mono text-2xl font-bold text-white">
<nuxt-link to="/">Nuxt BLOG</nuxt-link>
</div>
<div class="spacer"></div>
<CustomButton
@click="$router.push('/admin/new-post')"
class="mr-10 font-mono font-bold text-white"
>New Blog</CustomButton
>
</header>
</div>
</template>
<script lang="ts">
import TheSideNavToggle from '@/components/Navigation/TheSideNavToggle.vue'
import CustomButton from '@/components/UI/CustomButton.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'TheHeader',
components: {
TheSideNavToggle,
CustomButton,
},
})
</script>
<style scoped>
.the-header {
width: 100%;
position: fixed;
height: 60px;
display: flex;
justify-content: space-around;
align-items: center;
background-color: black;
z-index: 100;
box-sizing: border-box;
padding: 0 20px;
}
.spacer {
flex: 1;
}
</style>
CustomButton.vue
<template lang="">
<button
class="px-4 py-2 font-semibold border rounded"
v-bind="$attrs"
v-on="$listeners"
>
<slot />
</button>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'CustomButton',
props: {
btnStyle: {
type: String,
default: '',
},
},
})
</script>
これでHeaderが最上部に固定されます。
Firestoreにデータ保存
まずは状態管理のパッケージであるVuex
を用いてBlogの情報(author,titile,thumbnailLink,content)を管理していきます。
Nuxt.js
ではstoreディレクトリに, Vuexストアのファイルが格納します。
VuexストアはNuxtに最初から付属していますが, デフォルトでは無効になっています。このディレクトリにindex.tsファイルを作成すると, ストアが有効になります。
import Vuex from 'vuex'
import { ActionContext } from 'vuex/types'
import { Blog, Id } from '../types/index'
import firebase from 'firebase/compat/app'
export const state = () => ({})
export type RootState = ReturnType<typeof state>
const createStore = () => {
return new Vuex.Store({
state: {
loadedPosts: [] as Blog[],
},
mutations: {
setPosts(state, posts) {
state.loadedPosts = posts
},
addPost(state, post: Blog) {
state.loadedPosts.push(post)
},
},
actions: {
async addPost(vuexContext, post: Blog) {
const createdPost = {
...post,
updatedDate: new Date(),
}
return await firebase
.firestore()
.collection('blog')
.add({
...post,
updatedDate: new Date(),
})
.then(
(result) =>
vuexContext.commit('addPost', { ...createdPost, id: result.id }) //result.idには一意なID
)
.catch((e) => console.log(e))
},
},
})
}
export default createStore
ポイント
- stateにloadedBlogというブログの情報(author,titile,thumbnailLink,content)を管理
- mutationsはStateを更新するメソッド。非同期処理でなければならない。
- actionsはMutationsを介して, Stateを更新するメソッド。同期処理である必要がある。
-
collection('~').add({})
によってドキュメントIDを自動的に生成(任意のドキュメントIDを付与する場合はcollection("~").doc("~").set({})
)
その後,New Blog
(CustomButton)を押すした後の遷移先(ブログ作成ページ)を作っていきます。
<template lang="">
<section class="w-2/5 mx-auto my-5">
<AdminPostForm @submit="onSubmitted" />
</section>
</template>
<script lang="ts">
import Vue from 'vue'
import AdminPostForm from '@/components/Admin/AdminPostForm.vue'
import { Blog } from '../../../types/index'
export default Vue.extend({
components: {
AdminPostForm,
},
methods: {
onSubmitted(postData: Blog) {
this.$store.dispatch('addPost', postData).then(() => {
this.$router.push('/')
})
},
},
})
</script>
ポイント
onSubmitted
について
- storeのactions(addPost)を呼び出す
- addPostは非同期処理だったため, 呼び出したその後の処理(
pages/index.vue
に画面遷移)はthenで繋ぐ
自分用メモ
- actionsを呼び出すときは
store.dispatch('')
で呼び出す必要がある - mutationsを呼び出すときは
store.commit('')
で呼び出す必要がある - この2点の違いは同期処理(mutaitions)か非同期処理(actions)かの違い
v-on(@)
について
- onSubmittedメソッドをsubmitイベントリスナをバインド(縛る)
AdminPostForm
のコードは以下のトグルボタンに置いておきます。
ポイントとなる箇所についてはコメントアウトしています。
AdminPostForm.vue
<template lang="">
<!-- submitイベントは<form>で発生する -->
<!-- submitイベントが本来, action属性での画面遷移を想定しているので, 意図しない画面遷移を避けるためにpreventを使う -->
<form @submit.prevent="onSave" class="text-white">
<div class="flex justify-between mt-4 space-x-4">
<AppControlInput class="w-1/2" v-model="editedPost.author"
>Author Name</AppControlInput
>
<AppControlInput class="w-1/2" v-model="editedPost.title"
>Title</AppControlInput
>
</div>
<AppControlInput v-model="editedPost.thumbnail"
>Thumbnail Link</AppControlInput
>
<AppControlInput control-type="textarea" v-model="editedPost.content"
>Content</AppControlInput
>
<div class="flex justify-end">
<CustomButton type="submit" class="mr-5 text-white bg-blue-700"
>Save</CustomButton
>
<CustomButton
type="button"
btn-style="red"
class="bg-red-700"
@click="onCancel"
>Cancel</CustomButton
>
</div>
</form>
</template>
<script lang="ts">
import AppControlInput from '@/components/UI/AppControlInput.vue'
import CustomButton from '@/components/UI/CustomButton.vue'
import Vue from 'vue'
import useRoute from 'vue-router'
export default Vue.extend({
components: {
AppControlInput,
CustomButton,
},
props: {
post: {
type: Object,
required: false,
},
},
data() {
return {
editedPost: this.post
? { ...this.post }
: {
author: '',
title: '',
thumbnail: '',
content: '',
},
}
},
methods: {
onSave() {
this.$emit('submit', this.editedPost)
},
onCancel() {
// Navigate back
this.$router.push('/')
},
},
})
</script>
画面は以下のとおりです
ブログ情報を入力していきます。
入力後Save
を押すとfirestoreにデータが保存されました!
Firestoreからデータ取得
まず, storeを通してFirestoreからデータ取得するロジックを書いていきます。
import Vuex from 'vuex'
import { ActionContext } from 'vuex/types'
import { Blog, Id } from '../types/index'
import firebase from 'firebase/compat/app'
export const state = () => ({})
export type RootState = ReturnType<typeof state>
const createStore = () => {
return new Vuex.Store({
state: {
loadedPosts: [] as Blog[],
},
mutations: {
+ setPosts(state, posts) {
+ state.loadedPosts = posts
+ },
...
},
actions: {
+ async nuxtServerInit(
+ vuexContext: ActionContext<RootState, RootState>,
+ context
+ ) {
+ const data: Blog[] = []
+ return await firebase
+ .firestore()
+ .collection('blog')
+ .get()
+ .then((res) => {
+ res.forEach((doc) => {
+ data.push(doc.data() as Blog)
+ })
+ vuexContext.commit('setPosts', data)
+ })
+ .catch((e) => context.error(e))
+ },
getters: {
loadedPosts(state) {
return state.loadedPosts
},
},
},
})
}
export default createStore
ポイント
- nuxtServerInitはサーバーサイドで実行されるVuexアクション(クライアントサイドに直接渡したいデータがサーバ上にある場合に便利)
- このnuxtServerInitでFirestoreのデータを取得し, このデータをmutationsをコミット(setPostsを通して
loadedPosts
に格納) - gettersを使うことでデータを加工して提供する
この記事ではpages/index.vue
に各々のブログをグリッドカードで表示します。
<template lang="">
<div>
<section
:style="{ backgroundImage: 'assets/images/main-page-background.jpg' }"
>
<PostList :posts="loadedPosts" />
</section>
</div>
</template>
<script lang="ts">
import PostList from '@/components/Posts/PostList.vue'
import Vue from 'vue'
export default Vue.extend({
components: {
PostList,
},
computed: {
loadedPosts() {
//vuexで状態管理している`loadedPosts`をview側で呼び出す
return this.$store.getters.loadedPosts
},
},
})
</script>
PostList.vue
<template lang="">
<section
class="grid content-center grid-cols-1 gap-5 p-10 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<PostPreview
v-for="post in posts"
:key="post.id"
:id="post.id"
:is-admin="isAdmin"
:thumbnail="post.thumbnail"
:title="post.title"
:content="post.content"
/>
</section>
</template>
<script lang="ts">
import PostPreview from '@/components/Posts/PostPreview.vue'
import Vue from 'vue'
export default Vue.extend({
components: {
PostPreview,
},
props: {
isAdmin: {
type: Boolean,
default: false,
},
posts: {
type: Array,
required: true,
},
},
})
</script>
<style scoped>
.post-list {
display: flex;
padding: 20px;
box-sizing: border-box;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
</style>
PostPreview.vue
<template lang="">
<nuxt-link :to="postLink" class="overflow-hidden rounded shadow-lg w-80">
<article>
<div
class="w-full bg-center bg-cover h-52"
:style="{ backgroundImage: 'url(' + thumbnail + ')' }"
></div>
<div class="px-6 py-4">
<div class="mb-2 text-xl font-bold">{{ title }}</div>
<p class="text-base text-gray-700 truncate">{{ content }}</p>
</div>
</article>
</nuxt-link>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'PostPreview',
props: {
id: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
thumbnail: {
type: String,
required: true,
},
},
//NOTE: pages/admin/index.vueにて<PostList isAdmin />をつけている。
computed: {
postLink() {
return this.isAdmin ? '/admin/' + this.id : '/posts/' + this.id
},
},
})
</script>
<style scoped>
.post-thumbnail {
width: 100%;
height: 200px;
background-position: center;
background-size: cover;
}
</style>
以上にて,ブログを作成するとカードとしてブログが作成されるフローが実装できました!
各々のブログにIDを付与
ブログのカードをタップすると詳細ページに遷移する処理をPostPreview.vue
のpostLink
に書いていますが,
現状, 各ブログにはidが付与されていないため, ブログの詳細ページに遷移した時のURLがhttp://localhost:3000/posts/undifined
となっています。
ログにも警告を出してくれています。
ここではFirestoreのドキュメントIDを(FirestoreのフィールドにIDを追加せずに)各ブログ情報に添えていきます。
nuxtServerInit
に追記していきます。
const createStore = () => {
...
actions: {
async nuxtServerInit(
vuexContext: ActionContext<RootState, RootState>,
context
) {
+ const data: (Blog & Id)[] = []
return await firebase
.firestore()
.collection('blog')
.get()
.then((res) => {
res.forEach((doc) => {
+ const obj = {
+ ...(doc.data() as Blog),
+ id: doc.id,
+ }
+ data.push(obj)
})
vuexContext.commit('setPosts', data)
})
.catch((e) => context.error(e))
},
...
}
export default createStore
以上の対応で, ブログの詳細ページに遷移した際のURLにFirestoreのドキュメントIDが付与されました。
http://localhost:3000/posts/5OecS93IpTDSyjuEj5aX
〆
以上になります。
パート2では,ブログの編集 & 削除機能
パート3では,firebase authを用いた認証機能
↑こちらを実装しようと検討しています!
最後に, 現在働かせて頂いております会社のご紹介です🙌
株式会社ドリグロでは新しい仲間となるエンジニアを募集しております。
こちらから以下の内容とともにお気軽にご応募ください。
①どの勤務形態(正社員・アルバイト・業務委託など)に興味があるか
②対応言語と各経験年数
③生年月日・最終学歴
Discussion