20191212のvue.jsに関する記事は21件です。

✨ mo.js に恋して(あっ ? Vue の話だよ!) ✨

とある地方――、 Vue.js で開発をしているときのお話。
筆者はクリックイベントにアニメーションをつけたく、アニメーションフレームワークをネットで漁っていた。

「お! これいいじゃん」

mojs.gif

「 めっちゃかっこいい……。mo.js って言うのか」

おもむろにノート PC のキーボードを叩き始める筆者。
ディスプレイに映し出された検索サイトの入力欄には mo.js Vue とスムーズに打ち込まれ、タンッという打鍵音とともにページが再描画された。

「 Vue 用のパッケージは……、ほとんどないか」

◆ ◇ ◆ ◇ ◆

ということで、 mo.js を Vue で使いやすいようにプラグイン化して npm リリースした ので、そこで得た知見をまとめました。

? TL;DR

ターゲットとなる読者がわかりづらいのでまとめると、

  • インスタンスメソッドをもつプラグインの作成
  • Vue コンポーネントをもつプラグインの作成
  • Vue カスタムディレクティブをもつプラグインの作成
  • それら全部乗せのプラグインの作成
  • プラグインの npm パッケージ化

といった感じです。

npm パッケージ化は Vue CLI 3 で行っています。

? できあがったもの

vue-mo.js という npm パッケージをリリースしました。

✨ デモページ ✨ があるので良かったら見てみてください。

vue-mojs-demo.gif

ソースコードも GitHub で公開していますので、興味のある方はどうぞ。

:octocat: azukisiromochi/vue-mo.js | GItHub

:link: @azukisiromochi/vue-mo.js | npm

? プラグイン開発と npm パッケージ化まで

それでは本題、開発についてです。

類似記事は多いのですが、プラグインとしてインスタンスメソッド、コンポーネント、カスタムディレクティブをまとめてドン! みたいなのは少なかったので、なるべく体系的にまとめてみました。

⭐ インスタンスメソッドをもつプラグイン

どんなの?

Vue にグローバルレベルで機能を追加したものをプラグインといいます。

this.$vuemo.Star({
  parent: this.$refs.starParent
})
.play()

のように、ソースコード上で thisVue )から直接、開発したプラグインを参照することができるようになります。

プラグインの作成

プラグインと言ってもいろいろなものがありますが、今回は主に インスタンスメソッド・プロパティを追加 することで、プラグインとして用意したメソッドやプロパティが 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 というオブジェクトを追加していて、 $vuemoBurst という 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 を使っています)

参考

:link: プラグイン | Vue.js 公式

⭐ 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 の設定(エフェクトの種類など)を設定して利用します。

参考

:link: コンポーネントの登録 | Vue.js 公式

⭐ Vue カスタムディレクティブをもつプラグイン

どんなの?

Vue の標準セットである v-ifv-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 はディレクティブ引数で、このディレクティブではクリックするたびにイベント発火させるかを判断させるために利用しています。

参考

:link: カスタムディレクティブ | Vue.js 公式

⭐ それら全部乗せのプラグインの作成

これまで紹介した 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.png

のように dist ディレクトリが作成され、ビルド後の JavaScript 資産などが生成されているはずです。

npm に公開

これはたくさんの方が記事に書かれているので、省略しますが、こちらの記事がわかりやすいと思います。

:link: 初めてのnpm パッケージ公開 | Qiita

ちなみに、 npm publish を実行したときにエラーが返ることがあります。
エラーメッセージを取りそこねましたが、『すでに似たパッケージ名あるよ!』みたいな感じのものです。

npm では、 -_. などを区別せずにチェックしているようで、それを踏まえてパッケージ名を決めましょう。
どうしてもエラーになるパッケージ名を使いたい場合は、パッケージ名を @your-name/your-package のように npm アカウント名を付与する形で命名して、

$ npm publish --access=public

とコマンドを実行すれば公開することができます。

:link: [solving npm’s hard problem: naming packages | the npm blog]

? おわりに

Vue #2 Advent Calendar 2019 の13日目の記事でした。

もともと Qiita 記事を書くつもりでしたが、ちょうどアドベントカレンダーに空きがあったので差し込みました。
( 2 日前に思い立ったのでギリギリ ? )

Composition API で話題がもちきりななか基本的な内容になりましたが、どなたかの役に立つと嬉しいです :yum:

あと、 mo.js いい感じだからみんな使ってみてよ!( vue-mo.js もね!)

◆ ◇ ◆ ◇ ◆

とある地方――、 Vue.js で開発をしているときのお話。
筆者はクリックイベントにアニメーションをつけたく、アニメーションフレームワークをネットで漁っていた。

プラグイン作るのに夢中で、本来の目的がおざなりになっていることを筆者はまだ知らない――。

◆ ◇ ◆ ◇ ◆

12日目の記事 :arrow_right: @yaju さんの Handsontable for Vueを使ってみる

14日目の記事 :arrow_right: @kokky さんの VueとVue Routerで、リダイレクトのない理想の404を目指す

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

仮想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を仮想的に作って、

  1. 仮想DOMを二つ用意
  2. 一方の仮想DOMをJavascriptで操作(一般的にリアルDOMを操作するより速い)
  3. 変更前後の仮想DOMの差分を比較
  4. 差分だけをリアルDOMに反映
  5. 反映されたリアル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>

リンク先にもありますが、このコードを見ると

  1. stateというオブジェクトで現在のcountを管理しよう
  2. ボタンをクリックしたらインクリメント処理を行おう
  3. state.countをインクリメントしよう
  4. state.countを表示するために表示する要素(p#counter)を取得しよう
  5. 取得した要素の文字を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とロジックが分離されるという点も非常に強力で、生産性向上に寄与するものだと思います。

謝辞

今回の記事について、様々な記事にお世話になりました。
この場をお借りしてお礼申し上げます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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では、下のコードのように、reactivecomputedといった関数を用いて組み立てていく。見て分かる通り、今までとは全く違う、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.js
export default {
  mixins: [myGreatMixin] // mixinが何を提供しているかはファイルを見ないと分からない
}

Mixinの2つ目の問題は、「Mixinの中でしか用いないメソッドなどをprivateにすることができない」こと。
Vue.jsのスタイルガイドでは$_mixinName_methodNameのようにprefix付きで命名することを強要している。

vue2-component.js
const 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.js
export 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.ts
declare 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 · GitHub

Composition APIの登場

以上の諸問題によって、Vue.js は関数ベースのComposition APIを提供することを策定した。

Vue.js 2でもComposition APIを試せるようにプラグインを提供している。
https://github.com/vuejs/composition-api

Vue.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の書き方で貫き通してもよかったんじゃないかなぁと、少し思った。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue】不完全形態のWebページを表示させなくする方法(v-cloakディレクティブ)

はじめに

コンパイル完了後にページを表示することで、不完全なWebページをユーザーに見せないようにする方法です。

Vue.jsv-cloakディレクティブを活用していきます。

Vue.js 公式ドキュメント

環境

- 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.js
var app = new Vue({
  el: '#app',
  data: {
    message: "This is v-cloak directive."
  }
})

補足:v-cloakとは?

公式ドキュメントによると、

このディレクティブは関連付けられた Vue インスタンスのコンパイルが終了するまでの間残存します。

とのこと。

コンパイルが終了するまでの間残存する

コンパイルが終了すると消えてくれる

display: none;が消えて描画されるようになるということですね:point_up:

※今回のコードで、もし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.js
var app = new Vue({
  el: '#app',
  data: {
    message: "FADE IN..."
  }
})

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

これまで中途半端な状態を見せたくない要素にはCSSで時間を数秒遅らせて表示するようにしていました。

v-cloakを使ってコンパイル完了後というタイミングを指定出来れば、ユーザーの通信速度や端末のスペックに応じて表示されて便利ですね:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

API — Vue.js
基礎から学ぶ Vue.js | mio |本 | 通販 | Amazon

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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】導入と設定」と同じものです。
商品マスタ.png

CodePen がQiitaで埋め込みが出来るので、CodePen を使用します。

See the Pen Handsontable for vue by やじゅ (@yaju-the-encoder) on CodePen.

データの読み込み

今はHandsontable.vueのdataに初期テーブルデータをベタ書きしているが、実際にこのような使い方をすることはないので、データの読み込みをします。

ごめんなさい。あとで動作する部分を書きます。

最後に

未だに Vue.js を使いこなしていないので仕組みをまだ理解していません。
データの読み込みなど作成してから記事を書き直します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nuxt + typescript + vuex + axios に手を焼いたので共有

こんにちは
nuxt + typescript でフロントエンドを作っているエンジニアです。
store の getter や actions にも type を効かせるときに手を焼いたので共有します。

目次

ベース作成

nuxt × typescript の構築は他の記事に譲るとし、今回の説明で必要なファイルを列挙します。
なお、今回は shops を中心に store の説明をします。

まずは型定義

types/index.d.ts
export 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.ts
import { 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.vue
  mounted() {
      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-decorators

store を直していきます。
axios は後ほどやるので、ひとまずは仮データで進めます。

store/shops.ts
import {
  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.ts
import { 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.ts
import { 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-component

store を直していきます。

store/shops.ts
import {
  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.vue
shopsStore.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.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios';

let $axios: NuxtAxiosInstance;

export function initializeAxios(axiosInstance: NuxtAxiosInstance): void {
  $axios = axiosInstance;
}

export { $axios };
plugins/axios-accessor.ts
import { Plugin } from '@nuxt/types';
import { initializeAxios } from '~/utils/api';

export const accessor: Plugin = ({ $axios }): void => {
  initializeAxios($axios);
};

export default accessor;
nuxt.config.js
  plugins: [
    '@/plugins/axios-accessor',
  ]
store/shops.ts
import { $axios } from '~/utils/api';

(中略)

    const { data } = await $axios.get('/shops');

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueで開発したアプリケーションの構造

はじめに

コミュニケーションクラウドの新規開発からエンジニアとして参画している井川拓信です。

この記事は、「モチベーションクラウドシリーズ Advent Calendar 2019」の12日目の記事となります。

対象とする読者

  • Vue.jsでアプリケーションの新規開発をしようと思ってる人
  • Vue.jsで開発しているアプリケーションの構造で悩んでいる人

概要

CommunicationCloudは2019年2月から開発を始め、2019年8月に正式リリースしました。
2019年2月の新規開発から携わり、CommunicationCloudのフロントエンドの基底処理の設計/製造を行った私から、CommunicationCloudのフロントエンドの構造をご紹介します。

設計方針

  • Vue CLIで初期化したアプリケーションを拡張して開発する
  • 責務をモジュールごとに分離できるようにして、テストを容易にする
  • 共通的かつ横断的な機能を一元管理できるようにする

状態管理の設計

状態管理パターン.jpg

状態管理の設計は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のパッケージ構成-ルーティング (3).jpg

「認証チェック」、「認可チェック」、「ページのタイトルを変更」などのルーティングを横断する機能は、Vue Routerのナビゲーションガードを使用して実現しています。

「認証チェック」、「認可チェック」はrouter.beforeEach、「ページのタイトルを変更」はrouter.afterEachを使用しています。

「認証チェックは必要か」、「必要な権限は何か」、「ページのタイトル」は何かなどの値はルートメタフィールドとして、ルーティングに設定することができます。

ナビゲーションガードとルートメタフィールドを組み合わせて使用することで、ルーティングでフィルタを実現できます。ルーティングでフィルタを実装することで、viewsの各コンポーネントは画面の機能にのみ責務を持てば良いようになります。

Ajaxの抽象化

Vueのパッケージ構成-Ajax.jpg

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で入力検証して、検証結果を参照しやすくした
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 Framework

Herokuへのデプロイはこちらを参考にしました↓
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.vue
methods: {
    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つのことをしています。

  1. 「大石泉すき」にゼロ幅スペース(ZWSP)を挟む
  2. ツイート用URLくっつけてボタンに入れる
  3. コンソールに「大石泉すき」の文字数、バイト数を表示する

以上! 簡単ですね。

1.ゼロ幅スペースを挟む

ゼロ幅スペース(zero width space)って?

ゼロ幅スペース(ゼロはばスペース、英: zero width space, ZWSP)は、コンピュータの組版に用いられる非表示文字で、文書処理システムに対して語の切れ目を示すのに用いる。
Wikipedia - ゼロ幅スペース

?

要するに「表示されないけどちゃんと存在してる幅のないスペース」って事です。
ゼロ幅スペース(以下ZWSP)を挟むことで見た目は同じテキストでも重複なくツイートする事ができます。

Screenshot from 2019-12-12 15-06-29.png

画像の通り、見た目は同じでも文字数バイト数が違います。
これで連投しても怒られないぞ。やったね!

    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)
    },
  1. randomRange()で0から20までのランダムな数値を取得
  2. 数値をgenerateZWSP()に渡してその数だけ結合されたZWSPを返す
  3. insertZWSP()でテキストの頭から一文字とって作ったZWSPを結合していく

隙あらば再帰しています。好きなので。

generateZWSP()内の'\u200B'が
UnicodeエスケープシークエンスでのZWSPです

Unicodeエスケープシークエンス↓
#Unicode_escape_sequences 字句文法 - JavaScript | MDN

2.ツイート用URLくっつけてボタンに入れる

ツイート用URLは色々できるWeb Intentsで。
Web Intents - Twitter Developers

OhishiIzumiSuki.vue
    <div class="tweet-button-wrapper">
      <a class="tweet-button" @click="setUrl(msg)" :href="url" target="_blank">{{ msg }}</a>
    </div>
OhishiIzumiSuki.vue
    insertZWSP(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)です`)
    },
  1. insertZWSP()でテキストにZWSPを挟みこむ
  2. ツイート用URLを作ってthis.urlに代入

setURL()が呼ばれる度にaタグの:hrefが更新され、ツイート内容が変わります。

文字数、バイト数を表示

レギュレーションを遵守するべく「大石泉すき」と出力していきます。
大事な情報も一緒に表示してしまいましょう。

OhishiIzumiSuki.vue
    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)です`)
    },
  1. 変数展開してコンソールに出力
  2. バイト数も知りたいのでcountBytes()で数える

Screenshot from 2019-12-12 17-04-34.png

完成!

終わりに

当然ながらこのページからのツイートは「大石泉すき」サーチに引っかかりません。
担当サーチの邪魔にもならないので安心して大石泉すきツイートしていきましょう。

Screenshot from 2019-12-12 17-08-46.png

あんまり連投するとこわい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
const 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.js
var 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

さいごに

読んでいただきありがとうございます!
辛いものが好きな方、辛いものが好きアドベントカレンダーもお願いいたします!
辛いものが好きアドベントカレンダー

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

親から子へ、子から親へデータを渡す方法

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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>
javascript
new Vue({
  el: "#app",
  data: {
    message: "hello world!"
  }
})

【出力結果】
hello world!

ダウンロード (10).png

解説

1:CDNでVue.jsを読み込む
公式サイトのインストールの項目に載っているのでコピペ

2:idをつける

3:new Vueでインスタンスを宣言

4:el: "#app"で指定

5:dataの中にプロパティを書く
今回はmessage: "hello world!"

6:二重中括弧で表示したいものを囲う
{{ message }}



無事hello world!が表示されました!
感動。。これから新たな旅がハジマル〜!!



ではまた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の設定がありますが、
ここの設定をいじっても ちゃんと表示できませんでした、、、なぜ?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

コード.gs
function 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>

参考

Vue.js > 算出プロパティとウォッチャ > ウォッチャ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsはじめました(ビギナー向けまとめ)

食べログフロントエンドチームの@nakiaです。

元請けのSIerで3年半ほどシステム屋さんをやってましたが、もっと自分でコード書けるようになりたい!と一念発起しまして、ちょうど1年前に食べログにやってきました:tada:
優秀なメンバーに囲まれ、学びの多い毎日を送っています:hatching_chick:

今日のAdventCalendarは、フロントエンド初心者の私が0からVue.jsを勉強した学習記録です!
公式ガイドの他に「基礎から学ぶVue.js」(書籍)を参考にしています。

初心者向けなので、より実践的な内容をお求めの方はぜひ@empitsu88 さんの「Nuxt.js+TypeScriptのアプリケーションのためのコーディングガイドライン」をご覧ください:information_desk_person:

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初投稿でした…!
質問・コメントはお手柔らかにお願いします:pray:

明日は@k-sekidoさんの「システム開発で納得感を持って進めるために考えていること」です!
お楽しみに〜:christmas_tree:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js の inheritAttrs に関する大きな勘違い

はじめに

Vue コンポーネントには inheritAttrs というディレクティブがあります。

直訳すると 「属性を引き継ぐ」

まず簡単な例を示します。Vuetify.jsv-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 にはあまり頼らないほうがいいのかもしれませんね…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
import { 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の実行を推奨していないため、実際の登録はプロダクションで行う必要もある。

Image

serviceWorkerがちゃんと登録されているかどうかは開発者ツールのApplicationタブをみて、Sourceにファイルが登録されていればいい。

Push通知許可の実装

よくwebで見かけるPush通知を許可するかどうかのボタンを実装する。これが許可されるとブラウザごとに設定されているPushサーバーからエンドポイントとなるURLとキーが発行される。サーバー側はPush通知を送りたい際にそのエンドポイントに向けてキーと一緒にPush通知として表示する内容を送り付けることができる。

Image

https://developer.mozilla.org/ja/docs/Web/API/PushManager/getSubscription

PushManager.getSubscription().then(subscription => {})subscriptionnullが入っていた場合、そのユーザーはPush通知を許可してなく、Push通知を許可していた場合は、subscriptionに上で述べた情報が格納される。これらの情報はこのPush通知を許可したユーザーに対して、Push通知を送るためにアプリケーションサーバーで保管しておく必要がある。

registerServiceWorker.js
 swRegistration.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でセットしておく。
  • 次にクライアントから受け取ったauthp256dhの鍵、endpointのURLをセットすればPush通知を実行できる。
server.js
const 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が受け取ったメンションのスクショ

Image

(開発中のシステム名とアイコンが映ったので修正を入れてる)

参考

ウェブアプリへのプッシュ通知の追加 | Web | Google Developers
Push通知に関するクライアント側の設定や書き方を大いに参考にした。

@vue/cli-plugin-pwa
vue-cliでPWA環境を整えるために必要なモジュール

web-push
Node.js環境でPush通知を送るためのNPMパッケージ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

明日使えるかもしれない Nuxt 上で唱えるプチ闇魔法3連発

この記事は Sansan Advent Calendar 2019 の12日目の記事です。

今年に入ってから Nuxt.js を使い始めましたがとっても便利で、今まで React 派でしたがこれがあるから Vue っていいなと思いました。
みなさんも Nuxt 使っているでしょうか。そして、モダンにかっこよく使えているでしょうか。

僕はあんまりモダンじゃ無い、泥臭くやることを強いられた部分があったので、もしも同じことで悩む人のためにいくつか紹介したいと思います。

コンテンツは

の3点です。

前提

以下で動作確認

また、今回のコード全部入りはGitHubで公開しています

1. Component 内で外部 jQuery を利用する

これは比較的普通に使えるネタかもしれません。
Component 内で jQuery を import して、$で使用します。

型定義を準備

  1. package をインストール
npm i -D @types/jquery
  1. tsconfig.jsontypesを以下のように追記します。
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.js
import 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.ts
plugins: ['~/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 へリクエストするサーバーを書く

  1. リクエストを受けて
  2. パスやパラメータを解析して
  3. そのパスとパラメータそのまま外部 API にリクエストする

みたいなサーバーを node.js で書きます。これは正直どんな実装でも良いのですがサンプルを載せておきます。

server/index.js
const 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.ts
  mode: 'spa',
  serverMiddleware: ['~/server/'],

これで、以下のように書くと、上のサーバーへリクエストし、レスポンスを受け取ることができます。

this.response = await this.$axios.$get('/api/hoge')

こんなの2度と使うのか・・・わからないけど以上です!
取り扱いにはくれぐれも注意しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 yarn

2.作成したプロジェクトを起動してみましょう

cd <作成したprojectのディレクトリ>
yarn dev

http://localhost:3000/ にアクセスしてみる
以下のように表示されればOKです。

スクリーンショット 2019-12-12 3.02.48.png

Firebaseと連携してみる

事前準備

1.Firebaseのアプリ登録を行う
1.「プロジェクトの設定」からプロジェクトにアプリを追加します。
スクリーンショット 2019-12-12 3.07.54.png

今回はFirebaseAuthSampleというプロジェクト名にします

2.プラットフォームはwebを選択する
スクリーンショット 2019-12-12 3.11.39.png

3.画面に従って登録する
スクリーンショット 2019-12-12 3.13.02.png

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のみ連携を有効にしてみる
スクリーンショット 2019-12-12 3.27.24.png

6.有効にするをON、サポートメールを選択して、保存する
スクリーンショット 2019-12-12 3.28.16.png

7.有効になってればOK
スクリーンショット 2019-12-12 3.31.03.png

プロジェクトに導入する

1.Firebaseのプラグインを作成する
Firebaseの初期は一度のみ行えば良いのでpluginを作成して初期化を行う
dotenvとかでenvファイルに外だしで指定できますが今回はその辺は省略で

スクリーンショット 2019-12-12 3.49.34.png

plugins/firebase.js
import 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 firebase

2.ルーターを作成する
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で結果を受け取って処理するだけです

スクリーンショット 2019-12-12 5.25.02.png

まとめ

FirebaseAuthを利用することで、Nuxt.jsで作成したwebサイトに簡単にログイン機能を実装することができました。FB/Twitterとも連携できるので今後そちらも試してみたいと思います。

明日は @Gaku_Ishii さんの記事です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firestoreで無策にMMO的なものを作ろうとしたら料金が凄い事になることに気づいた話

前置き

半分くらい仮説とか入ってるので話半分で

経緯

ふとしたことでNuxt.jsとFirestoreでチャットアプリを作ってみたところ
「Firestoreしゅごいー!これ使えばMMOも簡単に作れるんじゃ?!(安易)」
と思い、とりあえず座標だけを共有する簡単な試作品を作る事にした。

そして複数人で動かしてみたところ数分で止まった(爆

試作品の概要

試作.png

左下にある変な丸っこいのが、ジョイスティック的なやつ。
ログインしているプレイヤーは(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日あたりの制限オーバー。
使用状況.png
画像はFirebaseのダッシュボードで
Database > 使用状況
を見たもの。
※ オーバーした当時のをスクショしてなったのでさっき撮った平凡なもの

当日に確認した際は、この「読み取り」が上限の5万をオーバーしていた。

なお今回調べて知ったのだが、FirestoreというかGCP全般、「割り当て」から何がどれくらい使ってるかを確認する事が出来る。
Google Cloud Platformのダッシュボードから
App Engine > 割り当て

Firebaseで確認した「読み取り」も
割当.png
「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で完結させたいので、このへんとか使いつつ現実的なものが出来たらまた続編書こうと思う。
オフラインでデータにアクセスする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

チーム開発日誌1-20191212-

プログラミングの勉強をはじめて9ヶ月目に入るのだけど
幸運なことにシステム開発の案件をいただくことができた(本当奇跡

チームでおこなうので記録に残していこうと思う
思いつくままツラツラと書いているので話が飛ぶこともある(許して

開発環境どうする?

JavaScriptでいける内容だからFirebase使ってつくろうか
と最初サクッと決めて進めていっていた
Vue.jsが良さそうだねーと

Firebaseでデータベース設計、バックアップ、セキュリティ、機能追加と考えたところ
これでいいのかなぁ...となって相談

うまく言葉にはできなかったのだけど
何を選ぶにしてもデータベース設計はよく考えなければなのは置いといて
1からつくるときにFirebaseをデータベースに使うのに何か抵抗がでた
(語彙力。。。)

チーム開発する&いずれ誰かが保守する+諸々考慮で
ある程度書き方にもルールがあって保守&機能追加しやすいものがよくないか
とも思って、LaravelでVue.jsを用いてMySQL使おうとなった

ただ、、開発コストと時間が不安点。。

つづく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サーバサイドエンジニアこそ習得したい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.jsonregisterServiceWorker.js を用意すれば、PWAも可能です。

サーバサイドエンジニアとしての力をフル活用する

Vue.jsとUIkitで実現したいこと、それはあくまで「自分のスキルを可視化すること」です。

サーバサイドエンジニア(広すぎるので良い単語が欲しい)には様々な腕の見せ所があると思います。API設計、URI設計、ドメインの蒸留度合い、機械学習、マイクロサービス、コンテナ、Kubernetes、高速レスポンスなどなど。自動化なんかもこれに入ってきそうですね。

Vue.jsとUIkitという武器を手にすることで、サーバサイドエンジニアとしての自分の強みを可視化することができます。
「なあ見てくれよ!こんな複雑な処理を30msecで返してくれるんだぜ!ほら、時間計測もしてるから、見てみなよ!」

これでモテモテです。

終わりに

モテるためにVue.jsとUIkitを使いましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む