- 投稿日:2019-12-18T23:27:08+09:00
VueのエラーハンドラーとAxiosで快適な例外処理を行う
こんなことありませんか?
- 例外処理忘れてて、コンソールに流れていた
![]()
- 例外発生時にアラートを表示するだけで4行も余分にコードを書かないといけなくて辛い
XXX.vuemethods: { onClick() { try { this.something(); } catch (error) { alert(error.message); } }, },Vueのエラーハンドラーについて
Vue.config.errorHandlerを使えばVueで発生するエラーをほぼキャッチできます。非同期処理内で発生したエラーはキャッチできないため、残りは
addEventListener
でキャッチします。main.ts// ほとんどのエラーをキャッチ Vue.config.errorHandler = function (err, vm, info) { // 2.2.0 以降で使用できます。 alert(err); } // 残りのエラーをキャッチ window.addEventListener("error", event => { alert(event.error); }); window.addEventListener("unhandledrejection", event => { alert(event.reason); });以下の説明にあるようにバージョンごとに仕様が異なるので注意。
Vue.config.errorHandlerはどこで発生したエラーをキャプチャできるのか - Qiitaが分かりやすくまとまってて参考になりました。
Axiosの共通処理について
Axiosの共通の処理はInterceptorsを使うと綺麗にかけます。以下の例ではrequestとresponseの時間を計測し、APIの通信に何ミリ秒かかったのかを毎回コンソールに出力します。Sentryを使うことで時間のかかってるAPIをフロント側から計測することも出来ますね。
Vueのエラー監視にSentryを使ってみた - Qiita
Repository.tsimport axios from 'axios'; const repository = axios.create({ baseURL: 'http://localhost:8080', }); repository.interceptors.request.use(request => { performance.mark('start'); return request; }); repository.interceptors.response.use( // 2XX範囲内のステータスコード (response) => { performance.mark('finish'); performance.measure('request-to-response', 'start', 'finish'); const message = `${performance.getEntriesByName('request-to-response')[0].duration}`; console.log(message); // Sentryでログを残すもよし // Sentry.captureMessage(message, Sentry.Severity.Debug); return response; }, // 2XX範囲外のステータスコード (error) => { return Promise.reject(error); } ); export default repository;結果
- 例外処理忘れても確実にアラートで表示してくれる
![]()
- 共通の例外処理については機械的なtry,catchを書かなくてよくなった
XXX.vuemethods: { onClick() { this.something(); }, },
- 投稿日:2019-12-18T23:27:08+09:00
VueのエラーハンドラーとAxiosで快適なエラー処理を行う
突然ですが、こんなことありませんか?
- エラー処理忘れてて、コンソールに流れていた
![]()
- エラー時にアラートを表示するだけで4行も余分にコードを書かないといけなくて辛い
XXX.vuemethods: { onClick() { try { this.something(); } catch (error) { alert(error.message); } }, },Vueのエラーハンドラーについて
Vue.config.errorHandlerを使えばVueで発生するエラーをほぼキャッチできます。非同期処理内で発生したエラーはキャッチできないため、残りは
addEventListener
でキャッチします。main.ts// ほとんどのエラーをキャッチ Vue.config.errorHandler = function (err, vm, info) { // 2.2.0 以降で使用できます。 alert(err); } // 残りのエラーをキャッチ window.addEventListener("error", event => { alert(event.error); }); window.addEventListener("unhandledrejection", event => { alert(event.reason); });以下の説明にあるようにバージョンごとに仕様が異なるので注意。
Vue.config.errorHandlerはどこで発生したエラーをキャプチャできるのか - Qiitaが分かりやすくまとまってて参考になりました。
Axiosの共通処理について
Axiosの共通の処理はInterceptorsを使うと綺麗にかけます。以下の例ではrequestとresponseの時間を計測し、APIの通信に何ミリ秒かかったのかを毎回コンソールに出力します。Sentryを使うことで時間のかかってるAPIをフロント側から計測することも出来ますね。
Vueのエラー監視にSentryを使ってみた - Qiita
Repository.tsimport axios from 'axios'; const repository = axios.create({ baseURL: 'http://localhost:8080', }); repository.interceptors.request.use(request => { performance.mark('start'); return request; }); repository.interceptors.response.use( // 2XX範囲内のステータスコード (response) => { performance.mark('finish'); performance.measure('request-to-response', 'start', 'finish'); const message = `${performance.getEntriesByName('request-to-response')[0].duration}`; console.log(message); // Sentryでログを残すもよし // Sentry.captureMessage(message, Sentry.Severity.Debug); return response; }, // 2XX範囲外のステータスコード (error) => { return Promise.reject(error); } ); export default repository;結果
- エラー処理忘れても確実にアラートで表示してくれる
![]()
- 共通の例外処理については機械的なtry,catchを書かなくてよくなった
XXX.vuemethods: { onClick() { this.something(); }, },
- 投稿日:2019-12-18T23:18:00+09:00
【Vue.js】タブ切り替えが出来る単一ファイルコンポーネントサンプル
はじめに
Vue.jsでタブ切り替えが出来る単一ファイルコンポーネントのサンプルを作成してみました。
v-for
v-bind
v-show
Vue.jsの基本機能である上記3点を活用しています。環境
OS: macOS Catalina 10.15.1 Vue: 2.6.10 vue-cli: 4.1.1サンプル確認用リンク
こちらから動きの確認ができます。
JSfiddle(外部サイト)へのリンク※動きの確認用のため、若干下記ソースコードと書き換えているところがあります。
ソースコード
Tab.vue<template> <div class="tabs"> <div class="btn-container"> <button v-for="(tab, index) in tabs" :key="tab.id" :class="{ active: currentTab === index }" @click="currentTab = index">{{ tab.tabName }}</button> </div> <div class="tab-content"> <div v-show="currentTab === 0"> <h1>Tab1 content</h1> <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p> </div> <div v-show="currentTab === 1"> <h1>Tab2 content</h1> <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p> </div> <div v-show="currentTab === 2"> <h1>Tab3 content</h1> <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p> </div> </div> </div> </template> <script> export default { name: 'tabs', data () { return { currentTab: 0, id: 0, tabName: "", tabs: [ {id: 1, tabName: 'Tab1'}, {id: 2, tabName: 'Tab2'}, {id: 3, tabName: 'Tab3'} ], } } } </script> <style lang="scss" scoped> .tabs { margin: 10px auto; padding: 10px; width: 80%; height: 80%; background: rgb(242, 242, 242); border-radius: 10px; } h1 p{ font-family: 'Courier New', Courier, monospace; } h1 { font-size: 20px; } p { font-size: 8px; } .btn-container { display: flex; justify-content: center; } button { width: 20%; font-size: 20px; text-align: center; margin: 10px; padding: 3px 10px; background: rgb(186, 214, 238); border-radius: 10px; } button.active{ background: lightcoral; } .tab-content div { padding: 30px; background: #ffffff; width: 80%; margin: 0 auto; } </style>おわりに
最後まで読んで頂きありがとうございました
コンポーネント単位で作成出来ると、機能が同じならスタイルを変えて流用出来ていいですね。
やっとVue.jsの良さを実感出来てきました参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-18T22:26:10+09:00
Vue初級者がPropsとv-Slotをどう使い分けるかを考えた話
はじめに
Vue.jsをある程度やり、propsで子コンポーネントにデータの受け渡しをできるようになった…。
ところでSlotてあるけど、どういうシチュエーションで使っていくのだろうか、ってのを考えていきましょう。
ついでにPropsとどう使い分けていくのかも。おさらい
まずはPropsとSlotは何なのかのおさらいをしていきましょう。
Propsとは
Vue.jsでコンポーネント間のデータの受け渡しをする上で最も基本的な機能ですね。
親側でv-bindや子側のProps名でデータを紐付けて、子側はProps内にプロパティで型とか状態を指定してって使い方。parent.vue<div> <child title="Qiita!"></child> <child :title="Vue"></child> <div> //*省略 data() { return { Vue: "Vue.js!" }; },child.vueprops:{ title:{ type:String, required: true } }上の
title="Qiita!"
は文字列を受け渡し、下の:title="Vue"
はVue
というインスタンス変数などをv-bindで受け渡ししています。Slotとは
コンポーネントの開始タグと終了タグの間に、何かしらの要素があった時に、それを子側で
<slot></slot>
と置換する機能ですね。
子側に<slot></slot>
がないと、親側でタグの間に要素があってもそれは表示されないので、表示させたい場合は子側に<slot></slot>
を指定してあげる必要があります。parent.vue<template> <child> Qiita! </child> </template>child.vue<template> <slot></slot> </template>
<child>
に囲まれたQiita!の文字列が、子の<slot></slot>
と置換され、Qiita!が表示されます。
なお、これは文字列だけではなく他のタグ要素に囲まれた物も置換されます。parent.vue<template> <child> <h1>Vue.js!</h1> <span>YES!Vue!</span> </child> </template>child.vue<template> <slot></slot> </template>
<h1>
タグと<span>
タグも、子の<slot></slot>
と置換されます。
主に親コンポーネントの要素を子要素に差し込みたいってときに使うと思います。使い分けを考える
値を渡すだけならProps
文字列なり数値なり、単にデータを受け渡すのならslotでやるよりもPropsでやるべきです。
状態で中の要素が変わるならslot
v-ifなどで中の要素の構成が動的に変わる事があり、それを子側で挟み込んでやりたい。
という場合にはslotを使ったほうが、子側にpropsを使い状態を渡してやるよりも依存関係が薄くなり、
子側のコンポーネントの再利用性が上がるので、このような場合はslotを使いましょう。parent.vue<template> <child> <template v-if="flag"> <h1>Vue.js!</h1> <span>YES!Vue!</span> </template> <template v-else> <p>Error!!!</p> </template> </child> </template>child.vue<template> <slot></slot> </template>flagの値がtrueの時は上の要素が、falseの時は下の
Error
とかかれた要素が表示されます。
また、置換したい親側の要素をVueコンポーネントにしても、それを子側のslotで置換することができます。AtomicDesignで使ってみよう
AtomicDesignを使ってコンポーネント設計を行っていた場合にでも、AtomやMoleculesなど、コンポーネントの粒度が細かい箇所でよく使う機能になります。
特にAtomレベルのコンポーネントは、それ以上機能として分けれない、親との疎結合性を保つ必要がある、
などの必要があるのと、なによりもAtomコンポーネントの中に他の要素を入れる事はAtomicDesignの考えから離れてしまいます。
なので、AtomicDesignでも要素の子に別の要素を入れたい…!という時には、slotを使って置換させる、などの手法が特に有効と思います。おわり
作成中のプロジェクトでslotを使う機会が出てきたので、今回は記事にしてみました。
propsもslotもどちらも子に何かしらの値や要素を受け渡す便利な機能ですが、
どちらも使い方によってはアンチパターンとなるので、しっかりとコンポーネントの設計を考えていく必要がありますね。
- 投稿日:2019-12-18T20:47:22+09:00
初心者によるプログラミング学習ログ 187日目
100日チャレンジの187日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
187日目は
おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) December 17, 2019
187日目
webサイトコーディング課題
udemyでvue.jsをさらっと復習
いちおう形にはなったので、メンターさんに提出してみました。#100DaysOfCode #駆け出しエンジニアとつながりたい #早起きチャレンジ
- 投稿日:2019-12-18T19:30:20+09:00
jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする
jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする
言葉だけだと何言ってるの??ってなるので
表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ
// これを const array = [ {id: 1, category: 'ねこ', name: 'ポチ'}, {id: 1, category: 'ねこ', name: 'ねこねこ'}, {id: 1, category: 'いぬ', name: 'にゃん'}, ]; // ↓↓↓ こうしたい ↓↓↓ const string = 'ポチ, ねこねこ, にゃん';結論
function(array) { // 配列からnameだけ取り出して「,」区切りの文字列にする return array.map( (row) => {return [row['name']]} ).join(',') },分解
function(array) { // 配列からkeyのnameだけ取り出した配列を作る const namesArray = array.map((row) => { return [row['name']] }); // 配列を「,」区切りの文字列にする const namesString = nameArray.join(','); },Vue.jsで実際に使った時の用途
<template> <div> <span> <!-- 「ねこ, にゃん, ぽち」 のように表示する --!> {{ array | convertArrayToString }} </span> </div> </template> <script> data: { array: array }, filters: { convertArrayToString(array) { // 配列からnameだけ取り出して「,」区切りの文字列にする return array.map( (row) => {return [row['name']]} ).join(',') }, } </script>
- 投稿日:2019-12-18T19:30:20+09:00
jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする
jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする
言葉だけだと何言ってるの??ってなるので
表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ
// これを const array = [ {id: 1, category: 'ねこ', name: 'ポチ'}, {id: 1, category: 'ねこ', name: 'ねこねこ'}, {id: 1, category: 'いぬ', name: 'にゃん'}, ]; // ↓↓↓ こうしたい ↓↓↓ const string = 'ポチ, ねこねこ, にゃん';結論
function(array) { // 配列からnameだけ取り出して「,」区切りの文字列にする return array.map( (row) => {return [row['name']]} ).join(',') },分解
function(array) { // 配列からkeyのnameだけ取り出した配列を作る const namesArray = array.map((row) => { return [row['name']] }); // 配列を「,」区切りの文字列にする const namesString = nameArray.join(','); },Vue.jsで実際に使った時の用途
<template> <div> <span> <!-- 「ねこ, にゃん, ぽち」 のように表示する --!> {{ array | convertArrayToString }} </span> </div> </template> <script> data: { array: array }, filters: { convertArrayToString(array) { // 配列からnameだけ取り出して「,」区切りの文字列にする return array.map( (row) => {return [row['name']]} ).join(',') }, } </script>
- 投稿日:2019-12-18T19:28:13+09:00
【Vue.js】メソッド vs computedプロパティ(算出プロパティ) サンプル
メソッドとcomputedプロパティの違いに関する記述はすでにたくさんあるので、今回は個人的にわかりやすいと思った、これらの違いを説明したコードを紹介したいと思います。
簡単に違いと使い分けについて
Computedプロパティとmethodの違い
算出プロパティとメソッドの違いは、“算出プロパティが処理の結果をキャッシュする”ことです。
算出プロパティがキャッシュした結果を更新するのは、依存するデータが変更されたときです。
一方、メソッドは常に処理が実行されます。Computedプロパティのメリット・メソッドとの使い分け
結果が同じなら、常にメソッドを使えば良いと思われるかもしれません。
算出プロパティのメリットは、結果をキャッシュしているため処理が高速であるという点です。
例えばリストをソートする処理を作成するときに、メソッドで実装するとソートするごとに毎回処理が実行されます。
ソートした結果を算出プロパティとして用意すれば、依存するデータが更新されるまでソート結果をキャッシュするため、処理が速くなります。参照
【Vue.js】算出プロパティとメソッドの違い。どう使い分ければ良いの?サンプル
参照
Vue JS 2 Tutorial #9 - Computed Properties
*コード
【メソッドの場合】index.html<div id="vue-app"> <h1>Computed Properties</h1> <button v-on:click="a++">Add to A</button> <button v-on:click="b++">Add to B</button> <p>A - {{ a }}</p> <p>B - {{ b }}</p> <p>Age + A = {{ addToA() }}</p> <p>Age + B = {{ addToB() }}</p> </div>vue.jsnew Vue({ el: "#vue-app", data: { age: 20, a: 0, b: 0 }, methods: { addToA: function() { console.log('addToA') return this.a + this.age; }, addToB: function() { console.log('addToB') return this.b + this.age; } } });【computedプロパティの場合】
index2.html<div id="vue-app"> <h1>Computed Properties</h1> <button v-on:click="a++">Add to A</button> <button v-on:click="b++">Add to B</button> <p>A - {{ a }}</p> <p>B - {{ b }}</p> <p>Age + A = {{ addToA }}</p> <p>Age + B = {{ addToB }}</p> </div>vue2.jsnew Vue({ el: "#vue-app", data: { age: 20, a: 0, b: 0 }, computed: { addToA: function() { console.log('addToA') return this.a + this.age; }, addToB: function() { console.log('addToB') return this.b + this.age; } } });コードだけ見ると、ほとんど変わらないことがわかるかと思います。
変わる箇所といえば、htmlのマスタッシュ内の書き方くらいです。<p>Age + A = {{ addToA() }}</p> <!-- メソッド --> <p>Age + B = {{ addToA }}</p> <!-- computedプロパティ -->ディベロッパーツールで確認
しかし、ディベロッパーツールでそれぞれコンソールで確認してみてください。
(コード内にconsole.logを書いたのもそのためです)「Add to A」、「Add to B」 いずれかのボタンをクリックすると、メソッドで書いたプログラムの場合(今回は「Add to A」をクリック)、AddToA、AddToB いずれも実行されているのがわかります。
一方computedプロパティで書いたプログラムの場合、「Add to A」ボタンをクリックすると、AddToA のみが実行されています。
まとめ
上記のサンプルコードで、computedプロパティの特徴である「依存するデータが変更されたときのみ処理が行われる」の部分が確認できたかなと思います。
- 投稿日:2019-12-18T19:23:36+09:00
Vue + Expressのテンプレート作成メモ
はじめに
バックエンドをNode.js、フロントエンドをVue.jsでwebアプリを開発することが増えたので簡単にひな形を作る手順を残しておこうと思います。
手順
expressのプロジェクト作成
express プロジェクト名
vueのプロジェクトを作成
expressで作ったプロジェクトのルートディレクトリへ移動し
vue create public
publicというタイトルになっているので、気になる方はvueプロジェクトのindex.htmlのtittleタグを編集vueプロジェクトでビルドする
publicディレクトリへ移動し
npm run build
expressのapp.jsに記述されているpublicのパスを変える
app.use(express.static(path.join(__dirname, 'public’)));
↓
app.use(express.static(path.join(__dirname, 'public/dist’)));
一応この記述も消しておく
// view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade');node.jsプロジェクトでサーバー起動
npm start
アクセスするとVueの初期画面が表示される
localhost:3000
おわりに
さくっと雛形を作ってすぐに開発に取り掛かりましょう!
- 投稿日:2019-12-18T18:46:23+09:00
【Nuxt.js】slot基礎編:コンポーネントに自由にデータを渡そう
前置き
Nuxt.jsと言えばコンポーネント。
componentの内容を、ページによって少しで良いから変えたい…
そんな時、slotに随分と助けられたので記録に残しました。
Vue.jsでも同様の使い方が可能です。
※サンプルテキストはHello Nuxt.js!ではなくHello Vue.js!になっていますが、ご了承ください。間違い等がございましたら、優しくご指摘いただけると嬉しいです!
基礎編:超簡単な解説から実践編まで
応用編:全体のまとめ、propsとの違い
を書いていきます。slotって?メリットは?
ウェブサイトを作ってて、
「同じコードをコピペしまくってめんどいなぁ〜
あ!コンポーネント使えばいいじゃん!!
あれ?でもページによって○○だけ変えたいんだよなぁ」
って時、ありますよね?コンポーネントを使いまわしたいけど、
ここの文字だけ変えたい!って時に使います。
(文字以外も変更は利きますが、今回はわかりやすく文字で例えます)コンポーネントが使えないから、
基本のコードコピペして中身だけ編集する…
なんて面倒なことをしなくていいんです!
コンポーネントにしちゃって、一部だけ変えればいいんです!
楽ちん!!slotを使わない場合どうなるの?
じゃあコンポーネントを使って…親コンポーネントで中身を変えよう!
slotなくてもの中身に書けばよくない?
と思ったそこのアナタ!
こちらをご覧ください。Component.vue // 子コンポーネント <div class="ItemTitle"> <h1>ここは親ページによってタイトルを変えたい</h1> </div>index.vue // 親ページ <Component> Hello Vue.js! </Component>表示結果 <div class="ItemTitle"> </div>そうなんです。。。
子のコンポーネントの中身は無視されちゃいます。
それも完全なシカトです。ツライ。。。<Component>ここは無視される</Component>slotの使い方(超簡単ver.)
そこでslot君の登場です!!
使い方は超〜〜〜簡単。
子コンポーネント内に変えたい部分をslotにするだけ。
上の説明に使ったコードでやってみます。<div class="itemTitle"> <h1>Hello Vue.js!</h1> </div>【コード】
子コンポーネントだけslotを加えます。
Component.vue // 子コンポーネント <div class="itemTitle"> <h1> <slot /> // ここは親ページによってタイトルを変えたい </h1> </div> <style scoped> .itemTitle { margin: 10px; } h1 { color: rgb(65, 193, 222); } </style>親は一緒。
index.vue // 親ページ <Component> Hello Vue.js! </Component>これだけで表示結果が変わる!!
slotくんに呼びかけて、ちゃんとデータを表示してくれます。
偉いぞslotくん!!!slotの使い方(複数ver.)
便利なslotくん
「じゃあいっぱいslot使おう!
h1とh2にも使おう〜」
って場合は
各slotに名前をつけてあげる必要があります。「これ表示して?」って言われても、
名前で呼んでくれないと、
どのslotくんが表示すればいいのか分からなくなっちゃいますよね。そういう先輩いません?
「これやっといて〜」って、
今誰に言ったの?私やるの???え、誰???
みたいな。なので、
もし名前をつけないでslotを2つ用意すると
親に入れたものが2回表示されます。
「はいはい、私やりますね」って2人反応しちゃいます。名前で呼ばなかった先輩はきっと
同じ仕事を無駄に2個やらせてしまったことを後悔するでしょう。
こんなにいらんわ。。。って。<div class="ItemTitle"> <h1> Hellot Vue.js! about slot </h1> <h2> Hellot Vue.js! about slot </h2> </div>【コード】
Component.vue // 子コンポーネント <div class="ItemTitle"> <h1> <slot /> // ここは親ページによってタイトルを変えたい </h1> <h2> <slot /> // ここは親ページによってサブタイトルを変えたい </h2> </div> <style scoped> .itemTitle { margin: 10px; } h1 { color: rgb(65, 193, 222); } h2 { color: gray; } </style>index.vue // 親ページ <Component> Hellot Vue.js! about slot </Component> 表示結果 <div class="ItemTitle"> <h1> Hellot Vue.js! about slot </h1> <h2> Hellot Vue.js! about slot </h2> </div>slotの使い方(実践ver.)
ということで各slotくんに名前をつけてあげましょう。
「◯◯くん、これ表示しといてよ」って言ってくれたら、
はい、私ですね!表示しますね!ってなりますよね。ここで1つ注意。
名前をつける場合は、
親でタグで囲う必要があります。
これさえできればもう完璧。<div class="itemTitle"> <h1 class="title">Hello Vue.js!</h1> <p>slotを学ぼう</p> <div class="catchCopy"># slotって?メリットは?</div> </div>【コード】
ItemTitle.vue // 子コンポーネント <div class="itemTitle"> <h1> <slot name="title" /> // 置き換えたい部分のslot nameを決める </h1> <p>slotを学ぼう</p> <h2> <slot name="catchCopy" /> // 置き換えたい部分のslot nameを決める </h2> </div> <style scoped> .itemTitle { margin: 10px; } h1 { color: rgb(65, 193, 222); } p { color: gray; h2 { color: rgb(76, 212, 227); } </style>index.vue // 親ページ <ItemTitle> <template v-slot:title> Hello Vue.js! // slot name="title"を置き換える </template> <template v-slot:catchCopy> # slotって?メリットは? // slot name="catchCopy"を置き換える </template> </ItemTitle>おめでとうございます!
これでVue.js、Nuxt.jsのslot、今日から使えますね!わーい!
次は更なる応用も記載していきます★
- 投稿日:2019-12-18T17:51:06+09:00
Nuxt.jsとmysqlを連携してデータを表示してみた
はじめに
前から気になっていたNuxt.jsを触ってみました。これ使って何かやってみようと考えた結果、
mysqlと連携させてみることにしました。Nuxt.jsに関して入門的な情報は既にいろいろと存在するので、
ここでは、そのつなぎの部分にフォーカスして記載したいと思います。内容
下記の項目に沿って記載します。
- Nuxt.jsについて
- Nuxt.jsのインストール
- データベース準備
- 必要なモジュール
- APIの実装
- フロントエンドの実装
- 結果
- おわりに
Nuxt.jsについて
ご存知の方には恐縮ですが、Nuxt.jsはナクストと読むようであり、
Webアプリケーションを開発するのに必要なものが既にある程度整った状態で
スタートできるVue.jsベースのJavaScriptフレームワークです。
UIではVue.jsを使いますが、UI以外の機能についてもいろいろ組み込まれているようです。
フレームワークなので効率的にアプリケーションの開発ができるようですね。Nuxt.jsのインストール
早速、自分のPCにインストールしてみます。
事前にnpm と Node.jsがインストールされていることが前提です。npx create-nuxt-app プロジェクト名この形式でインストールするようなので、
npx create-nuxt-app prjプロジェクト名をprjとして実行してみました。
いくつか聞かれるので、
- server framework を Express
- Nuxt.js module をAxios
- rendering mode をUniversal (SSR)
として設定しました。
何度も繰り返し試した結果、上記の設定に落ち着きました。
Nuxt v2.10.2 がインストールされたようです。次に、アプリを起動する必要があるので、
npm run devnpm run スクリプト名ここで、上記の形式で入力すると、package.json(下記)で記述されているscripts内のスクリプト名
(この場合は"dev")で定義されたコマンドが実行されます。{ "name": "prj", "version": "1.0.0", "description": "My swell Nuxt.js project", "author": "**** ****", "private": true, "scripts": { "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", "build": "nuxt build", "start": "cross-env NODE_ENV=production node server/index.js", "generate": "nuxt generate" }, "dependencies": { "@nuxtjs/axios": "^5.3.6", "cross-env": "^5.2.0", "express": "^4.16.4", "fs": "0.0.1-security", "mysql": "^2.17.1", "net": "^1.0.2", "nuxt": "^2.0.0", "tls": "0.0.1" }, "devDependencies": { "nodemon": "^1.18.9" } }下記URLをブラウザで表示すると、
http://localhost:3000こんな感じに初期表示されました。
Nuxt.jsのデフォルトポートは3000のようです。
インストールされたあとのディレクトリ構造は下記のようになってました。prj----assets |--components |--layouts |--middleware |--node_modules |--pages |--plugins |--server |--static |--store |nuxt.config.js |package.json調べたところ、最初に表示されたページは、
prj-- |--pages |index.vuepagesディレクトリ内にあるindex.vueに記載された内容でした。
<template> <div class="container"> <div> <logo /> <h1 class="title"> prj </h1> <h2 class="subtitle"> My laudable Nuxt.js project </h2> <div class="links"> <a href="https://nuxtjs.org/" target="_blank" class="button--green" > Documentation </a> <a href="https://github.com/nuxt/nuxt.js" target="_blank" class="button--grey" > GitHub </a> </div> </div> </div> </template> <script> import Logo from '~/components/Logo.vue' export default { components: { Logo } } </script> <style> .container { margin: 0 auto; min-height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; } .title { font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; display: block; font-weight: 300; font-size: 100px; color: #35495e; letter-spacing: 1px; } .subtitle { font-weight: 300; font-size: 42px; color: #526488; word-spacing: 5px; padding-bottom: 15px; } .links { padding-top: 15px; } </style>ごちゃごちゃ書かれているので、余計な記述をバッサリ削除してコードを見やすくします。
<template> <div class="container"> あいうえお </div> </template> <script> </script> <style> .container { margin: 0 auto; min-height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; } </style>表示は、下記のようになりました。
ここで気づいたのが、このindex.vueファイルを更新した場合、
ブラウザで再描画しなくても(F5押さなくても)自動でブラウザに反映されることです。
index.vueファイルを更新するたびに、自動でコンパイルが走るようで、
それが終わり次第ブラウザの表示が変更されるようです。データベース準備
mysqlと連携させて何かしたいな?と考えたところ、
一番簡単なお試し内容として、DBに登録されているデータを取り出して、
それをブラウザに表示させてみるのがよさそうかなと思い、それを実装
してみることにしました。なので、まずデータベースの準備をします。ローカル環境のmysqlにテスト用のデータベースを作成し
その中に都道府県情報のテーブルを作成しました。
データベース名:testdb
テーブル名:prefectures
この情報を取り出してブラウザに表示させてみようと思います。必要なモジュール
mysqlを使うには、どうやらmysql用のモジュールをインストールする必要が
あることがわかりましたので下記を実行します。npm install --save mysqlAPIの実装
index.vueの中のscritpt要素の中に、データベースアクセスの為の記述をしても
うまくいかずエラーを解消することができなかったので、
別途APIを作成し、そこにアクセスすることでDBの内容をとってくる、
という方法にしました。prj-- |--server |api.jsserverディレクトの直下にapi.jsというファイルを作成し、
dbに接続してデータを取ってくるコードを記述します。const express = require('express') const router = express.Router() router.get('/prefectures', (req, res, next) => { const mysql = require('mysql'); const connection = mysql.createConnection({ host : 'localhost', user : 'testuser', database: 'testdb', password: 'testuser' }); var ret=[]; connection.connect(); connection.query('SELECT * from prefectures;', function(error, row, fields){ if (error) { console.log(error); } var dat = []; for (var i = 0;i < row.length; i++) { dat.push({id: row[i].id, name: row[i].name}); } ret = JSON.stringify(dat); res.header('Content-Type', 'application/json; charset=utf-8') res.send(ret) }); connection.end(); }) module.exports = router実はこれだけではだめなようで、同じディレクトリに存在するindex.jsというファイルを
一部編集しないといけないようです(★印の箇所)const express = require('express') const consola = require('consola') const { Nuxt, Builder } = require('nuxt') const app = express() // Import and Set Nuxt.js options const config = require('../nuxt.config.js') config.dev = process.env.NODE_ENV !== 'production' //↓↓↓★この行の追加 const apiRouter = require('./api') async function start () { //↓↓↓★この行の追加 app.use('/api', apiRouter) // Init Nuxt.js const nuxt = new Nuxt(config) const { host, port } = nuxt.options.server // Build only in dev mode if (config.dev) { const builder = new Builder(nuxt) await builder.build() } else { await nuxt.ready() } // Give nuxt middleware to express app.use(nuxt.render) // Listen the server app.listen(port, host) consola.ready({ message: `Server listening on http://${host}:${port}`, badge: true }) } start()下記でアクセスするとDBからのデータをJSON形式でとってこれるようになりました。
http://localhost:3000/api/prefectures参考サイト
https://dev.classmethod.jp/etc/node-js-module-mysq/
(他にもいくつか参考サイトがあったのですが忘れてしまった。。)フロントエンドの実装
axiosというライブラリを使ってhttp通信を行います。
Node.jsで動作するhttpクライアントであり、通信するためのAPIが提供されているので
これを利用します(Ajaxのようなものですね。多分)上記でばっさり削除したindex.vueの中を下記のように変更します。
先ほどのAPIにアクセスして取ってきたデータをitemsの中に格納すると、
Vue.jsによってフロントエンドに反映されます。<template> <div class="container"> <table border="1"> <tbody> <tr> <th>ID</th> <th>都道府県名</th> </tr> <tr v-for="item in items" :key="item.id"> <td>{{ item.id }}</td> <td>{{ item.name }}</td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { items: [] } }, mounted: function() { this.$axios .$get('/api/prefectures') .then(response => { this.items = response }) .catch(error => { console.log(error) }) } } </script> <style> .container { margin: 0 auto; min-height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; } </style>結果
表示は、下記のようになりました。
おわりに
今回は、mysqlとの連携に注目しましたが、Nuxt.jsに関する知識が浅いこともあり、
もっといろんなこともできると思われますので、今後も引き続きいろいろ試してみたいと思います。
私はPHPを使っての作業が多いので、JavaScriptだけで当たり前のようにWebアプリケーションを
作れるようなったことに関心を寄せています。
試した内容は大したものではないですが、それでも途中いくつもの壁にぶち当たりました。
が、なんとか乗り越えました。
今後はよりスムーズに実装できるよう一層進化していくことと思います。
- 投稿日:2019-12-18T15:23:16+09:00
【vue】Vue Styleguidistの使い方を説明① 〜Laravel + vue環境でVue Styleguidistを動かす〜
※元々ブログに書いていたのですが、qiitaに転載しました
https://www.moyashidaisuke.com/entry/vue-styleguidist-install概要
vueのstyleguild「Vue Styleguidist」をLaravel + vue環境で使い始めてみました。
思ったより手間取ってしまったので、設定ファイルや、私の環境で発生したエラーの対応等を残しておきます。↑のカメレオンはVue Styleguidistのロゴ。きもかわいい。
※Laravel環境じゃなくても多分参考になると思います。
Vue Styleguidistとは
とりあえず動作サンプル見た方がわかりやすいのでいきなりですがリンクを。
https://vue-styleguidist.github.io/basic/
こういうコンポーネントの仕様と、サンプルのドキュメントを生成してくれるツールです。 こういうの。
GitHubのStar数はこの記事を書いてる時点で1419なので、デファクトになってる感はまだ無いですが、競合のvueseよりはstar数多いのでこちらを採用しました。
https://github.com/vue-contrib/vuese導入手順(理想形
特にはまらないですんなりいくパターン。
https://vue-styleguidist.github.io/docs/GettingStarted.html#_1-install
前提
- Laravel + vue環境導入済み
- npmじゃなくてyarn
- Vue CLIは使ってない
公式手順だとnpmですが、私の環境ではyarnを使っているのでyarnの手順を紹介します。
インストール
$ yarn add -D vue-styleguidist 〜色々インストールされる。省略〜 Done in 296.61s.style guildの設定
公式手順だとリンクが2つ貼ってあります。
https://vue-styleguidist.github.io/docs/Components.html#finding-components
https://vue-styleguidist.github.io/docs/Webpack.htmlいきなりぶん投げられてわかんないですが、プロジェクトルートに
styleguide.config.js
というファイルを作成してください。中身は
const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { webpackConfig: { module: { rules: [ // Vue loader { test: /\.vue$/, exclude: /node_modules/, loader: 'vue-loader' }, ] }, plugins: [ new VueLoaderPlugin() ] }, // vueファイルへのpathを指定 components: 'resources/js/components/**/[A-Z]*.vue', }で私の環境だといけました。
ポイントとしては、
webpackConfig
はlaravel mix
の設定を流用しても全く動かないです。(あれはmixで動的にconfigを生成したりしてるので)また、
vue-loader
の設定もちゃんと書いてあげないと動かないです。デフォルトで呼んでくれたりはしないようです。あと、vueのファイルはLaravelだと普通
resources/js
以下で作成してる事が多いと思いますが、適時調整してください。package.jsonにコマンド追加
これは公式そのままで大丈夫です。
{ "scripts": { + "styleguide": "vue-styleguidist server", + "styleguide:build": "vue-styleguidist build" } }実行
hot reload版
yarn run styleguideサーバが立ち上がってlocalhost:6060でつなげるようになります。vagrantやDocker等の仮想環境を使っている方はポートの設定をしてください。
私はdocker-composeを使っていたので
ports: - 6060:6060 # styleguideを追加しました。
htmlとjs吐き出す版
yarn run styleguide:buildstyleguideというディレクトリにhtmlとjsが吐き出されますので、htmlを開けばOKです。
エラー色々
styleguide.config.jsの設定系
componentsへのpathがおかしい
画面を開くとこれが表示されるパターン。
Welcome to Vue Styleguidist! We couldn’t find any components using these patterns: src/{components,Components}/**/*.vue Create styleguide.config.js file in your project root directory like this: module.exports = { components: 'src/components/**/*.vue' }; Read more in the locating components guide.componentsの設定を自分の環境に合わせればOK。
Failed to compile
Failed to compile ./resources/js/components/XXXXX.vue 1:0 Module parse failed: Unexpected token (1:0) You may need an appropriate loader to handle this file type.
vue-loader
の設定をいれてあげればOK。vueとvue-template-compilerのバージョン違い
Failed to compile ./resources/js/components/XXXXXX.vue (./node_modules/vue-styleguidist/loaders/vuedoc-loader.js!./resources/js/components/XXXXXX.vue) Error: Vue packages version mismatch: - vue@2.6.8 (/var/www/node_modules/vue/dist/vue.runtime.common.js) - vue-template-compiler@2.6.10 (/var/www/node_modules/vue-docgen-api/node_modules/vue-template-compiler/package.json) This may cause things to work incorrectly. Make sure to use the same version for both. If you are using vue-loader@>=10.0, simply update vue-template-compiler. If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.バージョン合わせないとダメらしい。
yarn.lockを確認するとvue-template-compiler@^2.0.0: version "2.6.10" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg== dependencies: de-indent "^1.0.2" he "^1.1.0"となっており、
^2.0.0
2以上を使ってね、の指定で2.6.10
をinstallしちゃってる。というわけで、無理やり
2.6.8
を入れてみる。$ yarn add vue-template-compiler@2.6.8 yarn add v1.13.0 info Direct dependencies └─ vue-template-compiler@2.6.8 info All dependencies └─ vue-template-compiler@2.6.8 Done in 223.61s.これで
2.6.8
入ったと思いきや、2.6.10
も入ったままなのでダメ。yarn.lockはこんな状態
vue-template-compiler@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c" integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng== dependencies: de-indent "^1.0.2" he "^1.1.0" vue-template-compiler@^2.0.0: version "2.6.10" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg== dependencies: de-indent "^1.0.2" he "^1.1.0"installしたりremoveしてもダメだったので、最終手段でyarn.lockを直接書き換える。
vue-template-compiler@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c" integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng== dependencies: de-indent "^1.0.2" he "^1.1.0" vue-template-compiler@^2.0.0: version "2.6.8" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c" integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng== dependencies: de-indent "^1.0.2" he "^1.1.0"$ yarn installこれで動きました。
issueはちょいちょい見かけるのだけど、ちゃんとした対応方法は不明。誰か知ってたら教えて下さい。
https://github.com/vuejs/vue/issues/3941
次
実際のドキュメントを生成したり、パラメータの解説したりの予定。
第2段
https://www.moyashidaisuke.com/entry/vue-styleguild-sections
- 投稿日:2019-12-18T14:29:39+09:00
【Nuxt.js】Nuxtプロジェクトにpug/scss/coffeescriptを導入する
はじめに
Nuxt.jsは優れたWebアプリケーションフレームワークです。
しかし、デフォルトのhtml/css/jsで記述するのは労力がかかります。
せっかくなので、pug/scss/coffeeを使って時短を図りましょう。導入方法
以下のコマンドをnuxtプロジェクトのディレクトリで実行するだけです。
$ npm i -D pug pug-plain-loader coffeescript coffee-loader node-sass sass-loaderこれで、pugとscssは記述できるようになりましたが、coffeescriptを使うにはもう一つ作業が必要です。
アプリディレクトリ内のnuxt.config.js
の下の方にあるbuild
を次のように変更してください。nuxt.config.js/* ** Build configuration */ build: { /* ** You can extend webpack config here */ extend (config, ctx) { // -------- ここから ---------- config.module.rules.push({ test: /\.coffee$/, use: 'coffee-loader', exclude: /(node_modules)/ }) // -------- ここまで ---------- } }これで、pug/scss/coffeescriptが使えるようになったはずです。適当なvueファイルを作って正常に動作するか確認してみましょう。
sample.vue<template lang='pug'> div h1 {{ msg }} </template> <script lang='coffee'> export default data: -> msg: 'Hello world!' </script> <style lang='scss'> h1 { color: blue; } </style>
- 投稿日:2019-12-18T14:24:07+09:00
Vue Fes Japan 2019 公式サイトから学ぶ CSS コーディングの知見
この記事は CSS Advent Calendar 2019 の 20日目の記事です。
Vue Fes Japan 2019 が開催中止になってしまい、大変残念だったのですが...
(私は Vue.js 日本ユーザーグループのコアスタッフで、今回は Web サイト制作班として少しコミットしていたのでした)先日公式サイトのソースコードが無事公開されました
![]()
そこで、Web サイト制作で得た CSS の知見がとてもためになったので、いくつかご紹介していきたいと思います。Vue Fes Japan 2019 公式サイト
公式サイト: https://vuefes.jp/2019/
GitHub: https://github.com/vuejs-jp/vuefes-20192019 の公式サイトも、2018 の公式サイトと同様に、Nuxt.js の静的ファイルの生成機能を使って作成しています。
CSS の構成について
- normalize.css を使っている
- SCSS を使っている
- グローバルな SCSS を利用している
- さらに、Nuxt Style Resources を利用して、SCSS の変数をグローバルで共有している
- 各コンポーネントでは Scoped CSS を利用している
このように、「グローバルな CSS」と「閉じられた CSS(Scoped CSS)」で構成されています。
ちなみにこのプロジェクトでは UI フレームワークや CSS フレームワークは使っていません。
コーディングするときにとても嬉しかったポイント
Web サイトデザイン主担当の沖さん(@448jp)が、Web サイトの仕様をドキュメント化して共有してくれました。
これがとても詳細まで定義されており、ブレークポイントごとの余白や、フォント・色などを CSS プロパティで指定してくださったおかげで、悩まずにすばやく実装することができました。
HTML/CSSを書く人はエスパーではないので、PSDやXDで配置されたオブジェクトのマージンがpxなのか、%なのか、vwなのかは読み解けない。ましてやブレークポイントごとに変わる場合はなお。デザイナーは見た目だけ作って終わりではなく、デザインの意図と仕様を実装者に伝えるところまで仕事としたい。
— 沖 良矢™ (@448jp) November 13, 2019つよい...
![]()
私は普段デザインカンプをもらってコーディングをするとき、ブレークポイント付近のデザインをエスパーしてコーディングしたり、仕様を自分で決めたりしていたのですが、そうするとデザイナーさんと齟齬が生まれたり手戻りが発生したりして、開発に余計な時間がかかってしまうことがあります。
沖さんのようなデザイナーがいる環境というのは、なかなか恵まれていると思うのですが、Web サイトをデザインする人は詳細な仕様を明文化することで、実装側がとても助かるということを知ってもらえると良いなと思いました
CSS コーディングの知見
グローバルな CSS の設計について
CSS の各レイヤーをディレクトリやファイルごとに分類することで、どんな意味を持った CSS をどこに配置するか、共同開発する際にメンバーと認識が合うようになります。
- foundation
- colors.scss ... 色の変数
- reset.scss ... normalize.css によってブラウザごとの差異を整えた後に、さらにリセットしたいブラウザのデフォルトのスタイル
- typography.scss ... タイポグラフィのスタイル
- variables.scss ... 共通化して使える変数
- main.scss ... 各 SCSS ファイルを import しているファイル
- media_queries.scss ... メディアクエリによって要素を表示させるか否かを制御できるクラス
- utilities.scss ... 各画面で共通して使えるクラス
また、foundation 配下の CSS を開発初期の段階で固めておくことで、CSS フレームワークを使わずとも統一感のとれた CSS を書いていくことができます。
ブレークポイントの名称は
sp
pc
ではなくsm
md
lg
を使う私は今まで、ブレークポイントの切り替えで使う変数名の表記には
sp
pc
を使っていたのですが、この 2パターンだと中間層のサイズのブレークポイントを書きたい際に困ることがありました。ブレークポイントの名前をサイズの名前にすると、中間層のサイズも柔軟に表すことができるようになるのでオススメです。
$layout-breakpoint--is-small: 768px !default; $layout-breakpoint--is-small-up: 769px !default; $layout-breakpoint--is-medium: 980px !default; $layout-breakpoint--is-medium-up: 981px !default;
sm
(モバイル)サイズの要素の指定を相対指定にするモバイル端末の違いによって、文字やコンテンツの幅が大きく変わってしまう問題については、サイズを相対指定(
vw
)にすることで解決しています。文字やコンテンツの幅を相対指定にすることで、iPad と iPhone SE というような画面幅が違う端末でも、画崩れを起こさずに表現することができます。
ちなみにデスクトップサイズ以上の文字については、固定で指定しています(さすがにラップトップと大きいサイズのモニターでは幅が違いすぎるため)
計算が簡単なカードリストレイアウトの実装方法
SPONSORS セクションでは、スポンサープランごとに 1列に並ぶカードの幅を変える必要がありました。
- ゴールドは 1列 4カード、シルバーは 1列 5カード... というように並ぶ
- それぞれ 20px のガター(余白)がついている
当初、これを実装するために考えていたこととしては、
- スポンサープランごとのカードの幅を計算する
- カードの
margin-right
とmargin-bottom
に 20px を設定する- 右端に来る n 番目のカードは、
margin-right: 0;
を設定する
- n 番目の n の値は、スポンサープランごとに変わる
というようなことを書こうとしましたが、計算と実装が複雑になるため、別の方法をとることにしました。
計算が簡単になる方法として、ネガティブマージンと
calc()
を使うやり方があります。template<ul v-for="sponsorPlan in sponsorPlansHavingSponsors" :key="sponsorPlan.plan" > <li class="sponsor-group" :class="sponsorPlan.plan"> <h3 class="sponsor-plan"> {{ sponsorPlan.name }} </h3> <ul> <li v-for="sponsor in sponsorsByPlan(sponsorPlan.plan)" :key="sponsor.sys.id" class="sponsor" > <!-- ... --> </li> </ul> </li> </ul>scss.sponsor-group { ul { display: flex; // flexbox で実装する flex-wrap: wrap; // アイテムがはみ出したときに折り返すようにする justify-content: center; // 中央揃えにする width: calc(100% + 20px); // ?幅を左右のガター分プラスする margin: -10px; // ?ネガティブマージンを使って余白を広めにとる } .sponsor { margin: 10px; // ?ガターの半分の幅を指定する(要素が隣り合うとガターの幅になる) width: calc((100% / 5) - 20px); // 普通サイズ(シルバー)の Sponsor バナーの横幅 } } .sponsor-group.bronze { .sponsor { width: calc((100% / 6) - 20px); // ブロンズのバナーのときの横幅 } } .sponsor-group.gold, .sponsor-group.special { .sponsor { width: calc((100% / 4) - 20px); // ゴールド、スペシャルのバナーのときの横幅 } }まず
.sponsor-group
にネガティブマージンを使って、ガターの幅分だけ要素の幅を広めに指定します。次に、カードの上下左右にガター分の
margin
を指定します(このときガターの半分の幅を指定することで、カードが隣り合うとガターの幅になります)そして、カードの幅を
calc()
で計算して、flexbox で要素を並べると、簡単にカードリストが出来上がります。ちなみにカードの幅の
calc((100% / 5) - 20px);
という部分は、
(sponsor-group の幅 / 1列に入るカードの数) - ガターの幅
という計算式になります。
こうするとことで、
1列に入るカードの数
を変更すればカードの幅が変わるので、とても計算が簡単になります。vuefes-2019/TheSponsorListSection.vue at master · vuejs-jp/vuefes-2019
↑のコードは TheSponsorListSection の簡略版ですが、コードだけではネガティブマージンをとっているあたりが分かりにくいと思うので、CodePen を用意しました。
See the Pen CSS coding for card list layout by rry (@ryamakuchi) on CodePen.
https://codepen.io/ryamakuchi/pen/LYExRpa
- group
- ネガティブマージンを
width
を使って、全体的にガターの幅分を広めにとる- card
- 上下左右にガターをつけて、
calc()
で幅ぴったりにする当初考えていた、左側と下にガターをつける方法と比べると、右端に来る n 番目のカードのスタイルの上書きをするということがなくなるため、計算が簡単になります。
補足:ちなみに
calc()
は、IE 10 / IE 11 では一部うまく機能しない場合もあるため、実装するときは気をつけてください。まとめ
CSS の設計をきちんと行うことで、フレームワークを使うことなく、メンテナンスしやすい&共同開発しやすいサイト作成ができました。
もし CSS / UI フレームワークを使うことを検討しているのであれば、
create-nuxt-app で利用できる UI フレームワークを比較する - Qiita
も、あわせてご覧いただければ幸いです。
メリークリスマス!
![]()
- 投稿日:2019-12-18T14:23:03+09:00
v-date-pickerをカスタマイズしてbirthday-pickerを作る
TL;DR
- VuejsのUIフレームワークであるVuetifyを使っていい感じの誕生日入力フォームを作成する
- 以下を満たすものとする
- 日本語対応
- 日付の「日」は取る
- 年から選択
- 最初に年を選択するとき、1995年からになるようにする
- 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
- 日付を選択と同時にpickerを閉じる
動作環境
Vue.js v2.x
Vuetify v2.1.14できたもの
https://codepen.io/kmr_0811/pen/oNgBzxR
解説
- 日本語対応
- v-date-pickerのlocaleプロパティを設定
- 日付の「日」は取る
- v-date-pickerのday-formatプロパティを設定
- 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
- max, minプロパティを設定
- 最初に年を選択するとき、1995年からになるようにする
- picker-dateプロパティを制御する必要がある
- picker-dateではpickerの値をコントロールできる
- そのため、v-modelでbindされたdateオブジェクトを触らずpickerについてのみ触ることができる
<v-date-picker ref="picker" locale="jp-ja" v-model="date" :day-format="date => new Date(date).getDate()" :max="new Date().toISOString().substr(0, 10)" :picker-date="pickerDate" min="1950-01-01" @change="save" ></v-date-picker>
- 年から選択
- v-menuにバインドしたmenuをwatchし、v-date-pickerのactivePickerを制御する。
- menuを選ばれたタイミングのみでactivePickerをYEAR指定にしないと、ずっと年選択になったりする
- 選び始めたらpickerDateを初期化しないと、ユーザが入力してもpickerDateの値になってしまうので注意
watch: { menu (val) { val && setTimeout(() => ( // 年から選ぶようにする this.$refs.picker.activePicker = 'YEAR', // 選び始めたら初期化 this.pickerDate = null )) }, },
- 日付を選択と同時にpickerを閉じる
- 日付を選択したとき = v-date-pickerの
@change
が発火するタイミング- その際、pickerDateと選択したdateを同期させないと、もう一度誕生日を選びなおした時に前回の日付が初期値として機能してくれないので注意
methods: { save (date) { this.$refs.menu.save(date) // 再入力に備えて、入力が終わったら同期する this.pickerDate = date; }, },感想
v-date-pickerは抽象的な作りをしていて、開発者が用意されたプロパティやコンポーネントをよく調べる必要があります。
そのため、必然的にVueコンポーネントの学びを多く得られると思いました。ぜひ、v-date-pickerもといVuetifyを触ってみてください!
付録
今回のコード
template
<div id="app"> <v-app id="inspire"> <v-menu ref="menu" v-model="menu" :close-on-content-click="false" transition="scale-transition" offset-y full-width min-width="290px" > <template v-slot:activator="{ on }"> <v-text-field v-model="date" label="誕生日を入力" prepend-icon="event" readonly v-on="on" ></v-text-field> </template> <v-date-picker ref="picker" locale="jp-ja" v-model="date" :day-format="date => new Date(date).getDate()" :max="new Date().toISOString().substr(0, 10)" :picker-date="pickerDate" min="1950-01-01" @change="save" ></v-date-picker> </v-menu> </v-app> </div>script
new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ date: null, menu: false, // pickerの初期値(95年から年が選べるようになる) pickerDate: '1995-1-1', }), watch: { menu (val) { val && setTimeout(() => ( // 年から選ぶようにする this.$refs.picker.activePicker = 'YEAR', // 選び始めたら初期化 this.pickerDate = null )) }, }, methods: { save (date) { this.$refs.menu.save(date) // 再入力に備えて、入力が終わったら同期する this.pickerDate = date; }, }, })
- 投稿日:2019-12-18T13:10:05+09:00
Vue.jsで作るプロジェクトの背景にPlayCanvasを組み込むことでリッチにできそう。
PlayCanvasで作成したプロジェクトをVue.js背景として埋め込む
やったこと
- PlayCanvasで作成したプロジェクトを背景として埋め込む
使った技術
- Vue.js
- PlayCanvas
作ったもの
このように背景に動くリッチなコンテンツを追加します。
作ったデモページ
https://trusting-heyrovsky-74ae1c.netlify.com/PlayCanvasのプロジェクトを作る
作ったプロジェクトについて、作ったプロジェクトについてはこちらのページからEDITORボタンを押すとソースコードや配置などを確認することができます。
https://playcanvas.com/project/647534/
PlayCavnasについて詳しく知りたい方はこちらのリンクなどを見ていただけると幸いです
PlayCanvasのプロジェクトをPUBLISHする
今回はPlayCanvasのプロジェクトをこのような形で作成しました。
3D モデルについては Google PolyからFBXをダウンロードして使用しました。
https://poly.google.com/設定を追加してから、PlayCanvasではウェブ上からPUBLISHします。
Transparent Canvasにチェックを入れる
PUBLISHする前にSETTINGS
→RENDERING
からTransparent Canvas
にチェックを入れます。PUBLISHをする
PUBLISHする際にはエディターの PUBLISH / DOWNLOADからPUBLISHすることができます。
PUBLISHをすると個別のURLが発行され誰でもURLにアクセスにすることでページを閲覧できるようになります
今回PUBLISHしたデモ
https://playcanv.as/p/8NZ92jAY/
HTMLに埋め込む
PlayCanvasで作成したゲームを背景として埋め込みます。
今回はVue.jsでinit
したプロジェクトを使用してPlayCanvasのプロジェクトを埋め込みます。今回埋め込んだプロジェクトのリポジトリに公開しています。
PUBLISHしたURLをiframeで埋め込む
PlayCanvasを
iframe
で埋め込む際にはPUBLISHされたURLにoverlay=false
というクエリパラメータを付けると全画面で表示されるようになります。https://playcanv.as/p/8NZ92jAY/?overlay=false
App.Vue
App.vue<template> <div> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> <iframe src="https://playcanv.as/p/8NZ92jAY/?overlay=false" /> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'app', components: { HelloWorld } } </script> <style> html{ background:white; } #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: white; margin-top: 60px; background: transparent; position:absolute; z-index: 1; width: 100vw; height: 100vh; } iframe{ width: 100vw; height: 100vh; border: none; position:fixed; z-index: 0; top: 0; left: 0; } </style>iframeを追加することで背景として埋め込む事ができます。ロードを考えるとあまり実用的ではなさそうだけれどロード画面とかを工夫できれば結構きれいなものが作れそう。
埋め込むとこのような形の表示になります。
https://trusting-heyrovsky-74ae1c.netlify.com/
PlayCanvas開発で参考になりそうな記事の一覧です。
入門
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む
その他の記事はこちらになります。
その他関連
- PlayCanvasタグの付いた記事一覧
PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!
- 投稿日:2019-12-18T12:40:24+09:00
Vuex に型情報を付加する話と今後の野望(だったもの)
本記事は Vue Advent Calendar 2019 18 日目の記事です。
Vuex に型情報を付加する話と今後の野望だったものと題して、 Vue + Vuex with TypeScript の開発体験を向上するために試行錯誤したことについて話していきます。
はじめに
Vuex モジュールに型情報を付けたい、そう思ったのは Vue を TypeScript で書くみなさんなら誰しも1度は考えたことがあるのではないかと思います。
今年の春、ちょうど型パズルを使えるようになってきた時期だった私は去来したその考えをすぐ実行に移すこととしました。
1. 後付の型情報
最初は、モジュールの型情報を
type
キーワードやinterface
キーワードを使って定義したあと、mapState
などのヘルパの返り値をas
キーワードでキャストして型情報を付けていきました。module.tsexport type State = { value: string; } export default { state: { value: 'Hello, Vuex!' } }Component.vue<template>...<template> <script lang="ts"> import { mapState } from 'vuex'; import { State } from '@/stores/modules/module.ts'; const mappedState = mapState('module', ['value']) as State; export default Vue.extend({ computed: { ...mappedState } }) </script>これで Vuex 由来のプロパティに型情報を乗せることができるようになりました。
しかし、この方法では以下の問題がありました。
- 手動での型付けのため、モジュールの実態と型情報がリンクしていない
- どこまで正確に型をつけるかは実装者の良心によるところが大きい
- 実装と型定義の二重実装になり、手間がかかる
mapXXXX
ヘルパは部分的にモジュールを引っ張ってこれる
- 型パズルを駆使して適宜型情報を制限しなくてはならない
- こちらも二度手間
これらの問題を解決するために考えついたのは、「
mapXXXX
ヘルパに実装から抽出した型情報を持たせる」ことでした。2.実装から型を抽出する
TypeScript で実装から型を抽出する方法、と聞いてみなさんは何を思い浮かべたでしょうか。
Compiler API などの裏技を除けば、関数の引数を使ってジェネリクス型に拾ってもらうのが一般的ではないかと思います。
実際、Vue.js がとった方法もVue.extends()
を用いた方法でした。const just = <T>(value: T) => value; just('hogehoge'); // T = string just(1); // T = 1 just(true); // T = booleanこのコードスニペットのように、ジェネリクス型
T
を引数として受け取る関数に、
任意の型を渡した際にT
はその型として推論が行われます。これを利用して、ジェネリクス型に State, Actions, Mutations, Getters の型をそれぞれ抽出してしまえば第1段階クリアです。
import { ActionTree, Module, MutationTree, GetterTree } from "vuex"; type State<S> = S | () => S; interface FullyTypedModuleDefinition< S, R extends any, A extends ActionTree<S, R> = {}, M extends MutationTree<S> = {}, G extends GetterTree<S, R> = {} > extends Module<S, R> { state?: State<S>; actions?: A; mutations?: M; getters?: G; } export interface FullyTypedModule< S, R extends any, A extends ActionTree<S, R> = {}, M extends MutationTree<S> = {}, G extends GetterTree<S, R> = {} > extends Module<S, R> { state: State<S>; actions: A; mutations: M; getters: G; } /** * Defines fully-typed Vuex module. * * @param mod definition of Vuex module */ export const buildModule = < S, R extends any, A extends ActionTree<S, R> = ActionTree<S, R>, M extends MutationTree<S> = MutationTree<S>, G extends GetterTree<S, R> = GetterTree<S, R> >( mod: FullyTypedModuleDefinition<S, R, A, M, G> ): FullyTypedModule<S, R, A, M, G> => ({ state: {} as State<S>, actions: {} as A, mutations: {} as M, getters: {} as G, ...mod });モジュール定義から型情報をしっかり持ったモジュール定義を出力しているだけの関数です。
オプショナルな項目に空の初期値を与えたくらいの変化しかありません。buildModule({ state: { value: 'Hello, Vuex!' } }); // S = { value: string }これで実装に沿った型情報を抽出することができました。
(例が長くなるので載せていませんが、他の項目もちゃんと抽出できます)3. 型パズルの時間
さて、あとは抽出した型情報をもとに、型パズルこねこねして
mapXXXX
の返り値の型を作るだけです。3-1. 抽出した型情報から mapXXXX の返り値の型を組み立てる
import { Dictionary, Computed, mapState } from "vuex"; type MappedState<S> = Dictionary<Computed> & { [P in keyof S]: () => S[P] };まずは返り値の型がなきゃ始まらないので、返り値の型を作ります。
Mapped Types を使えばかなり楽に定義できます。Actions などは関数なのでちょっとトリッキーなことをする必要がありますが、
Conditional Types を使えばどうにかなります。見栄えは悪いですが3-2. mapXXXX の型定義を参考に関数の型定義
export interface StateMapper<State> { <Key extends keyof State>(map: Key[]): MappedState<Pick<State, Key>>; <Key extends keyof State>(namespace: string, map: Key[]): MappedState< Pick<State, Key> >; (): MappedState<State>; (namespace: string): MappedState<State>; }
interface
の関数型のオーバーロードを使って、Vuex 内のmapXXXX
の型定義を参考に新しいmapXXXX
のインタフェースを定義していきます。Vuex の
mapXXXX
にはオブジェクトでのマッピングによる定義もありますが、簡単のためオミットしました。
また、全てのキーを取得する際はキーの指定が必要ないような定義を追加しています。3-3. 実装
定義したシグネチャに従って、
mapXXXX
をラップする形で実装を進めていきます。
関数の引数を介さないとジェネリクス型の評価を使用時に行えないので、
捨てパラメータとして_state: S
を定義しています。import { Dictionary, Computed, mapState } from "vuex"; import { keyOf } from "../utils/keyof"; type MappedState<S> = Dictionary<Computed> & { [P in keyof S]: () => S[P] }; const stateMapper = <S, K extends keyof S>(_state: S, map: K[]) => mapState(map as string[]) as MappedState<S>; const stateMapperWithNamespace = <S, K extends keyof S>( _state: S, namespace: string, map: K[] ) => mapState(namespace, map as string[]) as MappedState<S>; export interface StateMapper<State> { <Key extends keyof State>(map: Key[]): MappedState<Pick<State, Key>>; <Key extends keyof State>(namespace: string, map: Key[]): MappedState< Pick<State, Key> >; (): MappedState<State>; (namespace: string): MappedState<State>; } export const mapStateWithType = <S>(state: S): StateMapper<S> => < K extends keyof S >( ...args: [K[]] | [string, K[]] | [string] | [] ) => { if (!args.length) { return stateMapper(state, keyOf(state)); } const isNamespaceOnly = ( val: [K[]] | [string, K[]] | [string] | [] ): val is [string] => val.length === 1 && typeof val[0] === "string"; if (isNamespaceOnly(args)) { return stateMapperWithNamespace(state, args[0], keyOf(state)); } const isWithNamespace = (val: [K[]] | [string, K[]]): val is [string, K[]] => typeof val[0] === "string"; if (isWithNamespace(args)) { const [namespace, map] = args; return stateMapperWithNamespace(state, namespace, map); } const [map] = args; return stateMapper(state, map); };あとは 2. で抽出した型情報のついたモジュールの項目を
mapStateWithType
に食わせれば型情報のついたmapXXXX
ヘルパの出来上がりです。作成したライブラリはこちらになります。
4. 今後の野望だったもの
このライブラリを作り終え、使っていくうちにある野望が浮かんできました。
モジュール定義のときも型情報を付けたい!!
今回のライブラリで提供したのは既に定義済みのモジュールから
マッピングヘルパを介してコンポーネントに型情報を提供する方法であり、
モジュール定義自体は Vuex 自身の甘い型定義に頼るほかありませんでした。なので、モジュール定義自体も
Vue.extends()
が対応したようにしっかりと型定義できれば、
より良い開発体験が得られるに違いない!と考えたわけです。その際に重視したのは、現行の Vuex API を踏襲して
そのまま Vuex で使用可能な定義モジュールを出力することでした。そうすれば、公式に沿ったピュアなオブジェクトを介して、定義とマッピングを自作のライブラリで型情報をつけられる。
使って貰える人は両方セットではなくてどっちかほしい方だけでも使えるようになる、と考えてのことでした。結果的に言えば、この試みは失敗でした。
4-1. 自己言及的な API の存在
その原因は自己言及的な Vuex の構造にあります。
例えば、ある Action は他の Actions を呼び出せます。
それは、Action の第1引数の中に Actions が含まれているからです。
つまり、Actions の定義の際には Actions 全体の情報が必要になる、「鶏が先か卵が先か」のジレンマが存在したのです。Vue 自体の構造にも同じことが言えます。
算出プロパティは他の算出プロパティを呼び出せる、
すなわち算出プロパティの定義には他の算出プロパティが必要になるはずでした。しかし、Vue 自体の API はそれらは全て
this
オブジェクトから利用するため、
Vue.extends()
は定義から抽出した型情報を関数内のthis
オブジェクトの型として反映させる形で、鶏と卵のジレンマを回避することができたのです。ですが、Vuex 定義はそうは行きません。
ここで、開発を諦めるか、API 構造を変更するかの選択を迫られました。悩みましたが、API 構造を変更することを選びました。
- Actions が Actions を利用するのではなく、 Orchestrator という項目を新規定義して、これが Actions の呼び出しをコントロールするように API 構造を修正
- Actions, Getters の自己言及的な API を型定義から削除
この2点を実施することで、項目同士の依存関係は以下のようになりました。
これなら、再帰的な定義を必要とせずに型定義を作成できます。
しかし、なおも壁が立ちはだかります。
4-2. 型定義を抽出できない
前の依存グラフを見ると、例えば State は他の全ての項目から依存されています。
これは、ジェネリクス関数の引数定義に型変数が複数回登場するということを意味します。ジェネリクス関数の引数定義で型情報を抽出するには、引数から型が一意に定まることが重要です。
しかし、引数定義に型変数が複数回登場すると、型が一意に定まらず、型変数の表す型が曖昧になってしまいます。これを解決するためには、一度に全ての項目を定義することを諦めねばなりませんでした。
メソッドチェーンなどを利用して次の項目の定義を促すような API を作成する必要があったのです。現状、この問題を解決するためのスマートな API インタフェースを考案している段階です。
おわりに
現状の Vuex API は TypeScript で利用するためにはあまりにも型情報が曖昧です。
それを解決するために色々と模索してきた上で思うのは、
「確かにこれを現状のまま解決するのにはコストが掛かりすぎる」ということです。今、私が夢想しているのは、Vue Composition API によって Vuex の必要なくなった未来です。
Vuex がもたらしたのは、データハンドリングフローの一貫性と Vue.js Dev Tools によるアプリケーションの状態の透明化、タイムトラベル・デバッグでした。
Vue Composition API にこれらは不可能なことでしょうか?
私は決して不可能なことではないんじゃないかな、とかなり楽観的に考えています。。。
- 投稿日:2019-12-18T11:25:22+09:00
docker-composeでexit status 1
はじめに
macOS Mojave 10.14.6
Docker 2.1.0.5
とりあえず
$ docker-compose up ... front_nuxt | npm ERR! Exit status 1 front_nuxt | npm ERR! front_nuxt | npm ERR! Failed at the Musclers@1.0.0 dev script. front_nuxt | npm ERR! This is probably not a problem with npm. There is likely additional logging output above. front_nuxt | front_nuxt | npm ERR! A complete log of this run can be found in: front_nuxt | npm ERR! /root/.npm/_logs/2019-12-18T01_41_30_431Z-debug.log front_nuxt | front_nuxt exited with code 1といってフロントくん(nuxt)が起動しない
$ docker-compose ps Name Command State Ports ---------------------------------------------------------------------------- api_rails bash -c rm -f 'tmp/pids/se ... Up 0.0.0.0:8080->8080/tcp db_psql docker-entrypoint.sh postgres Up 5432/tcp front_nuxt docker-entrypoint.sh npm r ... Exit 1 0.0.0.0:3000->3000/tcpまぁ、だよね
原因
これがイマイチわからない。
やったこと
prune
でimageを一括削除した。$ docker-compose down $ docker system prune --volumes # docker system pruneではvolumeまで削除してくれない $ docker-compose up --build # build忘れないようにねで、buildが終わった後、無事起動しました。
最後に
最後にって書く必要あるのかわからないけどrailsとか使ってるなら、
db:create
、migrate
、db:seed
忘れないようにね。
DB空っぽだから何もできずに発狂した人もいるらしい←
- 投稿日:2019-12-18T09:04:44+09:00
Vue.jsでaxiosをモック化してテストする!
axiosのモック化にかなり手間取ったため、作業メモとして残しておこうと思いました。
必要なライブラリ
- @vue/cli-plugin-unit-jest
- @vue/test-utils
- flush-promises
jestを使ってモック化するため、
cli-plugin-unit-jest
を入れました。
最後のflush-promises
はVue Test Utilsの公式サイトで非同期動作のテクニックとして紹介されており、非同期処理を強制的に(?)実行させるためにインストールしました。
非同期のテストテストコード
axiosのインポート
import axios from 'axios'まずはaxiosモジュールをインポートします。
システムによっては、axiosの設定をindex.js
に書いていると思いますが、テストコードではこのindex.js
ではなく、生(と言っていいかは謎ですが)のaxiosをimportします。
理由としては、index.js
をimportすると、そのファイルに書かれている、例えばaxios.create等のメソッドもモック化しないといけないからです。
もちろん、別でindex.js
のテストは必要なのですが、今回のテスト対象は、axios.postやgetを使っているコンポーネントであるため、index.js
ではなく、axios
をimportします。axiosのモック化
jest.mock('axios')axiosモジュールをモック化します。
これで、プロダクトコードでaxiosを呼び出した場合、これから登録するモック関数が呼ばれるようになります。
このjest.mock('axios')
は、describeの前に実行してください。mountまたはshallowMountの実行時オプションにsync: falseを付ける
const wrapper = mount(sampleComponent, { sync: false })axios.postにモック関数を登録する
例えばこのようなプロダクトコードがあるとします。
methods: { aFunc() { axios.post('/sample') .then(response => { console.log('成功') }) .catch(error => { console.log('失敗') }) } }このaxios.postから返すレスポンスをモックにします。
const response = { message: '成功' } axios.post.mockImplementationOnce((url) => { return Promise.resolve(response) })
mockImplementationOnce
は、post
が呼び出されたときに「一度だけ」関数の中が実行されます。
もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定したPromise.resolve
が返りますが、2回目のpostはエラーになります。
この場合の解決策としては2つあります。それについては後述します。
ちなみにプロダクトコードでthis.$http
のように使われている場合は、テストコードのimport文直後くらいにprototype.$http = axios
を入れてください。
私はcreateLocalVueでlocalVueを作成し、それに対して定義してます。const localVue = createLocalVue() localVue.prototype.$http = axiosモック関数の他例
もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定した
Promise.resolve
が返りますが、2回目のpostはエラーになります。例えばこのようなコードがあった場合は、
mockImplementationOnce
を一つ指定しただけだとエラーになります。methods: { aFunc() { axios.post('/sample') // post呼び出し1回目 .then(response => { console.log('/sample成功') axios.post('/sample2') // post呼び出し2回目 .then(response => { console.log('/sample2成功') }) .catch(error => { console.log('/sample2失敗') }) }) .catch(error => { console.log('/sample失敗') }) } }ではどうすればよいか、ですが、求めるレスポンスによって2通りのやり方があります。
レスポンスが同じでも良い場合
jest.fn().mockImplementation
を使います。const response = { message: '成功' } axios.post.mockImplementation((url) => { return Promise.resolve(response) })こうすると
axios.post
が呼ばれたときには 常にresponseが返されます。レスポンスが異なる場合
jest.fn().mockImplementationOnce
を呼び出されるAPIの数分定義します。const response1 = { message: '成功' } const response2 = { message: '失敗' } axios.post.mockImplementationOnce((url) => { return Promise.resolve(response1) }).mockImplementationOnce((url) => { return Promise.resolve(response2) })こうすることで、1回目のAPI呼び出しのときはresponse1が返り、2回目のAPI呼び出しの時はresponse2が返ります。
モックの検証
きちんと想定通りのモックが呼ばれているかを検証します。
検証したいことによって、いろいろとメソッドが用意されていますが、ここではtoHaveBeenCalledWith
を使います。
その他のメソッドについてはJestのドキュメントをご覧ください。レスポンスが異なる場合の検証をしてみましょう。
expect(axios.post).toHaveBeenCalledWith('/sample') expect(axios.post).toHaveBeenCalledWith('/sample2')
expect
の引数には、実行したモックを指定します。
toHaveBeenCalledWith
の引数には、APIが実行されたときの引数を指定します。
今回、API実行時にはurlのみを引数としてaxios.postに渡しているため、それを指定します。
params等の引数を第2引数に渡している場合は、expect(axios.post).toHaveBeenCalledWith('/sample', { param1: 'aaa' })
のように渡します。axiosではなく作ったモジュールの一部をモック化する!
- モジュールを読み込む
import sampleModule from '@/mixin/sampleModule.js'
jest.mock
でモック化するjest.mock('@/mixin/sampleModule')
- モック化したいメソッドに
jest.fn
を入れ込むsampleModule.methods.sampleMethod = jest.fn((arg1, arg2) => { return data })これでモック化できます。
テスト対象コンポーネントのメソッドをモック化する!
jest.mock
でモック化するconst sampleMethod = jest.fn()
- mountオプションに
methods
を指定するconst wrapper = mount(sampleComponent, { localVue, methods: { sampleMethod } })ちゃんと調べたわけではないのですが、どうやらconstの名前のメソッドがモック化されるようです。
以上です!
- 投稿日:2019-12-18T08:57:59+09:00
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
はじめに
Vue.jsとLaravelによるSPA実装のチュートリアル記事です。
本記事は、4本の連載記事の4本目です。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
↑↑今ここ↑↑前回まで
Vue.jsでフロントエンド実装と、
LaravelのAPI実装が完了しました。APIにつないでない状態の
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページと、
・タスク一覧取得API
・タスク詳細取得API
・タスク登録API
・タスク更新API
・タスク削除API
が完成している状態です。今回はこの静的ページと
APIを繋ぎ込んでいきます。この全体図の赤色部分になります。
axios
今回、フロントページから
AjaxでAPIにリクエストを送信して
データの取得や更新を行います。Ajax通信を簡単に実装するため、
今回はaxiosというパッケージを利用します。
https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9特に難しいところはありませんが、
axiosの使い方を簡単に把握しておきましょう。laravel/uiでベースを構築したので、
自分でインストールや設定作業などしなくても
最初からaxiosが利用できる状態です。タスク一覧取得API繋ぎ込み
早速、タスク一覧ページとタスク一覧取得APIを繋ぎ込んでみましょう。
まずは
<script>
に必要なデータ、メソッドを定義します。resources/js/components/TaskListComponent.vue<script> - export default {} + export default { + data: function () { + return { + tasks: [] + } + }, + methods: { + getTasks() { + axios.get('/api/tasks') + .then((res) => { + this.tasks = res.data; + }); + } + }, + mounted() { + this.getTasks(); + } + } </script>まず
data
には空配列のtasks
を用意します。そして、
methods
にあるgetTasks()
メソッドで、
タスク一覧取得APIにリクエストして
そのレスポンスを先ほどのtasks
の中に入れています。
(このメソッドで先ほど話したaxiosを利用してリクエストしています)そして、画面描画時にこの
getTasks()
メソッドが実行されるように、
mounted()
でメソッドを呼び出しています。これで
<script>
側は完了です。次に
<templete>
側も修正します。resources/js/components/TaskListComponent.vue- <tr> - <th scope="row">1</th> - <td>Title1</td> - <td>Content1</td> - <td>Ichiro</td> - <td> - <button class="btn btn-primary">Show</button> - </td> - <td> - <button class="btn btn-success">Edit</button> - </td> - <td> - <button class="btn btn-danger">Delete</button> - </td> - </tr> - <tr> - <th scope="row">2</th> - <td>Title2</td> - <td>Content2</td> - <td>Jiro</td> - <td> - <button class="btn btn-primary">Show</button> - </td> - <td> - <button class="btn btn-success">Edit</button> - </td> - <td> - <button class="btn btn-danger">Delete</button> - </td> - </tr> - <tr> - <th scope="row">3</th> - <td>Title3</td> - <td>Content3</td> - <td>Saburo</td> - <td> - <button class="btn btn-primary">Show</button> - </td> - <td> - <button class="btn btn-success">Edit</button> - </td> - <td> - <button class="btn btn-danger">Delete</button> - </td> - </tr> + <tr v-for="task in tasks"> + <th scope="row">{{ task.id }}</th> + <td>{{ task.title }}</td> + <td>{{ task.content }}</td> + <td>{{ task.person_in_charge }}</td> + <td> + <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}"> + <button class="btn btn-primary">Show</button> + </router-link> + </td> + <td> + <router-link v-bind:to="{name: 'task.edit', params: {taskId: task.id }}"> + <button class="btn btn-success">Edit</button> + </router-link> + </td> + <td> + <button class="btn btn-danger">Delete</button> + </td> + </tr>まずはべた書きで表示していた
3行のデータを削除します。そして、先ほど定義した
tasks
データをv-for
で表示します。
<tr v-for="task in tasks">
ID、Title、Content、Person In Chargeの
各カラムは{{ task.title }}
のようにデータを動的に表示させます。- <td>Title1</td> + <td>{{ task.title }}</td>また、「Show」「Edit」ボタンの
リンクURLのパラメータもべた書きしていたので、
ちゃんと動的にidを設定します。- <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}"> + <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">これで、
APIからデータを取得し
それをv-for
で画面に一覧表示できるようになりました。commit:タスク一覧ページAPI繋ぎ込み
タスク一覧ページ完成です。
タスク詳細取得API繋ぎ込み
次に、タスク詳細ページとタスク詳細取得APIを繋ぎ込んでいきます。
まずは
<script>
。resources/js/components/TaskShowComponent.vue<script> export default { props: { taskId: String }, + data: function () { + return { + task: {} + } + }, + methods: { + getTask() { + axios.get('/api/tasks/' + this.taskId) + .then((res) => { + this.task = res.data; + }); + } + }, + mounted() { + this.getTask(); + } } </script>
一覧ページと同じように、
data
に空のtask
を用意。
methods
のgetTask()
でAPIからタスクデータを取得。
mounted()
で画面描画時にメソッド呼び出し。
としています。次に
<templete>
側。resources/js/components/TaskShowComponent.vue<div class="form-group row border-bottom"> <label for="id" class="col-sm-3 col-form-label">ID</label> <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" - v-bind:value="taskId"> + v-model="task.id"> </div> <div class="form-group row border-bottom"> <label for="title" class="col-sm-3 col-form-label">Title</label> <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title" - value="title title"> + v-model="task.title"> </div> <div class="form-group row border-bottom"> <label for="content" class="col-sm-3 col-form-label">Content</label> <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content" - value="content content"> + v-model="task.content"> </div> <div class="form-group row border-bottom"> <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge" - value="Ichiro"> + v-model="task.person_in_charge"> </div>各データを
v-model
で表示するようにしました。これでAPI取得したデータをタスク詳細ページに表示できました。
commit:タスク詳細ページAPI繋ぎ込み
タスク詳細ページ完成です。
タスク登録API繋ぎ込み
次に、タスク登録ページとタスク登録APIを繋ぎ込んでいきます。
まずは
<script>
。resources/js/components/TaskCreateComponent.vue<script> - export default {} + export default { + data: function () { + return { + task: {} + } + }, + methods: { + submit() { + axios.post('/api/tasks', this.task) + .then((res) => { + this.$router.push({name: 'task.list'}); + }); + } + } + } </script>空の
task
データを用意するところは先ほどと同じです。
methods
のsubmit()
メソッドで、
task
データをタスク登録APIにPOST送信する処理を書いています。また、APIによるデータ登録完了後、
this.$router.push({name: 'task.list'});
でタスク一覧ページにリダイレクトしています。
次に<templete>
側。resources/js/components/TaskCreateComponent.vue- <form> + <form v-on:submit.prevent="submit"> <div class="form-group row"> <label for="title" class="col-sm-3 col-form-label">Title</label> - <input type="text" class="col-sm-9 form-control" id="title"> + <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title"> </div> <div class="form-group row"> <label for="content" class="col-sm-3 col-form-label">Content</label> - <input type="text" class="col-sm-9 form-control" id="content"> + <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content"> </div> <div class="form-group row"> <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> - <input type="text" class="col-sm-9 form-control" id="person-in-charge"> + <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form>各フォームは
v-model
でtask
データとバインディングすることで、
フォームにデータが入力されたら
<scripts>
側のtask
データも更新されるようになっています。そして、
<form v-on:submit.prevent="submit">
で、フォーム送信時に先ほど定義したsubmit
メソッドを呼び出すようにしています。これで、入力内容が反映された
task
データを
submit
メソッドでAPI送信できる状態になっています。commit:タスク登録ページAPI繋ぎ込み
これでタスク登録ページ完成です。
タスク更新API繋ぎ込み
次に、タスク編集ページとタスク更新APIを繋ぎ込んでいきます。
まずは
<script>
。resources/js/components/TaskEditComponent.vue<script> export default { props: { taskId: String }, + data: function () { + return { + task: {} + } + }, + methods: { + getTask() { + axios.get('/api/tasks/' + this.taskId) + .then((res) => { + this.task = res.data; + }); + }, + submit() { + axios.put('/api/tasks/' + this.taskId, this.task) + .then((res) => { + this.$router.push({name: 'task.list'}) + }); + } + }, + mounted() { + this.getTask(); + } } </script>
タスク詳細ページとタスク登録ページでやったことを
両方やっているだけです。空の
task
データを用意し、
getTask()
メソッドでAPIから取得したデータをセットする。
submit
メソッドでは、
タスク更新APIにputリクエストを送信しています。
次に<template>
。resources/js/components/TaskEditComponent.vue- <form> + <form v-on:submit.prevent="submit"> <div class="form-group row"> <label for="id" class="col-sm-3 col-form-label">ID</label> - <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId"> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-model="task.id"> </div> <div class="form-group row"> <label for="title" class="col-sm-3 col-form-label">Title</label> - <input type="text" class="col-sm-9 form-control" id="title"> + <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title"> </div> <div class="form-group row"> <label for="content" class="col-sm-3 col-form-label">Content</label> - <input type="text" class="col-sm-9 form-control" id="content"> + <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content"> </div> <div class="form-group row"> <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> - <input type="text" class="col-sm-9 form-control" id="person-in-charge"> + <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form>これはタスク登録ページと同じです。
各フォームは
v-model
でtask
データとバインディングして、
formのv-on:submit.prevent="submit"
でsumit
メソッドを呼んでいます。commit:タスク編集ページAPI繋ぎ込み
これでタスク編集ページは完成。
タスク削除API繋ぎ込み
最後に、タスク一覧ページのDeleteボタンとタスク削除APIを繋ぎ込んでいきます。
まずは
<script>
。resources/js/components/TaskListComponent.vuemethods: { getTasks() { axios.get('/api/tasks') .then((res) => { this.tasks = res.data; }); }, + deleteTask(id) { + axios.delete('/api/tasks/' + id) + .then((res) => { + this.getTasks(); + }); + } },
deleteTask()
メソッドを追加しました。
タスクIDを引数で受け取り、
タスク削除APIにリクエストを送信しています。削除完了したら、
getTasks()
メソッドを呼んで
タスク一覧を再読み込みしています。次に
<template>
。resources/js/components/TaskListComponent.vue<td> - <button class="btn btn-danger">Delete</button> + <button class="btn btn-danger" v-on:click="deleteTask(task.id)">Delete</button> </td>もともと設置していたDeleteボタンに
v-on:click="deleteTask(task.id)"
を追加しました。これで、このボタンをクリックしたら
deleteTask()
メソッドが呼ばれます。commit:タスク一覧ページ削除API繋ぎ込み
これでタスク一覧ページの削除処理もできたので、
全ページ、全機能が完成しました。おわりに
シンプルなCRUD機能のアプリを
Vue.jsのSPAとLaravelのAPIで構築しました。Vue側もLaravel側もほとんど難しいところもなく、
かなり簡単に書けたと思います。今回はできるだけ簡単に一通りの機能を作るチュートリアルとしたかったため、
本来実装すべき処理を省いた箇所が多いです。Vue側では
Ajaxのエラーハンドリングや
API送信前のバリデーションなど
本来は実装すべきです。Laravel側もバリデーションや
APIの認証処理などがあるといいです。今回のチュートリアルで
ざっくりと全体イメージをまずはつかんで、
今後上記のような詳細な処理を少しずつ追加していくといいかと思います。
- 投稿日:2019-12-18T08:57:41+09:00
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
はじめに
Vue.jsとLaravelによるSPA実装のチュートリアル記事です。
本記事は、4本の連載記事の3本目です。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編前回まで
前回は、Vue.jsでフロントエンドのみ実装し、
静的なSPAができました。べた書きのサンプルデータが表示されている状態で、
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページが完成しています。API実装の進め方
この全体図の緑色部分にある
5つのAPIを実装していきます。今回は一番シンプルな形で進めるので、
各APIの処理は全てコントローラ内で数行で完結します。また、API自体の実装の前に
DBのセットアップや最低限のテストデータも準備します。SQLiteのセットアップ
今回は作業簡略化のため
MySQLやPostgreSQLを用意せず
SQLiteを使います。まずはSQLiteのストレージとなるファイルを用意します。
database/database.sqlite
に空のファイルを作成すればOKです。次に、.envのDB接続情報を修正します。
.env- DB_CONNECTION=mysql - DB_HOST=127.0.0.1 - DB_PORT=3306 - DB_DATABASE=laravel - DB_USERNAME=root - DB_PASSWORD= + DB_CONNECTION=sqliteこれでSQLiteを利用するための設定は完了です。
ただし、PHPのSQLiteドライバーが有効になっている必要がありますので
もしなっていなければ有効にしてください。
https://awesome-linus.com/2019/05/24/php-sqlite-driver-install/migration作成
migrationでタスクテーブルを作成します。
まずは下記コマンドでmigrationファイルを生成。
php artisan make:migration create_tasks_table生成されたmigrationのupメソッドの中をこのように書き換えます。
create_tasks_table.phpSchema::create('tasks', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title', 100); $table->string('content', 100); $table->string('person_in_charge', 100); $table->timestamps(); });commit:migration作成
モデル作成
次に、タスクテーブルに対応する
タスクモデルを作ります。php artisan make:model Task生成されたモデルファイルに、
$fillable
のみ追記しておきます。app/Task.phpclass Task extends Model { + protected $fillable = [ + 'title', + 'content', + 'person_in_charge', + ]; }
commit:タスクモデル作成
seeder作成
次に、テストデータを自動生成するための
seederを作成します。まずは下記コマンドでseederファイルを生成。
php artisan make:seeder TasksTableSeeder生成されたseederファイルのrunメソッドを
このように修正します。database/seeds/TasksTableSeeder.phppublic function run() { + for ($i = 1; $i <= 10; $i++) { + Task::create([ + 'title' => 'title' . $i, + 'content' => 'content' . $i, + 'person_in_charge' => 'person_in_charge' . $i, + ] + ); + } }
また、このseederを実行するためにDatabaseSeederファイルも修正します。
database/seeds/DatabaseSeeder.phppublic function run() { - // $this->call(UsersTableSeeder::class); + $this->call(TasksTableSeeder::class); }commit:タスクseeder作成
テーブル、テストデータ生成
テーブルとテストデータを生成する準備は整いましたので、
実際に生成しましょう。php artisan migrate --seed
これで先ほど作成したmigrationとseederが実行され、
テーブルとテストデータが10件できてるはずです。データがちゃんと入っているか確認した場合は
tinkerを使ってみてください。$ php artisan tinker >>> Task::all();これでタスクテーブルのデータが一覧で表示されます。
タスク一覧取得API実装
それでは早速API実装を始めます。
まずはタスク一覧取得APIから。ルーティングを追加。
routes/api.php+ Route::get('/tasks', 'TaskController@index');
次に、タスクコントローラを作成し、
そこにindexメソッドを追加します。まずはartisanコマンドでコントローラファイル自体を生成。
php artisan make:controller TaskControllerそして、indexメソッド追加。
app/Http/Controllers/TaskController.php+ <?php + + namespace App\Http\Controllers; + + use App\Task; + + class TaskController extends Controller + { + public function index() + { + return Task::all(); + } + }
ただTaskモデルから全件取得してreturnするだけです。
POSTMANなどで
http://localhost:8000/api/tasks
にリクエストすると
タスク一覧が取得できると思います。
※routes/api.php
にルーティング定義すると、自動でパスの頭に/api
がつきます。レスポンスはこのようなjson形式になります。
レスポンス形式[ { "id": 1, "title": "title1", "content": "content1", "person_in_charge": "person_in_charge1", "created_at": "2019-12-17 00:43:38", "updated_at": "2019-12-17 00:43:38" }, { "id": 2, "title": "title2", "content": "content2", "person_in_charge": "person_in_charge2", "created_at": "2019-12-17 00:43:38", "updated_at": "2019-12-17 00:43:38" }, ]commit:タスク一覧取得API実装
タスク詳細取得API実装
次にタスク詳細取得APIです。
ルーティング追加。
routes/api.phpRoute::get('/tasks', 'TaskController@index'); + Route::get('/tasks/{task}', 'TaskController@show');
コントローラにshowメソッドを追加。
app/Http/Controllers/TaskController.php+ public function show(Task $task) + { + return $task; + }
URLパラメータで受け取ったタスクモデルを
そのままreturnするだけです。
※これでLaravelが勝手にjson形式のレスポンスを返却しますcommit:タスク詳細取得API実装
タスク登録API実装
次に、タスク登録APIです。
ルーティング追加。
routes/api.phpRoute::get('/tasks', 'TaskController@index'); + Route::post('/tasks', 'TaskController@store'); Route::get('/tasks/{task}', 'TaskController@show');
※ルーティングの定義順を間違えると正しく動かないので、この通りに記述してください
コントローラにstoreメソッド追加。
app/Http/Controllers/TaskController.phpuse App\Task; + use Illuminate\Http\Request; + public function store(Request $request) + { + return Task::create($request->all()); + }リクエストで受け取ったデータをそのまま
モデルのcreateでデータ登録しているだけです。このようなjson形式のデータを受け取ることを想定しています。
リクエスト形式{ "title": "new title", "content": "new content", "person_in_charge": "new person_in_charge1" }commit:タスク登録API実装
タスク更新API実装
次に、タスク更新APIです。
ルーティング追加。
routes/api.phpRoute::get('/tasks', 'TaskController@index'); Route::post('/tasks', 'TaskController@store'); Route::get('/tasks/{task}', 'TaskController@show'); + Route::put('/tasks/{task}', 'TaskController@update');
コントローラにupdateメソッド追加。
app/Http/Controllers/TaskController.php+ public function update(Request $request, Task $task) + { + $task->update($request->all()); + + return $task; + }
受け取るリクエストの形は、
登録APIと同じjson形式です。URLパラメータで受け取ったTaskモデルのupdateメソッドで
そのままデータを更新するだけです。commit:タスク更新API実装
タスク削除API実装
次はタスク削除API。
ルーティング追加。
routes/api.phpRoute::get('/tasks/{task}', 'TaskController@show'); Route::put('/tasks/{task}', 'TaskController@update'); + Route::delete('/tasks/{task}', 'TaskController@destroy');
コントローラにdestroyメソッド追加。
app/Http/Controllers/TaskController.php+ public function destroy(Task $task) + { + $task->delete(); + + return $task; + }
URLパラメータでTaskを受け取り、
それをそのままdeleteします。commit:タスク削除API実装
おわりに
これで今回必要なAPIはすべて実装完了です。
POSTMANなどを利用して、
各APIの動作を確認するといいと思います。本来は、このAPIでは
バリデーションを入れたり、
検索処理を入れたりすることになるかと思います。次回は、
フロントのVueからAjaxで
このAPIに対してリクエスト送信し、
実際にデータの表示、更新、登録、削除ができるようにします。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
- 投稿日:2019-12-18T08:57:19+09:00
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
はじめに
Vue.jsとLaravelによるSPA実装のチュートリアル記事です。
本記事は、4本の連載記事の2本目です。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編前回まで
前回は、環境構築と必要なパッケージのインストールを行いました。
http://localhost:8000
でLaravelのウェルカムページが表示される状態で
次に進んでください。コンポーネントの構成
本記事では、この全体図の青色部分、
Vue.jsによるフロントエンド実装のみを行います。作るページ(コンポーネント)は全部で4つです。
- タスク一覧
- タスク詳細
- タスク登録
- タスク編集
最初に各ページの完成状態の画像を確認します。
前にインストールしたlaravel/ui vueに
デフォルトで組み込まれているbootstrapを使って
最低限のシンプルなUIにしています。
※今回はbootstrapの使い方には言及しません各ページ上部にある黒い背景色の部分はヘッダーナビで、
全ページ固定で表示されるコンポーネントです。ヘッダーナビより下の
一覧テーブルや入力フォーム部分が
URLごとに切り替わるメインのコンポーネントになります。それでは、各ページのメインコンポーネントに加えて
ヘッダーーコンポーネントの
計5つを実装していきます。ベースbladeとベースルーティングを追加
このアプリでは、
初回アクセス時のみLaravel側でリクエストを受けて
ページを表示し、
それ以降はフロント側のVue Routerによってルーティングが行われます。その最初のリクエストを受け取る
Laravel側のルーティングとbladeファイルを追加します。routes/web.php- Route::get('/', function () { - return view('welcome'); - }); + Route::get('/{any}', function() { + return view('app'); + })->where('any', '.*');resouces/views/app.blade.php+ <!doctype html> + <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <!-- CSRF Token --> + <meta name="csrf-token" content="{{ csrf_token() }}"> + + <title>{{ config('app.name', 'Vue Laravel SPA') }}</title> + + <!-- Styles --> + <link href="{{ mix('/css/app.css') }}" rel="stylesheet"> + </head> + <body> + <div id="app"> + + </div> + <!-- Scripts --> + <script src="{{ mix('/js/app.js') }}" defer></script> + </body> + </html>
commit:ベースのbladeとルーティング追加
これで、どのURLでアクセスしても
このapp.blade.phpが表示されるようになりました。また、前回の記事でインストールした
Vue.jsやbootstrapも
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
このjs、cssファイルで読み込まれているため
利用できる状態です。試しにデフォルトで用意されている
ExampleComponentを表示してみてください。resouces/views/app.blade.php<div id="app"> + <example-component></example-component> </div>
これで
http://localhost:8000
にアクセスすると、
このようにExampleComponentが表示されると思います。これが正しく表示されていれば、
Vue.js、bootstrapがちゃんと使えている状態です。
(このExampleComponentはbootstrapが使われています)ヘッダーコンポーネント実装
ベースのbladeが配置できたので、
次に全ページ共通で固定表示する
ヘッダーコンポーネントを実装します。HeaderComponentの追加
resources/js/components/HeaderComponent.vue+ <template> + <div class="container-fluid bg-dark mb-3"> + <div class="container"> + <nav class="navbar navbar-dark"> + <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span> + <div> + <button class="btn btn-success">List</button> + <button class="btn btn-success">ADD</button> + </div> + </nav> + </div> + </div> + </template> + + <script> + export default {} + </script>
classがいろいろとたくさん設定されていますが、
全部bootstrapのclassで見た目を整えているだけなので、
あまり気にしなくてOKです。
そのコンポーネントをVueインスタンスに登録
resources/js/app.js+ import HeaderComponent from "./components/HeaderComponent"; //↑ファイル先頭 Vue.component('example-component', require('./components/ExampleComponent.vue').default); + Vue.component('header-component', HeaderComponent);
登録したコンポーネントをベースbladeに追加
resources/views/app.blade.php<div id="app"> + <header-component></header-component> </div>
commit:ヘッダーコンポーネント実装
この状態でページを表示してみます。
※npm run dev
またはnpm run watch
でソースをビルドするのを忘れないようにしましょうページ上部に黒いヘッダーナビが表示されていると思います。
まだボタンのリンク先は設定されていませんが、
この後ページを追加した際にこのボタンのリンクも設定します。タスク一覧コンポーネント実装
まずタスク一覧コンポーネントを追加します。
resources/js/components/TaskListComponent.vue+ <template> + <div class="container"> + <table class="table table-hover"> + <thead class="thead-light"> + <tr> + <th scope="col">#</th> + <th scope="col">Title</th> + <th scope="col">Content</th> + <th scope="col">Person In Charge</th> + <th scope="col">Show</th> + <th scope="col">Edit</th> + <th scope="col">Delete</th> + </tr> + </thead> + <tbody> + <tr> + <th scope="row">1</th> + <td>Title1</td> + <td>Content1</td> + <td>Ichiro</td> + <td> + <button class="btn btn-primary">Show</button> + </td> + <td> + <button class="btn btn-success">Edit</button> + </td> + <td> + <button class="btn btn-danger">Delete</button> + </td> + </tr> + <tr> + <th scope="row">2</th> + <td>Title2</td> + <td>Content2</td> + <td>Jiro</td> + <td> + <button class="btn btn-primary">Show</button> + </td> + <td> + <button class="btn btn-success">Edit</button> + </td> + <td> + <button class="btn btn-danger">Delete</button> + </td> + </tr> + <tr> + <th scope="row">3</th> + <td>Title3</td> + <td>Content3</td> + <td>Saburo</td> + <td> + <button class="btn btn-primary">Show</button> + </td> + <td> + <button class="btn btn-success">Edit</button> + </td> + <td> + <button class="btn btn-danger">Delete</button> + </td> + </tr> + </tbody> + </table> + </div> + </template> + + <script> + export default {} + </script>
ID、Title、Content(内容)、Person In Charge(担当者)、各種操作ボタン
をカラムに持つテーブルです。現時点では、サンプルとして3行ほどべた書きで
タスクを表示しています。後々の作業でここは
LaravelAPIからデータを受け取り表示するようになります。また、
Show、Edit、Deleteのボタンを設置していますが
いまはリンク先が設定されていません。後々各コンポーネントを実装したらリンク先を設定していきます。
追加したタスク一覧コンポーネントを
Vue Routerに登録します。resources/js/app.js+ import VueRouter from 'vue-router'; import HeaderComponent from "./components/HeaderComponent"; + import TaskListComponent from "./components/TaskListComponent"; window.Vue = require('vue'); + Vue.use(VueRouter); + + const router = new VueRouter({ + mode: 'history', + routes: [ + { + path: '/tasks', + name: 'task.list', + component: TaskListComponent + }, + ] + }); const app = new Vue({ el: '#app', + router });VueRouter自体の詳しい解説は省略しますが、
ポイントはここです。routes: [ { path: '/tasks', name: 'task.list', component: TaskListComponent }, ]ここで、
「/tasks」のURLでアクセスしたら
「TaskListComponent」を表示する。
このルーティングの名前は「task.list」である。
と設定しています。別ページ(コンポーネント)を追加した際は、
同じようにこのroutes
に設定を加えていくことになります。
そして、ルーティングで紐づけられたコンポーネントを表示するために、
ベースのbladeに<router-view>
を配置する必要があります。resources/views/app.blade.php<div id="app"> <header-component></header-component> + <router-view></router-view> </div>
先ほどVue Routerで設定したとおり、
URLに紐づくコンポーネントがこの
<router-view>
の部分に表示されることになります。この状態で
http://localhost:8000/tasks
にアクセスしてみましょう。
※ビルドを忘れずにお手本で見た通りの
一覧テーブルが表示されていると思います。ついでに、
ヘッダーコンポーネントにある
「List」ボタンのリンク先を設定しておきましょう。resources/js/components/HeaderComponent.vue<nav class="navbar navbar-dark"> <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span> <div> <button class="btn btn-success">List</button> + <router-link v-bind:to="{name: 'task.list'}"> <button class="btn btn-success">List</button> + </router-link> <button class="btn btn-success">ADD</button> </div> </nav>このように
<route-link>
のv-bind:to
で
リンク先のルーティング名を設定することで
SPAのリンクとして動作させることができます。commit:タスク一覧コンポーネント実装
タスク詳細コンポーネント実装
次に、タスク詳細コンポーネントを追加します。
まずコンポーネントファイル作成。
resources/js/components/TaskShowComponent.vue+ <template> + <div class="container"> + <div class="row justify-content-center"> + <div class="col-sm-6"> + <form> + <div class="form-group row border-bottom"> + <label for="id" class="col-sm-3 col-form-label">ID</label> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" + v-bind:value="taskId"> + </div> + <div class="form-group row border-bottom"> + <label for="title" class="col-sm-3 col-form-label">Title</label> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title" + value="title title"> + </div> + <div class="form-group row border-bottom"> + <label for="content" class="col-sm-3 col-form-label">Content</label> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content" + value="content content"> + </div> + <div class="form-group row border-bottom"> + <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge" + value="Ichiro"> + </div> + </form> + </div> + </div> + </div> + </template> + + <script> + export default { + props: { + taskId: String + } + } + </script>
taskIdをURLパラメータとして受け取って、
そのIDのみ
<input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
のv-bind:value="taskId"
部分で動的に表示しています。それ以外のcontent、person-in-chargeは
まだべた書きにしているだけです。このコンポーネントをVue Routerに登録します。
resources/js/app.jsimport VueRouter from 'vue-router'; import HeaderComponent from "./components/HeaderComponent"; import TaskListComponent from "./components/TaskListComponent"; + import TaskShowComponent from "./components/TaskShowComponent"; { path: '/tasks', name: 'task.list', component: TaskListComponent }, + { + path: '/tasks/:taskId', + name: 'task.show', + component: TaskShowComponent, + props: true + },これで、
/tasks/:taskId
のURLでアクセスすると、
TaskShowComponentが表示されます。
:taskId
の部分は、任意のタスクIDが入ります。このURLパラメータが、
先ほどのタスク詳細コンポーネントの中で使われていた
taskId
となります。http://localhost:8000/tasks/3
のように:taskId
の部分に好きな数字を入れてアクセスすると
タスク詳細コンポーネントが表示されます。ついでにタスク一覧コンポーネントに置いていた
「Show」ボタンのリンク先を設定しておきましょう。resources/js/components/TaskListComponent.vue+ <router-link v-bind:to="{name: 'task.show', params: {taskId: 1}}"> <button class="btn btn-primary">Show</button> + </router-link> + <router-link v-bind:to="{name: 'task.show', params: {taskId: 2}}"> <button class="btn btn-primary">Show</button> + </router-link> + <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}"> <button class="btn btn-primary">Show</button> + </router-link>これで、一覧ページの「Show」ボタンをクリックすると
タスク詳細ページに遷移するようになりました。commit:タスク詳細コンポーネント実装
タスク登録コンポーネント実装
次にタスク登録コンポーネントを実装します。
まずコンポーネントファイル作成。
resources/js/components/TaskCreateComponent.vue+ <template> + <div class="container"> + <div class="row justify-content-center"> + <div class="col-sm-6"> + <form> + <div class="form-group row"> + <label for="title" class="col-sm-3 col-form-label">Title</label> + <input type="text" class="col-sm-9 form-control" id="title"> + </div> + <div class="form-group row"> + <label for="content" class="col-sm-3 col-form-label">Content</label> + <input type="text" class="col-sm-9 form-control" id="content"> + </div> + <div class="form-group row"> + <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> + <input type="text" class="col-sm-9 form-control" id="person-in-charge"> + </div> + <button type="submit" class="btn btn-primary">Submit</button> + </form> + </div> + </div> + </div> + </template> + + <script> + export default {} + </script>
ただ空のフォームを表示しているだけです。
現時点では送信処理は書いていません。このコンポーネントをVue Routerに登録します。
resources/js/app.jsimport VueRouter from 'vue-router'; import HeaderComponent from "./components/HeaderComponent"; import TaskListComponent from "./components/TaskListComponent"; + import TaskCreateComponent from "./components/TaskCreateComponent"; import TaskShowComponent from "./components/TaskShowComponent"; { path: '/tasks', name: 'task.list', component: TaskListComponent }, + { + path: '/tasks/create', + name: 'task.create', + component: TaskCreateComponent + }, { path: '/tasks/:taskId', name: 'task.show', component: TaskShowComponent, props: true },これで、
http://localhost:8000/tasks/create
でアクセスすればタスク登録ページが表示されます。ついでにヘッダーコンポーネントに置いていた
「Add」ボタンのリンク先を設定しておきます。resources/js/components/HeaderComponent.vue<div> <router-link v-bind:to="{name: 'task.list'}"> <button class="btn btn-success">List</button> </router-link> + <router-link v-bind:to="{name: 'task.create'}"> <button class="btn btn-success">ADD</button> + </router-link> </div>commit:タスク登録コンポーネント実装
タスク編集コンポーネント実装
次に、タスク編集コンポーネントを実装します。
まずコンポーネントファイルを作成。
resources/js/components/TaskEditComponent.vue+ <template> + <div class="container"> + <div class="row justify-content-center"> + <div class="col-sm-6"> + <form> + <div class="form-group row"> + <label for="id" class="col-sm-3 col-form-label">ID</label> + <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId"> + </div> + <div class="form-group row"> + <label for="title" class="col-sm-3 col-form-label">Title</label> + <input type="text" class="col-sm-9 form-control" id="title"> + </div> + <div class="form-group row"> + <label for="content" class="col-sm-3 col-form-label">Content</label> + <input type="text" class="col-sm-9 form-control" id="content"> + </div> + <div class="form-group row"> + <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label> + <input type="text" class="col-sm-9 form-control" id="person-in-charge"> + </div> + <button type="submit" class="btn btn-primary">Submit</button> + </form> + </div> + </div> + </div> + </template> + + <script> + export default { + props: { + taskId: String + } + } + </script>
詳細ページと同様に、
taskId
をURLパラメータで受け取り、
IDの欄にデータを表示しています。このコンポーネントをVue Routerに登録します。
resources/js/app.jsimport TaskCreateComponent from "./components/TaskCreateComponent"; import TaskShowComponent from "./components/TaskShowComponent"; + import TaskEditComponent from "./components/TaskEditComponent"; { path: '/tasks', name: 'task.list', component: TaskListComponent }, { path: '/tasks/create', name: 'task.create', component: TaskCreateComponent }, { path: '/tasks/:taskId', name: 'task.show', component: TaskShowComponent, props: true }, + { + path: '/tasks/:taskId/edit', + name: 'task.edit', + component: TaskEditComponent, + props: true + },これで、
http://localhost:8000/tasks/:taskId/edit
にアクセスするとタスク編集ページが表示されます。
:taskId
の部分は任意のタスクIDになります。ついでにタスク一覧コンポーネントに置いていた
「Edit」ボタンのリンク先も設定しておきます。resources/js/components/TaskListComponent.vue+ <router-link v-bind:to="{name: 'task.edit', params: {taskId: 1}}"> <button class="btn btn-success">Edit</button> + </router-link> + <router-link v-bind:to="{name: 'task.edit', params: {taskId: 2}}"> <button class="btn btn-success">Edit</button> + </router-link> + <router-link v-bind:to="{name: 'task.edit', params: {taskId: 3}}"> <button class="btn btn-success">Edit</button> + </router-link>commit:タスク編集コンポーネント実装
おわりに
これで、
・タスク一覧ページ
・タスク詳細ページ
・タスク登録ページ
・タスク編集ページ
が実装できました。現時点ではAPIでデータを取得する処理はできていませんが、
この状態でもVue.jsによる 静的な SPAにはなっています。もしデータベースを利用しないような
ウェブサイトなどをVue.jsでSPAとして構築する場合は
今回解説した内容を基本として
ページの追加をしていくだけです。それでは、次にLaravelのAPI実装に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
- 投稿日:2019-12-18T08:56:52+09:00
Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
はじめに
Vue.jsとLaravelによるSPA実装のチュートリアル記事です。
本記事は、4本の連載記事の1本目です。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編Vue.js 2.5
Laravel 6.7
を利用していますが、
別のバージョンでも大枠は同じだと思うので、
チュートリアルとしては参考にしていただけると思います。アプリ構成
タスクを
・一覧表示
・詳細表示
・登録
・更新
・削除
する機能がある
シンプルなアプリケーションです。一番シンプルな状態でCRUDの実装を
一通り実践することができます。Vue.jsでフロントエンドを実装し、
LaravelでAPIを実装します。各コンポーネントでは、
ajaxでLaravelのAPIにリクエストし、
データを取得、更新します。SPAになっているので、
フロントの各コンポーネントは
ページリロードせずにVue.jsによって表示切替されます。構築の流れ
まずこの記事で
環境構築と必要なパッケージのインストール、セットアップまで行います。そして、
1、Vue.jsで静的なSPA実装
2、LaravelでAPI実装
3、フロントエンドとAPIの結合
という順番で実装を進めます。上の構成図で言うと、
まず青色のVue.jsフロントエンド部分のみ実装し、
そのあと緑色のLaravelAPIを実装し、
最後に赤色のフロントエンドのAjax通信部分を実装してAPIと結合する
という流れです。少し長くなるので、
上記の3ステップはそれぞれ別のQiita記事とします。完成品のソースコードはGitHubに公開しています。
https://github.com/MinatoNaka/VueLaravelSpaまた、構築手順の通り1ステップごとにコミットしていますので、
コミット一覧を順に追っていくと
実装の流れが理解しやすいと思います。
https://github.com/MinatoNaka/VueLaravelSpa/commits/master環境構築
それでは、この記事では
環境構築と必要なパッケージのインストール、セットアップを済ませます。PHP、Composer、NPMが利用可能な環境での構築を前提としています。
(筆者はWindowsのPCにて構築しています)Laravelプロジェクト作成
まずは、
新品のLaravelプロジェクトを作成します。
任意のディレクトリで、下記コマンドを実行。composer create-project --prefer-dist laravel/laravel vue-laravel-spa
commit:Laravelプロジェクト作成
新品プロジェクトの状態で
一度表示確認してみます。まずはサーバ起動
cd vue-laravel-spa php artisan serve
このURLでアクセスします。
http://localhost:8000/Laravelのウェルカムページが表示されれば
正常に動作しています。laravel/uiインストール
次に、laravel/uiというパッケージをcomposerでインストールします。
これは、
Laravelでフロントエンド開発をするための
ベースを簡単に提供してくれるツールです。
下記コマンドを実行。composer require laravel/uicommit:laravel/uiインストール
laravel/ui vueインストール
先ほどインストールしたlaravel/uiを使うと、
bootstrapやvue、reactなどさまざまな
フロントエンドのベースコードを生成できます。
Laravel 6.x JavaScriptとCSSスカフォールド今回はvueのベースを作ります。
php artisan ui vueこのコマンドを実行すると、
package.jsonに様々なフロントエンドパッケージが追加されたり、
ベースとなるjsファイルやサンプルのVueコンポーネント、
Laravel Mixの設定ファイルなどが自動で配置されます。
commit:laravel/ui vueインストールフロントエンドパッケージインストール
laravel/uiのvueベースをインストールした際に、
必要なフロントエンドパッケージがpackage.jsonに追記されました。
bootstrap、jquery、vueなどが追記されています。これらのパッケージをインストールします。
npm install
このコマンドを実行したら、
/node_modules/
ディレクトリが作成され、
その配下に様々なパッケージのディレクトリ、ファイルが追加されます。commit:フロントエンドパッケージインストール
※/node_modules/
ディレクトリはgitignoreされているためコミットに含まれませんVue Routerインストール
今回はVue.jsでSPAを作るので、
Vue Routerというパッケージを追加でインストールしておきます。Vue Routerとは、
Vue.jsでSPAを構築するためのルーティング処理を行う
Vue公式のツールです。npm install --save vue-routercommit:Vue Routerインストール
フロントエンドビルド実行
必要なパッケージは全てインストール完了したので、
最後にフロントエンドソースコードをビルドしてみます。npm run devこのコマンドを実行することで、
Laravel Mixのビルド処理が実行され、
コンパイルされたjs、cssが
/public/js
public/css
に出力されます。
※Laravel Mixについては詳しく言及しません。わからない方は、こちらの記事を参照ください
Laravel Mixとは?webpackをより便利に、簡単に。Laravel以外でも使えるよ。この後実装するHTMLファイルでは、
このコンパイルされたjs、cssを読み込むことになります。コンパイル済みファイルはgit管理する必要がないので
gitignoreに追記しておきます。.gitignore+ /public/js + /public/css
今後jsファイルやcssファイル、vueコンポーネントを更新した際は、
毎回npm run dev
でソースをビルドしないと画面に反映されないので注意してください。毎回ビルドを実行するのが面倒な場合は
npm run watch
を実行するとウォッチモードになり
ビルド対象ファイルを更新、保存すると自動でビルドが実行されるようになるので便利です。おわりに
これで、環境構築と必要なパッケージ類のインストールは完了です。
次は「Vue.jsで静的なSPA実装」に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
- 投稿日:2019-12-18T03:29:03+09:00
人生を要約するサービス "Digest-of-Life" -5秒でエモい画像を作れるようにするProject-
お詫び
アドベントカレンダーの記事と同時にリリースする予定で作成していましたが、
バックエンドで使用するAPIのproduction Licence
が間に合わず、リリースできませんでした...
来月くらいにリリースする予定ですので、もしご興味を持っていただけたならば、ストックをお願いします。
(記事の更新で通知させていただきます。)
作ったもの(というか作っているもの)
"Digest-of-Life"
人生を要約するサービスを作ってます。
提供するものは、大切にしたい言葉を、きれいな画像とともに保存するサービスです。
こっそり記録していってもいいし、その場で画像に合成して共有してもいい
文字を画像にしたいとき、きれいな画像で言葉をドーンと伝えたいとき等にご利用ください。
動機(なぜ作ろうと思ったか)
伊坂幸太郎著「モダンタイムス」の作中のセリフですが、昔読んでからずーっと頭の片隅に残ってまして、、、
でも、個人的には哲学は人生の要約だと信じているので、それを形にできたらなと思い、
今回、個人の哲学を記録するサービスを作るに至りました。アプリケーション構成
ほぼすべてFirebaseで作成しました。
API
もデータベース
もログイン機能
もjavascript
ちょこっと書くだけで実装できるFirebase
先生素晴らしい!ホスティングサービス:
Firebase Hosting
- これがないとWebサービスが動かない。
- ローカルのエミュレータで動作確認したあと、コマンド1つでデプロイできる。
API:
Firebase Functions
- 面倒なサーバーサイドのAPIを
javascript
で数行書いてデプロイするだけで動く。- 今回は、
Unsplush api
のapi key
を秘匿するために使用。データベース:
Firebase Realtime Database
- メソッド1つ書くだけで、clientからDBにCRUDできる。すごい。。。
- 今回は、ユーザーが保存した言葉や写真のデータを永続化するために使用。
ユーザー管理:
Google アカウント
Firebase
側にGoogle ログインをアプリに統合
する機能が用意されていたので。- ユーザーを管理せずに、ユーザーを識別できるって素晴らしい。
画像サービス:
Unsplush api
- 今回の目玉、めちゃきれいな画像が大量にあるサイトのAPI
- APIを利用すると、細かい検索機能が使える。
- 普通の
developアカウント
だと50requests/hour
、ガイドラインに沿ってることを示せれば、Production版
のapi key
がもらえる要素技術
使用した技術要素と検証に使ったサンプルコードを書きます。
vuetify
なるべくCSS書かないマン
- https://vuetifyjs.com/ja/
-<v-XXXX>
のような独自のコンポーネントを使用して、独自のクラスでflex
margin
padding
等を適用します。
- 今回の見た目はほぼvuetify
のコンポーネントの見た目そのままになってます。
-モーダル
やページング
、トースト
等がすごく楽になります。
html2canvas
Webページの一部を画像にする
- https://html2canvas.hertzen.com/
- 「WebページをレンダリングしてCanvas
上に描画する」処理を行います。
- 2種類のやり方のうち後者を採用しています。
①読み込んだ要素を
toDataURL
にして<img>
のsrc
に埋め込むタイプ
(こちらの記事より: [JavaScript] JSだけでスクリーンショットを撮ってダウンロードもする方法(Qiita))html<div id="target"> <!-- ここがキャプチャされる --> </div> <!-- 下に生成した画像が出る --> <img src="" id="result" />javascripthtml2canvas(document.getElementById("target"),{ onrendered: function(canvas){ //imgタグのsrcの中に、html2canvasがレンダリングした画像を指定する。 var imgData = canvas.toDataURL(); document.getElementById("result").src = imgData; } });②読み込んだ要素を指定した場所の子要素に
canvas
を挿入するhtml<div id="target"> <!-- ここがキャプチャされる --> </div> <div id="result"> <!-- ここに生成した画像が出る --> </div>javascripthtml2canvas(document.querySelector("#target")) .then(function(canvas) { var result = document.querySelector("#result"); result.innerHTML = ""; result.appendChild(canvas); }ハマったところ
vuetify
の<v-text-field>
でvue.js
のv-model.lazy
が使えないイベントリスナーを使用して手動で行う必要があります
- https://github.com/vuetifyjs/vuetify/issues/1810
- 使えないようです。「入力が完了したら自動で検索」を実装したかったのですが、できませんでした。
- エンターキーで検索に切り替えました。
tmp.vue<v-text-field v-model="serchWords" @keydown.enter="execSearch(serchWords)"/>
html2canvas
で、外部から読み込んだ画像が表示されないデフォルトだと
CORS
で引っかかって、描画できないみたいです。attribute: allowTaint
type: boolean
default: false
description: Whether to allow cross-origin images to taint the canvas
- こちらを参考に、
option
を追加したらうまくいきました。javascripttml2canvas(document.querySelector("#target"), { + letterRendering: 0, + useCORS: true, + allowTaint: true }).then(function(canvas) { var result = document.querySelector("#result"); result.innerHTML = ""; result.appendChild(canvas); });
Firebase Funtions
で外部通信ができないBilling account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
- 無料枠(Sparkプラン)だとダメみたいです。
- Flameプラン($25/月)に切り替えて解決しました。
firebase emulators
のAPIの向き先がクラウドになる。$ firebase emulators:start
上記コマンドで
Hosting
とFunctions
を起動した状態で、Hosting上のvue.js
からAPI(Functions)
を叩くと、クラウドのFunctions
を見に行ってしまい、CROS
エラーになる。(404とかではなくて)
emulator
ではHosting
だけを上げて、Functions
はデプロイしてテストするようにしました。(functions.https.onCall()
の方を使ってます。)- 見逃してるだけできっと設定があると思います。
今後の展望
Unsplush
からProduction api key
がいただけたらサービスとしてリリースしようと思います。(1月予定)- 欲しい機能とかあれば気軽にコメントください!!喜びます
v1.0予定機能- googleでログインでき、「個人の言葉」を美しい画像ともに保存できる - 画像と言葉の組み合わせで、「5秒でエモい画像が作れる」 - 画像のランダム生成で大喜利 - Twitterでの共有最後に
ここまで、お読みいいただきありがとうございます!
最後に、開発途中で特に意味もなく作った画像を添えておきます。
おまけ:宣伝用Key Note
- 投稿日:2019-12-18T02:45:16+09:00
Vuetify�の開発に貢献する(ドキュメント翻訳とOSSコミット)
はじめに
VuetifyはVue.jsのUIフレームワークで、OSSで開発されています。もしVuetifyの開発に貢献していきたい場合、大きく分けて次の2つの方法があります。
- ドキュメントを翻訳する
- バグ修正をしてPRを投げる
これらについて自分自身の経験を元に、具体的な方法を解説していきます。
ドキュメントを翻訳する
Vuetifyのドキュメントには、日本語版のページがあります。ただページを見てもらうと分かる通り、日本語訳がまだまだ不十分です。
バージョン1系のころまでは翻訳もGitHubでのPRベースでも受付けていたようですが、現行のバージョン2系ではCrowdinという、翻訳支援のサービス上で翻訳するようになっています。Crowdinでの翻訳作業について
CrowdinはGitHubのアカウントで登録することができます。登録後にVuetifyのプロジェクトページから、翻訳を開始することができます。プロジェクトページには各言語の翻訳の進捗状況が載っているのですが、日本語は現状「8%」とかなり低いです?
Crowdinは翻訳支援のサービスということもあり、翻訳に関する機能が充実しています。以下は実際のCrowdin上の編集画面です。左から翻訳したいワード選び入力欄に翻訳を入力して、「SAVE」ボタンで保存します。
ワードによっては入力欄に翻訳提案を出してくれることがあります。今回の場合は他の箇所で「Hide controls」が「コントロールを隠す」とすでに翻訳されていたため、「Hide on scroll」が類似ワードとして判定されました1。これを「コントロールを隠す」から「スクロールを隠す」に変更するだけで翻訳が済むので、作業のスピードアップや表現の統一に繋げることができます。日本語版のページに反映されるまでの流れ
実際の流れは不明ですが、観察する限りでは次のような流れで反映しているようです。私たちが行うのは1〜3までの部分で、これを繰り返していきます。
- Vuetifyのプロジェクトページから、「japanese」を選択
- 翻訳したいファイルを選択
- 編集画面から翻訳して保存
- 翻訳したファイルがコアメンバーによって、GitHubにマージされる(例:https://github.com/vuetifyjs/vuetify/pull/8632)
- リリース
バグ修正をしてPRを投げる
Vuetifyのソースコードはvuetifyjs/vuetifyのリポジトリで管理されています。バグ修正は翻訳に比べると難易度が高く技術的なハードルも上がります。ただスター数が20000以上という大規模リポジトリで、README.mdにも自分のGitHubアイコンが表示されるようになるので、結構嬉しいです!
ちなみに私自身が初めてOSSにPRを出してマージされたのも実はVuetifyでした2。ルールに沿って意図した修正ならそこまで厳しい指摘は飛ばずにマージされる印象なので、大規模リポジトリと恐れずに初めての方でもおすすめです。バグ探し
自分でバグを見つけてPRを出すことももちろんできますが、そう簡単に発見できるものでもないのでIssueから探してみるのがおすすめです。いっぱいあるのでどれに取り組むのか悩ましいですが、自分の場合は次のような基準で探しています。
[Bug Report]
というタイトルが付いていること
- T: bugというラベルが付いているとなお良い(コアメンバーが少なくとも一度は確認して付けたラベルのため、バグである信ぴょう性が高そう)
- 誰もアサインされていないこと
- codepenなどへのリンクがあり、そのページでバグが再現できること
また初めての方は、good first issueのラベルがついたIssueもあるので、これを手がかりにするのも一つの手です。
環境構築
取り組むIssueを決めたら、環境構築を行っていきます。ここの内容についてはドキュメントのContributingにも書かれているので合わせてご参照ください。
commitizenのインストール
Vuetifyのリポジトリのコミットはcommitizenというツールで、対話的に生成されたコミットになっています(たまに普通のコミットもあったりしますが・・・)。そのためcommitizenをインストールしておいて、PRを出すときのコミットもcommitizenを使って行うようにします。
$ npm install -g commitizen開発環境のセットアップ
次にvuetifyjs/vuetifyリポジトリをforkしてcloneをします。その後、Vuetifyのソースコードのフォルダに移動して、パッケージのインストールとビルドを実行します。
$ git clone git@github.com:<github name>/vuetify.git $ cd vuetify/packages/vuetify/ $ yarn $ yarn buildプレイグラウンドファイルのコピー
リポジトリの「packages/vuetify/dev」フォルダに、
Playground.template.vue
というファイルがあります。
開発環境はこのフォルダ内のPlayground.vue
のvueファイルの内容で起動するようになっているため、そのためのサンプルファイルです。デフォルトだとPlayground.vue
はバージョン管理に入っていないために存在しないため、いったん同じ内容でコピーをしておきます。$ pwd <project root>/vuetify/packages/vuetify $ cp dev/Playground.template.vue dev/Playground.vue開発環境で修正したコンポーネントなどの動作を確認したいときは、この
Playground.vue
ファイルを修正して確認していく流れになります。後にまた書きますが、PRを出すときもこのPlayground.vue
ファイルの内容をペーストする必要があります。開発環境の起動
開発環境を起動するには次のコマンドを実行します。特に設定を変えてなければコンパイル後にhttp://localhost:8080 で、「Welcome to Vuetify」というタイトルのページが開くはずです。
$ yarn dev開発の流れ
開発環境が構築できたら、実際の修正に移っていきます。といっても例がないと説明がしずらいので今回は、[Bug Report] v-btn: Loading indicator too large on small buttonsというIssueに、仮に私自身が取り組む場合の流れを紹介していきます。
修正するコードに当てをつける
Issueタイトルと付いているラベルから、Buttonsに関するバグのようです。Vuetifyのコンポーネントのソースコードは「packages/vuetify/src/components」フォルダ配下に、コンポーネントごとにフォルダが切られています。Buttonsの場合は「packages/vuetify/src/components/VBtn」のような具合です。
コンポーネントのフォルダの中身は、コンポート定義・ユニットテスト・sassのスタイルなどが入っていて、他のコンポーネントもだいだいはこの構成になっています。このフォルダの中身を直せばバグも直るだろうという当てを付けます。
プレイグラウンドファイルを修正
コードを直したあと、先ほど作成した
Playground.vue
にButtonsのコンポーネントを置いてみて、開発環境で確認します。codepenがあるIssueならそれをコピペして、貼り付けるのが楽です。<template> <v-container> <!-- --> <div class="text-center"> <v-btn outlined x-small loading>btn</v-btn> </div> </v-container> </template> <script> export default { data: () => ({ // }) } </script>commitizenでコミットをする
開発環境でうまく動作しそうだったら、コミットします。前述した通り、commitizenでコミットを行います。
git cz
というコマンドを打つと次のような対話式のターミナルが開き、これに順番に答えていきます。詳しい説明はコミットのガイドラインがドキュメントにあるので、こちらをご確認ください。$ git cz cz-cli@4.0.3, cz-conventional-changelog@3.0.1 ? Select the type of change that you're committing: (Use arrow keys) ❯ feat: A new feature fix: A bug fix docs: Documentation only changes style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) refactor: A code change that neither fixes a bug nor adds a feature perf: A code change that improves performance test: Adding missing tests or correcting existing testsPRを出す
修正をして大丈夫そうだったらPRを出してみましょう!まず修正内容によってPRを出すブランチが決まっているので、ドキュメントのプルリクエストの部分の説明をよく読んでおきます。バグ修正の場合は
master
ブランチへのPRになります。
またPRはテンプレートがあるので、これに沿った形でPRの説明を埋めていきます。説明通りに内容を埋めたりチェックを付けていけばそこまで難しくないですが、気をつけるべきところは次の点です。
- Motivation and Context
Issueに対しての修正の場合は、#9870
のようにIssue番号を書く- Markup
// Paste your FULL Playground.vue here
となっている箇所に、自分の開発環境で動かしていた修正内容が確認できるような、Playground.vue
の中身をペーストします。PRをサブミットするとCIが動作します。もしバツマークが付いた場合はコードカバレッジのチェックに引っ掛かってしまっている場合が多いです。その場合はテストコードを追加する必要があります。
テストコードについても説明すると長くなってしまう(自分自身の理解が足りないのもある)ので、コンポーネントのフォルダの__tests__
配下の既存テストを参考にして、追加するのが一番良いと思います。Unit testingのページも合わせてご参照ください。無事にCIが通ったら、あとはマージされることを祈りましょう!
まとめ
少し長くなってしまいましたが、改めて今回はVuetifyの開発に貢献する方法として、次の2点を解説しました。
- ドキュメントを翻訳する
- バグ修正をしてPRを投げる
今後もVuetifyが開発され続けていくためにも引き続き貢献は続けていきたいと思っています。今回の記事で少しでもみなさんがVuetifyに貢献するきっかけになれば嬉しく思っています!
Translation Memoryという機能によって、行われています。 ↩
- 投稿日:2019-12-18T01:56:31+09:00
Vue-multiselectをnuxt.jsで使用する
概要
Vue.jsには
Vue-multiselect
という、多機能なセレクトボックスのライブラリがありますが、これをnuxt.jsで使いたい場合の方法を記載します。Vue-multiselectについて
どのような機能があるかは、Vue-multiselectにて紹介されています。また、「Vue-multiselect」を使って万能なセレクトボックスを実装するの記事も参考になります。
サーチ機能や、タグを追加する機能もあり、かなり便利な感じはします。nuxt.jsでの使用
nuxt-vue-multiselectというライブラリを使用します。タグの使い方は、上記のVue-multiselectと同様です。
導入方法は以下の通りです。①install
npm i nuxt-vue-multiselect
を実行。②nuxt.config.jsへの設定追加
modulesのセクションへ、
'nuxt-vue-multiselect'
を追加。
- 投稿日:2019-12-18T01:12:47+09:00
Vue.js + Vue2-DropzoneでS3の署名付きURLへファイルアップロード
この記事はうるる Advent Calendar 2019 18日目の記事です。
はじめに
株式会社うるるにお世話になっている、フリーランスエンジニアの福田と申します。
昨日の記事で、S3の署名付きURLを用いてファイルアップロードを行う方法が紹介されていました。
そこで、「じゃあフロントエンドから送るにはどうすりゃいいの?」という記事を書いてみたいと思います。要約
- Vue.jsでS3の署名付きURLを用いてファイルアップロードを行う方法の紹介です
- Vue2-Dropzoneを使用します
- CodeSandboxを使って動かしながら読み進められるように書いてます
スタート!
CodeSandbox上でVue2-DropzoneをInstallする
- 上記のリンクからCodeSandboxへアクセス
- DependenciesのAdd Dependencyボタンをクリック
- Vue2-dropzoneを検索して追加
これでVue2-Dropzoneを使用できるようになりました。
とりあえず動くようにする
Vue2-dropzoneのサイトのガイドに沿って修正してみます。
App.vueを、こういう風に修正しましょう。<template> <div id="app"> <vue-dropzone ref="myVueDropzone" id="dropzone" :options="dropzoneOptions"></vue-dropzone> </div> </template> <script> import vue2Dropzone from 'vue2-dropzone' import 'vue2-dropzone/dist/vue2Dropzone.min.css' export default { name: "App", components: { vueDropzone: vue2Dropzone }, data: function () { return { dropzoneOptions: { url: 'https://httpbin.org/post', thumbnailWidth: 150, maxFilesize: 0.5, headers: { "My-Awesome-Header": "header value" } } } } }; </script>Drop files here to uploadと書かれた四角い領域が表示されましたか?
そこをクリックするか、ファイルをドラッグアンドドロップすればサムネイルが表示されます。
これでUIは完成です。Fileオブジェクトを送信できるようにする
次はFileオブジェクトを送信できるようにしましょう。
Vue2-DropzoneはFormデータとして画像データを送信しますが、署名付きURLでS3へ送る場合はFileオブジェクトを送信する必要があります。
その方法を説明します。Vue2-Dropzoneには様々なイベントが存在します。
ファイル送信時に発火するsending
イベントにメソッドイベントハンドラを定義すると、引数としてFileオブジェクトとXHRオブジェクトを受け取ることができます。
そのXHRオブジェクトのBodyにFileオブジェクトをセットすればOKです。
また、署名付きURLへオブジェクトを送信する場合はPUTメソッドで送信する必要があるので、その設定も変えます。こちらのissuesを参考にしました。
https://github.com/enyo/dropzone/issues/33#issuecomment-150659202修正箇所を抜粋したものがこちら。
<!-- method に PUT を明示 --> <!-- @vdropzone-sending を追加 --> <vue-dropzone ref="myVueDropzone" id="dropzone" method="PUT" :options="dropzoneOptions" @vdropzone-sending="sending" ></vue-dropzone>export default { … methods: { sending(file, xhr) { const _send = xhr.send; xhr.send = function() { // Fileオブジェクトを送信するようにする _send.call(xhr, file); }; } } }ファイルの送信先を動的に変える
通常、ファイルのアップロード先は
options
のurl
で指定したURIになるのですが、これを署名付きURLに変えます。
DropzoneのQueueにあるファイルが処理されるタイミングでprocessing
イベントが発火するので、その時にoptions
のurl
を変えてやります。
以下、修正箇所の抜粋です。<!-- @vdropzone-processing を追加 --> <vue-dropzone ref="myVueDropzone" id="dropzone" method="PUT" :options="dropzoneOptions" @vdropzone-sending="sending" @vdropzone-processing="processing" ></vue-dropzone>export default { … methods: { … processing(file) { // 署名付きURLを取得 // httpbin はHTTPリクエストをテストできるサービス const uploadUrl = "https://httpbin.org/put" // 実際はこんな感じになると思います // const uploadUrl = await axios.get('/api/signed_url') // Dropzoneの送信先を変える this.$refs.myVueDropzone.dropzone.options.url = uploadUrl } } }
全体的にはこのようになっていると思います。
<template> <div id="app"> <vue-dropzone ref="myVueDropzone" id="dropzone" method="PUT" :options="dropzoneOptions" @vdropzone-sending="sending" @vdropzone-processing="processing" ></vue-dropzone> </div> </template> <script> import vue2Dropzone from "vue2-dropzone"; import "vue2-dropzone/dist/vue2Dropzone.min.css"; export default { name: "App", components: { vueDropzone: vue2Dropzone }, data: function() { return { dropzoneOptions: { url: "https://httpbin.org/post", method: "PUT", thumbnailWidth: 150, maxFilesize: 0.5, headers: { "My-Awesome-Header": "header value" } } }; }, methods: { sending(file, xhr) { const _send = xhr.send; xhr.send = function() { // Fileオブジェクトを送信するようにする _send.call(xhr, file); }; }, processing(file) { // 署名付きURLを取得 // httpbin はHTTPリクエストをテストできるサービス const uploadUrl = "https://httpbin.org/put"; // 実際はこんな感じになると思います // const uploadUrl = await axios.get('/api/signed_url') // Dropzoneの送信先を変える this.$refs.myVueDropzone.dropzone.options.url = uploadUrl; } } }; </script>アップロードしてみた結果がこちら。
processing
メソッド内でセットしたhttps://httpbin.org/put
にPUTメソッドで送信されていればOKです!
事前に署名付きURLを取得する
ファイル送信前に署名付きURLを取得しておけば、API呼び出しのターンアラウンドタイムを節約できます。
Dropzoneはファイルの追加のタイミングでイベントを発火するので、イベントハンドラを定義して署名付きURLを取得します。
取得したURLはFileオブジェクトに退避しておいて、ファイル送信時に使用します。<!-- @vdropzone-file-added を追加 --> <vue-dropzone ref="myVueDropzone" id="dropzone" method="PUT" :options="dropzoneOptions" @vdropzone-sending="sending" @vdropzone-processing="processing" @vdropzone-file-added="fileAdded" ></vue-dropzone>export default { … methods: { … fileAdded(file) { // ↓↓↓ここはprocessingから移植↓↓↓ // Fileオブジェクトに署名付きURLのプロパティを追加 file.uploadUrl = "https://httpbin.org/put" // 実際はこんな感じになると思います // file.uploadUrl = await axios.get('/api/signed_url') // ↑↑↑ここはprocessingから移植↑↑↑ }, processing(file) { // Fileオブジェクトに退避しておいた署名付きURLでファイル送信先を上書き this.$refs.myVueDropzone.dropzone.options.url = file.uploadUrl } } }
全体的にはこのようになっていると思います。
<template> <div id="app"> <vue-dropzone ref="myVueDropzone" id="dropzone" method="PUT" :options="dropzoneOptions" @vdropzone-sending="sending" @vdropzone-processing="processing" @vdropzone-file-added="fileAdded" ></vue-dropzone> </div> </template> <script> import vue2Dropzone from "vue2-dropzone"; import "vue2-dropzone/dist/vue2Dropzone.min.css"; export default { name: "App", components: { vueDropzone: vue2Dropzone }, data: function() { return { dropzoneOptions: { url: "https://httpbin.org/post", method: "PUT", thumbnailWidth: 150, maxFilesize: 0.5, headers: { "My-Awesome-Header": "header value" } } }; }, methods: { sending(file, xhr) { const _send = xhr.send; xhr.send = function() { // Fileオブジェクトを送信するようにする _send.call(xhr, file); }; }, fileAdded(file) { // ↓↓↓ここはprocessingから移植↓↓↓ // Fileオブジェクトに署名付きURLのプロパティを追加 file.uploadUrl = "https://httpbin.org/put"; // 実際はこんな感じになると思います // file.uploadUrl = await axios.get('/api/signed_url') // ↑↑↑ここはprocessingから移植↑↑↑ }, processing(file) { // Fileオブジェクトに退避しておいた署名付きURLでファイル送信先を上書き this.$refs.myVueDropzone.dropzone.options.url = file.uploadUrl; } } }; </script>先程と同様の結果になっていればOKです!
余談ですが、こんなことも
サムネイルをVue.jsのコンポーネントにする
サムネイル生成時に発火する
vdropzone-thumbnail
イベントを利用すれば、dataUrlを含んだFileオブジェクトを取得できます。
それをコンポーネントのprops
に渡してサムネイルを描画してやれば実現可能です。
コンポーネントなのでスタイルは自由に決められますし、様々なイベントを設定することもできます。サムネイルのドラッグアンドドロップ
サムネイルをコンポーネント化することができれば、DnDも実現可能です。
Vue.jsのDnDのライブラリは色々あると思いますが、それらと組み合わせることでリッチな画像アップローダーを簡単に作れます。この辺の話は別の機会に記事に纏められればと思います。
終わり
Advent Calendar 18日目でした。
明日19日目は tatsukoni さんによる記事を乞うご期待!
https://adventar.org/calendars/4548
- 投稿日:2019-12-18T01:03:59+09:00
遭難者を救うために学内Mapを作った話
自己紹介
こんにちは!2019年度の筑波大学園祭実行委員でWeb担当をしていました @SIY1121 です。
今回は学園祭の来場者向けWebマップアプリを開発しました!
すべて紹介するとかなり量が多くなるので。地図の表示部分に絞って少し紹介したいと思います。ざっくりいうと
- 全国2位の広さを誇る筑波大学 筑波キャンパス で学園祭が行われた
- 広いだけに、来場者が迷ってしまう
- Vue.js + SVG + 自作webpackloader でGoogleMapsライクなWebアプリを開発した
- コア機能はOSS化予定!
スマホに最適化されています↓
https://www.sohosai.com/map遭難者が相次ぐ大学
筑波大学 筑波キャンパスは全国で2番目に広いキャンパスです。
その大きさは258ha(ディズニーランド5個分)にもなります。
雙峰祭
雙峰祭(そうほうさい)は、毎年3万人以上が訪れる筑波大学の学園祭です。
一般の方がたくさんやってきますが、広いので迷う方も多いです。Webマップを作った
迷ってしまってもスマホで現在位置を調べたり、企画を調べたりできるWebアプリを作りました。
期間中10万ページビューを達成し、GPSを使用したユーザーは1.5万人にも登りました。
使ったもの
Vue.js
言わずと知れたフロントエンドのフレームワーク。
SVG
地図はイラレで作成し、SVGで出力しています。
SVGを使用することにより、拡縮しても画質が劣化しない上、建物や文字、アイコン等の要素を個別に制御することができるので
- 建物がタップできる
- 地図をピンチイン・アウトしても文字やアイコンのサイズや角度を固定したりできる
といったマップに欠かせない機能が実装しやすくなります。
eazy-pz-as
マップを拡縮したり回転できる機能は、easy-pz-asを使用しました。
指定したsvgを拡縮したり移動したりできるようになります。
ただ、これはVue.jsを想定した作りになっていないので、使うには多少工夫が必要でした。自作webpack loader
以下の3つの理由からwebpack loader を自作しました。
- Vue.js でSVGを制御するには
.vue
ファイルにSVGをインラインで記述する必要があり、イラレで生成された数万行のSVGを vueファイルに入れるのは扱いにくい- 文字やアイコンのサイズや回転を固定するには、一つ一つの要素に変形の中心座標を設定する必要があるため、すべての要素の座標を調べ、SVG内にベタ書きするのは人間がすることではない
- デザインの変更などにより、SVGがバージョンアップされるため、上記のようにSVGに変更を加えていると、もう一度変更し直す必要がある
そもそもwebpack loader とは?
webフロントエンド向けアセットバンドラ「webpack」の内部でファイルを変換したり、依存関係を構築するする工程で使用されます。
身近な例では、Vue.js単一コンポーネントや、Reactのjsxなど、ブラウザがそのままでは理解できないファイルをjsに変換するのに使われています。自作webpack loader で何をしてるか
今回作成したwebpack loaderは SVGにVueで制御するために必要な属性を自動で付与し、
.vue
にインライン展開します。
具体的には、指定した要素(アイコンや文字)のサイズや回転を固定するためにスタイルをバインディングします。一番簡単なwebpack loader
作り方はとっても簡単で、ソースコードをstringで受け取り、何かしらの処理を施したソースコードを返す関数をexportするだけです。
map-loader.js// webpackはnode.js上で動くため、CommonJS module.exports = function(source) { // 処理 return source }どのファイルにどのwebpack loaderを使うかはwebpackのconfigで定義できます。
VueやReactで専用のツールチェインを使う場合でも、それぞれの方法でwebpackのconfigを設定できます。今回はこの処理の中で SVG の処理&挿入を行っています。
SVGに属性を付加する
Vueファイルには
<img src="/path/to/svg" svg-map >
のように書いておき、正規表現で挿入するsvgファイル名を抽出します。(jsdomを使ってもいいかも)
次に、抽出したパス名からSVGファイルを読み込みます。読み込んだsvgはただの文字列なのでjsdomを使ってDOMを生成し、扱いやすくします。map-loader.jslet svgString = fs.readFileSync(filePath, 'UTF-8') const document = new JSDOM(svgString).window.document const elements = document.querySelectorAll('#アイコン > g') elements.forEach((el, index) => { const refID = `icon-${index}` el.setAttribute('ref', refID) el.setAttribute(':style',`getElementStyle('${refID}')`) }) svgString = root.outerHTML // 属性が付与されたsvg文字列上の例は、svgを読み込み
#アイコン
直下の要素(イラレ上でアイコン
レイヤー直下のオブジェクト)に対してref
と:style
属性を付与しています。
最後に、先程の imgタグを処理後のsvgStringで置換して、sourceとして返却します。挿入先のVueファイル
挿入先のVueファイルに以下のようなメソッドを追加しておきます。
webpack loader で:style
にバインドしたgetElementStyle
がこれに相当します。Map.vuegetElementStyle(refID) { const target = this.$refs[el] if (!target) return '' /* 地図の変形に応じて要素も変形 rotateZで要素を逆回転させると回転が固定されているように見える scaleで地図の拡大率の逆数を掛けることでサイズが固定されているように見える */ const transform = ` rotateZ(${this.mapTransform.rotate.deg * -1}deg) scale(${1 / this.mapTransform.scale}) ` // 要素を囲う最小の長方形を取得 const box = target.getBBox() // 要素が変形する中心を指定 transformOrigin = `${box.x + box.width / 2}px ${box.y + box.height / 2}px` // 要素に適用するstyleを返す return { transform, transformOrigin } }これで アイコンが地図の拡縮、回転を行っても固定されるようになります。
コンパイル時にvueファイルとSVGファイルが内部で統合されるので、巨大なvueファイルができることもなく、気軽にSVGを差し替えることもできます。おわりに
初めてwebpack loaderを自作しました。
今回の知見を生かして、コア機能はOSS化を予定しています。
誰でも簡単にWebマップを作れるようになって、遭難者を救っていただければ幸いです。
- 投稿日:2019-12-18T00:57:11+09:00
Nuxt.jsでvue-carouselで商品画像一覧をカスタマイズして表示する
概要
商品画像を画像と商品名の組み合わせのコンポーネントを作成し、カルーセルスライダーで表示を行う。
商品は4つずつ並べて横にスライドさせていけるイメージです。画面イメージ
参考
nuxt.jsにvue-carouselを導入してスライダーを作成
実装手順
vue-carouselのインストール
npm install -S vue-carouselNuxtのプラグインで
vue-carousel.js
を作成し、下記を実装するvue-carousel.jsimport Vue from 'vue' import VueCarousel from 'vue-carousel' Vue.use(VueCarousel)nuxt.config.jsplugins: [ { src: '~/plugins/vue-carousel', ssr: false } ],コンポーネント設計
今回は、4枚ごとの商品画像を表示するようのコンポーネントとして実装していおきます。
実装
実際のコンポーネントの実装
<carousel>
と<slide>
の中に商品画像と価格と商品名をv-for
で描画するようにする
:per-page="4"
で4枚単位で表示するように指定しました。中身のCSSの設定は
.VueCarousel-slide
の中に記載ProductCardCarousel.vue<template> <carousel :per-page="4" :pagination-enabled="false"> <slide v-for="(prodduct_item, key) in productList" :key="key"> <div class="product-card"> <div v-if="prodduct_item.imgURL != ''"> <img class="product-card-img" :src="prodduct_item.imgURL" /> </div> <div v-else> <img class="product-card-img" :src="require('@/assets/img/NoImage.png')" /> </div> <div class="product-card-content"> <div class="product-card-price"> {{ prodduct_item.price }}ポイント </div> <div class="product-card-text">{{ prodduct_item.name }}</div> </div> </div> </slide> </carousel> </template> <script> import Carousel from 'vue-carousel/src/Carousel.vue' import Slide from 'vue-carousel/src/Slide.vue' export default { components: { Carousel, Slide }, layout: 'client/simple', props: { productList: { type: Array, required: true, default: () => [] } } } </script> <style lang="scss" scoped> @import '~/assets/scss/base.scss'; .VueCarousel-slide { padding: $space_m $space_m $space_m $space_m; .product-card { .product-card-img { border-radius: 50%; height: 80px; } .product-card-content { text-align: center; .product-card-price { font-size: $font-size_xs; font-weight: bold; padding: $space-s 0 0 0; } .product-card-text { font-size: $font-size_xs; padding: $space-s 0 0 0; } } } } </style>
ProductCardCarousel.vue
を使用する際には下記のように実装<template> <div> <div v-for="(category_item, key) in categoryList" :key="key"> <div class="category-title">{{ category_item.categoryTitle }}</div> <product-card-carousel :product-list="category_item.productList" ></product-card-carousel> <product-link :url="category_item.categoryLink" :link-name="category_item.categoryName" ></product-link> </div> </div> </template> <script> import ProductCardCarousel from '~/components/client/ProductCardCarousel' import ProductLink from '~/components/client/ProductLink' export default { components: { ProductCardCarousel, ProductLink }, layout: 'client/simple', data() { return { categoryList: [] } }, // レンダリングの前に商品情報を取得する async asyncData({ app, params, store, $axios }) { const { data } = await $axios .$get( `/api/user/product/category/${params.id}?event_id=${ store.getters['event_info/eventSelected'].id }` ) .catch(errors => {}) return { categoryList: data } } } </script>まとめ
vue-carouselがあればデザインのカスタマイズもしつつ簡単に実装可能でした
あくまでメモ用なのでソースが汚かったりおかしかったりするかもしれませんが
良かったら参考にしてください。
- 投稿日:2019-12-18T00:31:00+09:00
[Vue + php] phpの配列データをVueで検索/ソートさせてみた
Vueでリスト検索とソート機能を作ってみたので
さらに応用して、phpの配列データからVueに流し込んで処理した機能を作ってみました。ロジック
php配列データ
↓
JSONデータに変換
↓
JSONデータをJSの配列に格納
↓
JSの配列をvueのdataに格納
これでvueで取り扱えるデータになりました。computed(算出プロパティ)
matched:
フォーム入力の数値 <= budgetの数値のリストのリストを表示
sorted:
ボタンのオンオフで昇順・降順ソートlimited:
limit数分表示できる<?php $list = [ ['id' => '1', 'name' => '商品A', 'price' => '500'], ['id' => '2', 'name' => '商品B', 'price' => '300'], ['id' => '3', 'name' => '商品C', 'price' => '2000'], ['id' => '4', 'name' => '商品D', 'price' => '5000'], ['id' => '5', 'name' => '商品E', 'price' => '1500'], ['id' => '6', 'name' => '商品F', 'price' => '250'], ['id' => '7', 'name' => '商品G', 'price' => '100'], ['id' => '8', 'name' => '商品H', 'price' => '750'], ]; $list_json = json_encode($list); ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Vue App</title> </head> <body> <div id="app"> <input v-model.number="budget">円以下 <p>{{ matched.length }}件表示中</p> <button v-on:click="order=!order">価格 ▼</button> <div v-for="item in limited" v-bind:key="item.id"> {{ item.name }}: {{ item.price }}円 </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script> <script> let list = JSON.parse('<?php echo $list_json; ?>'); const app = new Vue({ el: '#app', data: { // 検索初期値 budget: '', // 検索数 limit: 10000000000000, // 検索リスト list: list, // ソート初期値 order: false, }, computed: { matched: function() { return this.list.filter(function(el) { return el.price <= this.budget }, this) }, sorted: function() { return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc') }, limited: function() { return this.sorted.slice(0, this.limit) } } }); </script> </body> </html>