- 投稿日:2020-06-19T19:02:18+09:00
【Vue.js 】 watch データの変更を監視
はじめに
watchは特定のデータや算出プロパティの状態の変化を監視して、変化があったときに登録した処理を実行するプロパティ。
データや算出プロパティの変更が処理のトリガー(きっかけ)となる。使い方
watchオプションに
監視するデータの名前と変化した時に呼び出されるハンドラ(関数)を定義する。
データの比較が必要な場合は、第1引数として新しい値、第2引数として古い値が受け取れる。
※ここではインスタンスが生成されると同時にwatchが呼び出されている。基本的な書き方
sample.jsnew Vue({ watch: { 監視するデータ: function(新しい値, 古い値) { // データが変化した時に行いたい処理 }, // ↓ オブジェクトのプロパティも監視できる 'item.value': function(新しい値, 古い値) { // 処理 } } })↓ データ等を入れて記述してみる
sample.jsnew Vue({ el: 'app', data: { flag: true }, methods: { changeFlag: function() { this.flag = !this.flag } }, // データflagをwatchオプションに定義 watch: { flag: function(newVal, oldVal) { // データの値が変化した時にコンソールに新しい値と古い値を出力 console.log(newVal, oldVal) } } });watchのオプション
下記のオプションを任意で設定できる(デフォルトではfalse)
プロパティ 値 説明 deep Boolean ネストされたオブジェクトも監視する immediate Boolean 初期読み込み時にも呼び出す オプションを使用する際はオブジェクトにまとめ、
handlerプロパティにハンドラを定義する。sample.jsnew Vue({ el: 'app', data: { list: [ { name: '鉛筆', price: 98 }, { name: '消しゴム', price: 100 } ] }, watch: { // listもしくはlist内のプロパティに変更があった場合に処理が実行される list: { handler: function(newVal, oldVal) { console.log(newVal) }, // deepがfalseだとオブジェクト内の値が変更されても処理が実行されない deep : true, immediate: true } } });watchオプションとcomputedオプションの違い
同じようなプロパティとしてデータ変更時にのみ処理を実行するcomputedオプションがある。
→ computedオプションについてこちらまずはwatchプロパティとcomputedプロパティで同じ処理を記述して比較してみる。
↓ firstNum と secondNum を掛けてresultNumを表示するsample.js// watchの場合 new Vue({ el: 'app', data: { firstNum: 2, secondNum: 3, resultNum: 6 }, watch: { firstNum: function(newVal) { this.resultNum = newVal * this.secondNum; }, secondNum: function(newVal) { this.resultNum = this.secondNum * newVal; }, } })sample.js// computedの場合 new Vue({ el: 'app', data: { firstNum: 2, secondNum: 3 }, computed: { resultNum: function() { return this.firstNum * secondNum; } } })このようにcomputedオプションの方が簡潔で可読性の高いコードが記述できる。そのため、computedオプションで記述できる場合はcomputedオプションを使用するのが望ましい。
watchオプションは下記のような場面で使う。
◎ 非同期通信(APIアクセス)などの複雑な処理を行う場合
(computedオプションでは処理できない)
◎ 更新前と更新後の値を使う場合
◎ 処理を実行してもデータを返さない場合メソッド内でwatchを登録
インスタンスメソッドとして
this.$watchと定義することでメソッド内でもwatchが登録できる。
今までの書き方とは少し異なり、下記のように引数を渡してあげる。sample.js// 書き方 this.$watch('監視したいデータ',ハンドラ(関数),オプション) // ライフサイクルのcreatedにwatchを登録してみる created() { this.$watch('value', function(newVal, oldVal) { // 処理 }, { deep: true, immediate: true }) }監視の解除
返り値を使って監視を解除することができる。
sample.jsvar unwatch = this.$watch('value' function() {}) unwatch()一度だけ監視
一度しか監視しないwatchを
unwatch()を使って登録する。sample.jsnew Vue({ el: 'app', data: { edited: false, list: [ { name: '鉛筆', price: 98 }, { name: '消しゴム', price: 100 } ] }, created() { var unwatch = this.$watch('list', function() { // データlistが変更されたらeditedをtrueにする this.edited = true // データが変更されたら監視を解除 unwatch() }, { deep: true }) } })
- 投稿日:2020-06-19T18:25:15+09:00
ビデオチャット作るときの逆引きTipsまとめ
リモートワークのため社内用ビデオチャットツールを先日作ったのですが、せっかくなので一部をオープンソースにしてGitHubにあげておきました。
ukkz/v-sky: Single page video chat appplication
※リファクタしてないので見辛いですSPAなのでGiHub Pages上でそのまま利用もできます。
https://ukkz.github.io/v-skyで、一旦開発が落ち着いたので、作業時のメモを簡単に整理して公開しておくことにします。
構成
- Vue.js (@vue/cli v4.3.1)
- Vuetify @2.3.1
- Vuex @3.4.0
- Vue-Router @3.3.4
- SkyWay
まとめ一覧
ビデオストリームの情報を知りたい
使ったところ:実際の映像アスペクト比とブラウザ画面のアスペクト比から複数映像配置時の自動最適化
VideoTrack.getSettings()でいろいろ情報がとれました。// メディアストリームオブジェクト const my stream = new MediaStream; // ビデオトラックの配列(ないこともある) const video_tracks = mystream.getVideoTracks(); // 少なくとも1つのビデオトラックがあるとき、そのトラックの情報を見る(空配列はtrue判定になるので必ずインデックス0を確認する) if (video_tracks[0]) { const track_settings = video_tracks[0].getSettings(); console.log( track_settings ); /* 検出例(640x480の映像のとき) aspectRatio: 1.3333333333333333, deviceId: "1d3fa9…", frameRate: 30.000030517578125, groupId: “66fa41…", height: 480, resizeMode: "none", width: 640, */ }アス比は循環小数になることがあるためあまり使い勝手がよくなさそうです。
ストリームやトラックに関しては以下がたいへん詳しく書かれておりました。
getUserMedia()の設定項目|npaka|noteビデオタグに関して幅と高さを知りたい・設定したい
使ったところ:映像にcanvasをオーバーレイさせるときのcanvasサイズの指定
<video id="my-video" :srcObject.prop="my_stream" width="160px" height="120px"></video> <style> #my-video { width: 320px; height: 240px; background-color: red; } </style>上記のDOMの場合のビデオタグの大きさは、インライン指定よりもCSS指定のほうが優先されます。
実際のビデオトラック(映像)はCSSで指定されたサイズ(CSSしていなければインライン指定サイズ)の枠内に収まるように配置されます。
上記で枠内の余白部分はbackground-colorで指定された色になります。
CSSもインラインもどちらも指定されていなければ、ビデオトラックのオリジナルサイズで配置されます。const video_element = document.getElementById('my-video’); // 以下、取得できる様々なサイズの例 console.log( video_element.width ); // 160px(インライン指定の幅・未指定ならundefined) console.log( video_element.height ); // 120px(インライン指定の高さ・未指定ならundefined) console.log( video_element.offsetWidth ); // 320px(CSS指定の幅・mounted前ならundefined) console.log( video_element.offsetHeight ); // 240px(CSS指定の高さ・mounted前ならundefined) console.log( video_element.videoWidth ); // 640px(ビデオトラックの実際の幅・メタデータ受信前ならundefined) console.log( video_element.videoHeight ); // 480px(ビデオトラックの実際の高さ・メタデータ受信前ならundefined)メディアストリームをビデオタグに動的に追加したい [Vue.js]
使ったところ:相手からストリームを受信する前にvideoタグをDOMにマウントしたいとき
<video :srcObject.prop="my_stream"></video>
:srcObject.propにdataで定義した変数を指定しておいて、相手からストリーム受信したり自分がgetUserMediaしたりしたタイミングでメディアストリームオブジェクト(空の場合new MediaStream()で生成)を入れればよいです。
Vue歴2ヶ月ですが、リアクティブっていいなあと思いました。ビデオタグのプロパティについて
使ったところ:ビデオタグぜんぶ(ほぼテンプレ構文)
<video :srcObject.prop="my_stream" muted autoplay playsinline></video>
muted:音声再生しません。自身のストリームだとハウリング起こすので必須の属性です。
autoplay:mutedとともに有効のとき自動再生します。カメラからのストリームだとこれがないとそもそもデータが流れないので表示されません。インライン(videoタグ内)で指定しない場合は、スクリプト内のどこかで明示的にplay()を実行しないと再生されません。
playsinline:インライン(Webページ上の表示そのまま)で再生します。これがないとiOSでは自動的に映像がフルスクリーンになってしまうのでほぼ必須です。ボタンの背景色にあわせて文字色の白黒を変えたい [Vuetify]
使ったところ:ボタンのデザイン全般
Vuetifyのv-btnでは基本的にdarkプロパティをつけておくと良さそうです。
背景色にあわせて文字色も自動で変えてくれる唯一のコンポーネントのようです。<v-btn rounded dark color="blue darken-4" @click=“any"> <v-icon>mdi-message-text</v-icon> チャット </v-btn>v-dialogを使った子コンポーネントを親コンポーネント側から開閉させたいが反応しない [Vuetify, Vue.js:v-model]
使ったところ:ボタンクリックでテキストチャットダイアログを開くとき
v-dialogはv-ifなどではなくv-modelで開閉します。
(開いているときにダイアログ外側をクリックorタップで閉じれるようにするため)
このv-dialogを用いた子コンポーネントを親コンポーネントから制御するときは、computedにおいてゲッターとセッターを利用するとうまくいきます。
Vue.jsのv-modelを正しく使う - Qiita親コンポーネント<template> <v-container> <!-- v-dialogを開くボタン --> <v-btn @click.stop="chat_open = true">チャットを開く</v-btn> <!-- :showで開く・toggleイベント受信で閉じる --> <ChatWindow :show="chat_open" @toggle="chat_open = $event" /> </v-container> </template> <script> data() { return { chat_open: false, } } </script>子コンポーネント<template> <v-dialog v-model="toggle"> // 略 </v-dialog> </template> <script> name: 'ChatWindow', props: { show: { type: Boolean, required: true, default: false, }, }, computed: { toggle: { get() { return this.$props.show }, set(v) { this.$emit('toggle', v) } }, }, </script>ゲッターは
props変数を受け取り、セッターは$emitで親コンポーネントに変化を通知します。
親側では変化を受信してpropsに与えた変数を子側から受け取った$event値に変更します。チェックボックスなど設定値の変化でVuexの値を変えたい [Vue.js:v-model]
使ったところ:設定ダイアログで特定機能をスイッチボタンでON/OFFするとき
v-dialogの例と同様で、入力系コンポーネントでv-modelを使うときはゲッターとセッターを使います。
ゲッターは$store.stateから取り出し、セッターでは$store.commitでVuexにセットします。<template> <v-container> <v-switch v-model="speech_onoff" :label="` 音声認識:${(speech_onoff)?'有効':'無効'} `"></v-switch> </v-container> </template> <script> computed: { speech_onoff: { get() { return this.$store.state.config.speech_recognition }, set(onoff) { this.$store.commit('speechConfig', onoff) }, }, }, </script>テキストチャットで長文入力したときに折り返しできない・overflowやword-breakが効かない [Vuetify]
使ったところ:テキストチャット内の吹き出し部分(個別の発言)
pタグのようなブロック要素で囲み
text-wrapクラスを適用させましょう。
(Vuetifyのpタグはデフォルトでmargin-bottomが入ってしまうのでmb-0クラスと組み合わせるとよい)
text-align: left;にしておくと折り返しても左寄せで表示してくれます。文字起こししたい
使ったところ:そのまま(喋った内容を文字列にしてテキストチャットに逐次送信)
SpeechRecognition(Web Speech API)が便利です。すごく簡単。
Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition - Qiita音声入力を明示的に指定できるわけではない(というか選べない)みたいなので、マイクミュート時は手動で
abortすべきですね。
2020年6月現在もChromiumベースのブラウザしか対応していないので注意です。
Samsung, Baidu, QQといった中韓系ブラウザは利用者層が音声入力を好むためか対応しておりなかなか興味深いです。背景切り抜きなどの加工をしたい
使ったところ:画面共有とカメラ映像の合成(公開してないコンポーネントで利用)
- RGB値をキーにして合成(クロマキー)しているもの
canvasでクロマキー合成っぽいことしてみる - Qiita
SkyWayでデスクトップの映像とカメラの映像をクロマキー合成して配信する - Qiita- 静止時と人物登場時の差分を抽出しているもの
SkyWay のビデオチャットに雑なクロマキー合成してみた - Qiita- OpenCV.jsを使って白黒のマスクを作り、輪郭抽出で背景を消すもの
opencv.jsの作法 その9 - Qiita
OpenCV.jsはminifiedでも容量がけっこう大きいのでロード時には注意です。今回オープン版には実装してませんが内部向けで消したり合成したりの用途にはOpenCV.jsのパターンを使いました。
findContoursで輪郭抽出後、一番外の階層のある程度の大きさの輪郭をアルファで塗りつぶすだけで、クロマキー色を指定したりしなくても消したり背景合成したりすることができました。
上記参考記事では画像に対して利用されていますが、映像に対しても15fps程度の速さで処理できたのでそのうち別途記事かきます。スクリーン共有でストリームを置き換えたら音声が送られなくなった
使ったところ:画面共有とカメラ映像の切替時
getUserMediaで音声取得 +getDisplayMediaで映像取得 を組み合わせたメディアストリームを作ります。
MediaStream APIで画面キャプチャとマイクからの音声を同時に収録する - エムスリーテックブログconst display_stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); const camera_stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // ディスプレイ映像と外部音声のトラックから新しくメディアストリームを作成 const merged_stream = new MediaStream([ display_stream.getVideoTracks()[0], camera_stream.getAudioTracks()[0] ]); // または、既存のカメラからのストリームに画面キャプチャ用のトラック追加してもいい(受信側で区分する必要あり) camera_stream.addTrack( display_stream.getVideoTracks()[0] );スマホとPCでエレメントのサイズ・デザインを簡単に切り替えたい [Vutify]
使ったところ:レスポンシブデザインとして全体
v-ifと$vuetify.breakpoint.smAndDownを組み合わせましょう。
ブレイクポイントはほかにもたくさんあります。
Breakpoints — Vuetify.js
v-ifとv-elseで同一機能を異なるデザインで作成して並列に配置すると画面幅によって表示切り替えが可能です。
以下はボタンの例で、PCではアイコン+文字だが、smサイズ以下だとアイコンのみのボタンが表示されます。<v-list-item-action> <v-btn v-if="$vuetify.breakpoint.smAndDown" fab depressed small @click="open = true"><v-icon>mdi-open</v-icon></v-btn> <v-btn v-else rounded depressed @click="open = true"><v-icon>mdi-open</v-icon>開く</v-btn> </v-list-item-action>ビデオを上下中央センタリングしたい
使ったところ(トリミングなし):自分の映像の確認用・チャットルーム内で話者の映像を拡大する用
使ったところ(トリミングあり):チャットルーム内での基本的な映像表示単純なCSSだけどメモ
一番外側の枠となるdivはあらかじめサイズが決まっている(100%指定とかでもよい)ものとし、その中にビデオを配置するときにどうしましょ?という話です。<div class="video-frame"> <video :srcObject.prop="local_media_stream" muted autoplay playsinline></video> </div>上記DOMが基本構造とします。
トリミングなし(上下または左右に余白を作る)
div.video-frame { position: relative; background-color: black; width: 100%; // 枠幅:固定値でもよい height: 100%; // 枠高さ:固定値でもよい } div.video-frame video { position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%) scale(-1, 1); width: auto; height: auto; max-width: 100%; max-height: 100%; }ビデオ側で
widthとheightをautoにしつつも最大サイズを親要素の100%にしておくとアスペクト比を変えずにフィットさせられます。
このままだと左上に寄ってしまうので、topとleftを使って親要素の上左からそれぞれ半分(つまり真ん中)に寄せつつ、translateで自要素の幅と高さ半分ずつ戻すことでセンタリングさせています。また、センタリングとは関係ないですが、
scale(-1, 1)は左右フリップして鏡状態にしてくれるので自分のカメラ映像の時のみ使います。transformは多重に適用できないため、別のクラスなどでtransform: scale(-1, 1);を指定してもうまく反映されません。そのためここでまとめて指定しています。余白は外枠である
video-frameクラスで指定した背景色になります。トリミングあり(上下をフィットさせ左右をカットする)
div.video-frame { position: relative; background-color: black; width: 100%; // 枠幅:固定値でもよい height: 100%; // 枠高さ:固定値でもよい overflow: hidden; // はみ出ても表示させない } div.video-frame video { position: absolute; left: 50%; transform: translateX(-50%); width: auto; height: 100%; }
width: auto;とheight: 100%;で、アスペクト比を維持したまま高さが外枠に合わせられます。
そのあと左右方向に対してleft: 50%;とtranslateX(-50%)でセンタリングさせています。トリミングあり(左右をフィットさせ上下をカットする)
div.video-frame { position: relative; background-color: black; width: 100%; // 枠幅:固定値でもよい height: 100%; // 枠高さ:固定値でもよい overflow: hidden; // はみ出ても表示させない } div.video-frame video { position: absolute; top: 50%; transform: translateY(-50%); width: 100%; height: auto; }上下と左右、
widthとheightを入れ替えると左右方向のフィッティングおよび上下センタリングにも対応できます。
- 投稿日:2020-06-19T18:03:19+09:00
Firestoreに今月と前月のデータがあるか調べる処理を実装した
Nuxt.js+Firebaseを勉強しています。
やりたいこと
月ごとのデータを登録しているのですが、月初にログインしたとき今月分データがないときに前月分データをコピーして今月分データとする機能を実装したいと考えました。
ログインしてすぐにこの判定をしたいので、処理タイミングはmountedとします。環境
- Firebase 8.3.0
- Vue CLI 4.0.5
- Nuxt.js 2.11.1
- VuexFire 3.2.0
課題1
Firestoreのドキュメント の「クエリの制限事項」にある通り、Firestoreでは論理 OR クエリを利用できません。
今月分と前月分をWHERE句のOR条件で検索するつもりでしたが、これができないことが分かりました。今月と前月の2回に分けてデータベースにアクセスすることもできますが、できればアクセスは1回で済ませたいです。
課題2
VuexFireを使っています。
computedでマッピングされた情報をmountedで使うことができればいいのですが、これもできませんでした。
this.$store.dispatchをするタイミングはmountedです。このdispathの後に続けてコピー処理を実装すると、dispath完了よりも先にコピー処理が実行されます。データベースアクセスは非同期だからです。実装したこと
Firestoreのデータベース構造を以下のようにしました。
コレクション ドキュメント サブコレクション ドキュメント フィールド notes (userID) pages (年月) (各データ) notes (userID) index YM (年月) データを登録するときに、pagesサブコレクションの年月ドキュメントにデータを登録します。これに加えて、indexサブコレクションのYMドキュメントに登録した年月を記録することにしました。
mountedでindexサブコレクションのYMドキュメントを取得することで、どの年月がpagesサブコレクションに登録されているか判断できます。pages/index.vue// 省略 <script> // 省略 mounted () { if (this.$store.getters['auth/isLoggedIn']) { // ログインしている場合は以下の処理を続行 let userID = this.$store.getters['auth/getUid']; let docRef = db.collection('notes').doc(userID).collection('index').doc('YM'); let self = this; docRef.get().then( function(doc) { if (doc.exists) { // ドキュメントを取得できたら、ここでチェック&コピー処理 } }); } }, // 省略 </script>
- 投稿日:2020-06-19T17:47:57+09:00
Vue.js コンポーネント作成
コンポーネントを作成する
グローバル登録、ローカル登録、シングルファイルコンポーネントの作成方法がある
<div id="app"> <paragraph></paragraph> </div> <script> Vue.component('paragraph',{ data:function(){ return { counter: 0 } }, template:'<div><button @click="counter++">{{counter}}</button></div>' }) </script>
<paragraph message="hello"></paragraph>の
messageプロパティを受け取り表示させる<div id="app"> <paragraph message="hello"></paragraph> <paragraph message="world"></paragraph> </div> <script> Vue.component('paragraph',{ props:['message'], template:'<div>{{ message }}</div>' })プロパティの値が長い場合、
<slot></slot>で同様の動きが可能<div id="app"> <paragraph>hellohellohello</paragraph> </div> <script> Vue.component('paragraph',{ template:'<div><slot></slot></div>' })
- 投稿日:2020-06-19T17:26:01+09:00
el-uploadを使ったファイル添付機能
概要
laravel+vueで作成したWebサイトにて添付ファイル機能を実装したのでメモ。
フロント側のUIはelement-uiのel-uploadで作った。コード
vue側
<el-row :gutter="24" class="attachment-row"> <el-col :span="24"> <span class="item-label"><i class="fas fa-paperclip icon"></i>添付ファイル</span> <el-upload action="#" list-type="text" class="attachments-upload" :before-remove="beforeAttachmentsRemove" :on-change="addAttachments" :file-list="attachmentsList" :auto-upload="true" :show-file-list="true" :multiple="false" :drag="true" :limit="attachments_limit" :http-request="AttachmentsHttpRequest" > <i class="el-icon-upload"></i> <div class="el-upload__text"> ここにドラッグするか <em>ここをクリックして</em>アップロードしてください。 </div> <div slot="file" slot-scope="{ file }"> <div class="contents" :class="{ delete: file.isDelete }"> <span v-if="file.isDelete" class="el-upload-list__item-deleted"> <span class="el-upload-list__item-label"> <span class="each-row">削除は更新実行後に反映されます。</span> </span> </span> <li> <div v-if="!file.isUploaded"> <i class="el-icon-document icon"></i> <span v-if="!file.isUploaded" class="el-upload-list__item-noactions"> <span class="el-upload-list__item-label"> <span class="each-row">アップロード</span> <span class="each-row">待ち</span> </span> </span> <span class="filename">{{ file.name }}</span> <i class="el-icon-close" :disabled="file.isDelete" @click="deleteAttachments(file)" ></i> </div> <div v-else class="available-download"> <i class="el-icon-document icon" @click="downloadAttachment(file)"></i> <span class="filename" @click="downloadAttachment(file)">{{ file.name }}</span> <i class="el-icon-close close-icon" :disabled="file.isDelete" @click="deleteAttachments(file)" ></i> </div> </li> </div> </div> </el-upload> </el-col> </el-row> (略) private attachments_limit: number = 10; private attachmentsList: AttachementData[] = []; private fileReader: FileReader = new FileReader(); //添付ファイル追加(AttachmentsHttpRequestより後で呼ばれる) addAttachments(file: any, fileList: any) { // File size limitation const isLt5M = file.size / 1024 / 1024 < 100; if (!isLt5M) { this.$message.error("アップロードできるファイルサイズは100Mまでです。"); this.deleteAttachments(file); return false; } // Exceed limit if (this.attachmentsList.length >= this.attachments_limit) { this.$message.error("アップロードできるファイルは" + this.attachments_limit + "つまでです。"); this.deleteAttachments(file); return false; } file.id = file.uid; file.isUploaded = false; file.isDelete = false; this.attachmentsList.push(file); } //添付ファイル削除 deleteAttachments(file: any) { if (file.isUploaded) { //アップロード済み //画面表示リストの削除フラグ立てる let t = this.attachmentsList.filter(function(e) { return e === file; }); t[0].isDelete = true; //サーバへ送るリストの削除フラグを立てる let t2 = this.minute.attachments_base64.filter(function(e) { return e.id == file.uid; }); t2[0].isDelete = true; } else { //未アップロード //画面表示リストから消す this.attachmentsList = this.attachmentsList.filter(function(e) { return e !== file; }); //サーバへ送るリストから消す this.minute.attachments_base64 = this.minute.attachments_base64.filter(function(e) { return e.id != file.uid; }); } } //添付ファイル追加時のアップロード AttachmentsHttpRequest(options: any) { let file = options.file; let filename = file.name; if (file) { this.fileReader.readAsDataURL(file); } this.fileReader.onload = () => { if (!this.minute.attachments_base64.length) { this.minute.attachments_base64 = []; } this.minute.attachments_base64.push({ id: file.uid, filetype: file.type, original_filename: filename, comment: "", //アップロード時は空 base64data: this.fileReader.result, isUploaded: false, isDelete: false }); }; } //画面初期表示時に既存添付ファイルを反映する defaultAttachmentsListLoad(data: Attachement[]) { this.attachmentsList = []; data.forEach((value, index) => { if (this.attachmentsList.length < this.attachments_limit) { this.attachmentsList.push({ id: value.id, name: value.original_filename, comment: value.comment, isUploaded: true, isDelete: false, uid: value.id, url: "" }); } }); } //添付ファイルの属性入力時にdeleteキーやBackSpaceキーでリスト削除が走らないようにするための処理 beforeAttachmentsRemove(file, fileList) { return false; } /** * 添付ファイルダウンロード */ downloadAttachment(file: any) { const _this = this; axios({ url: "/api/minute/download/attachment", params: { minute_id: this.minute.id, attachment_id: file.uid }, method: "GET", responseType: "blob" // これがないと文字化けする }) .then(res => { const blob = new Blob([res.data], { type: res.data.type }); //レスポンスヘッダからファイル名を取得します var fileName = res.headers["download_filename"]; fileName = decodeURI(fileName).replace(/\+/g, " "); //ダウンロードします saveAs(blob, fileName); }) .catch(error => { var errorMsg = "不明"; // 通知 _this.$message({ message: "実行に失敗しました。[" + errorMsg + "]", type: "error", duration: 0, showClose: true }); }); }バックエンド側
[画面表示時のメタデータ取得]//添付ファイル(メタ情報だけ返す) $attachments_base64 = []; if ($minute->attachments) { $attachment_paths = json_decode($minute->attachments); foreach ($attachment_paths as $file) { $attachments_base64[] = [ 'id' => $file->id, 'original_filename' => $file->original_filename, 'isUploaded' => true, 'isDelete' => false ]; } } $minute['attachments_base64'] = $attachments_base64;[保存時]//添付ファイルをファイルとして保存する $attachment_json = []; foreach ($request->attachments_base64 as $file) { $path = str_pad($minute->id, 10, '0', 0) . '_' . $file['id']; if ($file['isUploaded'] && !$file['isDelete']) { //アップロード済みで削除でない->何もしない $attachment_json[] = ['id' => $file['id'], 'path' => $path, 'original_filename' => $file['original_filename']]; } elseif ($file['isUploaded'] && $file['isDelete']) { //アップロード済みで削除する->実体ファイルを削除しJSONに書き込まない if (Storage::disk('local')->exists('/minute/' . $path)) { Storage::disk('local')->delete('/minute/' . $path); } } elseif (!$file['isUploaded']) { //アップロード済されていない->アップロードする $fileData = $file['base64data']; if (count(explode(';', $fileData)) > 1) { list(, $fileData) = explode(';', $fileData); } if (count(explode(',', $fileData)) > 1) { list(, $fileData) = explode(',', $fileData); } $fileData = base64_decode($fileData); Storage::disk('local')->put('/minute/' . $path, $fileData); $attachment_json[] = ['id' => $file['id'], 'path' => $path, 'original_filename' => $file['original_filename']]; } } $minute->attachments = json_encode($attachment_json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); $minute->save(); //DBに添付ファイルパスを更新[ダウンロード時]public function attachmentDownload(Request $request) { $minute = Minutes:: firstOrNew(['id' => $request->minute_id]); //添付ファイル if ($minute->attachments) { $attachment_paths = json_decode($minute->attachments); foreach ($attachment_paths as $file) { if ($file->id == $request->attachment_id) { $mimeType = Storage::mimeType('/minute/' . $file->path); $headers = [['Content-Type' => $mimeType], 'download_filename' => urlencode($file->original_filename)]; return Storage::response('/minute/' . $file->path, $file->original_filename, $headers, 'attachment'); } } } }
- 投稿日:2020-06-19T17:03:42+09:00
Vue CLI+ Laravel 5.8 で、チャット作成 firebase Cloud Messaging対応
概要
前の 、クロスドメイン構成の
Vue-CLI + Laravel 関連となり。チャット機能の作成です・会員制となり、Google認証で
ログインに対応しています。
・PWAも対応しています
・ 前のLaravel版チャットを、Vue CLIに移植した形です構成
PWA
firebase Cloud Messaging / FCM
Notification API
Vue CLI
vue-router
vue/cli-service : 4.4.0
・API サービス:
Laravel 5.8
nginx
mysql
・フロント設置ドメイン ,Vue-CLI:
netlify / ホスティングサービス
画面
・web push 受信時の、通知API で。タスクバー表示
・チャット画面、詳細
Vue components
・index
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/Index.vue・create
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/new.vue・show
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/show.vuepackage.json
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/package.json
参考のページ
https://knaka0209.hatenablog.com/entry/lara58_31cross_chat
・Vue CLI+ Laravel 5.8で、クロスドメイン SPA構成/PWA対応のCRUD作成する。
https://qiita.com/knakaqi/items/1bae7a540aa13ce8233b
- 投稿日:2020-06-19T14:22:37+09:00
Nuxt.js で Slack のようなリアクション機能を実装する
Prologue
Slack のようなリアクション機能をNuxt.js(Vue.js) を使って実装してみました。
CSSの実装やカウント、リアクションをつけたユーザーの表示等に関しては割愛して、今回は Picker と絵文字の表示部分についてのみ説明します。
公式のドキュメントがかなりしっかり作られているので、そちらも確認してみてください。参考:
- Emoji Mart: https://github.com/missive/emoji-mart
- Emoji Mart Vue: https://github.com/jm-david/emoji-mart-vue
環境
- macOS: 10.15.4
- terminal: iTerm
- node.js: 10.16.0
- エディタ: VS Code
使用言語: TypeScript, Nuxt.js, CompositionAPI(v0.5.0)
Project概要
- パッケージマネージャ:
yarn
インストール
emoji-mart-vueをインストールします。yarn add emoji-mart-vueセットアップ
挙動を見たかった為、 Nuxt のプロジェクトを新しく作成し、必要最低限の code を書いていきます。
<template> <div class="section"> <div> <Picker /> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from '@vue/composition-api' // @ts-ignore import { Picker } from 'emoji-mart-vue' export default defineComponent({ name: 'EmojiPicker', components: { Picker, }, }) </script>最低限これだけでも emoji-picker は表示されました。
プロパティをセット
emoji-mart-vueは様々なプロパティを用意してくれているので、必要なものを確認していきます。
<Picker />component にプロパティを渡していきます。参考: https://github.com/jm-david/emoji-mart-vue
<template> <div class="section"> <div> <Picker title="pick emoji" emoji="grinning" set="twitter" color="pink" /> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from '@vue/composition-api' // @ts-ignore import { Emoji, Picker } from 'emoji-mart-vue' export default defineComponent({ name: 'EmojiPicker', components: { Picker }, }) </script>内容:
- title: フッターに表示されるテキスト
- emoji: フッターに表示される初期値の絵文字
- set: pickerに使われる絵文字のデザイン
- color: ヘッダーに使われるカテゴリ hover 時のカラー
個人的には Color を標準で簡単に切り替えられるのはすごい助かりました。CSSをいじればいいだけですが、バージョンアップ等を考えるといじらないに越したことはないので...
表示する内容(カテゴリ)を限定する
emoji-mart-vueはデフォルトでたくさんの絵文字を提供してくれています。
ただ、プロジェクトによっては不要だったり制限したりする可能性もあるので、カテゴリの制限の仕方を確認しました。
余談ですが、実はここがなかなかうまくいかず、一番てこずりました。ドキュメントをよく読まなかったのが敗因です。
includeというプロパティが用意されています。
そこにl18nと同じキーをセットします。複数使用する場合には配列で書くだけです。<template> <div class="section"> <div> <Picker title="pick emoji" emoji="grinning" set="twitter" color="pink" :include="cat" /> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from '@vue/composition-api' // @ts-ignore import { Emoji, Picker } from 'emoji-mart-vue' export default defineComponent({ name: 'EmojiPicker', components: { Picker }, setup(){ const cat = ['search', 'recent', 'people', 'objects'] return { cat } } }) </script>選択した絵文字を任意の場所に表示する
Emojiという component が用意されているので import します。Pickerにselectという event が用意されているため、そこから emoji object を取得します。- Emoji component の
emojiプロパティ に bind します。<template> <div class="section"> <div v-for="(item, index) in state.selectedItem" :key="index" class="emoji-wrapper"> <div> <Emoji :emoji="item" :size="20" class="emoji-image" /> </div> </div> <div> <Picker title="pick emoji" emoji="grinning" set="twitter" color="pink" :include="cat" @select="selectEmoji" /> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from '@vue/composition-api' // @ts-ignore import { Emoji, Picker } from 'emoji-mart-vue' export default defineComponent({ name: 'EmojiPicker', components: { Picker, Emoji }, setup(props, context) { const state = reactive({ selectedItem: ['grinning'], }) const cat = ['search', 'recent', 'people', 'objects'] function selectEmoji(item: any) { if (!state.selectedItem) { state.selectedItem = item.unified } state.selectedItem.push(item) } return { selectEmoji, state, cat, } } }) </script>これで表示されるはずです。
Emoji component を使わないパターンを検討する
ここまでで特に問題なく実装ができました。ただ今回はこの内容をデータとして、なるべくシンプルな形で保存しておく必要がありました。
Emoji Component を使わず、 string だけでやりとりができないものかと方法を探していたら、以下の記事を見つけました。参考: https://medium.com/@allegra9/add-emoji-picker-to-your-react-chat-app-30d8cbe8d9a6
selectevent で取得できる object の中にあるunifiedに接頭辞0xをつけて Unicode とし、それをString.fromCodePointで絵文字として表示します。
- String.fromCodePoint
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint以下の code は 抜粋になります。
state.emojiを template に置くと表示されます。function selectEmoji(item: any) { let splitCode = item.unified.split('-'); let codeArr = new Array() splitCode.forEach((c, index) => { splitCode[index] = `0x${c}` }) state.emoji = String.fromCodePoint(...splitCode) }注意したいのは、
emoji-mart-vueは肌の色なども変えることができるため、取得したunifiedの中身は-区切りの string になる場合があります。
その string 一つ一つの頭に0xをつけないと、望む形の絵文字は表示されません。
最終的にはこの形で渡せればOKです。String.fromCodePoint('0x1f44d', '0x1f3fd')
unifiedの中に-がいくつ存在しうるのか、肌の色以外にもどのパターンでそうなるのか、は一つ一つ検証していないのですが、この形で大丈夫だと思います。
何かあればフィードバックして頂けると助かります。^^Epilogue
絵文字を表示する、という一見するとたくさんやることがありそうな内容ですが、今回見つけた
emoji-martはVue.js用に作られたもの(emoji-mart-vue) があり、助かりました。
ドキュメントもわかりやすく、進めるのが楽しかったです。最後の
unifiedの部分だけ、実装段階まで知らず、気付かずで焦りましたが... JSが素敵な関数用意してくれていてよかった^^ というJSの勉強にもなりました。今回言及していませんが、この他にもSlack風にするためにカウントを増やしたり tooltip を実装したりしています。また、絵文字のサイズも任意で変えられたりするので、ぜひ実装する際には公式ドキュメントを見ながら進めてみてください。
何かおかしなところや、こういう方法もある、というものがありましたらお気軽にメンションしてください。
- 投稿日:2020-06-19T12:44:15+09:00
Quasar × VSCodeでのデバッグ(ブレークポイント張ったりなど)
1 概要
Quasar FrameworkをVSCodeでデバッグするときの手順
公式サイトサクッと3ポイント
①Debugger for Chromeをインストールしましょう
.vscode\launch.jsonを作成してこれをコピペ!launch.json{ // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Quasar App: chrome", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/src", "breakOnLoad": true, "sourceMapPathOverrides": { "webpack:///./src/*": "${webRoot}/*" } } ] }②quasar.conf.jsにsource-mapの設定を追加
quasar.conf.jsbuild: { //他の設定とか devtool: 'source-map', //←追加 extendWebpack(cfg) { }③quasar devコマンドを実行後、VSCode側からデバック実行をスタート!
終わり
後はブレークポイント張るなり、お好きにどうぞ
quasar devで立ち上げた環境にVSCode側からアタッチするような感じの動きになっているようです
- 投稿日:2020-06-19T08:34:29+09:00
Vue.jsからaxiosでGoにパラメータ渡せなくてつまり続けたが、Echo導入したら即解決した
Vueで入力された値をJson形式でGoに渡して処理をさせたかった。
うまい感じに渡せず、詰まったがEcho導入したら即解決した。やりたいこと
- VueからPost(非同期)でパラメータを渡して、Goで処理をさせたい。
- とりあえず、Axiosで送信したparamsがGoで受け取れていることを確認したい。
VueとAxios
あるボタン押下時にtestを実行して、data1とdata2をjson形式でpostする。
Goの処理結果をconsoleに表示する。axiosはpost時、以下のように渡せば自動的にjson形式にしてくれるらしい。
vue.js(一部抜粋)<script> export default { name: 'SqlGenerater', data() { return { data1:'1', data2:'2', } }, methods:{ test(){ let params= { data1: this.data1, data2: this.data2, } this.$axios.get('/api',{ params: params }) .then(response => { console.log(response.data) }) } } } </script>Go(Echo)
Echo公式のrequestにかなりわかりやすく書いてある。
下記のようにすれば自動でバインドできるみたい。すごく便利ぃmain.gopackage main import ( "net/http" "github.com/labstack/echo" ) type Test struct { Data1 string `json:"data1" form:"data1" query:"data1"` Data2 string `json:"data2" form:"data2" query:"data2"` } func main() { e := echo.New() e.GET("/", index) e.Logger.Fatal(e.Start(":8081")) } func index(c echo.Context) (err error) { t := new(Test) if err = c.Bind(t); err != nil { return } return c.JSON(http.StatusOK, t) }実行したら、ちゃんと取れてることが確認できた。
console{data1: "1", data2: "2"}ありがとうEcho
- 投稿日:2020-06-19T01:12:26+09:00
Vue.jsで複数モーダルを表示した時に背景(オーバーレイ)が重ならないようにする
こんな感じのモーダルコンポーネントがあるとします。
<template> <div class="background" @click.self="closeModal()"> <div class="Modal--form__medium"> <div class="Modal__header"> <div v-show="title" class="Modal__title--center"> {{ title }} </div> <slot name="header_button" /> </div> <slot name="subtile" /> <div class="Modal__body--dialog"> <slot name="main" /> </div> <div class="Modal__footer"> <slot name="footer" /> </div> <slot name="modal" /> </div> </div> </template>backgroundクラスにモーダルのオーバーレイにbackground-color:rgba(0,0,0,0.75);みたいなスタイルが設定することが多いと思います。
しかし、そうした場合、複数のモーダルを表示した場合にbackground-colorが重なってしまって濃い色になってしまう場合があります。
つまりは、一つのモーダルの背景のみbackground-colorをつけたいです。そこで以下のようなmixinを書くことで複数モーダルが出ていても必ず一つのモーダルのみ背景を当てることができます。
document.querySelectorは一番最初に見つけた要素しかとってこないためその仕組みを利用して#backgroundにスタイルを当てています。import { mapGetters } from 'vuex'; export default { computed: { ...mapGetters({ modals: 'modal/modals' }), }, watch: { modals: { immediate: true, handler(newValue) { this.$nextTick(() => { const background = document.querySelector('#background'); if (background) background.style.background = 'rgba(0, 0, 0, 0.5)'; }); } } } };これを使えば複数のモーダルの管理しやすくなると思いますので是非お試しください。
もっといい方法あるがあればコメントいただければと思います!
そろそろvue3もでるし、勉強熱が高まりますね。
ここまで見ていただきありがとうございました。
- 投稿日:2020-06-19T00:05:43+09:00
Vue.js 軽くおさわり
はじめに
フロントエンドのフレームワークを勉強してみようと思った。
ReactとVue.jsどちらにしようか悩んだのでとりあえずどちらも軽く触れてみてそこから勉強する対象を決定しようと思います。軽く触るついでに軽くアウトプットも、ただ軽く触る程度なので内容は薄っぺらいし間違ったことを書いている可能性があります。
※公式ドキュメントに従っておさわりしてます。環境、バージョン
- OS Windows 10 Home
- Vue.js 2.x
導入~軽く触る
ここからが内容
導入
軽く程度なのでライブラリはダウンロードしていません。
以下のように定義して導入。donyu.html<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>組んでみる
Hello World的な
こんなんでできました。
helloVue.js<div id="helloDiv"> {{ message }} </div> <script type="text/javascript"> let app = new Vue({el: '#helloDiv', data: {message: 'Hello Vue'}}) </script>
elで対象のタグを取得、data連想配列で定義した情報を{{ <key> }}に設定してる感じですね。
ブラウザで表示するとこのdiv中にはDello Vueが表示されます。※ちゃんと
divタグより下にscriptタグを入れないと動かないのでご注意を!
フロントエンドを普段触られてる方は大丈夫でしょうが、私は普段フロントエンドを触らないので少しはまりました!チュートリアルに「ブラウザの JavaScript コンソールを開いて・・・」と記載があります。
ブラウザの開発者ツール画面を開き、コンソールでapp.message = '<変更値>を打つとブラウザに反映されます。
この機能初めて知った・・・。条件分岐
タグ内に
v-if属性を付与することで条件分岐が可能となります。
以下のように組み込む。vuewif.html<div id="ifDiv" v-if="see"> 表示するかしないかのやーつ </div> <script type="text/javascript"> let ifApp = new Vue({el: "#ifDiv", data: {see: true}}); </script>
data.seeにtrueが入ってるのでdiv要素が表示されます。
逆にfalseを設定すると非表示になります。繰り返し
タグ内に
v-for属性を付与することで繰り返しが可能となります。
以下のように組み込む。vuefor.html<div id="forDiv"> <div v-for="itemObj in itemList"> {{ itemObj.txt }} </div> </div> <script type="text/javascript"> let forApp = new Vue({el: "#forDiv", data: {itemList: [ {txt: 'アイテムその1'}, {txt: 'アイテムその2'}, {txt: 'アイテムその3'}, ]}}) </script>
itemListに格納されているデータ分繰り返してv-forが設定されているタグを追加します。※Vue.js公式ドキュメントではリストタグで紹介してます。そちらの方がわかりやすいですね。
※注意事項※
以下のように組み込むとエラーとなります。errfor.html<div id="forDiv" v-for="itemObj in itemList"> {{ itemObj.txt }} </div> <!-- ※<script>の中は「vuefor.html」と同じとします。 -->TODO 上記の説明考える
コンポーネント
htmlのタグごと定義してテンプレート要素を作成するもの。的な感じかな?
違う気がするので実装参照↓component.html<ol id="cust"> <cust-compo v-for="item in liItemList" v-bind:liitem="item" v-bind:key="item.id"></cust-compo> </ol> <script type="text/javascript"> // コンポーネントを定義 Vue.component('cust-compo', { props: ['liitem'], template: '<li>{{ liitem.txt }}</li>' }); // 定義したコンポーネントをインスタンス化 let custCompo = new Vue({el: '#cust', data: { liItemList: [ {id: "001", txt: "あいてむ1"}, {id: "002", txt: "あいてむ2"}, {id: "003", txt: "あいてむ3"}, ] } }); </script>少し複雑ですね。
例のようにタグを含んだ情報をテンプレートとして定義すると他の機能で使いまわせる独自タグになる感じでしょうか?
もうちょっと調べたり触ってみないと真相はつかめないかなと。さいごに
Vue.jsの公式ドキュメントの「はじめに」で紹介されている機能を組んで動かしてみました。
この記事はここまでですが、もう少し触ってみようかなと思います。以上、
Vue.js軽くおさわりでした。



