20200906のvue.jsに関する記事は16件です。

Mutationsにcommitする2パターン

1つ目はVueファイルからコミットする方法。

index.vue
<script>
export default {
  fetch ({ store }) {
    store.commit('index/setData',1)
  }
}
</script>

2つ目はVuexのActionsからコミットする方法。

index.js
export const actions = {
  async fetchData({ commit, state }) {
    const resp = await this.$axios.$get('URI')
    commit('setData',resp)
  } catch (error) {
    console.log(error)
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1つ目のAPIが通った場合のみ次のAPIを実行するエラーハンドリング

1つ目のAPIを実行してうまく通った場合にのみ、
次のAPIを実行するような処理を実装する一例。

※下記のサンプルではVuex、Axiosで実装しています

index.js
async getData({ commit, state }) {
  try {
    await this.$axios.
      $get('URI')
      .then(() => {
        this.$axios
          .$get('URI')
          .then((resp) => {
            commit('setData', resp)
          })
          .catch((error) => {
            console.log(error)
          })
      } catch((error) => {
        console.log(error)
      })
    } catch (error) {
      console.log(error) [
    }
  }
}

Actiosの中での実装を想定しているので、
commitでMutationsにある(想定)setDataにデータを渡しています。

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

Firebase+Vueで作ったポートフォリオサイトの画像をWebP対応して軽くしたので解説とソース

絵描き兼フロントエンジニアのゆきです。今日はしばらく前に作ったポートフォリオ兼ギャラリーサイトの画像を新しい画像形式であるWebPに変えてさらに軽くしたお話です。

AutoDetectWepSupport.png
※ Safariは2020/9時点。次版で対応すれば自動的にWebP表示に切り替わる...はず

コードは全て公開しているので、流用・参考にしてください。このサイトはFirebase+Vueですが、他のスタックでも似たようなことはできると思います。

サイト: https://pf.nekobooks.com/
リポジトリ: https://github.com/yuneco/portfolio/

WebPはJPG/PNGに変わる新しい画像圧縮フォーマット

WebPはざっくり言うと、JPGとPNGの:hatched_chick:いいとこどり:hatched_chick:ができる:sparkles:夢のフォーマット:sparkles:です。

  • JPG並みかそれ以上に圧縮率が高い(同じ画質で27%軽くなるらしい)
  • PNGと同様、半透明の透過ができる
  • GIFやAPNGのようなアニメーションもできる

今回は扱わないけど、圧縮画像で透過付きのアニメーションを扱えるのとか、すごく夢がありますね。

WebP対応のアプローチ

まあなんとなく想像つく話だと思うのですが、夢のフォーマットなのにみんながそこまで使っていないのは、面倒なことがあるからですね:innocent:うん、知ってたよ...

WebPの場合、残念ながらSafariがまだ対応していません(2020/9/5時点)。
したがって現時点ではクライアント環境に合わせてWebPとJPEG/PNGのどちらかを出し分ける対応が必要です。

ただし今秋リリースのiOS14では対応すると言われているので、もうしばらくすれば何も考えずに使えるようになるかもしれません。1

どっちにしろ新しいフォーマットは今後も出続けるので、新しいものについていくのであればブラウザごとの「出し分け」機能は必須です。今回のWebP対応では次の2つの手法を組み合わせてこの「出しわけ対応」を行います:

  1. 静的な(ビルドに含める)画像はビルド時にJPEG/PNG版とWebP版を作成。どちらを使うかは実行時にVueで判定する
  2. 動的な(FirebaseのStorageに格納している)画像は実行時にリクエストに応じてFirebase functionsでWebPに変換する

今回は既存サイトの改修なので2つの手法を組み合わせていますが、新規で作るなら全て2の方法にまとめてしまうのも良いかもしれません。
逆に動的な要素が少なかったり、サーバサイドが使えない環境であれば1に寄せるのが良いかと思います。

Vueのビルド時にWebP画像を自動生成する

まずは静的なリソースに含まれる画像をVueのビルド時にWebPに変換します。

WebP変換ライブラリをインストールする

画像フォーマットの変更にはimageminという画像の変換や圧縮をおこなってくれるライブラリを使います。
Vue.js(VueCLI)のビルドはwebpackを使用しているので、今回はimageminをwebpackで簡単に使えるようにしてくれる派生版をインストールします。

yarn add -D imagemin-webpack-plugin

これだけだとWwebPには対応しないので、WebP変換をおこなってくれるimagemin-webp-webpack-pluginを入れます。ついでにJPEGをいい感じに小さくしてくれるimagemin-mozjpegも入れておきましょう。

yarn add -D imagemin-webp-webpack-plugin imagemin-mozjpeg 

他にも画像フォーマットごとに様々なライブラリがあるので必要に応じて追加してください。

Vue(webpack)ビルド時にWeb変換を行う

ビルドの設定を追加するために、(もし未作成なら)vue.config.jsをプロジェクトのルートに作成します。

vue.config.jsはVue(VueCLI)のビルド設定をカスタマイズするためのファイルで、下のような形でwebpackの設定を追加することができます:

vue.config.js
module.exports = {
  chainWebpack (config) {
    // ここにwebpackの設定を追加
  }
}

今回はここで画像のWebP変換と、JPEG/PNGの圧縮を行います。

vue.config.js
module.exports = {
  chainWebpack (config) {
    config
      .plugin('ImageminWebp')
      .use(ImageminWebp, [{ // ImageminWebpプラグインの設定
        test: /\.(jpg|png)$/i,  // 拡張子がjpg, pngにマッチするものを変換
        option: {
          quality: 85
        }
      }])

    config
      .plugin('ImageminPlugin')
      .use(ImageminPlugin, [{
        test: /\.(jpg|png)$/i,
        disable: process.env.NODE_ENV !== 'production', // JPEG/PNGの再圧縮はプロダクションビルド時のみ
        pngquant: { // PNG減色の設定(下げすぎるとGIFみたいになるので注意)
          quality: '95'
        },
        plugins: [
          ImageminMozJpeg({ // JPEG圧縮の設定
            quality: 75,
            progressive: true
          })
        ]
      }])
  }
}

webpackの設定については説明しませんが、雰囲気でコピペしてもらえれば動くと思います:information_desk_person:。この状態でyarn buildを実行すると、ビルド結果(distフォルダ)にPNG/JPEGと並んでWebPファイルが生成されます。

ブラウザがWebP対応か判断して適切な画像を読み込む

2種類の画像ができたので、あとはアプリ側からどちらか必要な方だけを読み込む処理を追加します。

複数の画像形式を出し分ける方法を検索すると、Picture要素を使う方法が出てくると思います。これでも良いのですが、Picture要素だと面倒な上にCSSの背景画像に対応できないのでVueを使って出し分けを行います。

ブラウザがWebPに対応しているかどうかはsupports-webpのようなライブラリを利用して簡単にチェックできます。ただ、このライブラリの検出方法は正攻法ではあるのですが、検出結果が非同期にしかとれなくてこれまたちょっとめんどくさいです。ということで今回はHow to Detect Browser Support WebPで紹介されていたCanvasを使った検出を利用します。2

isWebpSupported.js
const canUseWebP = () => {
  const elem = document.createElement('canvas')
  if (elem.getContext && elem.getContext('2d')) {
    // CanvasからWebPを出力して、結果がdata:image/webpで始まっているかチェック
    return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0
  }
  // Canvas自体が使えなければ非対応扱いにする
  return false
}

// 最初に一度だけ判定して結果をexport
export default canUseWebP()

あとはVueコンポーネントで画像のパスを指定している部分の拡張子を、WebP対応かどうかで変えてあげればOKです。必要に応じて単体のコンポーネントにするなり、いい感じに抽象化してください:wink:

webP対応かどうかで画像パスの拡張子を変えているコンポーネント.vue
<template>
  <article-item
    class="about"
    title="Welcome to Nekobooks"
    :image="`/img/profile.${format}`"
  />
</template>
<script>
import isWebpSupported from '@/core/isWebpSupported'
export default {
  computed: {
    format () {
      return isWebpSupported ? 'webp' : 'jpg'
    }
  }
}
</script>

Firebase functionsで動的にWebP変換する

これでビルドに含まれる静的な画像はWebPに対応しました。
続けて、Firebase Storageに入っている画像ファイルをリクエストに応じてWebPに変換する処理を作ります。

Storageの画像も事前に一括して変換してしまうのもありなのですが、ストレージ使用量が増えることと、将来的にフォーマットや画質を変更したくなった時の対応が:anger:超絶めんどくさい:anger:ので、Storageには高画質の原本だけ持ち、オンデマンドで必要な形式・画質に変換して返す方がオススメです。3

Firebase Hostingの設定でfunctionを呼び出す

まずは所定のURLを叩いた時にこれから作るfunctionを呼び出すよう、hostingの設定を行います。

firebase.jsonにrewriteの設定を追加します。sourcefunctionは他と被らなければ好きなURL・名前を設定してOKです。

firebase.json
{
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "/api/gallery/images/*/*",
        "function": "galleryImageWebp"
      }
    ]
  }
}

これでブラウザから/api/gallery/images/piyopiyo/myaomyaoのようなURLを叩くとgalleryImageWebpという名前のfunctionが呼ばれるようになりました。

functionでStorageの画像をWebPに変換する

functions側での画像形式の変換にはsharpを使います。
functionsディレクトリでsharpをインストールします。

cd functions
yarn add sharp

続けて、sharpを使って先ほど設定だけしたgalleryImageWebpを実装します。ちょっと長いので全体はGitHubのgalleryImageWebp.jsを参照してください:kissing:

まず重要なのは↓ここ

/functions/src/galleryImageWebp.js
/**
 * 画像ファイルを開いて、画像の圧縮・変換を行って返す
 * @param {string} imagePath 
 * @param {string} format webpを指定するとwebpに変換。jpg/jpegを指定すると圧縮。未指定・その他の場合は原本をそのまま返します
 * @return {Buffer} webp img buffer
 */
const loadImageBuffer = async (imagePath, format) => {
  const buffer = sharp(imagePath);
  if (format === 'jpeg' || format === 'jpg') {
    buffer.jpeg({
      quality: 80
    })    
  }
  if (format === 'webp') {
    buffer.webp({
      quality: 80
    });
  }
  return await buffer.toBuffer();
}

Storageから持ってきた画像ファイルを開いて、ついでにWebPへの画像変換を行います。buffer.webp()だけで変換できるので超絶簡単です。:angel_tone3::angel_tone3:

もう一つの大切なポイントがキャッシュの設定です。
リクエストされるたびに毎回この処理を実行していると時間もかかるし何より課金がやばいです。:moneybag:クラウド破産:moneybag:しないためにも必ずキャッシュを設定しましょう。4

/functions/src/galleryImageWebp.js
const galleryImageWebp = async (req, res) => {
  // ... 略 ...

  // 形式を指定して画像データをロード
  const buffer = await loadImageBuffer(tmpImg, format)

  // レスポンスタイプを設定
  const contentType = `image/${format}`;
  res.set('Content-Type', contentType);

  // レスポンスのキャッシュを設定
  const age = 86400 * 30; // 30日
  res.set('Cache-Control', `public, max-age=${age}, s-maxage=${age}`);

頻繁に変わるコンテンツ(URLは変わらず内容だけ変わるもの)でなければキャッシュの有効期間は適当に長めの数字を設定してOKです。5

最後に実装したgalleryImageWebpfunctionの呼び出しを設定します。

/functions/index.js
const galleryImageWebp = require('./src/galleryImageWebp')

exports.galleryImageWebp = firebase.functions
  .runWith({timeoutSeconds: 20, memory: '512MB'})
  .https
  .onRequest((req, res) => {
    galleryImageWebp(req, res)
  })

メモリやタイムアウトはよしなに設定してあげてください。今回は念の為割り当てを512MBに上げています。

これでブラウザーからhttps://pf.nekobooks.com/api/gallery/images/full/1558153452954_467.webpを呼ぶとWebPが、
https://pf.nekobooks.com/api/gallery/images/full/1558153452954_467.jpegを呼ぶとJPEGが返却されるようになります。

どちらを呼び出すかの判定は先ほどと同様isWebpSupportedを使ってVueで処理します。

もう一歩:対応画像を自動判定する

VueとFirebaseを使う場合は、基本的にはこれで十分かと思います。将来的に別のフォーマットが出てきてもおそらく簡単に対応できるはず:relaxed:

ただクライアント側の環境によっては今回使ったJS(isWebpSupported関数)での判定は使いたくないケースもあるかもしれません。最後におまけとして、WebP対応の有無自体をfunctions側で判定して、クライアント側では一切画像形式のことは考えなくてよい仕組みを紹介します。

functionsでWebPサポートを判定する

リクエストを送ってきたブラウザがWebPをサポートしているかどうかはAcceptヘッダーを見ると判定できます。

▼ Chrome85が送るAcceptヘッダーの例
image.png

WebP以外にもapng(アニメーションPNG)やavif(Chrome85から追加された新しい画像フォーマット)も受け入れできると書かれています。

このように、最近のブラウザーでは「他では対応できないかもしれないけど自分ならイケるよ!」という新し目のフォーマットを明示的にAcceptヘッダーに列挙してくれています。まじえらい

実際のコードは↓こんな感じ。拡張子がない場合のみAcceptヘッダーで自動判定しています。

/functions/src/galleryImageWebp.js
 const galleryImageWebp = async (req, res) => {
    // ... 略 ...

  let format
  if (!ext) {
    // 拡張子がない場合、WebvPサポートブラウザならWebPを使う
    const acceptHeader = req.get("Accept") || ""
    const isSupportWebp = acceptHeader.includes("image/webp")
    format = isSupportWebp ? "webp" : "jpeg"
  } else {
    // 拡張子が付いていればそれを利用
    format = ext.substr(1)
  }

最後に、自動判定した場合のキャッシュを設定します。
自動判定するということは、同じURLでもレスポンスがJPEGになったりWebPになったりするので、単純に「1URL=1キャッシュ」みたいなキャッシュ保存ができません。

この問題を回避するためにVaryレスポンスヘッダーを追加して、「このリクエストはAcceptヘッダーの内容によって変わる可能性があるよ!」と伝えます。あとはキャッシュを行うCDNがVaryの指定を見てよしなにキャッシュを行ってくれます。

/functions/src/galleryImageWebp.js
  if (!ext) {
    // 自動判定を行った場合はVaryレスポンスヘッダーを追加する
    res.set('Vary', 'Accept-Encoding, Accept')
  }

これでブラウザーからhttps://pf.nekobooks.com/api/gallery/images/full/1558153452954_467のようにを拡張子なしでリクエストすると、WebP対応ブラウザならWebPが、非対応ブラウザならJPEGが返るようになります。

自動判定はすべき?

返却する画像の形式を自動判定することで、クライアントアプリは一切画像のフォーマットを意識しなくてよくなります。

JSを使えない静的なhtml/cssでもWebPが使えますし、AVIFのような新しい形式の対応を追加してもアプリには一切手を入れなくて済みます。:angel:理想郷:angel:ですね。

その一方で、Acceptヘッダーごとにキャッシュを作るということは、Acceptヘッダーのバリエーションごとにいくつもキャッシュが生成されることを意味します。Acceptヘッダーはブラウザごとに異なる上、同じブラウザでもバージョンが変わると変化することがあるので、「今回のようにWebP対応かどうか?」の2択しかないケースでは(必要なバリエーションは2つだけなのにいくつもキャッシュが作られるので)かなり割の悪いキャッシュになる可能性があります。

特にFirebaseのようにCDN側の状態がブラックボックスな環境では注意が必要です。(このあたりはあまり知らないので、詳しい方がいらっしゃったらコメントください)
作るサイトの特性や環境に合わせて選択するのが良いと思います:cat::star:

まとめ:

:star2: 新しい画像形式への変換はwebpackでビルドにもできるし、Firebase functionsでオンデマンドにもできるよ
:star2: 画像形式をfunctionsで判定すればクライアントアプリ側は画像形式のことを考えなくてよくなるよ(キャッシュには注意!)
:star2: 新しい画像形式をサポートしてWebサイトを軽くしよう


  1. 現状Mac/Win共にOS標準ではWebPをサポートしないので、表示するためにはChrome等のWebP対応ブラウザが必要です。Macの場合はWebP用のQuickLookプラグインを入れておくと幸せになります。このプラグインはMacOS10.15.2以降ではGateKeeperにブロックされて動作しないことがあるので、その場合一度プラグインそのものをコンテキストメニューから「開く」して検疫フラグを落とす必要があります(2020.7時点) 

  2. 正攻法ではWebP画像のBase64文字列から実際に<img>要素を作ってロードできるかどうかをテストしています。今回使った邪道な方はCanvasからのWebP出力なので、もし「WebPの読み込みはできるけど出力はできない」ブラウザがあると判定を誤ることになります。不安なら正攻法で行きましょう 

  3. 実は現状もサムネイル画像の作成は画像の登録時にやっているのですが、後からサムネイルのサイズや画質を変えられないので結構運用が辛いです。ただし、オンデマンドで処理するものが増えるほど障害点も増えることになるので、その辺りはサービスのクリティカル度合いや自分のめんどくささと相談してください 

  4. 今回のサイトの内容は全てオープン(誰でも見られる)なので問題ありませんが、プライベートなコンテンツを含む場合、下手にキャッシュするとセキュリティ事故になるのでちゃんと調べてから設定してくださいね 

  5. Firebaseの場合、キャッシュされたデータはHostingで再デプロイするとクリアされるようです。 

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

パワポエンジニアでも作れる、Vue.jsとFirebase/Firestoreを使った家族用ToDoリスト

家族用ToDoリスト


Vue.jsの学習のために作成したので、その過程をまとめます。

学習用の成果物としてToDoリストは定番ですが、せっかくだから実生活で活用したいと思い、
家族、すなわち事前に登録した特定のユーザのみで閉鎖的に利用できるToDoリストを作りました。
(自分だけのToDo管理なら普通にKeepメモとか使いますしね)

まずは「1.ToDoリストの作成」でFirestoreを利用したToDoリストを作成し、
その後「2.認証機能の作成」で家族用に仕上げます。

※VuetifyとVue routerが導入済みであることを前提とします。
<参考> Vuetifyの使い方:Vueプロジェクトへのインストール手順
<参考> インストール | Vue Router

1.ToDoリストの作成

ありがたいことに、世の中ではいくらでもToDoリストの作り方が紹介されており
私のようなCOBOLしか読めない人間でも簡単にToDoリストを作ることができます。

ToDoの新規作成、読み取り、更新、削除機能を順に作っていきます。

1.1 ToDoの新規作成

まず最初に入力フォームやボタンなどの外見を作成し、次にFirestoreにToDoを保存する機能を作成します。

1.1-1) ToDo入力フォーム

ToDo名称を入力するためのフォームです。

ToDo入力フォーム抜粋
<v-text-field
 color="grey darken-1"
 label="ToDoを入力してください"
 v-model="newTodoName">
</v-text-field>

入力した値をあとでToDoタイトルとして使いたいので、変数newTodoNameにバインドしています。

1.1-2) 期限入力フォーム

期限がないと一生やらないので、各ToDoに期限を設定できるようにします。

VuetifyのDatePickerで実装します。(正直コピペです…)
このパーツだけで記述量がそこそこになってしまうのと、今後何かで再利用することもあろうと考えてコンポーネント化しています。

datePicker.vue
<template>
  <v-row>
    <v-col cols="5" sm="6" md="4">
      <v-menu
        v-model="menu"
        :close-on-content-click="false"
        :nudge-right="40"
        transition="scale-transition"
        offset-y
        min-width="290px"
      >
        <template v-slot:activator="{ on, attrs }">
          <v-text-field
            v-model="date"
            label="todo期限"
            prepend-inner-icon="event"
            readonly
            v-bind="attrs"
            v-on="on"
          ></v-text-field>
        </template>
        <v-date-picker
         v-model="date"
         @input="menu = false"
         @change="datePick">
        </v-date-picker>
      </v-menu>
    </v-col>
  </v-row>
</template>

<script>
  export default {
    name: 'datepicker',
    data () {
      return {
      date: new Date().toISOString().substr(0, 10),
      menu: false,
      }
    },
    created:function(){
      //date初期値(当日の日付)を親コンポーネントにわたすためにemit実行
      this.$emit('datePick',this.date)
    },
    methods:{
      datePick(){
        this.$emit('datePick',this.date)
      }
    }
  }
</script>

日付が選択された際に日付を親コンポーネントに渡すために、@change$emitを実行します。
加えて、日付は初期値でいい場合(当日をそのまま期限として設定したい場合)もあるので、createdフックでも実行しています。
今思えば、初期日付はブランクにして、必ず日付選択させるようにしてもよかったです。

なお、当コンポーネントは日付をYYYY/MM/DDの形式で渡しますが、今回のToDoリスト的には本来MM/DDで足ります。
しかし、再利用することも考えて汎用性のためにYYYYも含めた値を渡すようにしておき、親側でよしなに整形することとします。

todoリストのDatePicker部分抜粋
<template>
  <datepicker @datePick="dateSet"></datepicker>
</template>

<!-- ~中略~ -->

<script>
  methods:{
    dateSet(pickedDate){
      //datePickerコンポーネントから帰ってきた値を加工してappointedDateに格納する
      //YYYY-MM-DDで受け取るので、MM-DD形式に変換し、09などを9にするためNumberを噛ませる
      this.appointedDate = Number(pickedDate.substr(5,2)) + "/" + Number(pickedDate.substr(8,2))      
    },
</script>

前述のdatePickerの操作に応じてdateSetが動き、appointedDateにToDo期日を格納します。

1.1-3) ToDo追加ボタン

入力されたToDoを保存するためのボタンです。
image.png

ToDo追加ボタン抜粋
  <v-btn 
   small
   color="light-blue accent-4"
   dark
   @click="addTodo()">
   + 追加
  </v-btn>

このボタンを押したときに、後述するaddTodoメソッドでToDoの保存を実行します。

1.1-4) ToDoをFirestoreに書き込む(1/2:準備)

ここから本格的な機能を実装していきます。
まずはFirestoreを使うための準備です。

Firebaseのアカウント登録・プロジェクト作成をして云々といろいろやることがあるのですが、
はっきり言ってこのあたりの準備手順は他に素晴らしい解説記事がたくさんありますので、以下などをご参照いただければと思います。

公式ガイドや上記の記事などを参考に、firebase.jsを作成しmain.jsを編集します。

firabase.js
import Vue from 'vue'
import { firestorePlugin } from 'vuefire'
import firebase from 'firebase/app';
import 'firebase/firestore';

Vue.use(firestorePlugin)

const firebaseApp = firebase.initializeApp({
//"xxxxxxxxxxx"の部分は、Firebaseコンソールの
//「設定」→「マイアプリ」から確認できる値に置換する
  apiKey: "xxxxxxxxxxx",
  authDomain: "xxxxxxxxxxx",
  databaseURL: "xxxxxxxxxxx",
  projectId: "xxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxx",
  appId: "xxxxxxxxxxx",
  measurementId: "xxxxxxxxxxx"
});

export const db = firebaseApp.firestore();
main.js抜粋
import './plugins/firebase';

最後に、firebaseのモジュールを本体のtodo.vueでインポートして準備完了です。

todo.vue抜粋
import {db} from '@/plugins/firebase'

//~中略~

created: function() {
//todoコレクションへの参照  
this.todoListRef = db.collection("ToDoList")  

1.1-5) ToDoをFirestoreに書き込む(2/2:ToDo名称+期限の保存)

テキストフォームおよびDatePickerで入力した値をFirestoreに書き込みます。
ここで、書き込む際のメソッドはaddではなくsetを使います

<参考>Cloud Firestore にデータを追加する

addはドキュメントIDが無作為に採番されます。つまり降順に採番されていくわけではないので、ID順に表示すると新しいドキュメントが古いドキュメントより前に来たりします。

データ取得の際にソートしてもよいのですが、その場合も結局ソート用の項目を作らないといけないので
setを利用しドキュメントIDにミリ秒単位のタイムスタンプを設定しています。
家族しか使わないので、流石にミリ秒単位で重複することは考慮しなくてもよいでしょう。

addTodoメソッド
addTodo(){
  //入力がなければ抜ける
  if(this.newTodoName === ""){ console.log("todo入力なし");return }

  //処理日取得(todo登録日用の日付はYY/MM形式に、タイムスタンプはミリ秒形式の文字列に変換)
  const today     = new Date()
  const entryDate = (today.getMonth()+1) + "/" + today.getDate()
  const timestamp = today.toISOString()

  //todolistにsetする
  this.todoListRef.doc(timestamp).set({
  title:this.newTodoName,// 入力されたnewTodoName
  entry_date:entryDate,//todo登録日
  appointed_date:this.appointedDate,//DatePickerで設定したToDo期日
  complete_sts:false // 初期値はfalse(未完了)
  })

このメソッドが動くとこんな感じにFirestoreに登録されます。

1.2 ToDoの取得・表示

Firebase上に保存されているToDo一覧を取得して表示します。
併せてcomplete_sts更新用のチェックボックスと、ToDo削除用の削除ボタンも作ります。

まずはFirestoreから表示するためのデータを取得します。

データ取得処理抜粋
created: function() {

//~中略~

  //todoコレクションへの参照  
  this.todoListRef = db.collection("ToDoList")  
  //onSnapshotメソッドでリスナーを登録
  this.todoListRef.onSnapshot(querySnapshot => { 

    //Firestoreのデータが更新されるたびに以下の処理が実行される
    const obj = {} //object型でデータを取得するため定義しておく
    querySnapshot.forEach(doc => { 
      obj[doc.id] = doc.data()   
      })
    this.todos = obj
    })

onSnapshotquerySnapshot(コレクション配下にあるドキュメントのデータ)を取得し、
forEachでワーク用変数objに放り込んでいき、最後にthis.todosにまとめて格納しています。

このとき、doc.idをkeyとして、doc.dataをkeyに対するデータとして格納しています。
さらにdoc.data自体もオブジェクト型なので、最終的にtodosは以下のような入れ子構造になります。

todosの中身
{
  "2020-09-02T15:58:03.939Z": {
    "title": "牛乳買う",
    "appointed_date": "9/2",
    "entry_date": "9/3",
    "complete_sts": true
  },
  "2020-09-06T02:34:55.908Z": {
    "title": "洗剤買う",
    "appointed_date": "9/6",
    "entry_date": "9/6"
    "complete_sts": false,
  }
}

onSnapshotは一回きりのデータ取得ではなくドキュメントのリッスンを行うので、Firestoreのデータが更新されるたびに自動でこの格納処理が動きます。
 このおかげで、ToDoリスト取得処理を何度も実行せずとも、新規作成・更新・削除のたびに常に最新の状態に保たれます。

次に、このtodosをリスト形式で表示します。

ToDoリスト表示部分抜粋
<ul>
  <li
   v-for="(todo,key) in todos" :key="key">
    <input
     type="checkbox"
     v-model="todo.complete_sts"
     @click="updateTodo(todo,key)"/>
    <font
     :class="{done: todo.complete_sts}">  
     {{todo.title}}
    </font>
    <font 
      class="appointedDate" >
      {{todo.appointed_date}}
      まで
    </font>
    <font 
      class="entryDate" >
      {{todo.entry_date}}
    </font>
    <v-btn
     x-small
     color="grey darken"
     dark
     margin-left="100px"
     @click="deleteTodo(todo,key)"
     v-show="todo.complete_sts"
     class="delbtn">
     削除
    </v-btn>
  </li>   
</ul>
style抜粋
    .entryDate {
      color:gray;
      font-size:70%;
    }
    .appointedDate{
      color:#C62828;
      font-size:85%;
      margin-left: 15px;
    }
    .delbtn {
      margin-left: 15px;
    }
    .done {
      color:gray;
      text-decoration: line-through;
    }

<li>要素をv-forで繰り返し、以下の通り要素を1行に並べて表示しています。

  • [チェックボックス] [ToDoタイトル] [ToDo期限] [ToDo登録日] [削除ボタン]

チェックボックス、ToDoタイトル、削除ボタンはcomplete_stsにバインドしており、
complete_ststrue(完了)かfalse(未完了)かによって表示を切り分けます。

complete_ststrue(完了)のとき、チェックボックスはチェック状態になり、
ToDoタイトルはdoneクラスが適用されてグレーアウト・打ち消し線が入り、
削除ボタンは表示になります。

上の例だとFirestore側は以下のようになっており、「牛乳買う」が完了、「洗剤買う」が未完了という状態です。


また、チェックボックスと削除ボタンはそれぞれ@clickでメソッドを動かすようにしています。
これは後述する「ToDo完了時の状態更新(CRUDの"U")」「完了したToDoの削除(CRUDの"D")」で作成するメソッドです。

1.3 ToDo完了時の状態更新

先ほど登場したupdateTodoメソッドでは、対象ToDoのcomplete_stsを更新します。

updateTodo抜粋
updateTodo(todo,key){
  //checkbox押下時にcomplete_stsを反転させて更新する(完了⇔未完了)
  todo.complete_sts = !todo.complete_sts 
  this.todoListRef.doc(key).update({
    complete_sts: todo.complete_sts 
  })
},    

「2.2 ToDoの取得・表示」においてkeyは各ドキュメントのIDを設定しているため、
doc(key).updateとすることで対象のドキュメントを更新することができます。

1.4 完了したToDoの削除(CRUDの"D")

削除ボタン押下時に動くdeleteTodoメソッドです。

deleteTodo抜粋
deleteTodo(todo,key){
  //削除ボタン押下時にfirestoreの該当docを削除する
  this.todoListRef.doc(key).delete()
},

updateTodoと同様にkeyはドキュメントIDなので、
doc(key).deleteとすることで対象のドキュメントを削除することができます。

以上で通常のToDoリストとしての要件であるCRUD機能は揃ったので、
ようやく、今回の本丸である家族用ToDoリストとしての特別な要件を作っていきます。

2.認証機能の作成

このまま公開すると、家族以外の第三者が自由に閲覧・更新できてしまいます。
これをプライベートなサービスとするため、Firebaseで認証されたユーザのみが利用できるようにしていきます。

2.1 家族のみが閲覧・利用できる

2.1-1) Firabase Authenticationの設定

FirebaseのAuthentication設定画面で、家族のユーザ情報を登録しておきます。

「ユーザーを追加」ボタンを押下するとメールアドレスとパスワードを入力できるようになるので、適宜入力して追加します。
(Sign-in methodタブから「メール / パスワード」を有効にしておく必要があります)

最後に、認証ユーザでなければ読み書きの両方を拒否するようFirestoreのルールを設定しておきます。

2.1-2) ログイン機能の作成

先ほど設定したメールアドレスとパスワードを入力し、認証を行う画面を作成します。

ログインフォーム抜粋
<v-text-field
 v-model="email"
 label="E-mail"
 required>
</v-text-field>

<v-text-field
 v-model="password"
 label="Password"
 type="password"
 required>
</v-text-field>

<v-btn
 color="light-blue darken-3"
 dark
 large
 @click="signIn">
  sign-in
</v-btn>
ログイン処理抜粋
signIn: function () {
  console.log("signin")
  firebase.auth().signInWithEmailAndPassword(this.email, this.password).catch(function(error) {
  //エラー時はポップアップを出す
        alert(error.message)
  });
//認証状態(userがnullでない)のときにtodoに遷移する
firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    location.href="#/todo";
  }
})
}

入力されたメールアドレスとパスワードはそれぞれemailpasswordにバインドし、
signInWithEmailAndPasswordの引数として渡すことでログイン処理を行います。
認証に成功した場合、ToDoリストの画面に切り替えています。

また、認証済みの状態でログイン画面を表示したとき(一度閉じて開き直したときなど)は
自動でToDoリストを表示したいので、createdフックでリダイレクトさせます。

ログイン画面リダイレクト処理抜粋
created: function(){
  //初期表示時にログイン済みであればtodoにリダイレクト
    firebase.auth().onAuthStateChanged(function(user) {
    if (user) {
      location.href="#/todo";
   }})
  },

これの逆をToDoリストにも組み込みます。
非認証状態でToDoリスト画面を表示しようとした際に、ログイン画面にリダイレクトさせます。

todoリスト画面リダイレクト処理抜粋
created: function() {
  //認証状態でない場合、サインイン画面にリダイレクト
  firebase.auth().onAuthStateChanged(function(user) {
  if (!user) {
    location.href="#/signin";
  }})


2.1-3) ログアウト機能の作成

あまり使うことはないですが、ログアウトもできるようにしておきます。

ログアウトボタン抜粋
<v-btn
 @click="signOut"
 color="grey darken-1"
 dark
 dense>
 sign-out
</v-btn>
ログアウト処理抜粋
signOut(){
    firebase.auth().signOut()
    this.$router.push('signin')
  } 

ログアウトは非常に簡単で、signOut()を実行するだけです。
ログアウト後、signin画面に戻るようにしています。

2.2 ToDoを誰が新規作成したかがわかる

これまでの機能だけだと、家族の「誰が」登録したToDoなのかがわかりません。
よって、以下のようにToDoの登録者が誰であるかわかるようにします。

本来、ユーザのプロフィール情報から名前(displayName)を取得してaddTodoで一緒に登録すればよいのでとても簡単なのですが
今回の場合、以下2つの問題があります。

  • メールアドレスをFirebaseコンソールから登録しているので、ユーザプロフィールにdisplayNameが存在しない
  • displayNameの代わりにユーザのemailを名前のように使ってもよいが、イケてない

1点目は、たとえばユーザ認証をGoogleアカウントなどで実施していた場合、Googleのユーザ名がdisplayNameとしてそのまま使えるのですが、
今回はメールアドレスとパスワードのみをもったユーザであるため、デフォルトではdisplayNameには何も設定されていません。

コンソールなどからどうにか編集できないか調べたのですが見当たらず、以下の力技で解決することにしました。
もし良いやり方をご存じの方がいらっしゃれば教えて下さい……。

2.2-1) displayName登録フォームを作る

手作業で登録する方法はわかりませんでしたが、displayNameを更新するメソッドがあることはわかったため、displayName登録フォームを作成しました。

displayName登録フォーム抜粋
<v-text-field
 color="grey darken-1"
 label="新しいユーザ名を入力してください"
 v-model="userName"
 >
</v-text-field> 

{{msg}}<br> 

<v-btn
 @click="updateName"
 color="blue darken-3"
 dark>
  submit
</v-btn>
updateName抜粋
updateName: function () {
  var user = firebase.auth().currentUser;

  //thisの参照が切れるのでselfに退避
  const self = this

  //入力されたuserNameで、ログイン中ユーザのdisplayNameを更新
  user.updateProfile({ displayName:self.userName }).then(function() {
  self.msg = "登録成功"
  }).catch(function(error) {
  console.log(error)
  self.msg = "登録失敗"
  });
  this.userName=""
}

ログインした状態で任意の名前を入力してsubmitボタンを押すと、そのログイン中ユーザのdisplayNameが登録(更新)されます。
これを全ユーザ分行ってdisplayNameを登録します。

初回の登録作業が終わればこのフォームは不要なので、あとで消してしまってOKです。

2.2-2) ToDo登録者名を書き込み・読み取り・表示する


最後に、addTodoメソッドにおいてログインユーザのdisplayNameentry_userという項目名でFirestoreに書き込み、
またtemplate部でもFirestoreから取得したentry_userを表示させるようにします。

addTodoメソッド(2)
addTodo(){
  //入力がなければ抜ける
  if(this.newTodoName === ""){ console.log("todo入力なし");return }

  //ログイン中のユーザ情報を取得
  const user = firebase.auth().currentUser;

  //ログイン状態であれば処理を行う
  if (user != null) {

    //currentUserのdisplayNameを設定
    const userName  = user.displayName; 

    //処理日取得(todo登録日用の日付はYY/MM形式に、タイムスタンプはミリ秒形式の文字列に変換)
    const today     = new Date()
    const entryDate = (today.getMonth()+1) + "/" + today.getDate()
    const timestamp = today.toISOString()

    //todolistにsetする
    this.todoListRef.doc(timestamp).set({
    title:this.newTodoName,// 入力されたnewTodoName
    entry_user:userName,//ログイン中ユーザのdisplayName
    entry_date:entryDate,//todo登録日
    appointed_date:this.appointedDate,//DatePickerで設定したToDo期日
    complete_sts:false // 初期値はfalse(未完了)
    })
  }
ToDoリスト表示部分抜粋
<ul>
  <li
   v-for="(todo,key) in todos" :key="key">

  <!--中略-->
    <font 
      class="entryUserName" >
      {{todo.entry_user}}
    </font> 
  <!--中略-->

  </li>   
</ul>
style抜粋(2)
    .entryUserName {
      color:gray;
      font-size:70%;
      margin-left: 10px;

3.参考文献

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

[Vue/Nuxt] プロパティの値の有無によるエラーハンドリング

VuexのStateのプロパティにオブジェクトで値が入っている場合において、
値の有無による条件処理でのエラーハンドリング実装方法の一例を紹介します。

今回は、
プロパティに値が入っていたらdispatchでActionsを発火させる
という場合のコードとなっております。

index.vue
<script>
export default {
  async fetch({ store }) {
    if (
      store.state.index.data &&
      Object.keys(store.state.index.data).length > 0
    ) {
      await store.dispatch('index/fetchData')
    }
  }
}

store.state.index.data
では単にdataプロパティに値があるかの確認。

且つ、
Object.keys(store.state.index.data).length > 0
オブジェクト数が0より多いことを確認しています。

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

AxiosでAPIのエンドポイントを@nuxtjs/proxyを使って省略する

Axiosを使ってAPI実行する時にエンドポイントを全て書くのはめんどくさいですよね。

具体的にどういうことかというと、
こういうことです。

プロキシ未設定

index.js
async fetchData({ commit, state }) {
  try {
    const resp = await this.$axios.$get('http://localhost:8000/api/score')
    commit('setData', resp)
  } catch (error) {
    console.log(error)
  }
}

プロキシ設定済

index.js
async fetchData({ commit, state }) {
  try {
    const resp = await this.$axios.$get('/api/score')
    commit('setData', resp)
  } catch (error) {
    console.log(error)
  }
}

APIのエンドポイントが短くなっていますね。

このやり方を解説します。

@nuxtjs/proxyをインストール

npm install --save @nuxtjs/proxy

proxyの設定

nuxt.config.jsに下記の設定を追加します。

nuxt.config.js
modules: [
  '@nuxtjs/proxy'
],
proxy: {
  '/api': 'http://localhost:8000'
}

※8000のところは各自のサーバのものを入れてください。
これで設定完了です。

あとはAxios使う時のAPIエンドポイントは
http://localhost:8000
以降の部分を記述するだけでよくなります。

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

AxiosでAPI使う時のエンドポイントを@nuxtjs/proxyを使って省略する

Axiosを使ってAPI実行する時にエンドポイントを全て書くのはめんどくさいですよね。

具体的にどういうことかというと、
こういうことです。

プロキシ未設定

index.js
async fetchData({ commit, state }) {
  try {
    const resp = await this.$axios.$get('http://localhost:8000/api/score')
    commit('setData', resp)
  } catch (error) {
    console.log(error)
  }
}

プロキシ設定済

index.js
async fetchData({ commit, state }) {
  try {
    const resp = await this.$axios.$get('/api/score')
    commit('setData', resp)
  } catch (error) {
    console.log(error)
  }
}

APIのエンドポイントが短くなっていますね。

このやり方を解説します。

@nuxtjs/proxyをインストール

npm install --save @nuxtjs/proxy

proxyの設定

nuxt.config.jsに下記の設定を追加します。

nuxt.config.js
modules: [
  '@nuxtjs/proxy'
],
proxy: {
  '/api': 'http://localhost:8000'
}

これで設定完了です。

あとはAxios使う時のAPIエンドポイントは
http://localhost:8000
以降の部分を記述するだけでよくなります。

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

[Vuex] 複数のdispatchを全て実行するにはPromise.allを使う

VuexのActionsを実行するトリガーとなるdispatch
複数全て実行したい場合には
Promise.all
を使うと実装できます。

index.vue
<script>
export default {
  async fetch({ store }) {
    await Promise.all([
      store.dispatch('〜〜〜'),
      store.dispatch('〜〜〜'),
      store.dispatch('〜〜〜')
    ])
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Vue.js] ライフサイクルフック

はじめに

Vue.js には ライフサイクル という概念があって、Vue.js で作られたページはこのライフサイクルをもとに処理が実行されていく。
で、Vue.js を使うにあたり、ライフサイクルを理解しているとしていないとでは効率や作法といった点で違いが出てくると思うので、まずはこれを学んでいく。

( すでに こちらこちら に先達の方々の素晴らしい記事があるが、自分でも記事を書くことで知識の定着を狙う )

前提

  • Vue CLI でプロジェクトを作成していること
  • 単一ファイルコンポーネントであること( 1 )
  • 動作確認は $ npm run serve で起動した環境で行なっている

環境

Version 備考
Vue 2.5.17
Vue CLI 3.0.5

ライフサイクル

Vue.js におけるライフサイクルは以下の 8 つ

ライフサイクル タイミング 備考
1 beforeCreate インスタンスは生成されたがデータが初期化される前 Create の前 とか言っているくせに インスタンスは生成されている 点に注意
2 created インスタンスが生成され、且つデータが初期化された後 こちらは名前の通りで、直感的にわかりやすい
3 beforeMount インスタンスが DOM 要素にマウントされる前 こちらも名前の通りでわかりやすい
4 mounted インスタンスが DOM 要素にマウントされた後 同上
5 beforeUpdate データは更新されたが DOM に適用される前 Update の前 とか言っているくせに データは更新されている 点に注意
とはいえ、 Update が DOM の更新 を指すならば、名前のとおりか
6 updated データが更新され、且つ DOM に適用された後 こちらは名前の通りで、直感的にわかりやすい
7 beforeDestroy Vue インスタンスが破壊される前 同上
8 destroyed Vue インスタンスが破壊された後 同上

暗記するものでもないが、 create, mount, update, destroy の 4 つに対して、それぞれ before と after の動きがある と、捉えれば覚えやすい。

公式の こちら からライフサイクルの流れを描いた図を転載させていただく。

ライフサイクルダイアグラム

lifecycle.png

実際に動きを確認する

確認用コード

ライフサイクルの動きを見るために、各ライフサイクルのフックに対してログを仕込んだ。
後述の スクリーンショット で、本コードの説明を図とともに見ていく。

<template>
  <div :class="$style.lifecycle">
    <input
      v-model="properties.message"
      :class="$style.message"
      placeholder="edit me">
    <p>Message is: {{ properties.message }}</p>
  </div>
</template>

<script>
export default {
  name: 'Lifecycle',
  data: function() {
    return {
      properties: {
        message: 'default value.',
      },
    }
  },

  /* ################################ ライフサイクル ################################ */
  /**
   *  [公式](https://jp.vuejs.org/v2/api/index.html#beforeCreate) から拝借。
   *
   * データの監視とイベント/ウォッチャのセットアップより前の、インスタンスが初期化されるときに同期的に呼ばれます。
   */
  beforeCreate: function() {
    try {
      this.properties.message = 'set value on beforeCreate.'
      console.log(`message is ${this.properties.message}`)
    } catch (e) {
      console.log(e)
    }
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#created) から拝借。
   *
   * インスタンスが作成された後に同期的に呼ばれます。
   * この段階では、インスタンスは、データ監視、算出プロパティ、メソッド、watch/event コールバックらの
   * オプションのセットアップ処理が完了したことを意味します。
   * しかしながら、マウンティングの段階は未開始で、`$el` プロパティはまだ利用できません。
   */
  created: function() {
    this.properties.message = 'set value on created.'
    console.log(`[LifeCycle] created. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeMount) から拝借。
   *
   * `render` 関数が初めて呼び出されようと、マウンティングが開始される直前に呼ばれます。
   */
  beforeMount: function() {
    this.properties.message = 'set value on beforeMount.'
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#mounted) から拝借。
   *
   * 新たに作成される `vm.$el` によって置き換えられる `el` に対して、インスタンスがマウントされたちょうど後に呼ばれます。
   * ルートインスタンスがドキュメントの中の要素にマウントされる場合、`vm.$el` も `mounted` が呼び出されるときにドキュメントの中に入ります。
   * `mounted` は 全ての子コンポーネントもマウントされていることを保証**しない**ことに注意してください。
   * ビュー全体がレンダリングされるまで待つ場合は、 `mounted` の代わりに
   * [vm.$nextTick](https://jp.vuejs.org/v2/api/index.html#vm-nextTick) を使うことができます。
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  mounted: function() {
    this.properties.message = 'set value on mounted.'
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeUpdate) から拝借。
   *
   * データが変更されるとき、DOM が適用される前に呼ばれます。
   * これは、更新前に既存の DOM にアクセスするのに適しています。
   * 例: 手動で追加されたイベントリスナを削除する
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   * サーバサイドでは初期描画のみ実行されるためです。
   */
  beforeUpdate: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#updated) から拝借。
   *
   * データが変更後、仮想 DOM が再描画そしてパッチを適用によって呼ばれます。
   * このフックが呼び出されるとき、コンポーネントの DOM は更新した状態になり、このフックで DOM に依存する操作を行うことができます。
   * しかしがながら、ほとんどの場合、無限更新ループに陥る可能性があるため、このフックでは状態を変更するのを回避すべきです。
   *
   * `updated` は 全ての子コンポーネントも再レンダリングされていることを保証**しない**ことに注意してください。
   * ビュー全体が再レンダリングされるまで待つ場合は、 `updated` の代わりに
   * [vm.$nextTick](https://jp.vuejs.org/v2/api/index.html#vm-nextTick) を使うことができます。
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  updated: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeDestroy) から拝借。
   *
   * > Vue インスタンスが破棄される直前に呼ばれます。
   * この段階ではインスタンスはまだ完全に機能しています。
   *
   * **このフックはサーバサイドレンダリングでは呼ばれません。**
   */
  beforeDestroy: function() {
    console.log(`[LifeCycle] beforeDestroy. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#destroyed) から拝借。
   *
   * Vue インスタンスが破棄された後に呼ばれます。
   * このフックが呼ばれるとき、Vue インスタンスの全てのディレクティブはバウンドしておらず、
   * 全てのイベントリスナは削除されており、そして全ての子の Vue インスタンスは破棄されています。
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  destroyed: function() {
    console.log(`[LifeCycle] destroyed. this.properties.message = ${this.properties.message}`)
  },
}
</script>

<style module>
.lifecycle {
    margin: 20px;
}

.message {
    width: 400px;
}
</style>

1. beforeCreate > mounted まで

前掲のコードをベースに次の 3パターン で動きを確認する。

  1. 変数 properties.message の更新を created で止める
  2. 変数 properties.message の更新を beforeMount で止める
  3. 変数 properties.message の更新を mounted で止める

以下、順に見ていく。

1.1. 変数 properties.message の更新を created で止める

1.1.beforeCreate2mounted.JPG

<script>
// ...
// 省略
// ...

  data: function() {
    return {
      properties: {
        message: 'default value.',
      },
    }
  },
  beforeCreate: function() {
    try {
      this.properties.message = 'set value on beforeCreate.'
      console.log(`message is ${this.properties.message}`)
    } catch (e) {
      console.log(e)
    }
  },
  created: function() {
    this.properties.message = 'set value on created.'
    console.log(`[LifeCycle] created. this.properties.message = ${this.properties.message}`)
  },
  beforeMount: function() {
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },
  mounted: function() {
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

前掲の ライフサイクルダイアグラム と合わせて考えると、次の動きとなっていることが理解できる。

  1. インスタンス生成時にライフサイクルが初期化される
  2. beforeCreatedproperties.message の値を set value on beforeCreate. に更新しようとするが、このとき例外が発生している
    • つまり、このタイミングでは まだインスタンスの生成が終わったいない ため、 data に定義された変数を 参照できない
    • 今回のコードでは例外情報をログ出力したあと、そのまま次の処理に進むので、created が実行される
  3. created では properties.messageset value on created. で更新している
    • ログ出力されている情報から、正常にデータが更新されたことがわかる
  4. 続く beforeMount 並びに mountedproperties.message を更新していないので、 created フックでセットした set value on created. がそのまま DOM にセットされて画面上に表示 されている

1.2. 変数 properties.message の更新を beforeMount で止める

1.2.beforeCreate2mounted.JPG

<script>
// ...
// 省略: `beforeMount` まで省く
// ...
  beforeMount: function() {
    this.properties.message = 'set value on beforeMount.'
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },
  mounted: function() {
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

created までの動きは前項と同じ。 差分となる beforeMount 以降 を見る。

  1. beforeMount で変数 properties.messageset value on beforeMount. をセットしている
    • ログ出力されている情報から、created でセットされた後に本フックで正常にデータが更新されたことがわかる
    • 続く mountedproperties.message を更新していないので、 beforeMount フックでセットした set value on beforeMount. がそのまま DOM にセットされて画面上に表示 されている

1.3. 変数 properties.message の更新を mounted で止める

1.3_beforeCreate2updated.JPG

<script>
// ...
// 省略: `mounted` まで省く
// ...
  mounted: function() {
    this.properties.message = 'set value on mounted.'
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
  beforeUpdate: function() {
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },
  updated: function() {
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

beforeMount までの動きは前項と同じ。 *差分となる mounted *を見る。

  1. mounted で変数 properties.messageset value on mounted. をセットしている
    • ログ出力されている情報から、正常にデータが更新されたことがわかる

注目したいのは mounted 以降のログ。
beforeUpdate 、並びに updated で変数の更新は行っていない。 にも関わらず、 beforeUpdateupdated のログが出ている ことから、 mounted で変数を更新していることと併せて次のことがわかる。

  1. beforeMount > mounted の間に DOM の更新が行われた
  2. この時点で画面上には set value on beforeMount. が表示される
  3. その後 mounted のフックが実行される
  4. で、 mounted で変数を更新しているので、 beforeUpdate、次いで updated が実行された
  5. DOM の更新は beforeUpdateupdated の間に行われる ので画面上には mounted フックで更新した値である set value on mounted. が表示 されている
  6. これは ライフサイクルダイアグラム の流れに合致する

注意点

いわゆる コンストラクタ で行うような、 Vue インスタンスに対する初期処理created、もしくは beforeMount で行うと良さそう。
どちらで行うのがより良いのかは、実際にプロダクトを作っていく上で判断したい。

ただ mounted でやるのは実行コストが高そうなので止めておく。 理由は前掲のとおりで、mounted で DOM に紐づく変数を更新すると、それに引っ張られて beforeUpdateupdated のフックも実行されるから。

2. beforeUpdate > updated まで

こちらは特にパターンを確認することもない。前掲のコードとともに動きを見てみる。

2_beforeUpdate2updated.JPG

<script>
// ...
// 省略: `beforeUpdate` まで省く
// ...
  beforeUpdate: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },
  updated: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

初期表示の画面は以下の状態で表示されていた。

項目 表示されていた文字列
テキスト入力欄 set value on created.
入力内容の表示欄 Message is: set value on created.

上記を前提に、ここでは画面上でデータを更新したときの動きを確認した。
行った内容は次のとおり。

  1. テキスト入力欄に表示されている set value on created. の後に > updated の文字列を入力

入力した内容に対するログの出方はスクリーンショットの画像の通りで、

  1. beforeUpdate でログ出力
  2. update でログ出力

beforeUpdate > update の順で出力されており、 ライフサイクルダイアグラム の流れに合致することが確認できた。

注意点

で、ここではスクリーンショットを貼らなかったが、ソースコードのコメントに記載したとおり、

// beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る

ので注意。

これは次の動きによるものだと思う。

  1. beforeUpdate で変数を更新
  2. updated が走り、ここでも変数を更新
  3. するとまた beforeUpdate が走る
  4. 以下、 ループ

beforeUpdateupdated での変数の扱いには充分に注意 したい。

3. beforeDestroy > destroyed まで

こちらも特にパターンを確認することもない。前掲のコードとともに動きを見てみる。

3_beforeDestroy2destroyed.JPG

<script>
// ...
// 省略: `beforeUpdate` まで省く
// ...
  beforeDestroy: function() {
    console.log(`[LifeCycle] beforeDestroy. this.properties.message = ${this.properties.message}`)
  },
  destroyed: function() {
    console.log(`[LifeCycle] destroyed. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

画面上部の Life Cycle がこれまでライフサイクルの確認をしていた画面。
beforeDestroydestroyed の確認をするため、隣の Sample Buefy に移動してみた。

ログには beforeDestroy > destroyed の順で出力されており、ここも ライフサイクルダイアグラム の流れに合致することが確認できた。

注意点

beforeDestroy は重要な役割を持つ。

beforeDestroy はいわゆる デストラクタ 的な役割を持つと解釈してよく、このフックで 子コンポーネント や独自で用意した イベントリスナー とか タイマー処理 とかの後始末をした方が良い。

前掲の ライフサイクル で触れたとおりdestroyedVue インスタンスの破棄した後 に実行されるフックなので、そこでやろうとしても 遅すぎる ためである。

ライフサイクルの注意点

公式ページの こちら に注意点が記載されているので転載しておく。
上記によると、

全てのライフサイクルフックは、this が Vue インスタンスを指す形で実行されます。

とのこと。

またこれに関連して

インスタンスプロパティまたはコールバックでアロー関数 を使用しないでください。例えば、 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod()) です。アロー関数は this をもたないため、this は他の変数と同様に見つかるまで親スコープをレキシカルに探索され、そしてしばしば、Uncaught TypeError: Cannot read property of undefined または Uncaught TypeError: this.myMethod is not a function のようなエラーが発生します。

とある。

以下、語弊があるかもしれないが、自分なりの解釈を。

上記で記載されているアロー関数に対する記述は通常メリットとして捉えられて、そのためにアロー関数は const self = this;といったことをせずにメッセージの送り元を扱える。

だが Vue.js におけるライフサイクルではこれがデメリットとなり得るとのこと。うっかりアロー関数を使って迷路に迷い込まないよう、充分に注意したい。

まとめにかえて

beforeCreate から destroyed までの動きをざっと見てきた。 その中での注意点を書き出すことでまとめに代えたい。

  • Vue インスタンスの初期処理は createdbeforeMount で行うのが良さげ
  • beforeUpdateupdated で DOM に関係する変数の更新を 下手に行う無限ループに陥る可能性がある ので注意する
  • beforeDestroy では Vue インスタンスの後始末を行う

DOM に紐づく変数の更新については、 算出プロパティとウォッチャ もあるので、今後はそちらも見ていきたい。

ソースコード

今回の記事で動作確認に使用したコードは下記にアップしてあるのでご参考まで。( 以下は ブランチのリンクですが、 master にもマージ済みです )

参考

公式

上記以外


  1. Vue.js のコンポーネントを単独のファイルとして作成する機能
    拡張子「.vue」のファイルのことで<template>, <script>, <style> のブロックで構成されている。 

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

[Vue.js] ライフサイクルをみる

はじめに

Vue.js には ライフサイクル という概念があって、Vue.js で作られたページはこのライフサイクルをもとに処理が実行されていく。
で、Vue.js を使うにあたり、ライフサイクルを理解しているとしていないとでは効率や作法といった点で違いが出てくると思うので、まずはこれを学んでいく。

( すでに こちらこちら に先達の方々の素晴らしい記事があるが、自分でも記事を書くことで知識の定着を狙う )

前提

  • Vue CLI でプロジェクトを作成していること
  • 単一ファイルコンポーネントであること( 1 )
  • 動作確認は $ npm run serve で起動した環境で行なっている

環境

Version 備考
Vue 2.5.17
Vue CLI 3.0.5

ライフサイクル

Vue.js におけるライフサイクルは以下の 8 つ

ライフサイクル タイミング 備考
1 beforeCreate インスタンスは生成されたがデータが初期化される前 Create の前 とか言っているくせに インスタンスは生成されている 点に注意
2 created インスタンスが生成され、且つデータが初期化された後 こちらは名前の通りで、直感的にわかりやすい
3 beforeMount インスタンスが DOM 要素にマウントされる前 こちらも名前の通りでわかりやすい
4 mounted インスタンスが DOM 要素にマウントされた後 同上
5 beforeUpdate データは更新されたが DOM に適用される前 Update の前 とか言っているくせに データは更新されている 点に注意
とはいえ、 Update が DOM の更新 を指すならば、名前のとおりか
6 updated データが更新され、且つ DOM に適用された後 こちらは名前の通りで、直感的にわかりやすい
7 beforeDestroy Vue インスタンスが破壊される前 同上
8 destroyed Vue インスタンスが破壊された後 同上

暗記するものでもないが、 create, mount, update, destroy の 4 つに対して、それぞれ before と after の動きがある と、捉えれば覚えやすい。

公式の こちら からライフサイクルの流れを描いた図を転載させていただく。

ライフサイクルダイアグラム

lifecycle.png

実際に動きを確認する

確認用コード

ライフサイクルの動きを見るために、各ライフサイクルのフックに対してログを仕込んだ。
後述の スクリーンショット で、本コードの説明を図とともに見ていく。

<template>
  <div :class="$style.lifecycle">
    <input
      v-model="properties.message"
      :class="$style.message"
      placeholder="edit me">
    <p>Message is: {{ properties.message }}</p>
  </div>
</template>

<script>
export default {
  name: 'Lifecycle',
  data: function() {
    return {
      properties: {
        message: 'default value.',
      },
    }
  },

  /* ################################ ライフサイクル ################################ */
  /**
   *  [公式](https://jp.vuejs.org/v2/api/index.html#beforeCreate) から拝借。
   *
   * データの監視とイベント/ウォッチャのセットアップより前の、インスタンスが初期化されるときに同期的に呼ばれます。
   */
  beforeCreate: function() {
    try {
      this.properties.message = 'set value on beforeCreate.'
      console.log(`message is ${this.properties.message}`)
    } catch (e) {
      console.log(e)
    }
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#created) から拝借。
   *
   * インスタンスが作成された後に同期的に呼ばれます。
   * この段階では、インスタンスは、データ監視、算出プロパティ、メソッド、watch/event コールバックらの
   * オプションのセットアップ処理が完了したことを意味します。
   * しかしながら、マウンティングの段階は未開始で、`$el` プロパティはまだ利用できません。
   */
  created: function() {
    this.properties.message = 'set value on created.'
    console.log(`[LifeCycle] created. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeMount) から拝借。
   *
   * `render` 関数が初めて呼び出されようと、マウンティングが開始される直前に呼ばれます。
   */
  beforeMount: function() {
    this.properties.message = 'set value on beforeMount.'
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#mounted) から拝借。
   *
   * 新たに作成される `vm.$el` によって置き換えられる `el` に対して、インスタンスがマウントされたちょうど後に呼ばれます。
   * ルートインスタンスがドキュメントの中の要素にマウントされる場合、`vm.$el` も `mounted` が呼び出されるときにドキュメントの中に入ります。
   * `mounted` は 全ての子コンポーネントもマウントされていることを保証**しない**ことに注意してください。
   * ビュー全体がレンダリングされるまで待つ場合は、 `mounted` の代わりに
   * [vm.$nextTick](https://jp.vuejs.org/v2/api/index.html#vm-nextTick) を使うことができます。
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  mounted: function() {
    this.properties.message = 'set value on mounted.'
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeUpdate) から拝借。
   *
   * データが変更されるとき、DOM が適用される前に呼ばれます。
   * これは、更新前に既存の DOM にアクセスするのに適しています。
   * 例: 手動で追加されたイベントリスナを削除する
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   * サーバサイドでは初期描画のみ実行されるためです。
   */
  beforeUpdate: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#updated) から拝借。
   *
   * データが変更後、仮想 DOM が再描画そしてパッチを適用によって呼ばれます。
   * このフックが呼び出されるとき、コンポーネントの DOM は更新した状態になり、このフックで DOM に依存する操作を行うことができます。
   * しかしがながら、ほとんどの場合、無限更新ループに陥る可能性があるため、このフックでは状態を変更するのを回避すべきです。
   *
   * `updated` は 全ての子コンポーネントも再レンダリングされていることを保証**しない**ことに注意してください。
   * ビュー全体が再レンダリングされるまで待つ場合は、 `updated` の代わりに
   * [vm.$nextTick](https://jp.vuejs.org/v2/api/index.html#vm-nextTick) を使うことができます。
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  updated: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#beforeDestroy) から拝借。
   *
   * > Vue インスタンスが破棄される直前に呼ばれます。
   * この段階ではインスタンスはまだ完全に機能しています。
   *
   * **このフックはサーバサイドレンダリングでは呼ばれません。**
   */
  beforeDestroy: function() {
    console.log(`[LifeCycle] beforeDestroy. this.properties.message = ${this.properties.message}`)
  },

  /**
   * [公式](https://jp.vuejs.org/v2/api/index.html#destroyed) から拝借。
   *
   * Vue インスタンスが破棄された後に呼ばれます。
   * このフックが呼ばれるとき、Vue インスタンスの全てのディレクティブはバウンドしておらず、
   * 全てのイベントリスナは削除されており、そして全ての子の Vue インスタンスは破棄されています。
   *
   * このフックはサーバサイドレンダリングでは呼ばれません。
   */
  destroyed: function() {
    console.log(`[LifeCycle] destroyed. this.properties.message = ${this.properties.message}`)
  },
}
</script>

<style module>
.lifecycle {
    margin: 20px;
}

.message {
    width: 400px;
}
</style>

1. beforeCreate > mounted まで

前掲のコードをベースに次の 3パターン で動きを確認する。

  1. 変数 properties.message の更新を created で止める
  2. 変数 properties.message の更新を beforeMount で止める
  3. 変数 properties.message の更新を mounted で止める

以下、順に見ていく。

1.1. 変数 properties.message の更新を created で止める

1.1.beforeCreate2mounted.JPG

<script>
// ...
// 省略
// ...

  data: function() {
    return {
      properties: {
        message: 'default value.',
      },
    }
  },
  beforeCreate: function() {
    try {
      this.properties.message = 'set value on beforeCreate.'
      console.log(`message is ${this.properties.message}`)
    } catch (e) {
      console.log(e)
    }
  },
  created: function() {
    this.properties.message = 'set value on created.'
    console.log(`[LifeCycle] created. this.properties.message = ${this.properties.message}`)
  },
  beforeMount: function() {
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },
  mounted: function() {
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

前掲の ライフサイクルダイアグラム と合わせて考えると、次の動きとなっていることが理解できる。

  1. インスタンス生成時にライフサイクルが初期化される
  2. beforeCreatedproperties.message の値を set value on beforeCreate. に更新しようとするが、このとき例外が発生している
    • つまり、このタイミングでは まだインスタンスの生成が終わったいない ため、 data に定義された変数を 参照できない
    • 今回のコードでは例外情報をログ出力したあと、そのまま次の処理に進むので、created が実行される
  3. created では properties.messageset value on created. で更新している
    • ログ出力されている情報から、正常にデータが更新されたことがわかる
  4. 続く beforeMount 並びに mountedproperties.message を更新していないので、 created フックでセットした set value on created. がそのまま DOM にセットされて画面上に表示 されている

1.2. 変数 properties.message の更新を beforeMount で止める

1.2.beforeCreate2mounted.JPG

<script>
// ...
// 省略: `beforeMount` まで省く
// ...
  beforeMount: function() {
    this.properties.message = 'set value on beforeMount.'
    console.log(`[LifeCycle] beforeMount. this.properties.message = ${this.properties.message}`)
  },
  mounted: function() {
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

created までの動きは前項と同じ。 差分となる beforeMount 以降 を見る。

  1. beforeMount で変数 properties.messageset value on beforeMount. をセットしている
    • ログ出力されている情報から、created でセットされた後に本フックで正常にデータが更新されたことがわかる
    • 続く mountedproperties.message を更新していないので、 beforeMount フックでセットした set value on beforeMount. がそのまま DOM にセットされて画面上に表示 されている

1.3. 変数 properties.message の更新を mounted で止める

1.3_beforeCreate2updated.JPG

<script>
// ...
// 省略: `mounted` まで省く
// ...
  mounted: function() {
    this.properties.message = 'set value on mounted.'
    console.log(`[LifeCycle] mounted. this.properties.message = ${this.properties.message}`)
  },
  beforeUpdate: function() {
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },
  updated: function() {
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

beforeMount までの動きは前項と同じ。 *差分となる mounted *を見る。

  1. mounted で変数 properties.messageset value on mounted. をセットしている
    • ログ出力されている情報から、正常にデータが更新されたことがわかる

注目したいのは mounted 以降のログ。
beforeUpdate 、並びに updated で変数の更新は行っていない。 にも関わらず、 beforeUpdateupdated のログが出ている ことから、 mounted で変数を更新していることと併せて次のことがわかる。

  1. beforeMount > mounted の間に DOM の更新が行われた
  2. この時点で画面上には set value on beforeMount. が表示される
  3. その後 mounted のフックが実行される
  4. で、 mounted で変数を更新しているので、 beforeUpdate、次いで updated が実行された
  5. DOM の更新は beforeUpdateupdated の間に行われる ので画面上には mounted フックで更新した値である set value on mounted. が表示 されている
  6. これは ライフサイクルダイアグラム の流れに合致する

注意点

いわゆる コンストラクタ で行うような、 Vue インスタンスに対する初期処理created、もしくは beforeMount で行うと良さそう。
どちらで行うのがより良いのかは、実際にプロダクトを作っていく上で判断したい。

ただ mounted でやるのは実行コストが高そうなので止めておく。 理由は前掲のとおりで、mounted で DOM に紐づく変数を更新すると、それに引っ張られて beforeUpdateupdated のフックも実行されるから。

2. beforeUpdate > updated まで

こちらは特にパターンを確認することもない。前掲のコードとともに動きを見てみる。

2_beforeUpdate2updated.JPG

<script>
// ...
// 省略: `beforeUpdate` まで省く
// ...
  beforeUpdate: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] beforeUpdate. this.properties.message = ${this.properties.message}`)
  },
  updated: function() {
    // 注意!!
    // beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る
    console.log(`[LifeCycle] updated. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

初期表示の画面は以下の状態で表示されていた。

項目 表示されていた文字列
テキスト入力欄 set value on created.
入力内容の表示欄 Message is: set value on created.

上記を前提に、ここでは画面上でデータを更新したときの動きを確認した。
行った内容は次のとおり。

  1. テキスト入力欄に表示されている set value on created. の後に > updated の文字列を入力

入力した内容に対するログの出方はスクリーンショットの画像の通りで、

  1. beforeUpdate でログ出力
  2. update でログ出力

beforeUpdate > update の順で出力されており、 ライフサイクルダイアグラム の流れに合致することが確認できた。

注意点

で、ここではスクリーンショットを貼らなかったが、ソースコードのコメントに記載したとおり、

// beforeUpdate と updated で同じ変数に対してデータを更新かけると無限ループに陥る

ので注意。

これは次の動きによるものだと思う。

  1. beforeUpdate で変数を更新
  2. updated が走り、ここでも変数を更新
  3. するとまた beforeUpdate が走る
  4. 以下、 ループ

beforeUpdateupdated での変数の扱いには充分に注意 したい。

3. beforeDestroy > destroyed まで

こちらも特にパターンを確認することもない。前掲のコードとともに動きを見てみる。

3_beforeDestroy2destroyed.JPG

<script>
// ...
// 省略: `beforeUpdate` まで省く
// ...
  beforeDestroy: function() {
    console.log(`[LifeCycle] beforeDestroy. this.properties.message = ${this.properties.message}`)
  },
  destroyed: function() {
    console.log(`[LifeCycle] destroyed. this.properties.message = ${this.properties.message}`)
  },
// ...
// 省略
// ...
</script>

画面上部の Life Cycle がこれまでライフサイクルの確認をしていた画面。
beforeDestroydestroyed の確認をするため、隣の Sample Buefy に移動してみた。

ログには beforeDestroy > destroyed の順で出力されており、ここも ライフサイクルダイアグラム の流れに合致することが確認できた。

注意点

beforeDestroy は重要な役割を持つ。

beforeDestroy はいわゆる デストラクタ 的な役割を持つと解釈してよく、このフックで 子コンポーネント や独自で用意した イベントリスナー とか タイマー処理 とかの後始末をした方が良い。

前掲の ライフサイクル で触れたとおりdestroyedVue インスタンスの破棄した後 に実行されるフックなので、そこでやろうとしても 遅すぎる ためである。

ライフサイクルの注意点

公式ページの こちら に注意点が記載されているので転載しておく。
上記によると、

全てのライフサイクルフックは、this が Vue インスタンスを指す形で実行されます。

とのこと。

またこれに関連して

インスタンスプロパティまたはコールバックでアロー関数 を使用しないでください。例えば、 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod()) です。アロー関数は this をもたないため、this は他の変数と同様に見つかるまで親スコープをレキシカルに探索され、そしてしばしば、Uncaught TypeError: Cannot read property of undefined または Uncaught TypeError: this.myMethod is not a function のようなエラーが発生します。

とある。

以下、語弊があるかもしれないが、自分なりの解釈を。

上記で記載されているアロー関数に対する記述は通常メリットとして捉えられて、そのためにアロー関数は const self = this;といったことをせずにメッセージの送り元を扱える。

だが Vue.js におけるライフサイクルではこれがデメリットとなり得るとのこと。うっかりアロー関数を使って迷路に迷い込まないよう、充分に注意したい。

まとめにかえて

beforeCreate から destroyed までの動きをざっと見てきた。 その中での注意点を書き出すことでまとめに代えたい。

  • Vue インスタンスの初期処理は createdbeforeMount で行うのが良さげ
  • beforeUpdateupdated で DOM に関係する変数の更新を 下手に行う無限ループに陥る可能性がある ので注意する
  • beforeDestroy では Vue インスタンスの後始末を行う

DOM に紐づく変数の更新については、 算出プロパティとウォッチャ もあるので、今後はそちらも見ていきたい。

ソースコード

今回の記事で動作確認に使用したコードは下記にアップしてあるのでご参考まで。( 以下は ブランチのリンクですが、 master にもマージ済みです )

参考

公式

上記以外


  1. Vue.js のコンポーネントを単独のファイルとして作成する機能
    拡張子「.vue」のファイルのことで<template>, <script>, <style> のブロックで構成されている。 

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

Nuxt.jsで、AxiosとVuexを使ったサーバ通信からデータ表示までの流れ

はじめに

今回はNuxt.jsでAxiosやVuexを使う方法について解説していきます。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

データ取得はVuexでいうActiosで行いますが、Vuexについてはまた後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)という変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

<script>
import Top from '@components/Top'

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

Nuxt.jsで、AxiosとVuexってどう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れについて解説します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)という変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

<script>
import Top from '@components/Top'

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

AxiosとVuexってどう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れを紹介します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

<script>
import Top from '@components/Top'

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

AxiosとVuexって実際どう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れを紹介します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

<script>
import Top from '@components/Top'

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

Docker環境でvite+Nginxを動作させた時に詰まったポイント

TL;DR

vite + Nginxの構成をDokcer上で作成したいと思ったんですが、
一部詰まったポイントがあったので備忘録的に残しておきます。

なおライブラリのソースを直接弄ってごまかしているので、
基本的には公式のアップデートを待つのが吉かと思います。

viteについて

公式リポジトリ
viteはVue.jsの作者のEvan You氏が作成したビルドツールです。

Vue-cliでは諸々のバンドルツールをいれてましたがviteは不要なため、
devサーバーが高速に動作するのが一番の特徴かと思います。
マジで早い。

ちなみにビルドにはRollupを利用しているようです。

Docker環境で動かす

さて今回の本題に入りたいと思います。
Dokcer環境上にNginx+viteの環境を構築するために準備をします。
HOST(8080) => Nginx(80) => vite(3000)で通信をする想定です。

※Docker環境の構築そのものの説明は割愛します。

最終的なディレクトリ構成は以下のような形になります。

.
├── config
│   └── nginx
│       └── dev.conf
├── docker-compose.yml
├── index.html
├── logs
│   └── nginx
│       └── error.log
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── index.css
│   └── main.js
└── yarn.lock

現時点でのdocker-compose.ymlとNginxの設定ファイルは以下のような状態。

docker-compose.yml
version: "3"
services:
  vite:
    image: node:12.6.0
    container_name: vite
    working_dir: /var/local/app
    volumes:
      - .:/var/local/app:cached
    environment:
      - HOST=0.0.0.0
    command: /bin/sh -c "yarn cache clean && yarn install && yarn dev"

  proxy_nginx_vite:
    image: nginx:1.19.1
    volumes:
      - ./config/nginx/dev.conf:/etc/nginx/nginx.conf:cached
      - ./logs/nginx:/var/log/nginx:cached
    container_name: proxy_nginx_vite
    ports:
      - 8080:80
    depends_on:
      - vite
dev.conf
error_log       /var/log/nginx/error.log;

events{
}

http {
    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://vite:3000/;
            proxy_intercept_errors on;
        }
    }
}

Docker環境の立ち上げ(1回目)

環境の準備をしたら以下のコマンドで立ち上げます。

docker-compose -f docker-compose.yml up --build

この時点では以下のエラーが出て表示されません。

wa-error-01.png

NginxでWebSocketを使うには設定が必要だったので早々にNginxのconfファイルを修正します。

NginxでWebsocket通信を使うための修正

以下のあたりを参考に修正します。

NginxのリバースプロキシでWebソケットを通す際の設定

以下をconfファイルに追加します。

dev.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

~~中略~~

proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade; 
proxy_set_header Connection $connection_upgrade;

この時点のNginxのconfファイル

dev.conf
error_log       /var/log/nginx/error.log;

events{
}

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://vite:3000/;
            proxy_intercept_errors on;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade; 
            proxy_set_header Connection $connection_upgrade;
        }
    }
}

Docker環境の立ち上げ(2回目)

ここでdokcerを立ち上げ直します。

docker-compose -f docker-compose.yml up --build

先程のエラーは解消されて表示自体はされますが、繰り返しページのリフレッシュが走ります。
consoleを見ると以下のエラーが出ています。

ws-error02.png

先程のエラーとは異なっているようですがこれもWebSocket関連のようです。

viteのWebSocket通信のPORTの向き先を変更する

hostからはポート8080でDockerコンテナに接続しているんですが、
vite自体はポート3000で起動しているもんだから、
WebSocket通信でlocalhost:3000を見に行こうとしてつながらない=>繰り返しリフレッシュされている模様。

↓此処から先は自己責任↓

ライブラリ内のファイルを直接弄って編集

node_modules/vite/dist/client/client.ts内のファイルの33行目でWebsocket通信に使うURLを生成しているんですが、ここで引いてきているPORTが3000のままのためおきているようです。

viteのリポジトリにもISSUEが立っていたので参考に無理やりつながるようにしてみます。

WebSocket connection can not work inside Docker container
公式リポジトリのコードの該当箇所

// dockerコンテナ内でコマンドを実行
docker exec -it vite bash

// vimがなければインストール
apt-get update
apt-get install vim

// 以下のファイルを編集
vi node_modules/vite/dist/client/client.js

// before
const socketUrl = `${socketProtocol}://${location.hostname}:${__PORT__}`;
↓↓↓以下に変更↓↓↓
// after
// HOSTから見に行く先のPORT番号を具体的に指定
const socketUrl = `${socketProtocol}://${location.hostname}:8080`;

Docker環境の立ち上げ(3回目)

もっかい再起動

docker-compose -f docker-compose.yml up --build

スクリーンショット 2020-09-06 9.48.45.png

リフレッシュもせずに無事動きました。

今後どうなっていきそうか

正直この状態で動かすのは好ましくありません。

ただ先程のISSUEにぶら下がっているPR見てみると
WebSocket用のconfigが追加されそうな気配を感じます。

feat(dev): add config for websocket connection

どう反映されるかわかりませんが、以下はこんな感じになりそうかな?という想像です。

vite.config.js
module.exports = {
  socketPort: 8080
}

結論

とりあえず動作するとこまでは行きましたが、
さすがにこのままつかっていくのは厳しいなとは思っています。
今回のような構成にしなければこういう問題は発生しないんですけどね、、、。

まだ絶賛開発中なところもあるので、色々と手間をかける部分は必要そうです。

趣味や個人で使う分にはローカルでのサーバー起動も早くすごく快適ですので、
Vue使っている人は是非試してみてください!

参考

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

NUXTJS公式:S3 と CloudFront を使用して AWS へデプロイするには?の補足

はじめに

※画像多めです
公式ドキュメント内のAWS: S3 バケットと CloudFront Distribution の設定の参考記事が古く、そのままの手順では動かなかったので、画像多めで手順をまとめたいと思います。

(公式ドキュメント)S3 と CloudFront を使用して AWS へデプロイするには?
(参考記事)S3 と CloudFront をセットアップするためのチュートリアル

(次の記事でデプロイをdocker-composeコマンド1つでできるようにカスタマイズする記事を書く予定です。)

前提

  • AWSアカウントを作成済み
  • Route53に作成予定のドメインを設定済み
  • サブドメインを作成して公開する

AWSの環境準備

S3、CloudFront、Route53、Certificate Managerを使用します。

1. S3バケットを作成する

バケット名は任意の文字列を入力します。
今回はcloudFrontを使用し、S3自体を公開するわけではないので公開するドメインと同じである必要はありません。
例としてs3-devというバケットを作成します。

image.png

デフォルトのまま次へ。

image.png

パブリックアクセスもデフォルト(全てブロック)。

image.png

内容を確認し、問題なければバケットを作成します。

image.png

作成が完了したら動作確認用に適当なファイルをアップロードしておきます。

image.png

2. AWS Certificate ManagerでCDNドメインのSSL証明書を取得する

SSL証明書は、リクエストから発行まで時間がかかるので、こちらを優先で実施します。

リージョンUS-East-1のAWS Certificate Managerコンソールを開き、証明書のリクエストを実施します。

image.png

パブリック証明書のリクエストを選択し、証明書のリクエストを実施します。

image.png

公開するドメイン名を入力します。
今回はRoute53に所持しているテスト用ドメインsudev.worktestというサブドメインを指定して使用します。
test.sudev.work

image.png

今回使用するドメインはRoute53で管理するため、DNSの検証を選択します。

image.png

タグの指定は特にしないのでそのまま次へ。

image.png

内容を確認し、問題なければ確定とリクエストを実施。

image.png

この時点ではまだRoute53でサブドメインを作成していないので、検証保留になっています。
そのまま続行を押下します。

image.png

赤枠の部分を押下し、Route53にレコードを作成します。
画像の通り成功と出たら完了です。
image.png

※作成完了までに時間がかかるので、完了後にCloudFrontセットアップにすすむ

3. CloudFrontをセットアップ

Create Distributionを押下し、次画面でWebのGet Startedを押下します。

image.png

Origin Settings

  • Origin Domain Name:S3のバケット名を入力し、プルダウンで選択
  • Origin Path:空白
  • Restrict Bucket Access:YES
  • Origin Access Identity:Create a New Identity
  • Grant Read Permissions on Bucket:Yes, Update Bucket Policy

Default Cache Behavior Settings

  • Viewer Protocol Policy:Redirect HTTP to HTTPS
  • Compress Objects Automatically:オブジェクトを自動圧縮する場合はYes
  • Grant Read Permissions on Bucket:Yes, Update Bucket Policy

Distribution Settings

  • Alternate Domain Names:test.sudev.work
  • SSL Certificate:Custom SSL Certificate (example.com):
    • プルダウンで先ほど作成したSSL証明書test.sudev.workを選択
    • ※作成までに時間がかかるので、出てこない場合は時間をおいて再度実施
  • Default Root Object:index.html

他はデフォルトでCreateを押下し、作成します。

S3でアップロードしたテストファイルがCloudFrontのドメイン/ファイル名でで表示されれば完了です。

image.png

次の手順でRoute53の設定でCloudFrontのドメインをRoute53のサブドメインと紐づけます。
赤枠のドメインを控えておいてください。
image.png

4. Route53でCloudFrontを指定のドメインで公開する

Route53 > ホストゾーン > 対象ドメイン(sudev.work) > レコード作成 >
シンプルルーティング > シンプルなレコードを定義

  • レコード名:公開するドメインtest.sudev.work
  • レコードタイプ:CNAME
  • ルーティング先:レコードタイプに応じたIPアドレスまたは別の値
    • 値:先ほどメモ控えたCloudFrontのドメイン

image.png

接続確認

S3でアップロードしたテストファイルがhttpsでRoute53のドメイン:test.sudev.work/ファイル名でで表示されれば完了です。

image.png

手順は以上になります。

補足

サブドメインで公開したURLでF5を押すとエラーになるので、下記を参考に対策をしたほうが良いと思います。
https://qiita.com/Safire/items/505cdf9b981c58ad38f6

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