「Nuxt.jsビギナーズガイド」の感想・備忘録5

スポンサーリンク
「Nuxt.jsビギナーズガイド」の感想・備忘録4の続き

サンプルアプリの作成(続き)

ログイン情報の永続化

1. サーバサイドでもCookieを扱うことができるuniversal-cookieを導入

npm install universal-cookie

2. app/pages/index.vueにCookieへの書き込み追加

  • import Cookies from 'universal-cookie'で読み込み。
  • const cookies = new Cookies()でインスタンス生成。
  • cookies.set('user', JSON.stringify(this.user))で保存。
<template>
  <section class="container">
    <el-card style="flex: 1">
      <div slot="header" class="clearfix">
        <span>ログイン</span>
      </div>
      <form>
        <div class="form-content">
          <span>ユーザー ID</span>
          <el-input placeholder="" v-model="formData.id"/>
        </div>
        <div class="form-content">
          <el-checkbox v-model="isCreateMode">アカウントを作成する</el-checkbox>
        </div>
        <div class="text-right">
          <el-button type="primary" @click="handleClickSubmit">{{ buttonText }}</el-button>
        </div>
      </form>
    </el-card>
  </section>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
import Cookies from 'universal-cookie'

export default {
  asyncData ({ redirect, store }) {
    if (store.getters.user) {
      redirect('/posts/')
    }
    return {
      isCreateMode: false,
      formData: {
        id: ''
      }
    }
  },
  computed: {
    buttonText () {
      return this.isCreateMode ? '新規登録' : 'ログイン'
    },
    ...mapGetters(['user'])
  },
  methods: {
    async handleClickSubmit () {
      const cookies = new Cookies()
      if (this.isCreateMode) {
        try {
          await this.register({ ...this.formData })
          this.$notify({
            type: 'success',
            title: 'アカウント作成完了',
            message: `${this.formData.id} として登録しました`,
            position: 'bottom-right',
            duration: 1000
          })
          cookies.set('user', JSON.stringify(this.user))
          this.$router.push('/posts/')
        } catch (e) {
          this.$notify.error({
            title: 'アカウント作成失敗',
            message: '既に登録されているか、不正なユーザー ID です',
            position: 'bottom-right',
            duration: 1000
          })
        }
      } else {
        try {
          await this.login({ ...this.formData })
          this.$notify({
            type: 'success',
            title: 'ログイン成功',
            message: `${this.formData.id} としてログインしました`,
            position: 'bottom-right',
            duration: 1000
          })
          cookies.set('user', JSON.stringify(this.user))
          this.$router.push('/posts/')
        } catch (e) {
          this.$notify.error({
            title: 'ログイン失敗',
            message: '不正なユーザー ID です',
            position: 'bottom-right',
            duration: 1000
          })
        }
      }
    },
    ...mapActions(['login', 'register'])
  }
}
</script>

<style scoped>
.form-content {
  margin: 16px 0;
}
</style>

3. mkdir app/middleware; touch app/middleware/auth-cookie.js

import Cookies from 'universal-cookie'

export default ({ req, store }) => {
  if (process.browser) {
    return
  }
  const cookies = new Cookies(req.headers.cookie)
  const user = cookies.get('user')
  if (user && user.id) {
    const { id, likes } = user
    store.commit('setUser', { user: { id, likes } })
  }
}

4. nuxt.config.jsに登録

router: {
  middleware: [
    'auth-cookie',
  ]
},

5. app/components/TheHeader.vueにログイン中のユーザー情報表示を追加

<template>
  <el-menu mode="horizontal" :router="true">
    <el-menu-item index="1" style="pointer-events:none;">
      Nuxt Diary App
    </el-menu-item>
    <el-menu-item index="2" :route="{ path: '/posts/' }">
      投稿一覧
    </el-menu-item>

    <no-ssr>
      <el-menu-item index="4" style="float: right;" :route="{ path: `/users/${user.id}` }" v-if="user">
        <span>{{user.id}}</span>
      </el-menu-item>
      <el-menu-item index="4" style="float: right;" :route="{ path: topLink }">
        <span>ログイン</span>
      </el-menu-item>
    </no-ssr>
    <el-menu-item index="5" style="float: right" :route="{ path: '/posts/new' }">
      新規投稿
    </el-menu-item>
  </el-menu>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    topLink () {
      if (this.$store.getters.user) {
        return '/posts/'
      }
      return '/'
    },
    ...mapGetters(['user'])
  }
}
</script>

投稿機能の実装

1. momentの導入

  • npm install moment
  • touch app/plugins/moment.js
import 'moment/locale/ja'
import moment from 'moment'
moment.locale('ja')
export default moment

2. vuexのpostsモジュールを作成

touch app/store/posts.js

import moment from '~/plugins/moment'

export const state = () => ({
  posts: []
})

export const getters = {
  posts: state => state.posts
}

export const mutations = {
  addPost (state, { post }) {
    state.posts.push(post)
  },
  updatePost (state, { post }) {
    state.posts = state.posts.map(p => (p.id === post.id ? post : p))
  },
  clearPosts (state) {
    state.posts = []
  }
}

export const actions = {
  async publishPost ({ commit }, { payload }) {
    const post = { ...payload, created_at: moment().format() }
    // 書籍ではfirebaseに登録しているが、ここでは何もしない
    await this.$axios.$put('', [post])
    commit('addPost', post)
  }
}

3. 投稿ページの作成

touch app/pages/posts/new.vue

<template>
  <section class="container posts-page">
    <el-card style="flex: 1">
      <div slot="header" class="clearfix">
        <el-input placeholder="タイトルを入力" v-model="formData.title"/>
      </div>
      <div>
        <el-input placeholder="本文を入力……" type="textarea" rows="15" v-model="formData.body"/>
      </div>
      <div class="text-right" style="margin-top: 16px;">
        <el-button type="primary" @click="publish" round>
          <span class="el-icon-upload2"/>
          <span>Publish</span>
        </el-button>
      </div>
    </el-card>
  </section>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  asyncData ({ redirect, store }) {
    if (!store.getters.user) {
      redirect('/')
    }
    return {
      formData: {
        title: '',
        body: ''
      }
    }
  },
  computed: {
    ...mapGetters(['user'])
  },
  methods: {
    async publish () {
      await this.publishPost({
        payload: {
          user: this.user,
          ...this.formData
        }
      })
      await this.$router.push('/posts')
    },
    ...mapActions('posts', ['publishPost'])
  }
}
</script>

<style>
.posts-page .el-table__row {
  cursor: pointer;
}
</style>

投稿一覧を非同期で取得するように変更

1. 書籍ではfirebaseを使っているが、ここではローカルにjsonを用意する。

touch static/posts.json

[
  {
    "id": "001",
    "title": "テストタイトル",
    "body": "ここに本文が入ります。",
    "created_at": "2021/10/30 06:00:00",
    "user": {
      "id": "hoge"
    }
  },
  {
    "id": "002",
    "title": "テストタイトル2",
    "body": "ここに本文2が入ります。",
    "created_at": "2021/10/31 06:30:00",
    "user": {
      "id": "hoge2"
    }
  }
]

2. app/store/index.jsのactionsにfetchPostsを追加

async fetchPosts ({ commit }) {
  const posts = await this.$axios.$get('/posts.json')
  commit('clearPosts')
  posts.forEach(content =>
    commit('addPost', {
      post: {
        ...content
      }
    })
  )
},

3. app/pages/posts/index.vueを修正

import { mapGetters } from 'vuex'
import moment from '~/plugins/moment'

export default {
  async asyncData ({ store }) {
    await store.dispatch('posts/fetchPosts')
  },
  computed: {
    showPosts () {
      return this.posts.map((post) => {
        post.created_at = moment(post.created_at).format('YYYY/MM/DD HH:mm:ss')
        return post
      })
    },
    ...mapGetters('posts', ['posts'])
  }
}

コメント