- 投稿日:2019-12-12T23:58:40+09:00
✨ mo.js に恋して(あっ ? Vue の話だよ!) ✨
とある地方――、 Vue.js で開発をしているときのお話。
筆者はクリックイベントにアニメーションをつけたく、アニメーションフレームワークをネットで漁っていた。「お! これいいじゃん」
「 めっちゃかっこいい……。mo.js って言うのか」
おもむろにノート PC のキーボードを叩き始める筆者。
ディスプレイに映し出された検索サイトの入力欄にはmo.js Vueとスムーズに打ち込まれ、タンッという打鍵音とともにページが再描画された。「 Vue 用のパッケージは……、ほとんどないか」
◆ ◇ ◆ ◇ ◆
ということで、 mo.js を Vue で使いやすいようにプラグイン化して npm リリースした ので、そこで得た知見をまとめました。
? TL;DR
ターゲットとなる読者がわかりづらいのでまとめると、
- インスタンスメソッドをもつプラグインの作成
- Vue コンポーネントをもつプラグインの作成
- Vue カスタムディレクティブをもつプラグインの作成
- それら全部乗せのプラグインの作成
- プラグインの npm パッケージ化
といった感じです。
npm パッケージ化は
Vue CLI 3で行っています。? できあがったもの
vue-mo.jsという npm パッケージをリリースしました。✨ デモページ ✨ があるので良かったら見てみてください。
ソースコードも GitHub で公開していますので、興味のある方はどうぞ。
azukisiromochi/vue-mo.js | GItHub
@azukisiromochi/vue-mo.js | npm
? プラグイン開発と npm パッケージ化まで
それでは本題、開発についてです。
類似記事は多いのですが、プラグインとしてインスタンスメソッド、コンポーネント、カスタムディレクティブをまとめてドン! みたいなのは少なかったので、なるべく体系的にまとめてみました。
⭐ インスタンスメソッドをもつプラグイン
どんなの?
Vue にグローバルレベルで機能を追加したものをプラグインといいます。
this.$vuemo.Star({ parent: this.$refs.starParent }) .play()のように、ソースコード上で
this(Vue)から直接、開発したプラグインを参照することができるようになります。プラグインの作成
プラグインと言ってもいろいろなものがありますが、今回は主に インスタンスメソッド・プロパティを追加 することで、プラグインとして用意したメソッドやプロパティが Vue インスタンスで利用できるようになるものを作っていきます。
const Burst = function(binding) { // mo.js の Burst を利用するための関数 } const Vuemo = { install: function(Vue, options) { Vue.prototype.$vuemo = { Burst, // any... } } } export default Vuemoこんな感じで
installメソッドをもつオブジェクトを定義して、exportすればプラグインとして利用ができます。この例では、
installメソッドでVue.prototypeに$vuemoというオブジェクトを追加していて、$vuemoはBurstという mo.js の Burst を利用するための関数を持ったプラグインです。使い方
まずはインポートして
Vue.useしましょう。// プロジェクト内のプラグインなら `@/plugins/vuemo.js` みたいなパスを. import Vuemo from '@azukisiromochi/vue-mo.js' Vue.use(Vuemo)あとは簡単、
this.$vuemoでアクセスできます。<template> <button type=button ref="vuemoElement" v-on:click="replay">Burst!</button> </template> <script> export default { data() { return { burst: null } }, mounted() { this.burst = this.$vuemo.Burst({ parent: this.$refs.vuemoElement, radius: { 25: 75 }, count: 10, duration: 2000, children: { shape: ["circle", "polygon"], fill: ["#11CDC5", "#FC2D79", "#F9DD5E"], angle: { 0: 180 }, degreeShift: "rand(-360, 360)", delay: "stagger(0, 25)" } }) }, methods: { replay: function() { this.burst.replay() } } } </script>
vue-mo.jsプラグインを利用したデモページのソースコードの一部ですが、mounted内でプラグインを活用しています。コンポーネントの
dataに Burst (爆発のようなエフェクトアニメーション)関数をもたせて、ボタンクリックでアニメーションするようになっています。
(ちなみに、上に貼ったデモページの gif 画像も Burst を使っています)参考
⭐ Vue コンポーネントをもつプラグイン
どんなの?
Vue を使ったことがある人なら大抵はコンポーネントを作成して利用していると思います。
<font-awesome-icon icon="coffee"></font-awesome-icon>これは Font Awesome の例ですが、コンポーネントを
importすることでカスタム要素として利用できるようになります。プラグインの作成
.vueファイルで定義されたコンポーネントをパッケージ化して利用する、実はこれも プラグインにコンポーネントを追加 することで実現できます。import _MojsBurst from "@/components/MojsBurst.vue" export default const MojsBurst = { install(Vue, options){ Vue.component("MojsBurst", _MojsBurst) } }『⭐ インスタンスメソッドをもつプラグイン』のときと同じですね。
installメソッドを持つオブジェクトをexportしています。ただ、
installメソッド内ではVue.componentにより作成したコンポーネントを追加しています。これでコンポーネントをもったプラグインの出来上がりです。
使い方
インポート &
Vue.useは同じなので省略。<template> <mojs-burst :options="burstOptions" :is-replay-when-clicked="true" class="any-style" /> </template> <script> // プロジェクト内のプラグインなら `@/plugins/vuemo.js` みたいなパスを. import MojsBurst from "@azukisiromochi/vue-mo.js" export default { data() { return { burstOptions: { radius: { 25: 75 }, count: 10, duration: 2000, children: { shape: ["circle", "polygon"], fill: ["#11CDC5", "#FC2D79", "#F9DD5E"], angle: { 0: 180 }, degreeShift: "rand(-360, 360)", delay: "stagger(0, 25)" } } } }, components: { MojsBurst } } </script>
MojsBurstというコンポーネントは、カスタム要素<mojs-burst>に対して Burst が発火するクリックイベントが設定されています。
options属性に Burst の設定(エフェクトの種類など)を設定して利用します。参考
⭐ Vue カスタムディレクティブをもつプラグイン
どんなの?
Vue の標準セットである
v-ifやv-modelのようなディレクティブを自作したものがカスタムディレクティブです。<button type=button v-mojs-star-burst="{ burstShape: 'star' }"> ⭐Star Burst⭐ </button>html 要素に
v-始まりの属性を設定することで独自の機能を付与することができます。プラグインの作成
カスタムディレクティブの作成は、やったことがない人もいると思いますので、まずはそこの説明から。
const MojsBurstDirective = { bind: function(el, binding) { const options = binding.value || {} options.parent = el const burst = new mojs.Burst(options) el.addEventListener("click", function(e) { const left = e.pageX - el.offsetLeft const top = e.pageY - el.offsetTop burst.tune({ left, top }).replay() }) } }この例では
MojsBurstDirectiveというカスタムディレクティブを作成しています。
ディレクティブが初めて対象の要素にひも付いたときに1度だけ呼ばれるフック関数bindを定義していて、クリックした場所に Burst を用いたエフェクトアニメーションが表示されるように設定しています。ディレクティブのフック関数は、
bind以外にもありますので、軽く紹介しておきます。
フック関数 概要 bind ディレクティブが初めて対象の要素にひも付いた時に 1 度だけ呼ばれます。ここで 1 回だけ実行するセットアップ処理を行えます。 inserted ひも付いている要素が親 Node に挿入された時に呼ばれます。(これは、親 Node が存在している時にだけ保証します) update ひも付いた要素を抱合しているコンポーネントの VNode が更新される度に呼ばれます。子コンポーネントが更新される前になるので、バインディングされている値と以前の値との比較によって不要な更新を回避することができます。 次に、このディレクティブをプラグイン化します。
const MojsBurstDirective = { bind: function(el, binding) { // 上を参照. } } export default const MojsBurst = { install(Vue, options){ Vue.directive("mojs-burst", MojsBurstDirective) } }3 度めなので流石に慣れてきました。
今回は、
installメソッド内でVue.directiveにより作成したディレクティブを追加しています。これでディレクティブをもったプラグインの出来上がりです。
使い方
例のごとく、インポート &
Vue.useは同じなので省略。<template> <button v-mojs-burst:[arg]="burstOptions">Burst!</button> </template> <script> export default { data() { return { burstOptions: { radius: { 25: 75 }, count: 10, duration: 2000, children: { shape: ["circle", "polygon"], fill: ["#11CDC5", "#FC2D79", "#F9DD5E"], angle: { 0: 180 }, degreeShift: "rand(-360, 360)", delay: "stagger(0, 25)" } }, arg: 'is-replay-when-clicked' } } } </script>ボタン要素を
v-mojs-burstディレクティブとして利用しています。コンポーネントのときと同じように、ディレクティブに Burst の設定(エフェクトの種類など)を渡して設定しています。
また、
argはディレクティブ引数で、このディレクティブではクリックするたびにイベント発火させるかを判断させるために利用しています。参考
⭐ それら全部乗せのプラグインの作成
これまで紹介した 3 種類のプラグインをひとつにまとめます。
import _MojsBurst from "@/components/MojsBurst.vue" const MojsBurstDirective = { bind: function(el, binding) { // 上を参照. } } const Vuemo = { install: function(Vue, options) { Vue.prototype.$vuemo = { Burst, // any... } Vue.directive("mojs-burst", MojsBurstDirective) Vue.component("MojsBurst", _MojsBurst) } }; export default Vuemo export const MojsBurst = _MojsBurstなんだ、混ぜただけじゃないか――と思うかもしれませんが、よく見てください。
最後に
export const MojsBurst = _MojsBurstという 1 行が追加されています。
export defaultは default というだけあって、ひとつしか書くことができません。コンポーネントのみをプラグイン化する場合はよかったですが、 プラグインが複数の機能をもつような場合はコンポーネントは別途
exportする必要があります 。また、利用する際も同様に、
import { MojsBurst } from "@azukisiromochi/vue-mo.js"と書く必要があります。
Font Awesome でよく使われているやつですね。
⭐ プラグインの npm パッケージ化
作成したプラグインを npm パッケージとして公開していく手順をまとめます。
package.json を編集
package.jsonに必要情報を記載します。{ // 公開するパッケージ名. "name": "your-package", "version": "1.0.0", // 以下3つの `your-package` のところはパッケージ名に応じて変える. "main": "dist/your-package.common.js", "unpkg": "dist/your-package.umd.min.js", "jsdelivr": "dist/your-package.umd.min.js", // ライセンス. "license": "MIT", // 作成者名. "author": "your-name", "files": [ "dist" ], // デフォルト(true)のままだと公開できないため `false` に. "private": false, // GitHub などのリポジトリ情報を記載しておくと npm のパッケージ画面に表示される. "repository": { "type": "git", "url": "https://github.com/xxxxx/xxxxx" }, // キーワードを記載しておくと npm のパッケージ画面に表示される. "keywords": [ "vue", "mo.js" ], "scripts": { // ライブラリビルド用のスクリプト. // `--name` のあとにパッケージ名、プラグインがコーディングされているファイルパスと続くので記載する. "build-bundle": "vue-cli-service build --target lib --name your-package ./src/main.js" }, // dist のみ公開する場合は、 "dependencies": {} でOK "dependencies": { // パッケージで外部ライブラリなど使用している場合は記載する( npm install おまかせでいい) // "@mojs/core": "^0.288.2", "core-js": "^3.3.2", "vue": "^2.6.10" },プラグインをビルド
npm パッケージとして公開するためには、ビルド済みのプラグインが必要です。
先程の
package.jsonにスクリプトを設定済みのため、$ npm run build-bundleコマンドでビルドを行います。
ビルドが完了したら、
のように
distディレクトリが作成され、ビルド後の JavaScript 資産などが生成されているはずです。npm に公開
これはたくさんの方が記事に書かれているので、省略しますが、こちらの記事がわかりやすいと思います。
ちなみに、
npm publishを実行したときにエラーが返ることがあります。
エラーメッセージを取りそこねましたが、『すでに似たパッケージ名あるよ!』みたいな感じのものです。npm では、
-や_、.などを区別せずにチェックしているようで、それを踏まえてパッケージ名を決めましょう。
どうしてもエラーになるパッケージ名を使いたい場合は、パッケージ名を@your-name/your-packageのように npm アカウント名を付与する形で命名して、$ npm publish --access=publicとコマンドを実行すれば公開することができます。
[solving npm’s hard problem: naming packages | the npm blog]
? おわりに
Vue #2 Advent Calendar 2019 の13日目の記事でした。
もともと Qiita 記事を書くつもりでしたが、ちょうどアドベントカレンダーに空きがあったので差し込みました。
( 2 日前に思い立ったのでギリギリ ? )
Composition APIで話題がもちきりななか基本的な内容になりましたが、どなたかの役に立つと嬉しいですあと、 mo.js いい感じだからみんな使ってみてよ!(
vue-mo.jsもね!)◆ ◇ ◆ ◇ ◆
とある地方――、 Vue.js で開発をしているときのお話。
筆者はクリックイベントにアニメーションをつけたく、アニメーションフレームワークをネットで漁っていた。プラグイン作るのに夢中で、本来の目的がおざなりになっていることを筆者はまだ知らない――。
◆ ◇ ◆ ◇ ◆
12日目の記事
@yaju さんの Handsontable for Vueを使ってみる
14日目の記事
@kokky さんの VueとVue Routerで、リダイレクトのない理想の404を目指す
- 投稿日:2019-12-12T23:56:48+09:00
仮想DOMってすげーんだぜ!
この記事は福岡若手Sier_bc Advent Calendar 2019の11日目の記事です。
はじめに
今回は仮想DOMについて書いてみました。
- フロントエンドに興味のある方
- 仮想DOMについて知りたい方
を対象にしています。
そもそもDOMって何よ?
「Document Object Model」の略ですが、Wikipedia先生の説明では
DOMは、HTML文書やXML文書(あるいはより単純なマークアップされた文章など)をオブジェクトの木構造モデルで表現することで、ドキュメントをプログラムから操作・利用することを可能にする仕組みである。Documentの種類、操作に用いるプログラミング言語の種類に依存しない仕様である。
要するに、
HTMLを構築する木構造データのことだよ!プログラミング言語で操作できるよ!
ってことです。ここで注意しなければならないのは、MDNの説明にも(前略)ふつうは JavaScript を使用しますが、 HTML、 SVG、 XML などの文書をオブジェクトとしてモデリングすることは JavaScript 言語の一部ではありません。
とあるように、
DOMはJavascriptを用いて操作することができるけど、Javascriptの一部じゃない
ってことです。さらに、WebブラウザはDOMからHTMLを解析してWebページをレンダリングします。本記事では仮想DOMと区別して、通常のDOMをリアルDOMと呼称します。
じゃあ仮想DOMって何よ
正体はJavascriptのオブジェクトです。
JavascriptのオブジェクトでリアルDOMを仮想的に作って、
- 仮想DOMを二つ用意
- 一方の仮想DOMをJavascriptで操作(一般的にリアルDOMを操作するより速い)
- 変更前後の仮想DOMの差分を比較
- 差分だけをリアルDOMに反映
- 反映されたリアルDOMをブラウザがレンダリング
ということで最終的にリアルDOMを操作するのですが、通常、リアルDOMを操作する場合はリアルDOMが変更されるたびにブラウザがHTMLを解析してレンダリングするのでコストが高いです。
仮想DOMを使うメリットはレンダリングコストを低くできることの他に、
- UIとロジックを分離できる
- 状態の管理を簡略化できる
- UIとロジックを繋ぐ処理が簡単になる
です。
じゃあどう変わるのか見てみようじゃないの
- リアルDOMを操作する場合
- 仮想DOMをVue.jsを使って操作する場合
を見てみましょう。
リアルDOMを操作する場合
こちらを参考にさせていただきました。
<div id="app"> <p id="counter">0</p> <button type="button" id="increment">+1</button> </div> <script> const state = { count: 0 }; const btn = document.getElementById('increment'); btn.addEventListener('click', () => { const counter = document.getElementById('counter'); counter.innerText = ++state.count; }) </script>リンク先にもありますが、このコードを見ると
- stateというオブジェクトで現在のcountを管理しよう
- ボタンをクリックしたらインクリメント処理を行おう
- state.countをインクリメントしよう
- state.countを表示するために表示する要素(p#counter)を取得しよう
- 取得した要素の文字をstate.countで更新しよう
と考えると思います。まぁこれでもいいんですけど、
- いちいち要素をJavascriptで取得してるからUIとロジックが混在
- HTMLにもJavascriptにも状態の初期値が記載
- UIとロジックを結びつけるためにわざわざリスナーを定義してる
っていうのがめんどくさいですね。
仮想DOMをVue.jsを使って操作する場合
こんな感じのコードになるかと思います。
<template> <div> <p>{{ count }}</p> <button v-on:click="increment">+1</button> </div> </template> <script> new Vue({ data: { count: 0 }, methods: { increment: function() { this.count += 1 } } }) </script>ね?
- JavascriptでHTMLの要素を取得しないからUIとロジックが分離
- 状態の初期値はJavascript側で完結
- リスナー代わりのv-onディレクティブがHTML側に記載されてるからUIとロジックを繋ぐ処理が簡略化
されているでしょう。これが仮想DOMを使うメリットです。
あれ?結果的にレンダリングコストは変わるん?
一応色々とベンチマークはあるようなのですが、フレームワークによって得意不得意がある模様です。
まとめ
以上で、仮想DOMの説明をしてみました。仮想DOMは確かにレンダリングコストを低減する画期的なものですが、やはり開発する上ではUIとロジックが分離されるという点も非常に強力で、生産性向上に寄与するものだと思います。
謝辞
今回の記事について、様々な記事にお世話になりました。
この場をお借りしてお礼申し上げます。
- 投稿日:2019-12-12T23:56:12+09:00
2020年のVue.jsとReactの選定基準を考える(Hooks vs Composition API)
Vue.jsの次メジャーバージョン(v3)が2020年Q1にリリースされる。特に目新しいのが、Vue Composition APIという、これまでのVueの書き方とは違う関数ベースのAPI。
これに伴って、Vue.jsとReactの選定基準についても改めて考えないとなぁと思い書いた。
今回はまず新しいVue Composition APIに触れて、最後にReactとの違いについて書いてみる。TL;DR(忙しい人向け)
- TypeScript前提ならReact
- TypeScriptを使わない(or 使えない)規模ならVue.js
- 既存の大規模Vue.jsプロジェクトは、徐々にComposition APIに移行すると幸せ
Vue.js 3の Composition API とは
Composition APIでは、下のコードのように、
reactiveやcomputedといった関数を用いて組み立てていく。見て分かる通り、今までとは全く違う、React Hooksと近しい書き方になる。vue3-component.vue<template> <button @click="increment"> Count is: {{ state.count }}, double is: {{ state.double }} </button> </template> <script> import { reactive, computed } from 'vue' export default { setup() { const state = reactive({ count: 0, // data: { count: 0 } と同じ double: computed(() => state.count * 2) // computed: { double: () => { this.count * 2 } } と同じ }) // methods: { increments() { this.count++ } } と同じ (下でsetupからreturnしているため) function increment() { state.count++ } // template内で使いたいものを返す return { state, increment } } } </script>このComposition APIを触ってみた時は興奮したんだけど、冷静になって考えてみると、Vue.jsの存在意義ってなんだろうとふと思った。
なぜVue.jsにComposition APIが実装されたか
Composition APIのRFCでも書かれているが、このAPIが誕生した理由は2つある。
https://vue-composition-api-rfc.netlify.com/#motivation
- ロジックの抽出と再利用性
- TypeScript(型推論)の改善
ロジックの抽出と再利用性
VueはComponentの分割手法として、Mixinを提供していたが、Mixinでは2つの問題があった。
Mixinの1つ目の問題は「Mixinにどのようなメソッドなどがあるかを、Mixinを利用する側から一見して分からない」こと。
vue2-component.jsexport default { mixins: [myGreatMixin] // mixinが何を提供しているかはファイルを見ないと分からない }Mixinの2つ目の問題は、「Mixinの中でしか用いないメソッドなどをprivateにすることができない」こと。
Vue.jsのスタイルガイドでは$_mixinName_methodNameのようにprefix付きで命名することを強要している。vue2-component.jsconst myGreatMixin = { methods: { publicMethod: function () { console.log('Hello from public') }, $_myGreatMixin_privateMethod: function () { // 親からも呼べてしまう console.log('Hello from private') } } }これら2つの問題により、Vueが提供しているMixinは、可読性やメンテナンス性において良いものとは言えない。Mixinの中でさらにMixinを使う場合なんかは、正直書いていて楽しくない。
それに対しVue Compostion APIでは以下のように書くことができる。
vue3-component.jsexport default createComopnent({ setup() { const { hello } = useSayHello() onMounted(() => { hello() // 'Hello' }) } }) // lib/say-hello.js export const useSayHello = () => { const hello = () => { console.log('Hello') } return { hello } }比較して分かる通り、Composition APIでは
const { hello } = useSayHello()といった具合に、抽象化されたコードがどのようなメソッドやデータを提供しているかひと目で分かる。またプライベートなデータもreturnしない限りスコープを閉じることができる。2. TypeScript(型推論)の改善
これは現状、Vue.jsとTypeScriptの相性が悪いという問題を抱えているためである。
例えば
Vue.extend()内で、mixinが提供するメソッドなどに対して型推論する場合は、以下のように、そのmixinが提供するメソッドなどを全て個別に型定義しなければいけない。vue.d.tsdeclare module 'vue/types/vue' { interface Vue { items: Item[] // 「itemsというdataを提供するmixin」のための定義 setSnackbar: (message: string) => void // 「setSnackbarというメソッドを提供するmixin」のための定義 } }これでは、Mixinとは別に型を二重定義する必要があり、本来バグを防ぐためのTypeScriptが逆にバグの温床になってしまう。しかしComposition APIは純粋な関数であるため、特に工夫をせずとも型推論ができる。
TypeScriptデコレータによる推論は廃止
一時期、Vueをvue-property-decoratorなどのTypeScriptデコレータを使って型推論をするという流れがあり、Vue 3でもクラスベースのAPIを公式に提供しようかという話が挙がっていた。
しかしTypeScriptデコレータ自体まだexperimental(実験的)な機能であるため、その話は議論の末に捨てられてしまった。
詳細はIssueにある。[Abandoned] Class API proposal by yyx990803 · vuejs/rfcs · GitHubComposition APIの登場
以上の諸問題によって、Vue.js は関数ベースのComposition APIを提供することを策定した。
Vue.js 2でもComposition APIを試せるようにプラグインを提供している。
https://github.com/vuejs/composition-apiVue.jsとReactはどちらを選ぶべきか
早速本題に入ってみる。
TypeScriptならReact
まず、「はじめからTypeScriptで書く」というプロジェクトにおいては、Reactを選択するべきだと思う。Vue.jsであればComposition API自体がまず正式リリースされていないし、バージョン2の書き方は先に挙げたTypeScriptとの相性問題がある。それなら安定したReact Hooksを選ぶのが間違いない。
ではComposition APIが正式リリースされた際はどうすればいいか?。少し悩ましいがこれも自分の答えはReact。Composition APIから後発ならではの良さというのも特に感じなかったし、vuexやvue-routerといった周辺ツールに関しては、概念はReact系と一緒なのでそれほど学習コストに差はない。
またReactのコミュニティとの差を感じることもたまにある。例えばマテリアルデザインのUIライブラリにおいて、Vue.jsベースのVuetify.jsよりもReactベースのMaterial-UIがライブラリとして完成度が高い。他のライブラリのケースも、ほぼReactの方が安定して開発されている場面によく会う(2021年はそれほど変わらなくなると思うけど)。
いつVue.jsを使うべきなのか
Vue.jsは、TypeScriptやReactの習熟度が高くないチームであったり、小規模なケースにおいて採用することで強みを発揮できる。
そもそもVue.jsは、親しみやすいインタフェースである程度の複雑なアプリケーションを作れる、といった点がユーザーに愛されていた点だと思っている。Mixinを使わなければVue.jsは読みやすく、それほどJavaScriptに精通していなくとも書くことができる。
またSSRアプリケーションのフレームワークとしてReactのNext.jsよりも、Vue.jsのNuxt.jsの方が予め用意してくれている領域が広く、サクッとモダンなアプリケーションを作ることができる。
そういった点でも、Vue.jsはReactよりも小規模なプロジェクトに向いている。
いつComposition APIを使うべきか
Composition APIは、既にVue.jsを導入しているプロジェクトで有用な選択肢になる。
というのも、Vue2の記法自体は3でも使うことができ、Composition APIとも併用して動かすことができる。そのため、今現在運用しているVue 2のプロジェクトがTypeScriptを欲しくなるような規模になれば、部分的にComposition APIへ移行し、TypeScriptの強化とコードの抽象化を行うことができる。
所感
2020年の選定基準について、一通り書いてみた。
Vue.jsが型推論を考慮せずに設計されたこともあり、JavaScript界隈において少し曖昧な立ち位置になっているなぁと感じる。こういったVue.jsのAPIの拡張はメリットもある一方、ユーザーにとって選択疲れを引き起こしてしまっている。
もういっそのことTypeScriptを捨てて、バージョン2の書き方で貫き通してもよかったんじゃないかなぁと、少し思った。
- 投稿日:2019-12-12T23:32:06+09:00
【Vue】不完全形態のWebページを表示させなくする方法(v-cloakディレクティブ)
はじめに
コンパイル完了後にページを表示することで、不完全なWebページをユーザーに見せないようにする方法です。
Vue.jsで
v-cloakディレクティブを活用していきます。環境
- OS: macOS Catalina 10.15.1 - zsh: 5.7.1 - Vue: 2.6.10結論:コード
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./style.css"> <title>Title</title> </head> <body> <div id="app" v-cloak> # ここにv-cloak {{ message }} </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="main.js"></script> </body> </html>style.css[v-cloak] { display: none; }main.jsvar app = new Vue({ el: '#app', data: { message: "This is v-cloak directive." } })補足:
v-cloakとは?公式ドキュメントによると、
このディレクティブは関連付けられた Vue インスタンスのコンパイルが終了するまでの間残存します。
とのこと。
コンパイルが終了するまでの間残存する
↓
コンパイルが終了すると消えてくれる
↓
display: none;が消えて描画されるようになるということですね※今回のコードで、もし
v-cloakを使わなかったら、{{ message }}というMustacheがコンパイル前の不完全な状態で一瞬表示されてしまうことになります。アレンジ:コンパイル後ふぁっと表示させる
CSSを少しいじって、コンパイル後にフェードインで表示するようにしたコードです。
index.html(変更なし)<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./style.css"> <title>Title</title> </head> <body> <div id="app" v-cloak> {{ message }} </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="main.js"></script> </body> </html>style.css@keyframes cloak-in { 0% { opacity: 0; } } #app { animation: cloak-in 1s; } [v-cloak] { opacity: 0; }main.jsvar app = new Vue({ el: '#app', data: { message: "FADE IN..." } })おわりに
最後まで読んで頂きありがとうございました
これまで中途半端な状態を見せたくない要素にはCSSで時間を数秒遅らせて表示するようにしていました。
v-cloakを使ってコンパイル完了後というタイミングを指定出来れば、ユーザーの通信速度や端末のスペックに応じて表示されて便利ですね参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-12T23:13:22+09:00
Handsontable for Vueを使ってみる
はじめに
これは、Vue #2 Advent Calendar 2019の12日目の記事となります。
昨年、Handsontable Advent Calendar 2018 を1人開催しまして、その中で、Handsontable for Vue の記事を書いたのですが、
動作方法
ごめんなさい。あとで動作する部分を書きます。
Handsontable for Vueの紹介ということで動作方法を書かないまま1年が経ってしまいました。
動作方法
テーブルの作成
未だに Vue.js を使いこなしていないので、「Vue.jsでhandsontableを使う」を参考にしてみます。
サンプルは「【Handsontable】導入と設定」と同じものです。
CodePen がQiitaで埋め込みが出来るので、CodePen を使用します。
See the Pen Handsontable for vue by やじゅ (@yaju-the-encoder) on CodePen.
データの読み込み
今はHandsontable.vueのdataに初期テーブルデータをベタ書きしているが、実際にこのような使い方をすることはないので、データの読み込みをします。
ごめんなさい。あとで動作する部分を書きます。
最後に
未だに Vue.js を使いこなしていないので仕組みをまだ理解していません。
データの読み込みなど作成してから記事を書き直します。
- 投稿日:2019-12-12T20:57:05+09:00
nuxt + typescript + vuex + axios に手を焼いたので共有
こんにちは
nuxt + typescript でフロントエンドを作っているエンジニアです。
store の getter や actions にも type を効かせるときに手を焼いたので共有します。目次
- ベースの作成
- 公式の見解
- vuex-module-decorators で型付け
- vuex-class-component で型付け
- vuex-class-component での注意点
- まとめ
- axios の設定
ベース作成
nuxt × typescript の構築は他の記事に譲るとし、今回の説明で必要なファイルを列挙します。
なお、今回は shops を中心に store の説明をします。まずは型定義
types/index.d.tsexport interface Shop { name: string; }そして pages
pages/index.vue<template> <div> {{ shopOptions }} </div> </template> <script lang="ts"> import Vue from 'vue'; import { mapGetters } from 'vuex'; import { Shop } from '~/types'; export default Vue.extend({ computed: { ...mapGetters({ shopOptions: 'shops/values', }), }, mounted() { this.$store.dispatch('shops/fetch'); }, }); </script>今回問題となっている store
fetch を実行すると、サーバーからファイルを取得し、values に保持します。
clear を実行すると、保持していた values を clear します。store/shops.tsimport { Shop } from "~/types"; import { GetterTree } from "vuex/types/index"; import { RootState } from "~/store/types"; export const types = { SET: 'SET', CLEAR: 'CLEAR', }; interface State { values: Shop[]; } export const state: () => State = () => ({ values: [], }); export const mutations = { [types.SET](state: State, shops: Shop[]): void { state.values = shops; }, [types.CLEAR](state: State): void { state.values = []; }, }; export const actions = { async fetch({ commit, getters }): Promise<void> { if (getters.values.length > 0) { return; } const { data } = await this.$axios.get('/shops'); commit(types.SET, data); }, clear({ commit }): void { commit(types.CLEAR); }, }; export const getters: GetterTree<State, RootState> = { values(state: State): Shop[] { return state.values; } };以上の 3ファイルになります。
上記のコードだと、 component 側で typo しても、なんのエラーも出ません。
pages/index.vuemounted() { this.$store.dispatch('shops/fech'); // typo }, </script>これを typescript のエラーとして警告してくれるのがゴールです。
公式の見解
Nuxt TypeScript では、以下の選択肢が与えられています
- vanilla (今回は触れません)
- vuex-module-decorators
- vuex-class-component
ググってみると、
vuex-module-decoratorsがベストのような記事がありますが、
vuex-class-componentもいいよ!みたいな記事もあったので、試してみました。vuex-module-decorators で型付け
まずは
vuex-module-decoratorsで型付けをするやり方を見ていきます。いつも通りライブラリをインストールします。
yarn add vuex-module-decoratorsstore を直していきます。
axios は後ほどやるので、ひとまずは仮データで進めます。store/shops.tsimport { Module, VuexModule, Mutation, Action, } from "vuex-module-decorators"; import { Shop } from "~/types"; @Module({ name: 'shops', stateFactory: true, namespaced: true, }) export default class ShopsStore extends VuexModule { private shops: Shop[] = []; @Mutation private SET(shops: Shop[]): void { this.shops = shops; } @Mutation private CLEAR(): void { this.shops = []; } @Action({}) public async fetch(): Promise<void> { if (this.shops.length > 0) { return; } const data = [{ name: 'tokyo' }]; this.SET(data); } @Action({}) public clear(): void { this.CLEAR(); } public get values(): Shop[] { return this.shops; } }State は内部変数として書きます。
privateをつけると直接参照したときに Typescript エラーを起こしてくれます。
getter はpublic getのメソッドとして書き直します。次に Initialise plugin を設定していきます。公式で書かれている内容なのでさらっといきます。
store/index.tsimport { Store } from 'vuex'; import { initialiseStores } from '~/utils/store-accessor'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const initializer = (store: Store<any>): void => initialiseStores(store); export const plugins = [initializer]; export * from '~/utils/store-accessor';utils/store-accessor.tsimport { Store } from 'vuex'; import { getModule } from 'vuex-module-decorators'; import ShopsStore from '~/store/shops'; let shopsStore: ShopsStore; // eslint-disable-next-line @typescript-eslint/no-explicit-any function initialiseStores(store: Store<any>): void { shopsStore = getModule(ShopsStore, store); } export { initialiseStores, shopsStore };設定した store を読み込むように pages 側を変更します。
pages/index.vue<script lang="ts"> + import { shopsStore } from '~/store'; (中略) - computed: { - ...mapGetters({ - shopOptions: 'shops/values', - }), - }, + shopOptions(): Shop[] { + return shopsStore.values; + }, mounted() { - this.$store.dispatch('shops/fetch'); + shopsStore.fetch(); }, </script>ここまでの変更で、typescript error が出るようになります。
pages/index.vue<script lang="ts"> (中略) mounted() { shopsStore.fech(); // typo }, </script>180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'? > 180 | await shopsStore.fech(); | ^以上で vuex-module-decorators による型付けは成功しました。
axios がまだ設定できていないので、そちらはおまけでやっていきます。vuex-class-component で型付け
vuex-class-componentで型付けをするやり方を見ていきます。ライブラリをインストールします。
yarn add vuex-class-componentstore を直していきます。
store/shops.tsimport { createModule, mutation, action, createProxy, extractVuexModule, } from "vuex-class-component"; import Vuex from 'vuex'; import Vue from 'vue'; import { $axios } from '~/utils/api'; import { Shop } from "~/types"; Vue.use(Vuex); const VuexModule = createModule({ namespaced: 'shops', strict: false, target: 'nuxt', }); export class ShopsStore extends VuexModule { private shops: Shop[] = []; @mutation private SET(shops: Shop[]): void { this.shops = shops; } @mutation private CLEAR(): void { this.shops = []; } @action public async fetch(): Promise<void> { if (this.shops.length > 0) { return; } const data = [{ name: 'tokyo' }]; this.SET(data); } @action public async clear(): Promise<void> { this.CLEAR(); } public get values(): Shop[] { return this.shops; } } const store = new Vuex.Store({ modules: { ...extractVuexModule(ShopsStore), } }); export const shopsStore: ShopsStore = createProxy(store, ShopsStore);
vuex-module-decoratorsと似ていますが、Module 指定のや store 登録周りの書き方が違います。
また、vuex-module-decoratorsとは違い、initialise plugin にあたる処理を各 store の中でやっています。設定した store を読み込むように pages 側を変更します。
pages/index.vue<script lang="ts"> import { shopsStore } from '~/store/shops'; (中略) - computed: { - ...mapGetters({ - shopOptions: 'shops/values', - }), - }, + shopOptions(): Shop[] { + return shopsStore.values; + }, mounted() { - this.$store.dispatch('shops/fetch'); + shopsStore.fetch(); }, </script>vuex-module-decorators の時と同じように、typescript error が出るようになります。
pages/index.vue<script lang="ts"> (中略) mounted() { shopsStore.fech(); // typo }, </script>180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'? > 180 | await shopsStore.fech(); | ^vuex-class-component での注意点
pages/index.vueshopsStore.CreateProxy(~, ~)このような形で、store.CreateProxy() を呼びましょうと書いてある記事が多かったのですが、これは古い readme に書かれていた内容のようです。(2019/12/12 時点)
まとめ
調べてみる限り
vuex-module-decoratorsが優勢のようですが、書いてみるとvuex-class-componentの方が変更ファイルも少なく、書きやすい印象がありました。
公式はvuex-module-decoratorsを推してるけど、vuex-class-componentも悪くないよ! という内容でした。ご指摘あったらコメントください!
axios の設定
上記のコードでは axios の設定ができていないので、やっていきます。
とはいえ、公式 にも書いてあるので、参考程度に載せておきます。
なお、vuex-module-decorators でも vuex-class-component でも同じ書き方で動きます。utils/api.tsimport { NuxtAxiosInstance } from '@nuxtjs/axios'; let $axios: NuxtAxiosInstance; export function initializeAxios(axiosInstance: NuxtAxiosInstance): void { $axios = axiosInstance; } export { $axios };plugins/axios-accessor.tsimport { Plugin } from '@nuxt/types'; import { initializeAxios } from '~/utils/api'; export const accessor: Plugin = ({ $axios }): void => { initializeAxios($axios); }; export default accessor;nuxt.config.jsplugins: [ '@/plugins/axios-accessor', ]store/shops.tsimport { $axios } from '~/utils/api'; (中略) const { data } = await $axios.get('/shops');以上!
- 投稿日:2019-12-12T19:49:41+09:00
Vueで開発したアプリケーションの構造
はじめに
コミュニケーションクラウドの新規開発からエンジニアとして参画している井川拓信です。
この記事は、「モチベーションクラウドシリーズ Advent Calendar 2019」の12日目の記事となります。
対象とする読者
- Vue.jsでアプリケーションの新規開発をしようと思ってる人
- Vue.jsで開発しているアプリケーションの構造で悩んでいる人
概要
CommunicationCloudは2019年2月から開発を始め、2019年8月に正式リリースしました。
2019年2月の新規開発から携わり、CommunicationCloudのフロントエンドの基底処理の設計/製造を行った私から、CommunicationCloudのフロントエンドの構造をご紹介します。設計方針
- Vue CLIで初期化したアプリケーションを拡張して開発する
- 責務をモジュールごとに分離できるようにして、テストを容易にする
- 共通的かつ横断的な機能を一元管理できるようにする
状態管理の設計
状態管理の設計はVuexの設計を一部カスタマイズして使ってます。Vuexと違う部分としては、Actionsが直接Backend API/S3などの永続化のためのシステムと連携せずにDAO(Data Access Object)を経由するようにしていることです。
Actionsは、「Commitの実行」、「State/Gettersからデータを取得」、「他Actionsの呼び出し」など責務が多くなりがちです。DAOに、「URL/クエリパラメータ/リクエストボディの生成」、「レスポンスのハンドリング」、「HTTPステータスエラー時の制御」の責務を分離することで、Actionsが肥大化しないようにしています。
通信制御をDAOに抽象化することで、一度の操作で複数のBackend APIを呼び出す必要がある場合も、DAO内に影響範囲を抑えることができ、Actionsに簡単に利用できるメソッドを提供できます。ユニットテスト時には、ActionsでAjaxの考慮を行う必要がなくなるため、テストの責務が分離するメリットがあります。
例としては、ファイルアップロードの機能を実装するために、3件のAjaxを順次実行するとします。
1. Backend APIからS3にアップロードするための署名付きURLを取得する。
2. S3にファイルをアップロードする。
3. Backend APIにファイルをアップロードしたことを通知する。DAOにアップロードに必要な3件のAjaxを順次実行するメソッドをupload(file)メソッドとして実装することで、Actionsはどのような通信が必要かを意識することなく、State/Gettersからのデータ取得やCommit/Dispatchの呼び出しの状態管理へ集中することが出来ます。
ルーティング
「認証チェック」、「認可チェック」、「ページのタイトルを変更」などのルーティングを横断する機能は、Vue Routerのナビゲーションガードを使用して実現しています。
「認証チェック」、「認可チェック」はrouter.beforeEach、「ページのタイトルを変更」はrouter.afterEachを使用しています。
「認証チェックは必要か」、「必要な権限は何か」、「ページのタイトル」は何かなどの値はルートメタフィールドとして、ルーティングに設定することができます。
ナビゲーションガードとルートメタフィールドを組み合わせて使用することで、ルーティングでフィルタを実現できます。ルーティングでフィルタを実装することで、viewsの各コンポーネントは画面の機能にのみ責務を持てば良いようになります。
Ajaxの抽象化
Ajaxにはaxiosを使用しています。axiosはVue.jsが一般的なアプローチとして、提案しているライブラリです。
axiosにはInterceptorsという機能があります。axiosのInterceptorsを利用することで「requestに認証のためのJWTを設定」、「通信中表示の切り替え」、「エラー制御」などをaxiosが呼び出されたときに実行させることができます。
Ajaxを実行する際、横断的に行う制御をaxiosに寄せることで、DAOはモデルに対する操作にのみ責務を持てば良いようになります。
入力検証
入力検証にはvalidatorjsを使用しています。
validatorjsを採用した理由は、「フレームワーク/ライブラリに依存していない純粋なプログラムによる入力検証」ということです。そのため、Vuexで入力検証を行い、検証結果を管理することができます。
Vuexで検証結果を管理することができるため、検証結果をコンポーネントをまたいで利用することが容易になっています。ディレクトリ構成
|--App.vue … ルートコンポーネント |--assets … 共通CSSやメディアファイル |--common … 汎用的なクラス・関数 | |--auth … 認証 | |--error … エラークラス | |--http … Ajaxオブジェクト |--components | |--系統の分類 … メンバー用/管理用/個人設定などの分類 | | |--モデルの分類 … カテゴリ/タグなどの分類 | | | |--画面の分類 … カテゴリ一覧/新規カテゴリなどの分類 | | | | |--パーツ.vue … ヘッダ/検索フォーム/一覧などのパーツ毎のコンポーネント | |--common … グローバルヘッダなどのコンポーネント | |--ui … 各InputやButtonなどを抽象化したコンポーネント |--dao … DAO(Data Access Object) | |--index.js … モデル毎のファイルをまとめるためのオブジェクト | |--モデル毎.js … モデル毎の処理 |--filters … Vue.jsのFilter |--main.js … Vue.jsの初期化 |--plugins … Vueのプラグイン |--router … Vue Router | |--index.js … Vue Routerの初期化 | |--config.js … Vue Routerの初期化設定 | |--filters … Vue Routerのフィルタ | |--routes … ルーティングの設定 |--store … Vuex | |--index.js … Vuexの初期化 | |--modules … Vuexのモジュール用ディレクトリ | | |--系統の大分類 … メンバー用/管理用/個人設定などの分類 | | | |--モデル毎の分類 … カテゴリ/タグなどの分類 | | | | |--モデルに対する画面の分類.js … カテゴリ一覧/新規カテゴリなどの分類 | | |--common … 画面毎ではないモジュール |--util … ユーティリティクラス・関数 |--validator … 入力検証(validatorjs) | |--index.js … validatorjsの初期化 | |--lang_*.js … 言語毎の入力検証エラーメッセージの定義 | |--rules … カスタム入力検証ルール |--views … ルーティングで表示されるコンポーネント | |--系統の大分類 … メンバー用/管理用/個人設定などの分類 | | |--App.vue … グローバルヘッダ/ネストしたルーティング画面などの表示 | | |--モデル毎の分類 … カテゴリ/タグなどの分類 | | | |--モデルに対する画面の分類.vue … カテゴリ一覧/新規カテゴリなどの分類まとめ
- DAO経由で永続化処理を行い、状態管理から永続化処理の責務を分離した
- ルーティングにフィルタ機能を作成して、ルーティングで「認証」、「認可」、「ページのタイトルを変更」などを行った
- axiosのInterceptorsを利用して、AjaxにJWTを設定したり、通信表示の切り替え、共通エラー制御などを行った
- Vuexで入力検証して、検証結果を参照しやすくした
- 投稿日:2019-12-12T18:16:05+09:00
Vue.jsで好きなだけ「大石泉すき」とツイートしてみる
この記事は 「大石泉すき」アドベントカレンダー 12日目の記事です。
今回は重複を気にせず「大石泉すき」とツイートしていきたいと思います。好きなだけ「大石泉すき」とツイートできるページ
こちらからどうぞ。
Tweet 大石泉すき without duplicated entry
https://ohishi-izumi-suki.herokuapp.com/ソースコードはこちら。
m19e/non-duplication-tweet
https://github.com/m19e/non-duplication-tweet諸注意
このページはVue.jsで作ってHerokuでホスティングされていますが、
両方とも細かい説明はしていないので予めご了承ください。公式サイト↓
Vue.js - The Progressive JavaScript FrameworkHerokuへのデプロイはこちらを参考にしました↓
Vue.jsのアプリケーションを手早くHerokuで公開する何をしているのか
templateとmethodsはこんな感じ。
OhishiIzumiSuki.vue<template> <div class="wrapper"> <h1>Tweet {{ msg }} without duplicated entry</h1> <div class="tweet-button-wrapper"> <a class="tweet-button" @click="setUrl(msg)" :href="url" target="_blank">{{ msg }}</a> </div> </div> </template>OhishiIzumiSuki.vuemethods: { randomRange(start, end) { return Math.round(Math.random() * (end - start)) + start }, generateZWSP(width, result = '') { if (!width) return result return this.generateZWSP(--width, result += '\u200B') }, insertZWSP(text, result = '') { if (!text) return result result += text[0] + this.generateZWSP(this.randomRange(0, 20)) return this.insertZWSP(text.slice(1), result) }, countBytes(text) { return encodeURIComponent(text).replace(/%../g,"x").length }, setUrl(text) { let content = this.insertZWSP(text) this.url = "https://twitter.com/intent/tweet?text=" + encodeURI(content + "\nhttps://ohishi-izumi-suki.herokuapp.com") console.log(`「${content}」は${content.length}文字(${this.countBytes(content)}bytes)です`) }, },大きく分けて3つのことをしています。
- 「大石泉すき」にゼロ幅スペース(ZWSP)を挟む
- ツイート用URLくっつけてボタンに入れる
- コンソールに「大石泉すき」の文字数、バイト数を表示する
以上! 簡単ですね。
1.ゼロ幅スペースを挟む
ゼロ幅スペース(zero width space)って?
ゼロ幅スペース(ゼロはばスペース、英: zero width space, ZWSP)は、コンピュータの組版に用いられる非表示文字で、文書処理システムに対して語の切れ目を示すのに用いる。
Wikipedia - ゼロ幅スペース?
要するに「表示されないけどちゃんと存在してる幅のないスペース」って事です。
ゼロ幅スペース(以下ZWSP)を挟むことで見た目は同じテキストでも重複なくツイートする事ができます。画像の通り、見た目は同じでも文字数バイト数が違います。
これで連投しても怒られないぞ。やったね!randomRange(start, end) { return Math.round(Math.random() * (end - start)) + start }, generateZWSP(width, result = '') { if (!width) return result return this.generateZWSP(--width, result += '\u200B') }, insertZWSP(text, result = '') { if (!text) return result result += text[0] + this.generateZWSP(this.randomRange(0, 20)) return this.insertZWSP(text.slice(1), result) },
- randomRange()で0から20までのランダムな数値を取得
- 数値をgenerateZWSP()に渡してその数だけ結合されたZWSPを返す
- insertZWSP()でテキストの頭から一文字とって作ったZWSPを結合していく
隙あらば再帰しています。好きなので。
generateZWSP()内の'\u200B'が
UnicodeエスケープシークエンスでのZWSPですUnicodeエスケープシークエンス↓
#Unicode_escape_sequences 字句文法 - JavaScript | MDN2.ツイート用URLくっつけてボタンに入れる
ツイート用URLは色々できるWeb Intentsで。
Web Intents - Twitter DevelopersOhishiIzumiSuki.vue<div class="tweet-button-wrapper"> <a class="tweet-button" @click="setUrl(msg)" :href="url" target="_blank">{{ msg }}</a> </div>OhishiIzumiSuki.vueinsertZWSP(text, result = '') { if (!text) return result result += text[0] + this.generateZWSP(this.randomRange(0, 20)) return this.insertZWSP(text.slice(1), result) }, setUrl(text) { let content = this.insertZWSP(text) this.url = "https://twitter.com/intent/tweet?text=" + encodeURI(content + "\nhttps://ohishi-izumi-suki.herokuapp.com") console.log(`「${content}」は${content.length}文字(${this.countBytes(content)}bytes)です`) },
- insertZWSP()でテキストにZWSPを挟みこむ
- ツイート用URLを作ってthis.urlに代入
setURL()が呼ばれる度にaタグの:hrefが更新され、ツイート内容が変わります。
文字数、バイト数を表示
レギュレーションを遵守するべく「大石泉すき」と出力していきます。
大事な情報も一緒に表示してしまいましょう。OhishiIzumiSuki.vuecountBytes(text) { return encodeURIComponent(text).replace(/%../g,"x").length }, setUrl(text) { let content = this.insertZWSP(text) this.url = "https://twitter.com/intent/tweet?text=" + encodeURI(content + "\nhttps://ohishi-izumi-suki.herokuapp.com") console.log(`「${content}」は${content.length}文字(${this.countBytes(content)}bytes)です`) },
- 変数展開してコンソールに出力
- バイト数も知りたいのでcountBytes()で数える
完成!
終わりに
当然ながらこのページからのツイートは「大石泉すき」サーチに引っかかりません。
担当サーチの邪魔にもならないので安心して大石泉すきツイートしていきましょう。
あんまり連投するとこわい
- 投稿日:2019-12-12T15:08:59+09:00
Flamelinkでブログ機能をVue+Firebaseで作ったSPAに追加する
はじめに(言い訳)
クソアプリアドベントカレンダー初参加のかつおです!
昨年は完全に読む側で、投稿されるアプリのセンスや利用技術のレベルの高さ、力の入れ具合にただただ関心していました。みなさん本当に凄い!
まさか憧れのクソアプリアドベントカレンダーに記事を投稿する日が来ようとは想像だにしなかったのですが、今回は募集期間に偶然出会ってしまったので、勇気を出して申し込みをさせていただきました!!伝統のクソアプリアドベントカレンダー
「クソアプリは俺が制す。」
断固たる決意を胸に秘め、脳内企画に技術を試したりとかしていたのですが、12月の某日、状況が一変します。「辛いものが好きアドベントカレンダー」たるものが、私が運営しているカライイネのユーザー様の手によって立ち上がったのです!
「これは...クソアプリ作ってる場合じゃねーな。。」
私はカライイネのユーザー様に心臓を捧げた人間です。
辛いものが好きアドベントカレンダーを少しでも盛り上げるべく、カライイネにアドベントカレンダーに合った新機能を作ろう!
(作った機能は大したことないのでクソっちゃクソです)という流れで今回の開発に至ったのでした。。
つくったもの
もともと運用していた激辛口コミサイトカライイネのマガジン機能です。
CMSとしてFlamelink CMSを使ってみました。Flamelink CMSとは?
- コンテンツ管理(記事投稿とか)のみをやってくれるSaaS
- 自分で用意したFirebaseプロジェクトと連携してCMSを構築
- 投稿したコンテンツは自分のFirebase上に保存される
- フロントはなし。コンテンツを取得するAPI等が用意されている
- Firebaseにデータ保持されるので、構造が理解できれば普通のデータアクセスも可能
- 無料枠あり。個人開発レベルなら無料枠に全然収まる。
使ってみた感想
good
- CMSが簡単に構築できるのは便利
- 使い慣れたFirebaseにデータが乗っかるので理解しやすい。
bad
- アップデートちゃんとされてるか、今後もされるか不安
- Wardpressと比較して画像の挿入とか面倒
- データ取得のAPIが権限エラーになって使えなかった.. (Functionsから直接Firestoreを参照することで回避)
システム構成
- もともと本体もFirebaseで運用していたのですが、今回のマガジン用には別のFirebaseプロジェクトを作成しました。(計2プロジェクトのサービスに...)
- Flamelinkは記事の編集に利用。記事データはFirestoreとStorageに保存されます。
- SPAからの記事の取得はFunctionsで作成したAPIを利用しました。
- Functionsで記事の取得は普通にFirestoreの参照をしています。
作り方
Flamelinkの設定
以下が詳しすぎるので、参照ください。
今すぐ始められる!FIrebaseをブログのCMSに変える「Flamelink」を使ってみた!上記の記事通りにやってないと
①DB
Realtime DatabaseとFirestoreの選択が必要なのですが、私は慣れてるFirestoreを選択しました。(PaizaさんはRealtime Database)
②コンテンツの取得方法
「記事コンテンツを取得してみよう!」の章でflamelink.jsのライブラリ(flamelink sdk)を利用して記事データを取得していますが、私は利用していません。。
flamelink sdkを利用しようとしたのですが、Firebaseの権限エラーを解消できず、もういいやって利用をやめました。
ググってみると同様のエラーに対して、RealtimeDatabaseを使えとの指示が...③flamelink sdkインストール時のエラー対処
(結局やめましたが)flamelink sdkはnpmでインストールして利用したのですが、npm installでエラーとなりました。
ググってみると、node.jsのバージョン10だとエラーになるとの記載が...
node.jsのバージョンを8に変更することで、エラー回避してインストールはできました。
権限エラーになるので、結局利用はやめましたが…
ちなみにnode.jsのバージョン変更にはnodistを利用しました。(Windows環境)
nodist利用の際には、既存のnode.jsを1度アンインストールが必要なので面倒くさかったです。④functionsをAPI化
flamelinkのプロジェクトとフロントのプロジェクトが分かれているのでfunctions経由でflamelinkのデータを取得しています。
functionsのコードを載せておきます。index.js
index.jsconst functions = require('firebase-functions') const admin = require('firebase-admin') admin.initializeApp(functions.config().firebase) const express = require('express') const app = express() app.use((req, res, next) => { // 許可ドメイン設定 res.header('Access-Control-Allow-Origin', 'https://karaiine.jp') res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') next() }) app.use('/api/posts', require('./apps/posts.js')) exports.app = functions.https.onRequest(app)./apps/posts.js
./apps/posts.jsvar router = require("express").Router() const admin = require('firebase-admin'); var db = admin.firestore() router.get('/', (request, response) => { // isOpen=trueで公開記事を、新しい順に取得 db.collection('fl_content').where('isOpen', '==', true).orderBy('date', 'desc').get() .then((snapshot) => { let list = [] snapshot.forEach(doc => { let data = doc.data() list.push(data) }) response.json(list) }) .catch(error => { console.log(error) }) }) module.exports = routerさいごに
読んでいただきありがとうございます!
辛いものが好きな方、辛いものが好きアドベントカレンダーもお願いいたします!
辛いものが好きアドベントカレンダー
- 投稿日:2019-12-12T13:43:42+09:00
親から子へ、子から親へデータを渡す方法
props
親から子へデータを渡したい場合。
親コンポーネントから送られきた情報によって子コンポーネントの表示を変えたいときなど。親要素で下記のように記載。
<template> // ↓子コンポーネント名 ↓渡したいデータ <DetailTemplate v-bind="detail" /> </template> <script> export default { data() { return { detail: "OK!" } } } </script>子コンポーネントは下記のように記載。
<template> {{ detail }} </template> <script> export default { props: { dateil: "" } } </script>これで子コンポーネントでOK!が表示される。
$emit
子から親へデータを渡したい場合。
子コンポーネント内のボタンを押したりした場合に、親コンポーネントが持つfunctionを発火させたいとき使う。
今回は子コンポーネントにあるselectタグで選択した値を親要素に遷移する+親要素のfunctionを発火させる処理。子コンポーネントで下記のように記載。
<template> //@changeはv-on:chengeの省略形。選択すると自身のメソッド、pushButtonが呼び出される。 <select v-model="selected" @change='changeSelectTab'> //選択肢はdataのなかで記載している。今回は関係ないのでdata部分省略。 <option v-for="option in options" v-bind:value="option.value" v-bind:key="option.value"> {{ option.text }} </option> </select> </template> <script> methods: { //pushButtonメソッドを関数にしてしまって、その中で親要素に送るメソッド名と選択した値を第二引数にして送る。 changeSelectTab: function(){ this.$emit('selectTab', this.selected) } } </script>親コンポーネントで下記のように記載。
<template> <div> //↓子コンポーネント名 ↓子コンポーネントで$emit('selectTab')が実行されると親のparentEventが発火。 <ChildComponent @selectTab="parentEvent"/> </div> </template> <script> methods: { //引数には子コンポーネントのthis.selectedが入る。 parentEvent: function(selected) { if (selected === 1) { console.log("1が選択されました!") }); } </script>emitに関してはこの記事参考。
https://qiita.com/shosho/items/b9b24a52dc0cc0fc33f5
- 投稿日:2019-12-12T12:43:16+09:00
hello world!を出力する ❏Vue.js❏
まずはここからですよね!
HTMLやRubyを初めて学習した時のことを思い出します。感慨深い。開発環境は
JSFiddleを使います。
参考はこちら
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3b
html<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"> <p> {{ message }} </p> </div>javascriptnew Vue({ el: "#app", data: { message: "hello world!" } })【出力結果】
hello world!解説
1:
CDNでVue.jsを読み込む
公式サイトのインストールの項目に載っているのでコピペ2:
idをつける3:
new Vueでインスタンスを宣言4:
el: "#app"で指定5:
dataの中にプロパティを書く
今回はmessage: "hello world!"6:
二重中括弧で表示したいものを囲う
{{ message }}
無事hello world!が表示されました!
感動。。これから新たな旅がハジマル〜!!
ではまた!
- 投稿日:2019-12-12T12:28:41+09:00
Vue-cliでlocalhost:8080が接続エラーになる件の原因と解消[macとeset使用の場合]
あらすじ
勉強がてらVueの開発環境を一から自分で作ってみたい!
と思い立ったある日のこと。
いろいろ調べるうちに便利そうなのあるやんと、Vue-cliを発見。ネットサーフィンをしながら情報を集めて、いろいろ設定を進めると無事に出来上がった模様。
(立ち上げ方はネットに死ぬほど落ちてるので、本記事では割愛)えっめっちゃ楽やん!とか思ってました。
ただ、まだこの時はどハマりするとは思っていませんでした...。設定完了後、黒い画面(console)に
App running at: - Local: http://localhost:8080/ - Network: http://***.***.***.***:8080/ //<- *の部分は自分のIPだよここに接続して確認するのね。と、クリックすると接続エラー画面を連発、、、
表示してくれ!と願いを込めて押すリロードボタンは空回るばかり。
どちらもダメです。
たまに時間を置くと、まぐれで画面が表示するけど、またしばらくすると接続エラー...。不安定すぎて、開発できねーよ(T-T)、、、とか思いながらいろいろ調べたけど解消できず、半日が経過。
精気を全て奪われ、社内のインフラ担当に相談して無事に解決した件を忘備録。原因は2つありました。
社内のセキュリティーにesetを使用していた。
まずこれ!
単にlocalhost:8080の接続を許可していなかった。
これは僕の方で設定できなかったので、権限がある人に許可できるよう追加設定してもらいました。
するとlocalじゃなくて、Networkのほうのリンクは効く!画面が表示されました。
「ほほう、新人インフラくんやるやん!」と褒めてあげます。
ただまだ肝心のhttp://localhost:8080が接続エラー。IPv6の設定が邪魔してる?
すると新人くんまたまたファインプレー。
試したいことがあります。と言って
黒い画面を召喚。$ sudo vi /private/etc/hosts魔法のコマンドを発動。
sudo(アカウントの権限)でetcディレクトリのhostsファイルをviで編集するという意味のようです。
その後パスワードを求められるので、アカウントのパスワード(パソコン自体やつね)を入力。すると!
こんな画面が表示されます。## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## ***.***.***.*** localhost ***.***.***.*** broadcasthost ::1 localhost※IPは*で隠してます。
Host Database...よくわかりません。
新人インフラくん曰く、IPアドレスに名前をつけておく管理ファイルのようです。原因はここ。
***.***.***.*** localhost ***.***.***.*** broadcasthost ::1 localhostこの部分の3行目が悪さしてました。
あんまりよく理解してないので、間違っていたらご指摘・補足を。1行目のlocalhostはIPv4です。
3行名のlocalhostはIPv6です。簡単に説明するとIPV4は0〜255を.で4つ繋いだもので、よく見るIPアドレス。
これが枯渇してきている問題があるので、新しいIPフォーマットを作ろう!
というので出てきたのがIPv6。実際にはまだ使っていないのですが、使っているMacでは設定箇所が効いてる模様。
::1 localhostこれを
# ::1 localhostこのようにコメントアウトします。
(viの入力モードはaキーを押します。入力モード解除はescキー)そのあと:wq(wは保存/qは終了)と打って元の画面に戻ります。
改めて画面を確認すると、無事にVueの画面が表示されてました。よき。
※強めのキャッシュが効いている可能性もあるので、本当に変わっているかどうかを確認するには違うブラウザで試してみるといいですよ。Ipv6はこれから徐々に浸透していくみたいです。
でもまだ使う予定はないので、今はまだコメントアウトしておくほうがいいかもしれないですね。ちなみにMacのシステム環境設定 -> ネットワーク画面にある右下の詳細からIPv6の設定がありますが、
ここの設定をいじっても ちゃんと表示できませんでした、、、なぜ?
- 投稿日:2019-12-12T12:22:04+09:00
10分コーディング:ユーザーの入力に応じてリストの内容を書き換える。(Google Apps Script + Vue.js + Bootstrap)
はじめに
部署リストと氏名リストを設置し、
選択された部署に紐づく氏名のみ抽出して氏名リストに表示させる。vue.html
部署のリストデータはスプレッドシートから読み込み[deptList]にセットし、
そのリストから選択された値を[sortUser]に格納する。[watch]は,データ変更の検知して処理を実行するプロパティ。
そのプロパティに[sortUser]を指定して、
ユーザーが部署を選択した際にその選択した部署でユーザー情報をフィルタリングする。vue.html<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> <script> var vm = new Vue({ el: '#app', data: { changeTemplate:'applyDisp', deptList:[{}], userName:'', sortUser:'', userList:[{}], selectedUserList:[{}], }, computed: { }, watch: { //sortUser値の変化をwatchして変化したら選択された値でリストをフィルターする sortUser: function (){ var selDept = this.sortUser; this.selectedUserList = this.userList.filter(function(el,index){ if (el.Dept == selDept) return true; }); } }, methods:{ setDeptList: function(deptData){ this.deptList = deptData; }, setUserList: function(userData){ this.userList = userData; }, checkForm: function(){ this.changeTemplate = 'confirmDisp'; }, appendData: function(){ this.changeTemplate = 'thanksDisp'; }, }, created: function(){ google.script.run .withSuccessHandler(this.setDeptList).getDeptList(); google.script.run .withSuccessHandler(this.setUserList).getUserList(); }, }) </script>index.html
index.html<!DOCTYPE html> <html> <head> <base target="_top"> <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="#">Link</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a> <div class="dropdown-menu" aria-labelledby="dropdown01"> <a class="dropdown-item" href="#">Action</a> <a class="dropdown-item" href="#">Another action</a> <a class="dropdown-item" href="#">Something else here</a> </div> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <main role="main" class="container"> <div class="starter-template"> <h1>Bootstrap starter template</h1> <p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p> </div><!-- /.starter-template --> <div id="app"> <div class="form-group row"> <label for="selectedDept" class="col-sm-2 col-form-label">Select Name</label> <div class="col-sm-5"> <select id="sortUser" v-model="sortUser" class="form-control"> <option v-for="option in deptList" v-bind:value="option.ID"> {{ option.Name }} </option> </select> </div> <div class="col-sm-5"> <select id="userName" v-model="userName" class="form-control"> <option v-for="option2 in selectedUserList" v-bind:value="option2.ID"> {{ option2.Name }} </option> </select> </div> </div> </div><!-- /.vue.el.app --> </main><!-- /.container --> <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?> </body> <?!= HtmlService.createHtmlOutputFromFile('vue').getContent(); ?> </html>コード.gs
コード.gsfunction doGet() { var html = HtmlService.createTemplateFromFile("index").evaluate().addMetaTag('viewport','width=device-width,initial-scale=1,minimal-ui'); return html; } function getSS(spreadSheetID, sheetName){ var res = SpreadsheetApp.openById(spreadSheetID) .getSheetByName(sheetName).getDataRange().getDisplayValues(); var keys = res.splice(0, 1)[0]; return value = res.map(function(row) { var obj = {} row.map(function(item, index) { obj[keys[index]] = item; }); return obj; }); } function getDeptList() { var SSID = "yourSpreadsheetID"; var SN = "DeptList"; var res = getSS(SSID, SN); return res; } function getUserList() { var SSID = "yourSpreadsheetID"; var SN = "UserList"; var res = getSS(SSID, SN); return res; }css.html
css.html<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <style> body { padding-top: 5rem; } .starter-template { padding: 3rem 1.5rem; text-align: center; } .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } </style>js.html
js.html<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>参考
- 投稿日:2019-12-12T10:56:32+09:00
Vue.jsはじめました(ビギナー向けまとめ)
食べログフロントエンドチームの@nakiaです。
元請けのSIerで3年半ほどシステム屋さんをやってましたが、もっと自分でコード書けるようになりたい!と一念発起しまして、ちょうど1年前に食べログにやってきました
優秀なメンバーに囲まれ、学びの多い毎日を送っています今日のAdventCalendarは、フロントエンド初心者の私が0からVue.jsを勉強した学習記録です!
公式ガイドの他に「基礎から学ぶVue.js」(書籍)を参考にしています。初心者向けなので、より実践的な内容をお求めの方はぜひ@empitsu88 さんの「Nuxt.js+TypeScriptのアプリケーションのためのコーディングガイドライン」をご覧ください
Vue.jsの基本
Vue.jsとは
比較的新しいJavaScriptのフレームワークです。以下のような特徴があります。
- 導入のしやすさ・学習コストの低さ
- バンドル・プリコンパイルしなくても動く
- Hello World!を表示するまでが簡単
- スケールの柔軟性
- ページ内の1機能〜SPAの大きなプロダクトまで対応可能
- 日本語ドキュメントの充実
- 本体以外のドキュメントも日本語充実!(ビギナー的にこれは非常にありがたい)
機能単体でサクッと導入できる感じは、フレームワークというよりライブラリに近い使用感です。
jQuery的な手軽さがあります。Vue.jsのキーコンセプト:データ駆動
Vue.jsの基本となる考え方は、データ駆動です。
- 従来のJSの考え方
- DOM構造に合わせてデータを加工し描画する
- Vue.jsの考え方
- データの変更に合わせてDOMを構築・更新する(データバインディング)
簡単なサンプルを紹介します。
適当なフォルダに以下のファイルを作成して、ブラウザで開いてみてください。<!DOCTYPE html> <html> <body> <div id="app"> <h1>{{ message }}</h1> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.13"></script> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue.js!' } }) </script> </body> </html>dataに定義されたmessageを更新すると、
{{ message }}の中身が動的に更新されます。上記のようなテキストバインディングの他に、
- イベントを利用する
- フォーム入力した内容と表示を同期する
- 条件によって表示を出し分ける
- 簡易なアニメーションをつける
などなど、様々な便利機能が用意されています。
詳しい説明はVue.js公式ガイドをご覧ください。Nuxt.jsとは
Vue.jsをベースにしたフレームワークです。
上で書いたように、Vue.jsは機能単位で簡単に導入でき、柔軟に拡張できるというメリットがあります。
一方で、きちんとルールを決めて運用していかないとコードがカオスになりやすいという欠点もあります。
(これはVue.jsに限った話ではなく、従来のフロントエンド開発の課題でもあります)この欠点を補ってくれるのが、Nuxt.jsのもたらす「規約」です。
Ruby on RailsがRuby開発のベストプラクティスをルールとして定めているように、Nuxt.jsはVue開発のベストプラクティスを定めています。
Nuxt.jsの規約に従うことで、設計や実装方法について議論するコストを最小限に抑えつつ、一定の納得感が得られるアーキテクチャと統一的な記述を担保できます。もちろん、Nuxt.jsは現代のフロントエンド開発に必要な機能についても一通り備えています。
- SSR(Server Side Rendering)
- webpack、モジュールの最適化(ビルドプロセスの隠蔽)
- Vue本体,Vue拡張機能との連携
こちらも日本語で公式ガイドが公開されてますので、詳細はそちらをご確認ください。
Vue.jsの周辺技術
Vue + Nuxtで開発するときにお世話になる周辺技術を紹介します!
状態管理したい:Vuex
Vuexはデータとその状態を一元管理するための拡張ライブラリです。
コンポーネントベースの開発(※)では、基本的に$emitやpropsを使ってコンポーネント間でデータのやりとりをしますが
アプリケーションの規模が大きくなると、データ管理の処理もそれだけ煩雑になります。Vuexでデータを管理すると、コンポーネントの構造に関わらず
アプリケーション全体で同じデータを同期的に共有できるようになります。※コンポーネント開発はそれだけで1冊本が書けるぐらい壮大なテーマなので、今回は説明を割愛します。
Vueにおけるコンポーネントについては公式にちょっとだけ説明があるので、気になる人は見てみてください。Vuexストアはこんな感じで書きます。
// モジュールシステムを利用しているときはあらかじめ Vue.use(Vuex) を呼び出していることを確認しておいてください const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } } })これで
store.stateでストアオブジェクトの状態を参照、store.commitで状態の変更を行うことができるようになります。ルーティングしたい:Vue Router
Vue Routerは、Single Page Application(SPA)を構築するための拡張ライブラリです。
各コンポーネントとURLを紐付けてくれます。SPAとは
単一のWebページの中で複数の要素(コンポーネント)を置き換えることで画面遷移を実現する設計のことです。
必要な要素だけを読み込むので描画が素早いのが特徴です。
遷移時にアニメーションをつけることでいわゆる「イマドキなWebページ」を作ることができます。Nuxt.jsにおけるルーティングの仕組み
Nuxt.jsでは<nuxt-link>というコンポーネントを使用してルーティングを実現します。
<template> <nuxt-link to="/">Home page</nuxt-link> </template>以下のように、決められたディレクトリ構造に従ってファイルを配置するだけで自動的にURLを生成してくれるので、とっても便利です!
■ディレクトリ構造
pages/ --| user/ -----| index.vue -----| one.vue --| index.vue■生成されるroutes
router: { routes: [ { name: 'index', path: '/', component: 'pages/index.vue' }, { name: 'user', path: '/user', component: 'pages/user/index.vue' }, { name: 'user-one', path: '/user/one', component: 'pages/user/one.vue' } ] }具体的な設定方法は公式のサンプルをご覧ください。
バリデーションしたい:Vee Validate
Vee Validateは複雑なバリデーションを実装する場合に便利なVueライブラリのひとつです。
(こちらのページで紹介されていますが、Vue.jsが公式で提供しているものではなく、日本語版のガイドがまだありません…)対象のコンポーネントにValidationProviderをimportすると、バリデーションが利用できるようになります。
import { ValidationProvider } from 'vee-validate'; Vue.component('ValidationProvider', ValidationProvider); // ...対象のフォーム全体を<ValidationProvider>で囲み、rulesプロパティにバリデーションルールを設定します。
ruleに違反した場合、ValidationProviderから受け取ったerrorsを描画します。<ValidationProvider rules="positive" v-slot="{ errors }"> <input v-model="value" type="text"> <span>{{ errors[0] }}</span> </ValidationProvider>バリデーションルールは以下のように関数で定義することができます。
import { extend } from 'vee-validate'; extend('positive', value => { return value >= 0; });ちなみに、submit直前のエラー表示など、フォーム全体を監視したい場合は<ValidationObserver>でフォームを囲みます。
HTTP通信したい(axiosを使いたい)
axiosとは
HTTP通信を簡単に行うことができる、PromiseベースのJSライブラリです。
主にAPIからデータを取得して表示したいときに使用します。// GET通信 axios.get('http://localhost:7000/user') // thenで成功した場合の処理をかける .then(response => { console.log('status:', response.status); // 200 console.log('body:', response.data); // response body. // catchでエラー時の挙動を定義する }).catch(err => { console.log('err:', err); });Vue.jsでaxiosを使う書き方は公式をご覧ください。
Nuxt.js用のaxios module
コンポーネントを初期化する前に非同期の処理を行いたいケースを想定し、Nuxt.jsはasyncDataというメソッドを用意しています。
asyncData は ページ コンポーネントがローディングされる前に常に呼び出されます。サーバーサイドでは 1回だけ(Nuxt アプリへの最初のリクエスト)呼び出され、クライアントサイドではページ遷移をするたびに呼び出されます。
とのことです。
export default { async asyncData ({ params }) { const { data } = await axios.get(`https://my-api/posts/${params.id}`) return { title: data.title } } }asyncDataの結果を受けてコンポーネントの描画を更新できます。
<template> <h1>{{ title }}</h1> </template>TypeScriptで書きたい:vue-class-component/vue-property-decorator
TypeScriptと使用すると、型定義によってより堅牢にJSを書けるようになります。
VueアプリケーションをTypeScriptで実装する場合、以下のようなプラグインを利用することで可読性を高めることができます。vue-class-component:Vueを継承したClassとしてコンポーネントを宣言できるようになります。
vue-property-decorator:@Propsなどデコレーターを使って宣言できるようになります。具体的な書き方については、以下の記事が大変参考になりました!
https://qiita.com/ryo2132/items/4d43209ea89ad1297426
https://qiita.com/hatakoya/items/8d9968d07748d20825f8まとめ
いかがでしたか?実は、記念すべきQiita初投稿でした…!
質問・コメントはお手柔らかにお願いします明日は@k-sekidoさんの「システム開発で納得感を持って進めるために考えていること」です!
お楽しみに〜
- 投稿日:2019-12-12T10:24:32+09:00
Vue.js の inheritAttrs に関する大きな勘違い
はじめに
Vue コンポーネントには
inheritAttrsというディレクティブがあります。直訳すると 「属性を引き継ぐ」。
まず簡単な例を示します。Vuetify.js の v-btn をラップしたボタンを作ってみましょう。
- クリックすると消滅する
DismissibleButtonを作ります。- 消滅するまでの時間をミリ秒で
timeoutで指定できるようにします。DismissibleButton.vue<template> <v-btn v-if="visible" @click="hide"> <slot /> </v-btn> </template> <script> export default { props: { timeout: { type: Number, default: 0, }, }, data() { return { visible: true, }; }, methods: { hide() { setTimeout(() => { this.visible = false; }, this.timeout); }, }, }; </script>使用側<dismissible-button :timeout="1000">Click Me</dismissible-button>
- スロットとしてボタンテキストを受け取り,それをそのまま
<v-btn>に受け流し。- 可視性の制御を加えるちょっとした実装を書きました。
inheritAttrsの動作ではここで,
「
<v-btn>にtoプロパティとnuxtプロパティを渡したい!」というニーズが発生したとします。
ここで挙げられている解決策の
<v-btn to="/path/to/link" nuxt>リンク</v-btn>これですね。これをラップした
<dismissible-button>において,できるだけ再利用性の高い形で<v-btn>に受け流します。<dismissible-button :timeout="1000" to="/path/to/link" nuxt>リンク</dismissible-button>このような使い方をすることを考えます。
trueのとき (デフォルト)何も指定していないときはこれが適用されます。
上記の場合 component B は展開されると
<div color="red" type="number">{ color: 'red', type: 'number' }</div>という html を出力します。
component B に props が定義されていない属性を与えるとデフォルトでは生成されるルートの要素に属性が追加されます。
これを読んだ僕は,このようなレンダリング結果を期待しました。
<v-btn>がコンポーネントのルート要素だから,勝手にそこに引き継いでくれる!!!と。
期待した結果<template> <v-btn v-if="visible" @click="hide" :to="to" :nuxt="nuxt"> <slot /> </v-btn> </template>でも実際はそこに展開されず,なんと
<v-btn>より更に下のHTML要素の<button>に渡っていまっていました…実際の結果<button to="/path/to/link" nuxt=""></button>こういうことなのだ…
(この図を目に焼き付けて帰ってください)端的に言葉で表現すると
「
inheritAttrsで下位コンポーネントに流すと『propsとして定義されていたら$propsに流す』という処理をスキップして全部$attrsに流す」ということになります。これは初見殺しだ…
falseのとき
inheritAttrs: falseにすると,暗黙的な受け流しは行われなくなります。明示的にv-bindで受け流した場合は$propsに流すかどうかの判定が行われますが,自分でその処理を書く必要があります。DismissibleButton.vue<template> <v-btn v-if="visible" @click="hide" v-bind="$attrs"> <!-- ← これを追加! --> <slot /> </v-btn> </template> <script> export default { inheritAttrs: false, // ← これを追加! props: { timeout: { type: Number, default: 0, }, }, data() { return { visible: true, }; }, methods: { hide() { setTimeout(() => { this.visible = false; }, this.timeout); }, }, }; </script>
v-bind="$attrs"に関して説明すると,以下の2つの記述は等価となります。<v-btn v-if="visible" @click="hide" v-bind="$attrs"><v-btn v-if="visible" @click="hide" :to="$attrs.to" :nuxt="$attrs.nuxt">実際には
$attrsはこれに限らないので,数が増えても自動的にすべて受け流されるv-bind="$attrs"が優秀だと言えますね。まとめ
大事なことなのでもう一度。`
inheritAttrsはすべて$attrsに流す- 明示的に
v-bind="$attrs"で流すと$propsと$attrsの振り分けが行われる基本的に
inheritAttrs: trueにはあまり頼らないほうがいいのかもしれませんね…
- 投稿日:2019-12-12T09:04:18+09:00
ExpressとServiceWorkerを利用してPush通知を送る
はじめに
PCのブラウザにslackのようなPush通知を送りたい場合、SSRの構成であれば、アプリケーションサーバーとクライアントとプッシュサーバーがあれば実現できる。
プッシュサーバーはクライアントのブラウザごとに用意できるかどうかが異なり、Chrome Firefox Edgeなどが対応している。
Push通知を送るまでの流れとしては…
- クライアント側でPush通知受け取りの許可がされた場合必要な情報をアプリケーションサーバーに送る。
- アプリケーションサーバーでそれらの情報を保管しておく。
- Push通知を送る際に、保管情報をもとに、プッシュサーバーへリクエストを送る。
- プッシュサーバーからブラウザがPush通知を受け取る。
という形になる。
現在仕事でチャットシステムを開発しており、特定のユーザーに向けてメンションが送られた際、そのユーザーのデバイスにPush通知を送るようなシステムを開発したのでその手順を紹介する。
ようはslackのメンションを作りたい。
構成
サーバー
- Express
- SocketIO
- Node.jsの環境でWebsocketの接続ができるようになるライブラリ
- web-push
- サーバーからクライアントアプリに向けPush通知を流してくれるNPMパッケージ
DB
- MongoDB (CosmosDB)
クライアント
- Vue.js
- SocketIO-client
- SocketIOへ接続するためのクライアント側のライブラリ
- SocketIOとSocketIO-clientで統一したバージョン管理を行わないと動かない
- Axios
- HTTPリクエストはAxiosで行う
service workerの登録
システムをログイン制にし、チャットで他のユーザーへメンションを投げられるようにしてみる。
その際、メンションをserviceWorkerの機能を利用しログインユーザーの端末にPush通知を送れるようにしてみたい。VueでPWAを導入するための手順はNuxtで開発しているのか、vue-cliで開発しているのかで異なる。今回はvue-cliを利用していたため、そちらの方法を紹介する。
vue add @vue/pwaを実行すれば既存のプロジェクトにpwaの機能を追加することができる。
追加されるファイルに、registerServiceWorker.jsがあるが、これはブラウザにserviceWorkerを登録するjavascriptファイルになる。registerServiceWorker.jsimport { register } from 'register-service-worker' if (process.env.NODE_ENV === 'production') { register(`${process.env.BASE_URL}service-worker.js`, { ready () { console.log( 'App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB' ) }, registered () { console.log('Service worker has been registered.') }, cached () { console.log('Content has been cached for offline use.') }, updatefound () { console.log('New content is downloading.') }, updated () { console.log('New content is available; please refresh.') }, offline () { console.log('No internet connection found. App is running in offline mode.') }, error (error) { console.error('Error during service worker registration:', error) } }) }それぞれのファンクションは登録時のステータスに応じて呼び出される。エラー時にどうしたいといったことはここでハンドルができる。
また、vueはdevelopmentモードでのserviceWorkerの実行を推奨していないため、実際の登録はプロダクションで行う必要もある。
serviceWorkerがちゃんと登録されているかどうかは開発者ツールのApplicationタブをみて、Sourceにファイルが登録されていればいい。
Push通知許可の実装
よくwebで見かけるPush通知を許可するかどうかのボタンを実装する。これが許可されるとブラウザごとに設定されているPushサーバーからエンドポイントとなるURLとキーが発行される。サーバー側はPush通知を送りたい際にそのエンドポイントに向けてキーと一緒にPush通知として表示する内容を送り付けることができる。
https://developer.mozilla.org/ja/docs/Web/API/PushManager/getSubscription
PushManager.getSubscription().then(subscription => {})でsubscriptionにnullが入っていた場合、そのユーザーはPush通知を許可してなく、Push通知を許可していた場合は、subscriptionに上で述べた情報が格納される。これらの情報はこのPush通知を許可したユーザーに対して、Push通知を送るためにアプリケーションサーバーで保管しておく必要がある。registerServiceWorker.jsswRegistration.pushManager.getSubscription() .then(function(subscription) { // 通知が購読されたかどうか isSubscribed = !(subscription === null); // もし購読されていれば、アプリケーションサーバーへ購読者情報の登録 if (isSubscribed) updateSubscriptionOnServer(subscription); });service workerの実装
serviceWorkerではイベントごとにコードを書くことになる。
sw.js// push通知を受け取ったときの挙動 self.addEventListener("push", function(event) { const data = JSON.parse(event.data.text()); const title = data.title; // push通知のbody アイコンの情報を詰める。 const options = { body: data.message, // 表示メッセージ icon: "../thumbnail/pwa/android-chrome-192x192.png", // アイコン badge: "../thumbnail/pwa/android-chrome-192x192.png", // バッチアイコン }; event.waitUntil(self.registration.showNotification(title, options)); });また、serviceWorkerにはPush通知をクリックした際の挙動を記すことができ、アプリサーバー→Pushイベント→クリックイベントとその内容を指定することもできる。
アプリケーションサーバーでPush通知の認証情報を受け取る
serviceWorkerで発行されるPush通知に必要な情報は以下のような情報である。
{ "endpoint":"endpointURL", "p256dh":"...", "auth":"..." }
- endpoint - Push通知を送るためのPushサーバーのURL
- p256dh - ブラウザが発行した公開鍵
- auth - ユーザーエージェントとサーバー側で共有される共有鍵生成を難化するための乱数
開発しているシステムはログイン制のシステムなのでこれらの情報とユーザー情報を紐づけて保管している。ユーザーは複数のブラウザでシステムを扱う可能性があるためユーザーと認証情報は1:多の関係になる。
WebPushを用いてPush通知を送る
アプリケーションサーバーからPush通知を送るにあたってNode.js環境で利用できるweb-pushを利用した。
- サーバー側でもあらかじめ公開鍵と秘密鍵のセットを生成しておき、それをweb-pushの
setVapidDetailsでセットしておく。- 次にクライアントから受け取った
auth、p256dhの鍵、endpointのURLをセットすればPush通知を実行できる。server.jsconst webpush = require('web-push'); // サーバー側の鍵情報を詰める webpush.setVapidDetails( 'mailto:example@yourdomain.org', vapidKeys.publicKey, vapidKeys.privateKey ); // Push通知を送るクライアント側の情報を詰める const pushSubscription = { endpoint: '.....', keys: { auth: '.....', p256dh: '.....' } }; // Push通知を送る webpush.sendNotification(pushSubscription, 'Your Push Payload Text');Push通知は通常のHTTPリクエスト同様endpointのURLからPush通知の実行結果をHTTPステータスコードで受け取れるため、エラーハンドリングなどは各エンドポイントの仕様を確認すればできる。
腹が立つのがこのあたりの仕様がまったくエンドポイントのドメインごとに異なり、いちいち確認しなければいけないところ。
ユーザーがPush通知許可を取りやめたときのHTTPステータスコード
- chrome - 403
- firefox - 410
最終的に
あとはチャットの通信にWebPushを送る関数を呼び出せばチャットとPush通知機能がつながることになる。メンション先のユーザーがPush通知を許可していればブラウザから通知が表示されるようになる。
実際に世界で最も優れたブラウザVivaldiが受け取ったメンションのスクショ
(開発中のシステム名とアイコンが映ったので修正を入れてる)参考
ウェブアプリへのプッシュ通知の追加 | Web | Google Developers
Push通知に関するクライアント側の設定や書き方を大いに参考にした。@vue/cli-plugin-pwa
vue-cliでPWA環境を整えるために必要なモジュールweb-push
Node.js環境でPush通知を送るためのNPMパッケージ
- 投稿日:2019-12-12T06:38:31+09:00
明日使えるかもしれない Nuxt 上で唱えるプチ闇魔法3連発
この記事は Sansan Advent Calendar 2019 の12日目の記事です。
今年に入ってから Nuxt.js を使い始めましたがとっても便利で、今まで React 派でしたがこれがあるから Vue っていいなと思いました。
みなさんも Nuxt 使っているでしょうか。そして、モダンにかっこよく使えているでしょうか。僕はあんまりモダンじゃ無い、泥臭くやることを強いられた部分があったので、もしも同じことで悩む人のためにいくつか紹介したいと思います。
コンテンツは
- Component (Typescript) 内で外部 jQuery を利用する
- SPA + Producitonモードでエラーthrow時にエラー画面へ飛ばす
- ServerMiddlewareでCORSを無視してAPIリクエストする
の3点です。
前提
以下で動作確認
- Nuxt.js は v2.10
- Typescriptを使用
- Componentの作成には vue-property-decorator を使用
また、今回のコード全部入りはGitHubで公開しています。
1. Component 内で外部 jQuery を利用する
これは比較的普通に使えるネタかもしれません。
Component 内で jQuery を import して、$で使用します。型定義を準備
- package をインストール
npm i -D @types/jquery
tsconfig.jsonのtypesを以下のように追記します。tsconfig.json{ // "@types/jquery" を追加! "types": ["@types/jquery", "@types/node", "@nuxt/types"] }Component を書く
こんな感じ
jquery-example.vue<template> <div> <button @click="buttonClick" class="continue"> Click me </button> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' // Ref: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jquery#authoring-type-definitions-for-jquery-plugins declare const $: JQueryStatic // このページだけで外部jQueryを利用する書き方 // Ref: https://ja.nuxtjs.org/faq/#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%AA%E8%A8%AD%E5%AE%9A @Component({ head() { return { script: [{src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'}] } } }) export default class extends Vue { buttonClick() { $('button.continue').html('Next Step...') } } </script>これで使えます。
2. SPA モードでもエラーが起きた時にエラーページへ飛ばす
これは今回書く中でも特にやってみたら動いた系のネタなので Production レベルでは使わないか、使うにしても Nuxt のバージョンをしっかり固定しましょう
Nuxt では、
layout/error.vueを置くことで、404 にアクセスされた時や予期せぬエラーが起きた時に、そのエラーページを表示してくれる機能があります。 (参考)ですが、SPA モードかつ production build の場合、Component でエラーが throw されるとこのエラーページへ行かなかったので、無理やり制御しました。
エラーページを準備
これは
layout/error.vueを追加するだけです。layout/error.vue<template> <div> <h1>エラーが発生しました</h1> <nuxt-link to="/"> ホーム </nuxt-link> </div> </template>error を制御する plugin を追加する。
Vue.config.errorHandler を使ってエラーをキャッチして、nuxt インスタンスの
error()を呼び出します。
この部分が、ソースを追っかけていて「これで行けるんじゃね?」ってやったら動いた系なので取り扱い注意です。plugins/error-handler.jsimport Vue from 'vue' import { NuxtApp } from '@nuxt/types/app' Vue.config.errorHandler = (err, vm) => { const $nuxt = vm.$root as NuxtApp $nuxt.error(err) }nuxt.config.ts にも忘れず追加します。
nuxt.config.tsplugins: ['~/plugins/error-handler.ts'],これで、Component 内でエラーが起きた時に error.vue へ飛ぶようになります。
ちなみに SPA Production モードの挙動は build した後にnuxt-ts startで確認できます。エラーを起こすためのお試しComponentはこちら
3. CORS を無視して API にリクエストするための Proxy サーバーを立てる
backend がまだ準備できてない等の理由で、開発中、どうしてもクロスオリジンな API にリクエストしたい場合がないでしょうか。まあ、普通は無いと思います。
Nuxt には ServerMiddleware という機能があり、例えば
/apiという serverMiddleware を定義すると
http://localhost:3000/apiというエンドポイントを作ることができます。これを活用して、あたかも同じサーバーのエンドポイントなのに、裏では外部 API をリクエストする、みたいなことを実現できます。
外部 API へリクエストするサーバーを書く
- リクエストを受けて
- パスやパラメータを解析して
- そのパスとパラメータそのまま外部 API にリクエストする
みたいなサーバーを node.js で書きます。これは正直どんな実装でも良いのですがサンプルを載せておきます。
server/index.jsconst express = require('express') const request = require('request') const app = express() // この場合 localhost:8080/api/~~ とリクエストするとこのserverが受ける const rootPath = '/api' // 実際のエンドポイントを入力 const actualEndpoint = process.env.ENDPOINT || 'https://example.com/v1' const requestWrapper = async (options) => { const result = await new Promise((resolve, reject) => { request(options, (err, response) => { if (err) { reject(err) } if (!response) { reject(err, 'Nothing response') } resolve(response) }) }) return result } // 全部のリクエストを受けて外部APIへ飛ばす app.all('/*', async (req, res) => { console.log(`original url: ${req.originalUrl}`) const options = { url: actualEndpoint + req.originalUrl.replace(rootPath, ''), method: req.method, qs: req.query, json: req.body } console.log('request with following options.') console.log(options) const response = await requestWrapper(options) res.status(response.statusCode).send(response.body) }) // for Nuxt.js server middleware module.exports = { path: rootPath, handler: app }express とか依存関係がある場合は、
npm installも忘れずに!nuxt.config に serverMiddleware を追加する
serverMiddleware に先ほど追加したパスを指定します。
nuxt.config.tsmode: 'spa', serverMiddleware: ['~/server/'],これで、以下のように書くと、上のサーバーへリクエストし、レスポンスを受け取ることができます。
this.response = await this.$axios.$get('/api/hoge')こんなの2度と使うのか・・・わからないけど以上です!
取り扱いにはくれぐれも注意しましょう。
- 投稿日:2019-12-12T06:03:58+09:00
Next.js + FirebaseAuthでサインイン機能を実装してみた
本記事は、サムザップ #2 Advent Calendar 2019 の12/12の記事です。
株式会社サムザップの枦川です。
クライアントエンジニアをしていますはじめに
直近今関わっているPJの案件でSNSのサインイン機能を検討しているのですが、
Webの知識は乏しいので勉強をかねて簡易なサインイン機能を
Vue.js + Next.js + FirebaseAuth
の組み合わせで実装してみたいと思います今回は簡易版のためgoogleアカウントでのログインができるところまでをゴールとして
記事を書きますNuxt.jsとは
Zeit社が開発したユニバーサルなReactアプリの開発が可能なフレームワーク。
Webアプリ開発の機能が最初から組み込まれているVue.jsベースのJavaScriptフレームワークです。環境情報
Node v10.16.0
プロジェクトの準備
1.Nuxtのプロジェクトを作成してみましょう
以下のサイトを参考にインストールしてみてください
https://ja.nuxtjs.org/guide/installation/
を確認してみてください。とりあえすプロジェクトを作成してみましょう
npm i -g create-nuxt-app npx create-nuxt-app <project-name>いくつか質問されますが今回はそのままEnterで大丈夫です。
create-nuxt-app v2.12.0 ✨ Generating Nuxt.js project in firebase_auth_sample ? Project name firebase_auth_sample ? Project description My impeccable Nuxt.js project ? Author name HashikawaKazuhiro ? Choose the package manager Yarn ? Choose UI framework None ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose test framework None ? Choose rendering mode Universal (SSR) ? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection) ⠋ Installing packages with yarn ⠹ Installing packages with yarn ⠼ Installing packages with yarn2.作成したプロジェクトを起動してみましょう
cd <作成したprojectのディレクトリ> yarn devhttp://localhost:3000/ にアクセスしてみる
以下のように表示されればOKです。Firebaseと連携してみる
事前準備
1.Firebaseのアプリ登録を行う
1.「プロジェクトの設定」からプロジェクトにアプリを追加します。
今回はFirebaseAuthSampleというプロジェクト名にします
4.アプリ登録をすると以下のにように表示されるのでfirebaseConfigの内容をひかえておく
<script> // Your web app's Firebase configuration var firebaseConfig = { apiKey: "AIzaSyDE0mFS6zU-570bEu-r4UiGrCNGzDjwTK4", authDomain: "fir-authsample-ead7e.firebaseapp.com", databaseURL: "https://fir-authsample-ead7e.firebaseio.com", projectId: "fir-authsample-ead7e", storageBucket: "fir-authsample-ead7e.appspot.com", messagingSenderId: "982089738436", appId: "1:982089738436:web:870f3c6e92964d24a38191", measurementId: "G-YWWT4JPEFR" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); firebase.analytics(); </script>5.FirebaseAuth機能にて、gmailのみ連携を有効にしてみる
プロジェクトに導入する
1.Firebaseのプラグインを作成する
Firebaseの初期は一度のみ行えば良いのでpluginを作成して初期化を行う
dotenvとかでenvファイルに外だしで指定できますが今回はその辺は省略でplugins/firebase.jsimport firebase from 'firebase' //一度だけ初期化する if (!firebase.apps.length) { var firebaseConfig = { apiKey: "AIzaSyDE0mFS6zU-570bEu-r4UiGrCNGzDjwTK4", authDomain: "lfir-authsample-ead7e.firebaseapp.com", databaseURL: "https://fir-authsample-ead7e.firebaseio.com://lynomi-staging-711e3.firebaseio.com", projectId: "fir-authsample-ead7e", storageBucket: "fir-authsample-ead7e.appspot.com", messagingSenderId: "982089738436", appId: "1:982089738436:web:a68cf505064bc7a8a38191" } firebase.initializeApp(firebaseConfig) } export default firebase2.ルーターを作成する
https://ja.nuxtjs.org/guide/routing/3.ログイン画面を作成する
pages/index.vue<template> <div class="container"> <div class="row"> <div class="button--green"> <b-button block variant="primary" @click="login">Google Login</b-button></div> </div> </div> </template> <script> import firebase from 'firebase/app' import router from '../router' export default { name: 'login', methods: { login:function (){ var provider = new firebase.auth.GoogleAuthProvider() firebase.auth().signInWithRedirect(provider) .then(res => { this.$router.push('/logout') }) } } } </script>4.ログインする。以下のようにログイン画面が表示されます。
あとは、ログインできたらsignInWithRedirect.thenで結果を受け取って処理するだけですまとめ
FirebaseAuthを利用することで、Nuxt.jsで作成したwebサイトに簡単にログイン機能を実装することができました。FB/Twitterとも連携できるので今後そちらも試してみたいと思います。
明日は @Gaku_Ishii さんの記事です。
- 投稿日:2019-12-12T05:22:42+09:00
Firestoreで無策にMMO的なものを作ろうとしたら料金が凄い事になることに気づいた話
前置き
半分くらい仮説とか入ってるので話半分で
経緯
ふとしたことでNuxt.jsとFirestoreでチャットアプリを作ってみたところ
「Firestoreしゅごいー!これ使えばMMOも簡単に作れるんじゃ?!(安易)」
と思い、とりあえず座標だけを共有する簡単な試作品を作る事にした。そして複数人で動かしてみたところ数分で止まった(爆
試作品の概要
左下にある変な丸っこいのが、ジョイスティック的なやつ。
ログインしているプレイヤーは(FontAwesomeで適当にチョイスした)Twitterアイコンで表現。
ジョイスティックを動かして移動するだけのシンプルなもの。(もともとスマホでPWAを想定してNuxtを使ったという経緯もあってクリックとタッチどちらにも対応)
FPS60で座標の更新を行い、都度Firestoreへ反映。
スクショには写ってないけど下の方にチャット機能もある。ログイン周りや座標の共有はこのあたりを参考にしながら作った。
【v2対応】Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する
Cloud Firestore でリアルタイム アップデートを入手する
onSnapshotがとにかく便利。追加・変更・削除が行われるたび、Firestoreから通知が来るイメージ。
試作品の作りの事情とか入っていてあまり参考にならないソースだが雰囲気だけ。
・ログイン時にユーザ名・位置座標などをuserコレクションに追加
・アプリ側はログイン時にFirestore側で発行されるキーをそのままキーとして持たせたuserListでログインユーザを管理
・snapshot(onSnapshotのコールバックに渡ってくる変更内容のオブジェクト)のdocChangesメソッドで配列形式の変更内容がとれる// コレクションの変更を監視 this.listener.position = this.$firebase .firestore() .collection("user") .onSnapshot(snapshot => { // 変更無しなら何もしない if (snapshot.docChanges().length !== 1) return; snapshot.docChanges().forEach(change => { // vueは参照が変わらないと監視できないっぽいから配列・オブジェクトはcloneを作って後で代入してあげる let clone = { ...this.userList }; switch (change.type) { case "added": // 新規の場合(ログインなのでuserListに追加) clone[change.doc.id] = change.doc.data(); this.userList = clone; break; case "modified": // 変更の場合(移動なので対象の座標を更新) clone[change.doc.id].position.x = change.doc.data().position.x; clone[change.doc.id].position.y = change.doc.data().position.y; this.userList = clone; break; case "removed": // 削除の場合(ログアウトなのでuserListから除外) delete clone[change.doc.id]; this.userList = clone; break; default: break; } }); }, error => { console.error("Oh my God !!!", error); } );ログインキューの処理とかチャンネルごとのユーザの管理とかでFirestoreのドキュメント・コレクションに関するネタもあるのだけどそれは別の機会に書く。
あとジョイスティックの実装はvueの方のアドベントカレンダーに書く。止まったときの状況
自分1人で2ユーザで座標共有の検証は出来たので、10vs10くらいの対戦形式とか1チャンネル30人くらいでわちゃわちゃとかをこの仕組でやったらどうなのだろうと気になり数名に声をかける。
全然リッチな見た目でもないし(というか皆無)、座標共有くらいで処理落ちしたら話にならないからそのための簡単な確認が出来たらなぁという感じ。なので問題が起きても、FPSの調整とか座標計算のロジックがいけてなくて重いとかそんな話だろうなってこのときは思っていた。
(画面下に設置したチャットで)チャットしながら、5ユーザ(PC2ユーザ、スマホ3ユーザ)でジョイスティックを動かしていたら数分後に他のユーザが動かなくなった。
PCでコンソールを見るとエラーが…止まった原因
無料枠の1日あたりの制限オーバー。
画像はFirebaseのダッシュボードで
Database > 使用状況
を見たもの。
※ オーバーした当時のをスクショしてなったのでさっき撮った平凡なもの当日に確認した際は、この「読み取り」が上限の5万をオーバーしていた。
なお今回調べて知ったのだが、FirestoreというかGCP全般、「割り当て」から何がどれくらい使ってるかを確認する事が出来る。
Google Cloud Platformのダッシュボードから
App Engine > 割り当てFirebaseで確認した「読み取り」も
「Cloud Firestore 読み取りオペレーション数」という項目がそれだと思われる。onSnapshotが怪しい
そもそもなんで1日で5万超えたのか。
ダッシュボードで見る限りだと(1時間刻みなので正確には分からないけど)5人で始めてから止まるまでで急激に上昇し数分で4万ほど行ったように見えた。
厳密に検証したわけではないが、この「読み取り」はonSnapshotが絡んでいる気がしてならない。
ただダッシュボードを見た限りユーザ数が増えるとものすごい勢いで増加している。onSnapshotの動作を考えてみる。
・プレイヤー1人
自分の位置座標をFPS60、つまり60秒に1回更新している。(1秒で60回書き込み)
そしてonSnapshotで自分の変更分だけ1回受け取る。(1秒で60回読み込み)・プレイヤー2人
書き込みは2人分になっただけなので1秒で120回書き込み。
読み込みも2人分になっただけ…なんだけどそうじゃない…!
プレイヤーA、プレイヤーBとすると、
1FrameあたりプレイヤーAとプレイヤーBの2回の書き込みを、プレイヤーAとプレイヤーBがそれぞれ取得する。
1秒で240回の読み込み。つまり読み込みをonSnapshotの取得も含むとするなら、
(プレイヤー数)^2 × FPS
の回数発生する事になる。プレイヤー5人とすると1分で
10(人)^2 × 60(fps) × 60(秒) = 90,000上のはあくまでも1分間全員が常に動かしていたらの場合。
当時はちょろちょろ動かしていたし検証始めて数分で4万読み取りというのは、これが原因なんじゃないかと感覚値としては説明がついてしまう。これではFPSよりプレイヤー数の方が圧倒的なネックになる…
仮に試算
onSnapshotの取得がイコール「読み取り」だった場合
一応有料枠も考える。
Cloud Firestore の課金について東京リージョンだと10万読み取りで$0.038
約4円らしい(2019/12/12 現在)FPS60で10人がCPUのボス1人と10分戦うのを1クエストとして想定すると
10(人)^2 × 60(fps) × 60(秒) × 10(分) = 3,600,000(読み取り)
1クエスト約149円…上の仕組みで1000人のアクティブユーザ(100チーム)が5クエストすると約74,500円…
この仕組でリ○ージュ作ったら1日で破産する()
という事で
無策でMMOを作るのは石油王じゃないと無理。
格ゲーみたいなFPS60で1フレームの遅延が…みたいなもので無い限り、同期はFPSよりも遥かに長い(数秒に1回とか)スパンで行い、余計な通信をしないように節約した作りにしないといけないんだろうな…
そもそもNoSQLがセットでついてくるFirestoreじゃなく、別にソケットサーバを立てて、永続化が必要が無いものはより低コストにリアルタイム性を求めるとか。
某イカちゃんみたいにP2Pでサービス提供側のコストを抑えるとかとか。出来ればFirestoreで完結させたいので、このへんとか使いつつ現実的なものが出来たらまた続編書こうと思う。
オフラインでデータにアクセスする
- 投稿日:2019-12-12T02:10:35+09:00
チーム開発日誌1-20191212-
プログラミングの勉強をはじめて9ヶ月目に入るのだけど
幸運なことにシステム開発の案件をいただくことができた(本当奇跡チームでおこなうので記録に残していこうと思う
思いつくままツラツラと書いているので話が飛ぶこともある(許して開発環境どうする?
JavaScriptでいける内容だからFirebase使ってつくろうか
と最初サクッと決めて進めていっていた
Vue.jsが良さそうだねーとFirebaseでデータベース設計、バックアップ、セキュリティ、機能追加と考えたところ
これでいいのかなぁ...となって相談うまく言葉にはできなかったのだけど
何を選ぶにしてもデータベース設計はよく考えなければなのは置いといて
1からつくるときにFirebaseをデータベースに使うのに何か抵抗がでた
(語彙力。。。)チーム開発する&いずれ誰かが保守する+諸々考慮で
ある程度書き方にもルールがあって保守&機能追加しやすいものがよくないか
とも思って、LaravelでVue.jsを用いてMySQL使おうとなったただ、、開発コストと時間が不安点。。
つづく
- 投稿日:2019-12-12T00:39:29+09:00
サーバサイドエンジニアこそ習得したいVue.jsとUIkit
カジュアル文面でいきます。
タイトルは釣り気味です。はじめに
サーバサイドエンジニアにもいろいろあると思います。サーバサイドアプリケーション、データベース、機械学習、AWSなどなど。私はそこまで詳しくありません。
ひとつ言えることがあるとしたら、CSSからなるべく距離を置きたいと考えているサーバサイドエンジニアは少なくないのではないでしょうか。私もそのうちのひとりです。
そんな「Viewはあんま触りたくないな〜」という人に、Vue.jsとUIkitの組み合わせをお勧めします。
Vue.jsとUIkit
Vue.jsは言わずと知れたフロントエンドフレームワークです。
この辺の話はネットに余るほど存在していますので、ここでは割愛します。一方のUIkit、こちらはCSSフレームワークです。Bootstrapのようなやつです。
界隈では有名なのかもしれませんが、私は知ってから1年弱くらいです。フロントエンドでは
Cardと呼ばれるのでしょうか。
UIkitを使えば、簡単なCardならタダチニ実装可能です。<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.2.4/dist/css/uikit.min.css" /> <div class="container"> <div class="uk-card uk-card-default uk-card-body"> <h1 class="uk-text-lead">hello, world</h1> <p>Sample text here.</p> <button class="uk-button uk-button-default uk-button-primary">Click!!</button> </div> </div>CSSはひとつも必要ありません。
しかも命名がクールです。uk-の接頭辞から始まります。
名前が衝突することはほとんどないでしょう。デザインがWindows 8調な気はします。
余白の具合とかも調整可能です。
モダンなデザインだと思うので、私は好きです。なぜこの組み合わせなのか
UIkitの説明をしました。学習コストは低いのではないでしょうか。
一方のVue.jsですが、こちらもReactやAngularと比較すると学習コストが低いとよく耳にします。
それが爆発的に普及した一つの理由であると思うのですが。UIkitはVue.js向けにパッケージを提供しています。
yarnで追加可能です。yarn add uikitつまり何が言いたいかというと以下。
- 学習コストの低いVue.js
- 学習コストの低いUIkit
- UIkitはVue.js向けのパッケージを提供している
- CSSを1文字も書かず簡単にSPAできる
Vue.jsのプロジェクト立ち上げ手順などは割愛しますが、簡単なWebアプリケーションならすぐに作れますね。
<template> <div class="uk-container"> <div> <h1 class="uk-text-lead">Sample</h1> </div> <div class="uk-margin"> <div v-for="(item, index) in items" :key="index" class="uk-card uk-card-default uk-card-body uk-margin-small-top" > <p>{{ item.name }}</p> </div> </div> <div class="uk-margin"> <button @click="showModal" class="uk-button uk-button-default">Modal Open</button> </div> <div id="sample-modal" uk-modal esc-close="false" bg-close="false"> <div class="uk-modal-dialog uk-margin-auto-vertical uk-modal-body"> <p>This is on the modal.</p> <div class="uk-text-center"> <a @click="hideModal">Close</a> </div> </div> </div> </div> </template> <script> import axios from 'axios' import UIkit from 'uikit' import 'uikit/dist/css/uikit.css' export default { name: 'sampleApplication', data () { return { items: [] } }, methods: { showModal: function () { UIkit.modal('#sample-modal').show() }, hideModal: function () { UIkit.modal('$sample-modal').hide() } }, mounted () { axios .get('https://sample.com', { params: { category: 'hoge' } }) .then((response) => { this.items = response.data }) } } </script>CSSを1文字も書いていない!(本記事で最も伝えたいことです)
CSSを書かなくても、モーダルが実装可能です。
他にも横からニョキッと出てくるoffcanvasなどもCSSを1文字も書かずに実装可能です。
ちなみに、manifest.jsonやregisterServiceWorker.jsを用意すれば、PWAも可能です。サーバサイドエンジニアとしての力をフル活用する
Vue.jsとUIkitで実現したいこと、それはあくまで「自分のスキルを可視化すること」です。
サーバサイドエンジニア(広すぎるので良い単語が欲しい)には様々な腕の見せ所があると思います。API設計、URI設計、ドメインの蒸留度合い、機械学習、マイクロサービス、コンテナ、Kubernetes、高速レスポンスなどなど。自動化なんかもこれに入ってきそうですね。
Vue.jsとUIkitという武器を手にすることで、サーバサイドエンジニアとしての自分の強みを可視化することができます。
「なあ見てくれよ!こんな複雑な処理を30msecで返してくれるんだぜ!ほら、時間計測もしてるから、見てみなよ!」これでモテモテです。
終わりに
モテるためにVue.jsとUIkitを使いましょう。
























