- 投稿日:2020-08-11T23:20:35+09:00
【Vue.js】Vue CLIグローバルフィルタの作成
【Vue.js】Vue CLIグローバルフィルタの作成
Vue CLIで作成したサイトでグローバルフィルタを使う方法。
グローバルでフィルタを使用するには、filter関数とfilter名の登録が必要になる。下記ページを参考にさせていただきました。
https://teratail.com/questions/281227手順
- srcフォルダ配下にfilterディレクトリとindex.jsを作成
- vueのインポート
- フィルタの作成
- main.jsで作成したフィルタをインポート
- フィルタを適用する
1. Sorceフォルダ配下にfilterディレクトリとindex.jsを作成する
filterのライブラリとしてディレクトリを作成する。
フォルダやファイルの場所や名前は任意だが、以下では、srcディレクトリ配下に作成する例とする。
・ディレクトリとファイル
*PJ > src > filter > index.js *
2. vueのインポート
フィルタ関数とIDを登録するために、まずはvue自信をインポートする。
index.jsimport Vue from "vue"
<import Vue from "vue"について>
import Vue from "vue"は下記式と同義らしい。
何をしているかは明確ではないが、グローバルで使用できるVueインスタンスを生成しているとみられる。import Vue from "vue" ↓ const Vue = require('vue') ↓ const vue = require('./node_modules/vue/dist/vue.runtime.common.js'); ↓ if (process.env.NODE_ENV === 'production') { module.exports = require('./vue.runtime.common.prod.js') } else { module.exports = require('./vue.runtime.common.dev.js') }
3. フィルタの作成
Vue.filterを使い、フィルタ名と処理を定義する。
Vue.filter('フィルタ名', function(引数){return 処理})
1000桁区切りをするフィルタ1000separatorを作成する場合。
index.jsimport Vue from "vue" Vue.filter('1000separator', function(value) { return value.toLocaleString(); })
4. main.jsで作成したフィルタをインポートする。
srcディレクトリ配下のmain.jsファイルに移動し、filterディレクトリをインポートする。
main.jsimport "./filter"
5. フィルタを適用する
フィルタを適用したい箇所に移動し、フィルタを呼び出す。
呼び出し方は、マスタッシュ展開を用いる方法と、v-bindを用いる方法の2種類。(1)Mustache展開
{{式| フィルタ名}}
(2)v-bind
<要素 v-bind:属性="プロパティ名 | フィルタ名">
マスタッシュ展開でフィルタを使用する例。マスタッシュ展開<em>${{number | 1000separator}}</em>dataオプションのnumberプロパティに格納された数値を1000桁区切りにする。
以上。
- 投稿日:2020-08-11T18:05:51+09:00
【Vue.js】Vue CLIでaxiosを使う方法
【Vue.js】Vue CLIでaxiosを使う方法
Vue CLIで外部APIからデータ取得するためにaxiosを使う方法について。
通常のhtmlファイル上でaxiosを使うのと方法が異なるので注意が必要。
手順
- プロジェクトにaxiosをインストールする
- main.jsにaxiosをインポートする
- axiosを使いたいファイルにaxiosをインポートする
- axiosを使った処理を記述する
1. プロジェクトにaxiosをインストールする
初めに、プロジェクト自体にaxiosをインストールする必要がある。
ターミナルでプロジェクトフォルダに移動後、下記を実行。
$ npm install axiosインストールが完了すると、「package.json」ファイルのdependencies(PJに依存するパッケージ)にaxiosが追加される。
2. main.jsにaxiosをインポートする
main.jsにインポートすることで、他のファイルでもaxiosを呼び出せるようにする。
main.jsimport axios from 'axios'<以下補足>
Vue.use(axios)
を記述するといったページも散見されたが、これを記述するとエラーが発生する。
そして、記載せずとも無事APIが動く。Vue.use()はvue.js用のライブラリを読み込む場合に使用する。axiosはvue.js専用でないため読み込み不要。
3. axiosを使いたいファイルにaxiosをインポートする
axiosを実行したいファイルのscriptタグ内で、axiosをインポートする。
今回はBitcoinAPI.vueファイルに記述。
BitcoinAPI.vueimport axios from 'axios'
4. axiosを使った処理を記述する
scriptタグのexport defaultオプション内に、
axios.get('APIエンドポイントのURL')
を使って処理を記述する。export-default<script> import axios from 'axios' export default { name: 'BitcoinAPI', //API格納用のプロパティ data: function(){ return{ data:{ bpi: null } } }, //axiosによるデータ取得処理 mounted: function(){ axios.get('https://api.coindesk.com/v1/bpi/currentprice.json') .then(function(response){ //デバッグ用にconsoleに出力 console.log(response.data.bpi) this.bpi = response.data.bpi }.bind(this)) .catch(function(error){ console.log(error) }) } }
▼取得したデータがconsoleに表示できている。
以上。
- 投稿日:2020-08-11T18:03:48+09:00
【Nuxt.js】Nuxt実践編:ユーザーアイコン表示の切り替えをしよう
? この記事はWP専用です
https://wp.me/pc9NHC-ye前置き
今回は簡単な実践編です??
作るものはこちら!マイページのあるサイトで
⬆️ユーザーアイコンの初期状態と
⬇️画像を設定した時の
表示の切り替えをやってみましょう??コンポーネント間のやりとりや
演算子の復習になるのでぜひチャレンジしてください??今回のような簡単な実践編も
今後増やしていこうと思ってます?Let's try!
ディレクトリ
filecomponents/ --| atom/ ----| icons/ ------| IconUser.vue --| molecule/ ----| items/ ------| ItemUserIconName.vue pages/ --| user.vueアトミックデザインに基づき作成します?
atomはアイコン部分。
他のmoleculeにも使い回す前提です。
moleculeはアイコンとユーザーの名前を追加したものです。アドミックデザイン
要素の大きさごとにファイルを分けます?♀️
コンポーネントがどこにあるのか
分かりやすくなります?
大きさの分類はここが参考になります!
Atomic Designとはatom
構成
IconUser.vueを作成しましょう?♀️
初期アイコンと、
アイコンが設定された場合の
表示を切り替えます。
アイコン画像は親で設定します。背景は縦横64px
アイコンは縦横24pxアイコンはiconmonstrのsvgを使用しています?
https://iconmonstr.com/user-5-svg/それではやってみましょう!!!
ticktack…
まずは切り替えをするので
v-ifとv-elseが使えますね…
親で画像を設定するのでpropsをstringで渡します。
stringを真偽値にしてv-ifとv-elseにしたい…それではコードを見てみましょう✨?
解説
? 続きはWPでご覧ください?
https://wp.me/pc9NHC-ye
- 投稿日:2020-08-11T16:47:07+09:00
【 Vue.js】Vue CLI 新規ページの追加方法
【 Vue.js】Vue CLI 新規ページの追加方法
Vue CLIで作成したサイトに新たにページを追加する方法。
手順
- viewsフォルダ配下に新規ファイルを作成する
- ルーティングを追加
- router-linkタグでリンクを追加
1. viewsフォルダ配下に新規ファイルを作成する
viewsフォルダ配下にページを作成する。ファイル名はコンポーネントの作成規則に則り大文字始まり、キャメルケースとする。
以下、BitcoinAPI.vueを作成場合。
2. ルーティングを追加
routerフォルダのindex.jsファイルのルーティングに作成したページを追加する。
必須なのは、①pathオプションと②componentオプションの2つ。
①path:URLを記述。
②component:指定の記述で作成したファイルを指定。
component: () => import('../views/ファイル名')
nameオプションを追加しておくと、リンクを設置する際に、pathではなく、nameで指定した値も使える。
name:'ルーティングの名前'
index.jsconst routes = [ { path: '/bitcoin-api', component: () => import('../views/BitcoinAPI.vue'), } ]
3. router-linkタグでリンクを追加
リンクを設置したいページのtemplateタグ内に、router-linkタグを設置する。
リンク先のpathはto属性に記述する。
<router-link to="相対パス">アンカーテキスト</router-link>
templateタグ<router-link to="/bitcoin-api">Bitcoin API</router-link>設置したリンクから、該当URLでページが開けば完了。
- 投稿日:2020-08-11T16:19:21+09:00
Vue CLIをインストールしたらcommand not found: vueになったから解決したやり方共有!
はじめに
本日2回目の投稿になってしまいました。
先ほど、Vue CLIを導入しようと思って、ターミナルを操作してたら、なんかひたすらcommand not found: vueになってちょっと手間取ったので、無事解決したやり方を、こちらで共有させて頂きます。【利用環境】
MacBook pro(macOS Catalina バージョン10.15.6)
ターミナル(zsh)
node.js v12.18.2
npm 6.14.5Vue CLIをインストールしてみる
ターミナルに次のコマンドを入力し、インストールを試みました。
npm install -g @vue/cliしばらく待つと...
+ @vue/cli@4.5.2 added 1307 packages from 704 contributors in 29.079sっていうのが出てきたので、「いけた!」と思ったんです。
しかし!vue --versionとすると...
zsh: command not found: vueとなってしまって、「vue createできないじゃん!」ってなりました。
解決策
次のコマンドをターミナルに入力してください。
export PATH=~/.npm_global/bin:$PATHすると...
vue —version @vue/cli 4.5.2はい、解決しました!!!無事、vue createもできました!
もし、Macを利用していて同じような問題に遭遇してしまった方の手助けになれればと思います!
参考資料
以下のStack Overflowの質問・回答を参考にさせて頂きました!!
https://stackoverflow.com/questions/58590890/how-to-fix-zsh-command-not-found-vue
- 投稿日:2020-08-11T15:46:55+09:00
WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(2/2)
前回の投稿で、WiiリモコンとヌンチャクとバランスボードをNode.jsで動作させてMQTTに接続するところまでできました。( WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2) )
今回は、それをブラウザからSubscribeして、ボタンの押下状態を見たり、加速度センサやバランスボードの重量をリアルタイムに表示させたりしてみます。
ソース一式をアップしたGitHubはこちら
https://github.com/poruruba/WiiRemoconブラウザページはこちら
https://poruruba.github.io/WiiRemocon/html/ブラウザからのMQTT接続
以下のライブラリを使います。
Eclipse Paho Javascript Client
https://www.eclipse.org/paho/clients/js/こんな感じです。
start.jsconnect_mqtt: function(){ mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id ); mqtt_client.onMessageArrived = this.mqtt_onMessagearrived; mqtt_client.onConnectionLost = this.mqtt_onConnectionLost; mqtt_client.connect({ onSuccess: this.mqtt_onConnect }); }, mqtt_onConnect: function(){ console.log("MQTT.onConnect"); this.connected = true; wii = new WiiClient(mqtt_client, this.topic_cmd); this.init_graph(); mqtt_client.subscribe(this.topic_evt); },主要なファイル構成
〇js/start.js
Web画面の制御と、MQTT Subscribeして受信したデータの処理を行います。
加速度やバランスボードの重量のグラフ表示やヌンチャクのスティックの状態の表示に、Chart.jsを利用しています。また、ボタン押下状態や拡張コントローラ接続状態の表示には、双方向データバインディングが便利なVue.jsを利用しています。また、CSSテンプレートはBootstrap v3.4.1を利用しています。・Chart.js
https://www.chartjs.org/・Vue 2.x
https://jp.vuejs.org/v2/guide/・Bootstrap v3.4.1
https://getbootstrap.com/docs/3.4/〇js/wiiclient.js
Wiiリモコンたちに処理を依頼するMQTTのPublish処理をします。また、start.jsで行う受信データの解析に有用な関数群を提供します。Javascriptソースコード
メインとなるstart.jsのソースコードです。
start.js'use strict'; //var vConsole = new VConsole(); const mqtt_url = "【MQTTブローカのURL(WebSocket接続】"; var mqtt_client = null; const MQTT_CLIENT_ID = "browser"; const MQTT_TOPIC_CMD = 'testwii_cmd'; const MQTT_TOPIC_EVT = 'testwii_evt'; const UPDATE_INTERVAL = 200; const NUM_OF_DATA = 50; const NUM_OF_STICK_DATA = 3; var timer = null; var wii; var acc_x, acc_y, acc_z; var nck_evt; var blc_evt; var blc_temperature; var vue_options = { el: "#top", data: { progress_title: '', // for progress-dialog mqtt_url: mqtt_url, update_interval: UPDATE_INTERVAL, chk_btns: [], chk_nck_btns: [], battery: 0, wii_type: "remocon", btaddress: "", battery: 0, reporting_mode: WIIREMOTE_REPORTID_BTNS, flags: [], remote_address: "", blc_calibration: null, blc_battery: false, blc_total_weight: 0, connected: false, leds : [], rumble: false, topic_cmd: MQTT_TOPIC_CMD, topic_evt: MQTT_TOPIC_EVT, client_id: MQTT_CLIENT_ID, }, computed: { }, methods: { change_rumble_led: function(){ if(!this.connected) return; var value = 0; if( this.leds[0] ) value |= WIIREMOTE_LED_BIT0; if( this.leds[1] ) value |= WIIREMOTE_LED_BIT1; if( this.leds[2] ) value |= WIIREMOTE_LED_BIT2; if( this.leds[3] ) value |= WIIREMOTE_LED_BIT3; if( this.rumble ) value |= WIIREMOTE_RUMBLE_MASK; var data = [WIIREMOTE_REPORTID_LED, value ]; wii.writaValue(data); }, change_reporting_mode: function(){ var data = [WIIREMOTE_REPORTID_REPORTINGMODE, 0x00, this.reporting_mode ]; wii.writaValue(data); }, stop_graph: function(){ if( timer != null ){ clearTimeout(timer); timer = null; } }, init_graph: function(){ if( timer != null ){ clearTimeout(timer); timer = null; } var labels = []; for( var i = 0 ; i < NUM_OF_DATA ; i++ ) labels.push( -(NUM_OF_DATA - i - 1) * this.update_interval ); for( var i = 0 ; i < myChart_acc.data.datasets.length ; i++ ){ myChart_acc.data.datasets[i].data = []; for( var j = 0 ; j < NUM_OF_DATA ; j++ ) myChart_acc.data.datasets[i].data.push(NaN); } myChart_acc.data.labels = labels; for( var i = 0 ; i < myChart_nck_acc.data.datasets.length ; i++ ){ myChart_nck_acc.data.datasets[i].data = []; for( var j = 0 ; j < NUM_OF_DATA ; j++ ) myChart_nck_acc.data.datasets[i].data.push(NaN); } myChart_nck_acc.data.labels = labels; for( var i = 0 ; i < myChart_nck_stk.data.datasets.length ; i++ ){ myChart_nck_stk.data.datasets[i].data = []; for( var j = 0 ; j < NUM_OF_STICK_DATA ; j++ ) myChart_nck_stk.data.datasets[i].data.push(NaN); } timer = setInterval(() =>{ this.update_graph(); }, this.update_interval); }, update_graph: function(){ myChart_acc.data.datasets[0].data.push(acc_x); myChart_acc.data.datasets[1].data.push(acc_y); myChart_acc.data.datasets[2].data.push(acc_z); myChart_acc.data.datasets[0].data.shift(); myChart_acc.data.datasets[1].data.shift(); myChart_acc.data.datasets[2].data.shift(); myChart_acc.update(); if( nck_evt ){ myChart_nck_acc.data.datasets[0].data.push(nck_evt.acc_x); myChart_nck_acc.data.datasets[1].data.push(nck_evt.acc_y); myChart_nck_acc.data.datasets[2].data.push(nck_evt.acc_z); myChart_nck_acc.data.datasets[0].data.shift(); myChart_nck_acc.data.datasets[1].data.shift(); myChart_nck_acc.data.datasets[2].data.shift(); myChart_nck_acc.update(); myChart_nck_stk.data.datasets[0].data.push({ x: nck_evt.stk_x, y: nck_evt.stk_y }); myChart_nck_stk.data.datasets[0].data.shift(); myChart_nck_stk.update(); } if( this.blc_calibration && blc_evt ){ var weight = wii.calcurateBalanceBoard(blc_evt, this.blc_calibration); myChart_blc.data.datasets[0].data[0] = weight.topright; myChart_blc.data.datasets[1].data[0] = weight.bottomright; myChart_blc.data.datasets[2].data[0] = weight.topleft; myChart_blc.data.datasets[3].data[0] = weight.bottomleft; this.blc_total_weight = weight.total_weight; myChart_blc.update(); } }, mqtt_onMessagearrived: function(message){ try{ var topic = message.destinationName; if( topic == this.topic_evt){ var msg = JSON.parse(message.payloadString); console.log(msg); if( msg.rsp == WIIREMOTE_CMD_EVT){ var event = wii.parseReporting(msg.evt); if(event.btns != undefined){ for( var i = 0 ; i < 16 ; i++ ) this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0); } if(event.acc_x != undefined) acc_x = event.acc_x; if(event.acc_y != undefined) acc_y = event.acc_y; if(event.acc_z != undefined) acc_z = event.acc_z; if(event.battery != undefined) this.battery = event.battery; if(event.flags != undefined){ this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false; this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false; this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false; this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false; } if(event.report_id == WIIREMOTE_REPORTID_BTNS_EXT8 || event.report_id == WIIREMOTE_REPORTID_BTNS_EXT19 || event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_EXT16 || event.report_id == WIIREMOTE_REPORTID_BTNS_IR10_EXT9 || event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 || event.report_id == WIIREMOTE_REPORTID_EXT21) { if( this.wii_type == "remocon"){ nck_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_NUNCHUCK, event.extension); for( var i = 0 ; i < 2 ; i++ ) this.$set(this.chk_nck_btns, i, (nck_evt.btns & (0x0001 << i)) != 0); }else if( this.wii_type = "balance"){ blc_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_BALANCEBOARD, event.extension); if( blc_evt.temperature != undefined) blc_temperature = blc_evt.temperature; if( blc_evt.battery != undefined) this.blc_battery = blc_evt.battery; } } }else if( msg.rsp == WIIREMOTE_CMD_REQ_STATUS){ var event = wii.parseReporting(msg.status); console.log(event); if(event.btns != undefined){ for( var i = 0 ; i < 16 ; i++ ) this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0); } if(event.acc_x != undefined) acc_x = event.acc_x; if(event.acc_y != undefined) acc_y = event.acc_y; if(event.acc_z != undefined) acc_z = event.acc_z; if(event.battery != undefined) this.battery = event.battery; if(event.flags != undefined){ this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false; this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false; this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false; this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false; } }else if( msg.rsp == WIIREMOTE_CMD_READ_REG_LONG){ if( msg.offset == WIIREMOTE_ADDRESS_BALANCE_CALIBRATION){ this.blc_calibration = wii.parseBalanceBoardCalibration(msg.value); console.log(this.blc_calibration); } }else if( msg.rsp == WIIREMOTE_CMD_REQ_REMOTE_ADDRESS){ if(msg.address) this.remote_address = wii.addr2str(msg.address); else this.remote_address = ""; }else if( msg.rsp == WIIREMOTE_CMD_ERR ){ console.error("WIIREMOTE_CMD_ERR: " + msg.error); }else{ throw 'unknown rsp'; } }else{ console.error('Unknown topic'); } }catch(error){ console.error(error); } }, mqtt_onConnectionLost: function(errorCode, errorMessage){ console.log("MQTT.onConnectionLost", errorCode, errorMessage); this.connected = false; this.stop_graph(); }, mqtt_onConnect: function(){ console.log("MQTT.onConnect"); this.connected = true; wii = new WiiClient(mqtt_client, this.topic_cmd); this.init_graph(); mqtt_client.subscribe(this.topic_evt); }, connect_wii: function(){ if(!this.connected) return; wii.connect(this.btaddress); }, disconnect_wii: function(){ wii.disconnect(); }, request_status: function(){ if(!this.connected) return; wii.requestStatus(); wii.requestRemoteAddress(); }, enable_extension: function(){ if(!this.connected) return; wii.enableExtension(true); }, update_calibration: function(){ if(!this.connected) return; wii.readRegisterLong(WIIREMOTE_ADDRESS_BALANCE_CALIBRATION, 0x20); }, connect_mqtt: function(){ mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id ); mqtt_client.onMessageArrived = this.mqtt_onMessagearrived; mqtt_client.onConnectionLost = this.mqtt_onConnectionLost; mqtt_client.connect({ onSuccess: this.mqtt_onConnect }); }, disconnect_mqtt: function(){ if(!this.connected) return; mqtt_client.disconnect(); }, }, created: function(){ }, mounted: function(){ proc_load(); } }; vue_add_methods(vue_options, methods_bootstrap); vue_add_components(vue_options, components_bootstrap); var vue = new Vue( vue_options ); var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), { type: 'line', data: { labels: [], datasets: [{ label: "acc_x", fill: false, data: [] },{ label: "acc_y", fill: false, data: [] }, { label: "acc_z", fill: false, data: [] }] }, options: { animation: false, scales: { yAxes: [{ ticks: { suggestedMax: 255 * 4, suggestedMin: 0, } }] }, plugins: { colorschemes: { scheme: 'brewer.Paired12' }, } } }); var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), { type: 'line', data: { labels: [], datasets: [{ label: "acc_x", fill: false, data: [] },{ label: "acc_y", fill: false, data: [] }, { label: "acc_z", fill: false, data: [] }] }, options: { animation: false, scales: { yAxes: [{ ticks: { suggestedMax: 255 * 4, suggestedMin: 0, } }] }, plugins: { colorschemes: { scheme: 'brewer.Paired12' }, } } }); var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), { type: 'scatter', data: { labels: ["nck_stk"], datasets: [{ label: "stick", data: [] }] }, options: { scales: { xAxes: [{ ticks: { suggestedMax: 255, suggestedMin: 0, } }], yAxes: [{ ticks: { suggestedMax: 255, suggestedMin: 0, } }] }, plugins: { colorschemes: { scheme: 'brewer.RdYlBu11' }, } } }); var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), { type: 'bar', data: { labels: [], datasets: [{ label: "top_right", data: [] },{ label: "bottom_right", data: [] },{ label: "top_left", data: [] }, { label: "bottom_left", data: [] }] }, options: { animation: false, scales: { yAxes: [{ ticks: { suggestedMax: 34, suggestedMin: 0, } }] }, plugins: { colorschemes: { scheme: 'brewer.Paired12' }, } } });〇グラフの準備
以下の部分でグラフの準備をしています。あとは、リアルタイムに現在値で反映すればよいだけです。
var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), {
var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), {
var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), {
var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), {加速度に線グラフ、ヌンチャクのスティックに散布図、バランスボードの重量に棒グラフを採用しています。
色の選択が面倒なので以下のプラグインを使わせていただきました。chartjs-plugin-colorschemes
https://nagix.github.io/chartjs-plugin-colorschemes/ja/〇グラフ再描画の仕組み
グラフは固定の周期で更新をかけています。
setIntevalを呼び出しておいて、周期的にupdate_graphを呼んでいます。一方、MQTTで送られてくる受信データであるデータレポーティングは、非同期に受信されてきますので、受信したデータは変数に格納しておきます。
したがって、受信タイミングと描画タイミングは一致していません。もしもっとスムーズに動きを見たければ、再描画間隔を短くしてみてください。デフォルトで200msecにしています。50msecにすると、ヌンチャクのスティックの状態がかなりスムーズに見えるようになります。
〇受信データの解析
MQTTでSubscribeして受信したレポーティングデータは、mqtt_onMessagearrivedが呼び出されて受領できるようにしています。解析のための関数は、wiiclient.jsにまとめておきました。
〇バランスボードの重量の表示
バランスボードの重量の表示には、キャリブレーションデータが必要です。
そのため、生データのデータレポーティングが受信されていても、キャリブレーションデータも受信された状態となって初めてグラフに反映するようにしています。Wiiへの要求のためのユーティリティ
Wiiへの要求のための各種ユーティリティです。
さきほど述べましたが、Wiiへの要求のみで、応答はstart.jsの方で処理します。wiiclient.js'use strict'; const WIIREMOTE_CMD_EVT = 0x00; const WIIREMOTE_CMD_ERR = 0xff; const WIIREMOTE_CMD_CONNECT = 0x01; const WIIREMOTE_CMD_DISCONNECT = 0x02; const WIIREMOTE_CMD_WRITE = 0x03; const WIIREMOTE_CMD_ENABLE_SOUND = 0x04; const WIIREMOTE_CMD_ENABLE_EXTENSION = 0x05; const WIIREMOTE_CMD_REQ_REMOTE_ADDRESS = 0x06; const WIIREMOTE_CMD_READ_REG = 0x07; const WIIREMOTE_CMD_WRITE_REG = 0x08; const WIIREMOTE_CMD_REQ_STATUS = 0x09; const WIIREMOTE_CMD_READ_REG_LONG = 0x0a; const WIIREMOTE_EXT_TYPE_NUNCHUCK = 0x01; const WIIREMOTE_EXT_TYPE_BALANCEBOARD = 0x02; const WIIREMOTE_REPORTID_RUMBLE = 0x10; const WIIREMOTE_REPORTID_LED = 0x11; const WIIREMOTE_REPORTID_REPORTINGMODE = 0x12; const WIIREMOTE_REPORTID_IR_ENABLE = 0x13; const WIIREMOTE_REPORTID_SPEAKER_ENABLE = 0x14; const WIIREMOTE_REPORTID_STATUS_REQUEST = 0x15; const WIIREMOTE_REPORTID_WRITE = 0x16; const WIIREMOTE_REPORTID_READ = 0x17; const WIIREMOTE_REPORTID_SPEAKER_DATA = 0x18; const WIIREMOTE_REPORTID_SPEAKER_MUTE = 0x19; const WIIREMOTE_REPORTID_IR2_ENABLE = 0x1a; const WIIREMOTE_REPORTID_STATUS = 0x20; const WIIREMOTE_REPORTID_READ_DATA = 0x21; const WIIREMOTE_REPORTID_ACK = 0x22; const WIIREMOTE_REPORTID_BTNS = 0x30; const WIIREMOTE_REPORTID_BTNS_ACC = 0x31; const WIIREMOTE_REPORTID_BTNS_EXT8 = 0x32; const WIIREMOTE_REPORTID_BTNS_ACC_IR12 = 0x33; const WIIREMOTE_REPORTID_BTNS_EXT19 = 0x34; const WIIREMOTE_REPORTID_BTNS_ACC_EXT16 = 0x35; const WIIREMOTE_REPORTID_BTNS_IR10_EXT9 = 0x36; const WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 = 0x37; const WIIREMOTE_REPORTID_EXT21 = 0x3d; const WIIREMOTE_FLAG_BIT_BATTERY_EMPTY = 0x01; const WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED = 0x02; const WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED = 0x04; const WIIREMOTE_FLAG_BIT_IR_ENABLED = 0x08; const WIIREMOTE_RUMBLE_MASK = 0x01; const WIIREMOTE_LED_MASK = 0xf0; const WIIREMOTE_LED_BIT0 = 0x80; const WIIREMOTE_LED_BIT1 = 0x40; const WIIREMOTE_LED_BIT2 = 0x20; const WIIREMOTE_LED_BIT3 = 0x10; const WIIREMOTE_ADDRESS_BALANCE_CALIBRATION = 0xa40020; class WiiClient { constructor(mqtt_client, topic_cmd) { this.mqtt_client = mqtt_client; this.topic_cmd = topic_cmd; } connect(address, retry = 2){ var data = { cmd: WIIREMOTE_CMD_CONNECT, address: this.addr2bin(address), retry: retry }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } disconnect(){ var data = { cmd: WIIREMOTE_CMD_DISCONNECT, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } writaValue(value){ var data = { cmd: WIIREMOTE_CMD_WRITE, value: value }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } enableSound(enable){ var data = { cmd: WIIREMOTE_CMD_ENABLE_SOUND, enable: enable, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } enableExtension(enable){ var data = { cmd: WIIREMOTE_CMD_ENABLE_EXTENSION, enable: enable, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } requestRemoteAddress(){ var data = { cmd: WIIREMOTE_CMD_REQ_REMOTE_ADDRESS, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } readRegister(offset, len){ var data = { cmd: WIIREMOTE_CMD_READ_REG, offset: offset, len: len, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } writeRegister(offset, data){ var data = { cmd: WIIREMOTE_CMD_WRITE_REG, offset: offset, data: data, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } requestStatus(){ var data = { cmd: WIIREMOTE_CMD_REQ_STATUS, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } readRegisterLong(offset, len){ var data = { cmd: WIIREMOTE_CMD_READ_REG_LONG, offset: offset, len: len, }; var message = new Paho.MQTT.Message(JSON.stringify(data)); message.destinationName = this.topic_cmd; this.mqtt_client.send(message); } calcurateBalanceBoard(data, base){ var result = { topright: this.calc_balance(data.topright, base.topright ), bottomright: this.calc_balance(data.bottomright, base.bottomright ), topleft: this.calc_balance(data.topleft, base.topleft ), bottomleft: this.calc_balance(data.bottomleft, base.bottomleft ), }; result.total_weight = (result.topright + result.bottomright + result.topleft + result.bottomleft); return result; } calc_balance(value, base){ if( value <= base[0] ){ return 0.0; }else if( value > base[0] && value <= base[1] ){ return ((value - base[0]) / (base[1] - base[0])) * 17.0; }else if( value > base[0] && value <= base[1] ){ return (((value - base[1]) / (base[2] - base[1])) * (34.0 - 17.0)) + 17.0; }else{ return 34.0; } } parseBalanceBoardCalibration(data){ var result = { topright: [(data[0x04] << 8) | data[0x05], (data[0x0c] << 8) | data[0x0d], (data[0x14] << 8) | data[0x15]], bottomright: [(data[0x06] << 8) | data[0x07], (data[0x0e] << 8) | data[0x0f], (data[0x16] << 8) | data[0x17]], topleft: [(data[0x08] << 8) | data[0x09], (data[0x10] << 8) | data[0x11], (data[0x18] << 8) | data[0x19]], bottomleft: [(data[0x0a] << 8) | data[0x0b], (data[0x12] << 8) | data[0x13], (data[0x1a] << 8) | data[0x1b]], }; return result; } parseExtension(type, data) { if (type == WIIREMOTE_EXT_TYPE_NUNCHUCK) { var value = { stk_x: data[0], stk_y: data[1], acc_x: (data[2] << 2) | ((data[5] >> 2) & 0x03), acc_y: (data[3] << 2) | ((data[5] >> 4) & 0x03), acc_z: (data[4] << 2) | ((data[5] >> 6) & 0x03), btns: (~data[5] & 0x03), } return value; } else if (type == WIIREMOTE_EXT_TYPE_BALANCEBOARD) { var value = { topright: (data[0] << 8) | data[1], bottomright: (data[2] << 8) | data[3], topleft: (data[4] << 8) | data[5], bottomleft: (data[6] << 8) | data[7], }; if (data.length >= 11) { value.temperature = data[8]; value.battery = data[10]; } return value; } } parseReporting(data) { if (data[0] == WIIREMOTE_REPORTID_STATUS) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, leds: data[3] & 0xf0, flags: data[3] & 0x0f, battery: data[6] }; return report; } else if (data[0] == WIIREMOTE_REPORTID_READ_DATA) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, size: (data[3] >> 4) & 0x0f + 1, error: data[3] & 0x0f, address: (data[4] << 8) | data[5], data: data.slice(6) }; return report; } else if (data[0] == WIIREMOTE_REPORTID_ACK) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, report: data[3], error: data[4] }; } else if (data[0] == WIIREMOTE_REPORTID_BTNS) { var report = { report_id: data[0], btns: ((data[1] << 8) | data[2]), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03), acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01), acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT8) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, extension: data.slice(3) }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR12) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03), acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01), acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01), ir: data.slice(6), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT19) { var report = { report_id: data[0], btns: ((data[1] << 8) | data[2]), extension: data.slice(3), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_EXT16) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03), acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01), acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01), extension: data.slice(6), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_IR10_EXT9) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, ir: data.slice(3, 3 + 10), extension: data.slice(3 + 10), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6) { var report = { report_id: data[0], btns: (((data[1] << 8) | data[2])) & 0x1f9f, acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03), acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01), acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01), ir: data.slice(6, 6 + 10), extension: data.slice(6 + 10), }; return report; } else if (data[0] == WIIREMOTE_REPORTID_EXT21) { var report = { report_id: data[0], extension: data }; return report; } else if (data[0] == 0x3e || data[0] == 0x0f) { throw "not supported"; } else { throw "unknown"; } } addr2bin(address){ return bytes_swap(hexs2bytes(address, ':')); } addr2str(address){ return bytes2hexs(bytes_swap(address), ':'); } } function hexs2bytes(hexs, sep) { hexs = hexs.trim(hexs); if( sep == '' ){ hexs = hexs.replace(/ /g, ""); var array = []; for( var i = 0 ; i < hexs.length / 2 ; i++) array[i] = parseInt(hexs.substr(i * 2, 2), 16); return array; }else{ return hexs.split(sep).map(function(h) { return parseInt(h, 16) }); } } function bytes2hexs(bytes, sep) { var hexs = ''; for( var i = 0 ; i < bytes.length ; i++ ){ if( i != 0 ) hexs += sep; var s = bytes[i].toString(16); hexs += ((bytes[i]) < 0x10) ? '0'+s : s; } return hexs; } function bytes_swap(bytes){ for( var i = 0 ; i < bytes.length / 2 ; i++ ){ var t = bytes[i]; bytes[i] = bytes[bytes.length - 1 - i]; bytes[bytes.length - 1 - i] = t; } return bytes; }使い方
まずは、BluetoothでWiiリモコンたちと接続するサーバを立ち上げます。
前回の投稿で示しました。$ node index.js server testwii_cmd testwii_evt MQTT_CLIENT_ID: server MQTT_TOPIC_CMD: testwii_cmd MQTT_TOPIC_EVT: testwii_evt mqtt.connected. mqtt.subscribed.これで、testwii_cmdとtestwii_evtというトピック名でMQTTブローカに接続できました。
それでは、まずブラウザから表示させます。
この状態では、まだWiiにもMQTTにも接続されていない状態です。
mqtt_brokerのところに、MQTTブローカのURLを指定してください。Websocket接続のポート番号を指定する必要があります。
topic_cmdとtopic_evtには、サーバが接続したトピック名を指定します。さきのどは、testwii_cmdとtestwii_evtを指定しました。それでは、MQTT Connectボタンを押下します。
これで接続はできました。
ですが、まだWiiリモコンは接続していません。
Bluetooth Addressに接続したいWiiリモコンのBluetooth Addressを入力します。
そして、Wiiリモコンの①と②のボタンを両方押して、Discoveryモードにして、Wii Connectボタンを押下します。そうすると、サーバ側の方で、以下のように表示されて、Wiiリモコンとの接続が完了します。ブラウザからはちょっとわかりにくいので、改善の余地ありですね。。。
on.message topic: testwii_cmd message: {"cmd":1,"address":[XX,XX,XX,XX,XX,XX],"retry":2} Uint8Array [ XX, XX, XX, XX, XX, XX ] connect called s_11 connect result=0 s_13 connect result=0 startContinuousRead calledこの状態で、Wiiリモコンの各ボタンを押してみてください。ブラウザに押したボタンの色が濃くなるのがわかると思います。また、rumbleのチェックボックスをOnにするとWiiリモコンが振動し、LEDの1~4のチェックボックスをOnにするとLEDが点灯するのがわかるかと思います。
この状態は、レポーティングモードがBTNSですので、ボタンの状態しかPublishされてきません。そこで、report_idのところをBTNS_ACCに変更してsetボタンを押してみてください。すると、上側のグラフ(acc_xやacc_y、acc_z)にグラフが表示されます。これが、Wiiリモコンの加速度の状態です。Wiiリモコンを動かしてみるとわかります。
次に、ヌンチャクを動かしてみましょう。
まず、ヌンチャクをWiiリモコンに接続します。そうすると、extension_connectedがtrueに切り替わります。
ヌンチャクの情報の取得には、EXTが付いたreport_idを選択します。BTNS_ACC_EXT16を選択してsetボタンを押下します。グラフが表示されだしましたが、まだ値に動きがありません。それは拡張コントローラを有効にしていないためです。そこで、Enable Extensionボタンを押下すると、動き出します。
ヌンチャクを動かしたり、ヌンチャクのスティックを動かしたり、C/Zボタンを押したりしてみてください。次に、バランスボードを接続してみます。
さきほどのWiiリモコンを接続するサーバは、すでにWiiリモコンを接続済みですので、もう一つサーバを立ち上げます。MQTTの名前が被らないようにします。testwii_cmd2、testwii_evt2にしてみました。$ node index.js server2 testwii_cmd2 testwii_evt2 MQTT_CLIENT_ID: server2 MQTT_TOPIC_CMD: testwii_cmd2 MQTT_TOPIC_EVT: testwii_evt2 mqtt.connected. mqtt.subscribed.では、ブラウザ側も別のブラウザまたは別タブを立ち上げて表示させます。
今度は、topic_cmdとtopic_evtに先ほど指定したtestwii_cmd2とtestwii_evt2を指定し、client_idに他とかぶらない値を指定して、MQTT Connectボタンを押下します。
そして、Bluetooth Addressには、バランスボードのBluetooth Addressを指定し、wii_typeにはバランスボードを選択しておきます。それでは、バランスボードの裏面の電池蓋を開けて、Syncボタンを押してから、Wii Connectボタンを押下しましょう。
接続が完了すると、バランスボードのPowerボタンを押すとボタンの色が変わりますし、LED 4のチェックボックスをOnにすると、PowerボタンのLEDが点灯したりします。次に、report_idをBTNS_EXT19を選択してsetボタンを押下します。
ですが、まだ重量のグラフは表示されません。それはキャリブレーションデータを受信していないためです。Update Calibrationボタンを押下してみてください。
さあ、バランスボードに乗ってみてください。以上
- 投稿日:2020-08-11T15:23:06+09:00
【Vuex】Stateを丸ごと置き換える
- 投稿日:2020-08-11T14:11:58+09:00
【Vue.js】Vue CLIで共通テンプレートを作成する方法
【Vue.js】Vue CLIで共通テンプレートを作成する方法
Vue CLIで作成したページに共通ヘッダーや共通フッターを作成する方法。
作成手順
- componentsフォルダ配下に単一コンポーネントファイルを作成
- app.vueファイルで作成したコンポーネントを読み込み
- export defualtでモジュール化
- templateタグ内で呼び出し
以下、共通フッター(CommonFooter)コンポーネントを作成する場合で考える。
1. componentsフォルダ配下に単一コンポーネントファイルを作成
作成するコンポーネントは、componentsフォルダ内に作成し、各ページで呼び出す。
<コンポーネント作成時の注意点>
- ファイル名は、大文字始まり、キャメルケースとする。
- 例: CommonFooter.vue
- ファイル内は、template, script, styleタグで構成する。
- 単一ファイルコンポーネントと呼ぶ
<補足:単一ファイルコンポーネントについて>
文字通り、一つのファイルからなるパーツのことをいう。従来のWEBページはhtml, css, jsファイルから成るが、Vue CLIの思想では、各パーツ(コンポーネント)毎にhtml, css, jsを管理する。
html部はtemplateタグで囲み、cssはstyleタグ、jsはscriptタグで囲む。
2. app.vueファイルで作成したコンポーネントを読み込み
全てのページで共通となる要素は「app.vue」ファイルに記載する。
まず初めに、作成したコンポーネント(CommonFooter)をapp.vueにインポートする。
scriptタグ内に以下を記述。
scriptタグ内(app.vue)import CommonFooter from '@/components/CommonFooter'
export defualtでモジュール化
コンポーネントファイルを呼び出しただけでは使えない。templateタグ内で呼び出せるように、モジュール化する必要がある。
scriptタグ内のexports defaultで、componentsオプションを使用しインポートしたテンプレートを定義する。
scriptタグ内(app.vue)import CommonFooter from '@/components/CommonFooter' export default { components:{ CommonFooter } }<export defaultsについて>
単一ファイルコンポーネントを使うためには、export defaultで囲む必要がある。これをすることで、templateタグなど外部でそのコンポーネントが利用できるようになる。
呼び出し時にの名称としてテンプレート名を設定する。
4. templateタグ内で呼び出し
これまでで、作成したコンポーネントをtemplate内で呼び出し、画面に反映する準備が整った。
コンポーネントは呼び出したい箇所に
<コンポーネント名/>
を記述することで呼び出せる。<コンポーネント名/>は<コンポーネント名></コンポーネント名>の省略形。どちらでも正常に動く。
templateタグ内(app.vue)<template> <CommonFooter/> </template>以上。
- 投稿日:2020-08-11T13:54:28+09:00
Gridsome入門 SPAを作ってみよう 【4日目 棒グラフ編】
スケジュール
前提条件
- node.js v8.3以上
- yarn or npmが入っている(Document見るとyarnの方が推奨とのこと)
- Gridsomeのプロジェクトを作成している
前回同様vue-chart.jsでグラフを作成
その前に前回作ったcomponents名を変更
前回
src/components/Chart.vue
というコンポーネントを作りましたが、今回グラフのコンポーネントをもう一つ作るので名前を変更していきましょう。
変更前src/components/Chart.vue
変更後src/components/chart/Doughnut.vue
適宜このコンポーネントを読み込んでいたpageなども変更が必要です。新たに棒グラフ用のコンポーネントを作る
棒グラフ用は
import { Bar } from 'vue-chartjs'
で使えるようになります。
棒グラフを使うときの注意点はoptionsのscalesを指定しない場合、datasetsで渡した最小値が棒グラフのメモリの最小値になってしまうので見ずらかったです。
実際の作成したコンポーネントはこちらsrc/components/chart/Bar.vue<script> import { Bar } from 'vue-chartjs' export default { extends: Bar, name: 'bar', props: ['labels','chartData'], data () { return { chartLabel: 'Asset transition', options: { tooltips: { callbacks: { label: function(tooltipItem, data) { const currentData = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; return '$' + currentData; }, } }, scales: { yAxes: [ { ticks: { min: 0, // グラフのメモリ最小値 max: this.upperData() // グラフのメモリ最大値(渡したdatasetsの最大*1.2にしている) } } ] } } } }, mounted () { this.renderChart( { labels: this.labels, datasets: [ { backgroundColor: 'rgba(137,255,255,1)', data: this.chartData, label: this.chartLabel, } ] }, this.options) }, methods: { upperData: function () { const maxData = Math.max.apply(null, this.chartData) * 1.2; return parseInt(maxData, 10); } } } </script>また今回はpropsを使って親から子componentsに値を渡しています。
props: ['labels','chartData'],
ここで指定。親
src/pages/asset-transition/index.vue<template> <Layout> <h1>Asset transition</h1> // labels,chartDataというkeyで渡す <bar :labels="labels" :chartData="chartData"></bar> </Layout> </template> <script> import Bar from "../../components/chart/Bar"; export default { components: { Bar }, data() { return { labels: ['202001', '202002', '202003', '202004', '202005','202006','202007', '202008'], chartData: [10000, 11000, 11500, 11300, 12100, 12500, 20000, 19000], } }, metaInfo: { title: 'Hello, world!' } } </script> <style> .home-links a { margin-right: 1rem; } </style>出来上がった棒グラフはこちら
あとがき
昨日は折れ線グラフにしようかなあと思っていましたが、やっぱ棒グラフに変更。今回使うグラフこれで以上かな。たぶん。
明日はCSVからデータを読み込む処理に入る予定。
今日は暑いからアイスが美味しい。セブンのチョコミント氷最高。
- 投稿日:2020-08-11T12:13:42+09:00
Vue.jsでBootStrapをつまみ食い的に使う
概要
Vue.jsのVue CLIからBootStrapを使う方法。
「BootStrapを使おう!さぁ先ずは基本を学ぼう」と構えて臨むのではなく、「このデータ構造の表現にちょうどよいUI無いかな? お、BootStrapのxxのコンポーネントが良さ気じゃん」と気軽につまみ食いで使う、ことを目指すとする。
サンプルに用いるデータ構造
次のような配列データを、Vue.jsを用いたUIで表現する場合を考える。
表示対象は「datetime, type, notes
」の3つとする。activitylist = [ { id: 5, datetime: "1596229200", type: 1, notes: '翌日の6時に起きたとする' },{ id: 4, datetime: "1596223800", type: 1, notes: '翌日の4時半に目が覚めたとする' },{ id: 3, datetime: "1596201000", type: 0, notes: '2020-07-31 22:10、つまり夜22時過ぎに寝た場合を仮定' },{ id: 2, datetime: "1596164400", type: 2, notes: '薬を昼12時に飲んだとする。' } ];
type
に指定された値に対して、その値を配列番号と見なして、それぞれ配列要素のtitleキーに設定した文字列に置き換えて表示する、ものとする。typelist = [ { title: '寝た' }, { title: '起きた' }, { title: '服薬' } ];これらの配列データをViewListCard.vueで定義し、
コンポーネントItemCard.vueに配列をPropsで渡して、
表示の仕方はコンポーネントItemCard.vueに任せる、
という設計を仮定する。具体的なサンプルコードは次のようになる。
https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap
./src/components/ViewListCard.vue
./src/components/ItemCard.vue
ItemCard0.vue
~ItemCard3.vue
がそれぞれの段階ごとのサンプルコードテキストをマスタッシュ構文でそのまま表示する
上述の配列データactivitylistの各要素の3項目を、次の変換のみを行って
マスタッシュ(Mustache)を用いてテキストで表示すると
次のようになる。
- datetime: UNIX時間(秒)を「YYYY-MM-DD . HH:MM:00」の文字列に変換する
type
を、typelist
の配列番号に応じた要素のtitle
キーに設定された文字列に変換するnotes
はそのまま表示する。本サンプルでは、それぞれのカード(様の部分)をタップすると編集モードになるる、という設計とする。その編集モードは、先ずはHTML標準の
input
タグを用いて実装すれば、次のような表示となる。ここまでのサンプルコードは次のようになる。
※ItemCard0.vue
を、実際にはItemCard.vue
として動作させる。以下、上述までの表示形式を、BootStrapを用いていい感じにする方法を述べる。
BootStrap(BootStrapVue)を使う準備をする
Vue.js上でBootStrapを利用するには、BootStrapVueを用いるのが簡単。
BootstrapVue provides one of the most comprehensive
implementations of Bootstrap v4 for Vue.js.Vue CLIのプロジェクトのルートフォルダにて、以下のコマンドでインストールする。
npm install bootstrap-vue --save続いて、ルートにある
main.js
を開いて、次の2行(本サンプルではコメント含めて4行)を追加する。main.jsimport Vue from 'vue' import App from './App.vue' // +++ add for bootstrap +++ import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' // ------------------------- Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app')以上で、BootStrapをVue CLI上で使う準備は完了。
ref. https://bootstrap-vue.org/docs
項目のタイトル相当を見やすくしたい
先ほどの「card0.png」の画面を見やすくすることを考える。
「起きた」や「寝た」などの表示を上手く装飾するものがないか?
と公式BootStrapVueのComponentのページを見ていく。https://bootstrap-vue.org/docs/components
このページは、簡単な説明を一覧出来て、それぞれのComponentの説明ページに飛ぶとサンプル表示もあるので、「そのComponentによる装飾がどういうものか?」を掴みやすくて、助かる。
どんなコンポーネントがあるか?を上から順に見ていく中で、
今回のケースなら「バッジ(Badge)」で「起きた」「寝た」などを表示するのがよさそうだ、などのように装飾の仕方を決める。本サンプルでは、上述のBadgeによる装飾とnotesの値をReadOnlyの
Form Textarea
で表示するものとする。
この場合は、次のような表示になる。上記を実装するには、コンポーネントItemCard.vueに対して、次のような変更を加える。
{{typeStr}}
としていた部分を、<b-badge v-bind:variant="typeVariant">{{typeStr}}</b-badge>
とする
{{notesCurrent}}
としていた部分を、<b-form-textarea v-bind:value="notesCurrent" readonly rows="2" max-rows="2"></b-form-textarea>
とする- 利用するコンポーネントを「
import { BBadge, BFormTextarea } from 'bootstrap-vue'
」で読み込んで、「components: {}
」に指定するたったこれだけのコード修正で、で上図(card1.png)の様な見やすい表示に変更できる。BootStrapはとても簡単で使いやすい。
なお、編集モード(card0-edit.png)の表示については、
notesCurrent
の部分は、Form Textarea
を用いてreadonly
属性を外せばよいだろう。編集モード側のUI変更を含めたサンプルコードは以下。https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard1.vue
編集モード側の表示は以下のようになる。
なお、
<b-badge>
コンポーネントはvariant
属性でカラーリングを変更できる。サンプルコードでは、typelist
配列の各要素に、variant
キーを追加し、それに従ったBadgeカラーを表示する実装にしてある。[ { title: '寝た', variant: 'primary' }, { title: '起きた', variant: 'secondary' }, { title: '服薬', variant: 'success' } ]
variant
属性への指定において、デフォルトで利用可能な値は以下を参照。https://bootstrap-vue.org/docs/components/badge#contextual-variations
編集モードで選択項目をラジオボタンで、ついでに確定ボタンもいい感じに装飾する
続いて、上図の編集モード(card0-edit.png)における「起きた」「ネタ」をラジオボタンで選べるようにする。HTML標準の
<input type="radio">
でも良いのだが、BootStrapにForm Radio
コンポーネントがあるので、これを使う。次の公式ガイドに従って、
<b-form-group>
と<b-form-radio-group>
を用いる。https://bootstrap-vue.org/docs/components/form-radio#grouped-radios
編集対象は(propsで渡された
type
をもとに生成した)typeCurrent
なので、これをv-model
属性で<b-form-radio-group>
にバインドする。<b-form-group label="記録の種別を選んでください"> <b-form-radio-group v-model="typeCurrent" :options="typeOptions" ></b-form-radio-group> </b-form-group>選択肢は、
v-bind:options
属性(略記して:options
属性)で設定する。設定すべき変数のフォーマットは配列で、各要素は次の2つのキーを持つ。
- 選択肢の文字列として
text
- 選択されたときに編集対象(=
v-model
でバインドされた変数)へ代入する値としてvalue
したがって、本サンプルでは(propsで渡された)
typelist
を元にして次のようにtypeOptins
配列を生成しておく。this.typelist.forEach((elem, index)=> { this.typeOptions.push({ text: elem.title, value: String(index) }) })ついでなので、「確定」ボタンもBootStrapVueが提供する
Button
コンポーネントで装飾する。これは、「<button @click="clickBtnEditFinish">確定</button>
」としていたところを、「<b-button @click="clickBtnEditFinish">確定</b-button>
」と置き換えるだけで良い。以上の変更を加えたコンポーネントItemCard.vueのコード全体は以下となる(※
import
とcomponents
への追加も忘れずに→リンク先のコードを参照)。https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard2.vue
上記のコードへの変更によって、編集モードの表示は次のように変わる。
日時の編集ボックスを、いい感じに装飾する
最後に、上図(card2-edit.png)の日付と時刻の入力を良い感じに装飾する。
(※HTML5利用可能環境であれば、素のHTML inputタグが実装している入力支援のpickerを利用可能なので、BootStrap版に置き換えるか否かは好みの問題かもしれない。一応、IEとPC版SafariはHTML5に未対応のため同じ表示にならないが、BootStrap版なら同じ表示が可能、という差はある)
これまでと同様にBootStrapVueのコンポーネント一覧から、日付と時刻のPickerを探す。
https://bootstrap-vue.org/docs/components
Form Datepicker
とForm Timepicker
があるので、これを使う。https://bootstrap-vue.org/docs/components/form-datepicker
https://bootstrap-vue.org/docs/components/form-timepicker
使い方は、それぞれを次のように置き換えるだけ。
置き換え前:
<input v-model="dateCurrent" type="date">
<input v-model="timeCurrent" type="time">
置き換え後:
<b-form-datepicker v-model="dateCurrent" class="mb-2"></b-form-datepicker>
<b-form-timepicker v-model="timeCurrent" locale="ja"></b-form-timepicker>
置き換え後のサンプルコードは次のようになる。
https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard3.vue
※ここで「
class=mb-2
」を指定しているが、これはBootStrap v4.5で定義されているclassのこと。BootStrapVueでは、BootStrapで準備されているClassをそのまま利用できる。
- https://bootstrap-vue.org/docs#documentation-information
- https://bootstrap-vue.org/docs/reference/utility-classes
mb-2
はmargin-bottom
のことで、'-2'は0.5rem
を意味する。 https://getbootstrap.com/docs/4.5/layout/utilities-for-layout/#margin-and-padding上記のコードへの変更によって、編集モードの表示は次のように変わる。
なお、「Picker経由だけでなく、時刻を直にテキストとして入力もしたい」という場合は、inputタグを組わせることで実現できる。BootStrapVueでの、その実装例も公式サイトの以下に記載がある。とても親切♪
https://bootstrap-vue.org/docs/components/form-timepicker#button-only-mode
以上ー。
- 投稿日:2020-08-11T02:30:23+09:00
【Django REST Framework + Vue.js】APIからデータを取得し表示する
はじめに
ブログや投稿系のアプリを作成するとを考え、Django REST FrameworkとVue.jsを使って、投稿一覧データを取得し画面に表示させるまでの流れを扱いたいと思います。
大まかな流れは以下の通りです。
1. Django REST Frameworkで投稿一覧APIを作成する。
2. フロントのVue.jsからaxiosによってAPIからデータを取得し画面に表示する。1. Django REST Frameworkの記述
まず、Django REST Frameworkをインストールしsettings.pyのINSTALLED_APPSにrest_frameworkの追加をしてください。
Djangoの設定ディレクトリ名を「config」, アプリケーションディレクトリ名を「apiv1」としています。models.pyの設定
今回はCategoryモデルとPostモデルと作成しました。
apiv1/models.pyfrom django.db import models from django.contrib.auth import get_user_model class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) def __str__(self): return self.name class Post(models.Model): category = models.ForeignKey(to=Category, on_delete=models.CASCADE) author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) title = models.CharField(max_length=100) content = models.TextField() published_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-published_at'] def __str__(self): return self.titleserializers.pyの設定
apiv1/serializers.pyfrom rest_framework import serializers from django.contrib.auth import get_user_model from .models import Category, Post class UserSerializer(serializers.ModelSerializer): """ユーザーシリアライザー""" class Meta: model = get_user_model() fields = '__all__' class CategorySerializer(serializers.ModelSerializer): """カテゴリシリアライザ""" class Meta: model = Category fields = ('id', 'name') class PostSerializer(serializers.ModelSerializer): """投稿シリアライザ""" class Meta: model = Post fields = '__all__'views.pyの設定
Viewの指定方法はいくつか種類がありますが、今回は自分が一番わかりやすいと感じたgeneric viewを使いたいと思います。
generic viewは以下のコードにあるようなListAPIView(一覧取得)の他に、CreateAPIView(作成)やRetrieveAPIView(詳細取得)、UpdateAPIView(更新)などがあります。他にもたくさん種類があり、作成, 取得, 削除, 更新によって使い分けることができます。ListCreateAPIViewという一覧取得と作成を行ってくれるものもあります。apiv1/views.pyfrom rest_framework import generics from django.contrib.auth import get_user_model from .models import Post, Category from .serializers import CategorySerializer, PostSerializer class CategoryListAPIView(generics.ListAPIView): """カテゴリモデルの取得(一覧)APIクラス""" queryset = Category.objects.all() serializer_class = CategorySerializer class PostListAPIView(generics.ListAPIView): """投稿モデルの取得(一覧)APIクラス""" queryset = Post.objects.all() serializer_class = PostSerializerurls.pyの設定
config/urls.py(プロジェクトのurls.py)from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/v1/', include('apiv1.urls')), ]apiv1/urls.pyfrom django.urls import path from . import views app_name = 'apiv1' urlpatterns = [ path('categories/', views.CategoryListAPIView.as_view()), path('posts/', views.PostListAPIView.as_view()), ]この時点でスーパーユーザーを作成した後admin管理画面からカテゴリおよび投稿をいくつか作成し、 http://127.0.0.1:8000/api/v1/categories/, http://127.0.0.1:8000/api/v1/posts/ にアクセスするとそれぞれ以下のような表示になると思います。
http://127.0.0.1:8000/api/v1/categories/
http://127.0.0.1:8000/api/v1/posts/
これらのデータをVue.js側で呼び出し、表示させたいと思います。
2. Vue.jsの記述
まず最初にaxios, vue-axiosのインストールをしてmain.jsに設定を記述します。
src/main.jsimport Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import axios from 'axios' //追記 import VueAxios from 'vue-axios' //追記 Vue.config.productionTip = false Vue.use(VueAxios, axios) //追記 new Vue({ router, store, render: h => h(App) }).$mount('#app')次にVueRouterを使ってパスを指定します。
今回は http://127.0.0.1:8080/postlist と指定されたら、PostList.vueが表示されるように設定しました。src/router/index.jsimport Vue from 'vue' import VueRouter from 'vue-router' import PostList from '@/components/PostList' Vue.use(VueRouter) const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/postlist', component: PostList }, ] })PostList.vueファイルに記述していきます。
src/components/PostList.vue<template> <div> <div v-for="(post, key) in Posts" :key="key"> <hr> <p>カテゴリ: {{ post.category }}</p> <p>タイトル: {{ post.title }}</p> <p>内容: {{ post.content }}</p> <p>投稿日: {{ post.published_at}}</p> <hr> </div> </div> </template> <script> export default { data() { return { Posts: [] }; }, mounted() { this.axios .get("http://127.0.0.1:8000/api/v1/posts/") .then(response => (this.Posts = response.data)); } }; </script>http://127.0.0.1:8080/postlist にアクセスすると次の様に表示できました。
今の段階ではカテゴリが数字(id)で表現されています。今回は文字を表示して欲しいので、Djangoのserializers.pyに追記していきます。
PostSerializerにcategory = CategorySerializer()
を追記してカテゴリ名が取得できるようにします。apiv1/serializers.pyfrom rest_framework import serializers from django.contrib.auth import get_user_model from .models import Category, Post class UserSerializer(serializers.ModelSerializer): """ユーザーシリアライザー""" class Meta: model = get_user_model() fields = '__all__' class CategorySerializer(serializers.ModelSerializer): """カテゴリシリアライザ""" class Meta: model = Category fields = ('id', 'name') class PostSerializer(serializers.ModelSerializer): """投稿シリアライザ""" category = CategorySerializer() #追記 class Meta: model = Post fields = '__all__'そのあともう一度 http://127.0.0.1:8000/api/v1/posts/ にアクセスすると、以下のようになると思います。
CategoryとPostが1対多の関係でありForeignKeyで結び付けられているためcategoryの正体を見ることができました。
最後にPostList.vueの<p>カテゴリ: {{ post.category }}</p>
の部分を<p>カテゴリ: {{ post.category.name }}</p>
と変更し http://127.0.0.1:8080/postlist にアクセスすると、以下の様にカテゴリ名が正しく表示されるようになりました。
以上がDjangoRestFrameworkとVueを用いてAPIからデータを取得、表示する大まかな流れでした。