20190622のvue.jsに関する記事は16件です。

Vue.js + TypeScript + Jestでクラスをモックしてテスト

目標

jestでの単体テスト時にテスト対象クラスの依存先クラスをモックする。

環境

vue-cli 3.2.3
vue createでのプロジェクト作成時にTypeScript、単体テストフレームワークにjestを指定する。

テスト対象

例として下記のようにユーザ情報を取得するリポジトリクラスとリポジトリクラスを利用するサービスクラスを作成する。

リポジトリクラス

UserRepository.ts
export default class UserRepository {
  // 指定されたユーザIDのユーザ名を返す
  public async getUserName(userId: string): Promise<string> {
    // 本来はここでWeb APIを実行するなどしてユーザ情報を取得して返す。
    return "";
  }
}

サービスクラス

UserService.ts
import UserRepository from "./UserRepository";

export default class UserService {
  private userRepository = new UserRepository();

  // リポジトリクラスで取得したユーザ名に"Hello "をつけて返す
  public async helloUser(userId: string): Promise<string> {
    const userName = await this.userRepository.getUserName(userId);
    return "Hello " + userName;
  }
}

サービスクラスの単体テストを行うために、リポジトリクラスをモック化する。今回の例のようにインスタンス化して利用しているクラスをモック化するには、ドキュメントにあるようにコンストラクタ関数をモックする必要がある。
https://jestjs.io/docs/ja/es6-class-mocks

テストクラスを以下のように作成する。jest.mockを使用しUserServiceの依存先であるUserRepositoryをモック化する。そしてjest.fnを使用しコンストラクタ関数をモック化する。

test.spec.ts
import UserService from "@/UserService";

// モック化処理
jest.mock("@/repositories/UserRepository", () => {
  return jest.fn().mockImplementation(() => {
    return {
      // getUserName関数が返す値を引数userIdの値によらず、"hoge"に固定する
      getUserName: async (userId: string): Promise<string> => {
        return Promise.resolve("hoge");
      }
    };
  });
});


// テスト
describe("Test", () => {
  // 非同期処理のテストのためasyncをつけている
  it("helloUser test", async () => {
    const service = new UserService();
    // helloUser関数の返り値を検査
    expect(await service.helloUser("id1")).toEqual("Hello hoge");
  });
});

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

【Nuxt】共通のJavaScriptをheadに記述する方法

こんにちは、ブログ「学生ブロックチェーンエンジニアのブログ」を運営しているアカネヤ(@ToshioAkaneya)です。

【Nuxt】共通のJavaScriptをheadに記述する方法

このようにnuxt.config.jsに記述することで、全てのpageで共通のJavaScriptを実行することができます。

nuxt.config.js
// ...
  head: {
    script: [      
      {
        innerHTML: `alert('Hello!');`
      }
    ],
    __dangerouslyDisableSanitizers: ['script'],
// ...

__dangerouslyDisableSanitizers: ['script']は、innerHTML内の文字がエスケープされるのを防ぐためのオプションです。これがないと文字列がうまく出力できません。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

Vueのテンプレート中でspreadしてpropsを渡すにはどうすればいいか

はじめに

よくたくさんの項目をpropsをとして渡さなければいけないときがあります。

今までReactを使っていたときは「...」で良かったですが、Vueだとテンプレート中でspread operatorを使えません。

そこでどうやるかのメモ。

spreadとは

以下Javascript/MDNからコード引用

オブジェクトを分割して変数に入れることができます。

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// Object { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// Object { foo: "baz", x: 42, y: 13 }

悪い例

以下は最初にやりがち。よくやる悪い例。自分もspreadを知らないときはこう書いていました。

<template>
...
  <Form
    :firstname="firstname"
    :lastname="lastname"
    :birthday="birthday"
    :phonenumber="phonenumber"
  />
...
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import IUser from "@/Models/User";
import Form from "@/components/Organisms/Form.vue"

@Component({
  name: "UserForm",
  component: { Form }
})
export default class UserForm extends Vue {
  @Props() private user: IUser;
}
</script>

良い例

これでオブジェクトをspreadで分けてpropsとして渡すことができる。

<template>
  <Form v-bind="user"/>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import IUser from "@/Models/User";
import Form from "@/components/Organisms/Form.vue"

@Component({
  name: "UserForm",
  component: { Form }
})
export default class UserForm extends Vue {
  @Props() private user: IUser;
}
</script>

spreadして渡すにはv-bindを使用します。

これでコードがスッキリしました。

ちゃんと分割してからPropsとして渡すことができます。

まとめ

spreadを活用してPropsへ分割してから渡すようにすることでコーディングの時間を減らしていきましょう

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

Tornado(websocket)とVueを使ってTwitterのトレンドを表示する

出来上がったもの

こちら
ソースコード

Tornadoについて

公式ドキュメント
Facebookにより開発されているPythonで書かれたWebフレームワーク。
ノンブロッキングI/Oを使用しているので、WebSocketなど長期接続を必要とするアプリケーションに最適だとドキュメントで紹介されています。

アプリケーションの概要

TwitterApiでトレンドを一定間隔で取得websocketでクライアントにプッシュVue.jsでリストレンダリング

という単純なものです。

サーバーサイド

公式のwebsocketデモアプリを少し改変した程度です。

トレンドを一定間隔で取得する

app.py
api = tweepy.API(auth)
regional_id = {}
for place in api.trends_available():
    if place['countryCode'] == 'JP':
        regional_id[place['name']] = place['woeid']
JP = regional_id['Japan']

def loop_in_period_interval():
    PERIOD = 30
    ioloop = tornado.ioloop.IOLoop.current()
    ioloop.add_timeout(time.time() + PERIOD, loop_in_period_interval)
    trends = []
    for idx, trend in enumerate(api.trends_place(JP)[0]['trends'], 1):
        value = {
            "rank": str(idx),
            "name": trend["name"],
            "volume": trend["tweet_volume"],
            "url": trend["url"]
        }
        trends.append(value)
    TwitterTrendWebSocketHandler.trends_cache = trends
    json_str = json.dumps(trends)
    TwitterTrendWebSocketHandler.send_updates(json_str)

loop_in_period_intervalで30秒間隔でトレンドを取得しています。
なお、この部分はこちらを参考にさせていただきました。

クライアントにプッシュ

app.py
class TwitterTrendWebSocketHandler(tornado.websocket.WebSocketHandler):
    waiters = set()
    trends_cache = []

    def get_compression_options(self):
        return {}

    def open(self):
        TwitterTrendWebSocketHandler.waiters.add(self)
        trends = json.dumps(
            TwitterTrendWebSocketHandler.trends_cache
        )
        self.write_message(trends)

    def on_close(self):
        TwitterTrendWebSocketHandler.waiters.remove(self)

    @classmethod
    def send_updates(cls, trends):
        logger.info("sending message to %d waiters", len(cls.waiters))

        for waiter in cls.waiters:
            try:
                waiter.write_message(trends)
            except:
                logger.error("Error sending message", exc_info=True)

websocketのhandlerです。
接続時にopenメソッドでクラス変数waitersにクライアントが追加されます。
先程のloop_in_period_interval関数内でクラスメソッドsend_updatesが実行され各クライアントにブロードキャストされます。

app.py
if os.getenv("HEROKU") is None:
    dotenv_path = os.path.join(os.path.dirname(__file__), ".env")
    load_dotenv(dotenv_path)
    port = 8888
else:
    port = int(os.environ.get("PORT", 5000))

CONFIG = os.environ

HEROKUを使うにあたって.envファイルで環境変数を切り替えています。
また、こちらのプラグインを使うとheroku config:pushコマンドでローカルの.envファイルの内容をHEROKUの環境変数に設定できるのでおすすめです。

クライアントサイド

単純なアプリなのでvue-cliを使わずCDNを使っています。
その際、注意すべき点として記法の衝突があります。
Tornadoのテンプレートで使う{{}}がVueのマスタッシュ記法と同じになってしまいエラーが発生します。

app.js
const vm = new Vue({
  el: '#app',
  // 略
  delimiters: ["<%","%>"], //{{ }} から <% %>に変更
})

なので、このようにVue側でデリミタを変更する必要があります。

リストレンダリングにトランジションを追加する

テーブルに適用する際

index.html
<transition-group tag="tbody" id="left">
  <!-- 1 to 25 -->
    <tr v-for="trend in upTo25" :key="trend.name" v-cloak>
      <th><% trend.rank %></th>
      <td><a :href="trend.url" class="has-text-grey-darker"><% trend.name %></a></td>
      <td><% trend.volume %></td>
    </tr>
 </transition-group>

公式サイトにあるように、transition-groupタグを使い、tagtbodyを指定したのですが動作しませんでした。

index.html
<tbody is="transition-group" id="left">
  <!-- 1 to 25 -->
    <tr v-for="trend in upTo25" :key="trend.name" v-cloak>
      <th><% trend.rank %></th>
      <td><a :href="trend.url" class="has-text-grey-darker"><% trend.name %></a></td>
      <td><% trend.volume %></td>
    </tr>
</tbody>

こうすることでうまく動作しました。

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

【Vue.js】zip ファイルの送受信【Go】

概要

クライアントの Vue.js とサーバーの Go との間で zip ファイルをやりとりします。

  • クライアント:FormData に zip ファイルを入れてポストする
  • サーバー  :受け取った zip ファイルをそのまま返す
  • クライアント:返ってきたデータをダウンロードする

環境

$ vue --version
3.8.4

$ go version
go version go1.11.2 windows/amd64

クライアント

プロジェクトを作成します。

$ vue create client
$ cd client
$ npm install axios

App.vue を変更します。

App.vue
<template>
  <div id="app">
    <input @change="select" type="file" accept="application/zip"><br/>
    <button @click="upload">Upload</button>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'app',
  data () {
    return {
      file: null
    }
  },
  methods: {
    select(e) {
      this.file = e.target.files[0]
    },
    upload() {
      const url = 'http://localhost:3000'

      let data = new FormData()
      data.append('zip', this.file)

      const config = {
        headers: { 'Content-Type': 'application/multipart/form-data' },
        responseType: 'arraybuffer'
      }

      axios.post(url, data, config).then(res => {
        this.download(res)
      })
    },
    download(res) {
      const name = res.headers['content-disposition'].split('=')[1]
      const type = res.headers['content-type']
      const blob = new Blob([res.data], { type: type })
      const link = document.createElement("a")
      link.href = window.URL.createObjectURL(blob)
      link.download = name
      link.click()   
    }
  }
}
</script>

実行

$ npm run serve

クライアント補足説明

今回は zip ファイルのみを扱うので input タグの accept で zip に制限しています。

<input @change="select" type="file" accept="application/zip">

ファイルを送信するので Content-Type に application/multipart/form-data を設定し、
ダウンロード後に zip が解凍できるように responseType: 'arraybuffer' を設定しています1

const config = {
    headers: { 'Content-Type': 'application/multipart/form-data' },
    responseType: 'arraybuffer'
}

サーバー

プロジェクトを作成します

$ mkdir server
$ cd server

main.go を作成します

main.go
package main

import (
    "io/ioutil"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        //data.append('zip', this.file) の "zip"
        file, header, _ := r.FormFile("zip")
        defer file.Close()

        bytes, _ := ioutil.ReadAll(file)

        //確認用に main.go と同じディレクトリに保存する
        ioutil.WriteFile(header.Filename, bytes, 077)

        w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080")
        w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
        w.Header().Set("Content-Disposition", "attachment;filename="+header.Filename)
        w.Header().Set("Content-Type", "application/zip")
        w.Write(bytes)
    })
    http.ListenAndServe(":3000", nil)
}

実行

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

Vue.jsバージョン3はこう変わるかもしれない

Vue.jsのRequest For Commentsが公開されました。
https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md

これらのアイディアはまだ「コメントを求める」段階であるため決定したわけではありませんが、Vue使いの方々は内容を知っておくべきでしょう。

setupメソッド

今まで別々に分かれていたdataやwatch、methodsが全てsetupに集約されます。

<template>
  <div>
    <span>count is {{ count }}</span>
    <span>plusOne is {{ plusOne }}</span>
    <button @click="increment">count++</button>
  </div>
</template>

<script>
import { value, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}
</script>

なぜこんなことに?

Vueの良さはわかりやすさにありました。ReactやAngularに比べてどこで何をするかが分かり易かったため、特に初学者に対して敷居が低く好まれてきました。
この劇的な変化は分かりやすさという点において後退が見られます。

しかしこのsetupメソッドに全てを集約する記述によって、カプセル化を行うことができるようになります。
例えば、マウスの位置を扱う処理は次のようにuseMouse()の中にカプセル化できるようになるのです。

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// in consuming component
const Component = {
  setup() {
    const { x, y } = useMouse()
    return { x, y }
  },
  template: `<div>{{ x }} {{ y }}</div>`
}

これは従来の記述と比較するとそのカプセル化を理解できます。
マウスの位置を取得するための処理やデータが、data,mounted,methodsとバラバラになっていました。コンポーネントが肥大化した時にスパゲティコードになりがちになっていました。

<template>
    <div>
        {{ x }} {{ y }}
    </div>
</template>

<script>
export default {
  data() {
    return {
      x: 0,
      y: 0,
    };
  },
  mounted() {
    window.addEventListener('mousemove', this.update);
  },
  beforeDestroy() {
    window.removeEventListener('mousemove', this.update);
  },
  methods: {
    update(e) {
      this.x = e.pageX;
      this.y = e.pageY;
    },
  },
};
</script>

もっと

今までのVueはTypescriptの型推論を適切にサポートできていませんでした。しかし今回提案されたカプセル化によってVueにTypeScriptの恩恵が導入されます。
さらに、関数名と変数名は標準の最小化で短縮できるため(オブジェクト/クラスのメソッドとプロパティではできません)、コードはよりよく圧縮できるようになります。

最後に

今回のVueの変更の提案はかなりドラスティックで面白いと思いました。
SwiftUIでもそうでしたが、フロント側の技術はパラダイムシフトのように大きく変化するのでキャッチアップしていきたいです。
僕自身は今回のVueのこの変更の提案はすごく面白いと思いますし、さらなるVueの進化につながると思います。

参考 : https://dev.to/stefandorresteijn/vuejs-is-dead-long-live-vuejs-1g7f

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

Vuetify+Cordovaでハイブリッドアプリ開発

Vuetifyを使ったCordovaアプリ開発のサンプル(導入編)

前提条件

  • Cordovaの開発環境が構築済み

Vue-Cli 3の導入

npm install -g @vue/cli

確認

vue --vesion
# 3.8.4

vue プロジェクトの作成

vue create sample-app

※プロジェクト名は小文字じゃないとエラーになる。

オプションの選択

色々聞かれるのでとりあえずデフォルトで作成

Vuetify, Cordovaの追加

プロジェクトフォルダに移動しておきましょう

cd sample-app

vuetify

vue add vuetify

全てデフォルトで。

Cordova

vue add cordova

Cordovaソースが置かれる場所、アプリ名、パッケージ名が聞かれます。
ネイティブアプリプラットフォームの設定が保存されているsrc-cordovaに、cordovaプロジェクト用の別のsrcフォルダーが作成されます。

(任意)gitの追加

.gitignoreファイルが自動で生成されているので、ここでgithubのリモートリポジトリを追加します

git remote add origin https://github.com/xxx/xxx.git
git add .
git commit -m "first commit"
git push -u origin master

Cordova関連コマンドの動作確認

Cordova導入時に設定したソースフォルダに移動します。

cd src-cordova

あとは通常のCordovaコマンドが使用できます。

cordova platform ls

vueアプリケーションのソースを弄る

src-cordovaフォルダにいる場合は一旦プロジェクトのルートに移動します

../

ソースを触る際は、src/配下のソースを修正します。

Vueアプリケーションのデバッグ

単純にHTMLとかの確認をしたい場合は、以下のコマンドでサーバーを起動します。

npm run serve

シェルに表示されたアドレスをブラウザで開きます。
(デフォルトはおそらくhttp://localhost:8081)
これで現在のVueアプリケーションがブラウザに表示されます。
Hot reload対応なのでソースを保存すると勝手に画面が更新されます。

vueアプリケーションをcordovaに適用する

vueアプリケーションをビルドします。
とりあえず、ブラウザで実行してみます。

npm run cordova-serve-browser

・・・が、Windowsだとエラーが出てうまくビルドできません。
Githubに同様のissueが上がっていたのでそちらで進展があったら追記します。

※Macではうまくいくようです。

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

CentOSへVue.jsをインストール

書いてあること

  • CentOSへのVue.jsのインストール手順
  • webpackテンプレートによるVue.jsプロジェクトの作成手順

環境

  • CentOS Linux release 7.6.1810 (Core)
  • Node.js v10.16.0
  • Npm 6.9.0
  • Vue 3.8.4

インストール

Node.jsをインストール

CentOSへNode.jsをインストール

Vueをインストール

bash
$ npm install -g @vue/cli @vue/cli-init

バージョンを確認

bash
$ vue --version

webpackテンプレートによるVue.jsプロジェクト作成

プロジェクト作成

bash
#プロジェクトの作成
$ vue init <テンプレート名> <プロジェクト名>

#webpackテンプレートを利用した場合
$ vue init webpack vue-webpack-sample

? Project name vue-webpack-sample
? Project description A Vue.js project
? Author
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

#プジェクトディレクトリに移動
$ cd <プロジェクト名>

#インストール
$ npm install

開発サーバー起動

bash
$ npm run dev

ビルド

distディレクトリにビルド結果が保管されるため、このデータをレンタルサーバーなどにアップする

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

Vuefireが無いエラー【Vue.jsとfirebaseの環境構築】

firebase×Vue.js×firestoreでアプリ開発

Vue.jsで実装したアプリをfirebase環境下で動かしたい。
firebaseを使うからには、DBはcloud firestoreを使おう

前提

  • vue.jsをインストール
  • firebaseをインストール
  • vuefireをインストール
  • firebaseの設定を書くファイルfirebase.jsを作成

環境

MacOS HighSierra
vue.js 2.9.6
firebase 6.11.0

問題点

Vuefireがインポートされない

export 'default' (imported as 'VueFire') was not found in 'vuefire'

問題のファイル内

firebase.js

import Vue from 'vue'
import VueFire from 'vuefire'

Vue.use(VueFire)

const firebaseApp = firebase.initializeApp({
firebaseの設定
})

import VueFire from 'vuefire'でインポートするときに、VureFireが無いよって怒られているみたいです。

解決方法

import Vue from 'vue'
import { firestorePlugin } from 'vuefire'

Vue.use(firestorePlugin)

以下略

インポートするときのプラグインの名前が異なっているようなので、{ firestorePlugin }と変数にしてあげたらうまく行きました。

まとめ

アップデートで名前が変わることもありうるので、プラグインをインポートするときには変数名を入れてあげたほうが安全かもしれません。
勉強になりました。

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

【Vue.js 6】単一ファイルコンポーネントによる開発

1. 単一ファイルコンポーネンの構成

vue
<template>
  <div class="example">
    <span class="title">{{ text }}</span>
  </div>
</template>

<script>
//ライブラリや他のコンポーネントのインポート(ES2015のimport構文)
import MyModel from 'my-modal'
//exportは必須!(ES Modulesのexport構文)
export default {
 name: 'Example',
 data() {
   return {
     text: 'example'
   }
 }
}
</script>

<style scoped>
/* カプセル化されたローカルなスタイル */
.message {color: #000;}
</style>

<style>
/* グローバルなスタイル */
.message {color: #000;}
</style>

スコープ付きCSS(Scoped CSS)

vue
<!-- スコープ付きCSS -->
<style scoped>
  .title {
    color: #ffbb00;
  }
</style>
<!-- コンパイル後は[data-v-xxx]属性追加される -->
<style>
span.title[data-v-aaaaaa]{ color: #ff0000; }
</style>

<!-- 子コンポーネントの扱い -->
<div class="example">
 <child-comp/>
</div>

<div class="example" data-v-aaaaaa>
 <div data-v-aaaaaa data-v-bbbbbb><!-- 子のルート要素 -->
   <span data-v-bbbbbb>child-comp</span>
 </div>
</div>

<!-- スコープをまたぐ指定 -->
<!-- (お互いにスコープの付いたコンポーネントから子のセレクタ.bを指定したい場合) -->
<style scoped>
 .a >>> .b { color: #ff0000; }
</style>
<style lang="scss" scoped>
 .a /deep/ .b { color: #ff0000; }
</style>

外部ファイルのインポート

Vue.js
<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style1.css"></style>
<style src="./style2.scss" lang="scss" scoped></style>

PugやSassなどの使用(lang指定する)

Vue.js
<template lang="pug">
 div#exsample
   span {{ text }}
</template>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Vue]watchフックで配列を監視する場合、ディープウォッチャーにしておく必要がある件

先に結論のコード

See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.

JS側11行目(ディープウォッチャーの指定部分)
  watch: {
    users: {
      handler: function(){
        alert('変更を検出しました');
      },
      deep: true
    }
  },

今回したいこと

Vue.jsにはwatchフックという仕組みがあり、指定したデータに変更があった際、仕込んでおいたメソッドを起動してくれます。
便利な仕組みで重宝されますが、watchフックは監視する対象が配列(またはオブジェクト)の場合、配列自体が変更されると検知してくれるのですが、配列の中(要素など)の変更については、検知してくれません。
以下に具体例を出しながら、今回はこれについて一歩深入りしてみたいと思います。

監視対象が単なる文字列の場合

See the Pen watchフックについて by riotam (@riotam4) on CodePen.

入力欄を変更すると、その度に変更を検知して、

JS側6行目
  watch: {
    user: function(){
      alert('変更を検出しました');
    }
  },

↑が起動して、ダイアログを出します。
ちょっと、うっとしい感じになっちゃってますが…笑
ちゃんと、監視してくれてて、ほぼリアルタイムで検知してくれていることを確認してみてください。

監視対象が配列で、変更対象がその中の1要素の場合

See the Pen watchフックについて(2) by riotam (@riotam4) on CodePen.

次にこちらを試してみてください。
ちゃんと変更の反映はされますが、変更を検知していない様子。
これが、はじめの方にも書かせてもらった、監視できていないパターンです。
これの対策方法が、今回の本題です。

対策は「ディープウォッチャー」にすること

See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.

これが、ディープウォッチャーにしているパターンです。
変更を毎回検知してくれているのが、確認できるかと思います。
具体的に変更を加えるのは、

JS側11行目
  watch: {
    users: {
      handler: function(){
        alert('変更を検出しました');
      },
      deep: true
    }
  },

ここです。
処理内容は、hundler部分に転記して、加えてdeep:trueとすることで、ディープウォッチャーモードにしています。

結論

watchフックは、Vue.jsでも非常に便利な機能ですが、配列などに使う際にはディープウォッチャーモードにできているか、注意が必要。

以上です。
最後ありがとうございました。

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

[Vue]watchフックで連想配列を監視する場合、ディープウォッチャーにしておく必要がある件

先に結論のコード

See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.

JS側11行目(ディープウォッチャーの指定部分)
  watch: {
    users: {
      handler: function(){
        alert('変更を検出しました');
      },
      deep: true
    }
  },

今回したいこと

Vue.jsにはwatchフックという仕組みがあり、指定したデータに変更があった際、仕込んでおいたメソッドを起動してくれます。
便利な仕組みで重宝されますが、watchフックは監視する対象が連想配列の場合、連想配列自体が変更されると検知してくれるのですが、連想配列の中(要素など)の変更については、検知してくれません。
以下に具体例を出しながら、今回はこれについて一歩深入りしてみたいと思います。

監視対象が単なる文字列の場合

See the Pen watchフックについて by riotam (@riotam4) on CodePen.

入力欄を変更すると、その度に変更を検知して、

JS側6行目
  watch: {
    user: function(){
      alert('変更を検出しました');
    }
  },

↑が起動して、ダイアログを出します。
ちょっと、うっとしい感じになっちゃってますが…笑
ちゃんと、監視してくれてて、ほぼリアルタイムで検知してくれていることを確認してみてください。

監視対象が連想配列で、変更対象がその中の1要素の場合

See the Pen watchフックについて(2) by riotam (@riotam4) on CodePen.

次にこちらを試してみてください。
ちゃんと変更の反映はされますが、変更を検知していない様子。
これが、はじめの方にも書かせてもらった、監視できていないパターンです。
これの対策方法が、今回の本題です。

対策は「ディープウォッチャー」にすること

See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.

これが、ディープウォッチャーにしているパターンです。
変更を毎回検知してくれているのが、確認できるかと思います。
具体的に変更を加えるのは、

JS側11行目
  watch: {
    users: {
      handler: function(){
        alert('変更を検出しました');
      },
      deep: true
    }
  },

ここです。
処理内容は、hundler部分に転記して、加えてdeep:trueとすることで、ディープウォッチャーモードにしています。

結論

watchフックは、Vue.jsでも非常に便利な機能ですが、連想配列などに使う際にはディープウォッチャーモードにできているか、注意が必要。

以上です。
最後ありがとうございました。

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

LaravelとVue.jsでクイズアプリを作った話

はじめに

今回、日頃のプログラミング学習のアウトプットとしてLaravelとVue.jsでWebアプリを制作しました。
どういった方法で制作したのか、どういう流れで進めていったのかを個人的な振り返りもかねて記事にまとめてみたのでこれからWebアプリを作ってみようかなと考えている方、LaravelとVueで何か作ってみたい方の参考になれば嬉しいです。

目的

前回はPHPを用いてフルスクラッチでのWebアプリを開発しました。

WebサービスをXserverで公開する方法 - Qiita

Webサービスの基本的な機能の開発のアウトプットができたので、今回は開発現場では主流?のフレームワークを用いての開発経験をつけたくLaravelとVue.jsでWebサービスを作っとみようと思った次第です。

今回のポートフォリオの制作で目指したこと。

  • MVCモデルの理解
  • LaravelとVue.jsでのWebアプリの完成
  • フレームワーク独自の機能について知る。利用する。
  • CSS設計
  • スマホでの利用に重きを置いたのでレスポンシブ対応を強化 # 開発環境 ## 使用言語・データベース
  • PHP 7.2.15
  • JavaScript
  • HTML
  • CSS
  • MySQL

フレームワーク

  • Laravel
  • Vue.js

使用ツール・ライブラリ

  • jQuery
  • Bootstrap
  • SASS
  • axios
  • GitHub

その他は以下参照↓

package.json
{
    "private": true,
    "scripts": {
        "dev": "NODE_ENV=development webpack --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "NODE_ENV=production  webpack --config=node_modules/laravel-mix/setup/webpack.config.js",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "NODE_ENV=development webpack --config=node_modules/laravel-mix/setup/webpack.config.js --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "axios": "^0.18",
        "bootstrap": "^4.0.0",
        "browser-sync-webpack-plugin": "^2.2.2",
        "cross-env": "^5.1",
        "jquery": "^3.2",
        "laravel-mix": "^4.0.15",
        "lodash": "^4.17.5",
        "popper.js": "^1.12",
        "resolve-url-loader": "^2.3.1",
        "sass": "^1.15.2",
        "sass-loader": "^7.1.0",
        "vue": "^2.6.10",
        "vue-template-compiler": "^2.6.9"
    }
}

制作物について

アプリ名:『やいまクイズ
やいまクイズは沖縄のさらに南にある八重山(やえやま)諸島についてクイズ形式で学べるクイズアプリです。

私自身が八重山諸島の出身という事もあり、地元に関する何かを作りたいと考え、利用者にクイズ形式で楽しみながら八重山諸島について知る機会になればと思い今回のようなクイズアプリを制作しました。

主な機能

■ユーザー関連

  • ユーザー登録
  • ログイン・ログアウト
  • パスワードリマインダー
  • パスワード、メールアドレス変更
  • 退会
  • マイページ

■クイズ関連

  • クイズページ
  • クイズ結果をツイート
  • クイズの作成・編集・削除
  • クイズ一覧ページ

デモ ?

クイズページ

quiz.gif

クイズ作成

create.gif

メールアドレス変更

mail_change.gif

制作手順

1. 機能洗い出し

言わずもがな、まず初めに何を作るのかどんな機能を入れるのかを考えていきます。

↓簡単に表にまとめる
スクリーンショット 2019-06-17 23.05.56.png

↓MVCモデルを実際に書いてみると構造のイメージが掴みやすくなるのでおすすめ✨
スクリーンショット 2019-06-17 23.11.28.png

2. テーブル設計

ユーザー登録や、今回のようなクイズ作成時に必要なデータ情報は何かを洗い出しテーブル設計をしていきます。
スクリーンショット 2019-06-17 23.24.48.png

3. WF(ワイヤーフレーム)作成

画面をすぐさまコーディングするのではなく、紙や、WF作成ツール等でどういうレイアウトにするのかを考えてからコーディングをするようにしましょう。

デザインが曖昧なままコーディングに取りかかるとあとあと修正が増えたりして返って非効率になってしまします。
(と、偉そうにいってますが今回のWFめっちゃテキトー書いちゃってますww)
スクリーンショット 2019-06-17 23.48.30.png

[ひとりごと]
最近知ったんだけど、Adobe XDというツールがWF作成に便利っぽい!
しかも、無料で使えるとは!!
Adobeのツール(フォトショとかイラレ)って有料のイメージだったけどこのXDは無料でも使えるのね〜。
今度からはXD利用してWF作成してみよう。?

3. CSS設計について(反省...?)

コーディング当初はBootstrapを用いてコーディングをしていました。
主にレスポンシブに対応するためにグリットシステムを導入。また、ボタンフォームドロップダウンメニュー等です。

後半からひょんな事にCSS設計についての学習がてらFLOCSSを導入しようと試みてものすごくリファクタリングに時間がかかってしまいました。(結局中途半端になってしまったけど、、)

コーディングを行う前にWF作成をする事もそうですが、CSS設計についてもあらかじめルールを決めておくのが吉ですね。

4. Laravelの環境構築と画面作成

初めにLaravelが使用できるように環境構築します。

laravelの設置手順をまとめ - Qiita

画面のコーディングに関しては、

  • クイズ画面はVue.js
  • その他の画面(ユーザー登録、クイズ作成、一覧、etc..)はLaravelのblade

で作成しました。一部画面のソースコードを紹介↓

クイズTOP画面

show_quiz.blade.php
@include('layouts.head')

<title>やいまクイズ</title>
</head>

<body>
    <quiz-header id="quiz-header"></quiz-header>
    <quiz-contents id="quiz-contents"></quiz-contents>

    <footer class="l-footer">
        Copyright© <a href="https://yonaguni-media.com" target="_blank">どなんメディア</a>.
    </footer>
    <script src=" {{ mix('js/app.js') }} "></script>
</body>

</html>

↓クイズ画面のコンポーネントの構成としては、

クイズ画面の構成
├──QuizHeader.vue
|    ├──QuizMenu.vue
├──QuizContents.vue
     ├──QuizResult.vue

↓クイズの問題・選択肢部分のコンポーネント

QuizContents.vue
<template>
  <main id="quiz" class="l-section__wide">
    <article id="question" class="p-quiz">
      <section>
        <div v-if="hidden">
          <h1 class="c-bar c-bar--large c-bar--pink">問題 {{quizNum}}.{{quizzes[quizNum - 1].title}}</h1>
          <div v-if="showQuiz">
            <div v-if="quizzes[quizNum - 1].image_name">
              <div class="p-quiz__img">
                <img :src="quizzes[quizNum - 1].image_name" alt="クイズ画像">
              </div>
            </div>

            <div class="p-quiz__choice">
              <ul v-for="choice in aChoice">
                <li class="c-bar c-bar--gray" @click="showAnswer(choice)">{{ choice }}</li>
              </ul>
            </div>
          </div>
        </div>

        <div class="p-quiz__explain" v-if="showExplain">
          <h2 class="is-correct" v-if="judgment">
            <i class="far fa-circle mr-4"></i>正解!
          </h2>
          <h2 class="is-uncorrect" v-else>
            <i class="fas fa-times mr-4"></i>不正解
          </h2>
          <p>
            <strong>解説:</strong>
            {{quizzes[quizNum-1].explain_sentence}}
          </p>
          <button @click="next()" type="button" class="btn btn-default">次へ</button>
        </div>
      </section>

      <section class="p-quiz__empty-msg" v-if="alertMsg">
        <p>
          <i class="far mr-2 fa-lg fa-tired"></i>クイズはまだ登録されていません。
          <i class="far fa-lg fa-tired"></i>
        </p>
        <a href="/quiz">クイズTOPへ</a>
      </section>
    </article>

    <quiz-result ref="result" :totalCorrectNum="totalCorrectNum"></quiz-result>
  </main>
</template>

<script>
import QuizResult from "./QuizResult";
export default {
  name: "QuizContents",
  components: {
    QuizResult
  },
  data: function() {
    return {
      quizNum: 1,
      totalQuizNum: 0,
      totalCorrectNum: 0,
      quizzes: [
        {
          title: "",
          correct: "",
          uncorrect1: "",
          uncorrect2: "",
          image_name: "",
          explain_sentence: ""
        }
      ],
      aChoice: [],
      showQuiz: true,
      showExplain: false,
      existImage: false,
      hidden: false,
      alertMsg: false,
      judgment: "",
      axiosUrl: ""
    };
  },
  created() {
    //DOM構築前にクイズデータをaxiosで取得(そうしないとエラーでる↓)
    //"TypeError: Cannot read property 'title' of undefined"
    this.getQuizzes();
  },
  methods: {
    getQuizzes: function() {
      let quizUrl = location.pathname;
      let catId = quizUrl.match(/\d/g);
      let catNum;
      if (catId) {
        catNum = catId.join("");
      }

      if (quizUrl == "/quiz/" + catNum) {
        this.axiosUrl = "ajax/menu" + catNum;
      } else if (quizUrl == "/quiz/region/" + catNum) {
        this.axiosUrl = "ajax/region" + catNum;
      } else {
        this.axiosUrl = "ajax/menu";
      }

      axios
        .get(this.axiosUrl)
        .then(res => {
          this.quizzes = res.data;
          this.totalQuizNum = this.quizzes.length;
          //クイズがある時はDOMを表示しクイズがない場合は無いですメッセージを表示
          if (this.totalQuizNum) {
            this.hidden = true;
          } else {
            this.alertMsg = true;
          }
          this.getChoice(this.quizNum - 1);
        })
        .catch(error => {
          console.log(error);
        });
    },
    shuffleAry: function(array) {
      const ary = array.slice();
      for (let i = ary.length - 1; 0 < i; i--) {
        let r = Math.floor(Math.random() * (i + 1));
        [ary[i], ary[r]] = [ary[r], ary[i]];
      }
      return ary;
    },
    getChoice: function(index) {
      //前回の選択肢を削除してから新しく選択肢を追加する
      this.aChoice = [];
      this.aChoice.push(
        this.quizzes[index].correct,
        this.quizzes[index].uncorrect1,
        this.quizzes[index].uncorrect2
      );
      this.aChoice = this.shuffleAry(this.aChoice);
    },
    showAnswer: function(choice) {
      this.showQuiz = !this.showQuiz; //false
      this.showExplain = !this.showExplain; //true

      let answer = this.quizzes[this.quizNum - 1].correct;
      if (choice === answer) {
        this.judgment = true;
        this.totalCorrectNum++;
        this.$refs.totalCorrectNum;
      } else {
        this.judgment = false;
      }
    },
    next: function() {
      if (this.quizNum < this.totalQuizNum) {
        this.showQuiz = true;
        this.showExplain = false;
        this.quizNum++;
        this.nextCounter++;
        this.getChoice(this.quizNum - 1);
      } else {
        this.$refs.result.showResult();
      }
    }
  }
};
</script>

<style scoped>
</style>

クイズ登録画面

create_quiz.blade.php
@extends('layouts.formWithHeader')

@section('title','クイズ作成')

@section('content')
<form method="post" action="{{ url('mypage') }}" enctype="multipart/form-data" class="form">
  @csrf
  @method('POST')
  <div class="form-heading">
    <h1>クイズの作成</h1>
    <p>八重山についてのクイズを作成してみよう。</p>
  </div>

  <div class="form-group">
    <div class="row">
      <div class="col-6">
        <label>カテゴリ<span class="attention">必須</span></label>
        <select class="form-control" id="category" name="category_id">
          @foreach ($categories as $category)
          <option value="{{ $category->id }}" {{ $category->id == old('category_id') ? 'selected' : '' }}>
            {{ $category->name }}
          </option>
          @endforeach
        </select>
      </div>
      <div class="col-6">
        <label>地域<span class="attention">必須</span></label>
        <select class="form-control" id="region" name="region_id">
          @foreach($region as $island)
          <option value="{{ $island->id }}" {{ $island->id == old('region_id') ? 'selected' : '' }}>
            {{ $island->name }}
          </option>
          @endforeach
        </select>
      </div>
    </div>
  </div>
  <div class="form-group">
    <label>問題文を入力<span class="attention">必須</span></label>
    <textarea cols="40" rows="3" class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}" name="title" value="{{ old('title') }}" placeholder="例)日本最西端の島はどこでしょう?">
    {{ old('title') }}
    </textarea>
    @if($errors->has('title'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('title') }}
    </span>
    @endif
  </div>
  <div class="form-group">
    <label>選択肢を入力<span class="attention">必須</span></label>
    <span>注意</span>
    <p>・同じ内容の選択肢は入力しないでください。<br>
      ・クイズの回答は一番上に入力してください。<br>
      ・カテゴリで選択したことに関するクイズを投稿すること。</p>
    <!-- correct -->
    <input type="text" class="form-control{{ $errors->has('correct') ? ' is-invalid' : '' }}" name="correct" value="{{ old('correct') }}" placeholder="答え)与那国島">
    @if($errors->has('correct'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('correct') }}
    </span>
    @endif
    <!-- uncorrect1 -->
    <input type="text" class="form-control{{ $errors->has('uncorrect1') ? ' is-invalid' : '' }} mt-2" name="uncorrect1" value="{{ old('uncorrect1') }}" placeholder="選択肢1)択捉島">
    @if($errors->has('uncorrect1'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('uncorrect1') }}
    </span>
    @endif
    <!-- uncorrect2 -->
    <input type="text" class="form-control{{ $errors->has('uncorrect2') ? ' is-invalid' : '' }} mt-2" name="uncorrect2" value="{{ old('uncorrect2') }}" placeholder="選択肢2)沖ノ鳥島">
    @if($errors->has('uncorrect2'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('uncorrect2') }}
    </span>
    @endif
  </div>
  <!-- image -->
  <div class="form-group form-image-area">
    <label>画像挿入</label>
    <div class="form-image js-area-drop">
      <i class="far fa-image fa-5x"></i>
      <input type="file" class="form-control-file{{ $errors->has('image_name') ? ' is-invalid' : '' }} input-file" name="image_name">
      <img class="prev-img" src="" style="@if(!(old('image_name'))) {{ 'display:none' }} @endif" alt="投稿画像">
    </div>
    @if($errors->has('image_name'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('image_name') }}
    </span>
    @endif
  </div>
  <!-- explain -->
  <div class="form-group">
    <label>解説を入力<span class="attention">必須</span></label>
    <textarea cols="40" rows="3" class="form-control{{ $errors->has('explain_sentence') ? ' is-invalid' : '' }}" name="explain_sentence" value="{{ old('explain_sentence') }}" placeholder="解説)解説を書きます">
    {{ old('explain_sentence') }}
    </textarea>
    @if($errors->has('explain_sentence'))
    <span class="invalid-feedback" role="alert">
      {{ $errors->first('explain_sentence') }}
    </span>
    @endif
  </div>
  <button type="submit" class="btn btn-default btn-large">投稿</button>
</form>
@endsection

5. マイグレーションを使ってDB作成

Laravelにはマイグレーションといったデータベースをソース上で管理できる機能があります。
Laravel独自のコマンドを打つ事でテーブル作成に必要なテンプレートを自動で作成してくれたり、テーブル構造を書いたソースファイルをマイグレーション実行する事で簡単にDBにテーブルを構築することができます。

また、のちにカラムを追加したり削除したいとなった場合にも変更が容易に出来ます。

Laravelのマイグレーション - Qiita

create_quizzes_table.php
<?php

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

class CreateQuizzesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('quizzes', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->string('title');
            $table->string('correct');
            $table->string('uncorrect1');
            $table->string('uncorrect2');
            $table->string('explain_sentence');
            $table->string('image_name')->nullable()->default(NULL);
            $table->integer('category_id')->unsigned();
            $table->integer('region_id')->unsigned();
            $table->boolean('delete_flg')->default(0);
            $table->timestamp('created_at')->useCurrent();
            $table->timestamp('updated_at')->useCurrent();

            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade')
                ->onUpdate('cascade');
            $table->foreign('category_id')
                ->references('id')
                ->on('categories')
                ->onDelete('cascade')
                ->onUpdate('cascade');
            $table->foreign('region_id')
                ->references('id')
                ->on('region')
                ->onDelete('cascade')
                ->onUpdate('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('quizzes');
    }
}

6. シーダーでテストデータを投入

Laravel使った際にこれ便利!と思った機能の一つの「シーディング機能」
色々な機能を実装していく際にちゃんとデーターが表示できているか、カテゴリ別に表示できているか、などの動きを確認するのにはデータが必要になってきます。
一つ一つデータをDBにインサートしていくのはなかなか面倒。
もし、開発途中でデータが消えてしまった。。(泣)ってことになった時にまた1から入れなおすのも泣きたくなります。(実際開発中に訳わからなくなってDBを消したりしてやり直したりしてました?)

シーディング機能を使うとコマンド一つでデータをインサートすることができます。

QuizTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class QuizTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $allquiz = [
            $quiz1 = [
                'user_id' => '1',
                'title' => '長命草を食べるとどうなるといわれている?',
                'correct' => '長生きできる',
                'uncorrect1' => '与那国馬になれる',
                'uncorrect2' => '空を飛べる',
                'explain_sentence' => '長命草には豊富な栄養素が含まれています。皆さんも摂取して健康長寿!',
                'category_id' => '2',
                'region_id' => '2',
            ],
            $quiz2 = [
                'user_id' => '2',
                'title' => '与那国島に生息している世界最大の蛾の名前は?',
                'correct' => 'ヨナグニサン',
                'uncorrect1' => 'サイトウサン',
                'uncorrect2' => 'オオシロサン',
                'explain_sentence' => '与那国島で初めて発見されたことから「ヨナグニサン」という名前になりました。羽を広げると18cm~24cmにもなります。(でか!)ちなみに与那国の方言では「アヤミハビル」と言います。',
                'category_id' => '3',
                'region_id' => '4',
            ],
            $quiz3 = [
                'user_id' => '3',
                'title' => '与那国島の方言で「ありがとう」はなんという?',
                'correct' => 'ふがらっさ〜',
                'uncorrect1' => 'てんきゅ〜',
                'uncorrect2' => 'かむさ〜',
                'explain_sentence' => '与那国の方言でありがとうは「ふがらっさー」と言います。ネイティブの発音はぜひ現地で聞いてみてね〜♪',
                'image_name' => 'images/uma.jpg',
                'category_id' => '4',
                'region_id' => '6',
            ],
            $quiz4 = [
                'user_id' => '4',
                'title' => '44与那国島の方言で「ありがとう」はなんという?',
                'correct' => 'ふがらっさ〜',
                'uncorrect1' => 'てんきゅ〜',
                'uncorrect2' => 'かむさ〜',
                'explain_sentence' => '与那国の方言でありがとうは「ふがらっさー」と言います。ネイティブの発音はぜひ現地で聞いてみてね〜♪',
                'image_name' => 'images/uma.jpg',
                'category_id' => '4',
                'region_id' => '10',
                'delete_flg' => '1'
            ]
        ];

        foreach ($allquiz as $quiz) {
            DB::table('quizzes')->insert($quiz);
        }
    }
}

Laravelでシーダーを使う - Qiita

7. 各機能の実装

要件定義 → 設計 → WF作成 → 画面コーディング → DB作成
とやっていき、ここからやっとログイン機能やクイズ作成・編集などの実装をしていきます。

CRUD機能について

Laravelの便利な機能としてCRUD機能があります。

CRUD機能とは、
・登録機能(Create)
・参照機能(Read)
・変更機能(Update)
・削除機能(Delete)

のことを指します。

世に出ているシステムやWebサービスにはほぼ確実に備わっている機能であり、必要最低限これらの機能はないといけないよねっていう基本的な機能になります。

Laravelではこの必要最低限のCURD機能をコマンド一つで作ってくれます。(便利)
Laravel5.7: usersのCRUD機能を実装する - Qiita

そもそもフレームワークというのは開発キットみたいにあらかじめソフトを作る上で必要なものを用意してくれていて、開発スピードを上げたり、書き方が統一されるため改修がしやすくなったりというメリットがあります。

クイズCRUD機能

QuizPostController.php
<?php

namespace App\Http\Controllers\User;

use App\Http\Requests\StorePost;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Models\Quiz;
use Image;
use Log;

class QuizPostController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    //投稿クイズ一覧
    public function index()
    {
        $id = Auth::id();
        $quiz_posts = Quiz::latest()
            ->where([
                ['user_id', '=', $id]
            ])
            ->get();
        return view('userpage.quiz_posts', ['quiz_posts' => $quiz_posts]);
    }

    //ユーザー設定
    public function show()
    {
        return view('userpage.setting');
    }

    //クイズ作成フォーム
    public function create()
    {
        // カテゴリと地域をviewに渡す
        $categories = DB::table('categories')
            ->select('id', 'name')
            ->get();
        $region = DB::table('region')
            ->select('id', 'name')
            ->get();

        return view('userpage.create_quiz', compact('categories', 'region'));
    }

    //投稿されたデータをDBへ保存する
    public function store(StorePost $request)
    {
        $quiz = new Quiz();
        $quiz->user_id = $request->user()->id;
        $quiz->category_id = $request->category_id;
        $quiz->region_id = $request->region_id;
        $quiz->title = $request->title;
        $quiz->correct = $request->correct;
        $quiz->uncorrect1 = $request->uncorrect1;
        $quiz->uncorrect2 = $request->uncorrect2;
        $quiz->explain_sentence = $request->explain_sentence;

        //画像ファイル名をランダムの文字列へ&path変更
        $file = $request->file('image_name');
        if ($file != null) {
            $fileName = str_random(20) . '.' . $file->getClientOriginalExtension();
            Image::make($file)->save(public_path('images/' . $fileName));
            $quiz->image_name = '/images/' . $fileName;
        }

        $quiz->save();

        return redirect('/mypage');
    }

    //クイズ編集
    public function edit($quiz_id)
    {
        // カテゴリと地域をviewに渡す
        $categories = DB::table('categories')
            ->select('id', 'name')
            ->get();
        $region = DB::table('region')
            ->select('id', 'name')
            ->get();

        $quiz = Quiz::findOrFail($quiz_id);
        return view(
            'userpage.edit_quiz',
            compact('quiz', 'categories', 'region')

        );
    }
    public function update(StorePost $request, $quiz_id)
    {
        $quiz = Quiz::findOrFail($quiz_id);
        $quiz->category_id = $request->category_id;
        $quiz->region_id = $request->region_id;
        $quiz->title = $request->title;
        $quiz->correct = $request->correct;
        $quiz->uncorrect1 = $request->uncorrect1;
        $quiz->uncorrect2 = $request->uncorrect2;
        $quiz->explain_sentence = $request->explain_sentence;

        //画像ファイル名をランダムの文字列へ&path変更
        $file = $request->file('image_name');
        if ($file != null) {
            $fileName = str_random(20) . '.' . $file->getClientOriginalExtension();
            Image::make($file)->save(public_path('images/' . $fileName));
            $quiz->image_name = '/images/' . $fileName;
        }

        $quiz->save();

        return redirect('/mypage');
    }

    //削除
    public function destroy($quiz_id)
    {
        $quiz = Quiz::findOrFail($quiz_id);
        $quiz->delete();
        return redirect('/mypage');
    }
}

しんどかった実装

一つ目は、LaravelとVue.jsのデータの受け渡しの実装です。

クイズを解いていく部分はVue.jsで実装し、クイズのデータの受け渡しはLaravel側で制御する構成で作ったのですが、カテゴリ別の表示がなかなか上手くいかず。。

実装手法は簡単にまとめるとaxiosを使ってLaravel側にカテゴリ別にデータを取得するように通信してjsonデータをVue側に渡してあげるって感じです。
この部分の実装方法はまた別の記事で書いていこうと思います。?

二つ目は、マイグレーションでのテーブル構築です。
前半の方でマイグレーションを使うと容易に構築できると書いたのですが、初期設定やMySQLのバージョンの問題などで上手くDBに接続できなかったりしました。エラー解決したと思えば別のエラーとエラーループに陥って結構手強かったです。

おわりに

今回初めてフレームワーク(Laravel、Vue.js)を使用してのWebアプリ開発をしてみて感じたことは、フレームワークはあらかじめ便利な機能が用意されているけどそれを上手く活用して開発していくにはある程度実装したい機能の仕組みやWebサービスを作っていく中での流れを一通りふまえてないと宝の持ち腐れになってしまうなって感じました。

エラーでつまずいた時や、機能を実装していく時に役立ったのはグーグル先生もそうですが過去にフルスクラッチで開発した経験だったりもします。

とにかく作る経験を積めば積むほど技術も知識も自ずと身についていくもんだなと。?

これからもスキルアップに精進していきたいです。

以上!

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

[JS][Vue]confirm()とalert()の根本的な違いについて

今日のコード

See the Pen confirmについて(1) by riotam (@riotam4) on CodePen.

今回、横着したいという気持ちからVue.jsで書いてますが、説明したいの部分は生JSの部分です。
confirm()について、alert()と比較して説明したいと思います。

コードの解説

HTMLの3行目
<button @click="culc">計算する</button>

ここの部分で、ボタンをクリックすると「culcメソッド」が発火します。
これはVueの書き方なので、今回は説明を割愛。

JSの4行目
culc() {
  if(confirm('計算しますか?')){
    alert( 1 + 1);
  }
}

それで、発火されるメソッドはこちら。
confirmで確認されてOKなら、1+1の結果をalertで出力している…という、感じです。

confirm()とalert()の違い

今回はconfirmについて調べていきましょう。
書き方はalertとよく似ていて、実行結果もよく似ています。
しかし、この2つは致命的に違う部分があります。

それは、戻り値です。

alert()の戻り値

See the Pen confirmについて(3) by riotam (@riotam4) on CodePen.

簡単な確認用のアプリを用意しました。
「調べる」を押すとダイアログが出現し、「OK」を押すと、consoleにその戻り値を出力します。

alert()はundefinedを返す

結果は、確認できたでしょうか。
undefined(未定義)が返されています。
つまり、本当にダイアログを出力することのみに、機能が限定されています。
余計なものを戻して、戻した先で何かの影響を与えることもなさそうです。

confirm()の戻り値

See the Pen confirmについて(2) by riotam (@riotam4) on CodePen.

こちらも、同じ要領で「調べる」を押すとダイアログが出現し、「OK」を押すと、consoleにその戻り値を出力します。
ただし、中身はalertではなく、confirmに変えています。
ここではぜひ、「キャンセル」の方も押してみてください。

confirm()はブーリアン型(true/false)を返す

「OK」で「true」、「キャンセル」で「false」を返すようになっているのが、確認できるかと思います。今回のコードは、こういったconfirm()の特性を、if文に活かした形で作られています。

alert()と、confirm()…似ているようで戻り値の違う関数なんですね。

ちょっとした応用!confirm()について

See the Pen confirmについて(4) by riotam (@riotam4) on CodePen.

はい、お分かりになられますでしょうか。
true/falseが逆に返されています。
これは、たとえば下のような質問の確認に使えます。


See the Pen
confirmについて(5)
by riotam (@riotam4)
on CodePen.



これなら、「はい」を選択して結果を出力せず、「キャンセル」を選択して結果を出力してくれます。

confirm()の「はい」「キャンセル」はカスタムできない

さっきの例でも、「キャンセル」という選択肢だとなんか分かりにくいですよね。
できれば、「はい/キャンセル」「自分で計算する/計算して」に変えたいところです。
しかし、残念ながら生JSではカスタムできません。

HTML&CSSで、ダイアログみたいなものをつくって、それぞれのボタンに機能を割り当てる…という方法なら、できなくはないですが…面倒。
そんな方は、jQueryのプラグインやライブラリなら、カスタムに対応しているものもいくつかあるようですので、必要であれば調べてみてください。

というわけで、今回はconfirm()とalert()の違いについてでした!
ありがとうございました。

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

Rails+WebpackerでVue.jsとTurbolinksを同時に動かす

これは何

RailsでVue.jsとTurbolinks動かしていい感じのフロントエンド環境を構築しよう!!!

一応それぞれのざっくりとした説明。

  • Rails
    • みんな大好きWebフレームワーク
  • Webpacker
    • Railsで書いたJSやCSSをよしなにまとめてくれるやつ
  • Vue.js
    • 言わずと知れたJSのフロントエンドフレームワーク
  • Turbolinks
    • ページ移動を速くしてくれるやつ

リポジトリとバージョン情報

リポジトリ: https://github.com/rhistoba/rails_vuejs_turbolinks_template

  • Ruby: 2.6.3
  • Rails: 5.2.3
  • Webpacker: 4.0.7
  • Vue.js: 2.6.10
  • Turbolinks: 5.2.0

どういう感じにVue.jsを使えるようにするのか

  1. Railsアクションからビューをレンダリング
  2. turbolinks:load時にビューからVueインスンタンスのid要素を検索
  3. id要素が見つかればVueインスタンスを生成
  4. ビューのid要素以下をテンプレートとしてVueインスタンスが適用される

こんな感じで、ビューでidを指定した箇所にVueを適用するための方法を説明します。
部分的にVueを適用可能なので、いわゆる薄い使い方によりRails Wayから外れない開発が可能かと思います。

手順

今回は適当にrails newしてルートのビューだけ作成したRailsプロジェクトを対象に説明します。

Webpackerを導入

GemfileにWebpackerを追加。

Gemfile
# ...
gem 'webpacker', '~> 4.x'
# ...

追加したらbundle install

$ bundle install

以下のコマンドでプロジェクトにWebpackerをインストールする。

$ bundle exec rails webpacker:install

Vue.jsを導入

以下のコマンドでプロジェクトにVue.jsを追加する。

$ bundle exec rails webpacker:install:vue

Turbolinksなどの導入

yarnで以下のようにパッケージを取得する。

$ yarn add turbolinks vue-turbolinks

javascript/packs/application.jsに以下を追加する。

javascript/packs/application.js
import Turbolinks from 'turbolinks'

Turbolinks.start()

views/layouts/application.html.erbで以下の一文を追加する。

views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    ...
+   <%= javascript_pack_tag 'application' %>
  </head>
  ...
</html>

これでWebpackerによりjavascripts/packs/application.jsがビルドされるようになる。

Vue.jsで完全ビルドを有効にする

ビューから取得したid要素のhtmlを、Vueインスタンスにテンプレートとして渡してコンパイルされる必要があるのですが、Vueは標準でランタイム限定ビルドのみ有効になっており、このままでは意図通りに動かせません。
参考: https://jp.vuejs.org/v2/guide/installation.html#さまざまなビルドについて

そのため参考URLのページにも載っているように、Webpackの設定で完全ビルドを有効にします。

config/webpackvue.config.jsを作成します。

config/webpack/vue.config.js
module.exports = {
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
}

config/webpack以下の環境ごとの設定ファイルに上記の設定を取り込むようにします。
(以下はdevelopment.jsの例)

config/webpack/development.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')
const config = Object.assign(environment.toWebpackConfig(), require('./vue.config'))

module.exports = config

environment.toWebpackConfig()で生成される設定オブジェクトをObject.assign()を用いて上書きしています。

src/main.jsを作成

javascripts/packs/application.jsで各種Vueインスタンスを読み込む大元のファイルを作成しましょう。

javascripts以下にsrcディレクトリを作成します。

mkdir javascripts/src

javascripts/src以下にmain.jsを以下の内容で作成します。

main.js
import Vue from 'vue'
import TurbolinksAdapter from 'vue-turbolinks'

Vue.use(TurbolinksAdapter)

実際に使う

準備ができたので、実際にVueインスタンスを作成して動かします。

適当なビュー(今回はviews/home/index.html.erb)でVueインスタンスで使われるid要素を追加して、その要素以下でVueのテンプレート表記でhtmlを記述します。

views/home/index.html.erb
<div id="vue-app">
  {{message}}
</div>

javascripts/src以下にVueインスタンス作成のファイルを書きます。

javascripts/src/app.js
import Vue from 'vue'

document.addEventListener('turbolinks:load', () => {
  const el = document.getElementById('vue-app')
  if (el) {
    new Vue({
      el: el,
      data() {
        return {
          message: 'Hello Vue!'
        }
      }
    })
  }
})

最後にjavascripts/src/main.jsで上記のファイルをimportするよう追記します。

javascripts/src/main.js
import Vue from 'vue'
import TurbolinksAdapter from 'vue-turbolinks'

Vue.use(TurbolinksAdapter)

+ import './app.js'

rails sしてブラウザで以下のように確認できれば、完了です。

RailsVueTemplate.png

おしまい

RailsでのVueライフをごゆるりと…

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

NuxtでQRコードを読み込んでなにか処理をする

概要

今作成している機能で、WebアプリからQRコードを読み取り商品情報を取得し決済するという実装が実用になった

前提

Nuxt.js 2.4
SSR
vue-qrcode-reader
参考資料

実装方法

インストール

npm install --save vue-qrcode-reader

今回は、SSRなのでそのまま使用すると動かないみたいなので
プラグインを使用してクライアントサイドのみで実行するようにします。

plugin/vue-qrcode-reader.js
import Vue from 'vue'
import VueQrcodeReader from 'vue-qrcode-reader'

Vue.use(VueQrcodeReader)
nuxt.config.js
plugins: [
    ~
    { src: '~/plugins/vue-qrcode-reader', ssr: false }
],

<qrcode-stream>を使用すれば簡単にQR読み取り機能が実装できる
onDecodeで読み取ったデータの処理を行うことができる

<template>
  <div>
    <p class="error">{{ error }}</p>

    <p class="decode-result">
      Last result: <b>{{ result }}</b>
    </p>

    <qrcode-stream @decode="onDecode" @init="onInit" />
  </div>
</template>

<script>
export default {
  layout: 'client/simple',
  data() {
    return {
      result: '',
      error: ''
    }
  },
  methods: {
    onDecode(result) {
      this.result = result
    },

    async onInit(promise) {
      try {
        await promise
      } catch (error) {
        if (error.name === 'NotAllowedError') {
          this.error = 'ERROR: you need to grant camera access permisson'
        } else if (error.name === 'NotFoundError') {
          this.error = 'ERROR: no camera on this device'
        } else if (error.name === 'NotSupportedError') {
          this.error = 'ERROR: secure context required (HTTPS, localhost)'
        } else if (error.name === 'NotReadableError') {
          this.error = 'ERROR: is the camera already in use?'
        } else if (error.name === 'OverconstrainedError') {
          this.error = 'ERROR: installed cameras are not suitable'
        } else if (error.name === 'StreamApiNotSupportedError') {
          this.error = 'ERROR: Stream API is not supported in this browser'
        }
      }
    }
  }
}
</script>

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