- 投稿日:2019-12-15T23:53:01+09:00
kusoなusoアプリを作ってみた
嘘を嘘と見抜けないと生きていくのは難しい
どうやら人間は、嘘をつくといろいろな身体的特徴が現れてしまうらしい。
そんないくつかの身体的特徴を、下記のように測定できるWebアプリを作ってみた。心拍数
嘘をつくとき人は、緊張のため心拍数が高まったり、逆に一定の落ち込みがあるものらしい。
そこで心拍数を測定するために、fitbit SDKを使ってみた。
fitbitはJavaScriptでアプリを開発でき、fitbitアプリからスマホ上のfitbitアプリ(Companion機能)を経由して自由にインターネットアクセスもできる。fitbitでとった心拍数をどうWebアプリに送るか。
前述のCompanion機能を経由して、Firebase Cloud FunctionsからRealtime Databaseに値を格納。それをWebアプリに同期するように組んでみた。結果、特段の工夫もなくなかなかの追従性を実現でき、1秒くらいの遅れでWebアプリ側に反映できるようになった。Firebase有能。
一般的な安定時の心拍数を超えて、90オーバーになる頻度を測定し、嘘の度合いとしてみた。
目線
嘘をつくとき人は、どうしても相手から目線をそらしてしまうものらしい。
ここで必要になるのは顔認識の機能。最初はiOSやAndroidのネイティブアプリから試してみていたが、紆余曲折のすえWebアプリで。clmtrackrを使って実現することにした。
目線については、目の幅(端から端までの距離)のうち、瞳の位置がセンターから大きくズレたタイミングをとるようにした。右目を例にとると、図でいうp23とp25間の距離を求め、p27の位置をチェックしている。
まばたき
嘘をつくとき人は、まばたきの回数が増えたり、またはほとんどまばたきをしなくなったりするらしい。
こちらも、上で使ったclmtrackrによる認識結果から測定している。
上下のまぶたの距離が近くなるタイミングをとるが、さらに一定時間検出をスキップするなど自然なまばたきをカウントできるよう細かい調整を入れている。正常なまばたきの回数が1分間に20回程度といわれているので、それを大きく上回るか下回ると、嘘の可能性ありと判定している。または、瞬間的にやたらまばたきするタイミングなどを検出できるようにしても良いかも。
話すスピード
嘘をつくとき人は、説明過多になることで話すスピードが速くなるか、または極端に口数が落ちるらしい。
こんどは音声認識だ。
IBM WatsonのSpeech to Textを利用した。最近はJavaScriptからでもリアルタイムの音声認識ができるんだよね。凄い。watson-speechパッケージを使えば、マイクの起動から音声を逐次クラウドに送り結果を得るところまで数行のコードで実現できた。
ここで得られたテキストの文字数をカウントすることで、話すスピードを測定している。
1分あたり300文字が理想といわれているようなので、そこから大きく増える・または減る場合、嘘の可能性が高いとしている。単語の繰り返し
嘘をつくとき人は、説明過多になることにより、繰り返し同じ単語を話す傾向もあるらしい。
上で得られた音声認識結果のテキストを、さらにYahoo!の形態素解析 API に送り、単語に分解して測定する。
このAPIでは分解した単語の種類(名詞・動詞・接続詞など)も分かるので、名詞または動詞の繰り返される回数をカウントしている。嘘を嘘と見抜かれると生きていくのはつらい
と、ここまでいくつかの要素を組み合わせて嘘を嘘と判定できる仕組みを作ってみた。
これをどう活かすべきか。
考えるまでもないことだが、これを使って徹底的に嘘のトレーニングを積み、絶対にばれない嘘をつけるようになることである。そういうことでこのUSOTOREというWebアプリ。
出されたお題に対して30秒で嘘の申し開きをすることで、その間の嘘ぐあいを測定し、嘘のPDCAを高速に回すことができる。
https://lietrain.z11.web.core.windows.net/fitbitのマルチユーザー対応が未実装なのでいったん仮の値表示になるのだが、他はだいたい動いているハズ。fitbitはなにも身体のトレーニングだけに使うものではない、ということも証明できたのではないかと。
あざむく
たばかる
煙に巻く世界平和のためにも、よい嘘を。
- 投稿日:2019-12-15T22:31:43+09:00
DashのSnippet機能を活用したらNuxt.jsの開発がめちゃめちゃ捗った【Nuxt.js × Vuetify】
Snippet機能を活用する!
みなさん、こんにちは。どんぶラッコです。
普段Nuxt.js + Vuetify を使って開発を進めることが多いのですが、「あれ、あの書き方ってどう書くんだっけ...」となる事、ありませんか?そこで、DashというアプリのSnippet機能を使ってNuxt開発用のリファレンスを作ったところ、作業が非常に捗るようになりました。
Snippet機能いうのは、つまりエイリアスを作ることができる機能です。
このような設定をした後、
;nuxt-start
と入力をすると...なんと!書き換えをしてくれる機能です。
今回は、私が登録しているスニペットをご紹介したいと思います。
前提条件
スニペットの名前についてはどのように登録しても問題ありませんが、私は
;[ライブラリ名 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
他にも"これもあったら便利じゃない?", "こうやって登録した方がいいんじゃない?"というものがあったら、是非コメントで教えてください!
- 投稿日:2019-12-15T22:22:55+09:00
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兆通りぐらい?)
開発中のメモをもとに、いくつか備忘録を記述します。
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のみとなりました。そのうち時間を作ってチャレンジしてみたいと思います。
備考
以下のサイトのデータ・情報を元に開発しました。
- 英単語データ
- ランダムな配列生成ロジック
- 投稿日:2019-12-15T21:58:29+09:00
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こんな具合に入力していきます。
- テンプレートには初めから設定値が入っているので、使う場合は上書きしてください。
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.tsimport _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.jsimport 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を盛り上げていきましょう!!
- 投稿日:2019-12-15T21:51:54+09:00
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
で取得しその位置から泡が出るようにしました。また、クリックタイミングで自由に泡を破裂させることもできます。
こんな感じです↓vue で泡作った pic.twitter.com/lZvmUMD1Nc
— 山田健太郎/Yamada Kentaro (@reiwaStudent) December 15, 2019後ろの波は HTML5 の canvas 要素で作りました。
終わりに
途中、俺何してるんだろうってなりました。
楽しかったです。
- 投稿日:2019-12-15T18:30:37+09:00
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コントローラーとルーティングを追加していきます。
rails g controller rooms show
routes.rbRails.application.routes.draw do get 'rooms/show' root 'rooms#show' endこの時点で、こうなっていれば成功です。
②SkyWayのdeveloper登録
Community Editionが無料なので、このプランで登録していきます。
https://webrtc.ecl.ntt.com/signup.html登録して、ダッシュボードに行ったらアプリケーションを作成に進んでください。
ドメイン名の部分は、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で作ったコンポーネントをレンダリングしています。
ではいよいよSkywayAPIを導入していきます。
hello_vueと同じ階層にjsファイルを作成します。
app/javascript/packs/room.jsimport 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/6e1f56678b2eb6318594githubリポジトリ
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タグに反映されます。
最後にgit push heroku masterした結果がこれです。
あとで変わるかもしれません。
https://morning-meadow-17444.herokuapp.com/最後に
いかがでしたでしょうか?
ビデオチャットが簡単に作れるSkywayAPIすごいですね。
本来であれば、Turnサーバー・Stunサーバー立ててーごにょごにょしなきゃいけないと思いますが、そこの部分を全てやってくれます。もともと複数ユーザーが同時に参加できるカンファレンス式のビデオチャットにする予定だったので、roomという言葉をよく使っています。次回できたら複数参加もできるように実装したいです。
roomよりもchatとかの方がしっくりくるかもしれません。
- 投稿日:2019-12-15T18:12:38+09:00
【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>原因
削除モーダルをテーブルの行ごとに作っていたのが誤作動の原因。モーダルを呼び出したときに最後のモーダルだけが実行されていた。
解決策
削除モーダルを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のリストを削除してくれるようになった。めでたしめでたし。
- 投稿日:2019-12-15T17:24:20+09:00
new Vue() に渡されたオプションオブジェクトの行方を探るべく、我々は vue/src/core の奥地へと向かった
qnote Advent Calendar 2019 の16日目です。
はじめに
こんにちは。今日も元気に
npm run
してますか?
Vue.js
、いいですよね、ドキュメントも豊富で簡単でとっても便利。
しかしフレームワークとして簡単に使えてしまうあまり、Vue
の中身を気にすることはあまりないのではないでしょうか。
今日はそんなVue
の中身を覗いて、その謎を少しだけ解明してみることにしましょう。
取り上げるのは、Vue
インスタンスに渡されるオプションオブジェクトの行方です。オプションオブジェクトの行方
オプションオブジェクトは大まかに、下記の流れで各オプションとして機能するように定義されていきます。
new Vue()
に渡されるinitMixin()
でvm.$options
が定義される- 各
init...()
メソッドでリアクティブシステムへの追加などが行われる- 我々の手に届く
ではオプションオブジェクトの長く険しい道のりを、一緒に追っていきましょう。
vm.$options が定義されるまで
new Vue()
全ての始まり、コンストラクタ関数
Vue()
。
ここにオプションオブジェクトを渡すことで、Vue
インスタンスが生成されます。
これが定義されている箇所は vue/src/core/instance/index.js です。vue/src/core/instance/index.jsfunction 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.jsexport 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 */
イスタンブール?![]()
![]()
![]()
調べてみたら、テストのカバレッジを調べてくれるツールのようでした。
イスタンブールといえば、飛んでイスタンブールしか思い浮かばなかったのですが、新たな知識を得ることができました。話がそれましたが、オプションは
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
がもつextend
やmixin
を考慮した処理が行われています。
mixin
の数だけmergeOptions
を繰り返し、定義していることがわかります。if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } }その後オプションは一旦、空のオブジェクトとして定義され、
mergeField()
でparent
とchild
のオプションがマージされ、プロパティ毎の結果がオプションオブジェクトに格納されていきます。最後にマージされたオプションオブジェクトが
return
され、vm.$options
に入るわけですね。
data
オプションがリアクティブシステムに追加されるまで全部のオプションの行方を追うのは大変なので、今回は
data
がリアクティブシステムに追加されるまでに焦点を当ててみましょう。
再びinitMixin()
に戻ります。
vm.$options
が定義されたのち、 様々なinit...()
を経て、initState(vm)
にインスタンスが渡っています。vue/src/core/instance/init.jsinitLifecycle(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.jsexport 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.jsfunction 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
で回して、props
やmethods
ですでに定義されている名前でないかをチェックしています。
キーの名前はmethods
やprops
が優先ということですね。そして
isReserved(key)
でキー名が_
または$
から始まっていないことをチェックして、proxy()
に渡しています。vue/src/core/instance/state.jsexport 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
が使用できない関係上、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
- 投稿日:2019-12-15T16:58:57+09:00
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」とかあるみたいですが、とりあえず今回はここまでとしたいと思います。
- 投稿日:2019-12-15T16:58:00+09:00
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
システム構成図
まず、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関連
Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。
config.ymlversion: 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.confDockerfileFROM nginx COPY conf/default.conf /etc/nginx/conf.d/default.conf EXPOSE 80 ENTRYPOINT nginx -g 'daemon off;'default.confserver { 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
エラーハンドリング
CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考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の設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。
- 投稿日:2019-12-15T16:58:00+09:00
典型的な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
システム構成図
まず、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関連
Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。
config.ymlversion: 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.confDockerfileFROM nginx COPY conf/default.conf /etc/nginx/conf.d/default.conf EXPOSE 80 ENTRYPOINT nginx -g 'daemon off;'default.confserver { 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
エラーハンドリング
CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考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の設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。
- 投稿日:2019-12-15T16:25:28+09:00
kintone カスタマイズを Vue.js + TypeScript + Pug + SCSS でモダンに開発する (4) アプリ実装編 part2
目次
(1) 環境構築編
(2) アプリ構築・設定編
(3) アプリ実装編 part1
(4) アプリ実装編 part2(この記事)前置き
第 3 回 の記事では、第 1 回 で作成した
Vue.js
、TypeScript
、Pug
、SCSS
を組み込んだプロジェクトで 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 / SCSS
やESLint
、Prettier
、Jest
などがセットアップされています。(2) アプリ構築・設定編 の記事で、kintone 側で用意している雛形アプリ「案件管理」を使って新規アプリを構築し、型定義ファイルの生成まで行いました。
(3) アプリ実装編 part1 では画面をコンポーネントで分割し、ビジュアル面の実装を中心に進めました。今回のゴール
前回、Vue.Draggable を組み込んだ事で、以下のようにドラッグ&ドロップでリスト間を移動できるようになりました。
今回はカードを別なリストに動かした際に自動的にレコードを保存するようにしましょう。
これにより、カードを動かすだけで確度を変更する事ができるようになります。以下の順で説明していきます。
- ドロップ時のイベント処理を実装する
- 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-sdk
をrequire
している行を追加しています。
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 build10〜20 秒ほどで、
dist/
フォルダの下に以下のようなファイルが作成されます。
これらのファイルは難読化及び圧縮化が施されており容易にリバースエンジニアリングできないものになっています。
また、ファイル名にはキャッシュ除けのランダムな文字列が含まれています。この辺はwebpack
の設定で付けないように(常に固定名に)したりできますが、今回は説明を省きます。出来上がった JS ファイルと CSS ファイルをアプリに適用します。
これでアプリを更新すれば、ビルド前の状態と同じ動作をする事が確認できるはずです。まとめ
ここまででアプリの実装はひと段落です。
Vue.js
とTypeScript
、kintone JS SDK
を使い、kintone のカスタマイズを実装する一連の流れについて説明して来ました。
もちろんここまでの実装でこのアプリは実用レベルで使えるかと言うと、実際にはそんな事はありません。
例えば、
- 複数人でアプリを触っていた際、別の人がカードを動かしていたのを知らずに自分も動かしてしまってエラーが出てしまった
- あるいは他の人がレコードを編集したためエラーが出てしまった
- そもそもカードから別ウィンドウでレコード詳細に飛ぶ機能があるが、そちらでの変更結果はリロードしなければカスタマイズビューには反映されない
- 担当者以外はカードを動かせないようにしたい
- 1 つのリスト内で順番を任意に並び替えたい
- あるいは小計の降順など特定の条件でソートしたい
- リスト内のカードの小計の合計値をリストに表示したい
- 確度以外の条件でグルーピングしたい
など、ちょっと考えれば多数の問題点・改善点が見出せます。
この辺りはビジネス上の要求やアプリを使用するユーザー層によっても最適解が変わって来る部分でしょう。次回は
というわけで、今回は
Vue.js
とTypeScript
で開発するプロジェクトを kintone で運用に載せるところまでを見て来ました。
一昔前まではごくごく普通だった生の JavaScript ファイルを書いてアプリに適用して複数のブラウザで動作確認して・・・と言う開発の進め方と較べると、これらモダンなテクノロジーの採用による恩恵は計り知れないと言わざるを得ません。しかし、こう言った進め方をすれば効率よくバグのない開発ができるかと言えば、それは言い過ぎです。
ここまでの解説では テスト に関する観点がまるっきり存在していません。と言うわけで、最終回(予定)となる次回は、kintone カスタマイズを Jest でモダンにテストする手法について解説していこうと思います。
- 投稿日:2019-12-15T16:25:28+09:00
ナウい kintone カスタマイズ (4) アプリ実装編 part2
目次
(1) 環境構築編
(2) アプリ構築・設定編
(3) アプリ実装編 part1
(4) アプリ実装編 part2(この記事)前置き
第 3 回 の記事では、第 1 回 で作成した
Vue.js
、TypeScript
、Pug
、SCSS
を組み込んだプロジェクトで 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 / SCSS
やESLint
、Prettier
、Jest
などがセットアップされています。(2) アプリ構築・設定編 の記事で、kintone 側で用意している雛形アプリ「案件管理」を使って新規アプリを構築し、型定義ファイルの生成まで行いました。
(3) アプリ実装編 part1 では画面をコンポーネントで分割し、ビジュアル面の実装を中心に進めました。今回のゴール
前回、Vue.Draggable を組み込んだ事で、以下のようにドラッグ&ドロップでリスト間を移動できるようになりました。
今回はカードを別なリストに動かした際に自動的にレコードを保存するようにしましょう。
これにより、カードを動かすだけで確度を変更する事ができるようになります。以下の順で説明していきます。
- ドロップ時のイベント処理を実装する
- 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-sdk
をrequire
している行を追加しています。
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 build10〜20 秒ほどで、
dist/
フォルダの下に以下のようなファイルが作成されます。
これらのファイルは難読化及び圧縮化が施されており容易にリバースエンジニアリングできないものになっています。
また、ファイル名にはキャッシュ除けのランダムな文字列が含まれています。この辺はwebpack
の設定で付けないように(常に固定名に)したりできますが、今回は説明を省きます。出来上がった JS ファイルと CSS ファイルをアプリに適用します。
これでアプリを更新すれば、ビルド前の状態と同じ動作をする事が確認できるはずです。まとめ
ここまででアプリの実装はひと段落です。
Vue.js
とTypeScript
、kintone JS SDK
を使い、kintone のカスタマイズを実装する一連の流れについて説明して来ました。
もちろんここまでの実装でこのアプリは実用レベルで使えるかと言うと、実際にはそんな事はありません。
例えば、
- 複数人でアプリを触っていた際、別の人がカードを動かしていたのを知らずに自分も動かしてしまってエラーが出てしまった
- あるいは他の人がレコードを編集したためエラーが出てしまった
- そもそもカードから別ウィンドウでレコード詳細に飛ぶ機能があるが、そちらでの変更結果はリロードしなければカスタマイズビューには反映されない
- 担当者以外はカードを動かせないようにしたい
- 1 つのリスト内で順番を任意に並び替えたい
- あるいは小計の降順など特定の条件でソートしたい
- リスト内のカードの小計の合計値をリストに表示したい
- 確度以外の条件でグルーピングしたい
など、ちょっと考えれば多数の問題点・改善点が見出せます。
この辺りはビジネス上の要求やアプリを使用するユーザー層によっても最適解が変わって来る部分でしょう。次回は
というわけで、今回は
Vue.js
とTypeScript
で開発するプロジェクトを kintone で運用に載せるところまでを見て来ました。
一昔前まではごくごく普通だった生の JavaScript ファイルを書いてアプリに適用して複数のブラウザで動作確認して・・・と言う開発の進め方と較べると、これらモダンなテクノロジーの採用による恩恵は計り知れないと言わざるを得ません。しかし、こう言った進め方をすれば効率よくバグのない開発ができるかと言えば、それは言い過ぎです。
ここまでの解説では テスト に関する観点がまるっきり存在していません。と言うわけで、最終回(予定)となる次回は、kintone カスタマイズを Jest でモダンにテストする手法について解説していこうと思います。
- 投稿日:2019-12-15T16:00:57+09:00
300回プログラムじゃんけんでを戦わせるWebアプリを作った
作ったアプリ
https://janken-programmer.web.app/
遊び方
これはじゃんけんの出す"手"をプログラミングして戦わせるアプリです。
例えばグーだけを出すA君のプログラムと
グーとパーを交互に出すB君のプログラム
を300回戦わせるとB君が勝ちます
こんな感じでじゃんけんのアルゴリズムを考えて戦わせるアプリです
例
A君のプログラム
HAND = 0B君のプログラム
HAND = 1 if COUNT % 2 == 0: HAND = 0これを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で認証できるようにしました。
感想
自分の想定しているものが一通りできたので、だいぶ満足しています。
ただセキュリティやじゃんけんのプログラムに不備があるかもしれないのでこれからも運営、開発をしていきたいと思います。
- 投稿日:2019-12-15T14:15:40+09:00
vue.jsでtextareaの高さを動的に変える
textarea_rowsプロパティを持つhogeコンポーネントがあるとする。
サーバーサイドから
\n
の数をカウントして返すなどし、textarea_rowsに代入したとする。hoge_component.vue<!-- textareaのrows属性に、computedのtextarea_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 } }
- 投稿日:2019-12-15T13:44:53+09:00
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.tsimport { 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.tsexport 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 とは、
asyncData
やfetch
などのVueインスタンスが生成される前でもアクセスができるグローバルなオブジェクト、という認識で大丈夫かと思います。https://ja.nuxtjs.org/api/context/
プラグインを有効化
nuxt.config.js
のplugins
に追加したファイルを定義することで、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>引数の
app
にcontext.$myInjectedFunction
がInjectされているため、どのページからも関数が呼び出されるようになります。VueインスタンスへInjectする方法
Vueインスタンスに対してもInjectができるので紹介していきます。この場合はasyncDataからは参照することはできません。
プラグインの作成
/plugins/vueInstanceInject.tsimport Vue from 'vue' declare module 'vue/types/vue' { interface Vue { $vueInjectedFunction(name: string): string } } Vue.prototype.$vueInjectedFunction = (name: string) => name + 'さん、こんにちは!'今度は、
Vue
インターフェースに対して関数を追加します。
Vue
の型定義はこのようになっています。vue.d.tsexport 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.tsimport { 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.tsexport 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.tsexport 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の場合はプラグインのほうが実装もシンプルにできるかなと思います。
また、使用先でわざわざインポートする必要がないため使い勝手が良く、さらに型定義のおかげで補完が効くのでコーディングが捗ります。
とても便利な機能なので、試したことのない方は是非やってみてください!
- 投稿日:2019-12-15T12:52:22+09:00
Vue.jsの$listeners
vm.$listenersって何?
子コンポーネントで色々なイベントをlistenしてくれます。
sample code
https://github.com/tarunama/vm-listners
どういうこと?
親コンポーネントで、子コンポーネントが
click
ormouseover
されたら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
- 投稿日:2019-12-15T11:09:38+09:00
v-onceで一度だけ描画。その後は変えたくない! ❏Vue.js❏
v-once
とは最初に描画したものだけを永遠に表示します。
その後に代入して値を変えることはできません。使い方
タグの中に
v-once
を書く。開発環境はJSFiddleです。
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3bhtml<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"> <p v-once>{{ message }}</p> <p>{{ sayHi() }}</p> </div>javascriptnew 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!
と表示されます。
いつか使うときを信じて、ここにお納めします。。
ではまた!
- 投稿日:2019-12-15T10:43:23+09:00
Vue.jsでFF7風のポートフォリオを作った
作ったもの
- ff7風のポートフォリオを作ってみました。(レスポンシブ非対応なので、PCで見てください)
- URLはこちら、https://ryuzo-nakata.github.io/portfolio-ff7/
- ソースはこちら、https://github.com/ryuzo-nakata/portfolio-ff7
経緯
ff7のリメイクがもうすぐ発売されますね!!!
早くプレイしたいです。待ち遠しさを紛らわすために、遊び心でポートフォリオとして作成してみました。
マテリアをCSSで記載するなど、しなくてよい努力を詰め込んでますw使ったもの
- Vue.js
- Nuxt.js
- Vuetify
- Typescript
- Scss
フロントの勉強かつ、Typescriptの練習を兼ねて上記のような構成にしています。
Top画面の説明
メニュー画面にマウスカーソルを合わせると、あのお馴染みのカーソルを表示するようにしています。クリックするとページを遷移するように作成していますが、現在は「マテリア」ページのみ作成しております。
背景色
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
は、経験値とリミットのゲージマテリア画面の説明
マテリアにマウスカーソルを合わせると、カーソルとマテリアの説明が表示されます。マテリア毎に、自分のスキルを乗せるようにしてみました。
詳細説明とマテリアリストの部分は時間が足りなかったので、今後追加していきます。ちなみに、マテリアを外すと下記のようになります。マテリア穴までCSSで作成しています。見えないところにもこだわる!
マテリア
マテリアはコンポーネントとして作成しています。画像でしたらはるかに楽なのですが、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>
attackMaterias
、defenceMaterias
にマテリアを指定しています。type
でマテリアの種類を指定します。ポートフォリオとして、プログラミングスキルを下記のように当てはめています。
- 0: マテリアなし
- 1: コマンドマテリア→ フロントスキル
- 2: 独立マテリア → フレームワーク
- 3: 魔法マテリア → バックエンドスキル
- 4: 召喚マテリア → インフラ周りのスキル
- 5: サポートマテリア→ ツール類
おわりに
思い付きで始めたものの、面白くできました。途中感は否めないですが、ひとまず形にはなりました。
何かアイディアある方はコメント頂けると嬉しいです。
- 投稿日:2019-12-15T05:13:13+09:00
SVG要素を包含したVueのコンポーネントでマウスイベントを発火させる方法
この記事は 「Vue Advent Calendar 2019 #1」 15日目の記事です。
概要
「SVG要素を包含したVueのコンポーネント」で、マウスイベントを発火させる方法をご紹介します。
とても簡単な内容に思えるのですが、mousedown 等のイベントを上位のコンポーネントで単純に v-on するだけではうまく動きませんでした。
要約(五七五)
イベントは $emit させると いい感じ
背景
Vue.jsとSVGを使って、超簡易ビジュアルプログラミング環境を開発中です。1
「ブロック」を「リンク」で繋ぐことで処理の流れを構築する、ビジュアルプログラミングとしてはよくある感じのやつです。上図のようなものを描くには、「ブロック」を移動させたり「リンク」を繋いだりする操作が必要になります。つまり、ドラッグ&ドロップが必要です。
このとき、ドラッグ&ドロップしたいのは「 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 に相当するので、各イベントが登録されていてほしいのですが、、、
配下の 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 の各イベントリスナが登録されました。
しかし、実際に動作させると、上位側ではイベントを拾うことができませんでした。ドラッグ&ドロップを実現するには、上位コンポーネントでイベントをハンドリングしたいので、これでは不十分です。
動く例
では、どのようにすれば動くのでしょうか。
SVG の各要素( rect, circle 等)はマウス操作を検知してイベントを発火してくれますので、これらのSVG の各要素で発火したイベントを、上位のコンポーネントへ伝達していけば、うまく動きそうです。
伝達方法は、Vue公式のスタイルガイドにある
props down, events up
の原則に従い、$emit
でevents 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時間くらい試行錯誤して作った内容の一部をご紹介しました。
もし、もっと良い方法がありましたらお教え頂けますと幸いです。
最後までご覧いただきありがとうございました!
おまけ
ドラッグ&ドロップを実現するには他にも考慮することが多々あります。
リンクを追加する時に最も近いコネクタにスナップさせる方法とか。
(こんな動き)このあたりのコードはまだかなり汚いのでご紹介できるレベルではないのですが、整理できたらまた記事化できればと考えています。
- 投稿日:2019-12-15T04:27:57+09:00
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に上げるかもしれませんので、許してください。
- 投稿日:2019-12-15T02:51:24+09:00
お前らのクソアプリは間違えてる
クソアプリ2 Advent Calendar 2019の8日目が空いてましたので、担当させていただきます。
お前らは俺を怒らせた
今年もクソアプリの季節がやってきた〜〜〜〜〜〜〜〜!!
才能と技術の無駄遣いの季節がやってきました!!
クソアプリアドベントカレンダー!!
ここ最近は、カレンダーが二つもあって、豊作ですね!!
オラ、ワクワクするぞ!!
・・・
・・・
・・・は?なんすか、今年のクソアプリカレンダー。
クソアプリって、言っておきながら、実用的なアプリとか将来的につながるものが多いじゃないっすか。
真面目に素晴らしい良いアプリや記事じゃないですか!!
なんで、クソアプリカレンダーに書いてるんですか!クソアプリはそんなんじゃない...もっと、絶望するくらい何にも役に立たないもんでしょ!
だって、書いてあるじゃないですか!説明に!!今年も役に立たない、世の中に貢献しないアプリとかサービスを出しあって遊ぼうぜ!
役に立ったら、クソアプリじゃないんですよ!!
怒りの力が俺にクソアプリを作らせた
笑いなんていらねぇんだよ...俺に、今、必要なのは、怒りだ!クソまじめアプリをぶっ潰す!うわああああーっ!!(わかる人にしか伝わらない)というわけで、怒り気味にクソアプリを作ってみました。
皆さんは、たまに仕事中に寿司食べたくなりませんか?
僕はステーキが食いたくなります(そして、昼に食いに行きます)回らない寿司は、一般的に高いです。
でも、回転寿司は、安いですよね。
つまり、寿司が回ればいいんですよ。
そしたら、昼に食えるレベルになりますね。なので、寿司を回すアプリを作りました。
昔作ったアプリだったのですが、今回、寿司のお代わりができるように、ボタンを追加しました。(ついでに、色々といじりましたが)
減らすこともできますし、お腹がいっぱいになったら、止めることもできます。すごいでしょ?最高でしょ?天才でしょ?
こっちで遊べるので、ぜひ遊んでみてください!
https://sushi-go-round.netlify.com/ソースはこちらです。
https://github.com/EndoHizumi/sushi-go-round
- 投稿日:2019-12-15T01:28:55+09:00
【Vue.js】transitionのin-outとout-inの表示結果デモ
はじめに
Vue.jsの
transition
タグの属性であるin-out
とout-in
の動きの違いが分かるデモを作ってみました。公式ドキュメントはこちら。
環境
- OS: macOS Catalina 10.15.1 - Vue: 2.6.10基本構文
<transition mode="out-in"> <!-- ... the buttons ... --> </transition>この
mode
の値をin-out
、out-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.
おわりに
最後まで読んで頂きありがとうございました
特に意図が無ければ
out-in
を指定しておくのが無難ですね参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-15T00:50:06+09:00
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