- 投稿日:2019-02-18T18:11:41+09:00
axiosでPOSTした画像をLaravel APIで保存する
タイトルの通りです。画像やファイルのstorage保存の記事はいくつか見かけましたが、jsからPOSTする記事についてはあんまり見かけなかったので書きました。
この記事ではaxios経由で画像をPOST、LaravelのAPI処理に渡しているサンプルコードを簡単に説明します。概要は以下の通りです。
- canvasに画像を描画し、blobデータを作成
- blobをformDataにappendして、axiosでPOST
- POSTされたblobファイルを、Laravel API側で処理・保存
View側の処理
データのBLOB化
以下の処理か
toBlob
を使ってCanvasに描画した画像ファイルをblob化します。index.vue// Canvasのデータをblob化 const type = 'image/png'; const dataurl = this.canvas.toDataURL(type); // this.canvasは用途に合わせて書き換え const bin = atob(dataurl.split(',')[1]); const buffer = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) { buffer[i] = bin.charCodeAt(i); } const blob = new Blob([buffer.buffer], {type: type});
"multipart/form-data
のデータをPOSTするので、blobファイルをFormData
インタフェースにappendする形でデータを作成します。index.vueconst data = new FormData(); data.append('photo', blob, 'image.png'); //'photo'というkeyで保存blobを格納したdataを
axios.post
の第二引数にセットすれば、POST可能です。index.vueaxios.post('/api/photo', data, { headers: { 'content-type': 'multipart/form-data' } }) .then(res => { console.log('success') }).catch(error => { new Error(error) });Laravel API側の処理
api用のControllerで設定した
store
定義内に、受け取ったblobデータをファイルとして保存するための処理を書きます。View側でFormData
内にappendしたphoto
データを$request
から参照し、storeAs
メソッドを実行することで簡単に保存が可能です。IndexController.phppublic function store(Request $request) { // ファイル名を取得 $filename = $request->photo->name; // blobデータをstorageに保存する // diskの指定を特にしなければ、例の場合。`storage/app/images/`に画像が保存される $path = $request->photo->storeAs('images', $filename); }
- 投稿日:2019-02-18T13:59:08+09:00
【随時追記予定】Vuetify オレオレコンポーネント
この記事は何?
Vue.js (Nuxt.js) 向け Material Design フレームワークの一つに「Vuetify」というものがあります。
この記事では、自分用に改造した Vuetify コンポーネントを随時まとめていく予定です。万が一「私も使ってみたいよ」という方がいらっしゃいましたら、MIT License の権利内でご自由にご利用ください。
Slider の ticks をアクティブ/非アクティブっぽく表示する
公式の Ticks labels をベースに、現在の値がどの位置にあるのかを見やすくした感じ。
thumb のアイコンはおまけです。See the Pen Vuetify - Custom slider with active/inactive styled ticks by blachocolat (@blachocolat) on CodePen.
- 投稿日:2019-02-18T13:28:06+09:00
Vueでダイアログを動的に生成してマウントするサンプル作ってみた
課題
Vueでサービスを作っててダイアログやモーダルを表示したいときに、公式だとdataにフラグを持たせてClickイベントでtrue/falseを切り替えるようにしていた。ただ、これだと画面内で表示したいダイアログが増えるほどにdataのフラグも増えるし、テンプレートの中に各フラグと紐づいたdialogタグを量産することになって、めちゃくちゃ見にくいし何より格好悪い。
なので、showDialog()的な感じでメソッド呼び出しでダイアログを表示させたいやったこと
renoinn/vue-dialog-sample
というわけで、DialogHelper.showDialog()とすることでダイアログを表示させるサンプルを作ってみた。解説
キモになるのは以下の点
Dialog.vuemethods: { attach () { if (!this.$parent) { this.$mount(); document.body.appendChild(this.$el); } else { this.$mount(); this.$parent.$el.appendChild(this.$el); } }, remove () { if (!this.$parent) { document.body.removeChild(this.$el); this.$destroy(); } else { this.$parent.$el.removeChild(this.$el); this.$destroy(); } }, close () { this.isShow = false; }, show () { this.attach(); this.isShow = true; }, afterLeave () { this.remove(); } }まずDialog.vue側で自身をappendChildしたり、removeChildするメソッドを用意してやる。この時単に
document.body.appendChild(this.$el);
とすると、Vueの仮想DOMツリーから外れてしまって、例えばvue-routerを使ってて画面遷移してもダイアログが消えないとか問題が起きてしまう。
なので、this.$parent
を見て、設定されていればその下にappendChildするようにする。DialogHelper.jsimport Vue from 'vue'; import Dialog from '@/components/Dialog.vue'; const DialogHelper = { showDialog (context, { subject, message, ok, cancel }) { let DialogVM = Vue.extend(Dialog); let vm = new DialogVM({ parent: context, propsData: { subject: subject, message: message, onPrimary () { ok(); vm.close(); }, onSecondary () { cancel(); vm.close(); } } }); vm.show(); } } export default DialogHelper;次にダイアログを生成するHelperを用意する。Vueのコンポーネントは単一コンポーネントファイルであっても、
Vue.extend(Component);
とすることで、ソースコード上でクラスとして使用できるようになる。
メソッドの引数でcontextを受け取って、コンストラクタでparent: context
としてやることで、上述のthis.$parent
を設定できるようにしておく。<script> import DialogHelper from '@/DialogHelper'; export default { methods: { showDialog () { DialogHelper.showDialog(this, { subject: 'Subject', message: 'open temporary dialog sample', ok: () => { console.log('click ok') }, cancel: () => { console.log('click cancel') } }); } } } </script>呼び出しはHelperをimportして、showDialogから呼び出す。第一引数でthisを渡しているので、呼び出したコンポーネントの下にDialogコンポーネントがappendされることになる。
thisじゃなくて、this.$root
やthis.$parent
を渡すこともできる。今回はダイアログだったけど、同じような感じでモーダルやトーストも実装できる。
参考URL
https://qiita.com/hako1912/items/8c0462203987f2cd15b1
https://kitak.hatenablog.jp/entry/2017/04/04/044829
https://github.com/paliari/v-slim-dialog
- 投稿日:2019-02-18T12:54:20+09:00
Nuxt.js 状態管理でハマった件
(Nuxt.js v2.4.2)
ノリでNuxt.js使ったらえらい目にあったので共有します。1日潰しました。対象読者はまぁ自分がWEBの経験が浅い(現時点で8ヶ月程度)なので初心者レベルの内容です。tl;dr
- window.location や href でページ遷移すると、Storeに格納にした状態が飛ぶ。理由としてはページが初回ロード扱いとなるため。nuxtServerInitアクションで状態を復元するコードを書かなくてはならない。
- NuxtLink や this.$router.pushを使用してページ遷移すると、ページが初期化されないため状態が維持される。
Nuxt.js とは
いろんな人が書いている。自分の言葉でまとめるなら、Vueを使ったWEBフレームワーク。
認証ページ を作るには
ログインしてたら、この画面が表示されて、そうでなければリダイレクト or エラーページに遷移する的なことをやりたい。そこで Storeを使用する。Nuxt.jsが梱包するVuexの機能に「Store」が含まれている。Storeは画面間でデータを共有できるグローバルデータみたいなもの。これを使って、認証ページの機能が作れる。
公式ページ 認証ルート - Nuxt.jsで、まさにサンプルが用意されておりこれを元に読み進めるのがいいけど、まずはそのページにあるソースコード本体をダウンロードしてほしい。ソースとドキュメントの内容が乖離してるし、ドキュメントは一部にフォーカスしてるのであまりあてにならない。雰囲気を理解する程度に留めた方がいいように思う。この記事でもそのソースコードを元にハマりポイントを吐露したい。
本題; Storeに格納されたデータが飛ぶ
ログイン後、適当なページに遷移させたいときにこんなコードを書くと思う。
ダウンロードしたソースコードを以下のように修正します...pages/index.vue〜中略〜 <script> methods: { async login() { try { await this.$store.dispatch('login', { username: this.formUsername, password: this.formPassword }) this.formUsername = '' this.formPassword = '' this.formError = null window.location = '/secure'; // ?追加したコード } catch (e) { this.formError = e.message } }, 〜以下略〜このコードで/secureページへ遷移しようとすると、store/index.jsに定義したauthUserというstateが破棄され、認証に失敗する。secure.vueへのページ遷移ができない。なぜ?
Storeを調査してみる
store/index.jsexport const state = () => ({ authUser: null })保持したいstateの定義。特に深掘りする理由なし。
store/index.jsexport const mutations = { SET_USER: function (state, user) { state.authUser = user } }stateを更新する部分。公式ページによると、このmutationsにstate更新処理を登録しておく必要がある。それ以外の場所では、更新できない。
store/index.jsexport const actions = { // nuxtServerInit is called by Nuxt.js before server-rendering every page nuxtServerInit({ commit }, { req }) { if (req.session && req.session.authUser) { commit('SET_USER', req.session.authUser) } }, async login({ commit }, { username, password }) { try { const { data } = await axios.post('/api/login', { username, password }) commit('SET_USER', data) } catch (error) { if (error.response && error.response.status === 401) { throw new Error('Bad credentials') } throw error } }, async logout({ commit }) { await axios.post('/api/logout') commit('SET_USER', null) } }login/logoutメソッドは自前で定義された関数だとしてnuxtServerInitというメソッドはなんだろう。VuexではなくNuxt.jsのコールバックらしい。でも、そんなライフサイクル見つからないと思ったらここに良い記事があった。図を抜粋する。
Webアプリを作るとき、初回に本体(というのが適切?)一式をダウンロードする。この時、nuxtServerInitがコールバックされる。公式ページのソースコード上ではセッションデータからstate情報を復元しようとしていることがわかる。つまり、nuxtServerInitはその名の通り初期化なので、stateを復元をしようとしなければそれが初期化されたままということになる。SPAでない場合、このイベントはページ遷移毎にコールされる・・・。と思ったら、このNavigateの部分を見てほしい。これはnuxt-linkを使ったページ遷移を意味している。つまり、nuxt-linkを使うとstateが保持される。
nuxt-link と state
index.vueに戻ると確かにログイン後に遷移可能なページ遷移をする場合、nuxt-linkが使用されている。NuxtLinkはnuxt-linkの別名だ。(なんで別名用意した?)
pages/index.vue<template> <div class="container"> 〜中略〜 <p> <NuxtLink to="/secret"> Super secret page </NuxtLink> </p> </div> </template> 〜以下略〜試しにこれをhrefに変えてやると遷移に失敗する。
<!-- NuxtLink to="/secret" --> <a href="/secret">このNuxtLinkをJavascriptで書くには以下のようになる。
this.$router.push('/secure')全体としてはこんな感じ
pages/index.vue〜中略〜 <script> methods: { async login() { try { await this.$store.dispatch('login', { username: this.formUsername, password: this.formPassword }) this.formUsername = '' this.formPassword = '' this.formError = null // window.location = '/secure'; this.$router.push('/secure'); // ?修正したコード } catch (e) { this.formError = e.message } }, 〜以下略〜リンクにせよNuxtLinkにせよ、ページを離れてから戻ってきたときに状態を維持したいならnuxtServerInitで状態を復元する必要があるけど...。
以上です。
- 投稿日:2019-02-18T09:43:11+09:00
Element から学ぶ Vue.js の component の作り方 その3 (card)わ
Element-ui とは
第3回になり、今まではあまりにも説明不足だったと反省しました。
Elemnt-ui は Vue.js のコンポーネントライブラリです。
Vue.js で作成されているため、インポートすることで様々なコンポーネントを利用可能になるます。
CSS フレームワークとも捉えることができ、デザイン済みのコンポーネントを簡単に利用することが可能です。
CSSフレームワークなので、ある程度整形されたコンポーネントに変更を加えて、自身のプロダクト色に染めることも可能です。今回は Elemnt 2.52 がベースになってます。
公式ページ
https://element.eleme.io/#/en-US\今回は card コンポーネントを解析していきたいと思います。
ソースの構成は
index.js src |- main.vueとなっていいます。
main.vue
main.vue<template> <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'"> <div class="el-card__header" v-if="$slots.header || header"> <slot name="header">{{ header }}</slot> </div> <div class="el-card__body" :style="bodyStyle"> <slot></slot> </div> </div> </template> <script> export default { name: 'ElCard', props: { header: {}, bodyStyle: {}, shadow: { type: String } } }; </script>main.vue を見ると、難しいことはしてなさそうです。
<div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">上記では class を指定しています。
class="el-card"
は固定で指定されます。
shadow
は 指定されている場合は is-指定した値 が class になります。
指定されて表示が切り替わるのはalways / hover / never
のいずれかです。
:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'
もし、shadow が指定しれていない場合は is-always-shadow クラスが bind されます。
<div class="el-card__header" v-if="$slots.header || header"> <slot name="header">{{ header }}</slot> </div>次の div は
el-card__header
が固定で指定されています。
$slots.header、または prop の header が指定されている場所は{{ header }}
に指定された要素が入り表示されます。<div class="el-card__body" :style="bodyStyle"> <slot></slot> </div>次の div は el-card__body が固定で指定されています。 また、style 要素に prop で指定する bodyStyle が bind されています。
bodyStyle は CSS スタイルを渡すことができます。例)
{color: 'red', 'background-color': 'gray'}
<el-card :body-style="{color: 'red', 'background-color': 'gray'}">このように body 部分のスタイルに適用されます。
<slot></slot>
は<el-card></el-card>
の中に記述したものがそのまま入ります。今まででてきた要素を組み合わせてみました。
下記がソースになります。
<el-card class="box-card"> <div slot="header"> <!-- ヘッダー --> <span>サンプルですよ</span> <!-- ヘッダーの内容1 --> <el-button style="float: right;">スロット=header に ボタン</el-button> <!-- ヘッダーの内容2 --> </div> <!-- ボディーのスロット --> ここが スロット です。 ボタンをおいてみた <div> <el-button type="danger"> スロットにボタン </el-button> </div> <div> テーブルをおいてみた <table border> <tr> <th>ID</th><th>NAME</th><th>AGE</th> </tr> <tr> <td>1</td><td>ぺけぺけ</td><td>55</td> </tr> <tr> <td>2</td><td>ぷけぷけ</td><td>48</td> </tr> </table> </div> </el-card>
- 投稿日:2019-02-18T09:43:11+09:00
Element から学ぶ Vue.js の component の作り方 その3 (card)
Element-ui とは
第3回になり、今まではあまりにも説明不足だったと反省しました。
Elemnt-ui は Vue.js のコンポーネントライブラリです。
Vue.js で作成されているため、インポートすることで様々なコンポーネントを利用可能になるます。
CSS フレームワークとも捉えることができ、デザイン済みのコンポーネントを簡単に利用することが可能です。
CSSフレームワークなので、ある程度整形されたコンポーネントに変更を加えて、自身のプロダクト色に染めることも可能です。今回は Elemnt 2.52 がベースになってます。
公式ページ
https://element.eleme.io/#/en-US\今回は card コンポーネントを解析していきたいと思います。
ソースの構成は
index.js src |- main.vueとなっていいます。
main.vue
main.vue<template> <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'"> <div class="el-card__header" v-if="$slots.header || header"> <slot name="header">{{ header }}</slot> </div> <div class="el-card__body" :style="bodyStyle"> <slot></slot> </div> </div> </template> <script> export default { name: 'ElCard', props: { header: {}, bodyStyle: {}, shadow: { type: String } } }; </script>main.vue を見ると、難しいことはしてなさそうです。
<div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">上記では class を指定しています。
class="el-card"
は固定で指定されます。
shadow
は 指定されている場合は is-指定した値 が class になります。
指定されて表示が切り替わるのはalways / hover / never
のいずれかです。
:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'
もし、shadow が指定されていない場合は is-always-shadow クラスが bind されます。
<div class="el-card__header" v-if="$slots.header || header"> <slot name="header">{{ header }}</slot> </div>次の div は
el-card__header
が固定で指定されています。
$slots.header、または prop の header が指定されている場所は{{ header }}
に指定された要素が入り表示されます。<div class="el-card__body" :style="bodyStyle"> <slot></slot> </div>次の div は el-card__body が固定で指定されています。 また、style 要素に prop で指定する bodyStyle が bind されています。
bodyStyle は CSS スタイルを渡すことができます。例)
{color: 'red', 'background-color': 'gray'}
<el-card :body-style="{color: 'red', 'background-color': 'gray'}">このように body 部分のスタイルに適用されます。
<slot></slot>
は<el-card></el-card>
の中に記述したものがそのまま入ります。今まででてきた要素を組み合わせてみました。
下記がソースになります。
<el-card class="box-card"> <div slot="header"> <!-- ヘッダー --> <span>サンプルですよ</span> <!-- ヘッダーの内容1 --> <el-button style="float: right;">スロット=header に ボタン</el-button> <!-- ヘッダーの内容2 --> </div> <!-- ボディのスロット --> ここが スロット です。 ボタンをおいてみた <div> <el-button type="danger"> スロットにボタン </el-button> </div> <div> テーブルをおいてみた <table border> <tr> <th>ID</th><th>NAME</th><th>AGE</th> </tr> <tr> <td>1</td><td>ぺけぺけ</td><td>55</td> </tr> <tr> <td>2</td><td>ぷけぷけ</td><td>48</td> </tr> </table> </div> </el-card>
- 投稿日:2019-02-18T09:03:54+09:00
Vuex + watch + subscribeActionで拡張性のある単方向データフローを設計する
フロントエンドで開発していると、時々特定のstateの変化をトリガーとして処理を行いたいときがあります。
例えばブログサービスで記事の編集を開始すると未保存状態ではページ遷移時にブラウザの標準ポップアップを表示するなど。こんなときにVue、Vuex、Nuxt.jsでどのように実装していくかの記事になります。今回実装するもの
実装するのは以下のようなテキストを入力して保存を押すとサーバー側に保存(今回はモック)されるようなものです。
①全体を覆っているlayouts/default.vueのmount時にStoreのActions initをdispatchする
②initでサーバー側から取得したデータ(今回はモック)をstateにcommitし、最後にMutationsのchangeIsInitializedをcommitしてstateのisInitializedをtrueにする
③isInitializedがtrueになり、layouts/default.vue内でv-ifによって制御されていた子コンポーネントの部分が表示され、page/index.vueが表示される
④その後は各input要素のonChangeで該当input要素に紐づくstateを更新し、保存ボタンを押すことでサーバー側に保存し(今回はモック)、レスポンスをMutationsのcommitによりstateに格納する。ページ構造はNuxt.jsに従って以下のようになります。
nuxt ├── layouts │ └── default.vue ├── pages │ └── index.vue └── store └── index.jsコードは以下のようになります。
layouts/default.vue<template> <div><nuxt v-if="$store.state.isInitialized" /></div> </template> <script> export default { mounted() { const { $store } = this; $store.dispatch('init'); }, }; </script>pages/index.vue<template> <section> <div > <div>Watcher And Subscriber</div> <div> <label>rootHoge</label> <input :value="$store.state.content.hoge" @change="e => changeHoge({ value: e.target.value })" type="text" /> </div> <div> <label>rootFuga</label> <input :value="$store.state.content.fuga" @change="e => changeFuga({ value: e.target.value })" type="text" /> </div> <div> <label>rootOptionMemo</label> <input :value="$store.state.content.option.memo" @change="e => changeOptionMemo({ value: e.target.value })" type="text" /> </div> <button @click="save">保存</button> </div> </section> </template> <script> import { mapActions, mapMutations } from 'vuex'; export default { methods: { ...mapActions(['save']), ...mapMutations([ 'changeHoge', 'changeFuga', 'changeOptionMemo', ]), }, }; </script>store/index.jsexport const state = () => ({ content: { hoge: '', fuga: 0, option: { memo: '', }, }, isInitialized: false }); export const mutations = { changeHoge(state, payload) { state.content.hoge = payload.value; }, changeFuga(state, payload) { state.content.fuga = payload.value; }, changeOptionMemo(state, payload) { state.content.option.memo = payload.value; }, changeIsInitialized(state, payload) { state.isInitialized = payload.value; }, }; export const actions = { init({ commit }) { // モック(本来はデータをサーバー側から取得してcommitする) commit('changeHoge', { value: 'test', }); commit('changeFuga', { value: 10, }); commit('changeOptionMemo', { value: 'Hello World!', }); commit('changeIsInitialized', { value: true, }); }, save({ commit, state }) { // モック(本来はーバー側にリクエストしてそのレスポンスをcommitする) commit('changeHoge', { value: state.content.hoge, }); commit('changeFuga', { value: state.content.fuga, }); commit('changeOptionMemo', { value: state.content.option.memo, }); }, };【仕様追加】input要素を編集したらページ遷移の際にブラウザの標準ポップアップを出したい
さて、上記のようなフローで構成されたアプリケーションですがここでコンテンツを編集したらページ遷移の際にブラウザの標準ポップアップを出したい、保存したらそれを解除したいという仕様になりました。まず最初に考えつくのは該当する各Mutationsがcommitされたときとsave Actionに処理を追加するという実装方法です。
store/index.js// 中略 export const mutations = { changeHoge(state, payload) { state.content.hoge = payload.value; window.onbeforeunload = () => true; // 追加 }, changeFuga(state, payload) { state.content.fuga = payload.value; window.onbeforeunload = () => true; // 追加 }, changeOptionMemo(state, payload) { state.content.option.memo = payload.value; window.onbeforeunload = () => true; // 追加 }, changeIsInitialized(state, payload) { state.isInitialized = payload.value; }, }; export const actions = { init({ commit, state }) { // 中略 window.onbeforeunload = () => undefined; // 追加 }, save({ commit, state }) { // 中略 window.onbeforeunload = () => undefined; // 追加 }, };この実装でも実現可能ですが、アプリケーションの拡張を考えたときに以下の懸念点があります
- Mutationsが増える度にwindow.onbeforeunload = () => true;
を追加する必要があり、冗長かつ漏れが発生する可能性がある
- そもそもStateを書き換えるのみというコンセプトのMutationsにこの処理が入っているのが微妙
- Mutationsで発火しているので、init処理の後にもwindow.onbeforeunload = () => undefined;
を書く必要がある
- Actionsも同様で、例えば削除するActionができてその後にもアラートを無効にしたいなどというときにも1つ1つに書いていかないといけないこのように特に拡張性の部分についての問題のある設計となっています。またページ遷移の際にブラウザの標準ポップアップを出すようにする処理に関しては本来stateが変更したら変えたいという話なのにそれをMutationsでやってしまっているので、懸念点に加えて頑張って制御している感が出ていてあまりスマートではない書き方というのもありそうです。
watchを使ってstateの変更を監視する
このような処理はVuexのwatchを使うとスマートに書くことができます。watchは指定した関数の戻り値が変化したときに実行される関数を登録できるもので、stateの変更起因での関数実行を実現できます。
watchを使うとコードは以下のようになります。store/index.jsconst activateAlertStateKeys = ['content']; // 中略 export const actions = { init({ commit }) { // 中略 this.watch( state => activateAlertStateKeys.map(key => state[key]), () => window.onbeforeunload = () => true, { deep: true, } ); commit('changeIsInitialized', { value: true, }); }, save({ commit, state }) { // 中略 window.onbeforeunload = () => undefined; }, };init処理の中でwatchを行うようにしました。
watchは第一引数に関数を渡し、この関数の戻り値が変わったときに第二引数の関数が発火します。第三引数の{ deep: true }を与えることで、対象のstateのネスト構造の変化も検知するようになります。
つまりactivateAlertStateKeysの要素とプロパティ名が同じstateに変更があったときにwindow.onbeforeunload = () => true;
が走って、以後window.onbeforeunload = () => undefined;
を行うsaveが走るまではページ遷移時にブラウザの標準ポップアップが表示されます。init Action内でcontentの変更を行っているchangeHogeなどによりwatcherが発火しないようにするため、それらがcommitされた後にwatchで登録を行っています。
activateAlertStateKeysを配列で管理することで、別のstateを監視したくなったときもこの配列にそのプロパティ名を追加するのみで管理できますし、またコードを読むときもactivateAlertStateKeysを見るだけで監視しているプロパティ名が分かるという利点もあります。subscribeActionを使ってActionのdispatchを監視する
stateの変更検知に関してはうまくいくようになりましたが、今後delete Actionが追加されそのActionのdispatch時もページ遷移時のブラウザの標準ポップアップ表示を無効化したい、というような時のために無効化する処理もwatchのようにactionのdispatch起因でできると良いです。そういった処理を実現するためにVuexにはsubscribeActionが用意されています。subscribeActionを使用して実現する処理は以下のようになります
store/index.jsconst invalidateAlertActions = ['save']; // 中略 export const actions = { init({ commit, getters }) { // 中略 this.subscribeAction(action => { if (invalidateAlertActions.includes(action.type)) { window.onbeforeunload = () => undefined; } }); commit('changeIsInitialized', { value: true, }); }, // 中略 };subscribeActionはactionがdispatchされたときに第一引数に渡された関数を実行するものです。関数はactionの情報を引数として持っており、action.typeでdispatchされたaction名、action.payloadでactionに渡されたpayloadを取得できます。
これを利用して、action.typeがsaveだったときにページ遷移時のブラウザの標準ポップアップ表示を無効化しています。こちらも配列で管理することでsave以外にも特定のActionのdispatch起因で無効化したい場合は配列の要素としてAction名を追加するのみで実現できます。
まとめ
このように単方向データフローを維持したまま拡張をすることができました。stateとactionを囲っている点線は簡単に拡張できる設計にもなっており、拡張性のある単方向データフロー構成となっています。
日々状態管理をシンプルにする、特に単方向データフローを崩さないことは意識していますがVuexにはwatch、subscribeActionなどそれを可能にする機能が用意されているので、駆使することである程度複雑な処理でも綺麗に実装することは可能だと思いました。またそういったところにVuex開発の楽しさがあるのかなと感じました。
このプロジェクトのリポジトリ
- 投稿日:2019-02-18T07:53:08+09:00
Webアプリ無料運営のススメ:FirebaseとNuxt(Vue)なら最強!
まだサーバーで消耗してるの?Firebase(サーバーレス)とNuxt.js(Vue系)ならWebアプリ運営は最強でしょ!?
この記事は、
- サーバー費用をなるべくかけたくないけどショボいのはNO
- Firebaseの活用法をあまり理解していない
- Nuxt/Vueを使ってアプリ作ってみたい(Next/Reactと悩んでたり)
- アプリ構想はあるけどアイデアの落とし込みスピードが遅くて毎回挫折する
という人向けに、「こんな感じで構築すれば効率良く開発できそうよ」というのを、勉強になった記事や技術的トピック・躓いた点なども合わせて紹介させていただきます。初心者向け&技術トピック気になる方向けです!
今回作ったアプリ「Moji → Pic」
Moji → Picは、文字だけだとなかなか目につく投稿ができないなぁ…とお困りの時にインパクトある画像が即座に出来るアプリ。目立ったツイートで友達に差をつけろ!
提案手法の優位性
そんな感じで、他人の金で肉や寿司が食べたいときにでも使ってみて下さい。無為なツイートがもっと無為になることでしょう。
ログインしなくても画像作ったりDL出来たり楽しめるので、是非遊んでくださいませ。今回のトピック
基本技術トピック:FirebaseとNuxtが凄いところ
- Firebaseの力で複数プラットフォームでログイン可能に(Twitter・Google・Facebook)
- NuxtのSSR機能のお陰で、投稿画面をSNSにシェアすると、各投稿毎にOGP画像が設定されている
- Firestore・storageのルール設定をすればユーザ毎の細かな権限を設定できる
応用技術トピック:このアプリの工夫ポイント
- 全部SSRはFirebaseのFunction通すとレスポンス悪いと石を投げられるので、SSRしたい動的ページ以外は静的ページとして配信
- FirebaseでのTwitterログイン機能からアクセストークンを取得し、Twitter投稿をする
残念トピック:改善したいポイント
- プラグインが多くてロードが遅いのでなんとかしたかった話
このあたりの内容を入れてお送り致します。
このアプリ制作では、オレオレルールですが、「5秒でログイン、30秒で投稿」を肝に銘じ作りました。UXデザインに通じているわけではないので虚言甚だしいですが、どんなに良いアプリでも「導線が上手く出来てないアプリは死ぬ」「機能の詰めすぎ悲しいかなエンジニアのエゴ」などを常に意識しながら最近は生きています。ちなみにこのアプリがそれを体現できているかは実際に見に行って見て下さい(石は投げないで下さい)。
そういう意味では、Firebaseを使って構築ってなると、Firebaseの機能をそのままサクっと使いたいので、ミニマルでシンプル構成に引っ張られやすいのも、私はメリットだと感じています。(もちろん複雑なアプリも出来る…!)
はじめに
自己紹介
モノづくりをしたりプログラミング先生をやったり。最近は、Railsで歌声専門フリマサイトとか、React勉強で今期アニメ検索アプリ(Qiita記事)とか作ったりしました。
また、Web系言語やらのネタでブログにTIPSをたとめていたり、Twitter(@y_kawase)でもちょこちょこ真面目ネタも呟いているので宜しければ絡んで下さい。
アジェンダ
前半はわりと基本的な内容や紹介なので、技術的な内容で気になる方は後半をご覧下さい。
- 経緯と小話
- 小規模・個人開発こそ「コスト・機能・効率が良い開発を」
- FirebaseとNuxtで開発スピードアップ「初中級者こそ率先して使うべし」
- 技術要素のお話
- 全体構成
- Firebase(サーバーレス)はいいぞ
- Nuxt(Vueフレームワーク)はいいぞ
- Buefy(CSSフレームワークBulma in Vue)はいいぞ
- Konva(Canvas描画系ライブラリ)はいいぞ
- Google Fontsはいいぞ
- 技術トピック(TIPS・躓いた点など)
- Firebaseのルールの書き方:ユーザ毎の権限を付与&リファレンス参照
- firebaseapp.comドメインを使用不可にしたいけどダメそう
- FirebaseのTwitterログインからアクセストークンを取得する
- SSRしたいページ以外は静的ファイルで配信したい
- 本番ではconsole.logの表示をしないようにする
- emoji-martが重いのと変な挙動が
- konvaの文字配置は環境差分がありそう
- プラグインが多くてページ読込が遅いのでつらい
私自身も勉強中の身ですので不備不足はご容赦下さい。情強の方のアドバイス・ご指摘など大歓迎です…!
経緯と小話
小規模・個人開発こそ「コスト・機能・効率が良い開発を」
最近すごく感じるのが、小規模開発(個人開発含む)や趣味の場合、サーバーコストがサービス継続に影響しているという所です。
せっかく作ったし、いつかバズるんじゃねと思ってサービスを動かし続けてても、サーバーの維持費に月500円(年間6000円)かかるわけです。数年後アクセス解析をみて殆どアクセスなんてなかったんや…と途方に暮れながらサービスクローズする姿が私には見えます…(本当にあった怖い話)
そんなこともあり、最近は
- コスト:基本無料で利用量が一定量増えたら課金のFirebase
- 機能:Google様の加護Firebaseと、初心者でも触りやすいVue系のNuxt
- 効率:プラグイン豊富なNuxtでサクッと構築
という、Nuxt.js(Vue.js)+Firebaseが今一番身につけて損は無い技術なんじゃないかと思った次第です。特に、このセットの記事は最近良く見かけることもありますので、FirebaseとNuxtの2人なら最強!ってことだと思ってます。
FirebaseとNuxtで開発スピードアップ「初中級者こそ率先して使うべし」
鋭い方は覚えているかもしれませんが、以前私はVue系ではなく、React使ってました。いつの間にVue系に鞍替えしたのよという感じ。そうです、Reactで誰もがやりたかった10の機能。アプリ構想はあるけど作れない人の壁をぶっ壊す。という記事を書いたりして勉強していたのですが、その後いざSSR(サーバーサイドレンダリング)をしたくなってNext.jsに手を出しました…ただ悲しいかな私には難しすぎました…(特にstoreやthemeまわりの設定がしんどかった…)
ふと、Nuxt.jsを触ってみることにしたところ、便利プラグインやら簡単な設定やらで、初心者にとっては大変ありがたい仕組みが用意してくれていました。後ほど技術要素のお話でも取り上げようと思いますが、「store管理がデフォで入ってる」「Buefyテーマ(CSSフレームワーク)を読み込んでくれるプラグイン」とかがNuxt用に最適化し用意されていて、使いたいものを設定ファイルに書けばすぐに動きました…Nuxtさん優しすぎる…
もちろん最終的にはそれぞれのフレームワークには確固たる方針があってメリットがあると思います。React・Vue・Angularが今のところは3大JSフレームワークと言われていて、それぞれメリットがあります。それぞれ素晴らしい特色があるのできちんとどれが良さそうか見定めると良いと思います!!
参考になりそう→【JavaScript】3大フレームワーク Angular, React, Vue.jsを比べてみよう個人的な意見として、初心者がとりあえずサクッと構築したいという場合は、Vue.js/Nuxt.jsの方が日本語情報も多いですしとっつきやすそうだなという感想です。GitHubのStar数はVue>Reactになったし、ひとまずはVue系の時代がまだ続くでしょと思う。
あとは、Nuxt界隈では@potato4dという神が君臨しており、Vue・Nuxtまわりのお話をよくアウトプットしてくれるので、それも凄く心強いなと思っています。
技術要素のお話
全体構成
Draw.ioで作った雑な構成図で怒られそう。ポイントとしては静的ページは直接Hostingに配置したものを見に行って、動的ページはSSRのためにFirebaseのFunction越しに色々見に行っている感じ。ちなみに「SSR x Firebase」構成については詳しくはこのあたりの素晴らしい記事を読んでくれ。
Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する
Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築するTypeScriptで書くのもいいよね。私は今回は断念したけど。。
TypeScript + Nuxt.js + Firebase (+ SSR)でWebアプリを構築Firebase(サーバーレス)はいいぞ
サーバーレスとは
文字通りサーバーが無く、自分でサーバーを借りたり立ち上げたりする必要が無いということ。サーバーが常に待機状態にある従来と違い、アクセスされた瞬間に処理が走りアクセスが無い時はお休みをしているイメージです。
AmazonのAWSならLambdaなどがサーバーレス系で強かったイメージですが、最近はFirebaseがかなりグイグイ来ていますね。普通のサーバー有りのものと違い、制約は多いですが(動かせる言語に限りがある、バージョンが指定されている等)、常時サーバーを動かさなくて済むため、コストが大幅にカット出来ます。
また、サーバーの場合は規模を大きくする際に、構成を増やしたり、その分通信経路を振り分けたり…とスケールアップが大変な部分がありますが、サーバーレス系の場合はこのあたりのスケールアップを勝手にやってくれる(1アクセスにつき1処理なので、アクセスが増えたら処理要員を増やせばいいだけ)ので、インフラ系で悩むことから開放されます。
Firebaseは最高だぜ
FirebaseはBaaSと言って、アプリケーションのバックエンドのサービスになります。サーバーレスの機構を使いやすくするために、
- 認証機能(Google、Facebook、Twitter、メールなど)
- DB機能(Firestore(NoSQL))
- ストレージ機能
- ホスティング機能
など、とても便利な付随機能があります。
もちろん認証系に特化したサービスAuth0を使うとかも良いかとは思いますが、Firebaseですべてを完結できるので使わない手はないでしょう。
お値段もちろん無料から可能
そうなんです、お値段なんと無料です。すごい。 外部との通信などの場合はプランを有料プラン(Blazeプランという従量制プラン)に変更する必要はありますが、ご安心。毎月の無料枠がかならずついているので、たいていの人は無料枠内に収まるのかとは思います。
なお、無料枠内は、Function呼び出しが12.5万回/月や、HostingのTransferが10GB/月とかなので、これを超えるサービスはもう無料枠の必要ないでしょという感じがあります。Firebaseのお値段・プランなどは公式サイトで確認するといいです。
つまりサービス開始時はおそらくたいていはほとんど料金を払う必要が無く、ゆくゆくサービスが大規模化していった場合にようやくお金を払う、そんなシステムになっています。
バズってアクセスが集中した月だけお金を払うようなシステムなので、普段は音沙汰ないけど、突然有名人にRTされてアクセス集中してサーバーが落ちたなんてことはありません。これも小規模・個人開発ではありがたい点なのかなと思います。
Nuxt(Vueフレームワーク)はいいぞ
先述の通りNuxtは色々なプラグインが豊富に用意されており、初心者にとっては大変使いやすい仕組みができています。またNuxtのベースになっているVueも初心者受けが良いと好評なツールですので、「とりあえずNuxt&Vue
」で作ってみるのは良いんじゃないかなと思います。
ディレクトリ構成とかも既に決まっていたりするので、そういった制約が逆に構築しやすくなったりしたりします。
Vueについて
そもそもVue.jsって何って人はこのあたりを読んでおけばおk
jQuery から Vue.js へのステップアップ簡単に言うと、JavascriptとHTML表示側の連携をすごくやりやすくしてくれているツール。入力が発生するアプリを作らないと実感わかないと思うのですが、純jsやjQueryで入力フォーム沢山&フォーム入力イベント処理ありのアプリを作ると管理出来ない代物がもれなく出来るので死にます(私だ)。
Vueの公式ドキュメントがしっかりしているので、とっつきやすいです。日本語ドキュメントもあります(ちょっと最新バージョンには対応が遅いけど)
ちなみに、最近はTypescriptでVueを書くのも流行っているので気になる方はチェックしてみては如何でしょうか。vue.js + typescript = vue.ts ことはじめ
ただ、Vueはクライアント側でレンダリングをするため、よく言われている話ですが、TwitterなどSNSでシェアした時に、投稿ページ毎のOGP画像などが反映されません(これはTwitter等では、レンダリングする前のデータを読み込んでいたりするため)。そこで、SSRという技術が必要になり、Nuxtが活躍したりします。
そういう意味では、SNSでのシェア時、個別ページ毎にOGP設定とかタイトル設定とかしなくて良いよという方は、Nuxt使わずにVueだけでも良いと思います。
Nuxtについて
サーバーサイドレンダリングするための設定がすでにされているのでめっちゃ便利です。
Nuxtの公式ドキュメントもしっかりしているので大変分かりやすい。ちなみにVueで十分だとか言う話も勿論あるので、それでOKな人は良いと思う。いわゆるSSR不要論というやつ。クローラーとかSEOとかそういう小難しいのは分からないので、私の判断基準は「TwitterとかのSNSでURLがシェアされた時に、個別のサムネ画像を用意したいかどうか」だ。一般的にSNSにシェアしたりするWebサービスを作る場合は、この動的なサムネ画像(OGP画像)の設定が必要になるケースが多いのでSSRはしておいたほうが良いと私は思う。普通SSRは面倒だけど全部Nuxtがなんとかしてくれるからね。
※ちなみにGoogleとかのクローリングはレンダリングしてくれるので、Vue単体だけでSSRしてなくてもちゃんとコンテンツ内容を理解してくれている模様。TwitterとかFacebookとかのSNSサービスのクローリングがこの先の未来にレンダリングしてくれるようになったらSSR不要論は高まってくるとは思う。
今回Nuxt・Vueで使ったプラグイン紹介
どんなことが出来るのか紹介します。Nuxt・Vue周りは専用のプラグイン(頭にnuxtがついていたりvueがついていたりするやつ)が多いので本当にありがたいです。
全般
- @nuxtjs/pwa:PWA設定が簡単にできる
- @nuxtjs/markdownit:マークダウン記述で書ける(主に利用規約とか書く時に使った)
- nuxt-buefy:NuxtでBuefyのテーマを使う
- vuexfire:Firestoreとのデータ連携ができる
- vuex-persistedstate:画面更新したときにもデータを残せるようになる
- cross-env:develop環境とproduction環境みたいに分けたい時
参考:
Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する
Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する
nuxt.jsを使う時にlocalStorageでstoreを永続化する
Nuxtでcross-envを使い環境ごとに環境変数を分ける画像作成周り
- emoji-mart-vue:絵文字のパレットを表示できる(ただしめっちゃ重い)
- vue-konva:Konva(後述)によるCanvas描画が簡単にできる
- fontfaceobserver:フォント読み込みチェックなどで使える
投稿周り
- vue-social-sharing:SNSシェア系のボタンが簡単に作れる
- vue-clipboard2クリップボードコピーが簡単に出来る
Webサイト運営周り
- @nuxtjs/google-tag-manager:今どきAnalyticsタグはGoogleタグマネージャーで管理しよう
- @nuxtjs/sitemap:サイトマップ作りに必要
- vue-google-adsense:AdSenseタグを埋め込みが簡単
参考:
もうmeta要素を迷わない!最低限入れるべきmeta要素のまとめ
nuxt.js(v2)でgenerate納品する前にやっておきたい設定Buefy(CSSフレームワークBulma in Vue)はいいぞ
Bulmaいいですよね。軽くてシンプルだけどイケてる。それをVue用にしたのがBuefyです。Vueのおいしい書き方をしながらBulmaのフォームとかが使えるのですごく助かっています。
ちなみにスライダーとかはないので、使いたい場合はBulma-Extensionsというのがあるのでこれも便利。
Konva(Canvas描画系ライブラリ)はいいぞ
いまどきCanvasだけでゴリゴリ書いている人なんて存在しているんでしょうか…?そう思ってしまうほど、Konvaは一度使ったらやめられない便利ライブラリです。
Konvaなにそれって方は、今すぐKonvaのデモを見た方が良い。なんで今まで使っていなかったか悲しくなるくらい良い。そんなKonvaもVue用に最適化されたvue-konvaがあるのでそれを使いましょう。
ちなみにCanvasでStorageに上げた画像を使おうとした時に、Cross Originの影響でうまく使えないことがあったので、Firebase StorageにCORSの設定をするを参考に設定しました。
Google Fontsはいいぞ
このアプリではGoogle Fontsからフォントを読み込んでいます。無料で使えるWebフォントで、表示環境依存のフォント表示を解消することができます。フォントはCDN配信なのでDLもわりと早めかとは思います。
自前でフォントを用意すると、帯域圧迫や通信量などでこの手の従量課金制のサーバーだとちょっとつらいかもというのは感じましたので今回は迷うこと無くGoogle Fontsに手を出しました。
ただフォントを読み込んでる最中の挙動とかそのあたりは若干面倒だった(ここでfontfaceobserverを駆使する感じ)ので、一長一短かもしれない。
技術トピック(TIPS・躓いた点など)
Firebaseのルールの書き方:ユーザ毎の権限を付与&リファレンス参照
FirebaseでFirestore・Storageへの書き込み権限を与えることで、かなり細かい制御が出来るようになります。
読み込み権限は全員にもたせたいけど、書き込み(投稿作成・削除)は作った本人だけにしたい、といった、SNS系あるあるの仕様の場合はちゃんとルールを作ってあげないとNGです。(javascript側の処理で出来るんじゃ…ていう考え方をする方もいらっしゃるかとは思いますが、基本的にユーザがいじることができる、クライアントサイドにこういう削除系の処理を書くとハックされる余地が残ります)
長くなってしまったので、こちらの記事にまとめています
Firebaseのルールの書き方:ユーザ毎の権限を付与&リファレンス参照firebaseapp.comドメインを使用不可にしたいけどダメそう
調べてみたところ、firebaseapp.comのドメインを無かったことにして、全て独自ドメインにする術は今のところなさそう。
https://github.com/firebase/firebase-tools/issues/1072
ということでありきたりですが、愚直にJavascript側でリダイレクトする感じを常に読み込んでくれるような適当な箇所に書き足します。(layouts/default.vueとかのどっかに書くとか、もっといい場所はあるかも)
// もしfirebaseのURLだったらリダイレクトする if ("mojipic-production.firebaseapp.com" === window.location.hostname) { window.location.href = "https://mojipic.f-arts.work/" }調べてて見つけたリファラなど考慮するリダイレクトも面白い。
やはり、お前らのJavaScriptでのリダイレクト実装は間違っているただ、今回のケースではfirebaseapp.comでアクセスされる想定はそもそも想定外ですので、普通のリダイレクトでいきました。firebase.ruleとかでrewrite設定とか出来ないかな…
FirebaseのTwitterログインからアクセストークンを取得する
FIrebase認証して最初にリダイレクト時に取得出来る感じだったので、ログインチェック時にトークンを取得して、store(永続化済み)に入れることにしました。使いたいときにstoreから参照する感じです。
こんな感じ
credential = firebase.auth().getRedirectResult().then(function(result) { if (result.credential) { // credentialがあったときの処理 // 例えばそのままreturnとか return result.credential } }).catch(function(error) { // エラー処理 });参考:Google ログインと JavaScript を使用して認証する
これでaccess_token_key・access_token_secretが手に入るので、あとはアプリ側のconsumer_keyとconsumer_secretをセットにしてあげれば画像投稿など何でも出来るようになります。
ちなみに、consumer_keyとconsumer_secretはフロント側に入れるとバレちゃって大変なので、Firebase Functionの方に記述しました。この辺は「Javascript Twitter API 投稿」とかで検索すると情報が山程出てくるので今回は割愛します。
SSRしたいページ以外は静的ファイルで配信したい
今回挑戦したかったことの一つ。FIrebaseはFunctionによるレンダリングもキャッシュされるとのことなのである程度のスピードは出るようにはなりますが、他の方々も言う通りSSRはレンダリング処理をサーバー側で噛ませるため、ワンアクション遅くなります。(感覚的には、Webサイトにアクセスしたときに、真っ白画面がコンマ秒ですがあるな…という感じ。ちゃんとチューニングすればいい話ではありますが…)
そこで今回はFirebaseの設定をイジって、OGPを動的にしたいページのみをSSR配信することにしてみました。
- 投稿ページ(/p/****):https://mojipic.f-arts.work/p/91loNya3pdgSO0ebEib4
- ユーザページ(/u/****):https://mojipic.f-arts.work/u/HfhTLnCdzRRHyuvPN3P2039jO2t2
この2種類のページをSSR配信し、残りはNuxtのgenerateで生成したファイルでいきます。
まずは、
nuxt generate
コマンドで出来たdistディレクトリ
をpublic
に配置します。また、SSR用に作ったディレクトリもdist/functions
に配置します。dist/functions配置のあたりは、先述の「Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する」とか「Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する」でやっているFunctionでNuxtを配信するやり方そのままです。唯一の違いはgenerateしたのを配置して、特定URLだけfunctionに処理を流すというやり方です。
firebase.jsonはこのように書いています。画像配信assets周りはgenerateのを共有で使える&functionSSR側のassetsはdist/functionsの方に入ってるので、こんなシンプルな感じでも共存できました。
firebase.json{ "hosting": { "public": "public", "rewrites": [{ "source": "/p/**", "function": "ssr" },{ "source": "/u/**", "function": "ssr" }] }, "functions": { "source": "dist/functions" } }大丈夫だとは思うのですが、function側で使われている
dist/client/
と、静的配信で使っているdist/assets/
assetsファイルの差分無いよなとmd5sumを見てみましたが、今の所大丈夫そうでした。ちなみにFunction処理用にに渡す
.nuxt
のディレクトリはnuxt generate
で生成されたものを使っています。ここは割と怪しいところなので、強いひとの意見を知りたい。今の所変な影響は出てなさそうです。diff <(find .nuxt/dist/client/ -type f | xargs md5sum | cut -d " " -f 1) <(find dist/assets/ -type f | xargs md5sum | cut -d " " -f 1)本番ではconsole.logの表示をしないようにする
nuxt.config.jsのbuild設定に書き足します。環境変数NODE_ENVがproductionの時にdrop_consoleするようにしました。
const TerserPlugin = require('terser-webpack-plugin') const environment = process.env.NODE_ENV || 'development' module.exports = { // ...中略... build: { // ...中略... optimization: { minimize: environment == 'production', minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true } } }) ] } } }emoji-martが重いのと変な挙動が
emoji-mart重い…ので、デフォでは非表示にしてみた。
あと、emoji-mart-vueの最新バージョン起因かもですが、2.6.5にしたときに、画面遷移時に画面が微動だにしなくなって、数秒後に突然動くという、謎挙動(Webアプリとしては致命的)があったので、2.6.4にバージョンを少し落としました。最新バージョンではまだ確認してないですが、なんか変だなと思ったらバージョンを変えてみて下さい。
konvaの文字配置は環境差分がありそう
konvaで文字の位置がズレる現象がある。Canvas事情かもですが、意外と辛いです。この例では、下のロゴの位置が結構ずれます。
verticalAlign設定のせいかなと、top・middle・bottomと色々試したのですがダメでした。もしご存知の方がいましたら救いの手を…
プラグインが多くてページ読込が遅いのでつらい
プラグインの状況をnuxtのアナライズモードで見ることができます。
https://ja.nuxtjs.org/api/configuration-build#analyzeこの通り書いたら、http://localhost:8888 で起動にならなかった(analyze: trueのデフォルトがなぜかserverではなくstaticになってる?)のでこんな感じで書き直し。
analyze: { analyzerMode: 'server', analyzerHost: '0.0.0.0', analyzerPort: '8888' }視覚化できてめっちゃいいですよね。
私の環境では、「firebase.js」と「emoji-mart」が結構占めてた…消したくは無いしどうすればいいのか…今回は諦めましたw(emoji-martは別途非同期読み込みに回す感じがいいのかなとは思う)
まとめ
FirebaseとNuxtを使うと、結構ちゃんとした所までアプリが作れるんだなと驚きました。
Nuxtの場合は一度作っておけば、コンポーネント管理のメリットもあり、他のアプリを作るときにも使いまわせたりできるのですごく良いなと思いました。開発効率がめっちゃあがりますね。ぜひ皆さんもFirebaseとNuxtで最高のコーディングライフをお過ごしください!本記事をここまでお読みいただきありがとうございました。またダラダラ書いていたら長くなってしまいました。。。こんな書き方ありえねぇナメてんのかとかあると思うので、是非Twitter(@y_kawase)で優しく絡んで下さい。
過去記事・関連記事も宜しければどうぞ
Reactで誰もがやりたかった10の機能。アプリ構想はあるけど作れない人の壁をぶっ壊す。
Qiita初投稿でも狙ってバズらせられた話『5つの成功と2つの失敗』
- 投稿日:2019-02-18T00:02:21+09:00
AWS + Laravel + Vue.js でQiitaのストックを整理するサービスを作りました!【個人開発】
概要
Qiitaのストックを整理するためのサービス「Mindexer(ミンデクサー)」をリリースしました?
この記事では、Mindexerで利用している技術について、解説したいと思います。GitHubでソースコードも公開しています。
https://github.com/nekochans/qiita-stocker-frontend
https://github.com/nekochans/qiita-stocker-backend
https://github.com/nekochans/qiita-stocker-terraformサービスについて
個人サービスを開発しようとしたきっかけは、技術力の向上のためでした。でも、どうせなら自分だけでなく、多くの人に使ってもらえるようなサービスが良いと思いQiitaのストックを整理するためのサービスを作りました。
こんな問題抱えてませんか?
- Qiitaのストック一覧を見ても何のためにストックした記事か思い出せない
- 後で読もうと思ってとりあえずストックしたけれど、読まれない記事が溜まっていく
- ストックから欲しい記事を探せない
開発者はこれらの問題を抱えていました?
ストックを整理する機能を追加する
これらの問題はすべて、ストックの整理ができていないことに要因があるではないかと考え、ストックを整理するためのサービスを作りました。整理するための手段として、自分専用のカテゴリを作成する機能を追加しています。記事をフォルダに分けるようなイメージです。Qiitaのアカウントを持っていれば、すぐに使い始めることができます!
Mindexer | Qiitaのストックを整理するためのサービスです
アプリケーションアーキテクチャ概要
バックエンドはREST APIを提供し、フロントエンドはVue.js/Vuexを利用したSPAとなっています。
また、バックエンドにはDDD(ドメイン駆動設計)を採用しています。
インフラはAWSを採用し、すべてTerraformで管理しています。
Qiitaのストック記事の取得には、Qiita API v2 を利用しています。インフラ構成
AWS構成図
フロントエンド
ビルドしたSPAのソースコードをS3にデプロイし、CloudFrontで配信しています。
バックエンド
EC2にWEBサーバーを構築し、RDSはAurora MySQLを使用しています。
RDSにはAurora Serverlessも検討しましたが、Aurora Serverlessはオンデマンドで起動するため、初回起動時に25秒ほど時間がかかります。一般ユーザ向けのサービスには不適切であると判断し、Aurora MySQLを採用しました。バックエンド
技術要素
- Laravel5.7
- nginx
- Amazon Aurora(MySQL)
概要
Laravelを使用してREAT APIを作成しています。
DDD(ドメイン駆動設計)を取り入れ、ビジネスロジックであるドメインモデルを技術の関心事を分離し、変化に強いコードとなることを意識しました。ソースコード
https://github.com/nekochans/qiita-stocker-backendREST API
バックエンドが返すAPIは、RESTの原則に沿った形でAPIを設計しています。URLが操作する対象のリソースを指定し、それに対するCRUD操作をHTTPメソッドで指定する、というものです。
APIの設計については、翻訳: WebAPI 設計のベストプラクティスを参考にさせて頂きましたので、詳細はこちらをご確認頂ければと思います。
エラーの設計については、WebAPIでエラーをどう表現すべき?15のサービスを調査してみたを参考に下記の通り定義しています。
{ "code":エラーコード, "message":"エラーメッセージ", "errors":{ "フィールド名":[ "エラーエラーメッセージ" ], } }
errors
はバリデーション エラーの場合のみ使用。ドメイン駆動設計
ドメイン駆動設計で実装するにあたって、レイヤードアーキテクチャを採用しています。
実際のディレクトリ構成とレイヤードアーキテクチャの関係は以下の通りです。
レイヤードアーキテクチャの説明に不要な部分は、下記の図には載せていません。app ├── Http ------------- プレゼンテーション層 │ ├── Controllers │ └── Middleware ├── Infrastructure --- インフラストラクチャ層 │ └── Repositories │ ├── Api │ └── Eloquent ├── Models ------------ ドメイン層 │ └── Domain └── Services ---------- アプリケーション層レイヤードアーキテクチャの層ごとに、実装のポイントを解説していきたいと思います。
プレゼンテーション層
コントローラーにおいて、HTTPリクエストを受け取る・レスポンスを返すことのみを責務としています。
コントローラーには、ビジネスルールや知識を記述しないことを意識しています。アプリケーション層
シナリオクラスを作成しています。
シナリオクラスの責務は、ドメイン層が提供するビジネスロジックを調整することです。
ここにおいても、ビジネスルールや知識は含めないことを意識しています。ドメイン層
ビジネスルールや知識を表す層です。
エンティティ・値オブジェクト
ドメイン知識を
エンティティ
、値オブジェクト
として表現しています。
エンティティと値オブジェクトの違いは、「識別」を持つかどうかで区別しています。また、エンティティ・値オブジェクトの生成にBuilderパターンを採用しています。
バリデーション
バリデーションもドメイン知識であると考え、仕様パターンを定義しています。
下記の例では、カテゴリ名のバリデーションチェックを行なっています。
バリデーション自体を、エンティティ・値オブジェクトに追加する方法もあると思いますが、その場合
エンティティ・値オブジェクトが複雑になると思い、仕様パターンを採用しました。app/Models/Domain/Category/CategorySpecification.phpclass CategorySpecification { /** * CategoryNameValue が作成可能か確認する * * @param array $requestArray * @return array */ public static function canCreateCategoryNameValue(array $requestArray): array { $validator = \Validator::make($requestArray, [ 'name' => 'required|max:50', ]); if ($validator->fails()) { return $validator->errors()->toArray(); } return []; } }独自例外
ステータスコード、メッセージ、レスポンスの形式は全てドメイン知識であるため、ドメイン層に定義しています。
リポジトリインターフェース
データの永続化は、インフラストラクチャ層の責務ですが、そのインターフェースをドメイン層で定義しています。
インターフェースを定義することによって、データを永続する際はドメイン層からインターフェースのメソッドのみを呼び出せばよく、永続化に関する技術的な関心事を知る必要が無くなります。
また、フレームワークへの依存を避けるため、Eloquentに依存しない形でインターフェースを定義しています。インフラストラクチャ層
Repositoryを定義しています。ストレージへのアクセス手段として利用し、ドメイン知識は持ちません。
ここでは、DBの操作はLaravelのEloquentoモデルを利用しています。テストについて
APIの単位でテストクラスを作成しています。
API単位でテストを作成するメリットとして、APIのIFさえ変わらなければ動作を保証できるというメリットがあります。開発中に何度か仕様を変更する必要性が出てきたのですが、テストで動作の保証ができる分、仕様を変更することへのハードルがすごく低くなりました。また何度かリファクタリングを行いましたが、その際もリファクタリングの工数を減らすことができ、開発を続けながらコードを改善することができたと思います。
テストについて下記の記事を参考にさせていただきました。
Laravel 5.3でREST APIのテストコードを書く以下は、細かい部分になりますが、テストで使用した技術について参考になりそうなポイントです。
Guzzleを使ったMockの作成
HTTPクライアントにGuzzleを使用しています。主にQiitaAPIへのリクエストに使用しています。
Guzzleは非同期リクエストが可能であったり使い勝手がいいHTTPクライアントライブラリでしたが、テストにおいても簡単にMockを作成をすることができ便利でした。
Testing Guzzle Clientsテストコードの抜粋となりますが、Mockクライアントを生成するメソッドを下記の通り定義しています。
tests/Feature/AbstractTestCase.phpprotected function setMockGuzzle($responses) { app()->bind(Client::class, function () use ($responses) { $mock = []; foreach ($responses as $response) { $mock[] = new Response($response[0], $response[1] ?? [], $response[2] ?? null); } $mock = new MockHandler($mock); $handler = HandlerStack::create($mock); return new Client(['handler' => $handler]); }); }例えば、「ステータスコード200、ヘッダーは"total-count: 20"、BodyはJSON」のレスポンスを作成する場合、下記のように配列を引数で渡すことで作成できます。複数のレスポンスを作成する場合は、配列を追加するだです。
$this->setMockGuzzle([[200, ['total-count' => 20], '{ hoge: hoge}']]);カバレッジを出力
テストケースが十分に網羅されているか確認するために、カバレッジの出力を行なって確認しています。カバレッジを100%を目指すことが目的ではなく、テストが十分に網羅されていないコードを検出することを目的としています。
フロントエンド
技術要素
- Vue.js + Vuex
- Vue Router
- TypeScript
- Jest
- vue-test-utils
- Bulma
概要
Vue.js/Vuexを利用したSPAを作成しています。
Vueのプロジェクトは、Vue CLIのテンプレートを利用。スタイルについては、CSSフレームワークのBulmaを採用しています。ソースコード
https://github.com/nekochans/qiita-stocker-frontend設計
コンポーネント
ディレクトリを2つに大きく分けています。
- pages
- components
pages
は、Vue Routerのルーティングに対応するコンポーネントです。将来的に、Nuxt.jsに乗り換えたいという思いもありこのような形にしています。
components
のコンポーネントを組み合わせることによって、全体のレイアウトを構築しています。またpages
の特徴として、Storeを直接参照できるようにしています。
components
のコンポーネントは、比較的大きいコンポーネントとしています。
このような形にしている背景としては、以下のような理由が挙げられます。
- CSSフレームワークのBulmaを利用しているため、コンポーネントにスタイルを閉じ込める重要性があまりない
- サービスの規模が大きくないため、大きめのコンポーネントで実装を進めたほうが開発効率が良い
components
は、Storeに依存しない設計にしています。
なぜ、このような形にしているかというと、コンポーネントの依存を少なくすることで、コンポーネントの再利用性を高め、テストをしやすくするためです。今後も開発を進めていく予定なので、コンポーネントの設計は見直す必要が出てくるかもしれません。
Store
状態管理のため、Vuexを利用しています。
基本的には、Vuexのコアコンセプトを基に作成しており、特筆すべき点は無いのですが、下記の点だけ考慮しています。VuexではコンポーネントからMutationを直接コミットすることが可能となっていますが、このプロジェクトでは、コンポーネントから操作する際は、必ずActionを経由するようにしています。理由は、データの流れを見やすくするためです。多少コードが冗長になるデメリットもあるかと思いますが、複数人での開発になった場合、データの流れが明確になっているほうが不具合の発生が少なくなるのではないかと考えています。
テストについて
テストフレームワークはJest、コンポーネントのテストにはvue-test-utilsを使用しています。
Jestは、テストランナーとアサーションの機能を兼ね備えており、Jestのみでテストを実行できるという点で採用してしています。
テスト対象は、全てのコンポーネントとVuexのモジュールです。E2Eのテストは導入していません。コンポーネント
各コンポーネントのテストケースを作成しています。
テスト観点は、Jestの
describe()
の単位で下記の通りです。コンポーネント単体の振る舞いだけでなく、コンポーネント同士の連携についてもテストしています。props
propsが正しく受け取れているか。method
methodが正しく動作しているか。
主に、子コンポーネントのメソッドの実行により、親コンポーネントのメソッドがemit
されているかを確認しています。template
正しくレンダリングされているか。
DOMイベントが発火した際に、メソッドが実行されるか。
子コンポーネントとの結合テストとして、子コンポーネントのイベントが発火した際に期待する親コンポーネントのイベントが発火しているか。Vuexのモジュール
VuexのGetter、Mutation、Actionのテストケースを使用しています。
ここはビジネスロジックが詰まった部分であるので、テストケースとしては必須になると思います。現在、全てのテストケースを1つのファイルに書いておりテストファイルが肥大化しているため、Getter、Mutation、Action単位で分割する予定です。
(もうちょっと何か書くかも。これくらいで問題ない・・?)
改善点
実際に上記の観点でテストケースを作成しましたが、コンポーネントのテストにおいて、全てのコンポーネントのテストを書くのは大変でした・・・
細かい単位でコンポーネントが作成されており、かつ、再利用されているというケースにおいては、コンポーネントのテストを書くメリットがあると思いますが、今回ようなコンポーネントの再利用があまりされていないケースでは、全てのコンポーネントのテストを書くことはメリットがあまりないのではないかと思いました。
Storeに依存するコンポーネントに限定したテストでもよかったのではないかと思っています。
この点については、改善していきます!デザインについて
CSSフレームワークであるBulmaを使用しています。UIコンポーネントのElementUIやVuetifyについても検討しましたが、カスタマイズのしやすさという観点からBulmaを採用しています。
実際にBulmaを使って見たメリットは下記の通りです。レスポンシブ対応
CSSを読み込むだけで、レスポンシブ対応が完了する点が非常に便利でした。
例えばヘッダーのメニューについても、Bulmaだけでハンバガーメニューへの切り替えが可能です。
![]()
学習コストの低さ
Bulma公式のドキュメントに情報が揃っているので、ここを確認すればだいたいのことは実現できました。また、公式のExpoにBulmaを利用したwebサイトの一覧があるので、これらを参考にしながらデザインをしています。今後改善していきたい事
技術面
現時点では、以下を対応する予定です。
- CI/CDを導入しテスト&デプロイを自動化する
- フロントエンドを Nuxt.js ベースにしてPWAに対応させる
- APIサーバーをDockerで動作するように改修する(AWS Fargateとか使うかも)
機能の追加によって新しい技術の導入が必要になると思うので、これからも継続的に改善を続けていこうと思っています!
機能
最小限の機能しかないので、これからはもっとリッチにしていきたいと思っています。
現時点では、以下の機能があったらより便利かなと考えています?
- カテゴリの並び替え
- 検索機能の追加 → ストックした記事を検索出来る機能
- メモ機能の追加 → カテゴライズした記事に自分用のメモを追加出来る機能
実際に使ってみて、フィードバックを頂けますと励みになります?♀️
開発の進め方
このサービスは、2人で作成しています。
主な開発は自分(kobayashi-m42)が担当し、レビューや技術的なアドバイスを@keitakn にしてもらうという形で進めています。開発ルールの設定
プロジェクトの開始時点で、開発のルールを設定しました。
最初に認識を合わせておくことで、スムーズな開発が可能になりますので、チームで開発する場合は設定しておくといいと思います。
- Gitのコミットルール
- PRの作成ルール
- 全ての課題に共通するDoneの定義
スクラム開発
スクラム開発で開発を進めました。2人という少人数のチームのため、正確なスクラム開発ではありませんが、スクラムの開発方式をとっています。概要は以下の通りです。
- スプリントは2週間
- 2週間に1度、スプリント計画を対面で行う
- 普段のコミュニケーション手段はSlack
また、スクラムを導入するにあたりZenHubを使いました。Chromeの拡張機能をインストールすることで使い始めることができます。今まで、プロジェクト管理ツールであるBacklogを使用したスクラム開発をしたこともありましたが、個人的にはZenHubの方が使い勝手がよかったという印象です。
ZenHubを使ったスクラム開発の始め方は、下記の記事に詳しく書かれていますので、こちらをご覧ください。
ZenHubで始めるスクラム開発あとがき
ゼロからサービスを作成するために、企画、技術の選定、設計、実装、デザイン・・・など全て担当し、技術的に多くのことを学ぶことができました。やはり、手を動かして何かを作ることが、技術を身に着ける上で一番大切だと実感しました。
ひとまず、リリースすることができ安堵していますが、これからも「Mindexer」というサービスを成長させながら、技術力を上げていきたいと思っています!
無料で公開していますので、多くの人に使っていただけると嬉しいです?
Mindexer | Qiitaのストックを整理するためのサービスです
ソースコードもこちらで公開しています!
ここにはMindexerのソースコードだけでなく、プロジェクトの雛形となるようなboilerplateも追加しています!
https://github.com/nekochans