- 投稿日:2019-12-01T23:11:57+09:00
Vue.jsでローカルからcsvを持ってくるやつ(not File API)
まえおき
一か月でVue使って何か作る企画のはずでしたが、なんか忙しそうなので記事の方は色々やってお茶を濁します。
アプリはちゃんと作ります。経緯
Vue.jsでローカル(サーバー)においてあるcsvを持ってこようとしたら
みんなFileAPIのことばっか説明しててイラついたので書きました。そういうの(いちいちGUIでcsvファイルを選ぶ)がやりたいんじゃないんだよ!!!
やる事
こういうのをローカルサーバーのディレクトリ直下において読み込みます
結論
axios使っとけ
やり方
こうして
yarn add axios
こう
import Vue from "vue"; import axios from "axios"; new Vue({ el: '#app', data () { return { csv: null } }, mounted () { let source = "./test.csv" //読みたいcsvのパス axios .get(source) .then(res =>{ // csvファイルの内容を取得 let text = res.data; //csvファイルをバラす(やりたい事によって違うので参考程度に) let csvData = [] let splited = text.split("\n"); splited.forEach(row =>{ csvData.push(row.split(",")) }); //作ったデータを入れる this.csv = csvData; }); } })別にVueじゃないねこれ
ところで
Q. なんでXMLHttpRequest使わなかったの?
A. そう言うのもあるんだ
- 投稿日:2019-12-01T23:04:44+09:00
ラズパイを映像展示用デバイスにした話
はじめに
FESTA 2019に参加させて頂くことになったので、デモ映像を流す映像展示用デバイスを作りました。
やったこと
- ラズパイ上にVue.js環境を構築
- Vue.jsで動画再生サイトを作成(複数動画を順次再生
- 作成したサイトをオフラインで実行できるように修正
参考サイト
- https://jsfiddle.net/6qmjw5xd/2/
- vue.jsで動画を連続再生してみるネッサンス
- 動画再生サイトのソースはほぼそのまま拝借しました
- How to Install Node.js 12 on Ubuntu 19.04
- Vue.js を vue-cli を使ってシンプルにはじめてみる
- Ubuntu で日本語キーボードレイアウト
- 本筋から外れますが助かりました。
できたもの
ラズパイ接続のモニタに映像が表示されました。会場の通信障害が怖いのでオフラインでも動作するようにしました。
FESTA 2019用に映像展示デバイスを ラズパイ と Vue.js で作った。デザインは後まわし #MA_FESTA #ウソ穴 間に合うのだろうか。。 pic.twitter.com/ybhIhZI5q8
— j4amountain (@zsipparu) December 1, 2019
ラズパイについて
Raspberry Pi 3 Model B+
です。OSはこちら$ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster
では、ここから作り方を紹介します。
Nodejsをインストールしよう
npmのバージョンを最新版に更新します。
sudo npm install npm@latest -gバージョン確認
$ npm -v 5.8.0nodejs v12をインストールします。(※以前のバージョンがあると削除してからインストールしないとバージョンが上がらないそうです)
$ sudo apt update $ sudo apt install curl $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $ sudo apt install nodejs -y $ node -vバージョンはこのようになりました。
$ node -v v12.13.1vue-cliをインストールしよう
vue-cliをインストールします。
$ sudo npm install -g @vue/cliコマンド1つで終わりです。
プロジェクトを作成しよう
プロジェクトを作成します。vue create コマンド実行後に何か聞かれますが、今回はデフォルト設定 (何も入力せずEnter)で進めます。
$ vue create usoanaこれでプロジェクト作成が完了しました。
起動してみよう
起動します。
npm run serveこのような画面になると起動成功です。
このようなウェブサイトが開けば成功です。
動画再生サイト作ろう
サイトが作れたので、動作再生のサイトを作ります。
作成したプロジェクト配下にpublic
というフォルダがあるので、そのpublic
フォルダは以下にindex.htmlを作成します。(中身は以下です)~/data/usoana/public/index.html<!DOCTYPE html> <html> <head> <title>ウソ穴-覗きたい!を叶えたい!#ウソ穴</title> <script src="https://unpkg.com/vue"></script> </script> </head> <body> <h1>usoana</h> <div id="app"> <video id="video1" v-bind:src="video" v-on:ended="onEnded" autoplay></video> <a href="javascript:vplay();">再生</a> </div> <script> var v = new Vue({ el: "#app", data: { items: [ {"url": "xxx1.mp4"}, {"url": "xxx2.mp4"}, {"url": "xxx3.mp4"} ], next: 0, video: "" }, created: function () { this.getItems(); }, methods: { onEnded: function () { this.getItems(); }, getItems: function () { var url = this.items[this.next].url; console.log('success:' + new Date()); this.video = url; this.next++; if (this.items.length <= this.next) { this.next = 0; } } } }); function vplay() { var v = document.getElementById("video1"); v.play(); } </script> </body> </html>ブラウザで、
http://<ラズパイIP>:8080/index.html
を開き、動画が再生されれば成功です。インターネット上で公開されている動画のURLを指定しても再生できました。オフライン対応しよう
作成した動画サイトをオフラインでも動作できるように修正します。展示会場では通信障害が発生しても動画は再生できる、という算段です。
vue.jsをダウンロードします。バージョンによってURLが変わると思いますので気を付けてください。(
https://unpkg.com/vue
をブラウザで開くと、最新バージョンのURLが取得できると思います)# vue.jsダウンロード wget https://unpkg.com/vue@2.6.10/dist/vue.jsダウンロードした
vue.js
ファイルをindex.htmlと同じ階層~/data/usoana/public/
に格納しました。ダウンロードしたvue.js
ファイルをロードするようにindex.html
を修正します。~/data/usoana/public/index.html<!DOCTYPE html> <html> <head> <title>ウソ穴-覗きたい!を叶えたい!#ウソ穴</title> <!-- script src="https://unpkg.com/vue"></script --> <script src="vue.js"></script> </head> <body> <h1>usoana</h> <div id="app"> <video id="video1" v-bind:src="video" v-on:ended="onEnded" autoplay></video> <a href="javascript:vplay();">再生</a> </div> <script> var v = new Vue({ el: "#app", data: { items: [ {"url": "xxx1.mp4"}, {"url": "xxx2.mp4"}, {"url": "xxx3.mp4"} ], next: 0, video: "" }, created: function () { this.getItems(); }, methods: { onEnded: function () { this.getItems(); }, getItems: function () { var url = this.items[this.next].url; console.log('success:' + new Date()); this.video = url; this.next++; if (this.items.length <= this.next) { this.next = 0; } } } }); function vplay() { var v = document.getElementById("video1"); v.play(); } </script> </body> </html>これでオフライン用の修正は完了です。
ラズパイのネットワーク通信を無効化しても、ラズパイ接続のモニタにてサイトが動作すれば成功です。余談
ラズパイについて、
Raspberry Pi 3 Model B+
を使ったのですが、Raspberry Pi 3 Model B
(プラスじゃない)を使う予定だったのを今気が付いた。基板を変更して作り直し。。手順をまとめておいてよかった。
- 投稿日:2019-12-01T21:52:42+09:00
AWS Cognitoの画面遷移しないサインインページを作る
久しぶりの、Cognitoネタです。
Cognitoのサインインは、OpenID Connectに準拠しており、サインインは認証エンドポイントにGETを呼び出して画面遷移する必要があります。SPAにおいては画面がリロードされることを意味し、保持していたセッション情報が破棄されることにつながります。そこで今回は、OpenID Connectでのログインとしつつ、画面リロードを伴わないサインインを実現します。
大まかな流れ
元のページのほかに、ログイン専用のページを作成し、元のページから、ログイン専用ページをロードします。
ログイン専用ページは、別タブや別ウィンドウやポップアップとして開き、ログイン専用ページでログインした結果を元のページに戻します。(元のページ)→(ログイン専用ページ)(別ウィンドウ) 認証エンドポイント呼び出し →(認証プロバイダのサインイン画面) サインイン (ログイン専用ページ)(画面リロード)← 認可コードを返却 (元のページ)←認可コードを返却(別ウィンドウをクローズ) トークンエンドポイントを呼び出して、トークンを取得後で説明しますが、認可コードを元のページに戻すとき、元のページがログイン専用ページと同じオリジンの場合と、異なるオリジンの場合で多少異なります。
ちなみに、Cognitoのもろもろについては以下を参考にしてください。
AWS CognitoにGoogleとYahooとLINEアカウントを連携させる作成したソースコードは以下のGitHubに挙げておきました。
https://github.com/poruruba/openid_server
※\public\proxy のところです。Cognitoでアプリクライアントを作成
まずは、AWS Cognitoから、アプリクライアントを作成しておきましょう。
今回はresponse_typeとしてAuthorization code grantを使おうと思いますので、クライアントシークレットを生成 はOnにしておきます。次に、作成したアプリクライアントで、Authorization code grantを許可しておきます。
コールバックURLには、これから作成するログイン専用ページのURLを指定しておきます。
また、許可されているOAuthフローには、Authorization code grantを選択しておきます。
許可されているOAuthスコープには、許可するスコープを指定します。とりあえず、email、openid、profileを選択しておきましょうか。元のページからログイン専用ページをロード
HTML上は特に必要な記載はなく、以下のJavascriptのみを付記しておきます。重要なところだけ抜粋します。
start.jsconst REDIRECT_URL = 'ログイン専用ページのUrL'; const CLIENT_ID = 'アプリクライアントID'; var new_win; start_login: function(){ var params = { origin : location.origin, // 同じオリジンの場合はコメントアウト state: this.state, client_id: CLIENT_ID, scope: 'openid profile' }; new_win = open(REDIRECT_URL + to_urlparam(params), null, 'width=400,height=750'); },REDIRECT_URLが、ログイン専用ページです。
もし、ログイン専用ページと元のページが同じオリジンの場合は、param.originの指定は不要です。
ログイン専用ページのウィンドウサイズは適当に変更してください。ちなみに、ウィンドウサイズはWindowsやMacOSの場合に有効で、AndroidやiOSの場合には、結局は画面いっぱいに表示されます。ログイン専用ページ
こちらも、Javascriptのみが重要です。
start_login.js'use strict'; const REDIRECT_URL = 'このページのURL'; const COGNITO_URL = 'https://[ドメイン名].auth.ap-northeast-1.amazoncognito.com'; //var vConsole = new VConsole(); var encoder = new TextEncoder('utf-8'); var decoder = new TextDecoder('utf-8'); var vue_options = { el: "#top", data: { progress_title: '', }, computed: { }, methods: { }, created: function(){ }, mounted: function(){ proc_load(); if( searchs.code ){ var state = JSON.parse(hex2str(searchs.state)); console.log(state); var message = { code : searchs.code, state: state.state }; if( state.origin ){ window.opener.postMessage(message, state.origin); }else{ window.opener.vue.do_token(message); } window.close(); }else{ var state = { origin: searchs.origin, state: searchs.state }; auth_location(searchs.client_id, searchs.scope, str2hex(JSON.stringify(state))); } } }; vue_add_methods(vue_options, methods_utils); var vue = new Vue( vue_options ); function auth_location(client_id, scope, state){ var params = { client_id: client_id, redirect_uri: REDIRECT_URL, response_type: 'code', state: state, scope: scope }; window.location = COGNITO_URL + "/login" + to_urlparam(params); } function str2hex(str){ return byteAry2hexStr(encoder.encode(str)); } function hex2str(hex){ return decoder.decode(new Uint8Array(hexStr2byteAry(hex))); } function hexStr2byteAry(hexs, sep = '') { hexs = hexs.trim(hexs); if( sep == '' ){ 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((h) => { return parseInt(h, 16); }); } } function byteAry2hexStr(bytes, sep = '', pref = '') { if( bytes instanceof ArrayBuffer ) bytes = new Uint8Array(bytes); if( bytes instanceof Uint8Array ) bytes = Array.from(bytes); return bytes.map((b) => { var s = b.toString(16); return pref + (b < 0x10 ? '0'+s : s); }) .join(sep); } function to_urlparam(qs){ var params = new URLSearchParams(); for( var key in qs ) params.set(key, qs[key] ); var param = params.toString(); if( param == '' ) return ''; else return '?' + param; }vue_utils.jsvar hashs = {}; var searchs = {}; function proc_load() { hashs = parse_url_vars(location.hash); searchs = parse_url_vars(location.search); } function parse_url_vars(param){ var searchParams = new URLSearchParams(param); var vars = {}; for (let p of searchParams) vars[p[0]] = p[1]; return vars; }REDIRECT_URLは、自身のログイン専用ページのURLです。
COGNITO_URLは、Cognitoのエンドポイントです。こんな感じにCognito User Poolに割り当てられているかと思います。
https://<ドメイン名>.auth.ap-northeast-1.amazoncognito.com大事なのは、
mounted: function(){のところです。Vueを使っています。
最初にログイン専用ページがロードされたときには、まだ認可コードがないため、auth_location()を呼び出す分岐に入ります。
stateという変数を作っていますが、これは、Cognitoのエンドポイント呼び出し後に自身がリロードされ、元のページから取得していた情報を忘れてしまうため、Cognitoのエンドポイント呼び出し時のstateパラメータに埋め込んでしまっています。要は手抜きです。
auth_location() は、まさにCognitoのログインエンドポイントの呼び出しです。
呼び出しパラメータ等については、以下が参考になります。mountedは、Vueの規定で、画面ロード後に自動的に呼び出されます。
したがって、すぐに、Cognitoのログイン画面が表示されたかと思います。例えばこんな感じです。実際には、Cognito User Poolに組み込んでいる認証プロバイダの種類によって見え方が異なります。
ログインが完了すると、REDIRECT_URLで指定したURL、すなわち、再度自身の画面がロードされます。
そのときに、認可コードが渡ってきていますので、今度は別の分岐に入ります。その中で以下の分岐があります。
if( state.origin ){
これは、ログイン専用ページと元のページが同じオリジンの処理とするか、異なるオリジンの処理とするかの分岐になります。
同じオリジンの場合には、元のページでparam.originをコメントアウトしていたかと思います。■同じオリジンの場合
元のページのJavascript関数do_token()を直接呼び出しています。
■異なるオリジンの場合
JavascriptのPostMessage機能を使っています。
(参考情報) Window.postMessage
https://developer.mozilla.org/ja/docs/Web/API/Window/postMessageいずれも、認可コードと元のページから取得したstateパラメータを戻しています。
上記処理後、自身のログイン専用ページをcloseしています。元のページに戻る
今一度元のページを見てみましょう。
同じオリジンの場合と異なるオリジンの場合で、ログイン専用ページからの戻りを取得する処理が異なります。■同じオリジンの場合
do_tokenがログイン専用ページから直接呼び出されています。
start.jsconst CLIENT_SECRET = 'アプリクライアントシークレット'; const COGNITO_URL = 'https://[ドメイン名].auth.ap-northeast-1.amazoncognito.com'; do_token: function(message){ if( this.state != message.state ){ alert('state is mismatch'); return; } var params = { grant_type: 'authorization_code', client_id: CLIENT_ID, redirect_uri: REDIRECT_URL, code: message.code }; var url = COGNITO_URL + '/oauth2/token'; return do_post_basic(url, params, CLIENT_ID, CLIENT_SECRET) .then(json =>{ console.log(json); this.token = json; }); }渡されたstateが、ログイン専用ページ呼び出し時の値と同じであることを確認します。
そして、同じく渡された認可コードを使って、Cognitoのトークンエンドポイントを呼び出して、トークンを取得します。※念のためですが、アプリクライアントシークレットをブラウザ上で処理してしまっています。漏洩しますので、本番ではサーバ側で秘匿に処理してください。
■異なるオリジンの場合
postMessageで送信された情報は、以下のところで取得されます。
start.jswindow.addEventListener("message", (event) =>{ console.log(event); if( event.origin != location.origin ){ alert('origin mismatch'); return; } vue.do_token(event.data); }, false);まずは、自身のオリジンと同じかどうかを確認します。param.originで指定したものとなっているはずです。
そして、同じオリジンの場合と同様、do_token()を呼び出します。こんな感じで、認証およびトークン取得が成功し、画面に表示されるはずです。
以上
- 投稿日:2019-12-01T21:44:11+09:00
Vue.js で chart.js & vue-chart.js を使ってみる。
背景
サーバーレスで自分用の家計簿的なwebサービスを勉強も兼ねて開発中。入力部分はだいぶ出来てきた。次は出力系。まずはグラフ表示したい。
調査
調べていくうちに以下ページを発見。vue-chartjs というものを使うのがよさそう。
vue-chartjsでグラフを描く
Vue.jsでカッコいいグラフを手軽に作るChart.jsのラッパー3つやってみた。
ライブラリインストール
vue-chartjs は、Chart.js をVueで便利に使うためのラッパーらしい。なので、最初に両方インストール必要があるらしい。
npm install vue-chartjs chart.js --saveサンプルを試す
公式ページは、色々とサンプルを紹介してくれているが、その種類が多いために、自分の目的ではどれを選ぶのがいいのか悩む。
データはAPIから取得予定なので、以下リンクのサンプルがマッチしそう。
APIから取得したデータを用いたチャート(公式ページ)真似してみるも、なんかブラウザで以下エラーが。templateが定義されてないとは言っているけど、vue-chart.jsの公式ページではvueファイルにtemplate含めるなとか書いてあるし。
[Vue warn]: Failed to mount component: template or render function not defined.次はもうちょっとシンプルと思われる以下リンクにあるサンプルを試す。こちらは自分で作成する必要があるクラスがjsファイルだし、うまく行くかも。
チャートデータの更新(公式ページ)うまく行った。うーむ、多分vueファイルのサンプルの方法でも基本うまく行くのだろうけど、何か足りないのだろう。何にせよ、同じ事が出来るならシンプルな方がいい。後者のサンプル基本で進める。以下、サンプル成功までの手順。
- LineChart.js という新ファイルをcomponentsフォルダ以下に作成。中身はまんまサンプルのコピー。(コンポーネント側で標準のものが用意できそうなんだけどな・・・)
- サンプルではRandomChart.vueというファイルで行っているが、こちらを自分のvueクラスに適用。中身ほぼコピー。
おまけ
棒グラフと折れ線グラフは共存可能っぽい。chartdata.datasets.type にline とか bar とか指定するとそのデータセットは指定されたグラフで表示される模様。
より高度な事がしたくなったら、Chart.js 公式ドキュメントページ を見てオプションとか探すのがよさそう。自分用途に修正していく
作りたい形式
- 横軸は日付。1週間程度のスパン。(将来は週集計、月集計も)
- 縦軸は合計金額。ただ、消費種別によって色分けしたい。
- 棒グラフでの積み上げグラフにしたい。
基本部分変更
- 自分は線グラフでなく、棒グラフを使いたかったので、ファイル名やソース内の、Line を Bar に変え、line-chart を bar-chart に変更。※ライブラリソースのここらへん でコンポーネントのタグ名を指定している模様。この情報からタグ名取得。
- options には対応したいので、templateのbar-chart に :options="options" 指定を追加。もちろん data 関数の返り値にも options を追加。
応用部分
- 積み上げグラフにするには、軸オプションで、stacked: true を設定する。参考公式ページ
- 種別により色を変えるには datasetの各凡例のデータに backgroundColor を設定する。参考公式ページ
きれいな色使う.
前述テストでは適当に色を作って試したが、本番ではきれいな色を使いたい。自作では限界があるし超面倒。そこで色々調べたら、以下ページを発見。参考にする。
google-paletteとchart.jsでかっこいいグラフを描く結果
こんな感じ。大分実用に耐えるものになってきました。今回はグラフ作る部分はChart.jsに任せて、自分は表示データ構築部分ロジックを組む事がメインになりました。ブラボー Chart.js です。
参考になるかもしれな自分の現時点ソース。
フロント側メイン部分Vueソース
AWS lambda側グラフデータ構築pythonソース参考にさせてもらったページ
Vue.jsでカッコいいグラフを手軽に作るChart.jsのラッパー3つ
vue-chartjsでグラフを描く
google-paletteとchart.jsでかっこいいグラフを描く
Chart.js 公式ドキュメントページ
- 投稿日:2019-12-01T20:31:55+09:00
kintone カスタマイズを Vue.js + TypeScript + Pug + SCSS でモダンに開発する (2) アプリ構築・設定編
目次
(1) 環境構築編
(2) アプリ構築・設定編(この記事)前置き
前回 の記事では、 kintone カスタマイズで
Vue.js
、TypeScript
、Pug
、SCSS
などのモダンな開発環境を利用しますよと言う趣旨で解説を始めましたが kintone ぜんぜん関係ないじゃんむしろただの Vue CLI で TypeScript プロジェクト作ります って話だったじゃんと言う展開でした。
今回はいよいよちゃんと 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
などがセットアップされています。今回のゴール
今回は、 kintone のアプリを作り、前回作ったプロジェクトでそのアプリのカスタマイズスクリプトを開発を進め、その成果を kintone カスタマイズに適用する繋ぎ込みの部分を中心に説明していきます。
- kintone アプリを作成する
- kintone 関連ライブラリをインストールする
- カスタマイズを kintone に適用する
- もう少し楽に開発を進める
(2) アプリ構築編
kintone アプリを作成する
既にある程度 kintone 利用経験がある方にとっては言わずもがなでしょうが、もし kintone 触った事ないですって方は事前に cybozu developer network で kintone 開発者ライセンスを取得しておいてください。
今回はカスタマイズの説明が目的なので、アプリの作成そのものは特に解説しません。
話を簡単にするため、 kintone 側で用意している雛形アプリの中から「案件管理」アプリを使いましょう。
アプリ作成の際、「サンプルデータを含める」のチェックを付けてください。
これでデータがある程度格納されている状態でアプリが新規作成されます。
作成されたアプリはフィールドコードが「
文字列__1行_
」など分かりづらい内容になっています。
カスタマイズではフィールド名よりもフィールドコードの方が重要なので、各フィールドのフィールドコードをフィールド名と合わせておくとこの先のカスタマイズが楽になります。
サイボウズさんこの辺までちゃんとしといてくれれば良いのにとは思いますが。
とりあえずこれで一旦アプリの準備は終了です。
kintone 関連ライブラリをインストールする
kintone カスタマイズを便利に進めるためのライブラリ類をインストールします。
前回 の記事で作成したプロジェクト kintone-vue-ts で作業します。
プロジェクトを VS Code で開いてください。kintone JS SDK
まずは kintone JS SDK をインストールします。
ターミナルで以下のようにします。% yarn add @kintone/kintone-js-sdk
kintone JS SDK
は、従来のカスタマイズでは冗長な記述になりがちだったレコードの全件取得や複数登録更新など、面倒な API 操作を良い感じに巻き取ってくれる SDK です。
ドキュメント も(英語ですが)平易で体系的にまとまっていますので、着実な工数削減が見込まれます。kintone dts-gen
TypeScript
では d.ts ファイル(型定義ファイル)のあるなしが開発効率に大きく影響します。
kintone dts-gen はこの型定義ファイルの作成を支援してくれるツールで、よほどカスタマイズの規模が小さくない限りはTypeScript
で kintone 開発を行うなら必須と言えるものです。
kintone 公式ドキュメントではこちらで解説されています。@kintone/dts-genはkintoneのJavaScriptカスタマイズ用の関数定義に加えて、指定したアプリからフィールド情報を取り出すコマンドラインツールが同梱されています。
との通りで、これをインストールする事により、
- エディタ上でコード補完が有効になる
- 型の誤りや関数に渡す引数の誤りなどと言った実装ミスを機械的に指摘してくれる
などの効果がもたらされ、開発効率の向上に多大に寄与してくれます。
インストールそのものは非常に簡単です。
% yarn add -D @kintone/dts-gen
で、
% npx kintone-dts-gen --help
と実行してヘルプがずらずらっと出て来れば OK です。
アプリのフィールド定義情報を取得する
では、インストールした
dts-gen
を使い、先ほど作成した案件管理アプリのd.ts
ファイルを取得してみましょう。
コマンドのフォーマットは以下の通りです。% npx kintone-dts-gen --host https://(サブドメイン).cybozu.com \ -u (ユーザー名) \ -p (パスワード) \ --app-id (アプリID) \ --namespace (名前空間プレフィクス) \ -o (ファイル出力先)と言うフォーマットになっています。
例えば、% npx kintone-dts-gen --host https://example.cybozu.com \ -u kintone-taro \ -p ******** \ --app-id 101 \ --namespace leadManagement.types \ -o src/lead-management.fields.d.tsと言った具合です。
実行すると案件管理アプリのフィールド情報が格納されたsrc/lead-management.fields.d.ts
ファイルが作成されます。lead-management.fields.d.tsdeclare namespace leadManagement.types { interface Fields { 確度: { type: "RADIO_BUTTON"; value: string; disabled?: boolean; error?: string; }; 単価: { type: "NUMBER"; value: string; disabled?: boolean; error?: string; }; 先方担当者: { type: "SINGLE_LINE_TEXT"; value: string; disabled?: boolean; error?: string; }; (省略) } interface SavedFields extends Fields { $id: { type: "__ID__"; value: string; }; $revision: { type: "__REVISION__"; value: string; }; (省略) } }
Fields
インターフェイスがアプリに固有のフィールドに関する定義、SavedFields
インターフェイスはレコード ID や作成者・更新者などどのアプリでも必ず存在するフィールドに関する定義が含まれます。
先ほどアプリ作成後にフィールドコードを分かりやすくなるよう修正しましたが、その結果がここに現れて来るわけです。型定義ファイルの参照を指定する
d.ts
ファイルを取得しただけでは、まだエディタ上でその効果を発揮させることはできません。
これをTypeScript
で参照可能にするために、プロジェクトの直下にあるtsconfig.json
を編集します。tsconfig.json+ "files" : [ + "./node_modules/@kintone/dts-gen/kintone.d.ts", + "./src/lead-management.fields.d.ts" + ], "exclude": [ "node_modules" + "dist" ]併せて
exclude
にビルドしたファイルが格納されるdist
を加えておくと良いでしょう。コード補完の動作の様子は先ほども紹介した公式ドキュメントでも確認できます。
カスタマイズを kintone に適用する
これで一通り準備は整いましたので、いよいよ開発を進めていきます。
カスタマイズビューで Vue.js のマウントポイントを作成する
Vue.js
は DOM の特定の要素にインスタンスをマウントさせるところから始まりますが、kintone では カスタマイズビュー で DOM を定義してやるのが一般的でしょう。案件管理 アプリの設定で「一覧」から+ボタン(一覧の追加)をクリックし、「レコード一覧の表示形式」で「カスタマイズ」を選択します。
「HTML」のボックス内に、とりあえずシンプルに以下のように書いて、<div id="lead-management"></div>そのあと左上の「保存」をクリックし、アプリの設定も保存してください。
ここで記述したlead-management
が、これから実装するVue.js
のマウントポイントになるわけです。
カスタマイズビューに
Vue.js
インスタンスをマウントする当然ですが、この時点で「一覧1」ビューを見ても何も表示されません。
kintone カスタマイズの流儀に則り、レコード一覧イベント発生時に動作するよう実装していきます。
main.ts
現在のビューがカスタマイズビューであり、インスタンスが未マウントであり、マウントポイントが存在する際にマウント処理を実行すると言う流れにします。
main.ts
を以下のように修正します。main.tsimport Vue from "vue"; import App from "./App.vue"; Vue.config.productionTip = false; // マウントポイント const mountPount: string = "lead-management"; // インターフェイス interface KintoneEvent { offset: Number; records: leadManagement.types.SavedFields[]; size: Number; type: string; viewName: string; viewType: string; } /** * イベント処理 */ kintone.events.on(["app.record.index.show"], (e: KintoneEvent) => { // カスタマイズビューでないか既にマウント済みなら何もしない if (e.viewType !== "custom" || document.querySelector("#app")) { return e; } // マウントポイントにマウント実行 if (document.querySelector(`#${mountPount}`)) { new Vue({ render: h => h(App, { props: { records: e.records } }) }).$mount(`#${mountPount}`); } });上記のポイントとしては、
kintone.events.on()
で受け取るイベントオブジェクトをインターフェイスとして定義しておく- そのインターフェイスの
records
オブジェクトの型として、先ほどdts-gen
で作成した型定義の配列leadManagement.types.SavedFields[]
を指定してやる- イベントオブジェクトで受け取ったレコードを
props
としてApp
コンポーネントに渡してやると言った点が挙げられます。
App.vue
次に、そのレコードを受け取って表示する側の
App.vue
を実装します。App.vue<template lang="pug"> #app table thead tr th 会社名 th 先方担当者 th 見込み時期 th 確度 th 製品名 th 単価 th ユーザー数 th 小計 tbody tr(v-for="record in records") td {{record.会社名.value}} td {{record.先方担当者.value}} td {{record.見込み時期.value}} td {{record.確度.value}} td {{record.製品名.value}} td {{record.単価.value}} td {{record.ユーザー数.value}} td {{record.小計.value}} </template> <script lang="ts"> // デコレーター import { Component, Prop, Vue } from "vue-property-decorator"; // コンポーネント @Component // クラス本体 export default class App extends Vue { // [プロパティ] 表示対象のレコード @Prop({ default: [] }) records!: leadManagement.types.SavedFields[]; } </script> <style scoped lang="scss"> #app { table { th, td { border: 1px solid #9cf; padding: 2px 10px; } thead { tr { background-color: #dfd; } } tbody { tr { background-color: #fff; &:nth-child(even) { background-color: #dff; } } } } } </style>プロパティで受け取った
records
をv-for
で1つずつテーブルに出力するだけの簡単な処理です。
vue-property-decorator
を使うことでプロパティの受け取りの記述が非常にシンプルに書けます。ビルドしてカスタマイズを適用する
このように実装した
.ts
ファイルや.vue
ファイルをそのまま kintone のカスタマイズに適用する事は当然ながらできません。
kintone カスタマイズで利用可能なように、JavaScript
ファイルにコンパイルしてやります。
VS Code のターミナルで、以下のように実行します。% yarn buildすると、プロジェクト直下に
dist
ディレクトリが作成し、その下にjs
とかcss
とかのディレクトリ、そして.js
や.css
ファイルが作成されるはずです。
ここで作成されたファイルを案件管理アプリの JavaScript / CSS カスタマイズに適用してやります。
改めて「一覧1」を表示すれば、レコード一覧が出力されるのが確認できます。
もう少し楽に開発を進める
一連の開発の流れを説明して来ましたが、コードにちょっと手を入れるたびに
build
して kintone アプリに適用して・・・なんてことをやっていては効率が悪いことこの上ありません。
最終ビルド時は仕方ないとしても、開発中はソースの変更を保存したらすぐにブラウザ上で確認したいですよね。
しかしながら、このような用途で使われるyarn serve
(vue-cli-service serve
)では実ファイルが作成されないため(メモリ上に展開されるため)、そのままでは kintone のカスタマイズに適用する事はできません。
そこで、VS Code の拡張機能の1つである Live Server を利用します。
Live Server 自体のインストールや基本的な設定、HTTPS の設定はこちらを参考にしてください。Live Server をインストールしたら、
package.json
に以下のように設定します。package.json"scripts": { "serve": "vue-cli-service serve", + "devel": "vue-cli-service build --mode development", + "watch": "vue-cli-service build --mode development --watch", "build": "vue-cli-service build", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint" },
さらに、
.vscode/settings.json
も以下のようにしておきます。.vscode/settings.json{ "liveServer.settings.root": "/dist/", "liveServer.settings.https": { "enable": true, "cert": "(フルパス)", "key": "(フルパス)" } }で、VS Code のターミナルで
% yarn watchと実行し、エディタ右下の
Go Live
をクリックします。
これで、
- ソースコードを編集すると自動的にビルドが実行される
- ローカルの
dist/js/*.js
ファイルをhttps://localhost:5500/*.js
としてブラウザで閲覧可能となります。
アプリの閲覧結果も先ほどと同じ結果になるのが確認できるはずです。
次回は
というわけで、今回は
Vue.js
とTypeScript
で開発するプロジェクトを kintone にはめる一連の流れを見ていきました。
ここから先はもう普通に kintone のカスタマイズの流儀に則って進めればいいだけですが、せっかくですので、このアプリをもう少しいい感じに育てていきたいかと思います。
今回触れなかったkintone JS SDK
の話にも触れていければと思います。
- 投稿日:2019-12-01T20:24:15+09:00
Vueコンポーネント間まとめ2(イベント、Function)
イベント
イベントを利用して親子間でデータの受け渡し、method実行が可能
- 親:v-on を使って子コンポーネントで起きた任意のイベントを購読
- 子:$emit メソッド にイベントの名前を渡して呼び出すことで、イベントを送出
src/components/blogPost.vue<template> <div class="blog-post"> <h3>{{ post.title }}</h3> <button v-on:click="$emit('enlarge-text')"> Enlarge text </button> <div v-html="post.content"></div> </div> </template> <script> export default { props: ["post"] }; </script>src/App.vue<template> <div :style="{ fontSize: postFontSize + 'em' }"> <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-on:enlarge-text="postFontSize += 0.1" ></blog-post> </div> </template> <script> import blogPost from "./components/blogPost.vue"; export default { components: { blogPost }, data: function() { return { postFontSize: 1, posts: [ { id: 1, title: "My journey with Vue", content: "content1" }, { id: 2, title: "Blogging with Vue", content: "content2" }, { id: 3, title: "Why Vue is so fun", content: "content3" } ] }; } }; </script>イベントと値の送出
- 親:送出されたイベントの値に $event でアクセス
- 子:$emit の2番目のパラメータを使って値を提供
src/components/blogPost.vue<button v-on:click="$emit('enlarge-text', 0.1)"> Enlarge text </button>src/App.vue<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-on:enlarge-text="postFontSize += $event" ></blog-post>メソッドの場合
- 値はそのメソッドの最初のパラメータとして渡される
src/components/blogPost.vue<button v-on:click="$emit('enlarge-text', 0.1)"> Enlarge text </button>src/App.vue<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-on:enlarge-text="onEnlargeText" ></blog-post> <script> import blogPost from "./components/blogPost.vue"; export default { components: { blogPost }, data: function() { return { postFontSize: 1, posts: [ { id: 1, title: "My journey with Vue", content: "content1" }, { id: 2, title: "Blogging with Vue", content: "content2" }, { id: 3, title: "Why Vue is so fun", content: "content3" } ] }; }, methods: { onEnlargeText: function(enlargeAmount) { this.postFontSize += enlargeAmount; } } }; </script>メソッドに複数パラメータを渡す場合
- objectにして渡す
src/components/blogPost.vue<button v-on:click="$emit('enlarge-text', { size: 0.1, color: 'red' })"> Enlarge text </button>src/App.vue<template> <div :style="{ fontSize: postFontSize + 'em', color: color }"> <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-on:enlarge-text="onEnlargeText" ></blog-post> </div> </template> <script> export default { : : methods: { onEnlargeText: function(objectParam) { this.postFontSize += objectParam.size; this.color = objectParam.color; } } } </script>v-bind type:function(非推奨)
イベントが推奨されるのは以下のためみたいです
- This works in the same way that the DOM works — providing a little more consistency with the browser than what React does.src/components/blogPost.vue<template> <div class="blog-post"> <h3>{{ post.title }}</h3> <button v-on:click="onEnlargeText(0.1)"> Enlarge text </button> <div v-html="post.content"></div> </div> </template> <script> export default { props: ["post","onEnlargeText"] }; </script>src/App.vue<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-bind:on-enlarge-text="onEnlargeText" ></blog-post> <script> export default { : : methods: { onEnlargeText: function(enlargeAmount) { this.postFontSize += enlargeAmount; } } } </script>
- 投稿日:2019-12-01T18:54:40+09:00
Vue.jsで動的にコンポーネントを生成・削除・マウントする方法
はじめに
普段Vue.jsを使って開発をしているのですが、ある日、開発をしていたらコンポーネントを動的にマウントしたいという希望が出てきました。
この記事では、Vueのコンポーネントを動的にマウント、それに伴い動的にマウントしたコンポーネントを削除したり、propsを設定したりする方法について説明します。
どんな時に使うか
例えば、ボタンを押したらテキストエリアが出現するコードについて考えてみます。
これぐらいであればjQueryでできるかもしれませんが、テキストボックスにcssで装飾などをつけている場合には、コンポーネントにして、それをappendしたいと思います。Vueを使えばイベントの定義なども非常に簡単になるので一石二鳥です。
v-ifやv-showなどでもできますが、コンポーネントを最初どこに追加するかわからない場合などには動的にマウントする必要が出てくるでしょう。
ただ単にコンポーネントを表示するだけであれば以下のようになります。テキストエリアの横には文字数のカウントをつけています。
vueファイルはこんな感じ。
<template> <div> <count-text></count-text> <button>click me!</button> </div> </template> <script> import TextBox from './TextBox.vue'; export default { components: { 'count-text': TextBox } } </script>TextBox.vue<template> <div class="uk-margin-top uk-margin-left"> <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength"></textarea> <span>{{ length }}文字</span> </div> </template> <script> export default { data: function () { return { length: 0, text: '' } }, methods: { countTextLength: function () { this.length = this.text.length; } } } </script>コンポーネントを動的にマウントする
ポイントは、appendTextメソッドの中です。
コンポーネントのインスタンスを作成して、jQueryでappendしています。<template> <div> <div class="text-place"></div> <button @click="appendText">click me!</button> </div> </template> <script> import Vue from 'vue/dist/vue.esm.js'; import TextBox from './TextBox.vue'; export default { methods: { appendText: function () { var ComponentClass = Vue.extend(TextBox); var instance = new ComponentClass(); instance.$mount(); $('.text-place').append(instance.$el); } } } </script>画面は以下のような感じになります。
propsを設定する
動的にマウントするコンポーネントにpropsを設定したい時にはどうすれば良いのでしょうか。
テキストにtitleを設定してみます。
TextBox.vue<template> <div class="uk-margin-top uk-margin-left"> <span>{{ title }}</span> <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength"></textarea> <span>{{ length }}文字</span> </div> </template> <script> export default { props: { title: String //追加 }, data: function () { return { length: 0, text: '' } }, methods: { countTextLength: function () { this.length = this.text.length; } } } </script>propsを設定するには、以下のようにします。
<template> <div> <div class="text-place"></div> <button @click="appendText">click me!</button> </div> </template> <script> import Vue from 'vue/dist/vue.esm.js'; import TextBox from './TextBox.vue'; export default { methods: { appendText: function () { var ComponentClass = Vue.extend(TextBox) var instance = new ComponentClass({ // 追加 propsData: { title: 'タイトル' } }); instance.$mount(); $('.text-place').append(instance.$el); } } } </script>イベントを設定する
emitなどで親にイベントを通知したい時などは以下のようにします。
<template> <div class="uk-margin-top uk-margin-left"> <span>{{ title }}</span> <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength" @change="changeText"></textarea> <span>{{ length }}文字</span> </div> </template> <script> export default { props: { title: String }, data: function () { return { length: 0, text: '' } }, methods: { countTextLength: function () { this.length = this.text.length; }, changeText: function () { this.$emit('changed'); //追加 } } } </script>親のコンポーネントは以下の通り
<template> <div> <div class="text-place"></div> <button @click="appendText">click me!</button> </div> </template> <script> import Vue from 'vue/dist/vue.esm.js'; import TextBox from './TextBox.vue'; export default { methods: { appendText: function () { var ComponentClass = Vue.extend(TextBox) var instance = new ComponentClass({ propsData: { title: 'タイトル' } }); // emitで受け取るイベントを定義 functionの部分はmethodsで定義もできます。 instance.$on('change', function () { console.log('textchanged'); }); instance.$mount(); $('.text-place').append(instance.$el); } } } </script>instance.$on('イベント名', 処理)という感じで定義できます。
コンポーネントを削除するには
自分自身を削除するには$destroyを使います。
<template> <div class="uk-margin-top uk-margin-left"> <span>{{ title }}</span> <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength" @change="changeText"></textarea> <span>{{ length }}文字</span> <button @click="deleteSelf">destroy!</button> </div> </template> <script> export default { props: { title: String }, data: function () { return { length: 0, text: '' } }, methods: { countTextLength: function () { this.length = this.text.length; }, changeText: function () { this.$emit('changed'); }, deleteSelf: function () { this.$destroy(); this.$el.parentNode.removeChild(this.$el); } } } </script>this.$destroy(); this.$el.parentNode.removeChild(this.$el);でコンポーネントを削除します。
thisの部分をinstanceに変更すると、親からも削除できます。instance.$destroy(); instance.$el.parentNode.removeChild(instance.$el);終わりに
Vue.jsは非常に便利だと感じました。
この場を借りて、この方法を教えてくれた参考記事の偉大な先人たちへ深い感謝を送りたいと思います。
非常に助かりました。参考記事
https://css-tricks.com/creating-vue-js-component-instances-programmatically/
https://stackoverflow.com/questions/40445125/how-can-component-delete-itself-in-vue-2-0
- 投稿日:2019-12-01T18:40:31+09:00
Vue.jsでECサイトのモックアップを作ってみた。
この記事は、Vue.js Advent Calendar 2019 #1 1日目の記事です。https://qiita.com/advent-calendar/2019/vue
AdventCalendar初参加です。よろしくお願い致します。
VueCLIでECサイトのモックを作ってみる
はじめに出来た物の画像です。
デプロイしたサンプルのアドレスです。https://ec-sample-vue-cli.web.app/
コードはここです。https://github.com/yujiteshima/ec-sample-vue-cliモチベーション
Nuxt.jsを使ってみて、その便利さに、SSRの簡単さにVueCliをめっきり使わなくなってしまいました。
でも、VueCliを使ってCompositionApiの勉強をしている時にVueCliで作るのも面白いなと思いました。
何か作ってみたいと思い、今回ECサイトのモックを作ってみます。Nuxt.js風に作ってみるというのをテーマにしていきます。
何かを勉強する時のサンプルアプリを作るレパートリーを増やして行って、学習を楽しくしたいと思います。環境
@vue/cli 4.1.1
node v10.15.3
yarn 1.19.0仕様
データは外部APIから取って来ず、storeに仮のデータを入れておきます。
認証もAuth0やFirebaseAuthentication等の実装はせず仮の実装にします。
ページ構成は、Nuxt.jsでPageで実現する様な感じで作ってみます。プロジェクト作成
$ vue create ec-sample-vue-cli Vue CLI v4.1.1 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass) ? Pick a linter / formatter config: Basic ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? NoCSSフレームワークは、bulmaを使います。
bulmaの勉強を兼ねています。軽量だと聞きました。使っていない部分は使わずに部分的に取り込むことも出来ると聞きました。
まずは色々作ってみて、慣れてから使っている部分だけ部分的に取り込むことにチャレンジしてみます。$ yarn add bulma
初期のsrcディレクトリ
. ├── App.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── main.js ├── router │ └── index.js ├── store │ └── index.js └── views ├── About.vue └── Home.vue
Nuxt.jsを真似てフォルダ構成を作成してみる。
App.vueをNuxt.jsでのDefault.vueのにしたいと思います。名前もDefault.vueに変えちゃいます。
viewsの中にindex.vueを作ってルートディレクトリとして、Nuxt.jsと同じ様にページを配置して作ってみたいと思います。. ├── assets │ ├── filters.js │ ├── logo.png │ └── validators.js ├── components │ ├── footer │ │ └── Footer.vue │ ├── header │ │ └── Header.vue │ ├── hero │ │ └── Hero.vue │ ├── menu │ │ └── Menu.vue │ ├── modal │ │ ├── Checkout.vue │ │ ├── Login.vue │ │ └── Registration.vue │ ├── products_list │ │ ├── Products.vue │ │ └── ProductsListContainer.vue │ └── search │ └── Search.vue ├── defalt.vue ├── main.js ├── router │ └── index.js ├── store │ └── index.js └── views ├── index.vue ├── produst_detail │ └── _id.vue └── user └── wishlist └── index.vue
Default.vueで全ページ共通のComponentsを配置する。
default.vueのコード
全ページ、ヘッダーとフッター、モーダルは共通です。<template> <div> <Header /> <main> <!--ルーティングで表示するのはこちらです--> <router-view /> <!--モーダルはここにまとめました--> <LoginModal /> <RegistrationModal /> <CheckoutModal /> </main> <Footer /> </div> </template> <script> import LoginModal from "@/components/modal/Login"; import Header from "@/components/header/Header"; import Footer from "@/components/footer/Footer"; import RegistrationModal from "@/components/modal/Registration"; import CheckoutModal from "@/components/modal/Checkout"; export default { components: { Header, Footer, LoginModal, RegistrationModal, CheckoutModal } }; </script> ...scssは省略...Pageファイル
ルーティングで使うNuxt.jsでいうPagesに格納するファイル群はviewsフォルダにまとめます。
これでNuxt.jsに教えてもらったフォルダの配置に近くなったと思います。└── views ├── index.vue ├── produst_detail │ └── _id.vue └── user └── wishlist └── index.vuerouterも掲載しておきます。
import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/home.vue' Vue.use(VueRouter) const routes = [{ path: '/', name: '', component: Home, }, { path: '/product_detail/:id', name: 'product_detail-id', component: () => import('../views/produst_detail/_id.vue') }, { path: 'user/wishlist', name: 'user-wishlist', component: () => import('../views/user/wishlist/index.vue') } ] const router = new VueRouter({ mode: 'history', base: '/ec-sample-vue-cli/', // new! routes }) export default routerまとめ
ルーティングは、自分で書く必要がありますが、VueCliで書くのも面白いと思います。
Jsファイルの軽量化や、CompositionAPIの様な新しい仕様を試したり、新しいライブラリを試したりする時は出来る事が多い分、
Nuxt.jsよりやりやすいと思いました。今回、作ったSampleで次はCompositionAPIで作ってみたいと思います。
- 投稿日:2019-12-01T18:02:26+09:00
vuetifyのテーブルが簡単高機能でオシャレ 使い方
vuetifyを使うと、オシャレで高機能なテーブルが簡単に使えたのでメモします。
この記事でやることは以下の3点です。
* テーブルの設定
* アイテムの削除
* axiosを使ってアイテムを設定するここでお知らせです。
こんなサイトで使ってます。
https://kintorenote.com/これはaxiosでサーバーからデータを取得して、テーブルのデータとして使っています。
サーバーからデータを取得して、その内容をテーブルの要素(item)として表示するなんてこともできますので、便利です
テーブルの設定を行う
公式サイトはこれです。
https://vuetifyjs.com/ja/components/data-tablesテーブルを使う準備をします。
テーブルに必要なのは、headersとitemsです。
どちらも、配列の中にオブジェクトを入れて定義します。やってみると単純なのでやっていきます。main.vue<template> <v-container> <v-row> <v-col cols="10"> <v-data-table :headers="headers" :items="items" ></v-data-table> </v-col> </v-row> </v-container> </template>テーブルは v-data-tableにヘッダーとアイテムを指定すれば出来上がりです。
ヘッダーを定義する
ヘッダーではヘッダーに表示する内容を定義するのですが、同時にヘッダーと紐付くitemsのkeyを定義して起きます。
今回はIDと名前と年齢というカラムを持つテーブルを作ります。
main.vue<script> export default { data(){ return { headers: [ { text: 'ID', value: 'id' }, { text: '名前', value: 'name', }, { text: '年齢', value: 'age' }, ], } }, } </script>配列の中にオブジェクトを突っ込みます。
オブジェクトはtextとvalueを持っていて、textがヘッダーに表示される名称で、valueは、後に用意するitemのkeyになります。
まぁ書いてみるとわかりやすいです。itemを用意する
itemはテーブルの中身です。
今回必要な情報はidとnameとageになります。
テーブルのitemも、headerと同じで配列の中にオブジェクトを入れて作ります。main.vueitems:[ { id : 1 , name : "高田健志" , age : 33 }, { id : 2 , name : "横山緑" , age: 42 }, { id : 3 , name : "山田太郎" , age: 10 }, ]dataに追加します。
これで準備完了です。以下のように表示されています
ちなみに、ソート機能がついています。
年齢でソートしたものです。削除行を追加して削除機能を付ける
これだけでも超絶クールなテーブルなんですが
削除機能もつけます。
公式をみるとサンプルがあるので、ごっそりあれしてみます。まずはヘッダーに削除を追加します。
main.vueheaders: [ { text: 'ID', value: 'id' }, { text: '名前', value: 'name', }, { text: '年齢', value: 'age' }, { text:'削除', value:'delete', sortable:false } ],sortable:falseにするとソートできなくなります。
削除でソートすることはないのでfalseにしておきます。items配列の中のオブジェクトにdeleteというkeyを持った何かを定義するのはくたびれるので、突っ込んでしまいます。
main.vue<template> <v-container> <v-row> <v-col cols="10"> <v-data-table :headers="headers" :items="items" > <template v-slot:item.delete="{ item }"> <v-btn small color="error" @click="deleteItem(item)" > delete </v-btn> </template> </v-data-table> </v-col> </v-row> </v-container> </template>tableのslotを使ってdeleteに関して、deleteItemというイベントを持ったボタンを定義します。
これで削除の列にはこの削除ボタンが表示されます。main.vuemethods: { deleteItem (item) { const index = this.items.indexOf(item) confirm('ガチで削除しますか') && this.items.splice(index, 1) },
処理は公式のをあれしました。
インデックス番号を取得して、確認メッセージを出して、OKならそこを取り除いています。axiosを使ってサーバーからitem情報を取得する
テーブルの中身(items)はサーバーから取得してきて表示することも簡単です。
今回は便利な
https://jsonplaceholder.typicode.com/
を利用します。テーブルの設定を少し変更します。
itemsをserverDatasという配列にしておきます。main.vue<template> <v-container> <v-row> <v-col cols="10"> <v-data-table :headers="headers" :items="serverDatas" > <template v-slot:item.delete="{item}"> <v-btn small color="error" @click="deleteItem(item)" > delete </v-btn> </template> </v-data-table> </v-col> </v-row> </v-container> </template>serverDatasというitemsを使うように変更していますので
serverDatasを追加します。また、今回取得するデータにageがないのですが、emailが取れるので
headersのageをemailに変更しておきます。main.vueheaders: [ { text: 'ID', value: 'id' }, { text: '名前', value: 'name', }, { text: 'メルアド', value: 'email' }, { text:'削除', value:'delete', sortable:false } ], serverDatas:[ ],次にaxiosを使ってデータを取得します。
mountedの中でやればページが表示される頃にはデータが取得できていることでしょう。main.vuemounted(){ axios.get('https://jsonplaceholder.typicode.com/users') .then( res => { this.serverDatas = res.data }) .catch( e => { console.log(e) }) .finally(()=>{ console.log("通信完了") }) }これだけです。
serverDatasを取得してきたdataに置き換えました。しっかりデータを取得してテーブルに表示できています。
簡単におしゃれなテーブルが利用できて便利です。
最後にソースを全て貼っておきます。
main.vue<template> <v-container> <v-row> <v-col cols="10"> <v-data-table :headers="headers" :items="serverDatas" > <template v-slot:item.delete="{item}"> <v-btn small color="error" @click="deleteItem(item)" > delete </v-btn> </template> </v-data-table> </v-col> </v-row> </v-container> </template> <script> import axios from 'axios'; export default { data(){ return { headers: [ { text: 'ID', value: 'id' }, { text: '名前', value: 'name', }, { text: 'メルアド', value: 'email' }, { text:'削除', value:'delete', sortable:false } ], serverDatas:[ ], items:[ { id : 1 , name : "高田健志" , age : 33 }, { id : 2 , name : "横山緑" , age: 42 }, { id : 3 , name : "山田太郎" , age: 10 }, ], } }, methods: { deleteItem (item) { const index = this.serverDatas.indexOf(item) confirm('ガチで削除しますか') && this.serverDatas.splice(index, 1) }, }, mounted(){ axios.get('https://jsonplaceholder.typicode.com/users') .then( res => { this.serverDatas = res.data }) .catch( e => { console.log(e) }) .finally(()=>{ console.log("通信完了") }) } } </script>デリートのメソッドは、対象の配列がitemsからserverDatasに変更してあります。
- 投稿日:2019-12-01T17:50:46+09:00
IT業界4ヶ月の人間が、ある記事に触発されてTrelloダミーを作ってみたお話し。
Trelloダミー作ってみた!!
この記事【トップデベロッパーになるために作成したいアプリ8選】に触発されて、Trelloダミーなるものを作ってみました。
作品はこちら→TODOdo?Trelloって何?
TODOリストです。
タスクを自由に作成し、ドラッグ&ドロップで並び替えもできます。
Trello開発環境
バック: Laravel 6.5
フロント: Vue.js
DBは使用してません。
使ったライブラリ: Vue.Draggable仕組み
localStorageを使用し、曜日単位でTODOリストを管理しています。
ちなみにlocalStorageへ保存するときはkey: valueの形なので
[key]: [valiue]
[曜日]: [TODOリスト]
と言った形で保存しています。
なので、タスクの保存や並び替え時の順番保持なども全てlocalStorageへ保存しています。工夫点
・上記の記事のサンプルは消去ができなかったこと、Trelloでは消去の方法が分かりにくかったので、タスクの消去を直感的にできるようにしました。
・Trelloでもサンプルでも大枠を自由に作ることができますが、今回作成したものの大枠は、月から日曜日の一週間のみとし利便性を追求してみました。
・どの曜日を選んでも
するべきこと
作業中
完成
と言ったものをテンプレートで表示させるようにし、最初のリスト作成する手間を減らしました。流れ
メインのシステムのや工夫した点を一部載せていきいます。
作品→TODOdo?1,home画面
曜日の表示
こちらは見たままです。
vue-routerを使い曜日に対する画面を切り替えるために、定義した配列の中で曜日に応じたパスも定義しております。
sample.vue<script> import draggable from 'vuedraggable' export default { name: 'App', components: { draggable }, data() { return { weeks: [ {path: 'mon', week: '月'}, {path: 'tue', week: '火'}, {path: 'wed', week: '水'}, {path: 'thu', week: '木'}, {path: 'fri', week: '金'}, {path: 'sat', week: '土'}, {path: 'sun', week: '日'} ], } }, } </script>こんな感じ〜
2,メイン画面
左上の「新しくリストを作る」で新規のカードを作成し、その中の「するべきことを追加」「作業中を追加」「完了を追加」でリストを追加できます。
そして、ゴミ箱にリストを持っていくと削除することができます。並び替えしたときの順番の保持
このカードが追加されたり、リストが追加or削除されたりするとlocalStorageに保存されるようになっています。
ちなみに、並び替えはVue.Draggableで行なっています。
ここで工夫した点は、並び替え時の順番は保存する点です。
Vue.Draggableの機能で、並び替えしたときの順番も保持できるのですが、それを使うとゴミ箱にリストを捨てる際、選択していないリストも一緒にゴミ箱に捨てられるといった挙動になってしまいました。なので、Vue.Draggableに備わっているeventの
end
を使い並び替えしたときの順番を保持させました。
end
は選択したリストを掴んで、離したときに発火するイベントです。選択したリストの情報はeventobjectで取得できるので、その中からいろんなデータを引っ張り出しあーだこーだして並び替えしたときの順番を保持しlocalStorageへ保存しました。
trash
リストをゴミ箱に移動させた際もeventobjectでDOMを抜き出して、
display: none;
を付与し消えたように見せています。
今後はゴミ箱をクリックするとゴミ箱の中身が見れるようにし、そこから削除をできるようにしていきたいと思っています。まとめ
実際作ってみてどうだった?
実はTODOリストを作成するのが初めてなのもあって「まぁこんな感じかな〜」とシステムを頭の中で思い描いていたのですが、記述した並び替えの保持やtrash機能などについては少し悩みました。
「どうやって選択したDOMの情報を取得するんだ??」と。
結果的にeventobjectを見つけてからは早かったのですが、documentをよく読まねばなと思いました。そして、何より楽しかったです。
普段TODOリストは使わないのですが、Trelloにしろ参考にした記事のDEMOにしろ、「凄いな〜」と思う反面「こうだったらもっと使いやすくなるのにな〜」とか思いついてそのアイディアを実装していく過程がとても楽しかったです。この後も機能追加したり、他の作品を作っていきます。
おまけ
今後追加していきたい機能
・レスポンシブ化
・携帯と同期させられるようにする
・作業中から完了までの時間を可視化させる
・スケジュール張と同期させるようにする。
作品はこちら→TODOdo?
至らぬ点もありますが、良かったら使ってみてください。
そして、アドバイス等頂ければ幸いです。
- 投稿日:2019-12-01T16:40:22+09:00
同じコンポーネントでStoreを使って状態を共有できるか確かめてみた話[Vue]
はじめに
兄弟コンポーネント間での値のやり取りにはVuexや自作のstoreを使えば、
props
とemit
でバケツリレーをせずにやることが可能です。
ふと疑問に思ったのが同じコンポーネントを複数使い回して、その間でstoreを使えば状態の共有ができるのか?でした。
疑問に思ったらやってみよう!ということでやったみたのが今回の記事となります。実験
まずは準備
今回使うコンポーネントとstoreモジュールを作っていきましょう。
最初にStoreの方から作っていきましょう。store.jsexport const Store = { state: { StoreValue: "Qiita" }, setStore(value) { this.state.StoreValue= value; }, getStore(){ return this.state.StoreValue; } };状態を持つのはstateの部分で、実際に状態を取得したり設定したりするのもstoreにアクションを作っています。
次にVueのコンポーネントを作っていきます。
まずはHTML部分からです。component.vue<template> <div> <p>{{ ComponentValue }}</p> <button @click="onButtomClick">Click</button> </div> </template>結果を表示するためのPタグと、イベントを発火するためのButtonタグを配置しました。
次にScript部分。component.vue<script> import { Store } from "./store.js"; export default { data() { return { StoreObject: Store.state, }; }, computed: { ComponentValue: { set(value) { Store.setStore(value); }, get() { return Store.getStore(); } } }, methods: { onButtomClick(value) { this.ComponentValue = `${this.ComponentValue}!`; } } }; </script>全体としてはこのような感じになりました。
重要そうなとこを細かく見ていきましょう。component.vuedata() { return { StoreObject: Store.state, }; },dataオブジェクトで、読み込んだStoreの状態を持たせています。
ここでStoreをもたせておかないと、うまく更新されたものを反映できませんでした。component.vuecomputed: { ComponentValue: { set(value) { Store.setStore(value); }, get() { return Store.getStore(); } } }算出プロパティのほうはgetter/setterでstoreに対して状態を取得と設定を行うようにしています。
component.vuemethods: { onButtomClick(value) { this.ComponentValue = `${this.ComponentValue}!`; } }ボタンをクリックした時に行うメソッドです。
算出プロパティを介してstoreの値を取得し、それを!マークと文字列結合をして、再度storeのアクションで状態を設定しにいく。
という流れになっています。あとは親のほうで、作ったコンポーネントを複数配置すれば完了です。
component.vue<template> <div> <qiita></qiita> <qiita></qiita> </div> </template>やってみた
実際に動かしてみると、クリックしていないほうのも!マークが増えているので、同じコンポーネントでもstoreを介して状態を共有できることがわかりました。
本当にstoreを介してるのか?
ただたんに同じコンポーネントだから状態が共有されてるだけなのでは?という疑問もあったので、
dataのインスタンス変数を使った場合にどうなるかというのをやってみましょう。インスタンス変数に変えてみる
component.vuedata() { return { StoreObject: Store.state, childvalue:"Vue" }; },component.vuecomputed: { ComponentValue: { set(value) { // Store.setStore(value); this.childvalue= value; }, get() { // return Store.getStore(); return this.childvalue; } } }dataにインスタンス変数を追加し、算出プロパティでのgetter/setterもそちらに対しての処理を行うように変更します。
この状態でどのように動くかを見てみましょう。連動しなくなりました。
ということは、同じコンポーネントを使い回していても、コンポーネントはそれぞれ独立したインスタンスになっている、ということがわかります。
なので、上のほうのではstoreを介して状態が共有されている、と言えますね!まとめ
同じコンポーネントでもstoreを使えば状態を共有できることが解りました。
ただしこれを使うシチュエーション自体がかなり限られてきますが、これを知っておくと似た構成のコンポーネントを増やして対応…
というのがなくなるかと思います!
- 投稿日:2019-12-01T15:06:17+09:00
Vue.jsでFont Awesomeを使う
インストール
プロジェクトに移動してコマンドを打つ
npm install --save @fortawesome/fontawesome-svg-core npm install --save @fortawesome/free-solid-svg-icons npm install --save @fortawesome/vue-fontawesome他に使いたいアイコンがあれば適宜追加する
- Regular
npm install --save @fortawesome/free-regular-svg-icons
- Light
npm install --save @fortawesome/pro-regular-svg-icons
- Brands
npm install --save @fortawesome/free-brands-svg-icons使う
src/main.js
に以下のように記述するsrc/main.jsimport Vue from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; import "./registerServiceWorker"; /** * 追加 */ import { library } from "@fortawesome/fontawesome-svg-core"; import { faChevronRight, faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; library.add(faChevronRight, faChevronLeft); Vue.component("font-awesome-icon", FontAwesomeIcon); /** * ここまで */ Vue.config.productionTip = false; new Vue({ router, store, render: h => h(App) }).$mount("#app");アイコン全部を追加したい時
import { fas } from '@fortawesome/free-solid-svg-icons' import { fab } from '@fortawesome/free-brands-svg-icons' import { far } from '@fortawesome/free-regular-svg-icons' library.add(fas, far, fab)アイコンを表示させる
<span class="calender-header__prev"> <font-awesome-icon icon="chevron-left"/> </span>
- 投稿日:2019-12-01T14:33:42+09:00
CentOS7にフロントエンドエンジニアに必要な最小限の環境を整える
前提
@Vue/cliの環境を構築する
後で、ansible化するつもりで、最低限必要なパッケージなのをメモしたもの
環境
virtualboxでcentos7-minimamをインストール
ネットホストオンリーネットワークを作成したVMにアタッチしておく以下は、CentOS7でのIPアドレスなどの設定。FWで8080開けておく
# nmcli connect modify enp0s3 ipv4.addresses "192.168.137.10/24" # nmcli connect modify enp0s3 ipv4.gateway "192.168.137.1" # nmcli connect modify enp0s3 ipv4.method manual # nmcli connect modify enp0s3 ipv4.dns "192.168.137.1" # nmcli connection modify enp0s3 autoconnect yes # nmcli connect down enp0s3 # nmcli connect up enp0s3 # sudo firewall-cmd --add-port=8080/tcp --zone=public --permanentWindowsのネットワーク設定で、ネットワークの共有をして、ホストオンリーネットワークが外にでていけるようにする(説明省略)
Centos7でのソフトインストール
$ sudo yum install epel-release $ sudo sudo yum install python3 $ sudo sudo yum install npmこの状態でサーバをshutdownさせてスナップショットを作成しておく。
Vueインストール
.bash_profileに追加
export N_PREFIX=$HOME/.n
export PATH=$N_PREFIX/bin:$PATH
$ source ~/.bash_profile $ sudo npm install -g @vue/cli $ sudo npm install -g n $ n lts $ cd $ mkdir web $ cd web $ vue create my-app $ npm run serveLocalFowardの設定をしておくと、PCのブラウザーでhttp://localhost:8000 参照するとCentOS7のlocalhost:8000に接続できる
.ssh/configHost cent7 HostName 192.168.137.10 User xxxx LocalForward 8000 localhost:8000
- 投稿日:2019-12-01T13:16:32+09:00
v-for内のv-checkboxの値をwatchプロパティで検知したい
ZOZOテクノロジーズのぱきお@pakzzzz0です。
普段はZOZOTOWNのバックエンドの開発を行っているのですが、今回は本業とは別に行っているフロントエンド周りの話です。この記事はZOZOテクノロジーズ #5 Advent Calendar 2019 3日目の記事になります。
昨日は@satto_sannの英語論文をフォーマットして翻訳する方法でした。▼その他アドベントカレンダーはこちら
ZOZOテクノロジーズ #1 Advent Calendar 2019
ZOZOテクノロジーズ #2 Advent Calendar 2019
ZOZOテクノロジーズ #3 Advent Calendar 2019
ZOZOテクノロジーズ #4 Advent Calendar 2019背景
開発時にArray[Object]をv-forでレンダリングした際に、その中の1要素をバインドしたv-checkboxの動作に対して、選択の状態を持たせたオブジェクトのプロパティの変更がwatchで検知されなかったので、解決までの道のりをメモします。
やりたかったこと
watch or computed でArray of Objectな変数を監視して、変更を検出したい
環境
パッケージ バージョン vue 2.6.10 vuetify 1.5.16 Laravel-mix 4.0.7 状況
data(){ return { ObjectToWatch:[{id:1, hoge: true, huga: '100'}, {id:2, hoge: false, huga: '500'}], ObjectProcessed: [], } }, watch:{ ObjectToWatch:{ handler(after, before){ this.ObjectProcessed = after.filter(function(val){ if(val.hoge){ return true; }else{ return false; } })) } } }template<div v-for="obj in ObjectProcessed" :key="obj.id"> <v-checkbox v-model="obj.hoge"/>{{obj.huga}} </div>上記例でv-checkboxの状態を変更しても反映されないobjのwatchプロパティが働かないことが全てのはじまりでした。
これまでv-bindで9割方やりたいことができていたため少し戸惑いつつも原因を追っていくことに現象を追っていく
deep:trueとコンソール出力設定
watchメソッドでObjectToWatchを監視できてるかを確認するためにまずコンソール出力を噛ませました。
watcherwatch:{ ObjectToWatch:{ handler(after, before){ this.ObjectProcessed = after.filter(function(val){ if(val.hoge){ return true; }else{ return false; } })) console.log(this.ObjectProcessed) }, deep:true } }上記の時点で、Objectの監視には
deep:true
オプションが必要とのドキュメントを見つけたので、deep:trueオプションも追加してみるも、まだプロパティの変更が検知できずテンプレ側の値の変更メソッドと反映する値を変更
こちらの記事を参考に値変更時のメソッドとcheckedの裏側の値の持ち方を変更してみることに
methodonChechboxChange(checkboxId){ let index = this.ObjectProcessed.findIndex(item => item.id == checkboxId); this.ObjectProcessed[index].hoge = !this.ObjectProcessed[index].hoge; console.log(this.ObjectProcessed) }tamplate<div v-for="obj in ObjectProcessed" :key="obj.id" :input-value="obj.hoge" @change="onChechboxChange(obj.id)" > <v-checkbox v-model="obj.hoge"/>{{obj.huga}} </div>この時点で、console上ではonCheckboxChange内でObjectProcessed[index].hogeのtrue,falseが変更されていることは確認できましたが、watchメソッドでの算出はいまだにできず。
そこでテンプレ側にObjectProcessedをそのまま出力してみることに。
templade{{ObjectProcessed}} <div v-for="obj in ObjectProcessed" :key="obj.id" :input-value="obj.hoge" @change="onChechboxChange(obj.id)" > <v-checkbox v-model="obj.hoge"/>{{obj.huga}} </div>すると、consoleでの出力とは裏腹に描画されている内容は変わっていないことが発覚、またwatchでのコンソール出力もされないことから反映できていないことを裏付けていました。
解決への糸口
こちらのページを参考に、公式のListRenderingのページをみると以下の記述がありました
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLengthまた、サンプルとして以下のコードが紹介されていました。
spliceを用いての実装// Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)解決方法
これを参考に実装を変更。
methodonChechboxChange(checkboxId){ let index = this.ObjectProcessed.findIndex(item => item.id == checkboxId); let objToChange = this.ObjectProcessed[index]; objToChange.hoge = !objToChange.hoge; this.ObjectProcessed.splice(index, 1, objToChange) }この実装に変更することでArrayの要素が変わったことを検知して無事にwatchが動作しました!
結論
Vue.jsのwatchプロパティにArray[Object]の変更を認識させるにはオブジェクトの要素を書き換えるのではなく、オブジェクトそのものの置き換えが必要。
明日は再び@satto_sannによる「Alexaのアカウントリンクをデバッグする方法」です。
- 投稿日:2019-12-01T12:45:13+09:00
Twitter風の〜分前を実装する(JavaScript/Vue編)
ダッシュボードを作っていて最新データの更新時刻を一覧表示にしたけど、パッとみても時間がわかりにくい...ということでTwitter風の〜分前って表示を実装しようと思いました...とRails版をQiitaに書いてから早5年。
最近はVueJSメインなので、再調査してみました。
調査
Javascriptで時間を扱うならMomentJsだよね、と調べたら普通に関数がありました。TimeFrom, TimeToなどで時間差が文字列で帰ってくるようです。
MomentJS:"Time from now"
https://momentjs.com/docs/#/displaying/fromnow/正確な差がほしい場合はdiffを使います。こちらは好きな単位(秒、分、日など好きな単位で取得できます)
MomentJS:"Diffrence"
https://momentjs.com/docs/#/displaying/difference/実装
日本語で表示するにはLocaleを'ja'にセットするだけでよさそうです(簡単!。
以下にVueのテストコードをおいておきます。Inputエリアの時刻を書き換えると変わります。
https://bellx2.github.io/time_diff.htmltime_diff.html<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.js"></script> <div id="app"> <input v-model="mytime"> <p> <p>FromNow : {{ time_diff() }} <p>Diff(sec) : {{ time_diff_sec() }} </div> <script> var app = new Vue({ el: '#app', data: { mytime: moment().format("YYYY/MM/DD HH:mm:ss"), }, methods: { time_diff: function(){ moment.locale('ja') return moment(this.mytime).fromNow() }, time_diff_sec: function(){ return moment(this.mytime).diff(moment(),'seconds') } } }) </script>参考
Twitter風の〜分前を実装する
https://qiita.com/bellx2/items/30906a7832ef4ff4c886
- 投稿日:2019-12-01T01:05:11+09:00
Vue functional component advent calendarを始めた背景
この投稿は Vue functional component Advent Calendar 2019 の1日目の記事です
第1日目のこの記事では、Vue functional component advent calendar という特化型Advent calendarをやろうと思った背景について書きたいと思います。
Outline
- 0. これを書いている人(@tomlla)はどんなエンジニアか
- 1. フロントエンドのランタイムフォーマンス問題に直面した
- 2. 解決方法を調べた
- 3. Functional Componentを試して思ったこと0. これを書いている人(@tomlla)はどんなエンジニアか
こんな感じの経験があります.
- Linuxベースアプライアンス(ネットワーク機器っぽいもの)
- about 3 years
- Linuxの設定をやってファームウェアとして作成
- boot時の設定もろもろ
- web管理画面
- 言語とかそういうの: php, c(linux上application) c(NICいじるkernel module), python, vanilla js, sqlite, ...
- Webベースの業務システムとかtoCアプリケーション開発いろいろ
- about 4 years
- 対象: 社内向け受発注システム, 不動産関係, 広告の出稿/パフォーマンス管理, シフト管理/人材配置の効率化
- java8, rails, python(クローラ), postgres, mysql, elasticsearch, vanilla js, vue
経歴を話したいのではなく「フロントエンドだけガリガリ書いてきたわけではない人」がこの記事を書いていることに留意していただければと思います。
つまりこの記事やVue functional component advent calendarで私の書いた内容に対して、強い人からのご指摘/アドバイスをお待ちしております1. フロントエンドのランタイムフォーマンス問題に直面した
ランタイムパフォーマンスという言葉が正しく問題を指している言葉かわかりませんが、ここでいうランタイムとはコンテンツ配信やサブリソースローディング周りの問題ではなく js のscripting timeに関する問題です。
直近でやっている大きな案件が シフト管理/人材配置の効率化 のシステムでして、この案件での問題をご紹介したいと思います。
いわゆる出勤表 みたいなものをwebの画面にだすわけです。
エクセルみたいな表です
12/1 12/2 12/3 ... Benedict Blue 10:00 - 19:00 デートなので休みます Violet Evergarden 10:00 - 19:00 10:00 - 14:00, 20:00 - 24:00 ... 縦軸: スタッフ
横軸: 日付という構造なのですが、なんとおケースによっては
横軸31日 * スタッフ数100人
なんですね...
1セル = 1 vue component だとしても 普通にやると 3,000 componentsです!
さらにテーブルの内外には様々な関連情報を表示しています(リアルタイムで人件費計算したりとか)また画面には[次の月へ] や [前の月へ] みたいな遷移ボタンがあります。
VueのSPAの場合、普通にやると [次の月へ] をクリックすると
1. 現在表示されているコンポーネントに対して Vue.$destroy を実行
2. 遷移先ページのコンポーネントをrenderingという事が行われます
現在は改善してありますが、最悪のエッジケースでは 25秒間画面が固まる(js long task)ということが起きていました.....
(chromeのプロファイラ結果のframe graphを見ると、細かい処理の山が連続しているのではなく、数個の横幅の長い処理が時間を占めていました)遅い.... こんなもの使い物にならない....
2. 解決方法を調べるため我々はジャングルの奥地へと向かった
※ ここでいうジャングルの奥地とは vue関係の勉強会, Vue forum, Vueの本家リポジトリさらにvue関係なくブラウザのパフォーマンスに関するものです
改善の候補として以下のものが上がりました。
- A. そもそも1画面に表示できる最大コンテンツ量を下げる(仕様変更)
- これは現在も実際の利用データを見ながらビジネス側のメンバーと検討中です。
- 実際には1ヶ月分の出勤表を俯瞰でチェックしたい、というニーズがあるため、どうしても多くのコンテンツを扱わなければならないことがわかりました。
- B. Functional componentを使う
- この記事の次のチャプターで説明します。
- C. Vue virtual scroller を使ってviewport内に表示されている部分のみ表示する
- 詳細は省きますがいくつかの懸念により、スキップしました。確実性の高い D. やE. に時間を当てるためでした。
- ただし現在検討中です
- D. 長いjs scriptingを避ける / 長い処理の最中はローディング中などの表示を行う (応答性を高める)
- 人間の感覚上の「速さ」に貢献したのがこのD.でした。
- 具体的には以下の対策を行いました
- 1度に大量のコンポーネントのレンダリングを行わない(defered的な処理を行う)
- イベントハンドラの中で長い処理を始める際は、あらかじめvueを使わずにDOMのAPIでローディングUIを表示を行う.
Vueを使っているのに直接DOM操作するのは避けたかったのですが、以下の問題が起きていたため仕方なかったのです...
- 1. vuex stateや
$data
を更新する- 2. 大規模なComponent re-renderingが始まってしまう
(scripting timeが長い vue内部の処理が行われる)- 3. stateや$data でローディングUIを表示/非表示を行なっているとローディングUIがなかなか表示されない
- E. Vue Componentの再レンダリングやレンダリングタイミングを減らす
- D. の次に効果があったのがこのE.でした。具体的には以下の変更を行いました。
- 巨大なコンポーネント内で直接htmlレンダリングをしない.
なぜならごく小さな一箇所の変更であってもコンポーネント全体のレンダリング処理が行われるため。 変更が多い部分はコンポーネント化して再レンダリングの局所化を目指しました。- templateで参照されている
state/$data
の変更タイミングに注意する。
- 例:
this.dataA, this.dataB, ... this.dataX
および これらを参照しているComputed propertyZ
があるとします。
- 問題点: API-Aを呼び出してレスポンスデータをdadaAをセット, API-BをdataBに... とやっていると都度レンダリングが走ってしまいます。
- 解決法:
Computed propertyZ
ではなく、this.computedDataZ
というレンダリング用state dataを用意し、全てのデータがAPIから取得できた後にthis.computedDataZ
に値をセットしました。これでレンダリング回数が1回になるわけです。(もっと綺麗な方法を見つけたい)3. パフォーマンス改善としてFunctional componentを使うアプローチの感想
そもそもFunctional componentとは何でしょうか?
Functional componentを知らない人のために紹介します。
私はVue の Functional componentは 「Stateless component」という名前がふさわしいと思います。まずFunctional componentではない普通のVueインスタンスは以下の点がFunctional componentと違います。
- 普通のVueコンポーネントはインスタンス化される
- インスタンス単位でComputed propertyの結果がキャッシュされるので不要な再計算がスキップできる
- 普通のVueコンポーネントはライフサイクルをもつ
逆にいうとFunctional componentは以下のことができません。
- Computed propertyが使えない
- インスタンスが無いのでインスタンス単位での結果のキャッシュを持てない
- インスタンスを持っていないためにthisが使えない
- つまりコードの記述は通常のコンポーネントと大きく変わる
- jsコード内でもtemplate内でも使えない. methodsとかplugin/mixinも使えない
一方Functional Componentには以下のメリットがあります
- インスタンス生成時にやっている処理をスキップできる
- ライフサイクル変化の際の処理をスキップできる。
実際のコード変更時・変更後の感想
まずFunctional Componentを使って、確かに初回レンダリングは とても早くなりました!
一方、コードの変更は結構大きいものでした。変更工数と得られるパフォーマンスのコスパを考えると無条件におすすめできるものではありません。
ということでこの辺の現場感のある知見を Vue functional component advent calendar で紹介して行きたいと思います!
- 投稿日:2019-12-01T01:05:11+09:00
Vue functional component advent calendarを始めた背景 ~大量コンポーネント描画時のパフォーマンスを求めて~
この投稿は Vue functional component Advent Calendar 2019 の1日目の記事です
第1日目のこの記事では、Vue functional component advent calendar という特化型Advent calendarをやろうと思った背景について書きたいと思います。
Outline
- 0. これを書いている人(@tomlla)はどんなエンジニアか
- 1. フロントエンドのランタイムフォーマンス問題に直面した
- 2. 解決方法を調べた
- 3. Functional Componentを試して思ったこと0. これを書いている人(@tomlla)はどんなエンジニアか
こんな感じの経験があります.
- Linuxベースアプライアンス(ネットワーク機器っぽいもの)
- about 3 years
- Linuxのビルド設定をパッケージ入れて、自社で作った機能を入れて、ファームウェアとして作成
- boot時の設定もろもろ
- web管理画面
- 言語とかそういうの: php, c(linux上application) c(NICいじるkernel module), python, vanilla js, sqlite, ...
- Webベースの業務システムとかtoCアプリケーション開発いろいろ
- about 4 years
- 対象: 社内向け受発注システム, 不動産関係, 広告の出稿/パフォーマンス管理, シフト管理/人材配置の効率化
- java8, rails, python(クローラ), postgres, mysql, elasticsearch, vanilla js, vue
経歴を話したいのではなく「フロントエンドだけガリガリ書いてきたわけではない人」がこの記事を書いていることに留意していただければと思います。
つまりこの記事やVue functional component advent calendarで私の書いた内容に対して、強い人からのご指摘/アドバイスをお待ちしております1. フロントエンドのランタイムフォーマンス問題に直面した
ランタイムパフォーマンスという言葉が正しく問題を指している言葉かわかりませんが、ここでいうランタイムとはコンテンツ配信やサブリソースローディング周りの問題ではなく js のscripting timeに関する問題です。
直近でやっている大きな案件が シフト管理/人材配置の効率化 のシステムでして、この案件での問題をご紹介したいと思います。
いわゆる出勤表 みたいなものをwebの画面にだすわけです。
エクセルみたいな表です
12/1 12/2 12/3 ... Benedict Blue 10:00 - 19:00 デートなので休みます Violet Evergarden 10:00 - 19:00 10:00 - 14:00, 20:00 - 24:00 ... 縦軸: スタッフ
横軸: 日付という構造なのですが、なんとケースによっては
横軸31日 * スタッフ数100人
なんですね...
1セル = 1 vue component だとしても 普通にやると 3,000 componentsです!
さらにテーブルの内外には様々な関連情報を表示しています(リアルタイムで人件費計算したりとか)また画面には[次の月へ] や [前の月へ] みたいな遷移ボタンがあります。
VueのSPAの場合、普通にやると [次の月へ] をクリックすると
1. 現在表示されているコンポーネントに対して Vue.$destroy を実行
2. 遷移先ページのコンポーネントをrenderingという事が行われます
現在は改善してありますが、最悪のエッジケースでは 25秒間画面が固まる(js long task)ということが起きていました.....
(chromeのプロファイラ結果のframe graphを見ると、細かい処理の山が連続しているのではなく、数個の横幅の長い処理が時間を占めていました)遅い.... こんなもの使い物にならない....
2. 解決方法を調べるため我々はジャングルの奥地へと向かった
※ ジャングル = vue関係の勉強会, Vueの本家ドキュメント, Vue forum, Vueの本家Githubリポジトリのissueなど、さらにvue関係なくブラウザのパフォーマンスに関するtips情報色々.
改善の候補として以下のものが上がりました。
- A. そもそも1画面に表示できる最大コンテンツ量を下げる(仕様変更)
- これは現在も実際の利用データを見ながらビジネス側のメンバーと検討中です。
- 実際には1ヶ月分の出勤表を俯瞰でチェックしたい、というニーズがあるため、どうしても多くのコンテンツを扱わなければならないことがわかりました。
- B. Functional componentを使う
- この記事の次のチャプターで説明します。
- C. Vue virtual scroller を使ってviewport内に表示されている部分のみ表示する
- 詳細は省きますがいくつかの懸念により、スキップしました。確実性の高い D. やE. に時間を当てるためでした。
- ただし現在試してみたいと思っています。
- D. 長いjs scriptingを避ける / 長い処理の最中はローディング中などの表示を行う (応答性を高める)
- 人間の感覚上の「速さ」に貢献したのがこのD.でした。
- 具体的には以下2つの対策を行いました
- 1度に大量のコンポーネントのレンダリングを行わない(defered的な処理を行う)
- イベントハンドラの中で長い処理を始める際は、あらかじめvueを使わずにDOMのAPIでローディングUIを表示を行う.
Vueを使っているのに直接DOM操作するのは避けたかったのですが、以下の問題が起きていたため仕方なかったのです...
- vuex stateや
$data
を更新する- → 大規模なComponent re-renderingが始まってしまう
(scripting timeが長い vue内部の処理が行われる)- → stateや$data でローディングUIを表示/非表示を行なっているとローディングUIがなかなか表示されない
- E. Vue Componentの再レンダリングやレンダリングタイミングを減らす
- D. の次に効果があったのがこのE.でした。具体的には以下の変更を行いました。
- 巨大なコンポーネント内で直接htmlレンダリングをしない.
なぜならごく小さな一箇所の変更であってもコンポーネント全体のレンダリング処理が行われるため。 変更が多い部分はコンポーネント化して再レンダリングの局所化を目指しました。- templateで参照されている
state/$data
の変更タイミングに注意する。
- 例:
this.dataA, this.dataB, ... this.dataX
および これらを参照しているComputed propertyZ
があるとします。
- 問題点: API-Aを呼び出してレスポンスデータをdadaAをセット, API-BをdataBに... とやっていると都度レンダリングが走ってしまいます。
- 解決法:
Computed propertyZ
ではなく、this.computedDataZ
というレンダリング用state dataを用意し、全てのデータがAPIから取得できた後にthis.computedDataZ
に値をセットしました。これでレンダリング回数が1回になるわけです。(もっと綺麗な方法を見つけたい)3. パフォーマンス改善としてFunctional componentを使うアプローチの感想
そもそもFunctional componentとは何でしょうか?
Functional componentを知らない人のために紹介します。
私はVue の Functional componentは 「Stateless component」という名前がふさわしいと思います。まずFunctional componentではない普通のVueインスタンスは以下の点がFunctional componentと違います。
- 普通のVueコンポーネントはインスタンス化される
- インスタンス単位でComputed propertyの結果がキャッシュされるので不要な再計算がスキップできる
- 普通のVueコンポーネントはライフサイクルをもつ
逆にいうとFunctional componentは以下のことができません。
- Computed propertyが使えない
- インスタンスが無いのでインスタンス単位での結果のキャッシュを持てない
- インスタンスを持っていないためにthisが使えない
- つまりコードの記述は通常のコンポーネントと大きく変わる
- jsコード内でもtemplate内でも使えない. methodsとかplugin/mixinも使えない
一方Functional Componentには以下のメリットがあります
- インスタンス生成時にやっている処理をスキップできる
- ライフサイクル変化の際の処理をスキップできる。
実際のコード変更時・変更後の感想
まずFunctional Componentを使って、確かに初回レンダリングは とても早くなりました!
一方、コードの変更は結構大きいものでした。変更工数と得られるパフォーマンスのコスパを考えると無条件におすすめできるものではありません。
ということでこの辺の現場感のある知見を Vue functional component advent calendar で紹介して行きたいと思います!
- 投稿日:2019-12-01T00:52:34+09:00
Laravel + Nuxtで複数画像を投稿する
概要
Nuxt + Laravel構成で複数ファイルをアップロードを行えるようにしたい
構成
Laravel 5.8
Nuxt 2.4参考資料
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (10) 写真投稿フォーム
仕様方針
- 写真の実態はS3に保存し、ファイルパスのみをDBに保存する処理方式にする。
- 今回は最大4枚まで保存できるようにする
フロント(Nuxt)の実装
今回は最大で4枚まで画像を登録できるデザインにしています。
画像表示
<!-- 商品画像 --> <div class="field image-area"> <div v-for="(file, index) in files" :key="index" class="product-image"> <img :src="file" @click="selectProductImage(index)" /> <input :id="`product_image_` + index" type="file" accept="image/png,image/jpeg,image/gif" @change="uploadProductImage($event, index)" /> </div> </div>画像選択した際に、選択した画像をプレビューさせ、登録画像を取得する
// inputタグは非表示にしており、画像イメージをクリック時にinputタグのクリックイベントを発火させる selectProductImage(index) { const input = document.querySelector('#product_image_' + index) // 既に画像が選択されているかチェックする if (input.value !== '') { // 既に画像が選択されている場合は削除する if (window.confirm('画像を削除してよろしいですか?')) { input.value = '' // 表示用の変数は指定場所の画像情報を削除して削除したところをNoImageで末尾に追加する this.files.splice(index, 1, require('@/assets/img/NoImage.png')) // 登録用の画像データは削除する this.form.images.splice(index, 1, null) } } else { // 画像選択されていない場合、選択ダイアログを表示させる input.click() } }, uploadProductImage(event, index) { // nothing to do when 'files' is empty if (event.target.files.length === 0) { return null } // ファイルのオブジェクトURLを生成する const productImageUrl = (window.URL || window.webkitURL).createObjectURL( event.target.files[0] ) // $setを利用する、Vueが監視出来る配列のメソッドを使う // https://jp.vuejs.org/v2/guide/list.html#%E9%85%8D%E5%88%97%E3%81%AE%E5%A4%89%E5%8C%96%E3%82%92%E6%A4%9C%E5%87%BA this.$set(this.files, index, productImageUrl) this.$set(this.form.images, index, event.target.files[0]) }画像を登録APIを呼び出す
async register() { this.form.shop_id = this.$auth.user.id // ファイル送信 const formData = new FormData() formData.append('shop_id', this.form.shop_id) formData.append('product_name', this.form.product_name) formData.append('category_id', this.form.category_id) formData.append('description_product', this.form.description_product) formData.append('price', this.form.price) formData.append('stock', this.form.stock) formData.append('shipping', this.form.shipping) formData.append('shipping_method', this.form.shipping_method) formData.append('shipping_origin', this.form.shipping_origin) formData.append('shipping_estimated', this.form.shipping_estimated) formData.append('shipping_cost', this.form.shipping_cost) // ファイル情報を取得する for (let index = 0; index < 4; index++) { // inputタグにファイルが設定されているかチェックを行う if (this.form.images[index] === null) { continue } // 存在すればファイル情報を登録情報に追加する formData.append('images[]', this.form.images[index]) formData.append('order[]', index + 1) } await this.$axios .$post('/api/shop/product', formData) .then(data => { this.$router.push('/shop') }) .catch(errors => {}) },バックエンド(Laravel)の実装
- 画像保存用のS3 バケットを作成し、LaravelからS3にアップロードするためのIAMユーザを作成
- envファイルに IAM ユーザーおよび S3 バケットの接続情報を記述する
- APIの実装
1. 画像保存用のS3 バケットを作成し、LaravelからS3にアップロードするためのIAMユーザを作成
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API
上記を参考に、バケット作成とIAMユーザの作成を行う
Storage::cloud()
のデフォルトをS3に設定するfilesystems.php'cloud' => env('FILESYSTEM_CLOUD', 's3'),S3 にアクセスするために必要なライブラリをインストール
composer require league/flysystem-aws-s3-v32. envファイルに IAM ユーザーおよび S3 バケットの接続情報を記述する
AWS_ACCESS_KEY_ID=アクセスキーID AWS_SECRET_ACCESS_KEY=シークレットアクセスキー AWS_DEFAULT_REGION=ap-northeast-1 AWS_BUCKET=バケット名 AWS_URL=https://s3-ap-northeast-1.amazonaws.com/バケット名/3. APIの実装
基本的には、下記のサイトを参考に実装しました。
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API
サムネイルの作成は書きを参考
LaravelでIntervention Imageを使って加工した画像をS3へ保存する
サムネイルの作成
サムネイルを作成するために
画像処理のライブラリであるIntervention Imageをサーバへインストール$ composer require intervention/imageLaravelの設定ファイルの下記を追加する
return [ ...... 'providers' => [ ...... ......, Intervention\Image\ImageServiceProvider::class, ], 'aliases' => [ ..... ....., 'Image' => Intervention\Image\Facades\Image::class, ] ]キャッシュをクリアする
$ php artisan config:clearコントローラの実装
バリデーションの実装部分
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Exceptions\HttpResponseException; class StoreProductRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'shop_id' => ['required', 'numeric'], 'product_name' => ['required', 'string', 'max:255'], 'category_id' => ['required', 'numeric'], 'description_product' => ['required', 'string', 'max:1000'], 'price' => ['required', 'numeric'], 'stock' => ['required', 'numeric'], 'shipping' => ['required', 'boolean'], 'shipping_method' => ['required_if:shipping,1', 'numeric', 'nullable'], 'shipping_origin' => ['required_if:shipping,1', 'numeric', 'nullable'], 'shipping_estimated' => ['required_if:shipping,1', 'numeric', 'nullable'], 'shipping_cost' => ['required_if:shipping,1', 'numeric', 'nullable'], 'images.*' => ['file', 'mimes:jpg,jpeg,png,gif', 'max:10240'] ]; } /** * [Override] バリデーション失敗時 * * @param Validator $validator * @throw HttpResponseException */ protected function failedValidation( Validator $validator ) { $response['success'] = false; $response['errors'] = $validator->errors()->toArray(); throw new HttpResponseException( response()->json( $response, 422 ) ); } }画像のアップロードAPIの実装
/** * 商品登録 * @param Request $request * @return \Illuminate\Http\Response */ public function create(StoreProductRequest $request) { // データベースエラー時にファイル削除を行うため // トランザクションを利用する DB::beginTransaction(); $imagePaths[] = null; // dd($request['images']); // 画像情報を取得する $images = $request['images']; $order = $request['order']; try { // 商品情報を登録する $product = Product::create([ 'shop_id' => $request['shop_id'], 'product_name' => $request['product_name'], 'category_id' => $request['category_id'], 'description_product' => $request['description_product'], 'price' => $request['price'], 'stock' => $request['stock'], 'shipping' => $request['shipping'], 'shipping_method' => $request['shipping_method'], 'shipping_origin' => $request['shipping_origin'], 'shipping_estimated' => $request['shipping_estimated'], 'shipping_cost' => $request['shipping_cost'], ]); // 並び順の要素数を初期化する $index = 0; // 対象画像が存在する場合、画像登録処理を行う if($images != null) { foreach ($images as $image) { // 投稿写真の拡張子を取得する $extension = $image->extension(); $productImage = new ProductImage(); // インスタンス生成時に割り振られたランダムなID値と // 本来の拡張子を組み合わせてファイル名とする $fileName = $product->id . $product->shop_id . $this->getRandomFileName() . '.' . $extension; // S3にファイルを保存する // 第三引数の'public'はファイルを公開状態で保存するため Storage::cloud()->putFileAs('', $image, $fileName, 'public'); // S3にアップロードしたファイルのURLをDBに保存する $productImage->product_image = Storage::cloud()->url($fileName); $imagePaths[] = $productImage->product_image; // 一時保存するためのファイル名とファイルパスを生成する $now = date_format(Carbon::now(), 'YmdHis'); $tmpFile = $now . '.' . $extension; $tmpPath = storage_path('app/tmp/') . $tmpFile; // 画像を横幅300px・縦幅アスペクト比維持の自動サイズへリサイズ $image = Image::make($image) ->resize(300, null, function ($constraint) { $constraint->aspectRatio(); }) ->save($tmpPath); // サムネイルを作成し、S3を保存する $thumbnailFileName = 'thumbnail_' . $fileName; // configファイルに定義したS3のパスへ指定したファイル名で画像をアップロード Storage::cloud()->putFileAs('', new File($tmpPath), $thumbnailFileName, 'public'); // S3にアップロードしたファイルのURLをDBに保存する $productImage->product_thumbnail_image = Storage::cloud()->url($thumbnailFileName); $imagePaths[] = $productImage->product_thumbnail_image; // 一時ファイルを削除 Storage::disk('local')->delete('tmp/' . $tmpFile); // 商品IDと表示順を設定する $productImage->product_id = $product->id; $productImage->order_number = $order[$index]; $productImage->save(); $index++; } } DB::commit(); } catch (\Exception $exception) { DB::rollBack(); foreach ($imagePaths as $imagePath) { // DBとの不整合を避けるためアップロードしたファイルを削除 Storage::cloud()->delete($imagePath); } throw $exception; } return response()->json([ 'success' => true, 'data' => $product, 'message' => '商品の登録が完了しました。', ], 201); }