20210304のvue.jsに関する記事は9件です。

意外と便利!?認証コードでよく使う一文字ずつ入力するinputは「vue-fake-input」で楽々実装!!

FirebaseでSMS認証を行っているときのこと。

認証コードって一文字ずつ入力するのをよく見かけたのような。。。

一から実装すると中々骨のある作業だと思い、コードは出来るだけ書きたくないので調べているとなんとお探しのものがあった!

その名も「vue-fake-input」

こちらの記事にやり方はすべて書いてあります。

ぜひ学習の参考に

Thank you for reading

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】Vueとajaxを使用したWebサービスを作ってみた

はじめまして きゃっぷ@capgame_です。
ガチャのようなサービスを作るためにVueや非同期通信について学んだので、そのことについて書いていきます。

作ったもの

サイトのスクリーンショット

こんな感じで、ランダムで(投稿された)言葉を組み合わせて、文章を作れるサービスです。
現時点はで100words以上投稿されています。

技術

メイン部分 : JavaScript

言葉を組み合わせて文章を作るってのは

main.js
    console.log(["","",""][parseInt(Math.random() * 3)] + ["佐藤が","田中が","鈴木が"][parseInt(Math.random() * 3)] + ["叫んだ","泣いた","笑った"][parseInt(Math.random() * 3)]) 

みたいなコードで書くことができ、入門書のサンプルによく使われるほどの簡単なプログラムです。

これに、jQueryの$.get()で言葉を取得しています。

投稿部分 : jQuery

こちらもjQueryの $.get() を使って言葉を投稿しています。

そして、データベースは使っていません。
phpのfile_put_contents()及びfile_get_contents()を使ってファイルでセーブしています。
便利ですがちゃんと . とか / のエスケープは行いましょう。

CSS : SCSS

SCSSを使って書いてみました。
ヘッダー部分とか入れ子になっていると分かりやすいのでかなり書きやすかったです。
SCSS布教活動したいです。

最後に

今までかたくなにライブラリを使わなかったのを後悔するくらいVueで要素いじるのが楽でした。
今回のサイトでは、v-modelとv-bindとv-onくらいしか使ってないので、もっといろいろ学んでみたいと思いました。

あとこのサイトに単語たくさん追加してください?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue sass-loader エラー:Module build failed: TypeError: this.getResolve is not a function

背景

Vue.js Cliをいじっていて、Sass記述したらエラーが出た

エラー

Failed to compile.
./src/index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=scss& (./node_modules/css-loader/dist/cjs.js??ref--8-oneOf-1-1!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--8-oneOf-1-2!./node_modules/sass-loader/dist/cjs.js??ref--8-oneOf-1-3!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=scss&)
Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
TypeError: this.getOptions is not a function
    at Object.loader

が出力され、テンプレートが表示できない

原因1

最初「sass-loader」が入ってないと言われ、以下記事の内容を元に互換性があるwebpackとsass-loaderをnpmインストールした、がまだ解決しない→scssがコンパイルされるようにはなった
以下記事参照:
https://stackoverflow.com/questions/58546521/node-sass-and-sass-loader-module-build-failed-typeerror-this-getresolve-is-n

原因2

よく見たらちゃんと書いてありました^ ^;

Syntax Error: Error: Node Sass version 5.0.0 is incompatible with ^4.0.0.

解決

node-sassをアンインストールします

npm uninstall node-sass

次に、互換性のある、4.14.1をインストールします

npm install node-sass@4.14.1

参照

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt + Firebase入門としてシンプルなカラーコード管理アプリを作ってみた

はじめに

今回はFirebaseを軽く使ってみるために、簡単なカラーコードピッカーを作成してみました。制作期間は一週間ちょっとくらいです。初めてfirebaseやVuexをしっかり使ったので雑なところもありますがよろしくお願いします。

今回作ったアプリ
github

機能

至ってシンプルです。

・Google認証
・パレット毎のカラーコード管理
・カラーピッカーでcodeを自動生成
・パレットに登録したカラーコードをコピー

*ダッシュボード画面
スクリーンショット 2021-03-03 1.10.15.png

*パレット編集画面
スクリーンショット 2021-03-03 1.10.32.png

作成手順

今回は以下の手順で作成しました。

1. Firebaseの導入(Firebase JavaScript SDK)
2. Vuexでfirestoreの操作をするaction, mutationを作成
3. ページ、コンポーネントにて取得したデータを描画

Firebaseの導入

全て説明すると長くなりますので、簡潔に述べます。
参考記事は最後に載せておきますので、詳しいやり方はそちらでチェックしてみてください。

Firebase consoleにてプロジェクトを作成、databaseの紐付けをしたら、プロジェクトにFirebase SDKを入れます。

$ yarn add firebase save

Firebase consoleからプロジェクトにアプリを追加します。webを選択してHostingを設定したら、いろいろ指示が出てきますので、その通りに導入していきます。

$ yarn add firebase-tools

あとは指示通りにデプロイするだけです。指示はかなり親切に書かれているので困ることはないかと思います。

$ firebase deploy

Nuxt側でのFirebase設定

pluginsディレクトリの配下にfirebase.jsを作成して、コンソールから取得できる値を貼り付けます。

plugins/firebase.js
import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: process.env.apiKey,
      authDomain: process.env.authDomain,
      databaseURL: process.env.databaseURL,
      projectId: process.env.projectId,
      storageBucket: process.env.storageBucket,
      messagingSenderId: process.env.messagingSenderId,
      appId: process.env.appId,
    }
  )
}

export default firebase

これだけで設定完了です。

アプリ機能実装

Google認証を使ったログイン機能、一般的なCRUDを実装していきます。

ログイン機能

認証関係のStoreはauth.jsとして切り離しています。

store/modules/auth.js
import firebase from '~/plugins/firebase'
import Vuex from 'vuex'

const namespaced = true // モジュールを切り離して呼び出す時に必要

const state = () => ({
  userUid: '',
  userName: '',
  userImage: '',
  userEmail: '',
  loggedIn: false,
})

const mutations = {
  // ログイン状態の切替
  loginStatusChange(state, status) {
    state.loggedIn = status
  },
  // ここからユーザー情報をそれぞれ取得
  setUserUid(state, userUid) {
    state.userUid = userUid
  },
  setUserName(state, userName) {
    state.userName = userName
  },
  setUserImage(state, userImage) {
    state.userImage = userImage
  },
  setUserEmail(state, userEmail) {
    state.userEmail = userEmail
  }
}

const actions = {
  login({ commit }) {
    const provider = new firebase.auth.GoogleAuthProvider()
    firebase.auth().signInWithPopup(provider).then((result) => {
      const user = result.user
      commit('loginStatusChange', true)
      console.log('Login was successful')
      commit('setUserUid', user.uid)
      commit('setUserName', user.displayName)
      this.$router.push('/dashboard')
    }).catch((error) => {
      const errorCode = error.code
      console.log('error : ' + errorCode)
    });
  },
  // ログイン中のユーザー取得
  onAuth({ commit }) {
    firebase.auth().onAuthStateChanged(user => {
      user = user ? user : {}
      commit('setUserUid', user.uid)
      commit('setUserName', user.displayName)
      commit('setUserImage', user.photoURL)
      commit('setUserEmail', user.email)
      commit('loginStatusChange', user.uid ? true : false)
    })
  },
  logout({ commit }) {
    firebase.auth().signOut().then(() => {
      commit('loginStatusChange', false)
      console.log('Logout was successful')
      this.$router.push('/')
    }).catch((error) => {
      const errorCode = error.code
      console.log('error :' + errorCode)
    })
  },
}

const getters = {
  getUserUid(state) {
    return state.userUid
  },
  getUserName(state) {
    return state.userName
  },
  getUserImage(state) {
    return state.userImage
  },
  getUserEmail(state) {
    return state.userEmail
  },
}

export default {
  namespaced,
  state,
  mutations,
  actions,
  getters,
}

auth.jsで記述したactionをログイン・ログアウトボタンで呼び出せるようにします。
v-if="!$store.state.auth.loggedIn"でAuthストアを参照して、ログイン中ならログインボタンを表示しない・ログイン前ならログアウトボタンは表示されません。

components/beforeLogin/loginButton.vue
<template>
  <v-btn
    color="secondary"
    @click="login"
    v-if="!$store.state.auth.loggedIn"
    outlined
  >
    Login
  </v-btn>
</template>>

<script>
export default {
  methods: {
    login() {
      console.log('Login attempt')
      this.$store.dispatch('auth/login')
    },
  }
}
</script>
components/afterLogin/logoutButton.vue
<template>
  <div class='text-center'>
    <v-btn
      color="warning"
      @click="logout"
      v-if="$store.state.auth.loggedIn"
      block
      width="100px"
    >
      Logout
    </v-btn>
  </div>
</template>

<script>
export default {
  methods: {
    logout() {
      console.log('Logout attempt')
      const result = window.confirm('Do you want to logout?')
      if (result) {
        this.$store.dispatch('auth/logout')
      } else {
        console.log('Logout was canceled')
      }
    }
  }
}
</script>

これでログインボタンの実装が完了しました。認証時に取得できるuserからidやnameも抽出できるので、後に活用します。

パレットの作成・削除機能

auth.jsと同様にmoduleで切り離しています。actionやmutationの名前は機能そのままです。

store/modules/palette.js
import firebase from '~/plugins/firebase'
const db = firebase.firestore()
const palettesRef = db.collection('palettes')

const namespaced = true

const state = () => ({
  palettes: [], // ユーザーが所持しているパレット一覧
  paletteInfo: [], // パレットの詳細情報
})

const mutations = {
  getPalettes(state, palettes) {
    state.palettes = palettes
  },
  getPaletteInfo(state, paletteInfo) {
    state.paletteInfo = paletteInfo
  },
  addPalette(state, palette) {
    state.palettes.push(palette)
  },
}

const actions = {
  fetchPalettes({ dispatch, commit, rootState, rootGetters }) {
    const userUid = rootState.auth.userUid // rootから別モジュールのstateを参照
    // ログイン中のuserIDに結びついたパレットのみを取得
    palettesRef.where('user', '==', userUid).get().then((res) => {
      let palettesBox = []
      res.forEach((doc) => {
        const palette = doc.data()
        palettesBox.push(
          Object.assign({
           // firestoreで割り振られるdocumentIDも挿入しておく
            id: doc.id
          }, palette)
        )
      })
      commit('getPalettes', palettesBox)
    }).catch((error) => {
      console.log('error :' + error)
    })
  },
  fetchPaletteDetail({ commit }, id) {
    palettesRef.doc(id).get().then((doc) => {
      const paletteInfo = doc.data()
      commit('getPaletteInfo', paletteInfo)
    }).catch((error) => {
      console.log('error :' + error)
    })
  },
  addPalette({ commit }, palette) {
    palettesRef.add({
      name: palette.name,
      description: palette.description,
      user: palette.user
    }).then((decRef) => {
      commit('addPalette', palette)
    }).catch((error) => {
      console.log('Error adding palette :' + error)
    })
  },
  deletePalette({ dispatch, commit, rootState, rootGetters }, id) {
    palettesRef.doc(id).delete().then(() => {
    // 削除後に最新の状態を取得し直す
      dispatch('fetchPalettes')
      this.$router.push('/dashboard')
    }).catch((error) => {
      console.log('error :' + error)
    })
  },
}

const getters = {
  getPalettes(state) {
    return state.palettes
  },
  getPaletteInfo(state) {
    return state.paletteInfo
  }
}

export default {
  namespaced,
  state,
  mutations,
  actions,
  getters,
}

pages/newPalette.vue
<template>
  <v-app>
    <v-container>
      <palette-form-card v-slot:form-card-content>
        <v-form ref="form" v-model="isValid">
          <palette-name :name.sync="params.palette.name" />
          <palette-description :description.sync="params.palette.description" />
          <v-btn
            :disabled="!isValid || loading"
            :loading="loading"
            block
            color="primary"
            @click="addPalette"
          >
          Create palette
          </v-btn>
        </v-form>
      </palette-form-card>
    </v-container>
  </v-app>
</template>

<script>
import PaletteDescription from '~/components/paletteForm/paletteDescription.vue'
import PaletteFormCard from '~/components/paletteForm/paletteFormCard.vue'
import PaletteName from '~/components/paletteForm/paletteName.vue'

export default {
  middleware: 'authenticated',
  data() {
    return {
      isValid: false,
      loading: false,
      params: { palette: { name: '', description: '' } }
    }
  },
  methods: {
    addPalette() {
      this.loading = true
      const name = this.params.palette.name
      const description = this.params.palette.description
      const user = this.$store.state.auth.userUid
      setTimeout(() => {
        this.$store.dispatch('palette/addPalette', {name, description, user})
        this.$router.replace('/dashboard')
        this.loading = false
      }, 1500)
    }
  },
  created() {
    this.$store.dispatch('auth/onAuth')
  },
  components: {
    PaletteFormCard,
    PaletteName,
    PaletteDescription
  }
}
</script>

カラーの作成・削除

color.jsはパレットとほとんど構造は同じです。

store/modules/color.js
import firebase from '~/plugins/firebase'
const db = firebase.firestore();
const colorsRef = db.collection('colors')

const namespaced = true

const state = () => ({
  colors: [],
  paletteColors: [],
})

const mutations = {
  addColor(state, color) {
    state.colors.push(color)
  },
  getColors(state, colors) {
    state.colors = colors
  },
  getPaletteColors(state, paletteColors) {
    state.paletteColors = paletteColors
  }
}

const actions = {
  fetchColors({ dispatch, commit, rootState, rootGetters }) {
    const userUid = rootState.auth.userUid
    colorsRef.where('user', '==', userUid).get().then((res) => {
      let colorsBox = []
      res.forEach((doc) => {
        const color = doc.data()
        colorsBox.push(
          Object.assign({
            id: doc.id
          }, color)
        )
      })
      commit('getColors', colorsBox)
    }).catch((error) => {
      console.log('error : ' + error)
    })
  },
  fetchPaletteColors({ commit }, id) {
    const paletteId = id
  // パレットに紐づいているカラーコードを取得
    colorsRef.where('palette', '==', paletteId).get().then((res) => {
      let paletteColorsBox = []
      res.forEach((doc) => {
        const paletteColor = doc.data()
        paletteColorsBox.push(
          Object.assign({
            id: doc.id
          }, paletteColor)
        )
      })
      commit('getPaletteColors', paletteColorsBox)
    }).catch((error) => {
      console.log('error : ' + error)
    })
  },
  addColor({ commit }, color) {
    colorsRef.add({
      name: color.name,
      code: color.code,
      user: color.user,
      palette: color.palette,
    }).then((docRef) => {
      commit('addColor', color)
    }).catch((error) => {
      console.log("Error adding color: ", error)
    })
  },
  deletePaletteColor({ dispatch, commit, rootState, rootGetters }, id) {
    colorsRef.doc(id).delete().then(() => {
      dispatch('fetchColors')
      location.reload()
    }).catch((error) => {
      console.log('error :' + error)
    })
  }
}

const getters = {
  getColors(state) {
    return state.colors
  }
}

export default {
  namespaced,
  state,
  mutations,
  actions,
  getters,
}
components/color/colorInputForm.vue
<template>
  <v-app>
    <v-container>
      <color-form-card v-slot:form-card-content>
        <v-form ref="form" v-model="isValid">
          <color-name :name.sync="params.color.name" />
          <color-code :code.sync="params.color.code" />
          <color-picker :code.sync="params.color.code" />
          <v-btn
            :disabled="!isValid || loading"
            :loading="loading"
            block
            color="primary"
            @click="addColor"
          >
          Create color
          </v-btn>
        </v-form>
      </color-form-card>
    </v-container>
  </v-app>
</template>

<script>
import ColorFormCard from '~/components/colorForm/colorFormCard.vue'
import ColorName from '~/components/colorForm/colorName.vue'
import ColorCode from '~/components/colorForm/colorCode.vue'
import ColorPicker from '~/components/colorForm/colorPicker.vue'

export default {
  data() {
    return {
      isValid: false,
      loading: false,
      params: { color: { name: '', code: '' } }
    }
  },
  methods: {
    addColor() {
      this.loading = true
      const name = this.params.color.name
      const code = this.params.color.code
      const user = this.$store.state.auth.userUid
      const palette = this.$route.params.id
      this.$store.dispatch('color/addColor', {name, code, user, palette})
      setTimeout(() => {
        location.reload();
        this.loading = false
      }, 1500)
    }
  },
  created() {
    this.$store.dispatch('auth/onAuth')
  },
  components: {
    ColorFormCard,
    ColorName,
    ColorCode,
    ColorPicker,
  },
}
</script>

パレットとカラーは基本的にログイン中のuserID + 各々のID と照らし合わせながら取得・作成・削除される仕組みです。

store/index.js
import Vuex from 'vuex'
import auth from './modules/auth'
import color from './modules/color'
import palette from './modules/palette'

const createStore = () => {
  return new Vuex.Store({
    modules: {
      auth,
      color,
      palette,
    },
  })
}

export default createStore

リダイレクト

middlewareで簡単に、「ログイン状態でなければトップページにリダイレクト」とだけ記述しておきました。

middleware/authenticated.js
export default function ({ store, redirect }) {
  if (!store.state.auth.loggedIn) {
    return redirect('/')
  }
}

パレットの詳細画面作成

パレットの詳細画面(画像2枚目)の作成では、右にカラーピッカー、左にパレットが持つカラーを配置しています。

pages/palettes/_id.vue
<template>
  <v-app>
    <v-container>
      <v-alert
        color="blue"
        type="success"
        v-if="show === true"
      >
        Copied!
      </v-alert>
      <v-row cols="12">
        <v-col>
          <draggable tag="div" v-if="$store.state.color.paletteColors">
            <div
              v-for="paletteColor in $store.state.color.paletteColors"
              :key="paletteColor.paletteColor"
              :style="{backgroundColor: paletteColor.code, height: '50px', color: conversionToDecimal(paletteColor.code)? '#111' : '#fff'}"
              class="wrapper"
            >
              <div class="inner-name">
              {{ paletteColor.name }}
              </div>
              <div class="inner-code">
              {{ paletteColor.code }}
              </div>
              <button
                class="copy-button"
                @click="doCopy(paletteColor.code)"
              >
                <v-icon :style="{color: conversionToDecimal(paletteColor.code)? '#111' : '#fff'}">
                  mdi-content-copy
                </v-icon>
              </button>
              <button
                class="delete-button"
                @click="deletePaletteColor(paletteColor.id)"
              >
                <v-icon :style="{color: conversionToDecimal(paletteColor.code)? '#111' : '#fff'}">
                  mdi-delete
                </v-icon>
              </button>
            </div>
          </draggable>
        </v-col>
        <v-col>
          <color-input-form />
        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
import colorInputForm from '~/components/colorForm/colorInputForm.vue'
import draggable from 'vuedraggable' // パレットのカラーをドラッグで並び替える

export default {
  middleware: 'authenticated',
  components: {
    colorInputForm,
    draggable,
  },
  data() {
    return {
      show: false
    }
  },
  created() {
    const paletteId = this.$route.params.id
    this.$store.dispatch('palette/fetchPaletteDetail', paletteId)
    this.$store.dispatch('color/fetchPaletteColors', paletteId)
  },
  methods: {
    // カラーコードを十進数に直す
    // 色が明るい場合(true)では黒文字でカラーコードを表示
    // 色が暗い場合(false)では白文字でカラーコードを表示
    conversionToDecimal(colorCode) {
      let red = parseInt(colorCode.substring(1,3), 16)
      let green = parseInt(colorCode.substring(3,5), 16)
      let blue = parseInt(colorCode.substring(5,7), 16)
      if (red + green + blue > 255) {
        return true
      }
    },
    // クリップボードにカラーコードをコピーする
    doCopy(colorCode) {
      this.$copyText(colorCode)
      this.conversionToDecimal(colorCode)
      this.show = true
      setTimeout(() => {
        this.show = false}
        ,3000
      )
    },
    deletePaletteColor(id) {
      const result = window.confirm('Do you want to delete this palette color?')
      if (result) {
        this.$store.dispatch('color/deletePaletteColor', id).then(() => {
        console.log('deleted')
        }).catch((error) => {
          console.log(error)
        })
      } else {
        console.log('Delete was canceled')
      }
    }
  }
}
</script>

ログイン状態の永続化

vuex-persistedstateをインストールして認証状態を保つようにしました。

$ yarn add vuex-persistedstate
plugins/localStorage.js
import createPersistedState from 'vuex-persistedstate'

export default ({ store }) => {
  createPersistedState({
    paths: ['auth'],
  })(store);
};

課題点

・リロードをした時に表示に不具合が生じる時があります。まだまだライフサイクルの理解が甘いと思いますので、より改良を続けていきたいと思います。

・Jestでテストを書いていきたいと思います。しかしfirebase SDKを使用したtestの書き方を探っていく必要がありそうです。

参考にさせていただいた記事

Nuxt.js + Firebase Authentication + FireStoreでwebアプリケーションハンズオン
Cloud Firestore でデータを取得する
複数の自動生成されたIDでFirestoreのドキュメントをクエリする
Nuxt.jsでURLからパラメーターを取得する
Nuxt.js: storeモジュールから別のstoreモジュールのstateを参照する方法
Nuxt.js 親子コンポーネント間の双方向データバインディングを実装する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelとVue.jsを使った見積作成アプリ その3

前回の復習

前回はVue.jsを使用し見積編集ページのテンプレートを作成しました。今回はdompdfを利用したpdf表示ページとユーザー認証関係のページを作成していきます。

dompdfの導入

larave-dompdfパッケージのインストールはcomposerを使用して行います。

$ composer require barryvdh/laravel-dompdf

config/app.phpを以下のように編集します。
providersに下記を追記します。

app.php
Barryvdh\DomPDF\ServiceProvider::class,

aliasesに下記を追記します。

app.php
'PDF' => Barryvdh\DomPDF\Facade::class,

コントローラーの作成

PDFの作成を行うため、コマンドを使ってコントローラーを作成します。

$ php artisan make:controller PDFController

作成されたコントローラーに記述していきます。

PDFController.php
<?php

namespace App\Http\Controllers;

use App\Estimate;
use App\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PDF;

class PDFController extends Controller
{
    public function index(Request $request){
        $user = Auth::user();
        $estimate_id = $request->input('estimate');
        $estimate = Estimate::find($estimate_id);
        $estimated_at = $estimate->estimated_at;
        $items = Item::where('estimate_id', $estimate_id)->orderBy('id')->get();
        $item_price = Item::where('estimate_id', $estimate_id)->get(['quantity', 'unit_price']);
        $sum_price = 0;

        if($user->id != $estimate->user_id) {
            return redirect()->route('estimates.index');
        }

        for($i=0; $i<count($item_price); $i++){
            $calculation_price = $item_price[$i]['quantity'] * $item_price[$i]['unit_price'];
            $sum_price += $calculation_price;
        }

        $pdf = PDF::loadView('pdf/generate_pdf', [
            'user' =>$user,
            'estimate' => $estimate,
            'estimated_at' => date('Y年m月d日', strtotime($estimated_at)),
            'items' => $items,
            'sum_price' => $sum_price,
        ]);

        return $pdf->stream();
    }
}

loadView() を利用することで、通常のビューを用意するのと同じ手順で、PDFに出力したい内容をHTMLで記述することができるようになります。変数を渡すことも可能です。

ルーティングの作成

web.php
Route::get('pdf','PDFController@index');

これでテンプレートを作成すればPDFが表示されます。

日本語化

PDFの作成は可能になりましたが、このままだと日本語が文字化けしてしまいます。日本語のPDFが作成できるように設定を行います。

fontsディレクトリの作成

strageディレクトリの下にfontsディレクトリを作成します。

$ mkdir fonts

IPAフォントのダウンロード

以下のサイトからIPAフォントをダウンロードします。
https://moji.or.jp/ipafont/

zipファイルとしてダウンロードされるので、解凍後、その中にあるファイルを先ほど作成したstorage/fonts/ディレクトリの下にコピーしてください。

これで無事日本語に対応しましたのでテンプレートを作成していきます。

PDFテンプレートの作成

views/pdf/generate_pdf.blade.phpを作成し記述していきます。

generate_pdf.blade.php
<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <style type="text/css">
        @font-face {
            font-family: ipam;
            font-style: normal;
            font-weight: normal;
            src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype');
        }
        @font-face {
            font-family: ipam;
            font-style: bold;
            font-weight: bold;
            src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype');
        }
        body {
            font-family: ipam !important;
        }
        .box {
          width        : 50px;
          height       : 50px;
          background   : white;
          border       : medium solid #000000;
          position: absolute;
        }
    </style>
</head>
<body>
  <h1 style="text-align:center;">御見積書</h1>
  <div style="float:left">
    <h2>{{ $estimate['customer'] }}様</h2>
    <p>下記の通り御見積申し上げます</p>
    <p>件名:{{ $estimate['title'] }}</p>
    <p>納入期限:{{ $estimate['deadline_at'] }}</p>
    <p>納入場所:{{ $estimate['location'] }}</p>
    <p>取引方法:{{ $estimate['transaction'] }}</p>
    <p>有効期限:{{ $estimate['effectiveness'] }}</p>
    <u><h2>御見積合計金額 ¥{{ number_format($sum_price * 1.1) }}-</h2></u>
  </div>
  <div style="float:right">
    <p>見積日 {{ $estimated_at }}</p>
    <p style="padding-top:25px">〒{{ $user['postal_code'] }}</p>
    <p>{{ $user['address'] }}</p>
    <p>{{ $user['address2'] }}</p>
    <p>{{ $user['company'] }}</p>
    <p>TEL: {{ $user['phone_number']}}  FAX: {{ $user['fax_number'] }}</p>
    <p>担当者: {{ $user['name'] }}</p>
  </div>
  <p class="box" style="margin-top:375px;margin-left: 547px;"></p>
  <a class="box" style="margin-top:375px;margin-left: 597px;"></a>
  <a class="box" style="margin-top:375px;margin-left: 647px;"></a>
  <div style="margin-top:360px">
    <table border="1" width="100%" cellspacing="0" cellpadding="0" style="table-layout: auto">
      <tr>
        <th>商品名</th>
        <th>単位</th>
        <th>数量</th>
        <th>単価</th>
        <th>金額</th>
        <th>備考</th>
      </tr>
      @foreach ($items as $item)
        <tr>
          <td>
            {{ $item['name'] }}
          </td>
          <td style="text-align:center">
            {{ $item['unit'] }}
          </td>
          <td style="text-align:right">
            {{ $item['quantity'] }}
          </td>
          <td style="text-align:right">
            {{ number_format($item['unit_price']) }}
          </td>
          <td style="text-align:right">
            {{ number_format($item['quantity'] * $item['unit_price']) }}
          </td>
          <td style="font-size:12px">
            {{ $item['other'] }}
          </td>
        </tr>
      @endforeach
      <tr>
        <td style="text-align:right"><税抜合計金額></td>
        <td></td>
        <td></td>
        <td></td>
        <td style="text-align:right">{{ number_format($sum_price) }}</td>
        <td></td>
      </tr>
      <tr>
        <td style="text-align:right"><消費税></td>
        <td></td>
        <td></td>
        <td></td>
        <td style="text-align:right">{{ number_format($sum_price * 0.1) }}</td>
        <td></td>
      </tr>
      <tr>
        <td>毎度ありがとうございます。</td>
        <td></td>
        <td></td>
        <td style="text-align:center">合計</td>
        <td style="text-align:right">{{ number_format($sum_price * 1.1) }}</td>
        <td></td>
      </tr>
    </table>
  </div>
</body>
</html>

headタグの@font-faceでは使用するフォントの設定を行っています。storage_pathにはIPAフォントを保存したstorage/fontsを指定します。
font-style:normalだけでは、h1タグのようなfont-weightがboldに設定されているフォントに文字化けが発生します。上記のようにfont-faceを複数設定することで対応できます。

これでPDF表示ページが完成しました。

認証機能

ユーザーと見積を紐付ける

認証機能の追加にともなって、データ構造としてはまずユーザーが存在して、ユーザーごとに自分の見積もりを作っていく形にしたいと思います。

マイグレーション

$ php artisan make:migration add_user_id_to_estimates --table=estimates

作成されたファイルを編集していきます。

add_user_id_to_estimates.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddUserIdToEstimates extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('estimates', function (Blueprint $table) {
            $table->integer('user_id')->unsigned();
            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('estimates', function (Blueprint $table) {
            $table->dropColumn('user_id');
        });
    }
}

upメソッドではuser_idカラムを追加して外部キーを設定する処理を記述しています。downメソッドでは逆にuser_idカラムを削除する処理を記述しています。
マイグレーションを実行しましょう。

$ php artisan migrate:fresh

次にユーザーと見積の関係性をモデルに記述していきます。

User.php
class User extends Authenticatable
{
    // 中略

    public function folders()
    {
        return $this->hasMany('App\Folder');
    }
}

シーダーの作成

ユーザーのシーダーを作成しましょう。

$ php artisan make:seeder UsersTableSeeder

database/seeds/UsersTableSeeder.php が作成されるので、以下の内容で編集します。

UserTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('users')->insert([
            'name' => 'test',
            'email' => 'dummy@email.com',
            'password' => bcrypt('test1234'),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
    }
}

データベース上、ユーザーのパスワードは必ず暗号化してデータベースに保存します。平文では保存しません。bcrypt関数は与えられた文字列の暗号化を行います。

次に見積テーブル用のシーダーを編集します。星マークが追加した行です。

EstimateTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class EstimateTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = DB::table('users')->first(); // ★

        $titles = ['2021年おめでとうセール', '商品見積の件', 'サンプル見積の件'];
        $customers = ['株式会社XXX', '株式会社YYY', '株式会社ZZZ'];

        foreach (array_map(NULL, $titles, $customers) as [ $title, $customer ]) {
            DB::table('estimates')->insert([
                'title' => $title,
                'user_id' => $user->id, // ★
                'customer' => $customer,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}

会員登録機能

Laravelには認証機能が最初から搭載されています。認証機能を受け持つコントローラーはapp/Http/Controllers/Authディレクトリにすでに用意されています。ルーティングについても認証用の設定を吐き出すメソッドが用意されているので、基本的にはテンプレートを作成するだけでアプリケーションに認証機能を追加することができます。

ルーティング

web.php
Auth::routes();

この1行を記述するだけで、会員登録・ログイン・ログアウト・パスワード再設定の各機能で必要なルーティング設定をすべて定義します。

テンプレート

以下の内容で resources/views/auth/register.blade.phpを作成します。

register.blade.php
@extends('layout')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col col-md-offset-3 col-md-6">
        <nav class="panel panel-default">
          <h2 class="panel-heading" style="padding-top:25px">会員登録</h2>
          <div class="panel-body">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('register') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="name">ユーザー名</label>
                <input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password">
              </div>
              <div class="form-group">
                <label for="password-confirm">パスワード(確認)</label>
                <input type="password" class="form-control" id="password-confirm" name="password_confirmation">
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-dark">送信</button>
              </div>
            </form>
            <div class="text-center" style="padding-top:25px">
              <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a>
            </div>
          </div>
        </nav>
      </div>
    </div>
  </div>
@endsection

次にログイン機能を実装します。

resources/views/auth/login.blade.phpを以下の内容で作成します。

login.blade.php
@extends('layout')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col col-md-offset-3 col-md-6">
        <nav class="panel panel-default">
          <h2 class="panel-heading" style="padding-top:25px">ログイン</h2>
          <div class="panel-body">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('login') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password" />
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-dark">ログイン</button>
              </div>
            </form>
          </div>
        </nav>
        <div class="text-center" style="padding-top:25px">
          <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a>
        </div>
      </div>
    </div>
  </div>
@endsection

これでログイン機能の実装は完了です。

プロフィール編集ページ

最後に見積に表示するためのプロフィール編集ページを作成します。

コントローラーの作成

プロフィール編集ページを作成するためのユーザーコントローラーを作成します。

$ php artisan make:controller ItemController
UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    public function showEditForm() 
    {
        $auth = Auth::user();

        return view('auth/edit',[
            'auth' => $auth,
        ]);
    }

    public function edit(Request $request)
    {
        $current_user = Auth::user();

        $current_user->name = $request->name;
        $current_user->postal_code = $request->postal_code;
        $current_user->address = $request->address;
        $current_user->address2 = $request->address2;
        $current_user->company = $request->company;
        $current_user->phone_number = $request->phone_number;
        $current_user->fax_number = $request->fax_number;

        $current_user->save();
        return redirect()->route('estimates.index');
    }
}

Auth::user()でユーザーの情報を取得しユーザー情報を表示・編集します。

ルーティングの追加

web.php
Route::group(['middleware' => 'api'], function(){
    Route::get('/user/edit', 'UserController@showEditForm')->name('user.edit');
    Route::post('/user/edit', 'UserController@edit');
});

あとはテンプレートを作成するだけです。

ユーザー編集ページのテンプレートを作成

@extends('layout')

@section('content')
  <div class="container">
    <h2 class="panel-heading" style="padding-top:25px">ユーザー情報編集</h2>
    <form action="{{ route('user.edit')}}" method="post">
      @csrf
      <div class="row">
        <div class="col-sm-2">ユーザー名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="name" value="{{$auth->name}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">郵便番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="postal_code" value="{{$auth->postal_code}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">住所</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="address" value="{{$auth->address}}">{{$auth->address}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">ビル・マンション名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="address2">{{$auth->address2}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">会社名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="company">{{$auth->company}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">電話番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="phone_number" value="{{$auth->phone_number}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">FAX番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="fax_number" value="{{$auth->fax_number}}">
        </div>
      </div>
      <div style="padding-top:25px;">
        <button type="submit" class="btn btn-dark">保存</button>
      </div>
    </form>
  </div>
@endsection

これで予定していた全ての機能が完成しました。

LaravelとVue.js共にまだまだ使いこなせてない機能がたくさんあるので勉強と開発を進めていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【vue.js】フロントエンド開発にDockerを使う。つらみもあるよ

今までローカルでフロントエンド開発やってたけど、Docker使ってみるか〜。

でもDocker難しくてわからないよ〜〜〜〜〜〜〜!!!!!!

対象読者はvue-cli開発経験のある方です。
最終的なコードはこちら(github)

なぜDockerを使うのか

バックエンドの人たちがDocker信者すぎてフロントエンド開発者の肩身が狭いから

「環境を揃えたいよね」というフワッとした動機から。
実際使ってみて今のところメリットは感じていないですが、vue-cliのバージョンを固定したりするのは追々嬉しいことになるのかも、と思います。未来への投資だと思ってやっていきます。

「Dockerでフロントエンド開発する」とは

どこまでDockerに担わせるか、というところですが、調べた感じだと「実行環境」をDockerで構築するのがベターみたいですね。
image.png
クジラの写真なかった。

vueをDockerで動かすまでの手順

vue-cliプロジェクトの作成

ローカル環境でプロジェクトを作成します。
ローカルの環境はこんな感じです。新規プロジェクトを始めるときに慌てて最新バージョンにしたりしてます。

$ node --version
v14.15.5
$ npm --version
6.14.11 
$ vue --version
@vue/cli 4.5.11

あとはいつも通り

$ vue create docker-vue
$ ls -a
.                 .gitignore        node_modules      public
..                README.md         package-lock.json src
.git              babel.config.js   package.json

Dokcerfileを作成

プロジェクトディレクトリに作ります

Dockerfile
FROM node:14.15.5-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN apk update \
    && npm install -g npm@6.14.11 @vue/cli@4.5.11 \
    && npm install

FROMでnodeイメージのバージョンを指定します。今回はローカルと同じにします。
WORKDIRはDockerコンテナ内でのプロジェクトディレクトリです。なんでもよいです。
COPYでpackage.jsonとpackage-lock.jsonをWORKDIRにコピーします。
RUNでパッケージ等インストールし、環境構築します。ここでnpmとvue-cliのバージョンを固定します。

docker-compose.ymlの作成

docker-compose.yml
version: '3'
services:
  app:
    container_name: docker-vue-test
    build: .
    ports:
      - 8080:8080
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    stdin_open: true
    tty: true
    command: "npm run serve"

コンテナ名をdocker-vue-testと敢えてつけてます。あとで使います。
最後npm run serveを命令することで、コンテナ起動と同時にvueサーバーを起動させます。

動作確認する

まずDockerイメージをビルドします。

$ docker-compose build

そしてコンテナを起動。

$ docker-compose up -d

http://localhost:8080/ にアクセスすると、いつものVueの画面が表示されます。

(任意)ポートを変える

ここまででDockerで実行環境を作るという目標は達成しましたが、
8080ポートは何かと他のプロジェクトでも使ったりするので、明示的にポートを指定してみます。

vue.config.jsの作成

まず、vue-cliをどのポートを使って起動させるか指定します。
プロジェクトディレクトリにvue.config.jsを作成します。

vue.config.js
module.exports = {
  devServer: {
    port: 9000,     # 好きな数字にする
    host: '0.0.0.0',
    disableHostCheck: true,
  },
};

docker-compose.ymlの編集

docker-compose.ymlのportsを、先ほど指定した数字に合わせて修正します。

    ports:
      - 9000:9000

Docker再起動

$ docker-compose stop
$ docker-compose up -d

今度は http://localhost:9000/ で画面が確認できます。

所感

良いところ

いつも通りローカル開発でき、ホットリロードで画面に反映されます。普通です。

うーん…ってなったところ

ホットリロードしなくなった時、ローカル実行ならブラウザのキャッシュをクリアすれば大体治っていたのですが、
Dockerで実行してるとキャッシュを消しても治らないことがあります。
そういう時はコンテナをstop/startするか、全然関係ない箇所をいじったりすると治りました。ここはちょっとよくわからない…

また、追加でライブラリなどを入れる時はコンテナ側にインストールする必要があります。

$ docker exec -it docker-vue-test sh
/usr/src/app # npm install hogehoge...

さいごに

開発初期段階ではまだ「Dockerにして幸せだなあ」と思うことはないです。

ただ手順もそんなに多くはないですし、git cloneしたあとに
以前はnodebrewでnodeバージョンを変えてnpm installしてrun serveして…という手数を踏んでいたところが
Dockerのイメージビルド、コンテナ起動だけになるのは少しシンプルになって小気味好いかもしれませんね。知らんけど。

Dockerに頼りきりにならず、中で何をしているか理解することもとても重要だと思っているので、
それを心に留めつつ今後もDockerと仲良くしていきたいと思います。

参考記事

ローカルを汚さずdockerを使ってvue.jsの開発環境を作る[vuecli4]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】フロントエンド開発にDockerを使う。つらみもあるよ

今までローカルでフロントエンド開発やってたけど、Docker使ってみるか〜。

でもDocker難しくてわからないよ〜〜〜〜〜〜〜!!!!!!

対象読者はvue-cli開発経験のある方です。
最終的なコードはこちら(github)

なぜDockerを使うのか

バックエンドの人たちがDocker信者すぎてフロントエンド開発者の肩身が狭いから

「環境を揃えたいよね」というフワッとした動機から。
実際使ってみて今のところメリットは感じていないですが、vue-cliのバージョンを固定したりするのは追々嬉しいことになるのかも、と思います。未来への投資だと思ってやっていきます。

「Dockerでフロントエンド開発する」とは

どこまでDockerに担わせるか、というところですが、調べた感じだと「実行環境」をDockerで構築するのがベターみたいですね。
image.png
クジラの写真なかった。

vueをDockerで動かすまでの手順

vue-cliプロジェクトの作成

ローカル環境でプロジェクトを作成します。
ローカルの環境はこんな感じです。新規プロジェクトを始めるときに慌てて最新バージョンにしたりしてます。

$ node --version
v14.15.5
$ npm --version
6.14.11 
$ vue --version
@vue/cli 4.5.11

あとはいつも通り

$ vue create docker-vue
$ ls -a
.                 .gitignore        node_modules      public
..                README.md         package-lock.json src
.git              babel.config.js   package.json

Dokcerfileを作成

プロジェクトディレクトリに作ります

Dockerfile
FROM node:14.15.5-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN apk update \
    && npm install -g npm@6.14.11 @vue/cli@4.5.11 \
    && npm install

FROMでnodeイメージのバージョンを指定します。今回はローカルと同じにします。
WORKDIRはDockerコンテナ内でのプロジェクトディレクトリです。なんでもよいです。
COPYでpackage.jsonとpackage-lock.jsonをWORKDIRにコピーします。
RUNでパッケージ等インストールし、環境構築します。ここでnpmとvue-cliのバージョンを固定します。

docker-compose.ymlの作成

docker-compose.yml
version: '3'
services:
  app:
    container_name: docker-vue-test
    build: .
    ports:
      - 8080:8080
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    stdin_open: true
    tty: true
    command: "npm run serve"

コンテナ名をdocker-vue-testと敢えてつけてます。あとで使います。
最後npm run serveを命令することで、コンテナ起動と同時にvueサーバーを起動させます。

動作確認する

まずDockerイメージをビルドします。

$ docker-compose build

そしてコンテナを起動。

$ docker-compose up -d

http://localhost:8080/ にアクセスすると、いつものVueの画面が表示されます。

(任意)ポートを変える

ここまででDockerで実行環境を作るという目標は達成しましたが、
8080ポートは何かと他のプロジェクトでも使ったりするので、明示的にポートを指定してみます。

vue.config.jsの作成

まず、vue-cliをどのポートを使って起動させるか指定します。
プロジェクトディレクトリにvue.config.jsを作成します。

vue.config.js
module.exports = {
  devServer: {
    port: 9000,     # 好きな数字にする
    host: '0.0.0.0',
    disableHostCheck: true,
  },
};

docker-compose.ymlの編集

docker-compose.ymlのportsを、先ほど指定した数字に合わせて修正します。

    ports:
      - 9000:9000

Docker再起動

$ docker-compose stop
$ docker-compose up -d

今度は http://localhost:9000/ で画面が確認できます。

所感

良いところ

いつも通りローカル開発でき、ホットリロードで画面に反映されます。普通です。

うーん…ってなったところ

ホットリロードしなくなった時、ローカル実行ならブラウザのキャッシュをクリアすれば大体治っていたのですが、
Dockerで実行してるとキャッシュを消しても治らないことがあります。
そういう時はコンテナをstop/startするか、全然関係ない箇所をいじったりすると治りました。ここはちょっとよくわからない…

また、追加でライブラリなどを入れる時はコンテナ側にインストールする必要があります。

$ docker exec -it docker-vue-test sh
/usr/src/app # npm install hogehoge...

さいごに

開発初期段階ではまだ「Dockerにして幸せだなあ」と思うことはないです。

ただ手順もそんなに多くはないですし、git cloneしたあとに
以前はnodebrewでnodeバージョンを変えてnpm installしてrun serveして…という手数を踏んでいたところが
Dockerのイメージビルド、コンテナ起動だけになるのは少しシンプルになって小気味好いかもしれませんね。知らんけど。

Dockerに頼りきりにならず、中で何をしているか理解することもとても重要だと思っているので、
それを心に留めつつ今後もDockerと仲良くしていきたいと思います。

参考記事

ローカルを汚さずdockerを使ってvue.jsの開発環境を作る[vuecli4]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue + Rails】422 (Unprocessable Entity) 新規ユーザーの作成ができない原因と解決方法について

原因

  • CSRF保護がオフになっていたため

CSRF(Cross-Site Request Forgery)

悪意のあるユーザーがサーバーへのリクエストを捏造して正当なものに見せかけ、認証済みユーザーを装うという攻撃手法

このエラーが怒った原因として、Vue側(フロント)から送られてきたPOSTリクエストの中に含まれるCSRFトークン。そして、Railsはページのトークンとcookie内のトークンが同一のものではないと判断されたがために起こってしまったエラーと考えられます。

Vue + Railsを使用して作成したがため外部からのPOSTによってエラーが出たのではないかと思います。

解決(結論)

結論から申しますと、僕の場合はcotrollers/user_controller.rb
CSRF(Cross-Site Request Forgery)の対策用のコードを1行足すだけでした。

cotrollers/user_controller.rb
class UsersController < ApplicationController
  protect_from_forgery # 追記

  
  ・(略)
  
end

解決方法

解決に至った手順についてです。

①ディベロッパーツールにて、422 (Unprocessable Entity)を確認

スクリーンショット 2021-03-04 11.44.34.png

②ディベロッパーツールのNetworkタブ→JSでフロント側の確認

スクリーンショット 2021-03-04 11.47.10.png

スクリーンショット 2021-03-04 11.48.20.png

フロント側は大丈夫そう

③続いて、XHRタブ

スクリーンショット 2021-03-04 11.48.58.png

XHRタブは、非同期通信でリクエストを送っているリソースを確認できるみたいです。
スクリーンショット 2021-03-04 11.50.41.png

ここに422エラーがありました。

④色々、触ってみる。(ここでは)usersをタップ

スクリーンショット 2021-03-04 11.52.37.png

するとこんな画面が。

Previewタブをみてみると・・・

⑤ActionController::InvalidAuthenticityToken

スクリーンショット 2021-03-04 11.53.38.png

おお!見慣れたRailsのエラーが!!!!

ここで、「なるほど、CSRF保護がオフになっているのか」と解決まで辿り着きました。

最後に。今回学んだこと

ログや、ディベロッパーツールを舐めてはいけないこと思い知らされました。

恥ずかしながら、エラー発生時、ネットワークタブなどこれまで見向きもしていませんでした。
しかし今回、色々ディベロッパーツールをあさりながら解決に至ることが出来たのも事実です。

Networkタブ。活用していきます。

参考

【Rails API】ActionController::InvalidAuthenticityTokenの解決方法

RailsのCSRF保護を詳しく調べてみた(翻訳)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React/Vue】なぜコンポーネントベースでWebアプリを作るのか

React/Vue などを使ってフロントエンドを開発する際、基本はコンポーネントベースで設計・開発すると思います。
なぜコンポーネントベースなのかどういうメリット・目的があるのか、今一度確認する機会があったので記事にします。
新入社員に「ここってどうしてコンポーネントに分けるんですか?」と聞かれた時に答えられるようにしておきましょう。

コンポーネントベース以外経験が無い方へ
Web アプリの学習を始めて間も無い方の中には、そもそもコンポーネントベース以外経験がない方も居るかもしれません。
そんな方は極端な例ですが、Web アプリの画面がもし1画面1コンポーネントで作られていた場合小さなコンポーネントを積み上げて作られていた場合を想像し、以下の記事では後者のメリットが書かれていると思って読んでみてください。

コンポーネントが持つべき 5 つの特徴

はじめにコンポーネントのあるべき姿ついて確認しておきます。

  1. 単一責任の原則
    コンポーネントが責任を持つ問題は 1 つにするべきです。
    ソースがシンプルになることで可読性が向上します。
    また、ソース変更時の影響範囲が明確になることで保守性が向上します。

  2. カプセル化されている
    コンポーネントを使いたいとき、インターフェースだけ知っていれば内部の実装を気にしなくて良いです。

  3. 置換可能である
    インターフェースさえ同じであれば違うコンポーネントに差し替えることができます。

  4. 再利用可能である
    再利用可能なコンポーネントは、そのコンポーネントが担っている責務に対して過不足なく機能を提供していると言えます。
    「過不足なく」というのは意外と難しく、特にありがちなのが機能を過剰につけてしまうことです。
    ある画面の文脈では必要な機能でも、別の画面では邪魔になることもあります。
    コンポーネントの責務を常に意識し、再利用性を損なうことがないようにすることが大切です

  5. 組み合わせて別の大きなコンポーネントを作成可能である
    再利用性が十分に高いことの裏付けです。
    一つ一つのコンポーネントは小さな問題しか解決できませんが、大きな問題を小さな問題に分割し、適切なコンポーネントに振り分ける役割を持つコンポーネントを作れば、それ自体が大きな問題を解決するコンポーネントになります。

コンポーネントベースのメリット

先に紹介した 5 つの特徴を持つコンポーネントで Web アプリを構成すると以下のようなメリットがあります。

1.複雑な UI も確実に組み立てられる

コンポーネントベース最大のメリットは複雑な UI を確実・堅牢に組み立てられることです。

堅牢な UI 開発を実現する

多くの機能を提供するアプリでは複雑な UI 設計が求められます。
例として Facebook の Web アプリを見てみると、ヘッダー・サイドメニュー・投稿記事リスト・広告など沢山の UI が並んでいます。
これらの UI がもし互いに依存しているような作りになっていた場合、意図せずバグを産んでしまうことがあります。
(ある UI の機能を変更したら別の UI が動かなくなった、ある UI のレイアウトを変更したら別の UI のレイアウトが崩れてしまった、など)
コンポーネント化された UI が個別で不具合なく動作することがテストされていれば、それを正しく使ったアプリの品質も保証できます

コンポーネント単位でテストできる

アプリの品質を担保するための最も重要なポイントです
UI はアプリに組み込まれるとアプリ全体の状態に左右されてしまうことが多いので、単体でテストすることが難しいです。
コンポーネント化して独立した UI であればアプリの状態に依存せず単体でテスト可能です
適切に分割された小さな実装であれば必要なテストケースも少なく、テスト項目も作りやすいです。

不具合のリスクポイントを減らすことができる

一般的に書くコードの量が減ればバグの量も減ります
再利用可能なコンポーネントを作成することで全体のコード記述量を減らすことができます。

メンテナンスがしやすくなる

UI に変更を加えた際に影響を受ける範囲がコンポーネント内に留まるので、メンテナンスがしやすいです
見た目だけを責務とするコンポーネントであれば、修正作業はエンジニアではなくデザイナーが直接行うこともできます。

解決する問題を小さくすることで不具合発生リスクを減らす

複雑なコードより簡単なコードの方が不具合発生リスクは低いです
一つ一つのコンポーネントが単純な問題を解決するために作られているのであれば、内容は簡単になり実装の難易度も低くなります。

2.開発作業を効率化する

コンポーネントベースは品質を担保するとともに開発速度向上にも繋がります。

再利用で実装量を減らす

再利用しやすいコンポーネントを作ることは開発速度向上に直結します

並行開発で待ち時間を減らす

画面単位の開発ではなくコンポーネント単位の開発だと、1画面に必要な UI を複数人で並行開発することも可能です
各コンポーネントは独立しているため、他のタスクに依存せず開発可能です。

仕様変更による手戻り作業を効率化する

画面単位で UI 開発していた場合、画面仕様に変更があれば必ず手戻りが発生します。
小さなコンポーネントを積み上げて大きな部品を開発する方式だと、仕様変更によって影響を受けるコードは比較的少なくなります
また大きな部品ほど後から作られるので、仕様変更のタイミングでまだ画面自体の実装に着手していない可能性もあり、その場合手戻りは無くなることもあります。

新規参入開発メンバーを最短で戦力化する

コンポーネント内で使用する技術スタックだけ知っていれば開発可能なので、サービス背景についての知識は概要だけで良い場合もあります。

複数のテスト・アプローチでテスト工数を下げる

コンポーネントがアプリに依存しない環境で単体実行できるので、さまざまなアプローチでのテストが可能です。
画面に依存している UI ではテストしづらいケースを時間をかけて再現し、何とかテストするということもしばしばありますが、独立したコンポーネントだと単独のテストなら非常に簡単に行うことができます。

複数アプリケーションの開発を容易にする

UI コンポーネントが部品としてアプリから分離できる形になっていれば、別のアプリでコードを再利用することが可能です
仕組み次第では複数アプリから同じコードを参照して、全体のメンテナンスコストを下げることもできます
ただしコードを変更したときの影響範囲がかなり大きくなるので、より堅牢な運用が必要となります。

3.ユーザーメリット

ここまではアプリを開発するエンジニアが受けるメリットの紹介でしたが、アプリを使うユーザーにとってもメリットがあります。

多機能アプリのユーザービリティ向上

アプリが多機能になるとたくさんの UI を画面に表示するため複雑になります。
ある UI コンポーネントが別の画面で使われていたとしても、ユーザーはすでにその UI コンポーネントの使い方を知っているため、すぐにその機能を使い始めることができます

まとめ

コンポーネントが持つべき 5 つの特徴

  1. 単一責任の原則
  2. カプセル化されている
  3. 置換可能である
  4. 再利用可能である
  5. 組み合わせて別の大きなコンポーネントを作成可能である


コンポーネントベースのメリット

1.複雑な UI も確実に組み立てられる

  • 堅牢な UI 開発を実現する
  • コンポーネント単位でテストできる
  • 不具合のリスクポイントを減らすことができる
  • メンテナンスがしやすくなる
  • 解決する問題を小さくすることで不具合発生リスクを減らす


2.開発作業を効率化する

  • 再利用で実装量を減らす
  • 並行開発で待ち時間を減らす
  • 仕様変更による手戻り作業を効率化する
  • 新規参入開発メンバーを最短で戦力化する
  • 複数のテスト・アプローチでテスト工数を下げる
  • 複数アプリケーションの開発を容易にする


3.ユーザーメリット

  • 多機能アプリのユーザービリティ向上


コンポーネント設計をする際にはこれらを意識することでより良い設計ができるかもしれません。
最後に注意点ですが、現実問題としてWeb アプリは必ずしもこの記事で紹介したようなコンポーネントだけで作られるわけではありません
例えばAtomicDesignで言うところの Pages は、実際のコンテンツに影響されるためカプセル化はできず、さらに再利用もできません
React/Vue で書くソース全てが上記の目的を持って書かれるわけではないことは頭に入れておきましょう。

読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む