ラベル nuxt の投稿を表示しています。 すべての投稿を表示
ラベル nuxt の投稿を表示しています。 すべての投稿を表示

2019年11月23日土曜日

NuxtでFirestoreを利用してデータを保存する

Firestoreからデータを取得して表示するサンプルを作成します。
Jest でテストもします。

完成したプロジェクトは以下のリポジトリになります。
https://github.com/TAC/nuxt-firestore-example

Firestoreに接続する

まずはFirestoreへ接続します。
Firebaseのアカウント作成などは他のサイトなどで確認して下さい。
接続情報は.env/development.jsに記載して、nuxt.config.tsで読み込むようにします。

.env/development.js

module.exports = {
  apiKey: "<your apiKey>",
  authDomain: "<your authDomain>",
  databaseURL: "<your databaseURL>",
  projectId: "<your projectId>",
  storageBucket: "<your storageBucket>",
  messagingSenderId: "<your api messagingSenderId>"
}

app/nuxt.config.ts

import NuxtConfiguration from '@nuxt/config'
import pkg from './package.json'
import envSet from '../.env/environment.js'

const nuxtConfig: NuxtConfiguration = {
  mode: 'universal',
  srcDir: 'src',

  env: envSet,
  ...
}

設定値はprocess.env.apiKeyという形式で利用できます。
次にFirebaseFirestore の初期設定を行うプラグインを作成します。

app/src/plugins/firebase.ts

import firebase from 'firebase/app'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: process.env.apiKey,
    authDomain: process.env.authDomain,
    databaseURL: process.env.databaseURL,
    projectId: process.env.projectId,
    storageBucket: process.env.storageBucket,
    messagingSenderId: process.env.messagingSenderId
  })
}

export default firebase

ここまでは 前回のAuthenticationの実装 でやりました。
Firestore へ接続するプラグインは以下のように記述します。

app/src/plugins/firestore.ts

import firebase from '@/plugins/firebase'
import 'firebase/firestore'

const db = firebase.firestore()

export default db

修正箇所

ユーザのニックネームを Firestore へ保存します。
users コレクションを作成して、ドキュメントIDには AuthenticationユーザーUID を利用します。
認証データをそのまま保存していた store/models/users.ts を変更していきます。

store/models/users.ts

import {
  VuexModule,
  Module,
  getter,
  mutation,
  action,
  getRawActionContext
} from 'vuex-class-component'
import firebase from '@/plugins/firebase'
import db from '@/plugins/firestore'
import Users from '@/types/store/models/users'

const timestamp = firebase.firestore.FieldValue.serverTimestamp()

@Module({ namespacedPath: 'models/users/', target: 'nuxt' })
class Store extends VuexModule {
  @getter user: Users = {}

  get isAuthenticated() {
    return !!this.user.id
  }

  @mutation
  public SET_USER(payload: Users) {
    this.user = payload
  }

  @mutation
  public UNSET_USER() {
    this.user = {}
  }

  @action({ mode: 'raw' })
  public async set(uid: string) {
    if (uid) {
      const ref = db.collection('users').doc(uid)
      const doc = await ref.get()
      if (doc.exists) {
        const data: Users = {
          id: doc.id,
          nickName: doc.data()!.nickName,
          createdAt: doc.data()!.createdAt,
          updatedAt: doc.data()!.updatedAt
        }
        const context = getRawActionContext(this)
        context.commit('SET_USER', data)
      }
    }
  }

  @action({ mode: 'raw' })
  public async unset() {
    const context = getRawActionContext(this)
    context.commit('UNSET_USER')
  }

  @action({ mode: 'raw' })
  public async create(uid: string) {
    if (uid) {
      const ref = db.collection('users').doc(uid)
      const doc = await ref.get()
      if (!doc.exists) {
        const nickName = ''
        await ref.set({
          nickName,
          createdAt: timestamp,
          updatedAt: timestamp
        })

        const data: Users = {
          id: uid,
          nickName
        }
        const context = getRawActionContext(this)
        context.commit('SET_USER', data)
      }
    }
  }

  @action({ mode: 'raw' })
  public async update(user: Users) {
    const ref = db.collection('users').doc(user.id)
    const doc = await ref.get()
    if (doc.exists) {
      await ref.update({
        nickName: user.nickName,
        updatedAt: timestamp
      })
      const context = getRawActionContext(this)
      context.commit('SET_USER', user)
    }
  }
}

export default Store.ExtractVuexModule(Store)

以前は mutation しかありませんでしたが、 action を追加して、そこでユーザデータの登録・作成を行うようにしています。

認証のサインイン時の処理も修正します。

store/modules/auth.ts

  @action({ mode: 'raw' })
  public async signIn(provider) {
    const context = getRawActionContext(this)
    context.commit('models/users/UNSET_USER', null, { root: true })
    await firebase
      .auth()
      .signInWithPopup(provider)
      .then(result => {
        if (result && result.user && result.additionalUserInfo) {
          if (result.additionalUserInfo.isNewUser) {
            // new user
            context.dispatch('models/users/create', result.user.uid, {
              root: true
            })
          } else {
            // exists user
            context.dispatch('models/users/set', result.user.uid, {
              root: true
            })
          }
        }
      })
      .catch(error => {
        console.error(error)
        throw error
      })
  }

result.additionalUserInfo.isNewUser をみて新規ユーザかどうかを判定して、users コレクションへの登録処理を実行しています。
一度認証してしまっていると上記の値が false になってしまうので、動作確認する場合は新しいアカウントか Firebase Console で認証情報を削除すると users コレクションが登録されると思います。

テストの作成

認証処理の時にも使った firebase-mock を使ってテストケースを作っていきます。

app/tests/store/models/users.test.js

import db from '@/plugins/firestore'
...
describe('store/models/users.ts', () => {
  beforeAll(() => {
    db.autoFlush()
  })

  beforeEach(async () => {
    // mock data
    user = {
      nickName: 'alice',
      createdAt: '2019-10-26 00:00:00',
      updatedAt: '2019-10-26 00:00:00'
    }

    await db
      .collection('users')
      .doc('alice')
      .set(user)
  })

plugins/firestoreimport を追加してます。
前回の設定firestore の呼び出しはモックになっています。
beforeAll でFirestoreへの処理がすぐ反映されるように autoFlush() を実行しておきます。
beforeEach でモックデータを登録しています。
モックなので実際にFirestoreには登録されません。

app/tests/store/models/users.test.js

  describe('actions', () => {
    test('set', async () => {
      await store.dispatch('models/users/set', 'alice')
      expect(store.getters['models/users/user']).toMatchObject(user)
    })

    test('unset', async () => {
      await store.dispatch('models/users/unset')
      expect(store.getters['models/users/user']).toMatchObject({})
    })

    test('create', async () => {
      await store.dispatch('models/users/create', 'bob')
      const checkData = {
        id: 'bob',
        nickName: ''
      }
      expect(store.getters['models/users/user']).toMatchObject(checkData)
    })

    test('update', async () => {
      const updateData = {
        id: 'alice',
        nickName: 'test'
      }
      await store.dispatch('models/users/update', updateData)
      const data = store.getters['models/users/user']
      expect(data).toMatchObject(updateData)
      expect(data.updatedAt).not.toBe(user.updatedAt)
    })
  })

追加した action メソッドのテストを作成します。
なお、gettersmutation は動作を変えていないので、修正しなくても正常にテストを通過するはずです。

以上、簡単にですが Firestore への接続とテスト方法の紹介でした!


Written with StackEdit.

2019年10月9日水曜日

Nuxt.js v2.8.1 release note 和訳

※翻訳勉強中のため、間違っている可能性があります。


https://github.com/nuxt/nuxt.js/releases/tag/v2.8.1

🐛 Bug Fixes

バグ修正

  • vue-renderer
    • #5867 Fix exception on property access of undefined object
    • #5863 Prevent “can’t set headers after they are sent” error in modern server mode
  • vue-app
  • #5864 Reduce consola direct access and don’t override browser console.log
  • cli
    • #5865 Don’t mutate options export to prevent ESM regression with nuxt.config in mixed (cjs + esm) mode (TIP: never mix them as is non-standard and may be unsupported in any future version)

  • vue-renderer
    • #5867 未定義オブジェクトのプロパティアクセスの例外を修正
    • #5863 最新のサーバーモードで「送信後にヘッダーを設定できない」エラーを防止
  • vue-app
    • #5864 consolaの直接アクセスを減らし、ブラウザーの console.log をオーバーライドしないようにした
  • cli
    • #5865 mixed (cjs + esm) モードで nuxt.config を使用したESM回帰を防止するためにオプションのエクスポートを変更しないでください (TIP: 非標準であるため、これらを混合しないでください。将来のバージョンではサポートされない可能性があります)

💅 Refactors

リファクタ

  • typescript
    • #5854 Prepare for external typescript support

  • typescript
    • #5854 外部 Typescript サポートを準備する

Written with StackEdit.

2019年9月29日日曜日

Nuxtでcore-jsのエラーの解消

以前、こちらの記事
以下のエラーに対応するために core-js@2.6.7 を別途追加する
旨の記載をしましたが、対応が間違っていたようです。

こちらのリポジトリを修正してあります。
https://github.com/TAC/nuxt-firebase-auth-example

These dependencies were not found:                                                                                                                                      friendly-errors 16:19:36
                                                                                                                                                                        friendly-errors 16:19:36
* core-js/modules/es6.array.find in ./.nuxt/client.js                                                                                                                   friendly-errors 16:19:36
* core-js/modules/es6.array.iterator in ./.nuxt/client.js                                                                                                               friendly-errors 16:19:36
* core-js/modules/es6.date.to-string in ./.nuxt/utils.js, ./src/helpers/cookies.ts                                                                                      friendly-errors 16:19:36
* core-js/modules/es6.function.name in ./.nuxt/client.js, ./src/store/models/users.ts                                                                                   friendly-errors 16:19:36
* core-js/modules/es6.object.assign in ./.nuxt/client.js                                                                                                                friendly-errors 16:19:36
* core-js/modules/es6.object.keys in ./.nuxt/index.js                                                                                                                   friendly-errors 16:19:36
* core-js/modules/es6.object.to-string in ./.nuxt/router.scrollBehavior.js, ./.nuxt/components/nuxt-link.client.js and 2 others                                         friendly-errors 16:19:36
* core-js/modules/es6.promise in ./.nuxt/client.js                                                                                                                      friendly-errors 16:19:36
* core-js/modules/es6.regexp.constructor in ./.nuxt/utils.js                                                                                                            friendly-errors 16:19:36
* core-js/modules/es6.regexp.match in ./.nuxt/client.js                                                                                                                 friendly-errors 16:19:36
* core-js/modules/es6.regexp.replace in ./.nuxt/store.js, ./.nuxt/components/nuxt.js                                                                                    friendly-errors 16:19:36
* core-js/modules/es6.regexp.search in ./.nuxt/utils.js                                                                                                                 friendly-errors 16:19:36
* core-js/modules/es6.regexp.split in ./.nuxt/store.js, ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./.nuxt/components/nuxt-build-indicator.vue?vue&type=script&lang=js&
* core-js/modules/es6.regexp.to-string in ./.nuxt/utils.js, ./src/helpers/cookies.ts                                                                                    friendly-errors 16:19:36
* core-js/modules/es6.string.includes in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js and 1 other                                                          friendly-errors 16:19:36
* core-js/modules/es6.string.iterator in ./.nuxt/App.js                                                                                                                 friendly-errors 16:19:36
* core-js/modules/es6.string.repeat in ./.nuxt/utils.js                                                                                                                 friendly-errors 16:19:36
* core-js/modules/es6.string.starts-with in ./.nuxt/utils.js, ./src/helpers/cookies.ts                                                                                  friendly-errors 16:19:36
* core-js/modules/es6.symbol in ./.nuxt/store.js, ./.nuxt/components/nuxt-link.client.js                                                                                friendly-errors 16:19:36
* core-js/modules/es7.array.includes in ./.nuxt/store.js, ./.nuxt/components/nuxt-link.client.js and 1 other                                                            friendly-errors 16:19:36
* core-js/modules/es7.object.get-own-property-descriptors in ./.nuxt/utils.js                                                                                           friendly-errors 16:19:36
* core-js/modules/es7.promise.finally in ./.nuxt/client.js                                                                                                              friendly-errors 16:19:36
* core-js/modules/es7.symbol.async-iterator in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                friendly-errors 16:19:36
* core-js/modules/web.dom.iterable in ./.nuxt/App.js, ./.nuxt/components/nuxt-link.client.js

確かに core-js@2.6.7 を追加することでエラーは解消し、正常に動作するようになりますが、どうやら yarn.lock を削除して node_modules を構築し直すとエラーは解消するようでした。
バージョンアップによって依存関係が正しく変更されなかった影響のようです。

参考サイト

Nuxt 2.5.0 にバージョンアップしたら core-js のエラーが出るようになった
core-js problems with 2.5.0


Written with StackEdit.

2019年9月25日水曜日

`vuex-class-component` のバージョンアップにはまった

NuxtでFirebaseのAuthenticationを使った認証を行う」でつかったプロジェクトのモジュールをアップグレードした際に vuex-class-component がバージョンアップしていたことではまったので、その内容と解決方法を記録しておきます。

修正内容は以下のリポジトリに反映してあります。
https://github.com/TAC/nuxt-firebase-auth-example

Nuxtを2.9.2にバージョンアップ

Nuxt を 2.4.0 から 2.9.2 にバージョンアップする」でやった手順をもとにバージョンアップしたところ、以下のエラーが発生するようになりました。

ERROR  [Vue warn]: Error in render: "TypeError: Cannot redefine property: user"

当初はどこに問題があるのかわからなかったのですが、調べていくうちに vuex-class-component1.6.0 から 2.0.4 へ大きくバージョンアップして、使用方法が変わっていることにたどりつきました。
さらに調べた結果、バージョンアップの影響で stategetter を付与する方法だと、上記のエラーが出るようになってしまっていたので、該当箇所は以下のように変更しました。

app/src/store/models/users.ts

- @getter user: User | null = null
+ private user: User | null = null

+ get get() {
+   return this.user
+ }

app/src/pages/index.vue

export default class extends Vue {
- @Users.Getter('user') user
+ @Users.Getter('get') user

[追記:2019-10-26]

上記の対応方法を間違えていたようです。
初期値にnullundefineを設定していた場合に出るエラーのようでした。
以下で訂正します。

- @getter user: User | null = null
+ @getter user: User = {}

  get isAuthenticated() {
-   return !!this.user
+   return !!this.user.uid
  }

  @mutation
  public UNSET_USER() {
-   this.user = null
+   this.user = {}
  }

さらなるエラー

これでビルドは通ったのですが、Sign-In Google のボタンを押下したときに以下のエラーが出るようになりました。

[vuex] unknown local action type: modules/signIn, global type: modules/auth/modules/signIn

どうやらモジュールモードでのパスの設定がうまく行っていないようでした。
該当箇所を以下のように修正することで対応できました。

app/src/store/modules/auth.ts

- @action()
+ @action({ mode: 'raw' })
  public async signInGoogle() {
+   const context = getRawActionContext(this)
    const provider = new firebase.auth.GoogleAuthProvider()
-   await this.signIn(provider)
+   await context.dispatch('signIn', provider)
  }

Written with StackEdit.

2019年9月24日火曜日

Nuxt.js v2.8.0 release note 和訳

※翻訳勉強中のため、間違っている可能性があります。


https://github.com/nuxt/nuxt.js/releases/tag/v2.8.0

😎 Developer Experience

開発者の体験

  • #5770 Add a group for SSR logs to avoid polluting the browser console
  • #5810 Fancier browser logs with consola
  • #5820, #5832, #5834 Show build indicator in the browser when rebuilding code:
  • #5753 Watch for pages/ creation when default page displayed
  • #5812 Only listen for file changes for supported extensions
  • #5753 Re-use the same port when randomly assigned when restarting in dev mode

  • #5770 ブラウザコンソールを汚染しないように、SSRログのグループを追加します。
  • #5810 consolaを使用した派手なブラウザーログ
  • #5820, #5832, #5834 コードを再構築するときに、ブラウザにビルドインジケーターを表示します。
  • #5753 デフォルトのページが表示されたら、 pages/ の作成に注意してください。
  • #5812 サポートされている拡張子のファイル変更のみをリッスンします。
  • #5753 開発モードで再起動するときにランダムに割り当てられた場合、同じポートを再使用します。

🐛 Bug Fixes

バグ修正

  • vue-renderer
    • #5807 Add User-Agent‍ to Vary header in modern server mode
    • #5764 Safe format SSR logs
  • server
    • #5793 Preserve random port when restarting
    • Return listener when calling listen
  • builder
    • #5753 Watch for pages/ creation when default page displayed
    • #2812 Only listen for file changes for supported extensions
  • generator
    • #5791 Minify spa fallback
  • types
    • #5785 Add type definition for functional babel.presets
  • vue-app
    • #5757 Reuse page component with watchQuery
    • #5746 Remove trailing slash in vue-router non-strict mode
    • #5752 Don’t attach catch handler to already loaded component
    • #5824 fixPrepatch in-out transition fix (issue #5797)
  • utils
    • #5754 Handle serializeFunction edge case

  • vue-renderer
    • #5807 最新のサーバーモードで User-Agent‍Vary ヘッダーに追加
    • #5764 安全な形式のSSRログ
  • server
    • #5793 再起動時にランダムなポートを保持
    • listenを呼び出すときにリスナーを返す
  • builder
    • #5753 デフォルトのページが表示されたときに pages/ の作成を監視する
    • #2812 サポートされている拡張子のファイル変更のみをリッスンします
  • generator
    • #5791 SPAのフォールバックを縮小
  • types
    • #5785 機能的な babel.presets のタイプ定義を追加
  • vue-app
    • #5757 watchQueryでページコンポーネントを再利用します
    • #5746 vue-router non-strictモードで末尾のスラッシュを削除
    • #5752 既に読み込まれているコンポーネントにキャッチハンドラーをアタッチしないでください
    • #5824 fixPrepatchインアウト遷移の修正 (issue #5797)
  • utils
    • #5754 serializeFunction エッジケースの処理

🚀 Features

機能

  • vue-renderer
    • #5745 Add render.injectScripts option
    • #5784 Support render.ssrLog for controlling SSR logs

  • #5745 render.injectScripts オプションを追加
  • #5784 SSRログを制御するための render.ssrLog のサポート

💅 Refactors

リファクタ

  • core
    • #5796 Use require.resolve instead of Module internals
  • builder
    • #5792 Pass nuxt options to template as nuxtOptions
  • vue-app
    • #5770 Add a group for SSR logs
    • #5826 simplify mount error log
  • general
    • #5748 Small readability improvements

  • core
    • #5796 Module 内部の代わりに require.resolve を使用
  • builder
    • #5792 nuxtオプションを nuxtOptions としてテンプレートに渡します
  • vue-app
    • #5770 SSRログのグループを追加します
    • #5826 マウントエラーログを簡素化
  • general
    • #5748 小さな読みやすさの改善

📝 Examples

事例

  • auth-jwt
    • #5775 Use named store export to prevent warning
  • typescript
    • #5742 Add missing ts-node dependency

  • auth-jwt
    • #5775 警告を防止するために名前付きストアのエクスポート(※)を使用する
  • typescript
    • #5742 欠落しているts-node依存関係を追加

(※) Vuexモジュールモードのことと思われる


🏡 Chore

雑用

  • ci
    • #5802 Upload test report to CircleCI and Azure
    • Add flags for codecov
    • Enable audit
  • general
    • Remove unused dependency cross-env
    • Add FUNDING.md
    • Improve links in readme

  • ci
    • #5802 テストレポートをCircleCIおよびAzureにアップロードする
    • codecovのフラグを追加
    • 監査を有効にする
  • general
    • 未使用の依存関係 cross-env を削除します
    • FUNDING.md を追加します
    • readme のリンクを改善する

♻️ Tests

テスト

  • general
    • #5790 Add unit tests for core/resolver.js
    • #5782 Remove duplicate unit tests in packages/core/test/resolver

  • general
    • #5790 core/resolver.js の単体テストを追加
    • #5782 packages/core/test/resolver の重複した単体テストを削除します

Written with StackEdit.

2019年8月28日水曜日

NuxtでFirebaseのAuthenticationを使った認証を行う

FirebaseAuthenticationを使用しての認証機能の実装方法の紹介になります。

完成したプロジェクトは以下のリポジトリになります。
https://github.com/TAC/nuxt-firebase-auth-example

Firebaseの設定

最初にFirebaseの設定を行います。
Firebaseのアカウント作成や設定などは他のサイトなどで確認して下さい。
接続情報は.env/development.jsに記載してnuxt.config.tsで読み込むようにします。
これはリポジトリに入れていないので、上記リポジトリを clone した場合は自分で作成してください。

.env/development.js

module.exports = {
  apiKey: "<your apiKey>",
  authDomain: "<your authDomain>",
  databaseURL: "<your databaseURL>",
  projectId: "<your projectId>",
  storageBucket: "<your storageBucket>",
  messagingSenderId: "<your api messagingSenderId>"
}

app/nuxt.config.ts

import NuxtConfiguration from '@nuxt/config'
import pkg from './package.json'
import envSet from '../.env/environment.js'

const nuxtConfig: NuxtConfiguration = {
  mode: 'universal',
  srcDir: 'src',

  env: envSet,
  ...
}

設定値はprocess.env.apiKeyという形式で利用できます。

次にFirebase の初期設定を行うpluginsを作成します。

app/src/plugins/firebase.js

import firebase from 'firebase/app'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: process.env.apiKey,
    authDomain: process.env.authDomain,
    databaseURL: process.env.databaseURL,
    projectId: process.env.projectId,
    storageBucket: process.env.storageBucket,
    messagingSenderId: process.env.messagingSenderId
  })
}

export default firebase

FirebaseSDKを使用する箇所でこのプラグインを読み込んで使うことになります。

認証モジュール

Vuex.Store に認証モジュールを作成します。

app/src/store/modules/auth.ts

import {
  VuexModule,
  Module,
  action,
  getRawActionContext
} from 'vuex-class-component'
import firebase from '@/plugins/firebase'
import 'firebase/auth'

@Module({ namespacedPath: 'modules/auth/', target: 'nuxt' })
class Store extends VuexModule {
  @action({ mode: 'raw' })
  public async signIn(provider) {
    const context = getRawActionContext(this)
    context.commit('models/users/UNSET_USER', null, { root: true })
    await firebase
      .auth()
      .signInWithPopup(provider)
      .then(result => {
        if (result.user) {
          context.commit('models/users/SET_USER', result.user, { root: true })
        }
      })
      .catch(error => {
        console.error(error)
        throw error
      })
  }

  @action()
  public async signInGoogle() {
    const provider = new firebase.auth.GoogleAuthProvider()
    await this.signIn(provider)
  }

  @action({ mode: 'raw' })
  public async signOut() {
    const context = getRawActionContext(this)
    await firebase
      .auth()
      .signOut()
      .then(() => {
        context.commit('models/users/UNSET_USER', null, { root: true })
      })
  }

  @action({ mode: 'raw' })
  public isSignIn() {
    const context = getRawActionContext(this)
    return new Promise(resolve => {
      const unsubscribe = firebase.auth().onAuthStateChanged(user => {
        unsubscribe()
        if (user) {
          context.commit('models/users/SET_USER', user, { root: true })
        }
        resolve(user || false)
      })
    })
  }
}

export default Store.ExtractVuexModule(Store)

渡されたプロバイダーの認証を行う signIn アクションと認証データを削除する signOut アクション、サインイン状態の確認を行う isSignIn アクションがあります。
プロバイダーに firebase.auth.GoogleAuthProvider() を渡すとGoogleアカウントの認証ができます。
これを signInGoogle アクションとして作成しておきます。
他にもTwitterやFacebookが認証プロバイダーとして用意されています。
https://firebase.google.com/docs/auth/web/start#next_steps

認証データの保存

firebase.auth().onAuthStateChanged() で取得できるユーザデータを Vuex.Store に保存しておきます。
最初、ユーザデータをそのまま Vuex.Store に保存しようとしたのですが、エラーで動かなかったので修正しました。

参考記事 : Firebase AuthenticationとVuexの合わせ技バグでハマった

app/src/store/models/users.ts

import { VuexModule, Module, getter, mutation } from 'vuex-class-component'

interface User {
  [key: string]: any
}

@Module({ namespacedPath: 'models/users/', target: 'nuxt' })
class Store extends VuexModule {
  @getter user: User | null = null

  get isAuthenticated() {
    return !!this.user
  } 

  @mutation
  public SET_USER(payload) {
    this.user = {
      uid: payload.uid,
      displayName: payload.displayName,
      email: payload.email,
      emailVerified: payload.emailVerified,
      isAnonymous: payload.isAnonymous,
      phoneNumber: payload.phoneNumber,
      photoURL: payload.photoURL,
      providerData: payload.providerData
    }
  }

  @mutation
  public UNSET_USER() {
    this.user = null
  }
}

export default Store.ExtractVuexModule(Store)

認証のテスト

FirebaseSDKをモック化してくれるモジュールをインストールします。(※ただし、このモジュールは残念なことに現在メンテされていません。)

yarn add -D firebase-mock

セットアップ手順をもとに以下のファイルを作成します。

app/tests/__mocks__/plugins/firebase.js

import firebasemock from 'firebase-mock'

const mockauth = new firebasemock.MockAuthentication()
const mockdatabase = new firebasemock.MockFirebase()
const mockfirestore = new firebasemock.MockFirestore()
const mockstorage = new firebasemock.MockStorage()
const mockmessaging = new firebasemock.MockMessaging()

const mocksdk = new firebasemock.MockFirebaseSdk(
  // use null if your code does not use RTDB
  (path) => {
    return path ? mockdatabase.child(path) : mockdatabase
  },
  // use null if your code does not use AUTHENTICATION
  () => {
    return mockauth
  },
  // use null if your code does not use FIRESTORE
  () => {
    return mockfirestore
  },
  // use null if your code does not use STORAGE
  () => {
    return mockstorage
  },
  // use null if your code does not use MESSAGING
  () => {
    return mockmessaging
  }
)

export default mocksdk

@/plugins/firebase の読み込みがモックになるように jest.config.js で設定します。

app/jest.config.js

  moduleNameMapper: {
    '^vue$': 'vue/dist/vue.common.js',
+   '^@/plugins/firebase$': '<rootDir>/tests/__mocks__/plugins/firebase.js',
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/src/$1'
  },

これで Jest での実行時には @/plugins/firebase の読み込みがすべてモックになります。

次に store/modules/auth.ts のテストを書いていきます。

app/tests/store/modules/auth.test.js

import 'firebase/auth'
import Vuex from 'vuex'
import { cloneDeep } from 'lodash'
import { createLocalVue } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import firebase from '@/plugins/firebase'
import auth from '@/store/modules/auth.ts'
import users from '@/store/models/users.ts'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('store/modules/auth.ts', () => {
  let store
  let user

  beforeAll(() => {
    firebase.auth().autoFlush()
  })

  beforeEach(() => {
    // create store
    store = new Vuex.Store({
      modules: {
        'modules/auth': cloneDeep(auth),
        'models/users': cloneDeep(users)
      }
    })

    // mock data
    user = {
      uid: 'alice',
      email: 'alice@mail.com'
    }

    // initialize auth
    firebase
      .auth()
      .createUser(user)
      .then(user => {
        firebase.auth().changeAuthState(user)
      })
  })

  describe('actions', () => {
    test('isSignIn', async () => {
      await store.dispatch('modules/auth/isSignIn')
      const result = store.getters['models/users/user']
      expect(result).toMatchObject(user)
    })

    test('signOut', async () => {
      await store.dispatch('modules/auth/signOut')
      const result = store.getters['models/users/user']
      expect(result).toBeNull()
    })

    test('signInGoogle', async () => {
      firebase.auth().signInWithPopup = jest.fn(provider => {
        return new Promise(resolve => {
          user.providerData = [provider]
          resolve({
            user
          })
        })
      })

      await store.dispatch('modules/auth/signInGoogle')
      await flushPromises()

      const result = store.getters['models/users/user']
      expect(result.providerData[0]).toMatchObject({
        providerId: 'google.com'
      })
    })
  })
})

ポイントとしては以下の2点になります。

  • beforeEach() で初期化時に firebase.auth().autoFlush() を呼び出し、 firebase.auth().createUser() でモック用の認証データを作成しておく
  • firebase.auth().signInWithPopup をモック化する

firebase.auth().autoFlush() を呼び出すことで firebase-mock でモック化された firebase.auth() での変更が反映されるようになります。
また、firebase.auth().signInWithPopup はすでにモックになってますが、期待通りの動きはしてくれません。
なので、正しく動作する形に再度モック関数を設定し直しています。

認証ボタンの作成

最後に作成したアクションを呼び出すボタンを pages/index.vue に配置します。

app/src/pages/index.vue

<template lang="pug">
  section.container
    template(v-if='isAuthenticated')
      ButtonsAction(@action='signOut')
        template(v-slot:buttonText) SignOut
    template(v-else)
      ButtonsAction(@action='signInGoogle')
        template(v-slot:buttonText) Sign-In Google
</template>

<script lang="ts">
import { Component, Vue, namespace } from 'nuxt-property-decorator'
import ButtonsAction from '@/components/buttons/Action.vue'

const Users = namespace('models/users')
const Auth = namespace('modules/auth')

@Component({
  components: {
    ButtonsAction
  }
})
export default class extends Vue {
  @Users.Getter('user') user
  @Users.Getter('isAuthenticated') isAuthenticated
  @Auth.Action('signInGoogle') signInGoogle
  @Auth.Action('signOut') signOut
}
</script>

これで Sign-In Google のボタンが表示され、押すとGoogleアカウント認証のウィンドウがポップアップされると思います。
そのウィンドウで任意のアカウントを選択すると認証が完了します。(GoogleChromeで認証済みの場合はパスワードの入力がスキップされると思います。)
認証が完了するとボタンが SignOut に変わりユーザIDと名前が表示されるかと思います。

SSRに対応する

これでサインイン、サインアウトの実装は終わったのですが、サインインした状態でリロードするとボタンの表示やユーザ情報の表示がちらつくのに気づくと思います。

これはSSR時に認証情報が取得できないためです。
Firebase Authentication では認証情報を IndexedDB に保存しているため、IndexedDB にアクセスできないサーバ側の処理では認証情報を取得できないのです。

そこでサーバ側の処理でも認証情報を取得できるように cookie に認証トークンを保存するように変更します。

Cookieの保存

まずは以下のモジュールを追加します。

yarn add cookie-universal-nuxt jwt-decode core-js@2.6.7

cookie-universal-nuxt はフロント側でもサーバ側でも簡単に cookie にアクセスできるようにしてくれるモジュールです。$cookies をコンテキストに追加してくれます。

jwt-decode はJWTトークンをデコードしてくれるそのまんまのモジュールです。Firebase Authentication で使う認証トークンがJWTトークンなのでこれでデコードします。

core-js は後述するシリアライズ処理で必要になります。
(Nuxt.js 2.8.1 での問題かもしれませんが追加しないとエラーが出ます。)

[追記:2019-09-28]
上記の対応は正しくなかったようです。
詳しくはこちらの記事で説明しています。

次に cookie を操作するヘルパー関数を作成します。

app/src/helpers/cookies.ts

const _OPTIONS = {
  path: '/',
  maxAge: 60 * 60 * 24 * 7,
  secure: true
}

export function getCookie(cookies, name) {
  const data = deserialize(cookies.get('__session'))
  return data[name]
}

export function setCookie(cookies, name, value, isLocalhost) {
  const data = deserialize(cookies.get('__session'))
  data[name] = value
  _OPTIONS.secure = !isLocalhost
  cookies.set('__session', serialize(data), _OPTIONS)
}

export function removeCookie(cookies, name, isLocalhost) {
  const data = deserialize(cookies.get('__session'))
  delete data[name]
  _OPTIONS.secure = !isLocalhost
  cookies.set('__session', serialize(data), _OPTIONS)
}

function serialize(obj) {
  try {
    const str = JSON.stringify(obj, function replacer(k, v) {
      if (typeof v === 'function') {
        return v.toString()
      }
      return v
    })
    return str
  } catch (e) {
    return {}
  }
}

function deserialize(str) {
  try {
    const obj = JSON.parse(str, function reciever(k, v) {
      if (typeof v === 'string' && v.startsWith('function')) {
        return Function.call(this, 'return ' + v)()
      }
      return v
    })
    return obj
  } catch (e) {
    return {}
  }
}

注意点は __session という名前で保存する必要がある点です。Firebaseでホスティングする場合、これ以外の名前の cookie は削除されてしまいます。
https://firebase.google.com/docs/hosting/manage-cache#using_cookies

__session 以外の名前が使えないので、この中に連想配列をシリアライズして登録します。

認証トークンの保存

以下に記載する箇所で認証トークンの保存と読み込みを行います。

app/src/pages/index.vue

  private signInGoogle() {
    const that = this as any
    this.authSignInGoogle().then(() => {
      const currentUser = firebase.auth().currentUser
      if (currentUser) {
        currentUser.getIdToken().then(token => {
          setCookie(that.$cookies, 'token', token, that.$isLocalhost)
        })
      }
    })
  }

  private signOut() {
    const that = this as any
    this.authSignOut().then(() => {
      removeCookie(that.$cookies, 'token', that.$isLocalhost)
    })
  }

サインインした時に認証トークンを取得して保存、サインアウトした時に保存してある認証トークンを削除します。

app/src/plugins/auth.js

import firebase from '@/plugins/firebase'
import 'firebase/auth'
import { setCookie } from '@/helpers/cookies'

export default async function({ store, app }) {
  await store.dispatch('modules/auth/isSignIn')

  const currentUser = firebase.auth().currentUser
  if (currentUser) {
    currentUser.getIdToken().then(token => {
      setCookie(app.$cookies, 'token', token, app.$isLocalhost)
    })
  }
}

認証プラグインを作成して、リロード時に認証トークンが保存されるようにします。

app/src/store/index.ts

import jwtDecode from 'jwt-decode'
import { getCookie } from '@/helpers/cookies'

export const actions = {
  nuxtServerInit: ({ commit }, { app }) => {
    const token = getCookie(app.$cookies, 'token')
    if (token) {
      commit('models/users/SET_USER', jwtDecode(token))
    }
  }
}

Vuex.storenuxtServerInit 処理時に保存してある認証トークンからユーザデータを取得します。(今回はユーザデータをFirestoreなどから取得していないのでこのような形ですが、サーバ側からデータ取得する際には認証トークンの検証が必要になります。)

認証トークンから取得できるユーザデータが onAuthStateChanged とは若干違うのでユーザデータの保存処理も修正します。(ちょっと無理やりですが・・・)

app/src/store/models/users.ts

  @mutation
  public SET_USER(payload) {
    const uid = payload.uid !== undefined ? payload.uid : payload.user_id
    const displayName =
      payload.displayName !== undefined ? payload.displayName : payload.name
    const emailVerified =
      payload.emailVerified !== undefined
        ? payload.emailVerified
        : payload.email_verified
    const photoURL =
      payload.photoURL !== undefined ? payload.photoURL : payload.picture
    this.user = {
      uid,
      displayName,
      email: payload.email,
      emailVerified,
      isAnonymous: payload.isAnonymous,
      phoneNumber: payload.phoneNumber,
      photoURL,
      providerData: payload.providerData
    }
  }

これでサインインしたあとにリロードしてもボタン等がちらつかなくなったと思います。

まとめ

ざっくりとですが Firebase Authentication を使った認証の実装の一例を紹介しました。

Firebase は他にも便利な機能があるのでもっと紹介してきたいと思います!


Written with StackEdit.

2019年6月29日土曜日

Nuxt.js v2.7.1 release note 和訳

※翻訳勉強中のため、間違っている可能性があります。


https://github.com/nuxt/nuxt.js/releases/tag/v2.7.1

Fixes

修正

  • builder: use warn only for mismatched dependencies (#5723)
  • webpack: correctly resolve consola for the client bundle (#5729)

  • builder: 依存関係が一致しない場合にのみ警告を使用する (#5723)
  • webpack: クライアントバンドル時にconsolaを正しく解決する (#5729)

Dependency Upgrades

依存関係のアップグレード



Written with StackEdit.

2019年6月20日木曜日

Nuxt.js v2.7.0 release note 和訳

※翻訳勉強中のため、間違っている可能性があります。


https://github.com/nuxt/nuxt.js/releases/tag/v2.7.0

DX Improvements 💅

SSR logs in your browser 🖥️

SSRのログをブラウザへ出力


We all know the console.log debugging method, but when working with universal applications, you have to remember that sometimes, your logs are in your terminal and not in your browser console.

console.logというデバッグ方法がありますが、ユニバーサルアプリケーションを作成している場合、ログはブラウザコンソールではなくターミナルに出力されることがあるのを覚えておかなければなりません。


This can be really annoying when developing a Nuxt.js application, starting with this version and running nuxt dev, the ssr logs are now reported to your browser console:

このバージョンから始めて nuxt devを実行して、Nuxt.jsアプリケーションを開発するとき、これは本当に厄介なことになるかもしれません、SSRのログはブラウザのコンソールに表示されるようになりました。


Detecting store/ creation 👀

store/ ディレクトリの作成を検出


Nuxt.js now detects when you created a store/ directory and will reload himself auto-magically so you don’t have to restart it anymore.

Nuxt.jsは、あなたが store/ディレクトリを作成したことを検出し、自動的に自分自身を再ロードするので、もう再起動する必要はありません。


PS: We also improved the serverMiddleware watch to restart Nuxt.js and clean their cache

PS:Nuxt.jsを再起動してキャッシュを消去するために serverMiddlewareの監視を改良しました。

Fixes ✔️

修正

  • builder: corretly detect mode of hashed plugins (#5695)
  • builder: call watch:restart after watch:fileChanged hook (#5620)
  • utils: node v8 not support dotAll in regex (#5608)
  • vue-app: properly catch component loading error (#5687) (#5688) (#5690)
  • vue-app: consider watchQuery option in routerViewKey (#5516)
  • vue-renderer: await on spa:templateParams hook (#5619)
  • webpack: set log level to warn for HardSourcePlugin (#5653)
  • vue-app: properly catch loading error in component prefetching (#5688) (#5690)
  • vue-app: avoid using aliases in templates (#5656)
  • builder: watch store dir and serverMiddleware paths (#5681)

  • builder: ハッシュプラグインのモードを正しく検出する (#5695)
  • builder: watch:fileChangedフックの後にwatch:restartを呼び出す (#5620)
  • utils: ノードv8は正規表現でdotAllをサポートしていません (#5608)
  • vue-app: コンポーネントの読み込みエラーを正しく検出 (#5687) (#5688) (#5690)
  • vue-app: routerViewKeywatchQueryオプションを検討してください(#5516)
  • vue-renderer: spa:templateParamsフックを待ってください (#5619)
  • webpack: ログレベルを HardSourcePluginに警告するように設定します (#5653)
  • vue-app: コンポーネントのプリフェッチでロードエラーを適切に検出する (#5688) (#5690)
  • vue-app: テンプレートでエイリアスを使用しない (#5656)
  • builder: storeディレクトリと serverMiddlewareパスを監視する (#5681)

Features 🚀

特徴

  • vue-app: rename transition to pageTransition and deprecate it (#5558)
  • vue-renderer/vue-app: report SSR console logs to the browser with consola (#5673)
  • webpack: suppress not found typescript warnings (#5635)
  • webpack: extendable babel.presets and babel envName (#5637)
  • configurable aliases (#5655)

  • vue-app: transitionpageTransitionに改名して非推奨になりました (#5558)
  • vue-renderer/vue-app: SSRコンソールログをconsolaでブラウザに報告する (#5673)
  • webpack: 見つからないタイプスクリプトの警告を抑制 (#5635)
  • webpack: 拡張可能なbabel.presetsとバベルenvName(#5637)
  • 設定可能なエイリアス (#5655)

Refactors 🧹

リファクタリング

  • server: exclude dist files request from browser detection (#5571)
  • vue-renderer: remove chalk in renderer (#5609)
  • vue-renderer: split renderer into ssr, spa and modern (#5559)
  • move modern detection from server to utils (#5584)

  • server: ブラウザ検出からdistファイル要求を除外する(#5571)
  • vue-renderer: レンダラーのchalkの警告を削除 (#5609)
  • vue-renderer: レンダラーをssr、spa、modernに分割 (#5559)
  • 最新ブラウザの検出をサーバーからutilsに移動 (#5584)

Examples 📚

  • auth-routes: fix typo (#5651)
  • babel-preset-app: add core-js@3 example in the readme (#5633)
  • docker: fix Dockerfile casing (#5705)

  • auth-routes: typoを修正 (#5651)
  • babel-preset-app: readmeにcore-js@3の例を追加 (#5633)
  • docker: Dockerfileのスペルを修正 (#5705)

Typescript 👷

Only for typescript users, Nuxt.js v2.7 dropped support for node < 8.6 relate to ts-loader v6

TypeScriptユーザのみ、Nuxt.js v2.7ではts-loader v6に関連してNode v8.6未満のサポートを終了しました

  • add babel config types (#5666)
  • upgrade ts-loader to v6 (#5691)
  • fix context.app type (#5701)
  • fix extendRoutes method type (#5700)
  • prevent ts-node to register twice (#5699)

  • config typesbabelを追加 (#5666)
  • ts-loaderをv6にアップグレード (#5691)
  • context.appタイプを修正 (#5701)
  • extendRoutesメソッドの型を修正 (#5700)
  • ts-nodeが2回登録されるのを防ぎます (#5699)

Written with StackEdit.

2019年6月13日木曜日

JestでVuex.Storeのテストを行う

NuxtでTypeScriptを使うときのVeux.storeの設定方法で作成したプロジェクトに Jest でのユニットテストをつけていきます。

できあがったものは以下のリポジトリになります。
https://github.com/TAC/nuxt-vuex-typescript-jest-example

Jestの追加

Nuxt を新規作成時のオプションでテストフレームワークに Jest を選択すると自動で追加されますが、それらを手動で追加してみます。

yarn add -D jest vue-jest babel-jest ts-jest eslint-plugin-jest @vue/test-utils babel-core@^7.0.0-bridge.0

Babelのバージョンが7になっているのでbabel-core@^7.0.0-bridge.0 が必要になります。
Vuex.StoreTypeScriptなのでts-jestも追加します。

Jestの設定

jest.config.js.babelrc を以下の内容で追加します。

app/jest.config.ts

module.exports = {
  moduleNameMapper: {
    '^vue$': 'vue/dist/vue.common.js',
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1'
  },
  moduleFileExtensions: ['js', 'ts', 'vue'],
  transform: {
    '^.+\\.js$': 'babel-jest',
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.vue$': 'vue-jest'
  },
  collectCoverage: false,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue',
    '<rootDir>/store/**/*.ts'
  ]
}

.babelrc

{
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}

テストの作成

コンポーネントのテスト

まずはコンポーネントのテストから。

import Vuex from 'vuex'
import { cloneDeep } from 'lodash'
import { createLocalVue, mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
import counter from '@/store/modules/counter.ts'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/Counter.vue', () => {
  let wrapper
  const initValue = 10
  beforeEach(() => {
    // create store
    const store = new Vuex.Store({
      'modules': {
        'modules/counter': cloneDeep(counter)
      }
    })

    // initialize store
    store.commit('modules/counter/SET_VALUE', initValue)

    // mount Vue Component
    wrapper = mount(Counter, {
      store: store,
      localVue
    })
  })

  describe('template', () => {
    test('snapshot correctly', () => {
      expect(wrapper.element).toMatchSnapshot()
    })
  })

  describe('script', () => {
    test('get counter value correctly', () => {
      expect(wrapper.vm.counter).toBe(initValue)
    })
    test('increment button correctly', () => {
      wrapper.find('[name="increment"]').trigger('click')
      expect(wrapper.vm.counter).toBe(initValue + 1)
    })
    test('decrement button correctly', () => {
      wrapper.find('[name="decrement"]').trigger('click')
      expect(wrapper.vm.counter).toBe(initValue - 1)
    })
  })
})

@vue/test-utilscreateLocalVueを使ってローカルコピーを作成しておきます。
Vuex.Storeには使用するstoreの定義をmodulesの該当パスにcloneDeepでコピーしてあげます。
そうすることで他のテスト時に値が汚染されなくなります。
あとは好きなテストを記述していきます。
wrapper.find()にはCSSセレクターを指定できるので、テスト対象のエレメントなどが一意になるようにnameclassを設定しておくと探しやすくなります。

Veux.Storeのテスト

次はstoreのテストです。

import Vuex from 'vuex'
import { cloneDeep } from 'lodash'
import { createLocalVue } from '@vue/test-utils'
import counter from '@/store/modules/counter.ts'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('store/modules/counter.ts', () => {
  let store
  const initValue = 10
  beforeEach(() => {
    // create store
    store = new Vuex.Store({
      'modules': {
        'modules/counter': cloneDeep(counter)
      }
    })

    // initialize store
    store.commit('modules/counter/SET_VALUE', initValue)
  })

  describe('getters', () => {
    test('get value correctly', () => {
      expect(store.getters['modules/counter/value']).toBe(initValue)
    })
  })

  describe('actions', () => {
    test('increment correctly', () => {
      const addValue = 1
      store.dispatch('modules/counter/increment', addValue)
      expect(store.getters['modules/counter/value']).toBe(initValue + addValue)
    })
    test('decrement correctly', () => {
      const subValue = 1
      store.dispatch('modules/counter/decrement', subValue)
      expect(store.getters['modules/counter/value']).toBe(initValue - subValue)
    })
  })
})

ほぼコンポーネントと同じですが、作成したstoreを使用してテストしたいactionsなどを実行していく形になります。

package.jsonに以下のスクリプトを追加します。

    "test": "jest",
    "test:coverage": "jest --coverage",

これで準備が整ったので、yarn testを実行すると以下のような結果が得られると思います。

PS C:\work\repos\nuxt\jest\app> yarn test
yarn run v1.15.2
$ jest
$ C:\work\repos\nuxt\jest\app\node_modules\.bin\jest
 PASS  tests/store/modules/counter.test.js
 PASS  tests/components/Counter.test.js

Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   1 passed, 1 total
Time:        3.831s
Ran all test suites.
Done in 5.54s.

テストカバレッジも見たい時はtest:coverageを実行します。

PS C:\work\repos\nuxt\jest\app> yarn test:coverage
yarn run v1.15.2
$ jest --coverage
$ C:\work\repos\nuxt\jest\app\node_modules\.bin\jest --coverage
 PASS  tests/store/modules/counter.test.js
 PASS  tests/components/Counter.test.js
---------------|----------|----------|----------|----------|-------------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files      |    65.38 |      100 |     87.5 |    65.22 |                   |
 components    |    71.43 |      100 |      100 |    71.43 |                   |
  Counter.vue  |      100 |      100 |      100 |      100 |                   |
  Logo.vue     |        0 |      100 |      100 |        0 |             10,13 |
 pages         |        0 |      100 |      100 |        0 |                   |
  index.vue    |        0 |      100 |      100 |        0 |         8,9,10,18 |
 store         |        0 |      100 |        0 |        0 |                   |
  index.ts     |        0 |      100 |        0 |        0 |               1,3 |
 store/modules |      100 |      100 |      100 |      100 |                   |
  counter.ts   |      100 |      100 |      100 |      100 |                   |
---------------|----------|----------|----------|----------|-------------------|

Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   1 passed, 1 total
Time:        10.346s
Ran all test suites.
Done in 11.68s.

Written with StackEdit.

2019年6月4日火曜日

NuxtでTypeScriptを使うときのVeux.storeの設定方法

NuxtTypeScript で構築した時に Vuex.store の構成をどうしたらいいかを考えてみました。

結論としては vuex-class-component を使用してモジュールモードで構成する、という感じになりました。

完成したプロジェクトは以下のリポジトリになります。
https://github.com/TAC/nuxt-vuex-typescript-example

簡単なカウンターを作成しました。

環境

- version
OS Windows10
Node.js v8.15.0
Nuxt.js v2.7.1

どのモジュールがいいのか?

Vuex.store を簡潔にかけるようにするモジュールはいくつかあって、以下の2つが主に使われてるようでした。

vuex-class-component
https://github.com/michaelolof/vuex-class-component

vuex-module-decorators
https://github.com/championswimmer/vuex-module-decorators

Nuxt では store ディレクトリ以下にファイルを配置すれば自動で Vuex のモジュールモードで構築してくれます。

ただし、 store/index.ts が関数を返すとクラシックモードになり、その恩恵を受けることができません。
また、各モジュールのファイルも関数ではなくオブジェクトを返す必要があり、それができるのが vuex-class-component でした。
また、Nuxt.jsv3.0.0 ではクラシックモードが非対応になるようなので、それを考慮に入れての選択になります。

ディレクトリ構成

以下のような構成を考えてみました。

src/store/index.ts
  +- modules/
      +- counter.ts

modules に機能毎に分けたモジュールを作成していく感じです。
models とか作ってDBのテーブル毎などに分けたモジュールを作成してもいいかもしれない。

counter.ts

デコレータを使って以下のように記述します。

import {
  VuexModule,
  Module,
  action,
  getter,
  mutation
} from 'vuex-class-component'

interface Counter {
  value: number
}

@Module({ namespacedPath: 'modules/counter/', target: 'nuxt' })
export class CounterStore extends VuexModule implements Counter {
  @getter value = 0

  @mutation
  public SET_VALUE(value: number) {
    this.value = value
  }

  @action()
  public increment(value: number) {
    this.SET_VALUE(this.value + value)
  }

  @action()
  public decrement(value: number) {
    this.SET_VALUE(this.value - value)
  }
}

export default CounterStore.ExtractVuexModule(CounterStore)

ポイントは2つあって、1つは @Module のオプションに namespacedPathtarget:'nuxt' を指定するところです。
もう1つは、最後の行で ExtractVuexModule を使ってオブジェクト形式で export してあげるところです。
これで Vuex.store のモジュールモードに対応できました。

index.ts

index.ts もデコレータを使って記述しようとしたのですが、 nuxtServerInit の定義がどうしてもできなかったので、デコレータを使わずに記述します。

export const actions = {
  nuxtServerInit: ({ commit }) => {
    commit('modules/counter/SET_VALUE', 20)
  }
}

ここでは modules/counter の初期化を行ってみました。

呼び出すComponent

components/Counter.vue を作成します。

<template lang="pug">
  div [counter]
    div {{ counter }}
    div
      button(@click="increment") +
      button(@click="decrement") -
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class Counter extends Vue {
  get counter() {
    return this.$store.getters['modules/counter/value']
  }

  public increment() {
    this.$store.dispatch('modules/counter/increment', 1)
  }
  public decrement() {
    this.$store.dispatch('modules/counter/decrement', 1)
  }
}
</script>

呼び出す箇所は通常の使い方と同じです。

これでごちゃごちゃしがちな store 周りをスッキリとかけるようになったかと思います。

Written with StackEdit.

2019年5月23日木曜日

NuxtでTypeScriptを使えるようにする

https://ja.nuxtjs.org/guide/typescript/
基本的に上記の手順で設定していけばOK

  1. モジュールの追加
  2. nuxt.config.jsのTypeScript化
  3. nuxt-property-decoratorの導入
  4. ESLintの設定

完成したプロジェクトは以下になります。
ご査収ください。
https://github.com/TAC/nuxt-typescript-example

環境

- version
OS Windows10
Node.js v8.15.0
Nuxt.js v2.7.1

1. モジュールの追加

yarn add -D @nuxt/typescript
yarn add ts-node

上記のモジュール追加後、空のtsconfig.jsonファイルを作成して、nuxtコマンドを実行するとデフォルト値で更新されます。

2. nuxt.config.jsのTypeScript化

nuxt.config.jsnuxt.config.tsにリネームします。
そしてドキュメントの手順通り以下を追記して、コンフィグの export 方法を変更します。

import NuxtConfiguration from  '@nuxt/config'
import pkg from './package'

const nuxtConfig: NuxtConfiguration = {
...
}

export default nuxtConfig

ただ、上記の変更だけだと下記のようなエラーが出るはずです。

 ERROR  ⨯ Unable to compile TypeScript:                                                                                                                                                                 23:58:04
nuxt.config.ts:2:17 - error TS2307: Cannot find module './package'.

2 import pkg from './package'
                  ~~~~~~~~~~~
nuxt.config.ts:51:9 - error TS2532: Object is possibly 'undefined'.

51         config.module.rules.push({
           ~~~~~~~~~~~~~


  nuxt.config.ts:2:17 - error TS2307: Cannot find module './package'.

  2 import pkg from './package'
                    ~~~~~~~~~~~
  nuxt.config.ts:51:9 - error TS2532: Object is possibly 'undefined'.

  51         config.module.rules.push({
             ~~~~~~~~~~~~~

  at createTSError (node_modules\ts-node\src\index.ts:240:12)
  at reportTSError (node_modules\ts-node\src\index.ts:244:19)
  at getOutput (node_modules\ts-node\src\index.ts:360:34)
  at Object.compile (node_modules\ts-node\src\index.ts:393:11)
  at Module.m._compile (node_modules\ts-node\src\index.ts:439:43)
  at Module._extensions..js (module.js:664:10)
  at Object.require.extensions.(anonymous function) [as .ts] (node_modules\ts-node\src\index.ts:442:12)
  at Module.load (module.js:566:32)
  at tryModuleLoad (module.js:506:12)
  at Function.Module._load (module.js:498:3)

Done in 3.26s.

まずはこちらのエラー

Cannot find module './package'.

TypeScriptはデフォルトではJSONファイルのImportを許可していないのでコンフィグで設定を変更する必要があるようです。
https://www.typescriptlang.org/docs/handbook/compiler-options.html
"resolveJsonModule": truetscofig.jsonに追記します。

{
  "compilerOptions": {
    "resolveJsonModule": true
  }
}

そしてImport時には拡張子まで指定するようにすればOK

import pkg from './package.json'

続いてはこちらのエラー

Object is possibly 'undefined'.

config.moduleundefined になってしまうことがあるのが原因なので、undefined の場合の処理を追記します。
(参考サイト) https://qiita.com/dora1998/items/932506fa995962d4dc63#nuxtconfigjs–nuxtconfigts

  /*
   ** Build configuration
   */
  build: {
    publicPath: '/assets/',
    
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
+       if (!config.module) return
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue|ts)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }

これでひとまず起動できるようになっているはずです。

3. nuxt-property-decoratorの導入

コンポーネントの記述を楽にするためにデコレータモジュールを導入します。
ドキュメントの手順ではvue-property-decoratorとなっているが、nuxt-property-decoratorを追加します。

yarn add nuxt-property-decorator

各コンポーネントのスクリプト部分をTypeScriptにします。

page/index.vue

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import Logo from '@/components/Logo.vue'

@Component({
  components: {
    Logo
  }
})
export default class extends Vue {}
</script>

components/Logo.vue

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class Logo extends Vue {}
</script>

クラスの定義が必要になるので少し冗長になります。
なおnuxt.config.tssrcDirを設定している場合は、tsconfig.jsonbaseUrlをそれに合わせないとコンポーネントの読み込みに失敗します。

4. ESLintの設定

以下のモジュールを追加する。

yarn add -D @typescript-eslint/eslint-plugin

そして、デフォルトで作成されている.eslintrc.jsを以下のように修正します。

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'plugin:nuxt/recommended',
    'plugin:prettier/recommended',
    'prettier',
    'prettier/vue'
  ],
  plugins: [
    'prettier',
    '@typescript-eslint'
  ],
  // add your custom rules here
  rules: {
    '@typescript-eslint/no-unused-vars': 'error'
  }
}

package.jsonlintスクリプトにts拡張子を追加しておきます。

"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore .",

以上でNuxtでTypeScriptを使用できるようになりました。

Cloud FunctionsもTypeScriptにする

https://firebase.google.com/docs/functions/typescript?hl=ja
こちらの手順に従って進めればOKですが、いくつか追加で行う作業が必要でした。

まずはディレクトリ構成ですが、カレントディレクトリにアプリのビルド環境がある場合、予期せぬ動作が起こるようでした。

myproject
 +- src            # Nuxt src dir
 +- package.json   # Nuxt build script
 +- tsconfig.json  # Nuxt tsconfig
 +- nuxt.config.ts
 +- functions/
      +- package.json   # functions build script
      +- tsconfig.json  # functions tsconfig

上記の構成だと Nuxt を ビルドした際に functions 以下もビルドされてしまいコンフィグ設定の違いによりエラーが出てしまいます。
なので以下のような構成で Nuxtfunctions を配置します。

myproject
 +- app
 |    +- src            # Nuxt src dir
 |    +- package.json   # Nuxt build script
 |    +- tsconfig.json  # Nuxt tsconfig
 |    +- nuxt.config.ts
 +- functions/
      +- package.json   # functions build script
      +- tsconfig.json  # functions tsconfig

それぞれのディレクトリでビルドすることで干渉しなくなります。

次に app で追加したモジュールとExpressの型定義も追加しておきます。
また、lint が通らなくなるので、@nuxt/config を追加しておきます。

yarn add -D @nuxt/typescript @types/express
yarn add ts-node @nuxt/config

そして、公式の手順にあった firebase init functions で作成された functions/src/index.ts に色々と修正をいれて最終的に以下のようになりました。

import * as functions from 'firebase-functions'
import * as express from 'express'
import NuxtConfiguration from '@nuxt/config'
const { Nuxt } = require('nuxt')

const nuxtConfig: NuxtConfiguration = {
  dev: false,
  buildDir: 'nuxt',
  build: {
    publicPath: '/assets/'
  }
}
const nuxt = new Nuxt(nuxtConfig)

async function handleRequest(req: express.Request, res: express.Response) {
  res.set('Cache-Control', 'public, max-age=600, s-maxage=1200')
  await nuxt.ready()
  return nuxt.render(req, res)
}

const app = express()
app.use(handleRequest)

exports.ssr = functions.https.onRequest(app)

functions へ移動してビルドをすると functions/lib/index.js が出力されます。

cd functions/
yarn build

あとはアプリのビルドの成果物と合わせてローカルエミュレータを起動して動作確認ができると思います。

Written with StackEdit.

2019年5月21日火曜日

firebase-tools v6.10.0 でNuxtのSSRアプリを動かすことができた!

下記の記事で動かないといってたものが動くようになりました!
https://blog.28go.jp/2019/05/firebase-tools_19.html

きっかけはこちらの記事です。
https://www.memory-lovers.blog/entry/2019/03/31/003456

https://nuxtjs.org/guide/release-notes#-strong-important-strong-migration-guide

どうやら Cloud Functions での Nuxt の呼び出し方が変わっていたようです。
上記の修正をすることで firebase-toolsv6.10.0 の ローカルエミュレータでも動作するようになりました。

ただ、functions のパッケージにいくつか追加するモジュールがあったので記載しておきます。

yarn add firebase-admin
yarn add -D firebase-functions-test

firebase-admin は使用していなくても必要になったようです。

以前検証に使ったリポジトリも修正しました。
https://github.com/TAC/nuxt-ssr-firebase-example

動かなくてもやもやしていたのでスッキリしました!

Written with StackEdit.

2019年5月19日日曜日

firebase-toolsをバージョンアップすると動かない

firebase-toolsをバージョンアップするとNuxtで作成したSSRアプリがfirebase serveで動かなくなったので、その対処法を記録しておきます。
結論としては「最新バージョンでは動かない」ということになります。

環境

Windows10
Node.js v8.15.0
Nuxt.js v2.7.1

検証にはcreate-nuxt-appでなにもモジュールを追加しないプロジェクトを作成して、Firebase Functionsで動作するようにちょっと修正した以下のプロジェクトを使用しました。
https://github.com/TAC/nuxt-ssr-firebase-example

6.9.0

そもそもhostingが起動しません。
6.9.1で修正された模様です。

6.9.1

functions実行時に以下のメッセージが表示されて動きません。

!  The Cloud Functions emulator requires the module "firebase-admin" to be installed as a dependency. To fix this, run "npm install --save firebase-admin" in your functions directory.
i  functions: Your functions could not be parsed due to an issue with your node_modules (see above)

使ってないのにfirebase-adminを要求されます。
とりあえずdevDependenciesに追加して、再度実行すると以下のエラーに変わります。

!  The Cloud Functions emulator requires the module "firebase-admin" to be installed. This package is in your package.json, but it's not available. You probably need to run "npm install" in your functions directory.
i  functions: Your functions could not be parsed due to an issue with your node_modules (see above)

これは6.9.2で修正されたようです。

6.9.2

今度はfirebase-functions-testを要求されるようになります。

!  The Cloud Functions emulator requires the module "firebase-functions-test" to be installed as a development dependency. To fix this, run "npm install --save-dev firebase-functions-test" in your functions directory.
i  functions: Your functions could not be parsed due to an issue with your node_modules (see above)

メッセージ通りdevDependenciesに追加して再実行すると、以下のエラーメッセージが表示される。

!  The Firebase Admin module has not been initialized early enough. Make sure you run "admin.initializeApp()" outside of any function and at the top of your code

使ってないのだが初期化が必要になっています。
これは6.10.0で修正されたようです。

6.10.0

6.10.0でいろいろ修正されていたので、起動できるようになっているかと期待したが、以下のエラーが解消できませんでした。
以前は、モジュールの読み込み等でNuxtが起動できていなくて同様のエラーが出ていたが、そうではないようです。

>  C:\work\repos\nuxt\example\functions\index.js:19
>        promise.then(resolve).catch(reject)
>                ^
>
>  TypeError: Cannot read property 'then' of undefined
>      at Immediate.nuxt.render.promise [as _onImmediate] (C:\work\repos\nuxt\example\functions\index.js:19:15)
>      at runCallback (timers.js:812:20)
>      at tryOnImmediate (timers.js:768:5)
>      at processImmediate [as _immediateCallback] (timers.js:745:5)

6.9.0以降はまともに動かなくなってしまった。

6.8.0

今のところちゃんと動く最新バージョンは6.8.0です。
このバージョンあたりからNode.js v6が非推奨になったので、--ignore-enginesのオプションをつけないと@google-cloud/functions-emulatorのインストールに失敗します。

https://github.com/googlearchive/cloud-functions-emulator/issues/267

yarn global add firebase-tools@6.8.0 @google-cloud/functions-emulator --ignore-engines

まだ、しばらくは6.8.0を使っていくしかないようです。

追記(2019-05-21)

解決しました!
詳細は下記の記事に記載してあります。
https://blog.28go.jp/2019/05/firebase-tools-v6100-nuxtssr.html

Written with StackEdit.