20191023のvue.jsに関する記事は11件です。

vue2-google-mapsで右クリックした場所の座標を取得する

はじめに

最近 Vue.js と Firebase を使ってウェブアプリケーションを作っています。
GoogleMap を Vue.js で利用するためのメジャーなライブラリは vue2-google-maps があります。

メジャーと言ってもサンプルが少なく、Map を右クリックしたときの座標取得の方法がわからず困っていたのですが、
色々と調べてその方法が分かったので紹介します。

手順概要

  • GoogleMap API のカスタムイベントを VueGoogleMaps で受け取れるようにする
  • 右クリックのイベントハンドラを登録する
  • $eventでオリジナルのDOMイベントを取得する

GoogleMap API のカスタムイベントを VueGoogleMaps で受け取れるようにする

デフォルトでは vue2-google-maps で取得できるイベントは click のみになります。
right click イベントを取得するためには use 時に autobindAllEvents: false を追加してやる必要があります。

import * as VueGoogleMaps from 'vue2-google-maps';
Vue.use(VueGoogleMaps, {
  installComponents: true,
  load: {
    key: 'APIキー',
    libraries: 'places'
  },
+  autobindAllEvents: false,
});

右クリックのイベントハンドラを登録する

これは単純に GmapMap のエレメントに @rightclick="ハンドラ()" を追加するだけです。

$eventでオリジナルのDOMイベントを取得する

Vue.js で GoogleMap API のオリジナル DOM イベントを取得するためには $event を使います。
place というハンドラを作り、先ほどのハンドラを @rightclick="place($event)" のように書きます。

ソース

<template>
  <card class="card-map">
    <div class="map">
      <div id="map">
          <GmapMap @rightclick="place($event)" :center="center" :zoom="zoom" :options="options" style="width: 100%; height: 100%;">
          </GmapMap>
      </div>
    </div>
  </card>
</template>
<script>

export default {
    data () {
        return {
            center: {lat: 35.71, lng: 139.72},
            zoom: 14,
        }
    },
    methods:{
        place(event){
            if (event) {
                var lat = event.latLng.lat()
                var lng = event.latLng.lng()
                console.log(lat + ", " + lng)
            }
        },
    }
};
</script>
<style></style>

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

Nuxt.jsで異なるコンポーネントから共通で利用できる関数を定義する(inject編)

異なるコンポーネントから共通で利用できる関数を定義したかったので、試してみました。

以前の記事ではmixinを使って実現しましたが、この方法では asyncDatafetch 内で共通の関数を利用できませんでした。
そのため、今回はinjectを使って実現しています。

この方法では、 asyncDatafetch 等のSSR時も関数を利用できるため、こちらの方が良いかもしれません。

共通の関数を定義する

plugins/ 内に utils.js というファイルを作成し、ここに関数を定義していきます。
今回は例として hoge()fuga() という、2つの関数を定義するものとします。

使用する際は this.$huga() のように記述します。

plugins/utils.js
const hoge = () => {
  return "hoge";
}

const fuga = () => {
  return "fuga";
}

export default ({}, inject) => {
  inject('hoge', hoge);
  inject('fuga', fuga);
}

nuxt.config.jsに設定を追加

nuxt.config.js に設定を追加します。

nuxt.config.js
plugins: [
  '@/plugins/utils'
],

コンポーネントからの利用

asyncDatafetch 内で使用したい場合は、 context から呼び出します。
関数名の前に $ が付きます。

pages/index.vue
<script>
export default {
  asyncData(context){
    context.app.$hoge();  // ここで呼び出し
  }
}
</script>

SSR時以外は、 this.$hoge() のように呼び出します。

pages/index.vue
<script>
export default {
  methods: {
    hoge() {
      return this.$hoge();  // ここで呼び出し
    }
  }
}
</script>

便利!

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

Vue.jsについての基礎(Nuxt.js)

はじめに

おはようございます。こんにちは。こんばんは。
ワタタクです。
今回はNuxt.jsについて見けいけたらいいなと書いています。

Nuxt.jsとは?

公式サイトに詳しく書いていますが、Vue.js アプリケーションを構築するためのフレームワークです。
サーバーサイドレンダリングするアプリケーションの開発のために必要な設定があらかじめセットされているのが特徴です。

サーバーサイドレンダリングとは?

Vue.jsなどでDOMの構築をクライアント側で行うと、サイトにアクセスした際に一瞬画面が表示されない状態になります。
サーバー側で初期状態のDOMレンダリングを完了した状態で返すことで、サイトにアクセスした際にすぐに画面を描画することができます。
これをサーバーサイドレンダリング(SSR)と呼びます。

Nuxt.jsを使ってみよう

では、早速使ってみましょう。
今回も「vue-cli」を使います。
「vue-cli」がインストールされていない場合は、以下のコマンドを実行してvue-cliをインストールしてください。

$ npm install -g vue-cli

vue-cliがインストールされたら、以下のコマンドを実行すると、Nuxtのテンプレートプロジェクトが作成されます。
ここでは「sample」というプロジェクト名で作成します。

$ vue init nuxt-community/starter-template sample

sampleディレクトリに、プロジェクトが作成されます。
以下のコマンドを実行して、必要なライブラリをインストールします。

$ cd sample
$ npm install

開発サーバーを起動します。

$ npm run dev

localhost:3000でNuxtのサンプルアプリが起動します。
sample.png

ページの追加

従来Vue.jsでSPA作るときはVue-Routerを使っていました。
その際、componentを作成して、Vue-RouterでURIとcomponentをマッチングするための定義を記述していましたが、Nuxt.jsでは、pagesディレクトリにページ用のcomponentを配置します。
pagesディレクトリに*.vueファイルを配置することで、自動でルーティングの定義を行ってくれます。
例えば、
/pages/users/index.vueを作成するだけで、自動的に/usersのURIとマッチングしてくれて、
http://localhost:3000/users
にアクセスすると、作成した/pages/users/index.vueのページが表示されます。

[TIPS]

ビューにあたる部分は/components /layouts /pages となる
このディレクリ名は変更不可
- components:layoutspagesに使用できるコンポーネントファイル
- layouts:pages内のVueファイルが表示される(デフォルト以外にも作れる)
- pages:ページごとのVueファイル、ここで作ったディレクトリ通りにルーティングが作られる

ページ間を遷移するためには <nuxt-link> コンポーネントの使用を推奨します。
あとはVue.jsの知識があれば、だいたいの事は出来ます。
Nuxt.jsを触る前に一通りVue.jsを勉強しといた方がいいかもしれませんね。
Vue.jsについての基礎(インストール〜基本構文)←勉強の参考までに

まとめ

従来のVue.jsでSPAを作る際は、自由度があるが故に、自分でVue-Routerをもってきたり、Vuexをもってきたり、最初のプロジェクトを作る時にはいろいろ手探りしながらやっていました。
それらがNuxt.jsには最初から含まれているので、フロントエンド開発に慣れていない方でもすごくとっつきやすいのでこっちの方が良いかもですねw。ルーティングが自動で生成されるのも手軽で良いですね。

最後に

最後まで読んでいただきありがとうございました。
もし間違い等、アドバイス、ご指摘等有れば教えていただけたら幸いです。
次回はVuex(store)の使い方をやって行こうかな?

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

Vue.js 編 - 初心者こそ、Docker を使おう! 

簡単なTodo アプリを作りましょう

  • オリジナルはdotinstall さんの vue.js のtodoアプリをアレンジしたものですので詳しくはそちらをみてください。

今回は、Vue.js を使って、Todoに必要な処理の練習をします。

Todo タスクを追加してみましょう。

lang指定でts,stylus を指定していますが、typescript, stylus css 用ですが、実際つかっているのは、javascript, css です。 stylusにすると、混在できますのでstylus を使っています。 typescript の混在できますので、型つけたければつければいいかと思います。

Vue.js は、input周りは、簡単に処理できるのがいいですね。

src/MyApp.vue

<template lang="pug">
  div.container
    Todo
</template>
<script lang="ts">
import Vue from 'vue';
import  Hello  from './Hello.vue';
import Todo from './Todo.vue';
export default Vue.extend({
  components: {
    Todo,
  }
})
</script>
<style lang="stylus">
body {
  font-size: 16px;
  font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.container {
  width: 300px;
  margin: auto;
}
h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
li {
  line-height: 1.5;
}
input[type="text"] {
  padding: 2px;
}

.done
  color gray
  text-decoration line-through


</style>

src/Todo.vue

<template lang="pug">
  div
    ul
      li(v-for="todo in todos")
        | {{ todo }}
    form( @submit.prevent="addItem" )
      input(type="text" v-model="newItem")
      input(type="submit" value="Add")

</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    return({
      newItem: '',
      todos: [
        "task 0",
        "task 1",
        "task 2",
      ]
    })
  },
  methods: {
    addItem() {
      this.todos.push(this.newItem);
      this.newItem = '';
    }
  }
})
</script>
  • コンテナ(uname linux でしたね)
uname
cd src
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug

delete / checkbox

削除もチェックボックスも実装が楽ですね。
id 追加しましたが、使っていません(笑) index指定で削除していますが、まずければ id に変えようと思いましたが、問題なさそうなので、使っていません。

src/Todo.vue

<template lang="pug">
  div
    ul( v-if="todos.length" )
      li(v-for="todo, index in todos")
        input(type="checkbox" v-model="todo.isDone")
        span( :class="{done: todo.isDone }" )  {{ todo.title }} : {{ todo.isDone }}
        button( @click="deleteItem(index)" ) Delete
    ul( v-else )
      | Nothing todo
    form( @submit.prevent="addItem" )
      input(type="text" v-model="newItem")
      input(type="submit" value="Add")

</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    return({
      newItem: '',
      todos: [
        { id: 0, title: 'Task 0', isDone: false },
        { id: 1, title: 'Task 1', isDone: false },
        { id: 2, title: 'Task 2', isDone: true },
      ]
    })
  },
  methods: {
    addItem() {
      const newItem = {
        id: new Date().getTime().toString(36),
        title: this.newItem,
        isDone: false,
      }
      this.todos.push(newItem);
      this.newItem = '';
      console.log(newItem);

    },
    deleteItem(index) {
      this.todos.splice(index, 1)
    }
  }
})
</script>

複数削除

完了していないタスクは、computed で常に集計できるので、それを新しい配列にすれば、結果的に完了タスクを削除したことになりますね。

Todo.vue

<template lang="pug">
  div
    h1
      | todo
      span ( {{ remaining.length }} : {{ todos.length }} )
      button( @click="doneItemsPurge" ) Purge
    ul( v-if="todos.length" )
      li(v-for="todo, index in todos")
        input(type="checkbox" v-model="todo.isDone")
        span( :class="{done: todo.isDone }" )  {{ todo.title }} : {{ todo.isDone }}
        button( @click="deleteItem(index)" ) Delete
    ul( v-else )
      | Nothing todo
    form( @submit.prevent="addItem" )
      input(type="text" v-model="newItem")
      input(type="submit" value="Add")

</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    return({
      newItem: '',
      todos: [
        { id: 0, title: 'Task 0', isDone: false },
        { id: 1, title: 'Task 1', isDone: false },
        { id: 2, title: 'Task 2', isDone: true },
      ]
    })
  },
  methods: {
    doneItemsPurge() {
      this.todos = this.remaining
    },
    addItem() {
      const newItem = {
        id: new Date().getTime().toString(36),
        title: this.newItem,
        isDone: false,
      }
      this.todos.push(newItem);
      this.newItem = '';
      console.log(newItem);
    },
    deleteItem(index) {
      this.todos.splice(index, 1)
    }
  },
  computed: {
    remaining() {
      return this.todos.filter( todo => {
        return !todo.isDone
      })
    }
  }

})
</script>

データの永続化

初心者むけでは大体 localStorage を使う例が多いので、 ここでは、Firebase の DB FireStore を使ってみようかと思います。

FireStore db は、googole のアカウントがあれば、利用できますので、firebase console で検索してみてください。 ここでは、プロジェクトの作成やfireStoreの有効にする方法は説明しませんので、利用出来る前提で始めていきます。
fireStore の 読み書きルール はすべて true の テストモードで行っています。

  • ファイル構成です。
├── Dockerfile
├── docker-compose.yml
├── firebase.js
└── src
    ├── Hello.vue
    ├── MyApp.vue
    ├── Todo.vue
    ├── dist
    ├── index.js
    ├── index.pug
    ├── package.json
    └── yarn.lock

ルードディレクトリに firebase.js を作っていますので、そこにfirebase の configをコピペしてください。

firebase.js

export const config = {
  apiKey: "",
  authDomain: "",
  databaseURL: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
  measurementId: ""
};

MyApp.vue はほとんど変わっていませんが一応記載します。

src/MyApp.vue

<template lang="pug">
  div.container
    //- Hello
    Todo
</template>
<script lang="ts">

import Vue from 'vue';
import  Hello  from './Hello.vue';
import Todo from './Todo.vue';
export default Vue.extend({
  components: {
    Todo,
    Hello,
  }
})
</script>
<style lang="stylus">
body {
  font-size: 16px;
  font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.container {
  width: 700px;
  margin: auto;
}
h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
li {
  line-height: 1.5;
}
input[type="text"] {
  padding: 2px;
}

.done
  color gray
  text-decoration line-through


</style>

Todo.vue は、fireStore対応で大幅に変わっています。
リファクタリングもしていないので、見苦しい感じが残っていますが、実装の苦労の跡が見えていいのではないでしょうか(笑)
なるべく、fireStoreの機能を使って実装しようとしているので、データのやり取りは、fireStoreを毎回介して行っています。
マウントとアンマウントの時にfireStore にデータを渡したほうが動作が軽く楽かもしれませんが
fireStoreの練習には、こちらのほうがいいかと思います.

ラグが気になりますが、追加、削除、完了、未完了、一括削除ができているのOK でしょう。(笑)

async / await に慣れていない人へ

DB を使うと処理が非同期になるため、async / await を使っていますが、asyce/await に慣れていない人に一言いうと、処理に時間がかかりそうだと思うものの前に,すべてawait を 付けていけばいいです。
今回で言えば、データの読み書きで firestore に対してのと命令を書いた場合にあたります。
付けていないと、ほしいデータが入る前に次の処理が走ってしまって、空処理になるのを防いでいます。
ですから、時間がかかると思ったら、await を処理の前につけてください。
await を使うとき、親要素に async という呪文が必要なので、これを関数式に付けます。

awaitをつけたけれど、親要素がない場合は、そのあたりの処理一体を、関数にまとめてしまえばいいです。
そうすれば、非同期処理はできているはずです。

src/Todo.vue

<template lang="pug">
  div
    h1
      | todo
      span ( {{ remaining.length }} : {{ todos.length }} )
      button( @click="doneItemsPurge" ) Purge

    form( @submit.prevent="addItem" )
      input(type="text" v-model="newItem")
      input(type="submit" value="Add")

    ul( v-if="todos.length" )
      li(v-for="todo, index in todos")
        //- input(type="checkbox" v-model="todo.isDone")
        input(type="checkbox" @click="checkboxChange(index)" :checked="todo.isDone")
        span( :class="{done: todo.isDone }" )  {{ todo.title }} : {{ todo.isDone }} : {{ todo.date }}
        button( @click="deleteItem(index)" ) Delete
    ul( v-else )
      | Nothing todo

</template>
<script lang="ts">

import * as firebase from 'firebase/app';
import 'firebase/firestore';
import { config } from '../firebase';

firebase.initializeApp(config);
const db = firebase.firestore()
const collection = db.collection('todos')

import Vue from 'vue'
export default Vue.extend({
  data() {
    return({
      newItem: '',
      todos: [],
      itemsId: [],
      storeItem: {},
      state: [],
    })
  },
  watch: {
   state: { // 更新チェック されたら、実行
     handler: async function(){
        const queryItems = await collection.orderBy("created_at", "desc").get()
        const qureyItemsData = queryItems.docs.map( item => item.data())
        const qureyItemsId = queryItems.docs.map( item => item.id )

        this.todos = qureyItemsData;
        this.itemsId = qureyItemsId;
    }, deep: true
   },
   todos: {
     handler: async function(){
    }, deep: true
   }
  },
  methods: {
    async checkboxChange(index) {
      let isDoneState = Boolean;
      const item = await collection.doc(this.itemsId[index])
      const isDone = await item.get()
        .then(function(doc) {
          isDoneState = doc.data().isDone
        })
        .catch( err => {
          console.log(err);
        })
      const changeItem = await item.update({
        isDone: !isDoneState,
      })
      this.state = ["checkboxUpdate"]

    },
    async deleteItem(index) {
      const item = await collection.doc(this.itemsId[index]).delete()
      this.state = ["delete"]
    },
    addItem() {
      this.storeItem = {
        id: new Date().getTime(),
        date: new Date().toLocaleString("ja"),
        title: this.newItem,
        isDone: false,
        created_at: firebase.firestore.FieldValue.serverTimestamp(),
      }
      const item = collection.add(this.storeItem);
      this.newItem = '';
      this.state = ["addItem"] // 状態更新通知用
    },
    async doneItemsPurge() {
      // this.todos = this.remaining

      const itemsQuery = await collection.where("isDone", "==" , true)
        .get()
        .then(function(querySnapshot) {
          querySnapshot.forEach(function(doc) {
            const item = collection.doc(doc.id).delete();
            console.log(item);
          });
        })
        .catch(function(error) {
          console.log("Error getting documents: ", error);
        })

      this.state = ["purge"] // 状態更新通知用
    },
  },
  computed: {
    remaining() {
      return this.todos.filter( todo => {
        return !todo.isDone
      })
    }
  },
  async mounted() {
    const query =  await collection.get()
    const items = query.docs.map( item => item.data())
    const itemsId = query.docs.map( item => item.id)
    this.todos = items;
    this.itemsId = itemsId;
    this.state =  items;
  }

})
</script>

これで、Vue.js での練習は、終了です。
お疲れ様でした。

次回は, この環境で、React を使ってみたいと思います

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

Vue.js & Nuxt.js - オレオレかんばん

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

Vue paste時にイベントを発火させる

コード

</template>
  <input v-model="name" @paste="onPaste"></input>
</template>
<script>
export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    onPaste(event) {
      setTimeout( () => {
        console.log('お名前?', this.name);
      }, 10)
    }
  }
}
</script>

注意点

onpaste - ペースト時に発火する

注意しなければいけないのは、イベントが発火して処理が実行される時点では、まだフォームには貼り付けた値が入力されていないということです。流れとしては、貼り付けを実行、イベントが発火して処理を実行、フォームに貼り付けた値が入力される、という順番になります。なのでonpasteイベントで貼り付けた値を取得したい場合は、例えばsetTimeout()で処理の実行を遅らせましょう。

イベントが発火して、処理が実行される時点では、まだフォームに値が入っていない。(Vueだと、dataオブジェクトに反映されていない。)

参考資料

How to handle Paste(Ctrl+v or with mouse) event in vue.js?
onpaste - ペースト時に発火する

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

vue.js 全ページ遷移時にページTOPへスクロールさせる

全ページ遷移時にページTOPへスクロールする

ページ遷移時、遷移前の表示位置のまま、新しいページに遷移してしまう。

ページ遷移時にはページTOPへスクロールさせたい。

やり方

scrollBehaivor () {
  return { x: 0, y: 0 }
}

を使う。

router.js
export default new VueRouter({
  routes: [],
  scrollBehaivor () {
    return { x: 0, y: 0 }
  }
}

いちいちscrollTop(0, 0)の関数を作って呼び出す必要はなかった。

リファレンス
https://router.vuejs.org/ja/guide/advanced/scroll-behavior.html

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

サーバレスで動く研究室のHPを1年間運用してみて

指導教員の無茶振りで作り始めたサーバレス+SPA+GraphQL+ChatOpsで研究室のHPが完成してからだいたい1年が経過しました。(参考: 研究室のHPをサーバレス、SPA、GraphQL、ChatOpsで作った)研究室という、人が数年でほとんど入れ替わる特殊な組織内でどのようにHPを継続的に運用していったのか振り返ります。

HPの概要

Slack Botで更新する研究室のHPです。YAMLを添付して@labbot posts createのようにメンションを飛ばすと記事が作成されます。更新、削除もできます。

https://moriokalab.com

image.png

このHPは、(1) 研究室のみんなが更新できる, (2) 運用、ID/PASS周りの管理コストを最小限にする, の2点を要件として開発したものです。

大きく分けてフロントエンド、サーバーサイド、Slack Botの3つの部分で構成されており、フロントエンドはNuxt(Vue.js)、Buefy、AtomicDesign、ApolloClientなど、サーバサイドはNodejs、GraphQL、DynamoDBなど、Slack BotはNodejsやjestなどから作られています。そしてこれら全てのインフラをserverless framework(Lambda)で動かしています。以下の図は現在のシステム構成概要です。

image.png

_人人人人人人人人人人人人人_
> 全部sererless framework <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

serverless frameworkの開発者達には足を向けて寝られない。

数字で見るこの1年間

(1) アクセス数

GAでトラックしていたのでそのキャプチャです。去年書いた記事がバズった時以外はだいたい週20アクセス前後です。いや20人も誰が見とんねんって感じですが。
image (15).png

(2) 運用費

運用費はRoute53とS3が毎月\$0.5ずつかかって年間\$12です。ドメイン代の\$12を合わせても年間\$24で運用できています。とても財布に優しいですね。

たぶん月100万アクセスくらいまではこんな感じの値段になると思います。

(3) 記事数

週一で記事を追加するので4 x 12 = 48になるはずですが、長期休暇等があって書いてない人もいるので37件でした。思ったより多くて満足しています。

HPの成果

一番大きな目的は、学科内の研究室配属の時に「うちの研究室はこんなアクティビティがあるよ」というのを紹介するというものでした。これは去年の研究室選択の時期に指導教員がみんなに見せていたので役割は果たせたと思います。

あと、うちの研究室に院試受けて入ろうという人の参考にもなったみたいです。

この1年の開発

以下はこの1年で行った開発に関する取り組みです。

  • (1) Vue.jsのSPAからNuxtでのSSRへリプレイス
  • (2) 毎週担当者をアサインするBotの稼働
  • (3) 投稿前にインタラクティブに内容を確認できる
  • (4) Algoliaでの記事検索
  • (5) 指導教員に内緒でシークレットモードを追加
  • (6) serverless slack botのバグ修正

そういえば、今でもちょくちょく指導教員から「ここ機能追加して欲しいです」みたいな要望があって興が乗った時にガリガリ作っているのですが、しばらく触っていなくてもすんなり触れる感じのコードになっていて(自分が書いたので当たり前といえば当たり前だけど)大変喜ばしい限りです。

(1) Nuxtへのリプレイス

上の記事の通り、最初はVue.jsによるSPAとして動かしていたのですが、以下の辛みを感じたので勢いに任せてNuxtへ置き換えました。

  • 頭を使わずにSPAしてたからレスポンスが返ってくるまでの0.5秒くらい何も表示されない
  • SPAだから記事ごとにOG設定できない
  • 勢いだけで書いたからかなり雑でテストが書きにくい(リプレイスの後押しとなった要因)

書いて動かしてる感触なんですが、NuxtはプレーンなVue.jsで書くよりも適度な規約があってとても書きやすかったです。

なお、このNuxtアプリケーションについてもLambdaの上で動かしています。NuxtをLambda上で動かす仕組みはNuxt.js on AWS Lambda with Serverless Frameworkとほとんど同じ物を採用しました。

(2) 週次アサインBotの作成

研究室の仲間たち(合計20人くらい)で継続的に更新を続けていくために、週に一回担当者を決めて更新してもらうようにしました。この週一の担当者決めはSlack Botが担当します。

これもサーバレスで動いています。去年のre:InventでLambdaのRubyサポートが始まったので試してみたかったのでRubyで書きました。

(3) 投稿前の確認

ある日研究室で隣の席の人が「投稿時に確認画面が欲しい」と言ったのでガガっと実装したのがこの機能です。

こういう風に、Slack BotにSnipetとして送信するとプレビュー画像と共に「これ本当に投稿する?キャンセルする?」の2択を迫ってくるようにしました。
image.png

投稿プレビューの仕組みは擬似的なもので、実はプレビュー段階で本番環境にデータが作成されます。そしてPublishボタンを押すと作成済みデータのpublished_atに現在時刻が代入されてニュース一覧で見られるようになり、Cancelボタンを押すとこのデータが削除されます。

つまりプレビュー時はURL直打ちだと見える状態になっており、プレビューのスクリーンショットはここに直接アクセスして取得したものを返すようにしています。

ここで必要になったのが、任意のWebページのスクリーンショットを撮影してその画像を取得するという仕組みです。

スクリーンショット撮影部分

欲しかったのは「あるURLを渡すとそのサイトのスクリーンショットが返ってくる」機能だったので、作りました(asmsuechan/sshot)。

これもサーバレスで動かしています。ヘッドレスchromeを無理やりLambdaに押し込んでAPI Gateway経由で呼ぶような形式です。

以下のURLは公開されているもので、誰でもアクセスすることができます。あ、このままだとスクショは研究室のAWSアカウントのS3に保存されるので、保存されたくない方は自分で立てるかbase64オプション使うかで。

$ curl "https://vckvs9l162.execute-api.ap-northeast-1.amazonaws.com/production/screenshot?url=https://github.com/asmsuechan"
{"screenshot":{"url":"https://s3-ap-northeast-1.amazonaws.com/sshot-images/kvd6ajor2hbprpb9.png"}}

urlをパラメータに入れたcurlを投げるとそのサイトのスクリーンショットがアップロードされたS3のURLが返ってきます(このバケットは研究室のAWSアカウント上のものです)。また、base64パラメータをtrueに設定する事によってbase64にエンコードした画像を返すようにする事もできます。

また、設定ファイルをよしなにいじる事で自分のAWSアカウント上にデプロイすることもできます。

開発で一番大変だったのが日本語フォントへの対応で、かなり無理くりやって何とか対応しました。が、上の画像を見る通りフォントがダサいのでここもうちょっとどうにかならんかったんか・・・って感想です。

(4) Algoliaでの記事検索

Algolia使ってみたくて、なんとなく興が乗った時に記事検索機能https://moriokalab.com/news に追加しました。誰が使うんだろう、って感じですが。。。

NuxtへのAlgoliaの導入は公式のexampleと公式ドキュメント以外特に参照せず実装できました。すごく簡単。Algoliaめちゃくちゃ速くてびっくりです。Algoliaすごい。

(5) 指導教員に内緒でシークレットモードを追加

トップページにある指導教員の顔を30回クリックするとシークレットモードが発動し、イカしたデザインになります。なお画面から元に戻す方法はありません。戻したい方はLocalStorageでそれっぽいフラグをdeleteしてください。

(6) serverless slack botのバグ修正

Slack Botはjohnagan/serverless-slackbotを元に作ったのですが、画像を2回以上連続でSlackにアップロードするとBotが落ちてしまうという問題がありました。最初は放置していたのですがあまりにも不便だったので原因を探したところserverless-slackbot側のバグだったので直してPRを送りました。
https://github.com/johnagan/serverless-slackbot/pull/3

サーバレスで良かったこと・悪かったこと

良かったことは

  • serverless frameworkを使うとコマンド1つで簡単にデプロイできる
  • インフラのメンテコストがかからない
  • 料金が安い

悪かったことは

  • ローカル環境構築が大変
  • DB選択が悩ましい
  • 初回アクセスが遅い

1年経ちましたが、あんまりずっと面倒を見続けるようなシステムではないのでサーバレスを選択して良かったな、と思っています。

運用を振り返る

また、更新操作をSlackで完結させることで、CMSと比べて誰でも手軽に更新できる仕組みとなったのが良かったです。CMSだとどうしてもID/Passの問題があって、ここの管理が面倒でだんだん更新されなくなる未来が見えていました。

また、サーバレスな構成にしてサーバーサイドのインフラの面倒を見なくても動き続けるようにしたので「管理/運用したくない」という目標は達成したと思います。

更新を続ける仕組み

研究室の愉快な仲間たちみんなで更新していくための仕組みとして、毎週月曜日の18時にその週の更新担当者をアサインするCronを動かすようにしました。

image.png

これ最初は「みんな無視するんじゃね」って思ってたんですが案外そんなことはなく、当てられた人は結構マジメに更新してくれています。

打ち捨てられないための仕組み

このようなHPというのは作ったっきりで打ち捨てられてしまうことがままあります。これはだいたいの場合「めんどくさい」という気持ちと「優先度が低い」という状況のどちらか、またはどちらもが原因になります。この辺りで意識したことについて運用面と開発面の両方で書きます。

運用面

Botに指名されているのにも関わらずHPの更新をしていない人には指導教員が面談中に「書いてくれ」と言っていたことで、(その人の中での)優先度をあげていました。とにかくあたった人に、書くまでpingを送るという戦法ですね。

なるべく記事のハードルを下げるというのも気をつけたことです。目的は「アクティビティがあることを示すこと」なので内容のクオリティはそもそも問うていません。クオリティは必要になってから考えます。

あ、あと、こういうのはやる気がありすぎても飽きて打ち捨てられてしまう事があるのでほどほどのやる気で取り組んだというのも良かったと思います。指導教員に「これ欲しいから作って」と言われた時に「興が乗ったら作ります」と言って数カ月後ガリガリ作り出す、みたいな感じのゆるさ。

開発面

開発上では研究室のGitHubオーガナイゼーションを作りそこに全てのコードを置いてどこからでもコマンド1つでデプロイできるような仕組みを最初から作り、デプロイがめんどくさくてコードいじる気がなくなるという状況を緩和しました。

そしてコードも最初から予めある程度の決まった枠組みを作り、何か機能を追加する時には既存のものを見ながら思考停止でも基本はコピペで動くものができるようにすることで「コードの書き始めのだるさ、めんどくささ」を軽減しました。既存システムへの機能追加って、どうしても「壊すかもしれない」という気持ちがあって最初は億劫なものなので。最初さえ頭を使わずサクッとできればあとは流れに乗って書けるはずです。

現状の問題点

去年も同じこと言ってたじゃねえか!って話もありますが、今の問題点です。

(1) ローカル環境が適当

インフラのローカル環境が全然構築できていなくて、毎回デプロイしてテスト用Slackチャンネルで直接いじるという形にしていて非常にめんどくさいです。いい感じにDocker環境立ててやるとうまくできるのでしょうが、そこまでやる体力がなくてそのままにしています・・・

(2) 後任の不在

研究室は人間の流動性が高く、1人の学生がずっと面倒を見続けるわけにはいきません。このHPももちろんそうなのですが、指導教員と話していてこれから先Web人材が研究室に入ってきて自分の残したコードを読み保守運用してくれる人が現れる確率は低そうという結論になったので、卒業後はたまに報酬(うまい焼肉)を貰いながら私が運用していくという感じの話をしています。

(3) LambdaでSSRすると初回アクセス遅い

Lambdaがcold standbyしている時にアクセスすると表示に5秒くらいかかってしまうという残念な問題があります・・・解決策は10分に1回だかヘルスチェック回して常にHot standbyな状態にしてやることです。そんなに難しくはないと思うのですが気持ちが乗らないので手をつけていません。

(4) UIの改善は特になし

やはり仕事のコードではないのでUI上の細かい部分はおざなりになりがちです。どこかであまり良くない部分を改善したいとは思っているのですが後回しにしています。もっとカッコよくしていきたい。

まとめ

この手のHPの目的はだいたい「自分たちの組織がマトモに活動していることを対外的にアピールして印象を悪くしないこと」であることが多いので、作って公開することより公開して更新し続けることがよっぽど重要だと思います。ですのでここで書いた継続する仕組み打ち捨てられないための仕組みというのが、私と似たような状況に置かれた人の助けになれば幸いです。

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

vue-component内のinput-textタグのvalueにBlade上の変数を入れる

親コンポーネントから子コンポーネントへ単方向データバインディングさせる際、
v-modelディレクティブ名は

  • 親側:ケバブケース (例:foo-bar)
  • 子側:キャメルケース (例:fooBar)

で記述する必要がある(みたい)。

以下、サンプルコード。

/resources/js/components/SamplComponent.vue
<template>
    <div>
        <div class="input-group">
            <input type="text" class="form-control" name="text1" v-model="hoge">
        </div>
        ...(省略)
    </div>
</template>

<script>
export default {
    props: {
        inputVal: {
            type: String,
        }
    },
    data() {
        return {
            hoge: this.inputVal,    // プロパティのイミュータブル制約回避
            ...
        }
    },
    ...
}
</script>
/resources/js/app.js
require('./bootstrap');

window.Vue = require('vue');

Vue.component('sample-component', require('./components/SamplComponent.vue').default);

const app = new Vue({
    el: '#app',
});

$ npm run dev等でコンパイルし、

/resources/views/sample.blade.php
<body>
    <div id="app">
        ...
        <sample-component input-val="{{ old('hogehoge') }}"></sample-component>
     <!-- ↑例:oldヘルパーを使って入力値を保持 -->
        ...
    </div>
</body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerを使って構築した、vue.jsのアプリがデプロイした際に動かなくなった話

dockerのマルチステージビルドを使って構築した、vue.jsのアプリがデプロイした際にローカル環境では発生しないエラーが実際のサーバー上でだけ発生する事象が発生しました。
通常通りのやり方だと気づけなかったポイントなので、共有しようと思います。

マルチステージビルドとは?

マルチステージビルドはdockerのimageを作成する際、docker build時にだけ立ち上がるコンテナでそこでできた生成物を実際のイメージに配置するみたいなことができます。
どこかの環境で(jenkins等)node.jsをbuildしてその生成物を配置するみたいな必要がなくなりdockerだけで完結できるようになるのでVue.js公式も推奨しているデプロイ方法になります。

ハマったポイント

ローカルでnpm install したときに生成される node_modules と サーバーのコンテナ上で生成される node_modules に差分があり、ローカル環境でうまく実行できていたものがサーバーのコンテナ上ではうまく実行されずエラーになってしまった。

原因

ローカル環境で Vue.jsの開発をするときは、npm run dev 等で ローカルのサーバーを立てて開発することが多いと思います。
そのため、 node_modulesdist ファイルは`同じプロジェクト内にローカル環境だけは共存している形になると思います。
ローカルで以下のようなdockerfileをもとにvue.js のプロジェクトをbuildしたのですが、

Dockerfile
COPY ./vue ./
RUN npm install
RUN npm run build

このときの COPY の挙動ではローカルに含まれるnode_modulesdist ファイルも一緒にコンテナ内に配置されてしまいます。
そのため、RUN npm install が実行されても、新規でnode_modulesが生成されることがなく baseimageのversionとローカルのversionの差分等があるとうまく生成されないというような事象が発生してしまっていました。

対処方法

.dockerignoreと呼ばれるdockerから無視させるファイルリストを追加する方法があるので、このファイルに以下のように無視ファイルの対象として追加する。

./vue/dist/
./vue/node_modules/

※ディレクトリ階層は使っているものに合わせてください。

追加することで、docker build 時は毎回新規で npm install が走るようになるので、サーバーだろうがローカル環境だろうが同じ状態にすることができ、問題を解決することができました。

さいごに

今回のミスは、たまたまアプリを起動するときに出てくるエラーで動作の一部ができなくなるという感じのエラーではなく気づくことができましたが、ほんとに一部でしか使っていない動作でのエラーとかだとリリースしてからエラーが発生する等の問題になりかねないので注意が必要ですね。

あと、今回差分が発生してしまった一番の大きな原因は dockerの baseimageのversionが node:6.4 とかだったのに対して、ローカル環境が 10くらいになっていたことだったので、プロジェクトの生成からdockerのコンテナ内でやったほうが幸せになれると思いました。 Rubyとかは記事とかそこそこあるんですけどね…

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

Nuxt + Vuex + Vuetifyで簡易的なフラッシュメッセージを実装する

BFFでアプリを作る際、railsのフラッシュメッセージのような機能がフロントでも欲しかったので、簡易的に実装しました。

こんなものができます。
flush.mov.gif

環境

Nuxt: 2.8.0
Vuex: 3.1.1
Vuetify: 1.5.14

storeの用意

まずはフラッシュメッセージを格納するためのstoreをVuexに実装していきましょう。
私のディレクトリ構成はこのようになっています。
スクリーンショット 2019-10-22 22.13.39.png
flush_messageフォルダ配下にactions getters mutations stateを置いています。
が、今回はaction.jsは使用しません。
各ファイルの処理を記載していきます。

state.js
export default () => ({
  text: '', //フラッシュメッセージの内容が入る
})
mutations.js
export default {
  setMessage: (state, payload) => {
    state.text = payload.text; // stateの状態を変更する
  }
}
actions.js
export default {
  async showFlashMessage({commit}, message) {
    commit('setMessage', message); //mutationに値を渡す
  }
}

Vuex側はこれで準備完了。

アラートのコンポーネント作成

次にこちらを呼び出して変更するためのview側を実装していきます。
まずはアラートのコンポーネントを作成します。

notification.vue
<template>
  <v-alert
    v-bind="$attrs"
    :class="[`elevation-${elevation}`]"
    :value="value"
    class="v-alert--notification"
>
    <slot />
  </v-alert>
</template>

<script>
export default {
  inheritAttrs: false,

  props: {
    elevation: {
      type: [Number, String],
      default: 6
    },
    value: {
      type: Boolean,
      default: true
    }
  }
}
</script>

<style>
.v-alert--notification {
  border-radius: 4px !important;
  border-top: none !important;
}
</style>

実際にこのコンポーネントを使用していきます。
import先のパスに注意。

show-notification.vue
<template>
  <notification
    class="mb-3"
    color="info"
    >
    {{ $store.state.flush_message.text }}
  </notification>  
</template>

<script>
  import notification from '~/components/material/notification'

  export default {
    components: {
      notification
    }
  }
</script>

<style>

</style>

ひとまずこちらで空のメッセージボックスが出てきました。
スクリーンショット 2019-10-22 23.00.26.png
Vuex側のtextの値を変更する処理をして、この中にメッセージを追加していきましょう。

メッセージを更新させる

ここでは、

①別ページでボタンを用意
②ボタンを押すとstore内のtextの値を更新
③その後、上記のshow-notificationのページにリダイレクトされる
④リダイレクト先で更新したメッセージが表示される
といった風に実装します。

こんな感じです。

button.vue
<template>
  <v-btn
    class="mx-0 font-weight-light"
    color="success"
    v-on:click="submit"
  >
    送信
  </v-btn>
</template>

<script>
  import { mapActions } from 'vuex'

  export default {
    methods: {
      ...mapActions({
        showFlashMessage: 'flush_message/showFlashMessage',
        submit: function() {
        this.showFlashMessage({ text: "投稿完了" }); // ①フラッシュメッセージをセット
        this.$router.push({ path: "/show-notification" }); // ②show-notificationにリダイレクト
      },
      })
    }
  }
</script>

①でactions.jsで定義したメソッドを呼び出し、storeの値を更新させます。
その後、②でメッセージボックスを配置したページにリダイレクトさせます。
スクリーンショット 2019-10-23 0.21.31.png
値がちゃんと渡されました!

フラッシュさせる

さてメッセージが表示されたは良いものの、このままでは
・値が消えない限りそのまま
・値が消えても枠だけ残る
となってしまいます。
フラッシュメッセージらしく必要に応じて出たり消えたりするようにしてあげましょう。

show-notification.vue
<template>
  <notification
    class="mb-3"
    color="info"
    v-if="$store.state.flush_message.text" //追加
    >
    {{ $store.state.flush_message.text }}
  </notification>  
</template>

<!-- 以下省略 -->

フラッシュメッセージを呼び出している部分に、v-if属性を足してあげましょう。
これでtextに値がセットされているときだけ表示されるようになりました。

storeの値を空にする

このままでは常に表示されたままになってしまいます。
最後にセットした値を空にする処理を追加しましょう。
nuxtのミドルウェアという機能を使って実装します。

middlewareフォルダを作成し、その直下にclearNotification.jsというファイルを作成します。

middleware/clearNotification.js
export default function ({ store, redirect }) {
  store.dispatch('flush_message/showFlashMessage', {text: null});
}

こちらでtextの値をnullにしてあげます。
ミドルウェアはページを読み込む前に指定した処理を挟める機能です。
こちらを使って、特定のページにアクセスした際にtextの値がnullになる、といった風に実装します。
実環境で使用するイメージとしては、
スクリーンショット 2019-10-23 0.51.13.png
こんな感じです。
上記で作ったものだと、それぞれ
・show→show-notification.vue
・edit,create→button.vue
が担当します。

本当は非同期処理とか使って○秒後に非表示、といった処理ができればよかったのですが、JS初心者にはハードル高くて諦めました...
この内容だと、showのURLをID直接叩いた時に意図しないメッセージが出たりすると思うので、自身の用途に合わせて使ってください。

では上記のindexに値する、textをnullにする処理を行うページを実装します。

index.vue
<template>
  <p>text= nullにするだけのページ</p>
</template>

<script>  
  export default {
    middleware: 'clearNotification',
  }
</script>

このように、export default内で指定するだけで勝手に処理してくれます。
メッセージが出ているのを確認した後こちらのページに飛び、また元のページに戻ると何も表示されないのが確認できると思います。

ボタンを押すところの処理など、axiosに組み込んで使ってみてください!
JS初心者のためかなり力技での実装です。
変な所があったらご指摘お願いします!!!

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