vue.js vuex入門 開発で最低限必要そうなこと

はじめに

vue.js使うことになりそうなので少しいじってみたのでメモしておく。

とりあえず今回やってみようと思ったことは以下
・ヘッダー、フッター、メニューなどの共通化 
・vue-routeを使ったルーティング
・ログイン機能
APIへのリクエス
・vuexの導入

セットアップ
プロジェクトを作成してvuexとaxiosをインストール。vueでのajaxしたい場合axiosが奨励されてるぽい?

$ vue init webpack test
$ npm install --save vuex axios

作る画面
・ログイン画面
・商品一覧画面
・商品詳細画面
こんなかんじの管理画面とかでありそうな画面を想定。ログイン画面以外に未ログイン状態でアクセスしたらログイン画面へリダイレクト。商品一覧と詳細画面ではAPIへリクエストして取得したjsonを画面へ表示する。

ディレクトリ構成

vuexのexampleとか他の人のソースみてるとこんな感じなのかな。

$ tree src
├── App.vue             ## この辺はおきまりぽい
├── main.js
├── api                 ## APIリクエスト
│   └── index.js
├── components          ## コンポーネント
│   ├── Element
│   │   ├── Footer.vue
│   │   └── Header.vue
│   ├── Product
│   │   ├── Detail.vue
│   │   └── List.vue
│   ├── Login.vue
│   ├── Member.vue
│   └── Root.vue
├── router              ## ルーティング
│   └── index.js
└── store               ## vuex関連
    ├── index.js
    └── modules
        └── auth.js

また、config以下のdev.env.jsとprod.env.jsに環境ごとに変わる定数などを定義するのがよさそう。今回はAPIのURLをここに書いた。

var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  API_URL: '"http://api.example.com/"'
})

実装

main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store' // ★ 追加

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,                   // ★ 追加
  template: '<App/>',
  components: { App }
})

基本的に今回はvuexを使うのでstoreをVueのオプションに追加。

ルーティング

router/index.js

import Vue from 'vue'
import Router from 'vue-router'

import Auth from '@/store/modules/auth'

import Root from '@/components/Root'
import Login from '@/components/Login'
import Member from '@/components/Member'
import ProductList from '@/components/Product/List'
import ProductDetail from '@/components/Product/Detail'

Vue.use(Router)

var router = new Router({
  routes: [
    {
      path: '/',
      component: Root,
      redirect: '/member/list',

      // 大きく「login」と「member」に分ける。member以下はログインしてないと遷移できないように。
      children: [
        {
          path: 'login',
          name: 'login',
          component: Login
        },
        {
          path: 'member',
          name: 'member',
          component: Member,
          meta: {
            requiresAuth: true
          },
          // 各ページは以下に追加していく
          children: [
            {
              path: 'product/list',
              name: 'product/list',
              component: ProductList
            },
            {
              path: 'product/detail/:id',
              name: 'product/detail',
              component: ProductDetail
            }
          ]
        }
      ]
    }
  ]
})

// ログイン認証はここで行う
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth) && !Auth.state.loggedIn) {
    next({
      path: '/login'
    })
  } else {
    next()
  }
})

export default router
APIリクエス

きっとアプリごとに共通のhttpヘッダーとか送ることが多いと思うのでそういうのはここにまとめるようにする。 

api/index.js

import axios from 'axios'

export default {

  request (method, url, params) {
    var promise = null
    url = process.env.API_URL + url

    if (method === 'get') {
      promise = axios.get(url, { params: params })
    } else if (method === 'post') {
      promise = axios.post(url, params)
    }
    promise.catch(function () {
      return alert('エラーが発生しました')
    })
    return promise
  },

  get (url, params) {
    return this.request('get', url, params)
  },

  post (url, params) {
    return this.request('post', url, params)
  }
}
vuex関連

今回はログイン関連の情報をstoreで管理する。 

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

import auth from './modules/auth'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    auth
  }
})

store/modules/auth.js

import api from '@/api'

export default {
  namespaced: true,
  state: {
    loggedIn: false
  },

  mutations: {
    login (state) {
      state.loggedIn = true
    },
    logout (state) {
      state.loggedIn = false
    }
  },

  actions: {
    login ({ commit }, payload) {
      api.get('/login', {
        mail: payload.mail,
        pass: payload.pass
      }).then(function (response) {
        commit('login')
        // ログイン後、リダイレクト
        payload.router.push('/member')
      })
    }
  }
}
コンポーネント

残りはコンポーネントを実装して画面を作っていく。

コンポーネント概要
Root.vue コンポーネントのルートとなる
Login.vue 未ログインの画面
Member.vue ログイン済の画面のルートとなる。ヘッダやフッターなどはここで表示する
Element/Header.vue(Footer.vue) ヘッダー(フッター)
Product/List.vue 商品一覧画面(Memberの中に埋め込まれる)
Product/Detail.vue 商品一覧画面(Memberの中に埋め込まれる)

components/Root.vue

<template>
  <router-view></router-view>
</template>

components/Member.vue

<template>
  <div>
    <app-header></app-header>
    <router-view></router-view>
    <app-footer></app-footer>
  </div>
</template>

<script>
 import Header from '@/components/Element/Header.vue'
 import Footer from '@/components/Element/Footer.vue'

 export default {
   name: 'root',
   components: {
     'app-header': Header,
     'app-footer': Footer
   }
 }
</script>

components/Login.vue

<template>
  <div>
    <h1>ログインページ</h1>
    id:<input type="text" v-model="id" /><br />
    password:<input type="password" v-model="password" /><br />
    <input type="button" value="LOGIN" @click="onLogin" />
  </div>
</template>

<script>
 import { mapActions } from 'vuex'

 export default {
   data: function () {
     return {
       id: null,
       password: null
     }
   },

   methods: {
     ...mapActions('auth', [
       'login'
     ]),

     onLogin: function () {
       var self = this
       self.login({
         mail: this.$data.id,
         pass: this.$data.password,
         router: self.$router
       })
     }
   }
}
</script>

components/Product/List.vue

<template>
  <div>
    <h1>一覧ページ</h1>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
      </tr>
      <tr v-for="item in items">
        <td>{{ item.id }}</td>
        <td><router-link :to="{ name: 'product/detail', params: {id: item.id} }">{{ item.name }}</router-link></td>
      </tr>
    </table>
  </div>
</template>

<script>
 import api from '@/api'

 export default {
   data: function () {
     return {
       items: []
     }
   },
   beforeCreate: function () {
     var self = this
     api.get('/product')
        .then(function (response) {
          self.items = response.data.items
        })
   }
}
</script>

components/Product/Detail.vue

<template>
  <div>
    <h1>詳細ページ</h1>
    <a v-on:click="$router.go(-1)">戻る</a>
    <table>
      <tr>
        <th>id</th><td>{{ item.id }}</td>
      </tr>
      <tr>
        <th>name</th><td>{{ item.name }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
 import api from '@/api'

 export default {
   data: function () {
     return {
       item: {}
     }
   },
   beforeCreate: function () {
     var self = this
     api.get('/product/detail', {
       id: self.$route.params.id
     })
        .then(function (response) {
          self.item = response.data.item
        })
   }
 }
</script>
おわりに

また、作ったものをずらずら書いただけになってしまった。storeではどんな値を管理するのがよいのだろうか。APIから取得した値を一旦すべてStoreに保存するようなことも考えたけどけっこう手間そうな割に実装も複雑になりそう。。アプリの要件次第だけれども極力コンポーネントで完結させるのがシンプルになる気がした。以上です。

【mysql】date型とnow()を比較する

大した話じゃないけど、ミスったのでメモしておく。

calendar

idtarget
12017-09-20
22017-09-21
32017-09-22
こんなテーブルがあって、今日(9/21)以下の日付のレコードを抽出したかった。

ヒットしない

select * from calendar where target <= now();

ヒット

select * from calendar where target <= date(now());

now()は「2017-09-21 00:00:00」となるからあたり前なんだけど。。以上です

macのターミナルでsshが切れやすい場合の対処メモ

うちの回線が調子悪いのかsshの切断が頻発。昔にteratermで何か対処したのを思い出して検索したらこれでいけるみたい

~/.ssh/config

ServerAliveInterval 3
TCPKeepAlive yes

ServerAliveIntervalはもっと長い方がよいかも。。以上です

railsからsocket.ioへemitする

はじめに

今回やりたかったことは、railsサーバーからsocket.ioにつないでいるブラウザにemitしてメッセージを送るとういこと。ちょっと調べたら「socket.io-ruby-emitter」というgemがあったのでこれつかったらいけた。
で、環境としては以下。
railsサーバー
・socket.ioサーバー
・redisのpub/subを使用

redisのインストール

【socket.io】複数サーバーへの対応メモ - とりあえずphpとか
ここに書いたとおり。

rails

socket.io-emmiterをインストール

Gemfile

・・・
gem 'socket.io-emitter'
class SampleController < ApplicationController
  def index
    @emitter = SocketIO::Emitter.new(
      redis: Redis.new(
        :host => 'redis host',
        :port => 'redis port'
      )
    )
    @emitter.emit("server2client", {value:'message from rails'})
    render :text => 'OK'
  end
end
socket.io

socket.ioインストール

$ npm install socket.io sockt.io-redis

server.js

var io = require("socket.io").listen(3000);
var redis = require("socket.io-redis");

io.adapter(redis({
  host: "redis host",
  port: "redis port"
}));

io.sockets.on("connection", function(socket) {
  socket.on("client2server", function(data) {
    socket.broadcast.emit("server2client", data);
  });
});
html
<input type="text" id="text" />
<input type="button" value="send" onclick="send();"/>
<div id="messages"></div>

<script src="/node_modules/socket.io-client/dist/socket.io.js"></script>
<script>
var web = "socket.io host";
var socket = io.connect("http://" + web + ":3000/");
 function send() {
  var value = document.getElementById("text").value;
  socket.emit("client2server", {
    "value": value
  });
}
socket.on("server2client", function(data) {
  var messages = document.getElementById("messages");
  var text = document.createTextNode(data.value);
  messages.appendChild(text);
});
</script>

はまったところとしては、rails → ブラウザなの、上記の例だとでrailsからはserver2clientをemitしなくてはならないところがわかりずらかった。
「redis-cli monitor」で、各クライアントがどのようにpublishしてきているかを見ながらやるといいかも。以上です。

ionic(cordova)でdatepickerを使う

日付入力のUIを当初は「input type="date"」で実装していたのだが、「消去」ボタンを消したかったのだがこれのやりかたがわからず。。素直にDatepicker使うことにした。

プラグインインストール

$ ionic plugin add cordova-plugin-datepicker

テンプレート

<ion-view ng-controller="SampleCtrl">
  <input type="date" readonly ng-click="openDatepicker()" ng-model="date" />
</ion-view>

コントローラ

・・・
.controller("SampleCtrl", function($scope, $timeout) {
  $scope.date = new Date();

  var OK_TEXT = 'OK';
  var CANCEL_TEXT = 'キャンセル';

  $scope.openDatepicker() = function() {
    window.plugins.datePicker.show({
      date : $scope.date,
      mode : 'date',
      // iOS
      doneButtonLabel: OK_TEXT,
      cancelButtonLabel: CANCEL_TEXT,
      // Android
      okText: OK_TEXT,
      cancelText: CANCEL_TEXT,
      // 日本語
      locale: 'ja_jp'
    }, function(date) {
      // $timeoutしないと反映されなかった・・・ 
      $timeout(function() {
        $scope.date = new Date(date);
      });
    });
  };
};

「input type="date"」にそのまま入れられて使いやすいとも思った。以上です。

SNSでAPNS(iOSプッシュ通知)使う場合のJSONひな形

アプリ側だけawsのコンソールからチャチャッと動作確認しようと思ったらけっこうハマったのでひな形メモしておく。

このエラーがなかなか消えず。。

Invalid parameter: Message Reason: Invalid notification for protocol APNS_SANDBOX: Notification is malformed (Service: AmazonSNS; Status Code: 400; Error Code: InvalidParameter; Request ID: xxxxxxxx-xxxx-xxxx-x\
xxx-xxxxxxxxxxxx)

やりたかったのは「タイトル」「本文」「カスタム項目」の送信。

{
"APNS_SANDBOX":"{\"aps\": {\"alert\": {\"title\":\"タイトル\",\"body\":\"本文\"}}, \"custom_item\": \"カスタム項目\" }"
}

ダブルクォーテション多すぎで意味わからなくなった。。以上です。

【cordova】Androidプラグイン開発でgradleに設定を追加する

こんな感じでいけるみたい。

plugin.xml

<platform name="android">                                                                                                                                                                                          
  ・・・                                                                                                                                                                                                           
  <framework src="src/android/sample.gradle" custom="true" type="gradleReference" />
</platform>

sample.gradle

allprojects {
  repositories {
    maven {
      url "https://maven.google.com"
    }
  }
}

dependencies {
  compile "com.android.support:appcompat-v7:25.4.0"
}

以上です。