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

Vue.jsの基礎を学ぶ(2日目)

bind()メソッドについて

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

thisの意味するものがその時々で異なるので、bind()メソッドを使って指定してあげるというイメージで合ってるかな?

*以下参考サイト

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

ビットコインの価格表示プログラム

<div id="app">
  <h2>
    Bitcoin Price
  </h2>
  <section v-if="hasError">
    Error,
  </section>
  <section v-else>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else> 
      <ul v-cloak>
        <li v-for="(rate, currency) in bpi">
          {{ currency }} : {{ rate.rate_float | currencyDecimal }}
        </li>
      </ul>
    </div>   
  </section>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

注意すべきは、v-ifディレクティブとv-elseディレクティブ。
rubyのif elseと同じように、v-ifがfalseの時にv-else以下のコードが表示される。

var app = new Vue({
  //options
  el: '#app',
  data: {
    bpi: null,
    hasError: false,
    loading: true
  },
  //マウントされた後に呼ばれる
  mounted: function() {
    axios.get('https://api.coindesk.com/v1/bpi/currentprice.json')
    //responseにAPIからの戻り値が入る
    .then(function(response){
      //console.log(response.data.bpi)
      //console.log(response.data.bpi.USD.rate_float)
      this.bpi = response.data.bpi
    }.bind(this))
    //コンソールにエラーを出力
    .catch(function(error){
      console.log(error)
      this.hasError = true
    }.bind(this))
    //finallyは処理の最後に行うことができる
    //これでLoading...マークが消えて価格が表示される
    .finally(function(){
      this.loading = false
    }.bind(this))
  },
  filters: {
    currencyDecimal(value) {
      //toFixedメソッドは小数点の桁数を引数に指定することができる。
      return value.toFixed(2)
    }
  }
})

様々なテンプレート制御ディレクティブ

v-once

初回だけテキストバインディングを行いたい時に使う。

つまり、初回だけテンプレートを評価する

<div id="app">
  <p v-once>
    {{ message }}
  </p>
  <button v-on:click="clickHandler">
    Click!
  </button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    clickHandler: function(event) {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

上記ではボタンがクリックされるとmessageプロパティの文字を反転させるプログラムを組んでいるが、テンプレート側にv-onディレクティブが記載されているため、初回の
Hello Vue.js!は表示されるが、ボタンを押して文字が反転することはない。

これはv-onceが初回だけバインディングする機能によるもの。

v-pre

要素と全ての子要素のコンパイルをスキップしたい時に使う。

生のマスタッシュタグを表示したい
XSS対策
コンパイルのスピードを上げたい

そんな時に使うらしい。

<div id="app">
  <p v-pre>
    {{ message }}
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})

本来は{{ message }}によってブラウザにHello Vue.js!と表示される。

しかし、pタグにv-preディレクティブを追加すると、コンパイルが行われずに

{{ message }} とブラウザに表示されるようになる。

v-html

プレーンなhtmlを挿入したい時に使う

本来はXSS対策のため、ユーザーが

アイウエオ

と文字列を入力しても
<p>アイウエオ</p>

と表示される。

ただ、v-html="プロパティ名"を追加すると、それがhtmlタグとして評価される

<div id="app">
  <p>
    {{ message }}
  </p>
  <p v-html="message">
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello <span style="color:red">Vue.js!</span>'
  }
})

htmlファイルにpタグが2つあるが、

一つ目は

Hello <span style="color:red">Vue.js!</span>

と表示され、

v-htmlがついている2つ目は

Hello Vue.js!

と表示される。

v-cloak

cloakは日本語で覆い隠すという意味。

ページを表示開始してからインスタンスの作成が終わるまでの間に、
マスタッシュタグやコンパイル前のテンプレートが表示されてしまうのを防ぎたい。

v-cloakディレクティブを使ったチラつき防止のやり方

1.インスタンスのコンパイルが終了するまで、非表示にしたい要素に、v-cloakディレクティブを追加。

2.cssでv-cloakがついている要素を非表示にする

3.v-cloakディレクティブは、インスタンスの準備が終わると、自動的に取り除かれる。

<div id="app">
  <p v-cloak>
    {{ message }}
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>

pタグにv-cloakをつける

var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello Vue.js'
  }
})
[v-cloak] {
  display: none;
}

v-cloakをセレクタとしてcssを書けば、インスタンスの準備中だけマスタッシュタグを非表示にすることができる。

v-text

マスタッシュの代わりに、ディレクティブを使いたい時。

v-text="プロパティ名"とすることで、マスタッシュ構文を使わずにプロパティを表示することができる。

ただ、基本的にはマスタッシュ構文に統一するのがおすすめ。

<div id="app">
  <p>
    {{ message }}
  </p>
  <p v-text="message">

  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello Vue.js'
  }
})

バインディング式

マスタッシュ構文の中では、プロパティにメソッドを呼び出したり、三項演算子で場合分けをしたりすることができる。

var app = new Vue({
  //options
  el: '#app',
  data: {
    message: 'Hello Vue.js',
    number: 100,
    ok: true
  }
})
<div id="app">
  <p>
    {{ message }}
  </p>
  <p>
    {{ number + 1 }}
  </p>
  <p>
    {{ message.split('').reverse().join('')}}
  </p>
  <p>
    {{ ok? 'Yes' : 'No' }}
  </p>
  <p>
    {{ ok? message : '' }}
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>

このようにマスタッシュ構文内で計算をしたり、メソッドを呼び出したりできる。
ただ、var app = ~~ のような代入はすることができないので注意。

=の右側に書ける処理ならマスタッシュ構文内でも書けると覚えておこう。

フィルタ(ローカルフィルタ)

数字を千円区切りのカンマ区切りで表示したい。

ポイント!!!

Vue.jsでは、式の終わりに任意のフィルタを追加することができる。

{{ 式 | フィルタの名前 }}

上記のようにフィルタを呼び出す。

<div id="app">
   <p>
     {{ price | numberFormat }}
   </p>
   <input type="text" v-bind:value="price | numberFormat">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
    price: 29800000
  },
  filters: {
    numberFormat: function(value) {
      return value.toLocaleString()
    }
  }
})

このように、filtersの中にフィルター名を決めて処理を書いていく。

フィルターはマスタッシュ構文内だけではなく、

<input type="text" v-bind:value="price | numberFormat">

このようにプロパティを書けるところなら| フィルター名 で呼び出すことができる。

フィルタ(グローバルフィルタ)

グローバルフィルターを定義するには

Vue.filter(フィルタ名, フィルタの動作)

とする

<div id="app">
   <p>
     {{ price | numberFormat }}
   </p>
   <input type="text" v-bind:value="price | numberFormat">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
Vue.filter('numberFormat', function(value) {
  return value.toLocaleString()
})

var app = new Vue({
  //options
  el: '#app',
  data: {
    price: 29800000
  }
})

このように定義する。
グローバルフィルターを定義したことで、#appの要素以外でもフィルターを呼び出すことができるってこと。

フィルターの連結

日本円をUSDに変換して、さらに3桁ずつで区切りたい。

ポイントはフィルタは複数連結することが可能ということ。

//日本円をドルに変換
Vue.filter('toUSD', function(jpy) {
  return jpy / 100
})
//変換したドルを3桁ずつ区切る
Vue.filter('numberFormat', function(value) {
  return value.toLocaleString()
})

var app = new Vue({
  //options
  el: '#app',
  data: {
    jpyPrice: 29800000000
  }
})

ここでは、複数のグローバルフィルタを用意する。

<div id="app">
   <p>
     {{ jpyPrice | toUSD | numberFormat }}
   </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>

上記のように|を複数使って、フィルターを連結することができる。

フィルタの引数

長い文字列を、20文字...のように変換したい。

ポイントはフィルタをview側で呼び出す時に引数を指定できるということ。

//lengthは何文字に省略するか、suffixは省略後の...のような文字列をどうするか
Vue.filter('readMore', function(text, length, suffix) {
  return text.substring(0, length) + suffix
})

var app = new Vue({
  //options
  el: '#app',
  data: {
    text: 'In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design.'
  }
})
<div id="app">
   <p>
     {{ text | readMore(30, '...') }}
   </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>

readMoreフィルターでは引数が3つあるが、view側で呼び出す時は2つになっている点に注意。

この場合は、length, suffixをview側のフィルターを呼び出す引数で指定してあげれば良い。

v-bind省略記法

完全な構文
<a v-bind:href="url">LINK</a>

省略記法
<a :href="url">LINK</a>

v-bindを省略して:だけにすることができる。

var app = new Vue({
  //options
  el: '#app',
  data: {
   url: 'google.com'
  }
})
<div id="app">
   <p>
     <a v-bind:href="url" target="_blank">Link1</a>
     <a :href="url" target="_blank">Link2</a>
   </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>

二つのリンクがあるが、どちらも同じ。

算出プロパティ

算出プロパティ computedとは、
関数によって算出したデータを返すことができるプロパティ

算出プロパティを使って処理をまとめてみよう

<div id="app">
  <p>
    {{ message.split('').reverse().join('') }}
  </p>
  <p>
    {{ reversedMessage }}
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
   message: 'Hello Vue.js!'
  },
  computed: {
    reversedMessage: function() {
      return this.message.split('').reverse().join('')
    }
  }
})

算出プロパティに関しては、computed:以下に処理を書いていく。
Rubyでいうヘルパーみたいなものだと思っている。

今回はreversedMessageという算出プロパティを定義した。
これを

<p>{{ reversedMessage }}</p>

このようにview側に書くだけで、文字を反転させて表示できる。
しかもこれを何回も使い回すことができる。

メソッドと算出プロパティの違い

<div id="app">
  <p>
    {{ message.split('').reverse().join('') }}
  </p>
  <p>
    {{ reversedMessage }}
  </p>
  <p>
    {{ reversedMessageMethod() }}
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
   message: 'Hello Vue.js!'
  },
  //算出プロパティ。プロパティなので呼び出す時にカッコがいらない。
  computed: {
    reversedMessage: function() {
      return this.message.split('').reverse().join('')
    }
  },
  //メソッド。メソッドなので呼び出す時はカッコがいる。
  methods: {
    reversedMessageMethod: function() {
      return this.message.split('').reverse().join('')
    }
  }
})

一つ目の違いは、プロパティは呼び出す時に()が必要ない。対して、メソッドは呼び出す時に()が必要になる。

2つ目は、getterとsetter。これは以降で解説。

3つ目は、キャッシュがあるかどうか。プロパティはキャッシュがあるが、メソッドにはない。

ということで、使えるならなるべくプロパティを使った方が良いってこと?

算出プロパティのgetter,setter

プロパティはgetterだけではなく、setterも定義できるのがmethodとの違い。

税抜き価格を入れると、税込価格を表示するプログラムを見ていこう

<div id="app">
  <p>
    base price: <input type="text" v-model="basePrice">
  </p>
  <p>
    tax included price: <input type="text" v-model="taxIncludedPrice">
  </p>

</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',
  data: {
   basePrice: 100
  },
  computed: {
    taxIncludedPrice: {
      get: function() {
        //parseIntは整数値を返す
        return parseInt(this.basePrice * 1.08)
      },
      set: function(taxIncludedPrice) {
        //Math.ceilは引数として与えた数以上の最小の整数を返します。
        this.basePrice = Math.ceil(taxIncludedPrice / 1.08)
      }
    }
  }
})

getterとsetterは文字通り、値を取得するのか、それとも値をセットするのか、というもの。

前提として、v-modelなのでviewからtaxIncludedPriceの値をセットすることが可能。

set:以下では、viewからセットされた税込価格をさらに1.08で割って税抜き価格をbasePriceに代入している。

これにより、税込価格を入れても税抜き価格が算出されるような形になる。

算出プロパティのキャッシュ

comuputed キャッシュされる
methods キャッシュされない

ランダムな数字を取得する処理を算出プロパティとメソッドの両方でやってみる。

<div id="app">
  <h2>Computed</h2>
  <ol>
    <li>{{ computedNumber }}</li>
    <li>{{ computedNumber }}</li>
    <li>{{ computedNumber }}</li>
  </ol>
  <h2>Methods</h2>
  <ol>
    <li>{{ methodsNumber() }}</li>
    <li>{{ methodsNumber() }}</li>
    <li>{{ methodsNumber() }}</li>
  </ol>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
var app = new Vue({
  //options
  el: '#app',

  computed: {
    computedNumber: function() {
        return Math.random()
    }
  },
  methods: {
    methodsNumber: function() {
        return Math.random()
    }
  }
})

この場合、算出プロパティは3つのランダムな数字が全て同じ値になる。
なぜなら、キャッシュされるから。

対して、メソッドの場合は毎回異なる数字になる。
なぜなら、キャッシュされないから。

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

DjangoでVue.js(webpack)を扱うための備忘録

前書き

この記事はPythonをバックエンド(Djangoをフレームワークとして使用)、Node.jsでWebpackを用いてVue.jsをフロントエンドとして使用できるようにするための備忘録記事です。

node.jsの知識が浅いので、使い方を間違っていることもあります。ご了承ください。

また、この記事はpipenvyarnを使用しています。導入していない場合は導入してからこの記事を読むことをお勧めします。

パッケージのインストール

Djangoのstartprojectで自動作成されたフォルダを基にします。

Python

pip install django
django-admin startproject django_vuejs
cd django_vuejs
pipenv --python 3
pipenv install django
pipenv install django-webpack-loader==0.7.0

node.js

yarn init -yp # package.jsonを作る(対話なし)
yarn add --dev @babel/core @babel/preset-env
yarn add --dev babel-loader css-loader sass-loader style-loader vue-loader vue-template-compiler
yarn add --dev webpack webpack-cli
yarn add --dev webpack-bundle-tracker@0.4.3
yarn add vue vuex vue-router
yarn add --dev clean-webpack-plugin # これは任意ですが、これを入れておくとビルドしたjsファイルがかさばらなくなるので導入推奨。

気をつけてほしいのはwebpack-bundle-trackerのバージョン指定です。バージョンが新しすぎてPython側のdjango-webpack-loaderが上手く動作しなかったため、バージョン指定しています1。念の為Python側のdjango-webpack-loaderのバージョンも指定してます。

設定

webpack

webpack.config.js
var path = require("path");
var webpack = require('webpack');
var BundleTracker = require('webpack-bundle-tracker');
var VueLoaderPlugin = require('vue-loader/lib/plugin');
var { CleanWebpackPlugin } = require('clean-webpack-plugin'); // clean-webpack-pluginを導入した場合は追記

module.exports = {
  context: __dirname,
  mode: process.env.NODE_ENV,
  entry: {
    main: './src-front/main.js'
  },

  output: {
    filename: "[name]-[hash].js",
    path: path.resolve('./static/build/')
  },

  plugins: [
    new BundleTracker({
      path: '.',
      filename: 'webpack-stats.json'
    }),
    // clean-webpack-pluginを導入した場合は追記(ここから)
    new CleanWebpackPlugin({
      verbose: true
    }),
    // (ここまで)
    new VueLoaderPlugin()
  ],

  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              url: false,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },

  resolve: {
    extensions: ['.js', '.vue'],
    modules: [
      'node_modules'
    ],
    alias: {
      'vue': path.resolve('./node_modules/vue/dist/vue.js')
    }
  }
}

Django

django_vuejs/settings.py
# (省略)
INSTALLED_APPS = [
    # ...
    # 以下の文を追記
    'webpack_loader',
]

# 追記
WEBPACK_LOADER = {
   'DEFAULT': {
       'CACHE': not DEBUG,
       'BUNDLE_DIR_NAME': 'build/',
       'TIMEOUT': None,
       'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
       'IGNORE': [r'.+\.hot-update.js', r'.+\.map']
   }
}

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

package.json

package.json内に以下の内容を追記します。
--progressオプションはwebpackの進捗状況を可視化できるオプションのため、不要であれば付ける必要はありません。

package.json
  "scripts": {
    "dev": "webpack --mode development --progress",
    "build": "webpack --mode production --progress",
    "watch": "webpack --watch -d --progress"
  },

メインページ

これがフロントエンドがベースとなるファイルです。ここにwebpackでビルドしたCSSやjavascriptのリンクが埋め込まれます。

template/index.html
{% load render_bundle from webpack_loader %}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>django_vuejs</title>
        {% render_bundle 'main' 'css' %}
    </head>
    <body>
        <noscript>
            <p>Your browser is disabled JavaScript. To use this web application, please enable JavaScript.</p>
        </noscript>
        <div id="app"></div>
        {% render_bundle 'main' 'js' %}
    </body>
</head>

Django URL

※今回は1ファイルにまとめていますが、適宜場所を移してください。

django_vuejs
from django.contrib import admin
from django.urls import path
from django.shortcuts import render

def index(request):
    return render(request, "index.html")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index, name="index"),
]

フロントエンド

src-front/main.js
import Vue from 'vue'

import App from './App'
import router from './router.js'
import store from './store'

Vue.config.productionTip = false

const app = new Vue({
    el: '#app',
    router,
    store,
    template: "<App/>",
    components: { App }
})
src-front/App.vue
<template>
  <div>
      <p>Hello World!</p>
      <router-view></router-view>
  </div>
</template>

<script>
export default {
    name: 'App'
}
</script>

<style>

</style>

./router.js./storeはVue.jsのコマンドで自動生成されるものともろ一緒なので、申し訳ないのですが割愛します。

gitignore (gitで管理する場合は設定)

.gitignore
# 追記
/static/build/
/webpack-stats.json

ビルド

本番用でビルドする場合はyarn run build
開発用としてビルドする場合はyarn run devまたはyarn run watch(フロントエンド系のソースを集中的に弄る場合は推奨)
のコマンドを実行します。

ビルドが完了したらpython manage.py runserverでDjangoの開発サーバーを起動し、http://localhost:8000/にアクセスすると、表示されるはずです。

参考文献


  1. 動作しない問題はすでにdjango-webpack-loaderIssueで上がっています。これを見るまでわかりませんでした・・・。 

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

A-FrameをつかってWebVRをためしてみた!

概要

この記事はVue.jsで作ったサイトに3Dをいれてみたいというところで手軽にWebVRを実装できるというA-Frameをさわってみました

できたもの

https://nekosagashi.tk/

色々足りず、ホラーなものができました。
2匹猫がいて、猫をクリクすると、猫が回ったり、雪が降ったりします。
猫はこちらからシャム猫3Dモデルとアメリカンショートヘアの3Dモデルをお借りしたのですが、なぜか表面がついてきませんでした。。。

環境

macOS Catalina 
Visual Studio Code 1.45.1
Node.js: v13.11.0
npm:6.14.5
Vue:@vue/cli 4.3.1

大まかな流れ

  1. Vue.jsとFirebaseでログイン認証付きのページをつくる
  2. A-Frameをいれる
  3. 3Dモデルに動きをつける
  4. ドメインをとって公開

という流れです

1. Vue.js くみたてる

Vue.jsを勉強中のため、ちょっとむやみにセキュアなページにします。
こちらを参考に
Vue.jsでつくったサイトにfirebaseでユーザ認証してこっそり人の顔年齢を試して遊ぶ - Qiita
Firebaseでログイン処理をつくってVue.jsで3Pほどのページをつくります。

2. A-Frameをいれる

A-Frame – Make WebVR
スクリーンショット 2020-06-03 14.48.51.png

npmでいれたいのでaframe - npmこちらからインストールしていきましょう。
サンプルをベースに猫の3Dオブジェクトを2体配置します。
猫にたいしてクリックイベントをつけます。
A-Frameのパーティクルコンポーネントを使って雪をふらしましょう
Entity-Component-System – A-Frame
こちらもnpmでいれちゃいますaframe-particle-system-component - npm

3.3Dモデルに動きをつける

猫をクリックすると猫がグルングルン回るものと
猫をクリックしたら雪が降るというものをつくります

<template>
<a-text font="kelsonsans" value="Click Me!" width="6" position="1.5 1 -1.5" rotation="0 0 0"></a-text>
        <a-assets>
          <img id="sample_img" src="/static/cat.png" />
          <a-asset-item id="sample_obj" src="/static/cat-print.obj"></a-asset-item>
          <a-asset-item id="sample_mtl" src="/static/cat-print.mtl"></a-asset-item>
        </a-assets>
        <a-obj-model scale="0.3 0.3 0.3" position="2.5 0 -1.5" src="#sample_obj" mtl="#sample_mtl" animation__star_rotation="property: rotation; startEvents:click; from: 0 0 0; to: 0 360 0; loop:5;"></a-obj-model>

        <a-entity id="snow" visible="false">
        <a-entity particle-system="preset:snow;" position="0 0 -4"></a-entity>
      </a-entity>

        <a-assets>
          <img id="neko_img" src="static/CatTexture.png" />
          <a-asset-item id="neko_obj" src="/static/neko.obj"></a-asset-item>
          <a-asset-item id="neko_stl" src="/static/neko.stl"></a-asset-item>
        </a-assets>
        <a-obj-model scale="0.2 0.2 0.2" position="-4.5 -1 -1.5" src="#neko_obj" mtl="#neko_stl" @click="handlerMouseEnter"></a-obj-model>
</template>
<script>
import firebase from 'firebase'
import 'aframe'

export default {
  name: 'Vue3d',
  el: '#aframeApp',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      name: firebase.auth().currentUser.email,
      skyboxSrc: 'https://images.unsplash.com/photo-1505252772853-08ed4d526ceb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80'
    }
  },
  methods: {
    handlerClick: function (event) {
      let boxid = event.target.id
      if (boxid === 'areabox2') {
        this.skyboxSrc = 'https://images.unsplash.com/photo-1557971370-e7298ee473fb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80'
      }
    },
    handlerMouseEnter: function (event) {
      document.getElementById('snow').setAttribute('visible', 'true')
    }
  }
}
</script>

全体のコードはこちら

<template>
  <div class="hello">
    <div class="inf">
      <h2>猫を探してください</h2>
      <p>ここには猫が2匹います。<br>真ん中にある<span class="maru"></span>を動かして、猫を探してクリックしてあげてください
      </p>
    </div>
      <a-scene>
        <a-entity id="aframeApps">
          <!-- 背景設定 -->
          <a-sky ref="skybox" v-bind:src="skyboxSrc" rotation="0 -130 0"></a-sky>
          <a-box id="areabox2" position="2.5 0 1.5" rotation="45 45 45" @click="handlerClick"></a-box>
        </a-entity>
        <!-- ぐるぐる回る猫 -->
        <a-text font="kelsonsans" value="Click Me!" width="6" position="1.5 1 -1.5" rotation="0 0 0"></a-text>
        <a-assets>
          <img id="sample_img" src="/static/cat.png" />
          <a-asset-item id="sample_obj" src="/static/cat-print.obj"></a-asset-item>
          <a-asset-item id="sample_mtl" src="/static/cat-print.mtl"></a-asset-item>
        </a-assets>
        <a-obj-model scale="0.3 0.3 0.3" position="2.5 0 -1.5" src="#sample_obj" mtl="#sample_mtl" animation__star_rotation="property: rotation; startEvents:click; from: 0 0 0; to: 0 360 0; loop:5;"></a-obj-model>
        <!-- 雪を最初非表示 -->
        <a-entity id="snow" visible="false">
          <a-entity particle-system="preset:snow;" position="0 0 -4"></a-entity>
        </a-entity>
        <!-- 雪を降らす猫 -->
        <a-assets>
          <img id="neko_img" src="static/CatTexture.png" />
          <a-asset-item id="neko_obj" src="/static/neko.obj"></a-asset-item>
          <a-asset-item id="neko_stl" src="/static/neko.stl"></a-asset-item>
        </a-assets>
        <a-obj-model scale="0.2 0.2 0.2" position="-4.5 -1 -1.5" src="#neko_obj" mtl="#neko_stl" @click="handlerMouseEnter"></a-obj-model>

        <a-entity>
          <a-camera>
            <a-cursor></a-cursor>
          </a-camera>
        </a-entity>
      </a-scene>

</div>
</template>

<script>
import firebase from 'firebase'
import 'aframe'

export default {
  name: 'Vue3d',
  el: '#aframeApp',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      name: firebase.auth().currentUser.email,
      skyboxSrc: 'https://images.unsplash.com/photo-1505252772853-08ed4d526ceb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80'
    }
  },
  methods: {
    handlerClick: function (event) {
      console.log('handlerClick')
      console.log(event.target.id)
      let boxid = event.target.id
      if (boxid === 'areabox2') {
        this.skyboxSrc = 'https://images.unsplash.com/photo-1557971370-e7298ee473fb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80'
      }
    },
    handlerMouseEnter: function (event) {
      console.log('handlerMouseEnter')
      console.log(event)
      document.getElementById('snow').setAttribute('visible', 'true')
    }
  }
}
</script>
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
.inf{
    position: absolute;
    left: calc(50% - 16rem);
    top: 20%;
    z-index: 99999;
    color: #fff;
    font-size: 1rem;
    padding: 0.5rem;
    background: hsla(0,0%,100%,.18);
    width: 32rem;
}
.maru{
  font-size:80%;
  color:#000;
  font-weight:bold;
}
</style>

ドメインを取るNetlifyにDeploy

こちらを参考にfreenomでドメインをとってNetlifyにDeployします
Vue.jsでつくったサイトにfirebaseでユーザ認証してこっそり人の顔年齢を試して遊ぶ - Qiita

できなかったところ

最初のロード時にA-Frameのデフォルト?の水色とか出てきちゃうのをとめたかった。。。あれはなんだろう。
猫の表面が読み込めず、石膏像になってしまった。
Singin画面に画像を置いたのですがうまく反映されず。publicにおいたのに。。。

<img v-bind:src="img">
 ---略---
data: function () {
    return {
      img: '/public/img/nko3d.png'
    }
  }

参考サイト

AframeをVue.jsで使うよ! : 新人SEの気まぐれ日記
新しいWebVRフレームワークA-Frame入門 - Qiita
A-Frame v1.0 で クリスマスアニメーション - Qiita

感想

簡単に3Dが実装できた!でもちょっとやり込もうとするとやはり難しかった・・・。

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

難読化されたJavaScriptを解読するツールを作った

Obfuscator.ioというJavaScriptコードを難読化できるツールがあります。

例えば以下のような感じ

Before
難読化前

After
難読化されたコード

正直難読化されてても慣れればコードを読むのはそんなに苦じゃないのですが、自動ですべて解読するツールを作ったらおもしろそうだと思ってやってみました。

難読化されたJavaScriptを半自動で解読するツール

解読ツール

Vue.js(サイト側)とCloud Functions(API側)で作ってみました。

公開URL: https://sigr.io/deobfuscator/

残念ながら完全自動化までは至りませんでしたが、最初にターゲットとなる関数名を入力すれば後は自動で解析してくれます。

ツールの使い方

  1. 解読したいコードをInputに貼り付ける。
  2. 貼り付けたコードの中に以下のような部分があるので関数名をコピーする(3つのうちどれかに似てるはず)

_0x439c[103]
_0x439c('0x4')
_0x439c('0x2','f]Xg')

これの場合は_0x439cが対象(関数名)。Target function nameの欄にコピーしたのを貼り付ける

3.「Deobfuscate」をクリック
4.ちゃんと解読されればResultに結果が表示される

ちゃんと解読されると難読化された部分が読めるようになるはずです。
結果

さいごに

GitHubにフロント側のソースコードをうpしています。
https://github.com/LostMyCode/javascript-deobfuscator

CloudFunctionsで動いているAPI側のコードもいつか公開するかもしれません。

javascript-deobfuscator

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

【VueコピぺOK】なにこの動き?100倍センスを感じるCSSアニメーション実装

スクリーンショット 2020-06-03 15.31.28.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

なにこの動き?100倍センスを感じるアニメーション実装

上の方で確認してもらったかと思いますが、

すでに虹色の背景を付けたりとかなり洗練されている画像一覧に更にスタイル修正を行うことで

一方の画像をhoverするとスーッと画像全体が現れる不思議すぎてクールなアニメーション実装をしていきましょう
スクリーンショット 2020-06-03 15.31.15.png

デフォルトの上の画像をhoverしたら下の画像のように一方の画像のタイトルが消えて全体像が現れる
アニメーションにカスタマイズしていきます

スクリーンショット 2020-06-03 15.31.28.png

これやばくないですか?

上の画像を見てるだけだと実際の動きを想像しづらいかもしれませんが、

実装してみて初めてわかる快感を味わえます

これは損得抜きでまじでおすすめのアニメーションです

兎にも角にも、実装してみたら感動すること間違いなしです!

では早速コードをコピペしていきましょう

<template>
  <div class="images">
    <div class="image">
      <img src="https://images.unsplash.com/photo-1513543806865-85e29a7c0352?ixlib=rb-1.2.1&auto=format&fit=crop&w=2775&q=80">
      <span>Day</span>
    </div>
    <div class="image">
      <img src="https://images.unsplash.com/photo-1551607117-21fa129a211d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1720&q=80">
      <span>Evening</span>
    </div>
  </div>
</template>
<style>
.images {
  display: flex;
  width: 100%;
  padding: 4% 2%;
  box-sizing: border-box;
  height: 60vh;
}

.image {
  flex: 1;
  overflow: hidden;
  transition: .5s;
  margin: 0 2%;
  box-shadow: 0 20px 30px rgba(0,0,0,.1);
  line-height: 0;
}

.image img {
  width: 200%;
  height: calc(100% - 10vh);
  object-fit: cover;
  transition: .5s;
}

.image span {
  background : linear-gradient(41deg, purple 25%, orange 50%, rgba(255, 107, 0, 0.48)) fixed;
  font-weight: bold;
  color: #fff;
  font-size: 3.8vh;
  display: block;
  text-align: center;
  height: 10vh;
  line-height: 2.6;
  border-top: 3px solid #fff;
}

.image:hover > img {
  width: 100%;
  height: 100%;
}

</style>

いかがでしたでしょうか?

画像をhoverするとタイトルが消えて画像の全体像が現れるアニメーションが実行されましたか?

実際に自分のローカルブラウザで反映できると動きも確認できてめっちゃ興奮しますよね

こちらに他にも面白いアニメーション実装記事があるので参考までに

以上です


参考記事(他の魅力的なアニメーション)
現役フロントエンドエンジニアがおすすめするコピペで使えるアニメーション記事まとめ12選
スクリーンショット 2020-06-02 17.23.10.png

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

vue.jsでビルドしたら何も表示されなかった時の対処法

vue.jsでビルドしたらindex.htmlに何も表示されなかった時の対処法

vue.jsでビルドした時にできるdistフォルダの中のindex.htmlを開いてみたらブラウザに
なにも表示されていなかった。...せっかくwebアプリを作ったにも関わらず
今回はその時の自分が行った対処法を載せていきます。

1 ターミナルを開き vue ui と
入力

2 左下の家のマークをクリック

3 対象プロジェクトを選択

4 左ペインの上から4つ目の歯車のマークをクリック

5 右側にでてくる公開パスの/を決して更新する

この手順でもう一度ビルド後のindex.htmlファイルを更新すると内容が反映されていると思います。

最後まで見ていただきありがとうございました。
つっかえた技術とかあったらその都度書き込んでいきたいと思います

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

【Javascript】 2つの配列に格納されているオブジェクト内部要素を比較し、新しいオブジェクト要素を追加

背景

vueでaxios使ってAPI情報取得したときに、使うことが多いかもなーってことで、もっと良い方法がありそうだが、ひとまず、自分用にメモで取っておきます。

vueでAPI叩いたときに、配列内部にオブジェクトを抱えたものが返ってくる前提です。

もっとこのほうがいいなど、あればアドバイスいただければ幸いです。

内容

結論

内容
// axios叩いたときに返って来るdataの文字列
var obj = "data"
var test02 = [
    {
        "site_no": "00002",
        "term_id": "00002",
    },
    {
        "site_no": "00001",
        "term_id": "00001",
    },
    {
        "site_no": "00003",
        "term_id": "800003",
    },
    {
        "site_no": "00004",
        "term_id": "00004",
    }
]

// axios叩いたときに返ってくる配列(配列に格納されたオブジェクト一覧が返ってくる前提)
var test08 = [
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test01",
            "term_id": "800001",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    },
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test03",
            "term_id": "800003",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    },
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test02",
            "term_id": "800002",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    }
]



// 比較する配列内部のオブジェクトに一致する文字列があるかを確認し、ある・ないで処理を分ける
// (2つ配列の内部どちらにもオブジェクトがあり、オブジェクト内部の特定文字列が存在することを確認する)
// ある:比較対象側オブジェクト(端末一覧情報オブジェクト)に、要素を追加
// ない:特に処理をせず終了
test08.some((val01)=>{
    test02.some((val02) => {
        console.log("val01: ", val01)
        if(obj !== ""){
            if( ( "term_id" in val01[obj] && "term_id" in val02 ) && ( val01[obj]["term_id"] === val02["term_id"] ) ){
                // console.log("存在します。")
                val02.image_url = val01[obj].image_url
            }else{
                // console.log("存在しません。")
            }
        }else{
            // オブジェクト内部に、検査したい要素があること確認 && それぞれのオブジェクトで要素が一致すること確認
            if( ( "term_id" in val01 && "term_id" in val02 ) && ( val01["term_id"] === val02["term_id"] ) ){
                // console.log("存在します。")
                // 条件に合致した場合に、片方の配列のオブジェクト要素に代入
                val02.image_url = val01.image_url
                // 合致したタイミングで処理を抜ける
                return true;
            }else{
                // console.log("存在しません。")
            }
        }
    })
})
console.log("result: ", test02)
結果
result:  [
  { site_no: '00002', term_id: '00002' },
  { site_no: '00001', term_id: '00001' },
  { site_no: '00003', term_id: '800003', image_url: 'http://test03' },
  { site_no: '00004', term_id: '00004' }
]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js、AWS Amplifyおよびboto3でサンプルアプリを作ってみる(第三回:axiosでAPIコール編)

第二回で、Amplify CLIを使ってバックエンドリソースを作成し、CICDを回してVueアプリを外部公開するところまで辿り着いた。
今回は、さらにAPIとLambda関数を用意し、Vue.jsからaxiosで呼び出すところまでをトライ。これができれば、「Vue.jsでフロントエンドを作ってAWSサービスを叩いてみる」という当初の目的を達成できたことになる。

前回の内容はこちら。
Vue.js、AWS Amplifyおよびboto3でサンプルアプリを作ってみる(第二回:Amplifyとバックエンドリソース編)

(今回)やりたいこと

  • axiosをセットアップする
  • APIとLambda関数を作る
  • Vue.jsからaxios経由でAPIを呼び出す

image.png

あえてやらないこと

AmplifyはライブラリやUIコンポーネントを備えていて、その気になればVue.js内から直接AWSサービスを操作できる様子(例:学習/デプロイ済みの機械学習モデルエンドポイントに推論リクエストを投げる、S3にオブジェクトをアップロードするなど)。
ただ、それをやるとなるとVue.jsのディレクティブやJavascript SDKにもう少し深入りする必要がありそう。あっちもこっちも手を出したくないので、今回は初志貫徹でコードはPython、SDKはboto3に留めておく。

ということで、上図の通りフロントエンドからはAPIを叩くだけにして、ロジックやSDKの使用はバックエンドで行う役割分担にする。

axiosについて

簡単に言うとHTTPクライアントで、Vue.jsのコード内からPostman的にREST APIを呼び出す方法がないかと探すうちに発見。

axios

これなら簡単にAPIの呼び出しと結果の受領ができそうだ。

以下を参考にさせていただきました。感謝。
axios を利用した API の使用
axiosを乗りこなす機能についての知見集
Vue.js+axiosでDynamo DBにAjax通信する
[axios] axios の導入と簡単な使い方

Step by Step

1. Amplifyライブラリのインストール

やらないとは言いつつも、後学のために、セットアップして使えるようにするところまでは試しておく。

% npm install aws-amplify
% npm install aws-amplify-vue 

このあたりを参照しながらインストール。
公式も参照のこと。

2. src/main.jsへの取り込み

src/main.jsを編集して、Amplifyライブラリの取り込みを指定する。

src/main.js
// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)

Vue.use(AmplifyPlugin, AmplifyModules)

aws-exportsが見つからない、というエラーが出る場合は、.gitignoreでGitのトラッキング対象外になってしまっていないかを確認する。
自分の環境では、これをコメントアウトすると動いた。
.gitignoreへの追加はAmplify自身が行っているようなので若干謎だが、とりあえず動いたので、気にせず先に進む。

.gitignore
.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

#amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/mock-data
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation
build/
dist/
node_modules/
# aws-exports.js <-- ここ
awsconfiguration.json
amplifyconfiguration.json
amplify-build-config.json
amplify-gradle-config.json
amplifyxc.config

3. axiosのインストール

Amplifyライブラリと同じ手順。

% npm install axios

4. src/main.jsへの取り込み

ここもAmplifyライブラリと同様だが、axoisの仕様に若干のクセがありハマった。
axiosは厳密にはプラグインでないので、main.jsでVue.use()に書いてあっても、this.axiosUndefinedとなってしまう。
代わりに以下のようにprototype.$axiosとして定義し、読み込み元では$axios.get()として呼ぶ必要がある。

// ダメな例

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(AmplifyPlugin, AmplifyModules, VueAxios, axios)
// 動く例

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios

main.jsは最終的にこのようになる。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

// Element UI
import './plugins/element.js'

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)

Vue.config.productionTip = false
Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

これでようやくaxiosの事前準備が完了。

5. Lambda関数の準備

バックエンド側を作る。
そろそろAmplifyに全部やらせるのも飽きてきたので、練習も兼ねて、amplify add functionではなくスクラッチで用意する。
こんな感じのLambda関数を書く。

lambda_function.py
import boto3
import json
import os
import datetime

print('Loading function')
glue = boto3.client('glue')
gluedb = os.environ['GLUEDBNAME']

# dict内のdatetime型データをJSON対応のISOフォーマット(文字列)に変換する
def convert_datetime2iso(object):
    if isinstance(object, type(datetime)):
        return object.isoformat()

# 本体
def lambda_handler(event, context):
    operation = 'GET'
    if operation == 'GET': 
        tables = glue.get_tables(
            DatabaseName=gluedb
        )["TableList"]
        response = json.dumps(tables, default=convert_datetime2iso)
        return {
            'isBase64Encoded': False,
            'statusCode': 200,
            'headers': {
                # CORSの許可
                'Content-Type': 'application/json', 
                'Access-Control-Allow-Origin': '*' 
            },
            'body': response
        }
    else:
        response = ('Unsupported method:' + format(operation))
        return response

ファイル名がlambda_function.py、関数名がlambda_handler()なので、Lambdaからはlambda_function.lambda_handlerとして呼び出すことになる。

AWS Glueを呼び出し、DB名を渡してテーブル一覧を取得する簡単な関数で、GLUEDBNAMEをLambdaの環境変数として渡す形を取っている。今回はsh10_externalをDB名とした。よーく見る(見なくても)とメソッドがGETしか定義されてないが、お試しなのでご容赦。。

スクリーンショット 2020-06-03 02.07.35.png

実際にGlueに渡す命令はglue.get_tables()のみで、特に複雑な処理はない。

6. APIの準備

次に、axiosから呼び出すREST APIを作成する。このAPIのバックエンドとして、先程のLambda関数を実行する形。
これもamplify add apiではなくスクラッチでAPIを作成してみる。
マネジメントコンソールでAPI Gatewayの画面に移動し、以下の仕様でAPIを作成。

  • Lambdaプロキシー統合
  • /tablesリソース
  • ANYメソッド

スクリーンショット 2020-06-03 13.40.50.png

API作成まではすんなりいったものの、Lambda関数と統合して動かすまでに色々ハマった。

まず、not JSON seriarizableエラー(クライアントから見ると500エラー)が出まくる。Lambda単体では動くようになっても、APIから呼ぶとまた出る。ここで大分時間を使った。
詳細はまた項を改めるが、結論としてはGlueからのdict型のレスポンスをjson.dump()した上で、API GatewayがLambdaプロキシ統合で要求する形式に成形して返すことで、無事API様に受け取って貰えた。

ようやく関門突破かと思いきや、今度はCORS header 'Access-Control-Allow-Origin' missing'といったエラーが出る。何か見覚えある単語が。
これは、axiosというか今回のSPAが当該APIを別のオリジンから呼ぶ形になるので、CORSを許可する必要があるということだ。
CORSは一般にサーバー側で設定し、API Gatewayも
OPTIONS`メソッドでこれを設定するメニューがあったので設定してみたが、どうやら効いてなさそうだ。

色々調べたところ、今回使用したLambdaプロキシー統合の場合はどうもバックエンドのLambdaの方で明示的にCORSを許可するヘッダを記述してやる必要がある模様。
上記の返値の中ほど、'headers':{}の中にCORS設定を書いてやると、ようやく動いた。やれやれ。

        return {
            'isBase64Encoded': False,
            'statusCode': 200,
            'headers': {
                # CORSの許可
                'Content-Type': 'application/json', 
                'Access-Control-Allow-Origin': '*' 
            },
            'body': response
        }

7. API呼び出しとテーブルへの取り込み設定

最後は再びフロントエンド側に戻り、axiosでAPIを呼び出してデータを取得する箇所と、取得したデータをElement UIのtablesの中に成形して取り込む受け皿部分を作る。
今回のSPAではsrc/components/配下に全てのコンポーネントを配置してVue Routerからルーティングしているが、この一つとしてCatalogueTable.vueというページを用意する。

ここでのポイントは以下の三つ。
- getDummyData()内のthis.$axios.getでaxiosを呼び出し、REST APIを叩く
- tablesという配列でデータを受け取る
- Element UIのel-tableコンポーネントでtablesのデータ(:data="tables")を成形する

axiosの引数となるREST APIのURIには、先程API Gatewayで作成したAPIのエンドポイントを指定する。
成形は<el-table></el-table><el-table-column></el-table-column>の中で、Element UIの書式を使ってわりと自由に行うことができる。
ここでは最低限の属性だけを設定してみた。

  • テーブル
属性 内容
:data データソース
stripe ストライプ表示にする
style="width: 100%" 横幅の長さ
align="center" 中央揃え
属性 内容
prop 列名
label 列の表示名
width 列の長さ
sortable ソート可能な列として指定
src/components/CatalogueTable.vue
<template>
  <div class="cataloguetable">
    <p></p>
    <h2>テーブル一覧</h2>
    <el-button type="primary" @click="getDummyData" :loading="false">実行</el-button>
    <el-table
      :data="tables"
      stripe
      style="width: 100%"
      align="center">
      <el-table-column
        prop="Name"
        label="Table"
        width="250"
        sortable>
      </el-table-column>
      <el-table-column
        prop="DatabaseName"
        label="Database"
        width="200"
        sortable>
      </el-table-column>
      <el-table-column
        prop= "TableType"
        label="Type"
        width="200"
        sortable>
      </el-table-column>
      <el-table-column
        prop="PartitionKeys[0][Name]"
        label="Partition Key 1"
        width="150"
        sortable>
      </el-table-column>
      <el-table-column
        prop="PartitionKeys[1][Name]"
        label="Partition Key 2"
        width="150"
        sortable>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      input: '',
      tables: []
    }
  },
  methods: {
    getDummyData() {
      let uri = 'https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/tables';
      this.$axios.get(uri)
        .then((response) => {
          this.tables = response.data
        })
        .catch((e) => {
          alert(e);
        });
    }
  }
}
</script>

これを呼び出す側のApp.vueはこんな感じで記述する。
(最終的には色々やりたいので、Element UIを使ったコンポーネントのガラだけはたくさん作ってあるが、CatalogueTable.vue以外の中身はまだ空に近い)。

src/App.vue
<template>
  <div id="app">
    <div>
    <img alt="Vue Logo" src="@/assets/gluelogo.png/" width="60" height="60">
      <h1>Data Catalogue Explorer</h1>
      <el-menu :default-active="activeIndex" mode="horizontal" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" router>
        <el-menu-item index="home" :route="{ name:'Home' }">ホーム</el-menu-item>
        <el-menu-item index="dataset" :route="{ name:'Dataset' }">データセットの一覧を見る</el-menu-item>
        <el-menu-item index="finder" :route="{ name:'Finder' }">データセットを探す</el-menu-item>
        <el-menu-item index="adhoc" :route="{ name:'Adhoc' }">アドホック検索</el-menu-item>
        <el-submenu index="catalogue">
            <template slot="title">システムカタログ</template>
            <el-menu-item index="catalogue-database" :route="{ name:'CatalogueDatabase' }">データベース</el-menu-item>
            <el-menu-item index="catalogue-table" :route="{ name:'CatalogueTable' }">テーブル</el-menu-item>
            <el-menu-item index="catalogue-crawler">クローラー</el-menu-item>
            <el-menu-item index="catalogue-job">ジョブ</el-menu-item>
            <el-menu-item index="catalogue-jdbc">JDBC接続</el-menu-item>
        </el-submenu>
        <el-menu-item index="api" :route="{ name:'API' }">API実行</el-menu-item>
        <el-menu-item index="element" :route="{ name:'Element' }">Element UI</el-menu-item>
        <el-menu-item index="about" :route="{ name:'About' }">Vue.js</el-menu-item>
        <el-menu-item index="resources" :route="{ name:'Resources' }">リソース</el-menu-item>
      </el-menu>
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      activeIndex: this.$route.name
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Vue Routerにもこのコンポーネントへのルートを忘れずに追加してやる必要がある。

router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
...(略)...
# ここと
import CatalogueTable from '@/components/CatalogueTable.vue'
...(略)...

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
...(略)...
  # ここ
  {
    path: '/cataloguetable',
    name: 'CatalogueTable',
    component: CatalogueTable
  },
...(略)...

最後にここまでの内容をコミットし、レポジトリにプッシュ(このままAmplifyの方で自動ビルドが回り、ホスティングされている内容が更新される。詳しくは前回の記事を参照)。

% git add .
% git commit -m "axios defined"
% git push -u origin master

9. 動作確認

Amplifyがデプロイを回してサイトの更新を完了するまで、待つこと数分。
ブラウザ、スマホでアクセスしてみると、どうやら無事動作している模様。

スクリーンショット 2020-06-03 16.07.57.png

10. 落ち穂拾い

  • Amplifyマネージドのビルドで、なぜかconsole.logが失敗する。
    • 構文チェックツールのESlint設定が何かおかしいのかも、と疑って、このあたりを参考にあれこれ検証してみるも解決に至らず。 console.logを出さなければいいだけの話なので、ここではいったん忘れることにした。いずれ解決したい。

ポスト三個分の長丁場になってしまったが、ようやくタイトル通りの自習が完了。
追々、APIを追加したりUIをいじってみたりと、色々遊んでみたい。

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

NuxtJS(+Vuetify)にFirebaseを入れたらビルドエラーが出る

バージョン

"firebase": "^7.14.6"
"nuxt": "^2.0.0"
"@nuxtjs/vuetify": "^1.0.0"

エラー

core-js 関連でエラーが出ていることが分かります。

ERROR in ./node_modules/vuetify/lib/util/helpers.js
Module not found: Error: Can't resolve 'core-js/modules/es6.array.fill' in '/Users/shinozaki/src/github.com/shinoshu/google-analytics-for-firebase/node_modules/vuetify/lib/util'
 @ ./node_modules/vuetify/lib/util/helpers.js 3:0-40
 @ ./node_modules/vuetify/lib/components/VList/index.js
 @ ./layouts/default.vue
 @ ./.nuxt/App.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi ./.nuxt/client.js

# 省略

ERROR in ./node_modules/vuetify/lib/util/colorUtils.js
Module not found: Error: Can't resolve 'core-js/modules/web.dom.iterable' in '/Users/shinozaki/src/github.com/shinoshu/google-analytics-for-firebase/node_modules/vuetify/lib/util'
 @ ./node_modules/vuetify/lib/util/colorUtils.js 3:0-42
 @ ./node_modules/vuetify/lib/services/theme/utils.js
 @ ./node_modules/vuetify/lib/services/theme/index.js
 @ ./node_modules/vuetify/lib/services/index.js
 @ ./node_modules/vuetify/lib/framework.js
 @ ./.nuxt/vuetify/plugin.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi ./.nuxt/client.js

原因

The problem is nuxt/@nuxt/babel-preset-app is not able to locate the right version of core-js when there are multiple versions of core-js in node_modules, which is a common situation in the npm ecosystem. If anything, it is something nuxt should address.

問題はnuxt / @ nuxt / babel-preset-appが、node_modulesに複数のバージョンのcore-jsがある場合に正しいバージョンのcore-jsを見つけられないことです。これは、npmエコシステムの一般的な状況です。どちらかと言えば、それはnuxtが対処すべきものです。

https://github.com/firebase/firebase-js-sdk/issues/2968#issuecomment-617975070

解決方法

https://nuxtjs.org/guide/release-notes#v2.6.0

方法1

core-js ver.2 を使う方法

$ npm i -D core-js@2

$ yarn add -D core-js@2

方法2

core-js ver.3 を使う方法

こちらは公式サイトに詳しくやり方が記載されていますので、ご参照ください。

https://nuxtjs.org/guide/release-notes#v2.6.0

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

ゼロから始めるVue.js生活

はじめに

プログラミングの「プ」の字もわからなかった入社2年目が新たにVue.jsを学習していき、分かったことをまとめる日記です。

Vue.jsを始めてみよう!

まずは手軽にVue.jsを始めてみよう

↓こちらで試し撃ちができるようです。
CodePen Home Vue.js Playground

また、CDNとしても提供されているので、HTML中にこちらを挿入することでローカル環境でも使えるようになリマス。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

ゼロから始めるVue.js生活

Vueインスタンスを使いこなす

まずは以下のコードを打ち込んだhtmlを表示して、画面に「ほげぇぇぇぇぇぇぇぇ」を出力してみよう

<!-- HTML -->
<div>--------------</div>
<div id="hoge">
  {{ message }}
</div>
<div>--------------</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      message: 'ほげぇぇぇぇぇぇぇぇ'
    }
  })
</script>
<!-- Vue -->

..するとこのような表示が確認できる

--------------
ほげぇぇぇぇぇぇぇぇ
--------------

このとき、
Vueコード側では

  • new Vue()でVueインスタンスを作成
  • el:で適用する範囲を指定
  • data:でプロパティを設定(app.messageとして参照可)

一方、HTML側では、

  • {{ message }}data: {message:を参照し、展開している

dataに階層構造を作る

dataプロパティはこのように子プロパティをネストすることが可能

<!-- HTML -->
<div>--------------</div>
<div id="hoge">
  {{ oya_hoge.kodomo_hoge }} <!-- hogeeeee -->
</div>
<div>--------------</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      oya_hoge:
        {
          kodomo_hoge: 'hogeeeee'
        }
    }
  })
</script>
<!-- Vue -->

mustache {{ }} について

{{ }}では、vueのプロパティだけではなく、JavaScriptコードを展開可能

<!-- HTML -->
<div class="hoge">
  {{ message + '?????' }} <!--ほげぇぇぇぇぇぇぇぇ?????-->
  {{ Math.abs(-999999)}} <!--999999-->
</div>
<!-- HTML -->

el: について

el:で指定したタグ以外では、messageは展開できない

<!-- HTML -->
<div class="not_hoge">
  {{ message }} <!--{{ message }}-->
</div>

<div class="hoge">
  {{ message }} <!--ほげぇぇぇぇぇぇぇぇ-->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      message: "ほげぇぇぇぇぇぇぇぇ",
    },
  });
</script>
<!-- Vue -->

ただし、指定したタグ配下であれば、どこであっても展開できる

<!-- HTML -->
<div class="hoge">
    {{ message }} <!--ほげぇぇぇぇぇぇぇぇ-->
    <div class="in_hoge">
      {{ message }} <!--ほげぇぇぇぇぇぇぇぇ-->
    </div>
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      message: "ほげぇぇぇぇぇぇぇぇ",
    },
  });
</script>
<!-- Vue -->

複数のタグを対象とすることはできない

el:が適用されるタグは必ず1つ。
したがって、同じクラス名をもつタグを複数作っても、先頭のタグ内のみmessageが展開される。

<!-- HTML -->
<div class="hoge">
  {{ message }} <!--ほげぇぇぇぇぇぇぇぇ-->
</div>
<div class="hoge">
  {{ message }} <!--{{ message }}-->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      message: "ほげぇぇぇぇぇぇぇぇ",
    },
  });
</script>
<!-- Vue -->

同一の親ノード内で、既にあるプロパティは定義できない

今回は、app.messageを先に定義しているのでmessage: "ほげぇぇぇぇぇぇぇぇ"が優先されて適用されている。
ノードの親子関係は関係なく、先に書いてある方が優先して適用されるので注意されたし。

<!-- HTML -->
<div class="hoge">
  <div class="under_hoge">
    {{ message }} <!-- ほげぇぇぇぇぇぇぇぇ -->
  </div>
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      message: "ほげぇぇぇぇぇぇぇぇ",
    },
  });

  var app2 = new Vue({
    el: ".under_hoge",
    data: {
      message: "Hogeeee",
    },
  });
</script>
<!-- Vue -->

ディレクティブを使いこなす

ディレクティブというのはv-bindv-htmlなど、v-で始まる命令文のこと。
v-の後に続く命令によってVueは処理を切り替えている。

v-bindを使いこなす

v-bindでは、htmlタグの属性値(href,class,id...)を動的に変化させることができる。

<!-- HTML -->
<div class='hoge' v-bind:href="vue_href"></div> <!--<div href="http://hogehoge.com/" class="hoge"></div>-->
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      vue_href: "http://hogehoge.com/",
    },
  });
</script>
<!-- Vue -->
  • v-bind:hrefで、href属性を変更することを宣言
  • v-bind:href="vue_href"で、data: {vue_href:を参照し、展開

classを変更する時は、既存classに加えて、新たにclassを追加する。

<!-- HTML -->
<div class='hoge'  v-bind:class="vue_class"></div> <!--<div class="hoge hogeeeee"></div>-->
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      vue_class: "hogeeeee",
    },
  });
</script>
<!-- Vue -->

idを変更する場合は、既存idを上書きしてしまうので注意

v-bindの省略記法

また、v-bindはよく使われるため、このように省略されることがままある。

<div class='hoge'  v-bind:class="vue_class"></div>

これを、v-bind:を省略して:と書くことが可能

<div class='hoge'  :class="vue_class"></div>

v-htmlを使いこなす

v-htmlを使うことで、エスケープ処理が行われなくなる。
任意のHTML要素を動的に出したい場合に役に立つ

<!-- HTML -->
<div>-----------</div>
<div class='hoge' v-html="html_message"></div>
<div>-----------</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    data: {
      html_message: "<h1>ほげええええええ</h1>",
    },
  });
</script>
<!-- Vue -->

出力すると、しっかり<H1>タグが機能していることがわかる
VueArtic_html.png

v-ifを使いこなす

visible = trueであれば<button>タグが生成される

<!-- HTML -->
<div id="hoge">
  <button v-if="visible">ほげボタン</button> <!-- ほげボタン -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      visible: true
    }
  })
</script>
<!-- Vue -->

else文もこのように使うことができる

<!-- HTML -->
<div id="hoge">
  <div>{{now}}</div>
  <button v-if="visible">ほげボタン</button>
  <button v-else="visible">ホゲってないぞ</button> <!-- ホゲってないぞ -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      visible: false
    }
  })
</script>
<!-- Vue -->

v-showを使いこなす

visible = trueであれば<button>タグが生成される

<!-- HTML -->
<div id="hoge">
  <button v-show="visible">ほげボタン</button> <!-- ほげボタン -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      visible: true
    }
  })
</script>
<!-- Vue -->

v-ifとの違いについて

v-ifでは、条件がfalseの場合、タグ自体が生成されないのに対して、
v-showではタグそのものは存在するが見えない(display: none;)となる

したがって、表示切り替えを頻繁に行うならv-show
たまに行う程度ならv-ifという使い分けになると考えられる。

メソッドを使いこなす

<!-- HTML -->
<div class='hoge'>
  {{hoge_func()}} <!-- ほげえええええええ -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    methods: {
      hoge_func: function () {
        return "ほげえええええええ"
      }
    },
  });
</script>
<!-- Vue -->

メソッドに引数をとる

<!-- HTML -->
<div class='hoge'>
  {{hoge_func('ほゲゲゲゲゲゲ')}} <!-- ほゲゲゲゲゲゲ?????? -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: ".hoge",
    methods: {
      hoge_func: function (argument) {
        return argument+'??????'
      }
    },
  });
</script>
<!-- Vue -->

算出プロパティ(computed)を使いこなす

<!-- HTML -->
<div id="hoge">
  {{ hogeReverse }} <!-- ぇぇぇぇぇぇぇぇげほ -->
</div>
<!-- HTML -->

<!-- Vue -->
<script>
  var app = new Vue({
    el: '#hoge',
    data: {
      message: 'ほげぇぇぇぇぇぇぇぇ'
    },
    computed: {
      hogeReverse: function () {
        return this.message.split("").reverse().join(""); //文字列を反転させる
      }
    }
  })
</script>
<!-- Vue -->

ここで、
- thisはappインスタンスを表している(this.hogeReverse = app.hogeReverseと同等)
- このthisは省略できない(message is not defined)

また、
あくまでcomputedで定義されるのはプロパティであって、メソッドではないので引数を取ることはできない

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

props,emit備忘録

簡単なpropsはなんとなく分かっていましたが、親コンポーネント側の配列を子コンポーネント内でfor文で回した時の挙動が難しかったので、
【Vue.js】子から親コンポーネントのデータを更新する方法|たのしいWeb開発
を参考にさせていただき、少しまとめてみました。

93495604a3e649eb038e9a5ffbc255f3.png

1.親コンポーネント側で、配列fluitsを宣言。中には、2つのオブジェクトが入っている。

2.親コンポーネント側で子コンポーネントを表示した際に、for文で回して親コンポーネントの配列fluitsの中身を1つ1つ表示させている。
fluitの中のオブジェクトの要素数に応じて、表示する数は変動する。今回の表示は下記のようになる。

{{fluit.name}}={{subtotalCount}}
<button @click = "increment()">増やす</button>
<button @click = "reduce()">減らす</button>

{{fluit.name}}={{subtotalCount}}
<button @click = "increment()">増やす</button>
<button @click = "reduce()">減らす</button>

3.for文で親コンポーネントのオブジェクトを子コンポーネントに渡すときは、v-bind:【propsで渡す変数名】="配列データのキー"

4.子コンポーネント側で、clicked-increment-button,clicked-reduce-buttonが発火されたら、親コンポーネント側のincrementTotalCount,decrementTotalCount
を呼び出す、ということ。
この流れをemitと言い、子コンポーネントから親コンポーネントに値を渡すときに使用される。
子コンポーネント側でsubtotalCountが増減するのに合わせて、親コンポーネント側のtotalCountも増減する。

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

Notification API + Laravel + Vue.js、 PWAで メッセージ機能を作る 作例編

概要

以前の 製作事例公開内容となりますが
会員メンバー間で、メッセージを送受信できる機能を
Laravel+ Vue でPWA対応、実装しました。
新着の自動更新は、JSタイマで起動し、
Notification API での通知等の仕組みとなります。

参考のコード / GitHub

https://github.com/kuc-arc-f/lara58a_7message

構成

Progressive Web Apps / PWA
Notification API
Laravel 5.8
Vue.js
javascript
nginx
mysql

migrations

https://github.com/kuc-arc-f/lara58a_7message/blob/master/database/migrations/2020_05_16_155945_create_messages_table.php

画像

・通知の画面
ss-msg-notification.png

・受信一覧、
 メールのような、受信、送信タブで切替表示としました

ss-message-receive-0524.png

実装など

・JSタイマー
 定期実行、自分宛の送信メッセージを監視し、
 新着があれば、トリガー発火して。新着通知等の処理を実行します。

 function set_time_text(){
    var data = {
                'user_id': USER_ID,
                'type': 1,
            };           
    axios.post('/api-1234' , data).then(res =>  {
        var item = res.data
        if(item.id != null){
            $("input#time_text").val( item.id );
            $("input#message_title").val( item.title );
        }else{
            $("input#time_text").val( 0 );
        }
console.log( item );
    });  
 }
 set_time_text();
var timer_func = function(){
     set_time_text();
};
var TIMER_SEC = 1000 * 600;
setInterval(timer_func, TIMER_SEC );

・新着の通知

function display_notification(title, body ){
    if (!('Notification' in window)) {//対応してない場合
        alert('未対応のブラウザです');
    }
    else {
        // 許可を求める
        Notification.requestPermission()
        .then((permission) => {
            if (permission === 'granted') {// 許可
                var options ={
                    body: body,
                    icon: 'https://hoge.net/icon.png',                                      
                    tag: ''
                };
                var n = new Notification(title,options);
                console.log(n);
                setTimeout(n.close.bind(n), 5000);
            }
            else if (permission == 'denied') {// 拒否
            }
            else if (permission == 'default') {// 無視
            }
        });
    }  
}


参考のページ

https://knaka0209.hatenablog.com/entry/lara58_26message

.

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

Rails と Vue (Single File Components) 共存

Rails と Vue の共存をしたい。t
できれば以前のコードを活かしたい。
いきなりSPAにするにはサンクコストが大きくて、踏ん切りがつかない。

(編集中)

基本は置き換えていく方向で検討中。

参考

2018年くらいに書かれたものが多い。Railsの遺産を残しながらVueの単一コンポーントの利点を使いたい、しかしSPAには振り切りきれない。

webpack

おススメ派

要は、今後の主流になるのは間違いないので、やっておけ。最初は大変だがあとでいろんなところで役に立つよ。

Sprockets

未検討

検討中

  • Webpacker を使わずに webpack
    • あとから外すもありみたいなので、最初は学習コストを下げるために使う [決定]
  • Sprockets 検討中(よくわかっていない)
    • [使わない] わからない=評価できない=なしで進む
  • Vue をRailsとは別でBuildしてマウントする方式が良さそう

まずは

  • 別のサービスをRails6 Webpacker Vue で実装する
  • SPA にしなくてもいい気はする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails と Vue の共存

Rails と Vue の共存をしたい。
できれば以前のコードを活かしたい。
いきなりSPAにするにはサンクコストが大きくて、踏ん切りがつかない。

(編集中)

参考

2018年くらいに書かれたものが多い。Railsの遺産を残しながらVueの単一コンポーントの利点を使いたい、しかしSPAには振り切りきれない。

webpack

おススメ派

Sprockets

未検討

検討中

  • Webpacker を使わずに webpack
    • あとから外すもありみたいなので、最初は学習コストを下げるために使う [決定]
  • Sprockets 検討中(よくわかっていない)
    • [使わない] わからない=評価できない=なしで進む
  • Vue をRailsとは別でBuildしてマウントする方式が良さそう

まずは

  • 別のサービスをRails6 Webpacker Vue で実装する
  • SPA にしなくてもいい気はする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】半日でできる、ビデオチャット上で動くオリジナルゲームの開発から公開まで。

Vue.jsを使って簡単なゲーム開発し、完成したものをwh.imというゲームのプラットフォームに公開するまでをまとめてみました。
この記事を読めば、Vue.js未経験の方であっても数時間あればオリジナルゲームを開発・公開し、友達と遊べるようになります!
できる限りわかりやすく書いたので長くなりましたが、ぜひ最後まで読んでみてください(記事の完成に3日かかりました? 服装の変化を楽しんでください)。ご質問はコメントまで!

対象読者

  • html / css はある程度わかる方
  • Vue.jsに入門したい人
  • ビデオチャット上でゲームを作ってみたい方

はじめに

wh.im というサービスの開発している@aitaroです。Vue.jsを使い始めてかれこれ1年半になります。今でこそ wh.im を開発できるぐらいの知識がつきましたが(wh.imはVue.jsとそのフレームワークであるNuxt.jsを使っています)、Vue.jsを始めたときは右も左もわからない状態でした。そこで、同じようにVue.js初心者の方に対して、ゲームの作成といった一つのプロダクトの完成とデプロイを目指した入門記事を今回書こうと思いました。wh.im要素が多めはなりますが、Vue.jsの基礎も理解できるようになっているので、是非最後まで走りきってみてください。

また、Vue.js経験者の方にも、wh.imの始め方から公開の仕方までが一通りまとまっているので、ビデオチャットで遊べるでゲームを作ってみたいときは是非参考にしてください。

構成

このエントリは以下の6つのパートと21の章から成ります。

Ⅰ. wh.imとは?

Ⅱ. Vue.jsの環境構築

  • 1. node.jsをインストール
  • 2. npmでvue-cliをインストール
  • 3. Vue.jsのプロジェクトを作る
  • 4. ソースコードを変更してみる。

Ⅲ. wh.imでの開発のための環境設定

  • 5. wh.im上でvueを動かしてみる
  • 6. wh.im上で開発の準備

Ⅳ. ゲームの実装

  • 7. main.jsのでライブラリ読み込み
  • 8. vue.jsのコンポーネントの追加
  • 9. じゃんけん画像の設置
  • 10. グーを選択するメソッドを追加しよう!
  • 11. 選んだ手のstateの登録
  • 12. チョキやパーを選択できるようにしよう!
  • 13. 関数を抽象化しよう!
  • 14. 選択後の画面を作ろう
  • 15. 結果の画面を作ろう
  • 16. 結果の画面に全員の選んだ手を表示してみよう!
  • 17. 最後に見た目を整えよう!

Ⅴ デプロイして公開する

  • 18. githubにコードを上げる
  • 19. netlifyの設定をする
  • 20. wh.imのdevelop画面でゲームの登録をする
  • 21. 遊んで見る!

Ⅵ. まとめ

wh.imとは?

wh.imとは、ビデオチャットしながら遊べるゲームのプラットフォームです。wh.imにアクセスすると、じゃんけんやワードウルフなどのゲームが友達と遊べます。さらに、wh.imの特徴はなんと言ってもオリジナルゲームを投稿できること!自分で作ったゲームを登録すれば、今日中にでもそのゲームで友達と遊ぶことができます。今回はこの wh.im を題材に使って、Vue.jsを用いたゲームを作っていこうと思います!
image.png

wh.imについて詳しくしりたい方は、新型コロナの自宅待機中に、ビデオチャットしながらゲームで遊べるサービスを作った話を是非読んでみてください。

Vue.jsの環境構築

1. node.jsをインストール

まずはMacにnode.jsをインストールしてください。インストールの方法はこちらの記事に簡潔にまとまっています。
Macにnode.jsをインストール

2. npmでvue-cliをインストール

node.jsをインストールしたあなたは、npm使えるようになっているはずです。npmとは、node package managerの略で、JavaScript系のパッケージを管理するためのツールです。
以下の方法で、npmを使ってvue-cliをインストールしましょう。
-gをつけることで、グローバルにインストールされるので、どこからでもvueコマンドを呼び出せるようになります。

$ npm install -g @vue/cli

3. Vue.jsのプロジェクトを作る

ここから、簡単なじゃんけんアプリの開発を通じてVue.jsを使ったゲーム開発を学んでいきましょう。

まずはVue.jsのプロジェクトを作成してみます。今回はじゃんけんゲームを作っていくので、jankenとしますが、好きな名前で大丈夫です。

$ vue create janken

すると、次のような選択画面になります。

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

これはそのままEnterを押してしまいましょう。
最後にこのような画面ができたら成功です!

?  Successfully created project janken.
?  Get started with the following commands:

 $ cd jaknen
 $ npm run serve

作成されたら、画面の指示に従ってそのjankenというディレクトリに移動してみましょう。
この段階で試しに起動してみましょう。

$ cd janken
$ npm run serve

数秒でターミナルがこのような状態になったら成功です。

起動は成功しているので、Chrome等ブラウザのアドレスバーに
http://localhost:8080
と入力してみてください。

このような画面が出てくると思います。
これで環境構築はひとまず終了です!お疲れ様でした?

4. ソースコードを変更してみる。

これで、Vue.jsの起動は完了しました。
Vue.jsを初めて触る人向けに、コードの雰囲気を掴んでもらいます。
まず、vue create で作成されたサンプル画面を見ていきましょう。
このサンプル画面のコードは src > components > HelloWorld.vue にあります。

ではそのうちこの文章を変えてみましょう。

src/components/HelloWorld.vue
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

これを適当に変えてみます。

src/components/HelloWorld.vue
    <p>
      100日目にVue.jsをマスターする俺<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

変えたら、command + S(winはctrl + S)で変更を保存してみましょう。
すると、ブラウザでみると自動で反映されていると思います!

これはVue.jsのうちのWebpackというライブラリで、HotReloadという機能です!変更が保存されると、自動で反映されます!ただ時々バグるので、もしバグったなというときはおとなしくブラウザの画面をリロードしましょう。

wh.imでの開発のための環境設定

5. wh.im上でvueを動かしてみる

wh.im上でVueを動かしてみましょう。
wh.imから遊び場を作成します。

次に、wh.imを開発モードにします。
URLの最後に&develop=trueを追加して、リロードすると、wh.imが開発モードに変わります。

4開発モードになると、右上のボタンからアプリを選択するときに、開発用(port:8080)が出てきます。

この開発用(port:8080)を選択し、プレイすると先程の画面になります。

これで、開発の準備は整いました。

6. wh.im上で開発の準備

では、実際にwh.imを使ったゲームを作っていきましょう。wh.imはVueで開発しやすいように、ライブラリを用意しています。(ライブラリとは拡張機能みたいなもので、追加することでVueでやれることが広がります)
一旦、ターミナルにいき、 command + C (windows の場合は ctrl + C)でサーバーを止めましょう。
その後、次のコマンドを打ち込んでライブラリを追加します。

$ npm install whim-client-vue

これで、ライブラリが入りました!
package.jsonを開いてみると、追加されているのがわかります。(バージョンは多少ことなる場合があります。)

package.json
{
  "name": "janken",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "whim-client-vue": "^1.1.4" // ←ここ
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-plugin-vuex": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  }
}

そして次に、Vueの設定を変更します。janken(ルートディレクトリ)の直下にvue.config.jsを作り、以下のコードを加えましょう。
これはクロスドメインでアプリを呼び出すときに、host名を明示的にlocalhostにするためです。

vue.config.js
module.exports = {
  devServer: {
    host: "localhost"
  },
};

これでもう一度、Vueを起動します。

$ npm run serve

vueを再起動したときは、wh.imの方でも一回、ゲームを終了して再度選択する必要があります。

ゲームの実装

7. main.jsのでライブラリ読み込み

ここからは実際にコードを書いていきます。
まず、src/main.jsを開き、whim-client-vueを使うように設定します。
3,4行目を増やしました。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import whimClientVue from "whim-client-vue";
Vue.use(whimClientVue);

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

これは、whim-client-vueのライブラリを呼び出すコードです。

src/main.js
import whimClientVue from "whim-client-vue";

Vue.useを使うことで、さきほど呼び出した、whim-client-vueのライブラリを、Vueに登録します。

src/main.js
Vue.use(whimClientVue);

8. vue.jsのコンポーネントの追加

次に、App.vueを編集します。これは、ページにアクセスすると最初に表示される画面です。
これをデフォルト画面から変更してみます。変更点は、divタグの中身を消したこと、HelloWorldコンポーネントは今回使わないので、componentsを消したことです。

src/App.vue
<template>
  <div id="app">
    Player: {{ $whim.accessUser.name }}
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

すると、自分の名前が表示されていると思います。
template内の {{ }} は、そこの部分を式評価するという意味です。
今回は、そこに $whim.accessUser.name と記述することで、アクセスしてるユーザーの名前がとれました。
ここで言う、アクセスしているユーザーとはそのゲームをプレイしている人のことです。wh.imはビデオチャットサービスなので、複数人でゲームをプレイします。そのとき、このブラウザからアクセスしている人はだれかということをゲーム側で把握しなければなりません。
イメージ図

実際に確認してみましょう。プライベートブラウザや、違うブラウザで、wh.imの同じルームにアクセスします。すると今度は、 Player:モナリザ と表示されました。

このようにアクセスする人によって、表示を変えたい場合は、accessUserを使うことで可能になります。

9. じゃんけん画像の設置

次に、ゲーム上にじゃんけんの選択肢を配置しましょう。
まず、じゃんけんの画像を用意します。
私は素材ライブラリーさんの画像を使わせていただきました。

ダウンロードした画像を、 src/assetsの中に配置します。
ファイル構成は以下のようになると思います。

janken
└── assets
    ├── paper.png
    ├── rock.png
    └── scissors.png

そしてこれを先程のApp.vue画面に配置します。
先程の、Player: {{ $whim.accessPlayer.name }}を下のように書き換えてください。

src/App.vue
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

そして、wh.imの画面を確認します。すると、以下のような選択画面が現れていると思います!

10. グーを選択するメソッドを追加しよう!

次に、グーチョキパーを選択できるようにします。
目標は、画像をクリックすると文字が選択済み!に変わって、データベースに選択した手が書き込まれることです。
ではまず、データベースに書き込む処理をしましょう。

グーから行きます。
グーの画像に@click="selectRock"を付け足します。以下のような感じです。これはclickしたときにselectRockという関数を実行するという意味です。

src/App.vue
<img
  src="@/assets/rock.png"
  width="150"
  height="150"
  @click="selectRock"
/>

そしてselectRock関数をvueに登録します。
を以下のように編集します。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
    }
  }
}
</script>

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ここで出てきたconsole.logはブラウザのconsole画面にログを表示する命令です。
これで、グーを押したときの挙動を確認しましょう。
wh.imの画面に戻って、Chromeのデベロッパーコンソールを開きます。macの方は command + option + i で開きます。 そして、その状態でグーをクリックします。するとコンソール画面にselectされた!と表示されるはずです。(以下の画像を参照)

このように@clickでselectRock関数が実行されたのがわかると思います。

11. 選んだ手のstateの登録

次にデータベースに登録しましょう。selectメソッドを変更していきます。
データーベースはwh.im上ではstateと呼ばれます。また、このstateはデーターベースといいつつ、いわゆるRDBではなくJSON型で保存されます。(そしてこのデーターベースはこのアプリを立ち上げているブラウザ間で同期されます。)
stateを変更するときは、$whim.assignState関数を使います。
今回はアクセスしてるユーザーがグーを出したことを保存したいので、stateに[this.$whim.accessUser.id]: "rock"を書き込みます。

src/App.vue
selectRock() {
  // ここに処理を書いていく
  console.log('selectされた!')
  this.$whim.assignState({
    [this.$whim.accessUser.id]: "rock"
  })
}

動作確認をしてみましょう。グーをクリックしてみます。するとstateにグーが登録されます。wh.imの開発モードにはこのstateを確認する機能があります。右上のメニューボタンから、SHOW APP STATE を選んでください。
すると次にような画面が出てきます。

このObjectがstateです。
今回はstateにoNyPTFZuMbOOZruCR4UMSsP9zRu2:"rock"が登録されていることがわかりました。この、oNyPTFZuMbOOZruCR4UMSsP9zRu2はユーザーidです。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

12. チョキやパーを選択できるようにしよう!

今のところまだ、グーしか選択できません。チョキやパーも選択できるようにしましょう。これは今までの要領でいくと簡単です。
まずselect関数を2こ増やします。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
    selectScissors() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "scissors"
      })
    },    
    selectPaper() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "paper"
      })
    }
  }
}
</script>

これで、関数が3つに増えました。これをそれぞれの画像のclickイベントに追加していきます。

src/App.vue
<div>
    <img
      src="@/assets/rock.png"
      width="150"
      height="150"
      @click="selectRock"
    />
    <img
      src="@/assets/scissors.png"
      width="150"
      height="150"
      @click="selectScissors"
    />
    <img
      src="@/assets/paper.png"
      width="150"
      height="150"
      @click="selectPaper"
    />
</div>

これで、3つともクリックしたらstateに反映されるようになりました!
(注意事項:動作確認は必ずSTATE確認画面を閉じてから行ってください。)

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="selectScissors"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="selectPaper"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
    selectScissors() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "scissors"
      })
    },    
    selectPaper() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "paper"
      })
    }
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

13. 関数を抽象化しよう!

先程のようにグー・チョキ・パーselect関数を3つ作ってもいいですが、これらの関数には似たような処理が多いです。こういうときは一つの関数にまとめます。これはプログラミングにおいて重要な抽象化の概念です。処理の内容を日本語化するとわかりやすいと思います。

まとめる前

  • selectRock関数: グーをstateに登録する。
  • selectScissors関数: チョキをstateに登録する。
  • selectPaper関数: パーをstateに登録する。

まとめた後

  • select関数: 出した手をstateに登録する。

グー、チョキ、パーが出した手に変わったこと以外は同じです。けどこのままだと、出した手ってなんやねん!ってなると思います。ここで関数の引数機能を使います。引数は関数名(引数名)みたいな感じで使います。
では早速コードを書いてみましょう。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    }
  }
}
</script>

そして、この関数の呼び出し側も変更します。

src/App.vue
  <div>
    <img
      src="@/assets/rock.png"
      width="150"
      height="150"
      @click="select('rock')"
    />
    <img
      src="@/assets/scissors.png"
      width="150"
      height="150"
      @click="select('scissors')"
    />
    <img
      src="@/assets/paper.png"
      width="150"
      height="150"
      @click="select('paper')"
    />
  </div>

これで、先程と同じ用に動くはずです。確認してみましょう。

お気づきの人もいるかも知れませんが、このステップでは機能としてなにも進んでいません。コードの書き方を変えただけです。この機能を変えずによりよいコードにすることをリファクタリングといいます。「機能が増えないなら時間の無駄やんw」という人もいるかも知れませんが、どんどん大きなプロジェクトになっていくと、それをメンテナンスするためにはコードの品質が重要になっていきます。そして、「良いコード」を書くのは一朝一夕でできることでもないので、普段から意識的に「良いコード」を書くことを意識することがおすすめです。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

14. 選択後の画面を作ろう

今のままですと、何回もじゃんけんが選択できてしまいます。選択が終わったら、選択済みにかえましょう。
選択が終わったかどうかは、stateを見たらわかります。stateが空なら未選択ですし、stateに自分のaccessUserIdがあれば、選択済みです。これは$whim.state[$whim.accessUser.id]で確認できます。また、vue.jsのテンプレートではv-if, v-else を使って条件分岐ができます。確認してみましょう。

src/App.vue
<template>
  <div id="app">
    <div v-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
(以下略)

$whim.stateというのは以下のようなObject型です。(SHOW APP STATE で確認できると思います。)

{
    jibunNoID: "paper"
}

ここでいうjibunNoID$whim.accessUser.idで取れる値です。(これはscriptの方で書いたthis.$whim.accessUser.idと同じですが、vueのtemplateではthisは省略します。)
なので、すでにpaperを選んでいる場合は$whim.state[$whim.accessUser.id]の値はpaperになりますし、何も選んでいない場合はundefinedになります。これを利用して、v-ifで条件分岐をしています。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div v-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

15. 結果の画面を作ろう

最後に結果の画面です。まずは、二人プレイでどのようにstateが登録されているか確認します。

一人目はpaperを選んでいて、二人目はrockを選んでいます。これを画面に表示します。

まずはじめに、全員がじゃんけんの手をすでに選んだかどうかを確認します。javascriptではfor文でループを表すことができます。usersを一人ずつ、stateに手が登録されているかを確認します。for文の詳しい説明はこちらなどを参考にしてください。

src/App.vue
let result = true
for (let i = 0; i < this.$whim.users.length; i++ ) {
    if(!this.$whim.state[this.$whim.users[i].id]){
        result = false
    }  
}
return result

ここで、this.$whim.usersは今部屋にいるuser全員です。
まず、resultにtrueを設定しておきます。そして、users一人ずつ、手がセットしているかをthis.$whim.state[this.$whim.users[i].id]で確認します。
ここでセットされていなかったら、resultをfalseに変更します。こうすることで、ひとりでもじゃんけんの手を選んでなかったらresultがfalseになります。これを関数として実装し、またtemplate側でv-ifを使います。(もともと、v-ifだったところはv-else-ifに変えました)

この処理をisEveryoneSelectとして追加したコードは次のようになります。

src/App.vue
<template>
  <div id="app">
    <div v-if="isEveryoneSelect">
      <h2>
        全員が選択を終わりました。
      </h2>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ここで使用したcomputedとは、値を動的に算出するときに使うものです。今回ですと、usersが変わりうるので、methodsよりもcomputedのほうが適切です。詳しくはこの記事を読むといいでしょう。
では実際に確認しましょう。一人プレイにときはじゃんけんの手を選ぶと、すぐに全員が選択を終わりましたと出ると思いますが、2人プレイのときは、選択画面→グーを選択済みです。→全員が選択を終わりました。と順番に変更すると思います。

16. 結果の画面に全員の選んだ手を表示してみよう!

全員の選択が終わったら結果を表示します。ここではVue.jsのv-forという機能を使います。v-forはtemplate内でループをするという機能です。今回の場合、全員分の結果を表示させないと行けないので、usersに対してv-forでループさせます。

src/App.vue
<div v-if="isEveryoneSelect">
  <h2 v-for="user in $whim.users" :key="user.id">
    {{user.name}}の出した手は、{{$whim.state[user.id]}}です。
  </h2>
</div>

最終的に以下のような結果画面になれば成功です。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div v-if="isEveryoneSelect">
      <h2 v-for="user in $whim.users" :key="user.id">
        {{user.name}}の出した手は、{{$whim.state[user.id]}}です。
      </h2>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

17. 最後に見た目を整えよう!

最後に画像やcssを使って、見た目を整えます。Vue.jsでは srcの前でコロンをつけることで、動的に値を設定します。

全体のコードはこちら

src/App.vue
<template>
  <div id="app">
    <!-- class="result"を追記します。 -->
    <div v-if="isEveryoneSelect"  class="result">
      <div v-for="user in $whim.users" :key="user.id">
        <!-- じゃんけんの画像(rock.pngなど)を出した手に応じて表示します。 -->
        <img
          :src="require('@/assets/' + $whim.state[user.id] + '.png')"
          width="150"
          height="150"
        />
        <h2>{{user.name}}</h2>
      </div>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
/* resultクラスに対応するcssを追記します。 */
.result {
  display: flex;
  justify-content: center;
}
</style>

画像は、この行の$whim.state[user.id]('rock', 'cissors', 'paper'のいずれか)が動的に表示されます。

:src="require('@/assets/' + $whim.state[user.id] + '.png')"

ここまでで、実装は終わりです。お疲れ様でした?

デプロイして公開する

18. githubにコードを上げる

ではここから、このゲームの公開方法に移ろうと思います。今のところ、このゲームはローカルで動いてるだけなので、なんらかのサーバにホスティングする必要があります。まず1ステップ目として、今作ったゲームをgithubに上げましょう。具体的は方法はここらへんの記事が参考になると思います。

まずは、新しいリポジトリを作ります。

その後、codeをgithubに上げます。
以下のようなコマンドになるともいます。

$ git add .
$ git commit -m "janken完成"
$ git remote add origin git@github.com:username/janken.git
$ git push -u origin master

最終的にこのような感じで上げれたら完成です。

19. netlifyの設定をする

次にnetlifyの設定をします。こちらの記事が参考になりますが、この記事でも解説しようと思います。Netlifyは静的なサイトを無料でホスティングしてくれるサービスです。Netlifyのサイトよりアカウントを作成します。githubのアカウントに関連付けて作成します。

この画面に来たら、 New site from Git を選択します。

GitHubを選択します

自分のレポジトリ一覧が出てくるので、先程作ったレポジトリを選択します。
最後にデプロイ設定を以下のようにします。

Build commandはnpm run build
Publish directoryはdistに設定します。

設定が終われば Deploy site を押しましょう!

この画面になれば成功です!

表示されているURLをコピーしておきましょう。

20. wh.imのdevelop画面でゲームの登録をする

wh.imの開発者用画面でアプリを登録します。
アクセスすると、googleでのログイン画面になると思います。(これは、gmailで開発者の認証のためです。)

ここで自分のgoogleアカウントで登録します。
すると次の画面に遷移します。これが開発者アプリ登録画面です。

右上のボタンから、新規アプリを登録します。
その後、必要事項を記入していきます。
ゲームのhostのURLは先程コピーしたURLを記入します。
最後のチェックボックスは色々な人が遊べるように公開したい場合は、チェックをします。(個人的に遊ぶだけの場合はチェックを入れなくて大丈夫です。)

saveを押すと公開完了です!

21. 遊んで見る!

早速遊んでみましょう。まず、IDをコピーします。そして、wh.imに移動します。(今回は開発モードにする必要はありません!)
アプリ選択画面を開き、コピーしたIDを入力しましょう。

これで選べます!
友達と遊んでみましょう!

まとめ

長い記事になりましたが、これでVue.js未経験者の方でもwh.imを使ったゲーム公開までの流れは一通り理解できたかと思います。Vue.jsをもっと学んで行くと、ワードウルフといった複雑なゲームも作れるようになるので、是非挑戦して見てください!

最後に、これらの情報が体系的にまとめた(つもりの)、開発者用ドキュメントも用意しているので、参考にしてみてください。

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

サイトに3Dメニューページを作ってみる(A-frame + HTML Shader + html2canvas)

この記事でやっていること・できていないこと

  • できたこと
    • A-Frameを使ってサイトのメニューを3D化した。
    • A-Frame内で2次元の普通の画像要素を3次元で使うタイミングでできないことがあったので、HTML Shader + html2canvasを使った。
  • できてないこと

サンプル

image.png

このように3次元空間内にboxを置き、その上にhtmlで作った画像付き要素をのせています。
またクリックをすると、遷移対象のページに飛びます。

https://simasima.work/3d-menu.html

↑ こちらから試すことができます。

A-Frame

https://aframe.io/
ほんの数行のhtmlの記述だけで3D画像と空間を表現でき、とても便利です。
例えば下記サンプルコードですぐに複数の図形と3次元空間を表現できます。

aframe-sample.html
<html>
  <head>
    <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>
      <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>
  </body>
</html>

image.png

HTML Shader + html2canvas について

A-Frameの3次元空間内に2次元のDOM要素を加える場合に、どうしてもうまくいかないケースがありました。
本記事では、下記が大変参考になりました。
A-FrameとHTML Shaderで美しい日本語テキストを表示する方法

この記事によると、
HTML Shaderは、その名の通り2次元のDOM要素をマテリアルとして、3次元のA-Frameオブジェクト上に貼り付けることができるコンポーネントで、それは html2canvas というライブラリ上に成り立っているということです。

以降で説明していきますが、そのため上記2つ(HTML Shader + html2canvas)を使えるようにする必要があります。

HTML Shader

html2canvas

まず、HTML Shader と html2canvasを読み込む

HTML Shader

https://github.com/mayognaise/aframe-html-shader
このページによると、npmでのインストールに加えて、ブラウザファイルも用意されているようなので手ごろに使う場合は下記を

内にかけばOKです。
  <script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
  <script src="https://unpkg.com/aframe-html-shader@0.2.0/dist/aframe-html-shader.min.js"></script>

html2canvas

私が少し調べた限りでは、ブラウザファイルは用意されていないようなので、
http://html2canvas.hertzen.com/
このページにいき、html2canvas.min.jsを押しその中身をコピーして、html2canvas.min.js というファイル名で保存します。
image.png

今回のフォルダ構成

images
 └── ....
js
 └── html2canvas.min.js
style
 └── style.css
3d-menu.html

ソースコード

3d-menu.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <title>Hello!!!!</title>
  <meta name="description" content="Hello, WebVR! • A-Frame">
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>

  <script src="https://unpkg.com/aframe-html-shader@0.2.0/dist/aframe-html-shader.min.js"></script>
  <script src="js/html2canvas.min.js"></script>

  <link rel="stylesheet" href="style/style.css">  
</head>

<body>

    <div id="loader">

        <p>loading...</p>
    </div>
    <div id="target1" class="target">
        <img src="imgs/face.png" alt="A-Frame">
        <div class="cf"><h3>感情分析</h3></div>
        <p class="detail">クリックでページに遷移 </p>
    </div>
    <div id="target2" class="target">
        <img v-if="url" v-bind:src="url" alt="A-Frame">
        <div class="cf"><h3>犬の画像を表示</h3><p>立方体クリックで犬の画像を表示します(動かず)</p></div>
        <!-- <p class="detail">640px × 400px</p> -->
    </div>
    <div id="target3" class="target">
        <img src="imgs/gotop.png" alt="A-Frame">
        <div class="cf"><h3>TOPへ</h3></div>
        <p class="detail">クリックでページに遷移 </p>
    </div>
    <div id="target4" class="target">
        <img src="imgs/orc.png" alt="A-Frame">
        <div class="cf"><h3>OCR分析</h3></div>
        <p class="detail">クリックでページに遷移</p>
    </div>
    <div id="target5" class="target">
        <img src="imgs/dog.jpg" alt="A-Frame">
        <div class="cf"><h3>犬画像ページ</h3></div>
        <p class="detail">クリックでページに遷移</p>
    </div>
    <div id="target6" class="target">
        <img src="imgs/jaga.png" alt="A-Frame">
        <div class="cf"><h3>予備</h3></div>

    </div>


<div>
    <a-scene>
        <a-entity  id="aframeApp" >
            <a-box id="areabox1" position="-10  6 -15" width="16" height="10" rotation="0 30 0" material="shader:html;target: #target1;" @click="handlerClick"></a-box>
            <a-box id="areabox2" position=" 0  6 30" width="16" height="10" rotation="0 15 0" material="shader:html;target: #target2;"></a-box>
            <a-box id="areabox3" position=" 18  6 0" width="16" height="10" rotation="0 75 0" material="shader:html;target: #target3;" @click="handlerClick"></a-box>
            <a-box id="areabox4" position="-10 -6 -15" width="16" height="10" rotation="0 30 0" material="shader:html;target: #target4;" @click="handlerClick"></a-box>
            <a-box id="areabox5" position=" 0 -6 30" width="16" height="10" rotation="0 15 0" material="shader:html;target: #target5;" @click="handlerClick"></a-box>
            <a-box id="areabox6" position=" 18 -6 0" width="16" height="10" rotation="0 75 0" material="shader:html;target: #target6;" @click="handlerClick"></a-box>

            <a-box position="-2 0 5" rotation="0 45 0" color="#4CC3D9" @click="getdog"></a-box>
            <a-plane position=" 18 -6 0" width="16" height="10" @click="getdog"></a-plane>
            <a-sky src="https://images.unsplash.com/photo-1557971370-e7298ee473fb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80" rotation="0 45 0"></a-sky>
        </a-entity>

        <a-entity camera wasd-controls look-controls position="-8 0 8"></a-entity>

        <a-entity>
            <a-camera>
              <a-cursor></a-cursor>
            </a-camera>
        </a-entity>

    </a-scene>
</div>


    <script src="https://unpkg.com/vue@latest/dist/vue.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

    <script>
        // vue.jsの記載

        const app = new Vue({
          el: '#aframeApp',
          data: {
            url:"imgs/logo.png" 
          },
          methods: {
              getdog:async function(){
                    const URL = 'https://dog.ceo/api/breeds/image/random';
                    const response = await axios.get(URL);

                    this.message = response.data;
                    this.url = response.data.message;

                    console.log(this.url);
              }



            ,handlerClick: function (event) {
              console.log('handlerClick');
              console.log(event.target.id);
              const boxid = event.target.id;

              if( boxid == 'areabox1' ){
                location.href = 'https://simasima.work/contents/face-emotion.html';
              } else if( boxid == 'areabox4' ){
                this.skyboxSrc = 'https://simasima.work/contents/ocr-read.html';
              } else if( boxid == 'areabox3' ){
                this.skyboxSrc = 'https://simasima.work/contents/dog.html';
              }else if( boxid == 'areabox5' ){
                this.skyboxSrc = 'https://simasima.work/';
              }


            }
            ,
            handlerMouseEnter: function (event) {
              console.log('handlerMouseEnter');
              console.log(event);
              event.target.setAttribute('color', 'blue');
            }
            ,
            handlerMouseLeave: function (event) {
              console.log('handlerMouseLeave');
              console.log(event);
              event.target.setAttribute('color', 'red');
            }
          }
          ,
          mounted() {
            console.log('mounted');
          }
        })

      </script>

</body>

<script>
    var scene = document.querySelector('a-scene');
    var run = function () {
        document.getElementById("loader").classList.add("hidden");
    }

    if (scene.hasLoaded) {
      run();
    } else {
      scene.addEventListener('loaded', run);
    }
</script>




</html>

スタイルシート

style.css
* {
    margin: 0;
    padding: 0;
}

#loader {
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 100000000000;
    background-color: #333;
    color: #fff;
}

#loader>p {
    position: absolute;
    top: 50%;
    margin-top: -0.5em;
    width: 100%;
    text-align: center;
    font-size: 200%;
    font-weight: bold;
}

#loader.hidden {
    display: none;
}

.target {
    position: absolute;
    width: 1600px;
    height: 1000px;
    font-size: 500%;
    background-color: #FFF;
    display: hidden;
    /*z-index: 1;*/
}

.target>img {
    float: left;
    display: block;
    width: 32%;
    padding: 4% 0 4% 4%;
}

.target>div {
    margin-left: 36%;
    width: 52%;
    padding: 4%;
}

.target>.detail {
    padding: 3% 0;
    text-align: center;
    width: 92%;
    color: #fff;
    background-color: rgba(1, 1, 1, 0.6);
    margin: 4%;
}

ちょこっと解説・ポイント

今回はこのページを大変参考にさせて頂きました!
http://vr-lab.voyagegroup.com/entry/2016/11/16/122115

その中で、開発に慣れてなくてつまずいたことを記載しておきます。(初心者な内容もあり)

1. エディターのLive server的な機能でみると画像が崩れる時がある

「コメント部分消しただけなのに全体的になぜか崩れたぞ??」
→ これキャッシュが残ってるからでした。スーパーリロードしましょう。

// Mac版
Cmd + Shift + R

// Windows版
Shift + F5

2. 3次元空間ないのカーソルが出てこない

これを加えましょう。

       <a-entity>
            <a-camera>
              <a-cursor></a-cursor>
            </a-camera>
        </a-entity>

3. カーソルを追加したら達が出なくなった

要素は生き延びていたのですが、要素だけ軒並み非表示になってしまいました。。
おそらく奥行きの問題かと考え、をに変更しました。

4. 角度や場所の指定が難しい

ここは触りながら慣れるしかないですね。。
私もかなり苦労しました。

おわりに

A-Frameはすごいです。
3D空間を作ることに関しては、本当に何も考えることなくできました。
いろんなものが身近になってきていますね。

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