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

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

サンプルアプリの作成

環境構築

  1. npx create-nuxt-app test-blog
    以下を選択。
    • UI framework: Element
    • Nuxt.js modules: Axios – Promise based HTTP client
    • Linting tools: ESLint
    • Testing framework: None
    • Rendering mode: Universal (SSR / SSG)
    • Deployment target: Server (Node.js hosting)
  2. appディレクトリを作成し、いくつかのディレクトリをその中へ移動する。
    1. cd test-blog
    2. mkdir app
    3. mv components pages plugins static store app/
      appディレクトリに移動すると以下のメリットがある。
      • アプリケーション以外のコードを別の場所に配置することができる(testディレクトリなど)
      • Linterやコードフォーマッタのパス指定をappにすることで、nuxt.config.jsなどをignoreする必要がなくなる
  3. nuxt.config.jsに追加
    srcDir: 'app',
  4. npm run devで開発サーバで実行

共通レイアウトの作成

1. mkdir app/assets; touch app/assets/common.css

アプリケーション全体用のCSSを定義。

html {
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: border-box;
  margin: 0;
}

.wrapper {
  background: #FAFAFA;
  width: 100%;
  min-height: calc(100vh - 61px);
}

.el-card {
  flex: 1;
}

.container:not(.__nuxt-error-page) {
  width: 96%;
  max-width: 980px;
  padding: 30px 0;
  margin: 0 auto;
  min-height: calc(100vh - 61px);
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.text-left {
  text-align: left;
}

.text-center {
  text-align: center;
}

.text-right {
  text-align: right;
}

p {
  margin: 16px 0;
}

2. touch 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: 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>
export default {
  computed: {
    topLink () {
      if (this.$store.getters.user) {
        return '/posts/'
      }
      return '/'
    }
  }
}
</script>

Vue.jsのスタイルガイド「ページで1つしか存在しないコンポーネントの名前はTheで始める」
https://jp.vuejs.org/v2/style-guide/index.html#単一インスタンスのコンポーネント名-強く推奨

ログイン画面へのリンクは書籍通りにすると、ログイン中は「Redirected when going from “/posts/” to “/” via a navigation guard.」という警告が表示されてしまった。
ログイン中は/へアクセスすると/posts/にリダイレクトするようにしているため、2回リダイレクトが発生してしまうことが原因であると思われる。
よって、ログイン状態を判定してリンクを/と/posts/で切り替えるようにした。

3. mkdir app/layouts; touch app/layouts/default.vue

<template>
<TheHeader />
</template>
<script>
import TheHeader from '~/components/TheHeader.vue'
export default {
  components: {
    TheHeader
  }
}
</script>

4. nuxt.config.jsにCSSの読み込みを追加

css: [
  'element-ui/lib/theme-chalk/index.css',
  '~/assets/common.css'
],

ログインページと投稿一覧ページの作成

1. app/pages/index.vueを修正。

<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>
export default {
  asyncData () {
    return {
      isCreateMode: false,
      formData: {
        id: ''
      }
    }
  },
  computed: {
    buttonText () {
      return this.isCreateMode ? '新規登録' : 'ログイン'
    }
  }
}
</script>

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

2. mkdir app/pages/posts; touch app/pages/posts/index.vue

<template>
  <section class="container posts-page">
    <el-card>
      <div slot="header" class="clearfix">
        <span>新着投稿</span>
      </div>
      <el-table
          :data="showPosts"
          style="width: 100%"
          class="table"
      >
        <el-table-column
            prop="title"
            label="タイトル">
        </el-table-column>
        <el-table-column
            prop="user.id"
            label="投稿者"
            width="180">
        </el-table-column>
        <el-table-column
            prop="created_at"
            label="投稿日時"
            width="240">
        </el-table-column>
      </el-table>
    </el-card>
  </section>
</template>

<script>

export default {
  computed: {
    showPosts () {
      return [
        {
          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/30 06:10:00',
          user: {
            id: 'hoge'
          }
        }
      ]
    }
  }
}
</script>

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

ログイン機能の実装

1. touch app/store/index.js

export const state = () => ({
  isLoggedIn: false,
  user: null
})

export const getters = {
  isLoggedIn: state => state.isLoggedIn,
  user: state => state.user
}

export const mutations = {
  setUser (state, { user }) {
    state.user = user
    state.isLoggedIn = true
  }
}

export const actions = {
  async login ({ commit }, { id }) {
    const user = await this.$axios.$get(`/users/${id}.json`)
    if (!user.id) {
      throw new Error('Invalid user')
    }
    commit('setUser', { user })
  },
  async register ({ commit }, { id }) {
    const payload = {}
    payload[id] = { id }
    await this.$axios.$patch('/users.json', payload)
    const user = await this.$axios.$get(`/users/${id}.json`)
    if (!user.id) {
      throw new Error('Invalid user')
    }
    commit('setUser', { user })
  }
}

2. app/pages/index.vueを修正。

<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'

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 () {
      if (this.isCreateMode) {
        try {
          await this.register({ ...this.formData })
          this.$notify({
            type: 'success',
            title: 'アカウント作成完了',
            message: `${this.formData.id} として登録しました`,
            position: 'bottom-right',
            duration: 1000
          })
          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
          })
          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. touch static/users.json; touch static/users/hoge.json

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

{
  "id": "hoge"
}
{
  "id": "hoge"
}

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

コメント