20191215のvue.jsに関する記事は24件です。

kusoなusoアプリを作ってみた

嘘を嘘と見抜けないと生きていくのは難しい

どうやら人間は、嘘をつくといろいろな身体的特徴が現れてしまうらしい。
そんないくつかの身体的特徴を、下記のように測定できるWebアプリを作ってみた。

心拍数

嘘をつくとき人は、緊張のため心拍数が高まったり、逆に一定の落ち込みがあるものらしい。

そこで心拍数を測定するために、fitbit SDKを使ってみた。
fitbitはJavaScriptでアプリを開発でき、fitbitアプリからスマホ上のfitbitアプリ(Companion機能)を経由して自由にインターネットアクセスもできる。

fitbitでとった心拍数をどうWebアプリに送るか。
前述のCompanion機能を経由して、Firebase Cloud FunctionsからRealtime Databaseに値を格納。それをWebアプリに同期するように組んでみた。

fitbit.png

結果、特段の工夫もなくなかなかの追従性を実現でき、1秒くらいの遅れでWebアプリ側に反映できるようになった。Firebase有能。

一般的な安定時の心拍数を超えて、90オーバーになる頻度を測定し、嘘の度合いとしてみた。

目線

嘘をつくとき人は、どうしても相手から目線をそらしてしまうものらしい。

ここで必要になるのは顔認識の機能。最初はiOSやAndroidのネイティブアプリから試してみていたが、紆余曲折のすえWebアプリで。clmtrackrを使って実現することにした。

認識できるとこのように。顔の主要な特徴も抽出してくれる。
68747470733a2f2f617564756e6f2e6769746875622e696f2f636c6d747261636b722f6578616d706c65732f6d656469612f636c6d747261636b725f30332e6a7067.jpg

目線については、目の幅(端から端までの距離)のうち、瞳の位置がセンターから大きくズレたタイミングをとるようにした。右目を例にとると、図でいうp23とp25間の距離を求め、p27の位置をチェックしている。
68747470733a2f2f617564756e6f2e6769746875622e696f2f636c6d747261636b722f6578616d706c65732f6d656469612f666163656d6f64656c5f6e756d626572696e675f6e65775f736d616c6c2e706e67.png

まばたき

嘘をつくとき人は、まばたきの回数が増えたり、またはほとんどまばたきをしなくなったりするらしい。

こちらも、上で使ったclmtrackrによる認識結果から測定している。
上下のまぶたの距離が近くなるタイミングをとるが、さらに一定時間検出をスキップするなど自然なまばたきをカウントできるよう細かい調整を入れている。

正常なまばたきの回数が1分間に20回程度といわれているので、それを大きく上回るか下回ると、嘘の可能性ありと判定している。または、瞬間的にやたらまばたきするタイミングなどを検出できるようにしても良いかも。

話すスピード

嘘をつくとき人は、説明過多になることで話すスピードが速くなるか、または極端に口数が落ちるらしい。

こんどは音声認識だ。
IBM WatsonのSpeech to Textを利用した。最近はJavaScriptからでもリアルタイムの音声認識ができるんだよね。凄い。

watson-speechパッケージを使えば、マイクの起動から音声を逐次クラウドに送り結果を得るところまで数行のコードで実現できた。

ここで得られたテキストの文字数をカウントすることで、話すスピードを測定している。
1分あたり300文字が理想といわれているようなので、そこから大きく増える・または減る場合、嘘の可能性が高いとしている。

単語の繰り返し

嘘をつくとき人は、説明過多になることにより、繰り返し同じ単語を話す傾向もあるらしい。

上で得られた音声認識結果のテキストを、さらにYahoo!の形態素解析 API に送り、単語に分解して測定する。
このAPIでは分解した単語の種類(名詞・動詞・接続詞など)も分かるので、名詞または動詞の繰り返される回数をカウントしている。

speech.png

嘘を嘘と見抜かれると生きていくのはつらい

と、ここまでいくつかの要素を組み合わせて嘘を嘘と判定できる仕組みを作ってみた。

これをどう活かすべきか。
考えるまでもないことだが、これを使って徹底的に嘘のトレーニングを積み、絶対にばれない嘘をつけるようになることである。

そういうことでこのUSOTOREというWebアプリ。
出されたお題に対して30秒で嘘の申し開きをすることで、その間の嘘ぐあいを測定し、嘘のPDCAを高速に回すことができる。
https://lietrain.z11.web.core.windows.net/

TOP.png
ODAI .png
train.png

fitbitのマルチユーザー対応が未実装なのでいったん仮の値表示になるのだが、他はだいたい動いているハズ。fitbitはなにも身体のトレーニングだけに使うものではない、ということも証明できたのではないかと。

あざむく
たばかる
煙に巻く

世界平和のためにも、よい嘘を。

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

DashのSnippet機能を活用したらNuxt.jsの開発がめちゃめちゃ捗った【Nuxt.js × Vuetify】

Snippet機能を活用する!

みなさん、こんにちは。どんぶラッコです。
普段Nuxt.js + Vuetify を使って開発を進めることが多いのですが、「あれ、あの書き方ってどう書くんだっけ...」となる事、ありませんか?

そこで、DashというアプリのSnippet機能を使ってNuxt開発用のリファレンスを作ったところ、作業が非常に捗るようになりました。

image.png
https://kapeli.com/dash

Snippet機能いうのは、つまりエイリアスを作ることができる機能です。

image.png

このような設定をした後、;nuxt-startと入力をすると...

Image from Gyazo

なんと!書き換えをしてくれる機能です。

今回は、私が登録しているスニペットをご紹介したいと思います。

前提条件

スニペットの名前についてはどのように登録しても問題ありませんが、私は
;[ライブラリ名 or フレームワーク名]-[メソッド]
という形式で登録をしています。

今回はNuxt.jsのアドベントカレンダーですが、せっかくなのでVuetifyのエイリアスについても合わせて紹介します♪

また、私はyarnで管理をしているため、yarnでスニペットを作成しています。

;nuxt-***

;nuxt-start or ;nuxt-create

npx create-nuxt-app [appName]

;nuxt-template or ;nuxt-component

<template>
  <v-row>
    test
  </v-row>
</template>
<script>
export default {
  components: {

  },
  props: {

  },
  data () {
    return {

    }
  },
  computed: {

  },
  methods: {

  }
}
</script>

コンポーネントを作成する際のテンプレートです。

;nuxt-typescript

yarn add --dev @nuxt/typescript-build && \
echo '1. `nuxt.config.js` に `buildModules: ['@nuxt/typescript-build']` を追加' && \ 
echo '2. `tssonfig.json` を作り、`;nuxt-tsconfig` を実行'

TypeScriptの初期設定についてもスニペットを作成しています。
https://typescript.nuxtjs.org/ja/ の情報を参考にして作成していますが、手順が定期的に更新されるので都度確認した方が良いです。

また、@nuxt/typescript-buildのインストールが成功した場合、echoでその後の手順を表示させるようにしています。

;nuxt-tsconfig

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

;nuxt-v-model or ;vue-v-model

// v-model="hoge"
v-bind:value="hoge"
v-on:input="hoge = $event.target.value"

v-modelがなんの糖衣構文であるかを失念してしまうことが多いので、登録しておくとリファレンスとしても便利です。正確には;vueですが、Nuxtを用いて開発をすることが大半なので;nuxtの接頭辞でも登録をしてあります。

;vuetify-***

以前Vuetifyを使っているときによく見に行くページと情報
というQiita記事を投稿しましたが、ここに記載したものを一通り登録しています。

;vuetify-layout

<v-container>
  <v-row>
    <v-col>
    </v-col>
  </v-row>
</v-container>

;vuetify-css-weight

font-weight-bold

;vuetify-css-color

例として、赤文字で、暗さ4を設定した時のCSSを登録しています

red--text text--darken-4

他にも"これもあったら便利じゃない?", "こうやって登録した方がいいんじゃない?"というものがあったら、是非コメントで教えてください!

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

Vue.js / Web Speech API で作る、 PWA対応 英単語学習ソフト

この記事は「PWA Advent Calendar 2019」の18日目の記事です。

今年の春、Progressive Web Apps や Firebase の練習がてら、英単語学習ソフトを開発しました。
(が、そのまま放置していた)

作りっぱなしももったいないので、アドベントカレンダーに乗じてご紹介します。

以下のような特徴があります。

  • Vue.js を利用したMPA(Multi-page Application)
  • Progressive Web Apps 対応。Windows10/スマホにインストールして、オフラインで動作。
  • 英単語の発音をクリックして確認できる (Web Speech API 利用)
  • Firebase のHosting機能を利用して公開
  • 選択肢と回答をランダムに生成。英単語アプリにありがちな「同じ選択肢と回答が繰り返され、出題パターンを覚えてしまう」ことがないようにした。(800の4乗x10問で、組み合わせは4兆通りぐらい?)

be800_1.png

be800_2.png

開発中のメモをもとに、いくつか備忘録を記述します。

Web Speech API による英語音声の確認

アプリを起動すると、英単語と4つの選択肢が表示されます。英単語をクリック・タップすると、英語の発音を確認できます。
Speech Synthesis API という、Web Speech APIの音声合成機能を利用しました。

          pronounce: function () {

            // confirm English word's pronounciation

            let u = new SpeechSynthesisUtterance();
            u.lang = 'en-US';
            u.text = document.getElementById('englishWord').innerHTML;
            u.volume = "1";
            speechSynthesis.speak(u);

          }

※Speech Synthesis API の使い方については拙稿「Web Speech API を 利用して 英単語の音声確認をするアプリを作る」にまとめました。

Service Worker によるデータキャッシュ

html・CSS・効果音などのアセットファイル類を、Service Worker でまとめてキャッシュしています。

バージョンをキャッシュのキーとして登録。バージョン情報に更新があった場合、キャッシュパージ後、ファイルをキャッシュしなおすようにしました。

const CACHE_NAME = `BasicEnglish800-${version}`;

---- 中略 ----

// Service Worker へファイルをインストール

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function (cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

// リクエストされたファイルが Service Worker にキャッシュされている場合
// キャッシュからレスポンスを返す

self.addEventListener('fetch', function (event) {
  if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin')
    return;
  event.respondWith(
    caches.match(event.request)
      .then(function (response) {
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
      )
  );
});

// Cache Storage にキャッシュされているサービスワーカーのkeyに変更があった場合
// 新バージョンをインストール後、旧バージョンのキャッシュを削除する
// (このファイルでは CACHE_NAME をkeyの値とみなし、変更を検知している)

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!CACHE_NAME.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log(CACHE_NAME + "activated");
    })
  );
});

※Service Workerの使い方や詳細については拙稿「Progressive Web Apps (PWA) 学習者のメモ その1  (Service Worker)」にまとめました。

Vue.js で問題を生成、回答記録を追跡

Vue.js を利用して、回答数を記録。
最初に10問分の英単語・選択肢・回答を、ランダムに生成。
配列に単語・回答・選択肢のデータを格納。
問題を解いて「次へ」をクリック・タップすることで、配列を切り替え、次の問題を表示。
1問解くたびに、次の問題へ切り替え。

10問終了後に正誤のデータを確認し、回答を記録するようにしました。

      const vm = new Vue({
        el: '#el',
        data() {
          return {
            arr: answerList,
            count: 0,
            choice: quizList,
            result: [],
          }
        },
        methods: {
          check: function (event) {

            // check user's answer each by each

            let target = document.getElementById("answerOptions");

            target.setAttribute('style', 'pointer-events: none;');
            let rightAnswer = this.arr[this.count].Japanese;
            let chosenAnswer = event.target.innerHTML;

            if (rightAnswer === chosenAnswer) {

              correctSound.play();
              message.innerHTML = "正解!";
              addAnswer("");

            } else {

              wrongSound.play();
              message.innerHTML = "残念!";
              addAnswer("×");

            }

            this.count === 9 ? complete() : unComplete();
          },

※Vue.js を利用した回答の切り替え(=配列の切り替え)については、拙稿「Vue.js で 配列とJSONの切り替え表示を行う」にまとめました。

振り返り

一つ一つは単純なコードですが、組み合わせることで、それなりにアプリとして形になった気がしました。

できれば

  • Firebase のユーザー認証を利用して、リアルタイムDBに回答記録を保存
  • 全アプリユーザーの間で、どの単語の誤答率が高いか、統計を取る

までやってみたかったのですが、時間切れで実装できず。
Firebaseの利用はHostingのみとなりました。

そのうち時間を作ってチャレンジしてみたいと思います。

備考

以下のサイトのデータ・情報を元に開発しました。

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

Vue.jsプラグインで始めるOSS

はじめに

これはVue Advent Calendar 2019の16日目の記事です。

最近、Vue.jsのプラグインを作ってNPMパッケージとして公開する機会がありました。Vueのプラグインを作るのもNPMパッケージを公開するのも初めてでしたが、意外と手軽にできたので、「OSSはなんか敷居が高そうだ...」と思っているエンジニアにもオススメです。

今回、TypeScriptでVueプラグインを実装し、NPMに公開するためのテンプレートを用意してGithubに公開しました。この記事では、そのテンプレートの紹介をします。

テンプレートのご紹介

vue-plugin-ts-templateというテンプレートを用意しました。
中身は本当にシンプルで、一般的なtypescriptのプロジェクトでちょっとだけ実装したコードが入っているだけのものになります。

案外、TypeScriptやeslint、prettierの導入が面倒だったりするので、Vueのプラグインの作り方というよりかは開発環境を一発で作れるという意味の恩恵のほうが強いかもしれません。

プラグインの実装〜NPMパッケージの公開まで

流れはREADMEに書いてありますが、英語になっているので改めて日本語で解説していきたいと思います。

0. 要件

NPMパッケージとして公開するにあたり、NPMのアカウントが必要になるので、公開しようと思う人は作っておくようにしましょう。
また、typescriptはtscでコンパイルしているのでグローバルにインストールしておくと良いでしょう。

1. packageの初期化と依存関係のインストール

まずはテンプレートをダウンロードして依存関係をインストールしましょう。
NPMに公開する際には、パッケージのいろんな情報を入力する必要があります。
下記のコマンドを実行すると、コマンドライン上でインタラクティブに質問が出てくるので自分のパッケージにあったものに修正していきましょう。

$ yarn install
$ yarn init

スクリーンショット 2019-12-15 22.12.42.png

こんな具合に入力していきます。

  • テンプレートには初めから設定値が入っているので、使う場合は上書きしてください。 keywordsは聞かれないようなので、直接package.jsonを編集する必要があります。
  • endopointはデフォルト(index.js)で大丈夫です。tsconfig.jsonでビルドファイルをプロジェクト直下のindex.jsとして出力するようにしています(好みに応じて変えてください)。

2. プラグインの実装

Vueプラグインの実装方法については公式サイトが参考になります。
srcフォルダ内でTypeScriptを書いてください。
テンプレート内ではLoggerをインスタンスメソッドとして追加しています。

https://github.com/gyarasu/vue-plugin-ts-template/blob/master/src/index.ts

src/index.ts
import _Vue from 'vue';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const log = (value: any): void => {
  console.log(value);
};

export default {
  install(Vue: typeof _Vue): void {
    Vue.prototype.$log = log;
  }
};

ちなみに、公開したパッケージはこんな感じで使えます。

main.js
import Vue from 'vue';
import VueLogger from 'vue-logger'; // 公開するパッケージ名

Vue.use(VueLogger);
App.vue
<script>
export default {
  mounted() {
    this.$log('component is mounted!');
  }
};
</script>

3. ビルド

npmスクリプトとしてビルドコマンドも用意しています。

$ yarn build

これだけです。

4. コミット&プッシュ&タグ作成

ビルドまで終わったらNPMに公開できる状態になります。
NPMに公開してるバージョンとGithubのコードのパージョンは揃っていたほうが都合がいいと思うので、公開する前にコードをコミット・プッシュして、バージョンがわかるタグを打っておくようにしましょう。
package.json内にあるバージョン情報も必要に応じて更新しましょう。

5. NPMへの公開

ここまでくればあとは公開するのみです。
プロジェクト直下(package.jsonがいるディレクトリ) で下記のコマンドを実行すればOKです。

$ npm adduser 
$ npm publish ./

npm adduserでは、登録済みのNPMアカウントでログインを行います。
ここでログインしたアカウントにパッケージが追加されることになるので、会社用・仕事用で使い分けている場合などは注意しましょう。

まとめ

Vueでなにかする場合に、プラグインとして使える機能があると結構便利です。
また、プラグインの作成もそんなに難しいものではないですし、OSS活動を始めるにはもってこいの題材かもしれません。
便利なプラグインを作ってVueを盛り上げていきましょう!!

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

Vue.js の transition を用いてインタラクティブな「泡」のアニメーションを作った

泡の需要ない気がするけど...!

概要

業務では、どちらかというとReact.jsを触ることの方が多いのですが、たまたまコーポレートサイトをVue.jsを使う機会があったのでまとめてみました。
コーポレートサイトの見た目がちょっと寂しかったので少しアニメーションを入れてみることになり、泡をぷかぷか浮かべるアニメーションを作成してみました!
Vue.js 便利...

参考

Vue.js のドキュメントを参考にしました!
https://jp.vuejs.org/v2/guide/transitions.html

導入

transitionによる CSS アニメーション

Vue は、transition ラッパーコンポーネントを提供しています。このコンポーネントは、次のコンテキストにある要素やコンポーネントに entering/leaving トランジションを追加できます!
つまり、

<div id="demo">
  <button v-on:click="show = !show">
    Toggle
  </button>
  <transition>
    <p v-if="show">hello</p>
  </transition>
</div>

このように条件付きで hello と表示させる際には、enter/leave トランジションのために
v-enter,v-enter-active, v-enter-to,v-leave,v-leave-active, v-leave-to
というクラスが適用されます。
このクラスを用いて簡単にCSS トランジションを実現できるんです。
例えば

.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}

以上のようにcss を適用させるとトランジションのタイミングごとにクラスがふよされるのでふわっと文字が浮かび上がるアニメーションができます。

トランジション期間の設定

デフォルトではtransitionendイベントにフックすることもできますが、トランジション期間の設定をもっと明示的に設定したい場合もあるでしょう。
そんな時は、属性で JavaScript フックを定義することができます
例えば、vueファイルのtemplateで

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>

以上のように DOM イベントを読み込むために v-on ディレクティブを利用し、

// ...
methods: {
  // --------
  // ENTERING
  // --------

  beforeEnter: function (el) {
    // ...
  },

  enter: function (el, done) {
    // ...
    done()
  },
  afterEnter: function (el) {
    // ...
  },
  enterCancelled: function (el) {
    // ...
  },

  // --------

  beforeLeave: function (el) {
    // ...
  },
  leave: function (el, done) {
    // ...
    done()
  },
  afterLeave: function (el) {
    // ...
  },
  // v-show と共に使うときだけ leaveCancelled は有効です
  leaveCancelled: function (el) {
    // ...
  }
}

以上のように、メソッドをtransition内でDOM操作があった際のイベントにバインドできます。

実装

今回は2つ目の方法で実装していきます。
クリックイベントでstateが変更するようにし、

<div class="news-area" v-on:click="show = !show">
// ...
</div>

のようにクリックするとshow がtrue に変更されるようにします。

                        <transition
                            v-on:before-enter="beforeEnter"
                            v-on:enter="enter"
                            v-on:after-enter="afterEnter"
                            v-on:enter-cancelled="enterCancelled"
                            v-on:leave="leave"
                            v-bind:css="false"
                        >
                            <div class="babble" v-if="show">
                                <span id="babble1">●</span>
                                <span id="babble2">●</span>
                                <span id="babble3">●</span>
                            </div>
                        </transition>

vue の template内に上のように書きます。
今回は、3つの泡を同時に浮かばせて、もう一度クリックすると、3つの泡が別々の方向に飛んでいくような仕様にしました!
よってCSS は、position:absolute をつけて泡が初めは重なるようにしました。

.babble{
    position: absolute;
    color: #fff;
    span {
        position: absolute;
    }
}

javascript で、

 export default {
  data: function() {
    return {
      show: false
    };
  },

  methods: {
    beforeEnter: (el) => {
      el.style.opacity = 0
      el.style.left = event.pageX +'px'
    },
    enter: (el, done) => {
        Velocity(el, { opacity: 1, fontSize: '0.9em',translateX:'8px', translateY:'-90px' }, { duration: 1000,  easing: 'ease-in' })
        Velocity(el, {  translateX: '-8px;', translateY:'-190px' }, { duration: 700,  easing: 'linear' })
        Velocity(el, {  translateX: '8px', translateY:'-290px' }, { duration: 1000,  easing: 'ease-out' })
        Velocity(el, { fontSize: '1em' }, { complete: done })
    },
    enterCancelled: (el) => {
        Velocity(el.firstElementChild, {  opacity: 0, translateX: '-8px;', translateY:'-190px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' })
        Velocity(el.children[1], {  opacity: 0, translateX: '-98px;', translateY:'190px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' })
        Velocity(el.lastElementChild, {  opacity: 0, translateX: '80px;', translateY:'20px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' })
        this.setShow(false);
    },
    afterEnter: (el) => {
        Velocity(el, { fontSize: '0.5em' }, { duration: 800, loop: 3 })
        Velocity(el, { opacity: 0.8}, { duration: 800,  easing: 'swing'})
    },
    leave: (el, done) => {
        Velocity(el.firstElementChild, {  opacity: 0, translateX: '-8px;', translateY:'-19px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' })
        Velocity(el.children[1], {  opacity: 0, translateX: '-198px;', translateY:'190px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' })
        Velocity(el.lastElementChild, {  opacity: 0, translateX: '80px;', translateY:'20px', fontSize: '0.3em'  }, { duration: 700,  easing: 'swing' , complete:done})
    }
  }
};

Velocity.js のライブラリを使用し、アニメーションをつけました。
最終的には、マウスでクリックした位置の横軸を event.pageX で取得しその位置から泡が出るようにしました。また、クリックタイミングで自由に泡を破裂させることもできます。
こんな感じです↓

後ろの波は HTML5 の canvas 要素で作りました。

終わりに

途中、俺何してるんだろうってなりました。
楽しかったです。

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

SkyWay API + Rails6 + Vue でビデオチャットアプリを作る

ビデオチャットアプリを作るってハードル高そうですよね? 
SkyWayAPIを使うとリアルタイムでの動画通信が簡単にできます。

前提知識

ざっくり知っているだけで十分です。
・Websocket
・WebRTC
・RubyOnRails
・Vue.js
・heroku(heroku cliをダウンロードして、heroku loginした状態で始めます)

前置き

Railsを使う必要性は、この記事のアプリだとありません。

ユーザーや部屋の管理とかをRDS経由で行うことを前提に実装してみました。

目標物

ユーザー二人がcallIDを使ってビデオチャットをできるようにします。

①WEBアプリ側(Rails)
herokuへのデプロイを念頭に、プロジェクトを作成していきます。
herokuにアプリの初期状態をデプロイするところから始めます。

rails new skyway_test --database=postgresql --webpack=vue
rails db:create
heroku create

git add .
git commit -m "first commit"
git push heroku master

#heroku側でデプロイしたアプリをブラウザーで確認
heroku open

スクリーンショット 2019-12-15 16.07.14.png

コントローラーとルーティングを追加していきます。

rails g controller rooms show
routes.rb
Rails.application.routes.draw do
  get 'rooms/show'
  root 'rooms#show'
end

この時点で、こうなっていれば成功です。

スクリーンショット 2019-12-15 16.16.03.png

②SkyWayのdeveloper登録
Community Editionが無料なので、このプランで登録していきます。
https://webrtc.ecl.ntt.com/signup.html

スクリーンショット 2019-12-15 16.19.29.png

登録して、ダッシュボードに行ったらアプリケーションを作成に進んでください。
screencapture-console-webrtc-free-ecl-ntt-add-2019-12-15-16_20_53.png

ドメイン名の部分は、localhostとherokuで自動発行されたドメインを追加します。
それ以外は初期値のままで問題ありません。
それが終わるとアプリ詳細画面にAPIキーが表示されるので、それを控えておきます。

③クライアント側の実装(Vue.js)
ではクライアント側の実装です。

--webpack=vueオプションでrails newしてない方は、ここで下記のコマンドを実行しましょう。

$./bin/rails webpacker:install:vue
app/views/rooms/show.html.erb
#元あった中身を削除、以下を追加
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

これで自動生成されているhello_vue.jsをRails側に読み込んで、vue.jsで作ったコンポーネントをレンダリングしています。

こうなっていれば成功!
スクリーンショット 2019-12-15 16.31.45.png

ではいよいよSkywayAPIを導入していきます。

hello_vueと同じ階層にjsファイルを作成します。

app/javascript/packs/room.js
import Vue from 'vue/dist/vue.esm'
import Room from '../room.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#room',
    data: {
    },
    components: { Room }
  })
})

これでroomというIDをもつDOMがVueの影響範囲内になりました。
viewにそのDOMを設置して、jsファイルを読み込むタグを編集します。

app/views/rooms/show.html.erb
<div id="room">
 <room />
</div>

<%= javascript_pack_tag 'room' %>
<%= stylesheet_pack_tag 'room' %>

app.vueと同じ階層にroom.vueを作成します。
ここは@n0bisukeさんのqiitaの記事とgithubのリポジトリを参考にしました!
単一コンポーネントに書き換えています。

SkyWayのサンプルをVue.jsで書いていくチュートリアル vol1
https://qiita.com/n0bisuke/items/6e1f56678b2eb6318594

githubリポジトリ
https://gist.github.com/n0bisuke/88be07a6a16ee72b9bdf4fdcd12a522f

自分のAPIキーを入力するのを忘れずに!

app/javascript/room.vue
<template>
    <div id="app">
        <video id="their-video" width="200" autoplay playsinline></video>
        <video id="my-video" muted="true" width="500" autoplay playsinline></video>
        <p>Your Peer ID: <span id="my-id">{{peerId}}</span></p>
        <input v-model="calltoid" placeholder="call id">
        <button @click="makeCall" class="button--green">Call</button>
        <br />

        マイク:
        <select v-model="selectedAudio" @change="onChange">
          <option disabled value="">Please select one</option>
          <option v-for="(audio, key, index) in audios" v-bind:key="index" :value="audio.value">
            {{ audio.text }}
          </option>
        </select>

        カメラ: 
        <select v-model="selectedVideo" @change="onChange">
          <option disabled value="">Please select one</option>
          <option v-for="(video, key, index) in videos" v-bind:key="index" :value="video.value">
            {{ video.text }}
          </option>
        </select>

    </div>
</template>

<script>
const API_KEY = "自分のAPIKEY"; 
// const Peer = require('../skyway-js');
console.log(Peer)
export default {
    data: function () {
        return {
            audios: [],
            videos: [],
            selectedAudio: '',
            selectedVideo: '',
            peerId: '',
            calltoid: '',
            localStream: {}
        }
    },
    methods: {
        onChange: function(){
            if(this.selectedAudio != '' && this.selectedVideo != ''){
                this.connectLocalCamera();
            }
        },

        connectLocalCamera: async function(){
            const constraints = {
                audio: this.selectedAudio ? { deviceId: { exact: this.selectedAudio } } : false,
                video: this.selectedVideo ? { deviceId: { exact: this.selectedVideo } } : false
            }

            const stream = await navigator.mediaDevices.getUserMedia(constraints);
            document.getElementById('my-video').srcObject = stream;
            this.localStream = stream;
        },

        makeCall: function(){
            const call = this.peer.call(this.calltoid, this.localStream);
            this.connect(call);
        },

        connect: function(call){
            call.on('stream', stream => {
                const el = document.getElementById('their-video');
                el.srcObject = stream;
                el.play();
            });
        }
    },

    created: async function(){
        console.log(API_KEY)
        this.peer = new Peer({key: API_KEY, debug: 3}); //新規にPeerオブジェクトの作成
        this.peer.on('open', () => this.peerId = this.peer.id); //PeerIDを反映
        this.peer.on('call', call => {
            call.answer(this.localStream);
            this.connect(call);
        });

        //デバイスへのアクセス
        const deviceInfos = await navigator.mediaDevices.enumerateDevices();

        //オーディオデバイスの情報を取得
        deviceInfos
        .filter(deviceInfo => deviceInfo.kind === 'audioinput')
        .map(audio => this.audios.push({text: audio.label || `Microphone ${this.audios.length + 1}`, value: audio.deviceId}));

        //カメラの情報を取得
        deviceInfos
        .filter(deviceInfo => deviceInfo.kind === 'videoinput')
        .map(video => this.videos.push({text: video.label || `Camera  ${this.videos.length - 1}`, value: video.deviceId}));

        console.log(this.audios, this.videos);        
    }
}
</script>

<style scoped>
    p {
    font-size: 2em;
    text-align: center;
    }
</style>


skyway javascript SDKをCDN経由で読み込みます。(yarnやnpmで導入できたら多分そっちの方が良い。)

application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>SkywayTest</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <script type="text/javascript" src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'app![error]()
lication', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

カメラとマイクを選んだ時点で、ローカルの映像がvideoタグに反映されます。

名称未設定.gif

かけたい相手のIDを入力するとP2Pの通信が始まります!
スクリーンショット 2019-12-15 18.27.18.png

スクリーンショット 2019-12-15 18.27.26.png

最後にgit push heroku masterした結果がこれです。
あとで変わるかもしれません。
https://morning-meadow-17444.herokuapp.com/

最後に

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

ビデオチャットが簡単に作れるSkywayAPIすごいですね。
本来であれば、Turnサーバー・Stunサーバー立ててーごにょごにょしなきゃいけないと思いますが、そこの部分を全てやってくれます。

もともと複数ユーザーが同時に参加できるカンファレンス式のビデオチャットにする予定だったので、roomという言葉をよく使っています。次回できたら複数参加もできるように実装したいです。

roomよりもchatとかの方がしっくりくるかもしれません。

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

【Vue.js/Nuxt.js】モーダルを使ってリストの1件を削除すると違うやつが消える

こちらのサイトを参考に、モーダルを使ってリストからidを指定して1件削除しようとしたら、なぜかリストの最後だけ削除されてしまう。

(実際はvuexを使ってたり、追加・変更ボタンもあるが簡略化)

coping.vue
<template>
  <div>
    <table>
      <thead>
        <tr>
          <th width="30%">名前</th>
          <th width="50%">説明</th>
          <th></th>
        </tr>
        <tr v-for="(coping, index) in coping_list" :key="index" class="coping">
          <td>
            <input v-model="coping.name" type="text" />
          </td>
          <td>
            <input v-model="coping.detail" type="text" />
          </td>
          <!-- 押されたら削除モーダルを開く -->
          <td @click="openDeleteModal()">
            <i class="fas fa-times"></i>
          </td>
          <!-- 削除しますか?のモーダルを開く
          「削除」で1件削除(closeDeleteModal()を呼び出す)
          「キャンセル」でモーダルを閉じる(deleteCoping()を呼び出す)
           -->
          <DeleteModal
            v-if="is_delete_modal"
            @close="closeDeleteModal()"
            @delete="deleteCoping(coping.id)"
          />
        </tr>
      </thead>
    </table>
  </div>
</template>

<script>
import DeleteModal from '~/components/DeleteModal.vue'
export default {
  components: {
    DeleteModal
  },
  data() {
    return {
      coping_list: [
        { id: 1, name: 'aaa', detail: 'AAA' },
        { id: 2, name: 'bbb', detail: 'BBB' }
      ],
      // モーダルの表示・非表示を管理
      is_delete_modal: false
    }
  },
  methods: {
    // IDで指定したコーピングの削除
    // モーダルからcoping.idを指定すると、なぜか最後のIDが指定されてしまう
    deleteCoping(copingId) {
      // 引数に持ったID以外のリストを作る
      this.coping_list = this.coping_list.filter(
        (coping) => coping.id !== copingId
      )
      this.closeDeleteModal()
    },
    // モーダルを表示する
    openDeleteModal() {
      this.is_delete_modal = true
    },
    // モーダルを非表示にする
    closeDeleteModal() {
      this.is_delete_modal = false
    }
  }
}
</script>

2019-12-15_17h48_53.png
2019-12-15_17h49_52.png
2019-12-15_17h50_27.png

原因

削除モーダルをテーブルの行ごとに作っていたのが誤作動の原因。モーダルを呼び出したときに最後のモーダルだけが実行されていた。

解決策

削除モーダルを1つだけにする。
削除するIDは削除用IDとしてdata()に持つ

coping.vue
<template>
  <div>
    <table>
      <thead>
        <tr>
          <th width="30%">名前</th>
          <th width="50%">説明</th>
          <th></th>
        </tr>
        <tr v-for="(coping, index) in coping_list" :key="index" class="coping">
          <td>
            <input v-model="coping.name" type="text" />
          </td>
          <td>
            <input v-model="coping.detail" type="text" />
          </td>
          <!-- 押されたら削除モーダルを開く
          この時に押された行のIDを渡す -->
          <td @click="openDeleteModal(coping.id)">
            <i class="fas fa-times"></i>
          </td>
        </tr>
      </thead>
    </table>
    <!-- 削除モーダルはテーブルの外に出す
    deleteCoping()の引数はdata()から貰う-->
    <DeleteModal
      v-if="is_delete_modal"
      @close="closeDeleteModal()"
      @delete="deleteCoping(delete_id)"
    />
  </div>
</template>

<script>
import DeleteModal from '~/components/DeleteModal.vue'
export default {
  components: {
    DeleteModal
  },
  data() {
    return {
      coping_list: [
        { id: 1, name: 'aaa', detail: 'AAA' },
        { id: 2, name: 'bbb', detail: 'BBB' }
      ],
      // モーダルの表示・非表示を管理
      is_delete_modal: false,
      // 削除用のID
      delete_id: null
    }
  },
  methods: {
    // IDで指定したコーピングの削除
    // モーダルからcoping.idを指定すると、なぜか最後のIDが指定されてしまう
    deleteCoping(copingId) {
      // 引数に持ったID以外のリストを作る
      this.coping_list = this.coping_list.filter(
        (coping) => coping.id !== copingId
      )
      this.closeDeleteModal()
    },
    openDeleteModal(copingId) {
      this.is_delete_modal = true
      // ここで削除用IDを設定
      this.delete_id = copingId
    },
    closeDeleteModal() {
      this.is_delete_modal = false
    }
  }
}
</script>

ちゃんと指定したIDのリストを削除してくれるようになった。めでたしめでたし。

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

new Vue() に渡されたオプションオブジェクトの行方を探るべく、我々は vue/src/core の奥地へと向かった

qnote Advent Calendar 2019 の16日目です。

はじめに

こんにちは。今日も元気に npm run してますか?
Vue.js 、いいですよね、ドキュメントも豊富で簡単でとっても便利。
しかしフレームワークとして簡単に使えてしまうあまり、 Vue の中身を気にすることはあまりないのではないでしょうか。
今日はそんな Vue の中身を覗いて、その謎を少しだけ解明してみることにしましょう。
取り上げるのは、 Vue インスタンスに渡されるオプションオブジェクトの行方です。

オプションオブジェクトの行方

オプションオブジェクトは大まかに、下記の流れで各オプションとして機能するように定義されていきます。

  1. new Vue() に渡される
  2. initMixin()vm.$options が定義される
  3. init...() メソッドでリアクティブシステムへの追加などが行われる
  4. 我々の手に届く

ではオプションオブジェクトの長く険しい道のりを、一緒に追っていきましょう。

vm.$options が定義されるまで

new Vue()

全ての始まり、コンストラクタ関数 Vue()
ここにオプションオブジェクトを渡すことで、 Vue インスタンスが生成されます。
これが定義されている箇所は vue/src/core/instance/index.js です。

vue/src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

this._init() にオプションを渡していますね。
この中身を追ってみましょう。

initMixin()

this._init()vue/src/core/instance/init.js で定義されています。

vue/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
...

ここで気になるコメントがありました。
/* istanbul ignore if */
イスタンブール? :flag_tr: :thinking: :question:
調べてみたら、テストのカバレッジを調べてくれるツールのようでした
イスタンブールといえば、飛んでイスタンブールしか思い浮かばなかったのですが、新たな知識を得ることができました。

話がそれましたが、オプションは mergeOptions() に渡されているようですね。

mergeOptions()

mergeOptions()vue/src/core/util/options.js で定義されています。

vue/src/core/util/options.js
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions() には3つの引数が渡されています。
parent として渡される resolveConstructorOptions(vm.constructor)よくわからなかったので説明を省略いたします。
オプションオブジェクトは child として第2引数に渡されていますね。
第3引数には自身である Vue インスタンスが vm として渡されています。

checkComponents(child)
ここでは components オプションの値をチェックし、変な名前が使用されていないか、などをチェックしています。

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
この3つの関数はそれぞれ props , inject , directives オプションの内容の解析を行なっています。

次に、child がもつ extendmixin を考慮した処理が行われています。
mixin の数だけ mergeOptions を繰り返し、定義していることがわかります。

if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}

その後オプションは一旦、空のオブジェクトとして定義され、 mergeField()parentchild のオプションがマージされ、プロパティ毎の結果がオプションオブジェクトに格納されていきます。

最後にマージされたオプションオブジェクトが return され、 vm.$options に入るわけですね。

data オプションがリアクティブシステムに追加されるまで

全部のオプションの行方を追うのは大変なので、今回は data がリアクティブシステムに追加されるまでに焦点を当ててみましょう。
再び initMixin() に戻ります。
vm.$options が定義されたのち、 様々な init...() を経て、 initState(vm) にインスタンスが渡っています。

vue/src/core/instance/init.js
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
...

initState()

initState()vue/src/core/instance/state.js に定義されています。

vue/src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

メソッドの名前から、 initProps() では props を、 initMethods() では methods を定義していることがわかります。
読みやすいですね。
では、 initData() の中身を見ていきましょう。

initData()

initData()initState() と同じ vue/src/core/instance/state.js に定義されています。

vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

data オブジェクトの key の数だけ while で回して、 propsmethods ですでに定義されている名前でないかをチェックしています。
キーの名前は methodsprops が優先ということですね。

そして isReserved(key) でキー名が _ または $ から始まっていないことをチェックして、 proxy() に渡しています。

vue/src/core/instance/state.js
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

これで data のプロパティに、 vm インスタンスから代理アクセスできるようになります。
_ または $ から始まるプロパティには、公式リファレンスにもある通りvm.$data.{_または$から始まる名前} としてのみアクセスできます。
インスタンスからの代理アクセスができないのはこういうわけだったんですね。

さていよいよ大詰めです。 observe() の中身を見ていきましょう。

observe()

observe()vue/src/core/observer/index.js に定義されています。

vue/src/core/observer/index.js
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

new Observer()Observer インスタンスを作成しています。

vue/src/core/observer/index.js
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
...

data はオブジェクトなので walk() に渡り、 defineReactive(obj, keys[i]) に渡されています。

defineReactive()

vue/src/core/observer/index.js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

ここで Object.defineProperty() を使用しています。

Object.defineProperty

この仕組みを利用して、リアクティブシステムを可能にしているわけですね。
この Object.defineProperty が使用できない関係上、 Vue は IE8 以下をサポートしていないらしいです

さいごに

以上がオプションオブジェクト、というか data がリアクティブシステムに登録されるまでの流れでした。
お疲れ様でした。

Vue の中身ってそういえば気にしたことなかったな、と思い読んでみたのですが、なんとなく使っていたリアクティブシステムの仕組みを知ることができてよかったです。
頭のいい人たちが書いたコードだけあって、とても読みやすくて勉強になりました。
ただ読んだ本人(私)があまり頭がよくないので、間違って理解して書いている可能性もあります。
もし間違っている箇所があればご指摘くださると大変ありがたいです。

最後になりましたが、ここまでお読みいただきありがとうございました!

参考にさせていただいたページ
https://itnext.io/a-deep-dive-in-the-vue-js-source-code-4601a3f5584
https://github.com/ohhoney1/Vue.js-Source-Code-line-by-line

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

JSXに慣れないVue利用者のfunctional component

functional componentとは

Vue公式 描画関数とJSX
 あるのは知っていてなんとなくJSXでReact的(*)な書き方なんだなくらいの知識でした。
 これまであまり使う機会がなく、かつqiitaでもあまりないのでなんとなく手を出しづらい感がありましたが、今回Vueしか知らない人でも取り入れやすい使い方からまとめたいと思います。

*Reactは何となくしか分かっていませんが


 そもそもVueはテンプレートを使うことが推奨されています。その中でfunctional componentとはナニモノか、それにはまずrendar関数を理解しておくことが必要です。

・公式より
JavaScript による完全なプログラミングパワーを必要するときには、コンパイラに近い 描画 (render) 関数が使用できます。

理解:render関数を使用するとJavaScriptのパワーを解放できるらしい

rendar関数で何をするのか
・公式より
Vue は、実際の DOM に加える必要がある変更を追跡する仮想 DOM を構築することで、これを達成します。

return createElement('h1', this.blogTitle)

上記のようにDOMを生成していく、いわゆるJSXです。
それを省略記法にすると「createElement」が「h」になります。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

上記のコンポーネントは状態の管理や渡された状態の watch をしておらず、また、何もライフサイクルメソッドを持っていません。
ここでやっと出てきます
関数型コンポーネント(functional component)
ただし条件付きです。
・状態を持たない (リアクティブデータが無い)
・インスタンスを持たない ( this のコンテキストが無い)
この場合に関数型としてコンポーネントをマークできます

関数型コンポーネント(functional component)

メリット
 関数型コンポーネントは上記のように状態を持たないため、高速に描画ができることがメリットとなります。

形式
 形式は以下のようになります。

Vue.component('my-component', {
  functional: true,
  props: {
    // ...
  },
  // 2 つ目の context 引数が提供されます。
  render: function (createElement, context) {
    // ...
  }
})

だがしかし、Vueのテンプレートに慣れている人はこれではしんどいわけで
以下のように書けます(※)

※2.5.0以降では、単一ファイルコンポーネントを使用している場合、テンプレートベースの関数型コンポーネントは次のように宣言できます。

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    {{ props.title }}
  </button>
</template>

やっと見慣れたテンプレートと同じになりました。
ただそのまま同じようにいくかというと違うわけで、その辺をまとめていきます。

テンプレートとの差分

props

propsは {{ props.XXX }}のように記載する必要があります。
・理由
Vueインスタンスをもっていないので、Vue Loaderはpure JavaScritに変換するため

listeners(data.on)

イベントはlistenersを通して適用することになります。

childComponent
<button v-on="listeners">Click me</button>

イベントだけなら上記の書き方でも問題ないのですが、パラメータを追加する場合はイベントと一緒に指定して書きます

parentComponent
<Mybutton @click="clickMethod" title="Click me" />
childComponent
<button @click="listeners.click(1)">{{ props.title }}</button>

attr

属性値の引継ぎはdata.attrsで行うことができます

parentComponent
<div>
  <DisplayDate
    :date="'6 Dec 1999'"
    aria-label="6 of December of 1999 was a long time ago, but not so much"
  />
</div>
childComponent
<template functional>
  <span v-bind="data.attrs">{{ props.date }}</span>
</template>

class

静的なclass

class指定はdata.staticClassで引き継げます

parentComponent
<MyTitle
  title="Let's go to the mall, today!"
  class="super-bold-text"
/>
childComponent
<span class="span-class" :class="data.staticClass">
  {{ props.title }}
</span>
// ⇒ <span class="span-class super-bold-text">

動的なclassとのマージ1

parentComponent
<MyTitle
  title="Let's go to the mall, today!"
  class="super-bold-text"
  :some-prop="true"
/>
childComponent
<span class="span-class" :class="[data.staticClass, { 'another-class': props.someProp }]">
  {{ props.title }}
</span>
// ⇒ <span class="span-class super-bold-text another-class"> 

動的なclassとのマージ2

parentComponent
<MyTitle
  title="Let's go to the mall, today!"
  class="super-bold-text"
  :class="'another-class'"
/>
childComponent
<span class="span-class" :class="[data.staticClass, data.class]">
  {{ props.title }}
</span>
// ⇒ <span class="span-class super-bold-text another-class"> 

おわり

差分についてコンポーネント利用は「inject」を使うとか、「slots」とか「children」とかあるみたいですが、とりあえず今回はここまでとしたいと思います。

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

awsを使った典型的なwebサービスのインフラ構成を考えてみるS3, ECS

概要

フロントエンドをVueを使って作り、APIなどのバックエンドをpythonのFlaskという軽量なフレームワークを使って作りました。今回は、ローカル環境下で動いていたアプリケーションをgithubから自動的にAWSの方にdeployすることができるようなインフラ構成を設計してみます。

不十分な点などありましたら、アドバイスいただけると幸いです。

使うもの

AWS関連

  • EC2
  • RDS
  • ECR
  • ECS(fargate)
  • S3
  • CloudFront
  • Route53
  • IAM

CI/CD関連

  • Circle CI

システム構成図

インフラ のコピー-Page-1.png

まず、public subnetとprivate subnetを持つVPCを作成します。外部からのアクセスを許容するpublic subnetには、ロードバランサーと踏み台(ログイン)サーバーを設置します。外部から直接アクセスできないprivate subnetには、APIサーバーとデータベースを置きます。

次に、API serverであるFlaskアプリとproxy serverとして利用するNginxは、コンテナ化して、ECS(Forgate)で管理します。Forgateを使うことで、自動でコンテナのスケーリングや再起動などをしてくれます。RDSは、AWSのamazon Auroraを利用します。こちらも、定期的にレプリカを作成してくれるので、万が一データを損失する場合やデータベースにアクセスできなくなった場合にも安心です。amazon Auroraにアクセスできるのは、(APIサーバーと)踏み台サーバーのEC2からのみで、外部からデータベースをいじることはできません。Login serverのEC2のインスタンスは、秘密鍵が保存されているローカルPCからのみアクセスできます。

フロントエンドのコードは、S3にデプロイし、CloudFront経由で配信します。あとは、Route53でのドメイン設定や、Certificate Managerでの証明書取得、ロードバランサーの設置など、細々した設定をしてあげると完成です。

CI/CD関連

インフラ のコピー-Page-2 (1).png

Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。

config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@0.0.2
  aws-ecs: circleci/aws-ecs@0.0.3
workflows:
  build-and-deploy:
    jobs:
      - aws-ecr/build_and_push_image:
          account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          region: ${AWS_DEFAULT_REGION}
          tag: "${CIRCLE_SHA1}"
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build_and_push_image
          aws-region: ${AWS_DEFAULT_REGION}
          family: "${AWS_RESOURCE_NAME_PREFIX}-service"
          cluster-name: "${AWS_RESOURCE_NAME_PREFIX}-cluster"
          container-image-name-updates: "container=${AWS_RESOURCE_NAME_PREFIX}-service,tag=${CIRCLE_SHA1}"

上記.circleci/config.ymlをgithubにpushするとcircleCIの設定が適応されます。CircleCIで定めた環境変数は、CircleCIのダッシュボードのEnvironment Variables
から設定します(後述)。

S3へのdeploy

  • IAMでS3へのアップロード用のuserを作り、AmazonS3FullAccess権限を与える
  • aws cliからuploadするscriptsを書く

こちらに関しては、たくさん説明記事があるので、細かい内容は省略します。
参考: Amazon S3でSPAをサクッと公開する

インフラ構築までの流れ

VPCの作成

  • IPv4 CIDR 10.2.0.0/16

subnetの作成

  • IPv4 CIDR 10.2.0.0/20(public-subnet-a)
  • IPv4 CIDR 10.2.16.0/20(public-subnet-c)
  • IPv4 CIDR 10.2.32.0/20(private-subnet-a)
  • IPv4 CIDR 10.2.48.0/20(private-subnet-c)

アベイラビリティゾーンA, Cに2つずつ、public subnetと private subnetを設置します。CIDRの設定に気をつけてください。

Internet Gatewayの生成

  • Internet Gatewayを生成して、先ほど生成したVPCにアタッチする
  • 2つのpublic subnetをInternetGWに紐付ける

NAT Gatewayの作成

  • NAT Gatewayを作成して、public subnetの一方にアタッチする
  • これにより、private subnetへアクセスできるようになる

デフォルトでは、subnetを生成するとプライベートルートテーブルが選択されます。他の使用可能なルートテーブルを生成し、送信先 0.0.0.0/0 がインターネットゲートウェイ (igw-xxxxxxxx) にルーティングされるようにします。その後、public subnetとそのルートテーブルを紐付けます。

※VPCとsubnet,Internet GWなどの詳細な設定方法は公式ドキュメントを参照してください。

ECRでレポジトリを作成

  • circleciに権限を付与する
    IAMでcircleciによるdeploy用のuserを作成します。
    今回は、ECR/ECSに関する権限を付与します。

    • AmazonEC2ContainerRegistryFullAccess
    • AWSCodeDeployRoleForECS
    • AmazonEC2ContainerServiceFullAccess
    • AmazonECSTaskExecutionRolePolicy
    • AWSDeepRacerCloudFormationAccessPolicy
  • Environment Variablesの設定

    • AWS_ACCOUNT_ID(ex:754569708956)
    • AWS_DEFAULT_REGION(ex:ap-northeast-1)
    • AWS_RESOURCE_NAME_PREFIX(ex:flask-app)
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY

AWS_RESOURCE_NAME_PREFIXは、ECRのレポジトリの名前と同じにしないとエラーが出ます

nginxコンテナをアップロード

  • こちらは、頻繁に変更しないと思うので、circleCIには含めず手動で行う
  • 詳細は公式ページを参照のこと
  • confファイルで、nginxはproxy serverとしての設定する
ディレクトリ構成
nginx
├── Dockerfile
└── conf
    └── default.conf
Dockerfile
FROM nginx
COPY conf/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT nginx -g 'daemon off;'
default.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        #root   /usr/share/nginx/html;
        #index  index.html index.htm;
                proxy_pass      http://127.0.0.1:5000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

ECS Fargateの作成

  • fargateでclusterを作成する VPCは先ほど作った物を使いますので、ここで新しく作成する必要はありません。
  • fargateでtaskを作成する
    タスク実行ロールは、ecsTaskExecutionRoleを指定します。ECRで登録したdocker imageのURLを貼り付けます。コンテナのportを公開するのを忘れないようにしましょう(flask container port:5000)。

  • fargateでserviceを作成する

    • subnetは先ほど作ったprivate subnetを二つ割り当てます
    • fargate service用のsecurity groupを作成する(port:80)
    • EC2でロードバランサーを作成する
    • ELB用のsecurity groupを作成する(port:80)
    • target groupを作成する(port:80, ターゲットの種類:ip)
    • ロードバランス用のコンテナではnginxのcontainerを指定して、ターゲットグループは先ほど作った物を指定

※ flask-app用のコンテナの名前は、AWS_RESOURCE_NAME_PREFIX-serviceと同じ物にしないと、circleCIのdeployの時にエラーになります

RDBとの連携

  • 踏み台サーバーを立てる(EC2)
    • mysql-clientをinstallする
  • RDSでamazon Auroraを選択する
    • aurora DB用のsecurity groupを作成する(port:3306)
    • private subnetをまとめたsubnet groupを作成する
  • 踏み台サーバーからendpointに向けてログインできるか確認する
  • ECSのコンテナの方で環境変数を設定する
    • DB_NAME
    • DB_USER
    • PASSWORD
    • HOST

エラーハンドリング

  1. CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
    NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考

  2. Task failed ELB health checks in (target-group...)
    ELBのhealth checkに失敗すると表示されます。ELBを設定した時に、defaultでは、/がhealth checkのendpointになります。APIサーバーで/のPATHでGETを用意していていなかったら、ここでエラーになるので、ELBの設定の時に、PATHを変更するか、APIサーバーの方で、Health check用のAPIを作るようにします。

まとめ

AWSのサービスをうまく利用することで、再現性が高くスケーラブルなアプリケーションを作成することができました。また、terraformなどを使って、awsの設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。

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

典型的なwebサービスのawsを使ったインフラ構成を考えてみるS3, ECS

概要

フロントエンドをVueを使って作り、APIなどのバックエンドをpythonのFlaskという軽量なフレームワークを使って作りました。今回は、ローカル環境下で動いていたアプリケーションをgithubから自動的にAWSの方にdeployすることができるようなインフラ構成を設計してみます。

不十分な点などありましたら、アドバイスいただけると幸いです。

使うもの

AWS関連

  • EC2
  • RDS
  • ECR
  • ECS(fargate)
  • S3
  • CloudFront
  • Route53
  • IAM

CI/CD関連

  • Circle CI

システム構成図

インフラ のコピー-Page-1.png

まず、public subnetとprivate subnetを持つVPCを作成します。外部からのアクセスを許容するpublic subnetには、ロードバランサーと踏み台(ログイン)サーバーを設置します。外部から直接アクセスできないprivate subnetには、APIサーバーとデータベースを置きます。

次に、API serverであるFlaskアプリとproxy serverとして利用するNginxは、コンテナ化して、ECS(Forgate)で管理します。Forgateを使うことで、自動でコンテナのスケーリングや再起動などをしてくれます。RDSは、AWSのamazon Auroraを利用します。こちらも、定期的にレプリカを作成してくれるので、万が一データを損失する場合やデータベースにアクセスできなくなった場合にも安心です。amazon Auroraにアクセスできるのは、(APIサーバーと)踏み台サーバーのEC2からのみで、外部からデータベースをいじることはできません。Login serverのEC2のインスタンスは、秘密鍵が保存されているローカルPCからのみアクセスできます。

フロントエンドのコードは、S3にデプロイし、CloudFront経由で配信します。あとは、Route53でのドメイン設定や、Certificate Managerでの証明書取得、ロードバランサーの設置など、細々した設定をしてあげると完成です。

CI/CD関連

インフラ のコピー-Page-2 (1).png

Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。

config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@0.0.2
  aws-ecs: circleci/aws-ecs@0.0.3
workflows:
  build-and-deploy:
    jobs:
      - aws-ecr/build_and_push_image:
          account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          region: ${AWS_DEFAULT_REGION}
          tag: "${CIRCLE_SHA1}"
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build_and_push_image
          aws-region: ${AWS_DEFAULT_REGION}
          family: "${AWS_RESOURCE_NAME_PREFIX}-service"
          cluster-name: "${AWS_RESOURCE_NAME_PREFIX}-cluster"
          container-image-name-updates: "container=${AWS_RESOURCE_NAME_PREFIX}-service,tag=${CIRCLE_SHA1}"

上記.circleci/config.ymlをgithubにpushするとcircleCIの設定が適応されます。CircleCIで定めた環境変数は、CircleCIのダッシュボードのEnvironment Variables
から設定します(後述)。

S3へのdeploy

  • IAMでS3へのアップロード用のuserを作り、AmazonS3FullAccess権限を与える
  • aws cliからuploadするscriptsを書く

こちらに関しては、たくさん説明記事があるので、細かい内容は省略します。
参考: Amazon S3でSPAをサクッと公開する

インフラ構築までの流れ

VPCの作成

  • IPv4 CIDR 10.2.0.0/16

subnetの作成

  • IPv4 CIDR 10.2.0.0/20(public-subnet-a)
  • IPv4 CIDR 10.2.16.0/20(public-subnet-c)
  • IPv4 CIDR 10.2.32.0/20(private-subnet-a)
  • IPv4 CIDR 10.2.48.0/20(private-subnet-c)

アベイラビリティゾーンA, Cに2つずつ、public subnetと private subnetを設置します。CIDRの設定に気をつけてください。

Internet Gatewayの生成

  • Internet Gatewayを生成して、先ほど生成したVPCにアタッチする
  • 2つのpublic subnetをInternetGWに紐付ける

NAT Gatewayの作成

  • NAT Gatewayを作成して、public subnetの一方にアタッチする
  • これにより、private subnetへアクセスできるようになる

デフォルトでは、subnetを生成するとプライベートルートテーブルが選択されます。他の使用可能なルートテーブルを生成し、送信先 0.0.0.0/0 がインターネットゲートウェイ (igw-xxxxxxxx) にルーティングされるようにします。その後、public subnetとそのルートテーブルを紐付けます。

※VPCとsubnet,Internet GWなどの詳細な設定方法は公式ドキュメントを参照してください。

ECRでレポジトリを作成

  • circleciに権限を付与する
    IAMでcircleciによるdeploy用のuserを作成します。
    今回は、ECR/ECSに関する権限を付与します。

    • AmazonEC2ContainerRegistryFullAccess
    • AWSCodeDeployRoleForECS
    • AmazonEC2ContainerServiceFullAccess
    • AmazonECSTaskExecutionRolePolicy
    • AWSDeepRacerCloudFormationAccessPolicy
  • Environment Variablesの設定

    • AWS_ACCOUNT_ID(ex:754569708956)
    • AWS_DEFAULT_REGION(ex:ap-northeast-1)
    • AWS_RESOURCE_NAME_PREFIX(ex:flask-app)
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY

AWS_RESOURCE_NAME_PREFIXは、ECRのレポジトリの名前と同じにしないとエラーが出ます

nginxコンテナをアップロード

  • こちらは、頻繁に変更しないと思うので、circleCIには含めず手動で行う
  • 詳細は公式ページを参照のこと
  • confファイルで、nginxはproxy serverとしての設定する
ディレクトリ構成
nginx
├── Dockerfile
└── conf
    └── default.conf
Dockerfile
FROM nginx
COPY conf/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT nginx -g 'daemon off;'
default.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        #root   /usr/share/nginx/html;
        #index  index.html index.htm;
        proxy_pass      http://127.0.0.1:5000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

ECS Fargateの作成

  • fargateでclusterを作成する VPCは先ほど作った物を使いますので、ここで新しく作成する必要はありません。
  • fargateでtaskを作成する
    タスク実行ロールは、ecsTaskExecutionRoleを指定します。ECRで登録したdocker imageのURLを貼り付けます。コンテナのportを公開するのを忘れないようにしましょう(flask container port:5000)。

  • fargateでserviceを作成する

    • subnetは先ほど作ったprivate subnetを二つ割り当てます
    • fargate service用のsecurity groupを作成する(port:80)
    • EC2でロードバランサーを作成する
    • ELB用のsecurity groupを作成する(port:80)
    • target groupを作成する(port:80, ターゲットの種類:ip)
    • ロードバランス用のコンテナではnginxのcontainerを指定して、ターゲットグループは先ほど作った物を指定

※ flask-app用のコンテナの名前は、AWS_RESOURCE_NAME_PREFIX-serviceと同じ物にしないと、circleCIのdeployの時にエラーになります

RDBとの連携

  • 踏み台サーバーを立てる(EC2)
    • mysql-clientをinstallする
  • RDSでamazon Auroraを選択する
    • aurora DB用のsecurity groupを作成する(port:3306)
    • private subnetをまとめたsubnet groupを作成する
  • 踏み台サーバーからendpointに向けてログインできるか確認する
  • ECSのコンテナの方で環境変数を設定する
    • DB_NAME
    • DB_USER
    • PASSWORD
    • HOST

エラーハンドリング

  1. CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
    NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考

  2. Task failed ELB health checks in (target-group...)
    ELBのhealth checkに失敗すると表示されます。ELBを設定した時に、defaultでは、/がhealth checkのendpointになります。APIサーバーで/のPATHでGETを用意していていなかったら、ここでエラーになるので、ELBの設定の時に、PATHを変更するか、APIサーバーの方で、Health check用のAPIを作るようにします。

まとめ

AWSのサービスをうまく利用することで、再現性が高くスケーラブルなアプリケーションを作成することができました。また、terraformなどを使って、awsの設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。

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

kintone カスタマイズを Vue.js + TypeScript + Pug + SCSS でモダンに開発する (4) アプリ実装編 part2

目次

(1) 環境構築編
(2) アプリ構築・設定編
(3) アプリ実装編 part1
(4) アプリ実装編 part2(この記事)

前置き

第 3 回 の記事では、第 1 回 で作成した Vue.jsTypeScriptPugSCSS を組み込んだプロジェクトで kintone カスタマイズビューのイベント処理をきっかけに Trello のようなリスト・カード型の画面を構成するフロント側実装を行いました。
しかしながら、前回までの実装ではカードを動かしてもその情報は kintone には保存されず、あくまで見た目上カードを動かせるだけに過ぎないと言う話をしました。
今回はいよいよ動かしたカードの情報を kintone に保存する機能を実装し、アプリを完成させたいと思います。

前提

以下の環境で作業しています。

  • macOS Catalina
  • Homebrew 2.1.16
  • Node.js 13.1.0
  • VisualStudio Code 1.40.1

(1) 環境構築編の記事で、以下をセットアップしました。

  • Vue.js 4.0.5
  • TypeScript 3.5.3
  • vue-cli-plugin-pug 1.0.7

他、プロジェクト作成時の流れで Sass / SCSSESLintPrettierJest などがセットアップされています。

(2) アプリ構築・設定編 の記事で、kintone 側で用意している雛形アプリ「案件管理」を使って新規アプリを構築し、型定義ファイルの生成まで行いました。
(3) アプリ実装編 part1 では画面をコンポーネントで分割し、ビジュアル面の実装を中心に進めました。

今回のゴール

前回Vue.Draggable を組み込んだ事で、以下のようにドラッグ&ドロップでリスト間を移動できるようになりました。
resized-00-001-vuedraggable.gif
今回はカードを別なリストに動かした際に自動的にレコードを保存するようにしましょう。
これにより、カードを動かすだけで確度を変更する事ができるようになります。

以下の順で説明していきます。

  • ドロップ時のイベント処理を実装する
  • kintone JS SDK でレコードを保存する
  • プロジェクトをビルドしてアプリに適用する

(4) アプリ実装編 part2

ドロップ時のイベント処理を実装する

カードが別の確度のリストにドロップされたイベントを捕まえて、レコード保存する部分を実装します。
引き続き List コンポーネント に実装を加えていきます。

Vue.Draggable のイベントについてはプロジェクトのページにきちんと解説があります。
ここでは、ドラッグ&ドロップ終了後のイベントである @end を使います。

template を以下のように修正しましょう。

components/List.vue
<template lang="pug">
  .list
    .list-title
      span.list-title-label 確度:
      span.list-title-value {{listTitle}}
    .list-body
      Draggable.draggable(
        :group="'list'"
        @end="onDropEnd"
        :data-group="group"
      )
        Card(
          v-for="r in records"
          :key="r.$id.value"
          :record="r"
          :data-record-id="r.$id.value"
        )
</template>

.draggable@end="onDropEnd" イベントハンドラを割り当てています。
さらに、:data-group="group" として、どの確度のリストであるかを格納しておきます。
同様に、カードコンポーネントにも :data-record-id="r.$id.value" として、そのカードのレコード番号を dataset に格納しておきます。

@end イベントの引数は、前回の記事@types/vuedraggable/index.d.ts ファイルで定義した DropEvent になります。
これには、item としてドロップしたカードそのものの要素が、 from にはドラッグ前に所属していた要素が、to にはドロップされた先の要素の情報が格納されています。
そして、上の template でそれぞれの要素に割り当てた dataset の値を拾えば、どのレコード番号を持つカードをどのリストからどのリストにドロップしたかを捕捉できると言うわけです。

components/List.vue
  /**
   * カードのドラッグ&ドロップ終了時処理
   */
  onDragEnd(e: DropEvent) {
    // 動かされたカードのレコード番号
    const recordId: string = (e.item as HTMLElement).dataset.recordId!;

    // 動かす前のリスト(確度)と動かされた先のリスト(確度)を確認し、同じだったら何もしない
    const fromGroup: string = (e.from as HTMLElement).dataset.group!;
    const toGroup: string = (e.to as HTMLElement).dataset.group!;
    if (fromGroup === toGroup) {
      return;
    }
  }

今回のアプリではリスト内のカードの順番までは制御しないので、リスト内でカードを(上下に)動かした場合、つまり dataset.group の値に変化がない場合は何もせず処理を抜けるようにします。
リスト間でカードを移動した場合は、これらの情報をもとに kintone にレコード更新に行く事になります。

kintone JS SDK でレコードを保存する

ようやく kintone JS SDK の出番です。

まず、components/List.vue で kintone JS SDK を使えるようにしなければいけません。

components/List.vue
// デコレーター
import { Component, Prop, Vue } from "vue-property-decorator";

// kintone JS SDK
const kintoneJSSDK = require("@kintone/kintone-js-sdk");

// コンポーネント
import Card from "./Card.vue";
import Draggable, { DropEvent } from "vuedraggable";
@Component({
  components: {
    Card,
    Draggable
  }
})
(省略)

@kintone/kintone-js-sdkrequire している行を追加しています。
import じゃダメなんですか?と言うご意見もあるかもですが、

import * as kintoneJSSDK from "@kintone/kintone-js-sdk";

としても動くは動くのですが、 @kintone/kintone-js-sdk の d.ts ファイルがありませんぜ」と文句を言われるので、ここでは require にしています。
そのためせっかく TypeScript で記述しているのにタイプフリーにはならないしエディタでのコード補完も機能しません。残念。
d.ts ファイルを自作すれば良いかもですが、そこは公式に期待したいところです。issue も上がっている事ですし。

では、onDragEnd() に SDK を通じてレコードを更新する実装を加えましょう。

components/List.vue
  /**
   * カードのドラッグ&ドロップ終了時処理
   */
  async onDropEnd(e: DropEvent) {
    // 動かされたカードのレコード番号
    const recordId: string = (e.item as HTMLElement).dataset.recordId!;

    // 動かす前のリスト(確度)と動かされた先のリスト(確度)を確認し、同じだったら何もしない
    const fromGroup: string = (e.from as HTMLElement).dataset.group!;
    const toGroup: string = (e.to as HTMLElement).dataset.group!;
    if (fromGroup === toGroup) {
      return;
    }

    // レコード操作オブジェクトを作成
    const kintoneRecord = new kintoneJSSDK.Record();

    // 更新を実行
    const result = await kintoneRecord
      .updateRecordByID({
        app: kintone.app.getId(),
        id: recordId,
        record: {
          確度: { value: toGroup }
        }
      })
      .catch((e: object) => {
        window.alert(e);
      });
  }

いくつかポイントがあります。
まずメソッドを宣言する部分で、

  async onDropEnd(e: DropEvent) {

としています。
後で出て来るレコードを更新するメソッドは戻り値として Promise オブジェクトを返却するため、async / await で処理する事が可能です。

const kintoneRecord = new kintoneJSSDK.Record();

の部分が kintone JS SDK でレコードを操作するためのオブジェクトを作成するところです。
このように記述するとセッション認証でレコード操作ができます。
つまりログインユーザーの権限の影響を受けると言うわけです。
API トークンを使用したり、別途ユーザーアカウントの権限でレコード操作をする場合はこの前に kintone.Auth オブジェクトや kintone.Connection オブジェクトの準備が必要です。
この辺は公式のドキュメントに一通り記述があります。

さて、今回は既に更新対象とするレコードのレコード番号が分かっているので、 updateRecordByID() メソッドでレコードを更新します。

// 更新を実行
const result = await kintoneRecord
  .updateRecordByID({
    app: kintone.app.getId(),
    id: recordId,
    record: {
      確度: { value: toGroup }
    }
  })
  .catch((e: object) => {
    window.alert(e);
  });

メソッドにアプリ ID とレコード番号と変更するフィールドの値を引き渡すだけの、実に簡単なメソッドです。
これだけで簡単にレコードを更新できます。
ここではあくまでサンプルと言う事でエラー処理を window.alert() でやっていますが、ちゃんとしたアプリにするならもっと気の利いた実装にしましょう。

このような実装を加える事で、手で移動したカードがブラウザリロード後もそのリストに並んでいるのが確認できるはずです。

参考までに、これを従来の JavaScript API から kintone REST API を呼ぶ形式で実装したコードを見てみましょう。

// 更新を実行
const result = await kintone.api(kintone.api.url("/k/v1/record", true), "PUT", {
    app: kintone.app.getId(),
    id: recordId,
    record: { 確度: { value: toGroup } }
  })
  .catch((e) => {
    window.alert(e);
  });

え、あんまり変わってないじゃないかって?
確かにそうかも知れません。
けれども、 文字列とメソッドの組み合わせで API を指定するよりもオブジェクトの関数でやりたい事を明示的に呼び出す書き方の方が可読性が高くコードとしてクリーンであると言えるのではないでしょうか。

プロジェクトをビルドしてアプリに適用する

さて、ここまででアプリの実装はひと段落です。
このシリーズでは、VS Code の拡張機能である Live Server を使用してローカルで生成されたファイルを kintone 上で表示するやり方で開発を進めていました。
この方式では当然本運用はできないので、ここまでの開発成果を ビルド してアプリに適用しましょう。
VS Code のターミナルウィンドウで以下のように実行します。

% yarn build

10〜20 秒ほどで、 dist/ フォルダの下に以下のようなファイルが作成されます。
01-001-build.png
これらのファイルは難読化及び圧縮化が施されており容易にリバースエンジニアリングできないものになっています。
また、ファイル名にはキャッシュ除けのランダムな文字列が含まれています。この辺は webpack の設定で付けないように(常に固定名に)したりできますが、今回は説明を省きます。

出来上がった JS ファイルと CSS ファイルをアプリに適用します。
resized-01-002-apply.png
これでアプリを更新すれば、ビルド前の状態と同じ動作をする事が確認できるはずです。

まとめ

ここまででアプリの実装はひと段落です。
Vue.jsTypeScriptkintone JS SDK を使い、kintone のカスタマイズを実装する一連の流れについて説明して来ました。
もちろんここまでの実装でこのアプリは実用レベルで使えるかと言うと、実際にはそんな事はありません。
例えば、

  • 複数人でアプリを触っていた際、別の人がカードを動かしていたのを知らずに自分も動かしてしまってエラーが出てしまった
  • あるいは他の人がレコードを編集したためエラーが出てしまった
  • そもそもカードから別ウィンドウでレコード詳細に飛ぶ機能があるが、そちらでの変更結果はリロードしなければカスタマイズビューには反映されない
  • 担当者以外はカードを動かせないようにしたい
  • 1 つのリスト内で順番を任意に並び替えたい
  • あるいは小計の降順など特定の条件でソートしたい
  • リスト内のカードの小計の合計値をリストに表示したい
  • 確度以外の条件でグルーピングしたい

など、ちょっと考えれば多数の問題点・改善点が見出せます。
この辺りはビジネス上の要求やアプリを使用するユーザー層によっても最適解が変わって来る部分でしょう。

次回は

というわけで、今回は Vue.jsTypeScript で開発するプロジェクトを kintone で運用に載せるところまでを見て来ました。
一昔前まではごくごく普通だった生の JavaScript ファイルを書いてアプリに適用して複数のブラウザで動作確認して・・・と言う開発の進め方と較べると、これらモダンなテクノロジーの採用による恩恵は計り知れないと言わざるを得ません。

しかし、こう言った進め方をすれば効率よくバグのない開発ができるかと言えば、それは言い過ぎです。
ここまでの解説では テスト に関する観点がまるっきり存在していません。

と言うわけで、最終回(予定)となる次回は、kintone カスタマイズを Jest でモダンにテストする手法について解説していこうと思います。

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

ナウい kintone カスタマイズ (4) アプリ実装編 part2

目次

(1) 環境構築編
(2) アプリ構築・設定編
(3) アプリ実装編 part1
(4) アプリ実装編 part2(この記事)

前置き

第 3 回 の記事では、第 1 回 で作成した Vue.jsTypeScriptPugSCSS を組み込んだプロジェクトで kintone カスタマイズビューのイベント処理をきっかけに Trello のようなリスト・カード型の画面を構成するフロント側実装を行いました。
しかしながら、前回までの実装ではカードを動かしてもその情報は kintone には保存されず、あくまで見た目上カードを動かせるだけに過ぎないと言う話をしました。
今回はいよいよ動かしたカードの情報を kintone に保存する機能を実装し、アプリを完成させたいと思います。

今更ですがタイトルが長くてアレだったので短くしました。

前提

以下の環境で作業しています。

  • macOS Catalina
  • Homebrew 2.1.16
  • Node.js 13.1.0
  • VisualStudio Code 1.40.1

(1) 環境構築編の記事で、以下をセットアップしました。

  • Vue.js 4.0.5
  • TypeScript 3.5.3
  • vue-cli-plugin-pug 1.0.7

他、プロジェクト作成時の流れで Sass / SCSSESLintPrettierJest などがセットアップされています。

(2) アプリ構築・設定編 の記事で、kintone 側で用意している雛形アプリ「案件管理」を使って新規アプリを構築し、型定義ファイルの生成まで行いました。
(3) アプリ実装編 part1 では画面をコンポーネントで分割し、ビジュアル面の実装を中心に進めました。

今回のゴール

前回Vue.Draggable を組み込んだ事で、以下のようにドラッグ&ドロップでリスト間を移動できるようになりました。
resized-00-001-vuedraggable.gif
今回はカードを別なリストに動かした際に自動的にレコードを保存するようにしましょう。
これにより、カードを動かすだけで確度を変更する事ができるようになります。

以下の順で説明していきます。

  • ドロップ時のイベント処理を実装する
  • kintone JS SDK でレコードを保存する
  • プロジェクトをビルドしてアプリに適用する

(4) アプリ実装編 part2

ドロップ時のイベント処理を実装する

カードが別の確度のリストにドロップされたイベントを捕まえて、レコード保存する部分を実装します。
引き続き List コンポーネント に実装を加えていきます。

Vue.Draggable のイベントについてはプロジェクトのページにきちんと解説があります。
ここでは、ドラッグ&ドロップ終了後のイベントである @end を使います。

template を以下のように修正しましょう。

components/List.vue
<template lang="pug">
  .list
    .list-title
      span.list-title-label 確度:
      span.list-title-value {{listTitle}}
    .list-body
      Draggable.draggable(
        :group="'list'"
        @end="onDropEnd"
        :data-group="group"
      )
        Card(
          v-for="r in records"
          :key="r.$id.value"
          :record="r"
          :data-record-id="r.$id.value"
        )
</template>

.draggable@end="onDropEnd" イベントハンドラを割り当てています。
さらに、:data-group="group" として、どの確度のリストであるかを格納しておきます。
同様に、カードコンポーネントにも :data-record-id="r.$id.value" として、そのカードのレコード番号を dataset に格納しておきます。

@end イベントの引数は、前回の記事@types/vuedraggable/index.d.ts ファイルで定義した DropEvent になります。
これには、item としてドロップしたカードそのものの要素が、 from にはドラッグ前に所属していた要素が、to にはドロップされた先の要素の情報が格納されています。
そして、上の template でそれぞれの要素に割り当てた dataset の値を拾えば、どのレコード番号を持つカードをどのリストからどのリストにドロップしたかを捕捉できると言うわけです。

components/List.vue
  /**
   * カードのドラッグ&ドロップ終了時処理
   */
  onDragEnd(e: DropEvent) {
    // 動かされたカードのレコード番号
    const recordId: string = (e.item as HTMLElement).dataset.recordId!;

    // 動かす前のリスト(確度)と動かされた先のリスト(確度)を確認し、同じだったら何もしない
    const fromGroup: string = (e.from as HTMLElement).dataset.group!;
    const toGroup: string = (e.to as HTMLElement).dataset.group!;
    if (fromGroup === toGroup) {
      return;
    }
  }

今回のアプリではリスト内のカードの順番までは制御しないので、リスト内でカードを(上下に)動かした場合、つまり dataset.group の値に変化がない場合は何もせず処理を抜けるようにします。
リスト間でカードを移動した場合は、これらの情報をもとに kintone にレコード更新に行く事になります。

kintone JS SDK でレコードを保存する

ようやく kintone JS SDK の出番です。

まず、components/List.vue で kintone JS SDK を使えるようにしなければいけません。

components/List.vue
// デコレーター
import { Component, Prop, Vue } from "vue-property-decorator";

// kintone JS SDK
const kintoneJSSDK = require("@kintone/kintone-js-sdk");

// コンポーネント
import Card from "./Card.vue";
import Draggable, { DropEvent } from "vuedraggable";
@Component({
  components: {
    Card,
    Draggable
  }
})
(省略)

@kintone/kintone-js-sdkrequire している行を追加しています。
import じゃダメなんですか?と言うご意見もあるかもですが、

import * as kintoneJSSDK from "@kintone/kintone-js-sdk";

としても動くは動くのですが、 @kintone/kintone-js-sdk の d.ts ファイルがありませんぜ」と文句を言われるので、ここでは require にしています。
そのためせっかく TypeScript で記述しているのにタイプフリーにはならないしエディタでのコード補完も機能しません。残念。
d.ts ファイルを自作すれば良いかもですが、そこは公式に期待したいところです。issue も上がっている事ですし。

では、onDragEnd() に SDK を通じてレコードを更新する実装を加えましょう。

components/List.vue
  /**
   * カードのドラッグ&ドロップ終了時処理
   */
  async onDropEnd(e: DropEvent) {
    // 動かされたカードのレコード番号
    const recordId: string = (e.item as HTMLElement).dataset.recordId!;

    // 動かす前のリスト(確度)と動かされた先のリスト(確度)を確認し、同じだったら何もしない
    const fromGroup: string = (e.from as HTMLElement).dataset.group!;
    const toGroup: string = (e.to as HTMLElement).dataset.group!;
    if (fromGroup === toGroup) {
      return;
    }

    // レコード操作オブジェクトを作成
    const kintoneRecord = new kintoneJSSDK.Record();

    // 更新を実行
    const result = await kintoneRecord
      .updateRecordByID({
        app: kintone.app.getId(),
        id: recordId,
        record: {
          確度: { value: toGroup }
        }
      })
      .catch((e: object) => {
        window.alert(e);
      });
  }

いくつかポイントがあります。
まずメソッドを宣言する部分で、

  async onDropEnd(e: DropEvent) {

としています。
後で出て来るレコードを更新するメソッドは戻り値として Promise オブジェクトを返却するため、async / await で処理する事が可能です。

const kintoneRecord = new kintoneJSSDK.Record();

の部分が kintone JS SDK でレコードを操作するためのオブジェクトを作成するところです。
このように記述するとセッション認証でレコード操作ができます。
つまりログインユーザーの権限の影響を受けると言うわけです。
API トークンを使用したり、別途ユーザーアカウントの権限でレコード操作をする場合はこの前に kintone.Auth オブジェクトや kintone.Connection オブジェクトの準備が必要です。
この辺は公式のドキュメントに一通り記述があります。

さて、今回は既に更新対象とするレコードのレコード番号が分かっているので、 updateRecordByID() メソッドでレコードを更新します。

// 更新を実行
const result = await kintoneRecord
  .updateRecordByID({
    app: kintone.app.getId(),
    id: recordId,
    record: {
      確度: { value: toGroup }
    }
  })
  .catch((e: object) => {
    window.alert(e);
  });

メソッドにアプリ ID とレコード番号と変更するフィールドの値を引き渡すだけの、実に簡単なメソッドです。
これだけで簡単にレコードを更新できます。
ここではあくまでサンプルと言う事でエラー処理を window.alert() でやっていますが、ちゃんとしたアプリにするならもっと気の利いた実装にしましょう。

このような実装を加える事で、手で移動したカードがブラウザリロード後もそのリストに並んでいるのが確認できるはずです。

参考までに、これを従来の JavaScript API から kintone REST API を呼ぶ形式で実装したコードを見てみましょう。

// 更新を実行
const result = await kintone.api(kintone.api.url("/k/v1/record", true), "PUT", {
    app: kintone.app.getId(),
    id: recordId,
    record: { 確度: { value: toGroup } }
  })
  .catch((e) => {
    window.alert(e);
  });

え、あんまり変わってないじゃないかって?
確かにそうかも知れません。
けれども、 文字列とメソッドの組み合わせで API を指定するよりもオブジェクトの関数でやりたい事を明示的に呼び出す書き方の方が可読性が高くコードとしてクリーンであると言えるのではないでしょうか。

プロジェクトをビルドしてアプリに適用する

さて、ここまででアプリの実装はひと段落です。
このシリーズでは、VS Code の拡張機能である Live Server を使用してローカルで生成されたファイルを kintone 上で表示するやり方で開発を進めていました。
この方式では当然本運用はできないので、ここまでの開発成果を ビルド してアプリに適用しましょう。
VS Code のターミナルウィンドウで以下のように実行します。

% yarn build

10〜20 秒ほどで、 dist/ フォルダの下に以下のようなファイルが作成されます。
01-001-build.png
これらのファイルは難読化及び圧縮化が施されており容易にリバースエンジニアリングできないものになっています。
また、ファイル名にはキャッシュ除けのランダムな文字列が含まれています。この辺は webpack の設定で付けないように(常に固定名に)したりできますが、今回は説明を省きます。

出来上がった JS ファイルと CSS ファイルをアプリに適用します。
resized-01-002-apply.png
これでアプリを更新すれば、ビルド前の状態と同じ動作をする事が確認できるはずです。

まとめ

ここまででアプリの実装はひと段落です。
Vue.jsTypeScriptkintone JS SDK を使い、kintone のカスタマイズを実装する一連の流れについて説明して来ました。
もちろんここまでの実装でこのアプリは実用レベルで使えるかと言うと、実際にはそんな事はありません。
例えば、

  • 複数人でアプリを触っていた際、別の人がカードを動かしていたのを知らずに自分も動かしてしまってエラーが出てしまった
  • あるいは他の人がレコードを編集したためエラーが出てしまった
  • そもそもカードから別ウィンドウでレコード詳細に飛ぶ機能があるが、そちらでの変更結果はリロードしなければカスタマイズビューには反映されない
  • 担当者以外はカードを動かせないようにしたい
  • 1 つのリスト内で順番を任意に並び替えたい
  • あるいは小計の降順など特定の条件でソートしたい
  • リスト内のカードの小計の合計値をリストに表示したい
  • 確度以外の条件でグルーピングしたい

など、ちょっと考えれば多数の問題点・改善点が見出せます。
この辺りはビジネス上の要求やアプリを使用するユーザー層によっても最適解が変わって来る部分でしょう。

次回は

というわけで、今回は Vue.jsTypeScript で開発するプロジェクトを kintone で運用に載せるところまでを見て来ました。
一昔前まではごくごく普通だった生の JavaScript ファイルを書いてアプリに適用して複数のブラウザで動作確認して・・・と言う開発の進め方と較べると、これらモダンなテクノロジーの採用による恩恵は計り知れないと言わざるを得ません。

しかし、こう言った進め方をすれば効率よくバグのない開発ができるかと言えば、それは言い過ぎです。
ここまでの解説では テスト に関する観点がまるっきり存在していません。

と言うわけで、最終回(予定)となる次回は、kintone カスタマイズを Jest でモダンにテストする手法について解説していこうと思います。

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

300回プログラムじゃんけんでを戦わせるWebアプリを作った

作ったアプリ

https://janken-programmer.web.app/

遊び方

これはじゃんけんの出す"手"をプログラミングして戦わせるアプリです。

例えばグーだけを出すA君のプログラムと
グーとパーを交互に出すB君のプログラム
を300回戦わせると

B君が勝ちます

こんな感じでじゃんけんのアルゴリズムを考えて戦わせるアプリです

A君のプログラム

HAND = 0

B君のプログラム

HAND = 1
if COUNT % 2 == 0:
    HAND = 0

戦わせると・・・
スクリーンショット 2019-12-10 12.36.10.png
こんな結果になります。

これをWebで遊べるアプリです。

システム構成

今回使ったものは以下の通りになりました。

  • Vue.js
  • Buefy
  • AWS
  • serverless framework
  • dynamodb
  • firebase
  • circleCI

フロントエンドの部分はVueとBeufyを使いました。
同じような画面を作ることが多かったのでサクサク作れました。

バックエンドではAWSのサービスをserverless frameworkで構築しました。
アクセスが不安定なサービスになりそうなのでこちらのフレームワークを使いました。

ホスティングと認証にはfirebaseを使用しました。
本当はAWSなのですべてまとめたほうがいいのかもしれませんが
firebaseは無料で結構使えるのでこちらを採用しました。

無料枠などもあり、すべて無料でできました。
本当はRDBを使いたかったのですが、お金がないので無理やりdynamodbで作りました(反省)

機能

じゃんけんプログラム

じゃんけんのアルゴリズムを実装するにあたり、システム変数的なものを用意することにしました。

変数名 概要
HAND Intger 0~2 の数値を代入することでグーチョキパーを出せる
WIN Intger 勝った数
LOSE Intger 負けた数
DROW Intger あいこの数
P List 自分が出した手の履歴
E List 相手が出した手の履歴
COUNT Intger 対戦数

これを実装したことにより100回負けたらグーを出すみたいなプログラムを書けます

HAND = 1
if LOSE >= 100:
    HAND = 0

コーディング画面

ハイライトやインデントの機能が欲しかったため今回は「Codemirror」を使用させていただきました。

Vueで使用する場合は「vue-codemirror」を使うと単一コンポーネントで使用できるため、気持ちよく作れました。

ランキング機能

ランキングの実装には「イロレーティング」を使いました。

イロレーティングは対戦型の競技に使用されており、相対的な強さを評価します。

そのため、今回のランキングではこちらを使用し投稿されたプログラムを総当たりさせて順位を決定しました。

引用:イロレーティング
https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%AD%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

認証機能

今回は一人が複数のプログラムを投稿できないように認証機能を追加しました。

Firebase Authenticationを使用しGithubで認証できるようにしました。

感想

自分の想定しているものが一通りできたので、だいぶ満足しています。

ただセキュリティやじゃんけんのプログラムに不備があるかもしれないのでこれからも運営、開発をしていきたいと思います。

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

vue.jsでtextareaの高さを動的に変える

textarea_rowsプロパティを持つhogeコンポーネントがあるとする。

サーバーサイドから \n の数をカウントして返すなどし、textarea_rowsに代入したとする。

hoge_component.vue
<!-- textarearows属性に、computedtextarea_rowsの戻り値を適用する -->
                                      <!-- v-bindは省略しても良い :rows="textarea_rows" -->
  <b-form-textarea id="conversation-label" v-bind:rows="textarea_rows" v-model="hoge.conversation"></b-form-textarea>

  computed: {
    textarea_rows: function() {

      // textarea_rowsの\nに +1 をして返す
      return this.hoge.textarea_rows + 1
    }
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでDIっぽいことをして共通関数を作る

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の16日目の記事です。

はじめに

Vue.jsでの処理の共通化といったら、Mixinが有名です。

しかし、asyncData関数の中では参照することができなっかたり、TSでデコレーターを使用している場合はVueインスタンスでMixinsクラスを継承する必要があったりと、少し不便なところもあります。

Nuxtのpluginを実装することで、DIっぽいことをしてどこでも関数が使えることが分かったので、その方法を紹介していきます。
(Nuxtで明示的にDIの機構が用意されているわけではないのでDIっぽいこと、としています。)

公式のソースはこちらです。
https://typescript.nuxtjs.org/cookbook/plugins.html#plugins

環境

Nuxt.js 2.10.2
TypeScript 3.7.3

※ Nuxt + TypeScript の初期構築が完了していることを前提とします。
vue-property-decorator を使用したクラスベース、デコレータ方式で実装しています。

contextへInjectする方法

プラグインの作成

/plugins配下にTSファイルを作ります。

contextInject.ts
import { Plugin } from '@nuxt/types'

declare module '@nuxt/types' {
  interface Context {
    $contextInjectedFunction(name: string): string
  }
}

const myPlugin: Plugin = (context) => {
  context.$contextInjectedFunction = (name: string) => name + 'さん、おはよう!'
}

export default myPlugin

@nuxt/typesパッケージにあるContextインターフェースには$myInjectedFunctionなんてプロパティは存在しないので、declare moduleで新たに定義してあげます。

ちなみに、Contextの中身はこのようになっています。

app/index.d.ts
export interface Context {
  app: NuxtAppOptions
  base: string
  /**
   * @deprecated Use process.client instead
  */
  isClient: boolean
  /**
   * @deprecated Use process.server instead
  */
  isServer: boolean
  /**
   * @deprecated Use process.static instead
  */
  isStatic: boolean
  isDev: boolean
  isHMR: boolean
  route: Route
  from: Route
  store: Store<any>
  env: Record<string, any>
  params: Route['params']
  payload: any
  query: Route['query']
  req: IncomingMessage
  res: ServerResponse
  redirect(status: number, path: string, query?: Route['query']): void
  redirect(path: string, query?: Route['query']): void
  redirect(location: Location): void
  error(params: NuxtError): void
  nuxtState: NuxtState
  beforeNuxtRender(fn: (params: { Components: VueRouter['getMatchedComponents'], nuxtState: NuxtState }) => void): void
}

context とは、asyncDatafetchなどのVueインスタンスが生成される前でもアクセスができるグローバルなオブジェクト、という認識で大丈夫かと思います。

https://ja.nuxtjs.org/api/context/

プラグインを有効化

nuxt.config.jspluginsに追加したファイルを定義することで、context へアクセス可能なときにはいつでも関数を使用することができます。

nuxt.congig.js
 /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts'
  ],

定義した関数を呼び出す

/pages/sample.vue
<template>
  <div>
    <h1>{{ goodMorning }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { goodMorning: app.context.$contextInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
}
</script>

引数のappcontext.$myInjectedFunctionがInjectされているため、どのページからも関数が呼び出されるようになります。

VueインスタンスへInjectする方法

Vueインスタンスに対してもInjectができるので紹介していきます。この場合はasyncDataからは参照することはできません。

プラグインの作成

/plugins/vueInstanceInject.ts
import Vue from 'vue'

declare module 'vue/types/vue' {
  interface Vue {
    $vueInjectedFunction(name: string): string
  }
}

Vue.prototype.$vueInjectedFunction = (name: string) => name + 'さん、こんにちは!'

今度は、Vueインターフェースに対して関数を追加します。

Vueの型定義はこのようになっています。

vue.d.ts
export interface Vue {
  readonly $el: Element;
  readonly $options: ComponentOptions<Vue>;
  readonly $parent: Vue;
  readonly $root: Vue;
  readonly $children: Vue[];
  readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };
  readonly $slots: { [key: string]: VNode[] | undefined };
  readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
  readonly $isServer: boolean;
  readonly $data: Record<string, any>;
  readonly $props: Record<string, any>;
  readonly $ssrContext: any;
  readonly $vnode: VNode;
  readonly $attrs: Record<string, string>;
  readonly $listeners: Record<string, Function | Function[]>;

  $mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
  $forceUpdate(): void;
  $destroy(): void;
  $set: typeof Vue.set;
  $delete: typeof Vue.delete;
  $watch(
    expOrFn: string,
    callback: (this: this, n: any, o: any) => void,
    options?: WatchOptions
  ): (() => void);
  $watch<T>(
    expOrFn: (this: this) => T,
    callback: (this: this, n: T, o: T) => void,
    options?: WatchOptions
  ): (() => void);
  $on(event: string | string[], callback: Function): this;
  $once(event: string | string[], callback: Function): this;
  $off(event?: string | string[], callback?: Function): this;
  $emit(event: string, ...args: any[]): this;
  $nextTick(callback: (this: this) => void): void;
  $nextTick(): Promise<void>;
  $createElement: CreateElement;
}

いつもVueコンポーネントで呼び出す関数たちが定義されています。
declare moduleで$vueInjectedFunctionという関数を新たに追加したことになります。

プラグインの有効化

nuxt.config.jsにプラグインを追加します。

nuxt.config.js
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts',
    '~/plugins/vueInstanceInject.ts'
  ],

定義した関数を呼び出す

sample.vue
<template>
  <div>
    <h1>{{ goodMorning }}</h1>
    <h1>{{ hello }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { goodMorning: app.context.$contextInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
  hello: string = ''

  created() {
    this.hello = this.$vueInjectedFunction('misaosyushi')
  }
}
</script>

VueインスタンスにInjectしたので、thisでアクセスができるようになります。

context, Vueインスタンス, VuexストアにInjectする方法

context や Vueインスタンス、Vuexストア内でも関数が必要な場合、inject関数を使用することで共通関数を作ることができます。

プラグインの作成

combinedInject.ts
import { Plugin } from '@nuxt/types'

declare module 'vue/types/vue' {
  interface Vue {
    $combinedInjectedFunction(name: string): string
  }
}

declare module '@nuxt/types' {
  interface Context {
    $combinedInjectedFunction(name: string): string
  }
}

declare module 'vuex/types/index' {
  interface Store<S> {
    $combinedInjectedFunction(name: string): string
  }
}

const myPlugin: Plugin = (context, inject) => {
  inject('combinedInjectedFunction', (name: string) => name + 'さん、おはこんばんにちは!')
}

export default myPlugin

新たに、Storeインターフェースに対して共通化したい関数を定義し、inject関数に追加します。

Plugin型を見ると、injectの第1引数に関数名、第2引数に関数を渡せば良いことがわかります。

types/app/index.d.ts
export type Plugin = (ctx: Context, inject: (key: string, value: any) => void) => Promise<void> | void

プラグインの有効化

nuxt.config.jsにプラグインを追加します。

nuxt.config.js
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts',
    '~/plugins/vueInstanceInject.ts',
    '~/plugins/combinedInject.ts'
  ],

storeを作成する

Vuexストアを使用するため、/store配下にindex.tsを作成します。

/store/index.ts
export const state = () => ({
  storeMessage: ''
})

export const mutations = {
  changeValue (state: any, newValue: any) {
    state.storeMessage = this.$combinedInjectedFunction(newValue)
  }
}

プラグインを定義したことにより、mutations内の this を通して$combinedInjectedFunction関数が使用できるようになっています。

定義した関数を呼び出す

combinedSample.vue
<template>
  <div>
    <h1>{{ contextMessage }}</h1>
    <h1>{{ vueMessage }}</h1>
    <h1>{{ $store.state.storeMessage }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { contextMessage: app.$combinedInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
  vueMessage: string = ''

  created () {
    this.vueMessage = this.$combinedInjectedFunction('misaosyushi')
    this.$store.commit('changeValue', 'misaosyushi')
  }
}
</script>

これで、Context, Vueインスタンス, Vuexストア それぞれで共通関数が使えるようになっていることがわかります。

プラグインのinject関数を使用した場合、context の共通関数はcontext.appに注入されるため、asyncData内でapp.$combinedInjectedFunctionで参照できるようです。

公式のTIPにしれっと書いてあります。

https://typescript.nuxtjs.org/cookbook/plugins.html#usage-3

まとめ

いままでVue.jsで共通化といったらMixin!でしたが、Nuxtの場合はプラグインのほうが実装もシンプルにできるかなと思います。

また、使用先でわざわざインポートする必要がないため使い勝手が良く、さらに型定義のおかげで補完が効くのでコーディングが捗ります。

とても便利な機能なので、試したことのない方は是非やってみてください!

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

Vue.jsの$listeners

vm.$listenersって何?

子コンポーネントで色々なイベントをlistenしてくれます。

sample code

https://github.com/tarunama/vm-listners

どういうこと?

親コンポーネントで、子コンポーネントがclick or mouseoverされたらmethodを実行するようにしています。

ParentComponent
<template>
  <div class="app">
    <p>{{ clickCountText }}</p>  
    <p>{{ clickMouseOverText }}</p>
    <!-- 子コンポーネント -->
    <child-component
      @click="incrementClickCount()"
      @mouseover="incrementMouseOverCount()"
    ></child-component>
  </div>
</template>

<script>
import ChildComponent from './components/ChildComponent.vue'

export default {
  name: 'app',
  components: {
    ChildComponent
  },
  data() {
    return {
      clickCount: 0,
      mouseOverCount: 0
    }
  },
  computed: {
    clickCountText() {
      return `clickCount: ${this.clickCount}`
    },
    clickMouseOverText() {
      return `mouseOverCount: ${this.mouseOverCount}`
    }
  },
  methods: {
    incrementClickCount() {
      this.clickCount++
    },
    incrementMouseOverCount() {
      this.mouseOverCount++
    }
  }
}
</script>

<style>
.app {
  text-align: center;
}
</style>

vm.$emitで親コンポーネントにこんなイベントが起きたよー!と教えています。

ChildComponent
<template>
  <button
    @click="$emit('click')"
    @mouseover="$emit('mouseover')"
  >
    button
  </button>
</template>

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

vm.$listenersを使うと

ChildComponent
<template>
  <!-- ここ -->
  <button v-on="$listeners">button</button>
    button
  </button>
</template>

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

1行で済んだ、嬉しいー!!ってなります。

参考

https://jp.vuejs.org/v2/api/#vm-listeners
https://www.youtube.com/watch?v=YatSGkmiLRI

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

v-onceで一度だけ描画。その後は変えたくない! ❏Vue.js❏

v-onceとは

最初に描画したものだけを永遠に表示します。
その後に代入して値を変えることはできません。

使い方

タグの中にv-onceを書く。

開発環境はJSFiddleです。
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3b

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

<div id="app">
  <p v-once>{{ message }}</p>
  <p>{{ sayHi() }}</p>
</div>
javascript
new Vue({
  el: "#app",
  data: {
    message: "hello world!"
  },
  methods: {
    sayHi: function() {
      this.message = "hello Vue.js!"
      return this.message;
    }
  }
})

【出力結果】
hello world!
hello Vue.js!

解説

sayHiメソッドでthis.message = "hello Vue.js!"と値を書き換えていまス。
しかし、v-onceを書いた1つ目のpタグの表示はhello world!のままで変わりません。

2つ目のpタグは書き換えられたmessageが返りhello Vue.js!と表示されます。



いつか使うときを信じて、ここにお納めします。。



ではまた!

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

Vue.jsでFF7風のポートフォリオを作った

作ったもの

ff7.gif

経緯

ff7のリメイクがもうすぐ発売されますね!!!
早くプレイしたいです。

待ち遠しさを紛らわすために、遊び心でポートフォリオとして作成してみました。
マテリアをCSSで記載するなど、しなくてよい努力を詰め込んでますw

使ったもの

  • Vue.js
  • Nuxt.js
  • Vuetify
  • Typescript
  • Scss

フロントの勉強かつ、Typescriptの練習を兼ねて上記のような構成にしています。

Top画面の説明

ff7_top.png

メニュー画面にマウスカーソルを合わせると、あのお馴染みのカーソルを表示するようにしています。クリックするとページを遷移するように作成していますが、現在は「マテリア」ページのみ作成しております。

背景色

ff7の背景色は、下記のscssで作成しています。ff7-cardクラスを指定すれば、あの青い背景色になります。

$text-color: #eff1ff;
$background-color: #04009d;
$background-color-dark: #06004d;

.ff7-card {
    border: solid 1px #424542;
    box-shadow: 1px 1px #e7dfe7, -1px -1px #e7dfe7, 1px -1px #e7dfe7,
      -1px 1px #e7dfe7, 0 -2px #9c9a9c, -2px 0 #7b757b, 0 2px #424542;
    padding: 5px 10px;

    background: $background-color;
    background: -moz-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -webkit-gradient(
      linear,
      left top,
      left bottom,
      color-stop(0%, $background-color),
      color-stop(100%, $background-color-dark)
    );
    background: -webkit-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -o-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -ms-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: linear-gradient(to bottom, $background-color 0%, $background-color-dark 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$background-color', endColorstr='$background-color-dark',GradientType=0 );

    -webkit-border-radius: 7px;
    -moz-border-radius: 7px;
    border-radius: 7px;
    * {
        color: $text-color;
        text-shadow: 2px 2px #212421, 1px 1px #212021;
        font-family: Verdana, sans-serif;
        font-weight: normal;    
    }
}

メニューの部分

メニューの部分を説明していきます。

<template>
  <section>
    <v-container class="top-menu-list ff7-card">
      <v-row
        v-for="(item, index) in topMenuItems"
        :key="index"
        no-gutters
        @mouseleave="menuMouseleave(index)"
        @mouseover="menuMouseover(index)"
      >
        <v-col cols="12">
          <cursor-parts v-if="menuChoice == index" />
          <nuxt-link
            v-if="item.display == true"
            :to="item.path"
            class="top-menu-item"
          >
            {{ item.name }}
          </nuxt-link>
        </v-col>
      </v-row>
    </v-container>
  </section>
</template>

topMenuItemsの内容を表示するようにしています。また、マウスカーソルの操作は@mouseleave @mouseoverで対応しています。クリックした場合nuxt-linkで指定先に遷移します。

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import CursorParts from '~/components/parts/CursorParts.vue'

@Component({
  components: { CursorParts }
})
export default class TopMenu extends PortfolioVueEx {
  menuChoice: number = -1
  topMenuItems: {
    name: string
    path: string
    display: boolean
  }[] = [
    {
      name: 'アイテム',
      path: '/',
      display: true
    },
    {
      name: 'まほう',
      path: '/',
      display: true
    },
    {
      name: 'マテリア',
      path: 'materia',
      display: true
    },
    {
      name: 'そうび',
      path: '/',
      display: true
    },
    {
      name: 'ステータス',
      path: '/',
      display: true
    },
    {
      name: 'たいけい',
      path: '/',
      display: true
    },
    {
      name: 'リミット',
      path: '/',
      display: true
    },
    {
      name: 'コンフィグ',
      path: '/',
      display: true
    },
    {
      name: 'PHS',
      path: '/',
      display: true
    },
    {
      name: 'セーブ',
      path: '/',
      display: true
    }
  ]

  menuMouseover(i: number) {
    this.menuChoice = i
  }
  menuMouseleave() {
    this.menuChoice = -1
  }
}
</script>

<style scoped lang="scss">
.top-menu-item {
  margin: 10px;
}

.top-menu-list {
  text-align: left;
  width: 143px;
}

a:link {
  text-decoration: none;
  color: white;
}

a:visited {
  text-decoration: none;
  color: white;
}
</style>

メニューの内容は、topMenuItemsにリスト型で保持しています。

キャラクター

TopTeam.vue
<template>
  <section>
    <v-container class="top-team-list ff7-card">
      <v-row
        v-for="(item, index) in $store.state.players"
        :key="index"
        align="center"
      >
        <v-col cols="4">
          <v-row justify="center">
            <img :src="item.image" width="75" />
          </v-row>
        </v-col>
        <v-col cols="3">
          {{ item.name }}{{ $store.state.level }}
          <v-row>
            <span class="status-item">LV</span>
            <span class="level-margin status-content">{{ item.level }}</span>
          </v-row>
          <v-row>
            <span class="status-item">HP</span>
            <span class="status-content">
              <span>
                {{ item.hp }}/{{ item.maxHp }}
                <div class="progress-linear">
                  <progress-hp-parts
                    :parent-max.sync="item.maxHp"
                    :parent-value.sync="item.hp"
                  />
                </div>
              </span>
            </span>
          </v-row>
          <v-row>
            <span class="status-item">MP</span>
            <span class="status-content">
              <span class="mp-margin">{{ item.mp }}/</span>
              <span class="mp-margin">{{ item.maxMp }}</span>
              <div class="progress-linear">
                <progress-mp-parts
                  :parent-max.sync="item.maxMp"
                  :parent-value.sync="item.mp"
                />
              </div>
            </span>
          </v-row>
        </v-col>
        <v-col cols="3">
          <v-row>
            <span class="next-level">つぎのレベルまであと</span>
            <progress-parts
              :parent-max.sync="item.maxExp"
              :parent-value.sync="item.exp"
              class="next-level-margin"
            />
          </v-row>
          <br />
          <v-row>
            <span class="limit">リミットレベル {{ item.limitLevel }}</span>
            <progress-parts
              :parent-max.sync="item.maxLimit"
              :parent-value.sync="item.limit"
              class="limit-margin"
            />
          </v-row>
        </v-col>
      </v-row>
    </v-container>
  </section>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import ProgressParts from '~/components/parts/ProgressParts.vue'
import ProgressHpParts from '~/components/parts/ProgressHpParts.vue'
import ProgressMpParts from '~/components/parts/ProgressMpParts.vue'

@Component({
  components: {
    ProgressParts,
    ProgressHpParts,
    ProgressMpParts
  }
})
export default class TopTeam extends PortfolioVueEx {
  menuChoice: number = -1
}
</script>

<style scoped lang="scss">
.top-team-list {
  padding: 0px 0px 0px 30px;
  width: 457px;
  height: 360px;
}

.progress-linear {
  margin: -15px 0px 0px 0px;
}

.status-item {
  color: #00ddd6;
  margin-right: 10px;
  font-size: 100%;
}
.status-content {
  font-size: 80%;
}
.level-margin {
  margin-top: 3px;
  margin-left: 18px;
}
.mp-margin {
  margin-left: 6px;
}

.next-level {
  font-size: 60%;
}
.next-level-margin {
  margin-left: 15px;
  margin-right: 0px;
}

.limit {
  font-size: 60%;
}
.limit-margin {
  margin-left: 15px;
  margin-right: 0px;
}
</style>

'store'でキャラクターの情報(LV/HP/MPなど)を保持しており、それらを呼び出すようにしています。

ゲージは別途コンポーネントを作成しております。
- ProgressHpPartsは、HPのゲージ
- ProgressMpPartsは、MPのゲージ
- ProgressPartsは、経験値とリミットのゲージ

マテリア画面の説明

ff7_materia.png

マテリアにマウスカーソルを合わせると、カーソルとマテリアの説明が表示されます。マテリア毎に、自分のスキルを乗せるようにしてみました。
詳細説明とマテリアリストの部分は時間が足りなかったので、今後追加していきます。

ちなみに、マテリアを外すと下記のようになります。マテリア穴までCSSで作成しています。見えないところにもこだわる!
ff7_no_materia.png

マテリア

マテリアはコンポーネントとして作成しています。画像でしたらはるかに楽なのですが、CSSで無駄に開発しました。

MateriaParts.vue
<template>
  <div class="wrapper">
    <div class="materia" :style="{ background: color }" />
    <div class="downlight1" />
    <div class="highlight2" />
    <div class="highlight3" />
  </div>
</template>

<script lang="ts">
import { Component, Prop } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'

@Component({})
export default class MateriaParts extends PortfolioVueEx {
  @Prop()
  color: string
}
</script>

<style scoped lang="scss">
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background: linear-gradient(135deg, #f7f9fc 0%, #e1e7f0 100%);
  display: flex;
  justify-content: center;
  align-items: center;
}

.wrapper {
  width: 20px;
  height: 20px;
  position: relative;
}
.materia {
  width: 20px;
  height: 20px;
  z-index: 0;
  border-radius: 50%;
  background: rgb(43, 100, 21, 1);
}
.downlight1 {
  position: absolute;
  top: 10%;
  left: 15%;
  z-index: 1;
  width: 12px;
  height: 12px;
  background: rgba(0, 0, 0, 1);
  border-radius: 50%;
  filter: blur(2px);
  opacity: 0.6;
}
.highlight2 {
  position: absolute;
  top: 20%;
  left: 25%;
  z-index: 1;
  width: 3px;
  height: 1px;
  border-radius: 50%;
  border-top: 0.5px solid #fff;
  transform: rotate(-70deg) scaleX(0.9) scaleY(1.5) skewY(18deg);
  filter: blur(0.6px);
}
.highlight3 {
  position: absolute;
  top: 0%;
  left: 0%;
  width: 18px;
  height: 18px;
  background-color: transparent;
  box-shadow: inset -3px -6px 0 -3px rgba(255, 255, 255, 1);
  border-radius: 50%;
  filter: blur(1px);
  opacity: 0.15;
}
</style>

球型に、ハイライトを2種類、ダウンライトを1種類を乗せてマテリアを表現しています。マテリアの色は、Propで指定できるようにしています。

魔法マテリア

各マテリアは、MateriaParts.vueに色を指定しています。魔法マテリアは下記になります。

MagicMateria.vue
<template>
  <materia-parts :color="'rgb(43, 100, 21, 1)'" />
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import MateriaParts from '~/components/parts/MateriaParts.vue'

@Component({
  components: { MateriaParts }
})
export default class MagicMateria extends PortfolioVueEx {}
</script>

マテリア画面

突貫で作ったので、改良の余地がありまくりです。。。

materia.vue
<template>
  <v-container>
    <v-row align="center" justify="center">
      <div class="materia-box">
        <div class="character-box ff7-card">
          <v-row align="center">
            <v-col cols="2">
              <v-row justify="center">
                <img :src="$store.state.players[0].image" width="75" />
              </v-row>
            </v-col>
            <v-col cols="3">
              {{ $store.state.players[0].name }}{{ $store.state.level }}
              <v-row>
                <span class="status-item">LV</span>
                <span class="level-margin status-content">
                  {{ $store.state.players[0].level }}
                </span>
              </v-row>
              <v-row>
                <span class="status-item">HP</span>
                <span class="status-content">
                  <span>
                    {{ $store.state.players[0].hp }}/{{
                      $store.state.players[0].maxHp
                    }}
                    <div class="progress-linear">
                      <progress-hp-parts
                        :parent-max.sync="$store.state.players[0].maxHp"
                        :parent-value.sync="$store.state.players[0].hp"
                      />
                    </div>
                  </span>
                </span>
              </v-row>
              <v-row>
                <span class="status-item">MP</span>
                <span class="status-content">
                  <span class="mp-margin">
                    {{ $store.state.players[0].mp }}/
                  </span>
                  <span class="mp-margin">
                    {{ $store.state.players[0].maxMp }}
                  </span>
                  <div class="progress-linear">
                    <progress-mp-parts
                      :parent-max.sync="$store.state.players[0].maxMp"
                      :parent-value.sync="$store.state.players[0].mp"
                    />
                  </div>
                </span>
              </v-row>
            </v-col>
            <v-col cols="7">
              <v-row>
                <span class="attack-margin">
                  <span class="status-item">武器:</span>
                  <span>ノートPC</span>
                </span>
              </v-row>
              <v-row class="equipment-margin">
                <div
                  v-for="(item, index) in attackMaterias"
                  :key="index"
                  no-gutters
                  class="equipment"
                  @mouseleave="menuMouseleave(index)"
                  @mouseover="menuMouseover(item, index)"
                >
                  <cursor-parts v-if="menuChoice == index" />
                  <command-materia v-if="item.type == 1" class="content" />
                  <independent-materia
                    v-else-if="item.type == 2"
                    class="content"
                  />
                  <magic-materia v-else-if="item.type == 3" class="content" />
                  <summon-materia v-else-if="item.type == 4" class="content" />
                  <support-materia v-else-if="item.type == 5" class="content" />
                  <div v-if="item.type !== 0" class="highlight4" />
                </div>
              </v-row>
              <v-row>
                <span class="defence-margin">
                  <span class="status-item">防具:</span>
                  <span>お供のコーヒー</span>
                </span>
              </v-row>
              <v-row class="equipment-margin">
                <div
                  v-for="(item, index) in defenceMaterias"
                  :key="index"
                  no-gutters
                  class="equipment"
                  @mouseleave="menuMouseleave(index + 8)"
                  @mouseover="menuMouseover(item, index + 8)"
                >
                  <cursor-parts v-if="menuChoice == index + 8" />
                  <command-materia v-if="item.type == 1" class="content" />
                  <independent-materia
                    v-else-if="item.type == 2"
                    class="content"
                  />
                  <magic-materia v-else-if="item.type == 3" class="content" />
                  <summon-materia v-else-if="item.type == 4" class="content" />
                  <support-materia v-else-if="item.type == 5" class="content" />
                  <div v-if="item.type !== 0" class="highlight4" />
                </div>
              </v-row>
            </v-col>
          </v-row>
        </div>
        <div class="message-box ff7-card">
          <v-row v-if="selectedMateria !== ''">
            <command-materia class="content" v-if="selectedMateria.type == 1" />
            <independent-materia
              class="content"
              v-else-if="selectedMateria.type == 2"
            />
            <magic-materia
              class="content"
              v-else-if="selectedMateria.type == 3"
            />
            <summon-materia
              class="content"
              v-else-if="selectedMateria.type == 4"
            />
            <support-materia
              class="content"
              v-else-if="selectedMateria.type == 5"
            />
            {{ selectedMateria.name }}
          </v-row>
        </div>
        <div class="materias-box ff7-card"></div>
        <div class="status-box ff7-card">
          {{ description }}
        </div>
        <div class="page-box ff7-card">
          マテリア
        </div>
      </div>
    </v-row>
  </v-container>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PageBase from '~/logic/vue/PageBase'
import CommandMateria from '~/components/templates/CommandMateria.vue'
import IndependentMateria from '~/components/templates/IndependentMateria.vue'
import MagicMateria from '~/components/templates/MagicMateria.vue'
import SummonMateria from '~/components/templates/SummonMateria.vue'
import SupportMateria from '~/components/templates/SupportMateria.vue'
import ProgressParts from '~/components/parts/ProgressParts.vue'
import ProgressHpParts from '~/components/parts/ProgressHpParts.vue'
import ProgressMpParts from '~/components/parts/ProgressMpParts.vue'
import CursorParts from '~/components/parts/CursorParts.vue'

@Component({
  components: {
    CommandMateria,
    IndependentMateria,
    MagicMateria,
    SummonMateria,
    SupportMateria,
    ProgressParts,
    ProgressHpParts,
    ProgressMpParts,
    CursorParts
  }
})
export default class Materia extends PageBase {
  selectedMateria: any = ''
  description!: string
  details!: string
  menuChoice: number = -1
  attackMaterias: {
    name: string
    description: string
    details: string
    type: number
  }[] = [
    {
      name: 'Golang',
      description: 'Golang を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Solidity(Ethereum)',
      description: 'Solidity(Ethereum) を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Python',
      description: 'Python を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'C言語',
      description: 'C言語を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Java',
      description: 'Java を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Mysql',
      description: 'Mysqlを使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Typescript',
      description: 'Typescriptを使えます。',
      details: '',
      type: 1
    },
    {
      name: 'SCSS',
      description: 'SCSS を使えます。',
      details: '',
      type: 1
    }
  ]
  defenceMaterias: {
    name: string
    description: string
    details: string
    type: number
  }[] = [
    {
      name: 'AWS',
      description: 'Amazon Web Services を使えます。',
      details: '',
      type: 4
    },
    {
      name: 'Kubernetes',
      description: 'Kubernetesを使えます。',
      details: '',
      type: 4
    },
    {
      name: 'SpringBoot',
      description: 'SpringBoot のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Vue.js',
      description: 'Vue.js のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Nuxt.js',
      description: 'Nuxt.js のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Vuetify',
      description: 'Vuetify のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'GitHub',
      description: 'GitHubを使えます。',
      details: '',
      type: 5
    },
    {
      name: 'Unity',
      description: 'Unity を使えます。',
      details: '',
      type: 5
    }
  ]

  menuMouseover(item: any, i: number) {
    this.menuChoice = i
    this.selectedMateria = item
    this.description = item.description
  }
  menuMouseleave() {
    this.menuChoice = -1
  }
}
</script>

<style scoped lang="scss">
.materia-box {
  position: relative;
}

.character-box {
  position: relative;
  width: 600px;
  height: 150px;
  margin: 6px 0 6px 0;
}
.message-box {
  position: relative;
  z-index: 2;
  width: 386px;
  height: 300px;
  margin: 0px 0px 0px 0px;
  padding: 60px 0px 0px 20px;
  float: left;
  .content {
    margin: 0px 6px 0px 0px;
  }
}
.materias-box {
  position: relative;
  z-index: 1;
  left: 380px;
  width: 220px;
  height: 300px;
  padding: 60px 0px 0px 15px;
}
.status-box {
  position: relative;
  z-index: 3;
  width: 600px;
  height: 50px;
  top: -300px;
  display: flex;
  align-items: center;
}
.page-box {
  position: relative;
  z-index: 2;
  width: 150px;
  height: 40px;
  top: -506px;
  left: 450px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.progress-linear {
  margin: -15px 0px 0px 0px;
}

.status-item {
  color: #00ddd6;
  margin-right: 10px;
  font-size: 100%;
}
.status-content {
  font-size: 80%;
}
.level-margin {
  margin-top: 3px;
  margin-left: 18px;
}
.mp-margin {
  margin-left: 6px;
}
.attack-margin {
  margin: 6px 0px 6px 0;
}
.defence-margin {
  margin: 6px 0px 6px 0;
}
.equipment {
  margin: 0px 3px 0px 3px;
  width: 24px;
  height: 24px;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(
    closest-side at 49% 49%,
    rgb(150, 150, 150) 0%,
    rgb(40, 40, 40) 25%,
    rgb(40, 40, 40) 70%,
    rgb(100, 100, 100) 92%
  );
  background-color: transparent;
  box-shadow: inset 6px 6px 2px -6px rgba(200, 200, 200, 1);
  .content {
    position: relative;
    top: 2.2px;
    left: 1px;
  }
}
.equipment-margin {
  margin-left: 30px;
}
.highlight4 {
  position: relative;
  top: -40%;
  left: 40%;
  width: 4px;
  height: 4px;
  z-index: 2;
  border-radius: 50%;
  background: rgba(150, 150, 150, 1);
  filter: blur(1px);
}
</style>

attackMateriasdefenceMateriasにマテリアを指定しています。typeでマテリアの種類を指定します。ポートフォリオとして、プログラミングスキルを下記のように当てはめています。

  • 0: マテリアなし
  • 1: コマンドマテリア→ フロントスキル
  • 2: 独立マテリア → フレームワーク
  • 3: 魔法マテリア → バックエンドスキル
  • 4: 召喚マテリア → インフラ周りのスキル
  • 5: サポートマテリア→ ツール類

おわりに

思い付きで始めたものの、面白くできました。途中感は否めないですが、ひとまず形にはなりました。
何かアイディアある方はコメント頂けると嬉しいです。

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

SVG要素を包含したVueのコンポーネントでマウスイベントを発火させる方法

この記事は 「Vue Advent Calendar 2019 #1」 15日目の記事です。

概要

「SVG要素を包含したVueのコンポーネント」で、マウスイベントを発火させる方法をご紹介します。

とても簡単な内容に思えるのですが、mousedown 等のイベントを上位のコンポーネントで単純に v-on するだけではうまく動きませんでした。

要約(五七五)

イベントは $emit させると いい感じ

背景

Vue.jsとSVGを使って、超簡易ビジュアルプログラミング環境を開発中です。1
「ブロック」を「リンク」で繋ぐことで処理の流れを構築する、ビジュアルプログラミングとしてはよくある感じのやつです。

VlockFlow_DragAndDrop.gif

上図のようなものを描くには、「ブロック」を移動させたり「リンク」を繋いだりする操作が必要になります。つまり、ドラッグ&ドロップが必要です。

このとき、ドラッグ&ドロップしたいのは「 Vue のコンポーネント」です。例えば上記画像で動かしている、緑色で calc という文字列が書いてある「ブロック」は「 SVG の rect と text と ciecle を1つのブロックとしてコンポーネント化」してあります。

1つの rect だけをドラッグするようなサンプルはすぐ見つかります。しかしその方法を適用するだけでは、上記例なら緑色の角丸四角形だけが移動してしまい、calc という文字列や上下に付いている小さな丸は取り残されてしまいます。

それを解決するには、一工夫必要でした。

コンポーネント分割

今回のプロジェクトは、静的サイトとして generate するために Nuxt.js を採用しています。従って、コンポーネントはcomponentsフォルダの配下に入れることになります。さらに、SVG で表現する部分については Atomic Design の考え方を導入してコンポーネントを分割しました。2

全体は割愛しますが、ブロック関連の部分については、以下のように構成されています。

  • components/organisms
    • VFCanvas.vue   キャンバス。ブロック、リンクを配置する場所。
  • components/molecules
    • VFBlock.vue    ブロック。VFCrust, VFLabel, VFConnector を組み合わせて構築
  • components/atoms
    • VFCrust.vue    ベースとなる緑色の角丸四角形3
    • VFLabel.vue    ブロック名等を表示するためのラベル
    • VFConnector.vue ブロックの上下に付く、円形のコネクタ

マウスイベントを発火させる

ここからが本題です。

あるコンポーネントでマウスイベントを検知するなら、以下のようなコードを発想すると思います。

ParentComponent.vue
<template>
  <div>
    <svg>
      <SomeChildComponent
        v-for="item in items"
        :key="item.id"
        @mousedown="execMouseDown"
        @mouseup="execMouseUp"
        @mousemove="execMouseMove"
      />
    </svg>
  </div>
</template>

しかし、これだけではイベントが発火しません。Vue のコンポーネントは、そのままではマウスイベントを発火してくれないからです。

もう少し詳しくみていきます。

動かない例

<script>内のコードはここでは割愛。

VFCrust.vue
<!-- 最下層のコンポーネント -->
<!-- ここにはイベントリスナは記述していない -->
<template>
  <g>
    <rect
      :x="x"
      :y="y"
      :rx="rx"
      :ry="ry"
      :width="width"
      :height="height"
      :fill="fill"
    />
  </g>
</template>
VFBlock.vue
<!-- 中間のコンポーネント -->
<!-- ここにもイベントリスナは記述していない -->
<template>
  <g
    :x="x"
    :y="y"
  >
    <vf-crust
      :x="block_x"
      :y="block_y"
    />
    <vf-label
      :x="block_x"
      :y="block_y"
    />
    <vf-connector
      :x="block_x"
      :y="block_y"
    />
  </g>
</template>
VFCanvas.vue
<!-- 最上位のコンポーネント -->
<!-- mousedown, mouseup, mousemove を検出したい -->
<!-- マウスイベントを listen する v-onディレクティブはここにだけ記述 -->
<template>
  <div>
    <svg>
      <vf-block
        v-for="block in blocks"
        :key="block.id"
        :x="block.x"
        :y="block.y"
        @mousedown="execMouseDown"
        @mouseup="execMouseUp"
        @mousemove="execMouseMove"
      />
    </svg>
  </div>
</template>

このとき、デベロッパーツールで Event Listeners の状態を確認すると、どこにも mousedown, mouseup, mousemove のイベントが登録されていないことがわかります。

ここの g 要素が VFCanvas.vue での vf-block に相当するので、各イベントが登録されていてほしいのですが、、、
スクリーンショット 2019-12-15 2.37.49.png

ありません。
スクリーンショット 2019-12-15 2.35.07.png

配下の g 要素や rect 要素も同様に、mousedown, mouseup, mousemove のイベントは登録されていませんでした。

※補足:この時点では気づいていなかったのですが、動作するようになった最終形でも vf-block にはイベントリスナは登録されませんでした。DOM要素に追加されるわけではなく、Vueが内部でうまく処理してくれているだけのようです。(要研究)

一歩前へ(まだ動かない)

では、実際の DOM 要素である、SVG の rect 要素にイベントリスナを追加してみましょう。

VFCrust.vue
<!-- ここでマウスイベントを拾う -->
<template>
  <g>
    <rect
      :x="x"
      :y="y"
      :rx="rx"
      :ry="ry"
      :width="width"
      :height="height"
      :fill="fill"
      @mousedown="execMouseDown"
      @mousemove="execMouseMove"
      @mouseup="execMouseUp"
    />
  </g>
</template>

<script>
export default {
//(中略)
  methods: {
    execMouseDown: function(event){
      console.log("mousedown")
    },
    execMouseMove: function(event){
      console.log("mousemove")
    },
    execMouseUp: function(event){
      console.log("mouseup")
    },
  },
//(後略)
}
</script>

このコードを組み込んだ時、rect要素には mousedown, mousemove, mouseup の各イベントリスナが登録されました。

スクリーンショット 2019-12-15 3.18.16.png

しかし、実際に動作させると、上位側ではイベントを拾うことができませんでした。ドラッグ&ドロップを実現するには、上位コンポーネントでイベントをハンドリングしたいので、これでは不十分です。

動く例

では、どのようにすれば動くのでしょうか。

SVG の各要素( rect, circle 等)はマウス操作を検知してイベントを発火してくれますので、これらのSVG の各要素で発火したイベントを、上位のコンポーネントへ伝達していけば、うまく動きそうです。

伝達方法は、Vue公式のスタイルガイドにあるprops down, events upの原則に従い、$emitevents upしていく形にします。

このときthis.$emit(event.type, event)という形で引数をセットすると、上位コンポーネントでは、あたかもそのコンポーネント自身がマウスイベントを拾ったかのようにイベントをハンドリングすることができます。

VFCrust.vue
<!-- ここでマウスイベントを拾って、明示的にイベントを発火させる -->
<template>
  <g>
    <rect
      :x="x"
      :y="y"
      :rx="rx"
      :ry="ry"
      :width="width"
      :height="height"
      :fill="fill"
      @mousedown="raiseEvent"
      @mousemove="raiseEvent"
      @mouseup="raiseEvent"
    />
  </g>
</template>

<script>
export default {
//(中略)
//event.typeを第1引数、eventそのものを第2引数として渡すことで、上位に向けて同じイベントを発火できる
  methods: {
    raiseEvent: function(event){
      this.$emit(event.type, event)
    },
  },
//(後略)
}
</script>
VFBlock.vue
<!-- ここにもイベントリスナを記述して上位へ伝達する -->
<template>
  <g
    :x="x"
    :y="y"
  >
    <vf-crust
      :x="block_x"
      :y="block_y"
      @mousedown="raiseEvent($event, item_id)"
      @mousemove="raiseEvent($event, item_id)"
      @mouseup="raiseEvent($event, item_id)"
    />
    <vf-label
      :x="block_x"
      :y="block_y"
    />
    <vf-connector
      :x="block_x"
      :y="block_y"
    />
  </g>
</template>

<script>
import VfCrust from "../atoms/VFCrust.vue"

export default {
  name: "VFBlock",
  components: {
    VfCrust,
  },
  props: {
    item_id: {
      type: Number,
      default: 0
    },
  },
//(中略)
//ここのraiseEventでは、自分自身に付与されたitem_idも渡すようにしてある。
//どのブロックが操作されているのかを上位側で判断できるようにするため。
  methods: {
    raiseEvent: function(event){
      this.$emit(event.type, event, item_id)
    },
  },
//(後略)
}
</script>
VFCanvas.vue
<!-- mousedown, mouseup, mousemove を検出 -->
<!-- このファイルではマウスイベントの種類ごとに実施したい処理が異なるので、イベントハンドラも分けてある -->
<template>
  <div>
    <svg>
      <vf-block
        v-for="block in blocks"
        :key="block.id"
        :x="block.x"
        :y="block.y"
        :item_id="block.item_id"
        @mousedown="execMouseDown"
        @mouseup="execMouseUp"
        @mousemove="execMouseMove"
      />
    </svg>
  </div>
</template>

<script>
import VfBlock from "../molecules/VFBlock.vue"

export default {
  name: "VFCanvas",
  components: {
    VfBlock,
  },
//(中略)
  data() {
    return {
      is_draggable: false,
    }
  },

  methods: {
    execMouseDown: function(event, item_id){
      this.is_draggable = true
      //(略)
    },
    execMouseMove: function(event, item_id){
      //ドラッグを開始していない場合は処理なし
      if (!this.is_draggable) {
        return
      }
      //(略)
    },
    execMouseUp: function(event, item_id){
      this.is_draggable = false
      //(略)
    },
  },
//(後略)
}
</script>

まとめ

今回は、個人開発プロジェクトを進める中で発生した不明点のうち、調べてもなかなかズバリの回答がヒットしなかった部分を記事化してみました。

12月に入ってから偶然アドベントカレンダーの空きを見つけ、勢いで登録してから2週間弱。登録時点では構想だけでコード0行だった状態から実質12時間くらい試行錯誤して作った内容の一部をご紹介しました。

もし、もっと良い方法がありましたらお教え頂けますと幸いです。

最後までご覧いただきありがとうございました!

おまけ

ドラッグ&ドロップを実現するには他にも考慮することが多々あります。
リンクを追加する時に最も近いコネクタにスナップさせる方法とか。
(こんな動き)

VlockFlow_リンクの作成.gif

このあたりのコードはまだかなり汚いのでご紹介できるレベルではないのですが、整理できたらまた記事化できればと考えています。


  1. あくまでおもちゃレベルのもの。昔から作ってみたかったのですが、ようやく自分の技術が追いついてきたので作り始めました。 

  2. Atomic Design は元々UIデザインの設計手法のひとつですが、 Vue.js の「単一ファイルコンポーネント」の設計思想とも親和性が高いと感じます。 

  3. ピザやタルトのベース部分を crust と呼ぶので、この命名にしてみました。 

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

Nuxtで現在地情報を取得して利用する

ごめん、アドベントカレンダーはかけません。いま、HackDayにいます。この国をちょっと便利にするソリューションを作っています。……本当は、あの頃が恋しいけれど、でも今はもう少しだけ、知らないふりをします。私の作るこのアイデアも、きっといつか誰かの青春を乗せるから。

はじめに

と、言うわけで、現在、HackDay2019の会場でこの記事を執筆しています。
限界開発ですでに心が折れそうなのですが、本日はYumemiアドベントカレンダーの自分の担当日なので頑張って書いています。

今、目の前のチームメンバーが発狂しました。

さて、今回のお題ですが。フレッシュな内容ということで、HackDayでちょうど詰まった内容を投稿します。

やりたかったこと

Webページを使用しているユーザーの現在位置を取得して、APIに渡したかった。

やったこと

GeoLoacationAPIを使った。

GeoLocationAPIとは?

Geolocation API により、ユーザーは希望すれば自身の場所をウェブアプリケーションに通知することができるようになります。なお、プライバシー保護の観点から、ユーザーは位置情報が送信される際には確認を求められます。

Mozilaより

と、言うわけでたまにブラウザを開いているユーザーの現在位置を取得できるAPIです(そのまま)

例えば現在位置の緯度経度を取得する場合はこう

navigator.geolocation.getCurrentPosition(function(position) {
  console.log(position.coords.latitude, position.coords.longitude);
});

注意点として、現在位置を取得するにはある程度時間がかかるので、非同期的に処理する必要があります。

実際のコード

<script>
export default {
    mounted: {
       const position = await this.getPosition()
         .then((position) => {
            return {
              latitude: position.coords.latitude,
              longitude: position.coords.longitude
            }
          })
          .catch((err) => {
            console.error(err.message)
          })

       this.latitude = position.latitude
       this.longitude = position.longitude
       do_something(this.latitude, this.longitude)
    },
    methods:{
      getPosition(options) {
        return new Promise(function(resolve, reject) {
          navigator.geolocation.getCurrentPosition(resolve, reject, options)
        })
      }
    }
  }
}
</script>

見ての通り、無理やりPromiseにして返却しています。

注意点として、現在位置を利用するので、ユーザーの許可がなければ使えません。その場合の例外処理もしっかり書いておきましょう。

最後に

限界開発の真っ最中なので内容薄めで申し訳ございません。
後々HackDayで得た他の知見もQiitaに上げるかもしれませんので、許してください。

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

お前らのクソアプリは間違えてる

クソアプリ2 Advent Calendar 2019の8日目が空いてましたので、担当させていただきます。

お前らは俺を怒らせた

今年もクソアプリの季節がやってきた〜〜〜〜〜〜〜〜!!
才能と技術の無駄遣いの季節がやってきました!!
クソアプリアドベントカレンダー!!
ここ最近は、カレンダーが二つもあって、豊作ですね!!
オラ、ワクワクするぞ!!
・・・
・・・
・・・は?

なんすか、今年のクソアプリカレンダー。
クソアプリって、言っておきながら、実用的なアプリとか将来的につながるものが多いじゃないっすか。
真面目に素晴らしい良いアプリや記事じゃないですか!!
なんで、クソアプリカレンダーに書いてるんですか!

クソアプリはそんなんじゃない...もっと、絶望するくらい何にも役に立たないもんでしょ!
だって、書いてあるじゃないですか!説明に!!

今年も役に立たない、世の中に貢献しないアプリとかサービスを出しあって遊ぼうぜ!

役に立ったら、クソアプリじゃないんですよ!!

怒りの力が俺にクソアプリを作らせた

笑いなんていらねぇんだよ...俺に、今、必要なのは、怒りだ!クソまじめアプリをぶっ潰す!うわああああーっ!!(わかる人にしか伝わらない)

というわけで、怒り気味にクソアプリを作ってみました。

image.png

皆さんは、たまに仕事中に寿司食べたくなりませんか?
僕はステーキが食いたくなります(そして、昼に食いに行きます)

回らない寿司は、一般的に高いです。
でも、回転寿司は、安いですよね。
つまり、寿司が回ればいいんですよ。
そしたら、昼に食えるレベルになりますね。

なので、寿司を回すアプリを作りました。
昔作ったアプリだったのですが、今回、寿司のお代わりができるように、ボタンを追加しました。(ついでに、色々といじりましたが)
減らすこともできますし、お腹がいっぱいになったら、止めることもできます。

すごいでしょ?最高でしょ?天才でしょ?

こっちで遊べるので、ぜひ遊んでみてください!
https://sushi-go-round.netlify.com/

ソースはこちらです。
https://github.com/EndoHizumi/sushi-go-round

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

【Vue.js】transitionのin-outとout-inの表示結果デモ

はじめに

Vue.jsのtransitionタグの属性であるin-outout-inの動きの違いが分かるデモを作ってみました。

公式ドキュメントはこちら。

環境

- OS: macOS Catalina 10.15.1
- Vue: 2.6.10

基本構文

<transition mode="out-in">
  <!-- ... the buttons ... -->
</transition>

このmodeの値をin-outout-inどちらにするかで表示が変わります。

指定なし

See the Pen LYERjdZ by terufumi (@terufumi1122) on CodePen.

in-out

最初に新しい要素がトランジションして、それが完了したら、現在の要素がトランジションアウトする。

See the Pen wvBzPJG by terufumi (@terufumi1122) on CodePen.

out-in

最初に現在の要素がトランジションアウトして、それが完了したら、新しい要素がトランジションインする。

See the Pen XWJjzME by terufumi (@terufumi1122) on CodePen.

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

特に意図が無ければout-inを指定しておくのが無難ですね:thinking:

参考にさせて頂いたサイト(いつもありがとうございます)

Enter/Leave とトランジション一覧 — Vue.js

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

Vue.jsのコンポーネントのimport文をdynamic importに変換するcliコマンドを作りました

Vue.jsのコンポーネントのimportをdynamic importに変換するcliコマンドを作りました。
特定のディレクトリ配下のvueファイルを全てdynamic importに変換します。

ソースはこちらで公開しています。
https://github.com/harhogefoo/dynamic-import-converter

通常のcomponentのimport文
<template>
  <div>
    <hoge />
    <piyo />
  </div>
</template>

<script>
import Hoge from "@/components/Hoge.vue"
import Piyo from "@/components/Piyo.vue"

export default {
  components: {
    Hoge,
    Piyo
  }
}
</script>
dynamic_importに変換
<template>
  <div>
    <hoge />
    <piyo />
  </div>
</template>

<script>

export default {
  components: {
    Hoge: () => import("@/components/Hoge.vue"),
    Piyo: () => import("@/components/Piyo.vue")
  }
}
</script>

使い方

$ yarn global add dynamic-import-converter
or
$ npm install -g dynamic-import-converter

$ dynamic-import-converter ./Vueファイルが格納されたディレクトリのパス/

バグ、改善要望などは、リポジトリのissueまで!
https://github.com/harhogefoo/dynamic-import-converter/issues

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