20200118のvue.jsに関する記事は9件です。

Nuxt.jsとFirebaseのFirestoreでリアルタイムダッシュボードを作った

やりたいこと

Nuxt.jsとfirestoreを使用して、リアルタイムでダッシュボードを更新するシステムを作る。
リロードとかせずサクサクと!

firestoreの作成

コンソールからプロジェクトを作成して設定

※あとで書く

準備

①firebaseをインストール

yarn add firebase or npm install firebase --save

②plugins/firebase.jsの作成

firebaseの初期設定をします!
firebaseのページの設定画面下部にある情報をそのまま貼り付けてます。
開発環境と本番環境でfirebaseのアカウントを使い分けるためにENVにしています。
.envで書いた環境変数名を書きます

plugins/firebase.js
import firebase from 'firebase/app'
import 'firebase/firestore'

firebase.initializeApp({
  apiKey: process.env.FIRE_BASE_API_KEY,
  authDomain: process.env.FIRE_BASE_AUTH_DOMAIN,
  databaseURL: process.env.FIRE_BASE_DATABASE_URL,
  projectId: process.env.FIRE_BASE_PROJECT_ID,
  storageBucket: process.env.FIRE_BASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIRE_BASE_MESSAGING_SENDER_ID,
  appId: process.env.FIRE_BASE_APP_ID,
  measurementId: process.env.FIRE_BASE_MEASUREMENT_ID
})
const db = firebase.firestore()
export default db

③nuxt.config.jsへ追記

nuxt.config.js
...省略

plugins: [
  ...
  { src: '~/plugins/firebase' }
],

...省略

これでfirestoreを使う準備が整いました!

store/firestoreディレクトリを用意する

私はstore配下をAPI毎にディレクトリを切って、actions.js/getters.js/index.js/mutations.js
と分けて書く方法が好きです。
理由は見やすいし追いやすいからです。

今回はfirestoreの通信についてなので、firestoreとディレクトリを切ります。

まずはactions.js

store/actions.js
import db from '~/plugins/firebase'
// コレクション名をセット
const fireStore = db.collection('dashboard')

export const actions = {
  GET_REALTIME_LIST({ commit }, payload) {
    fireStore
      .where('report_datetime', '>', payload.from)
      .where('report_datetime', '<', payload.to)
      .orderBy('report_datetime', 'asc')
      .orderBy('created_at', 'asc')
      .onSnapshot(
        res => {
          commit('GET_REALTIME_LIST', res)
        },
        error => {
          console.error('GET_REALTIME_LIST', error)
        }
      )
  },
  UPDATE_FIRE_STORE({ commit }, payload) {
    fireStore
      .doc(payload.id)
      .set(payload)
      .then(res => {
        commit('UPDATE_FIRE_STORE', res)
      })
      .catch(e => {
        console.error('UPDATE_FIRE_STORE', e)
      })
  }
}

export default actions

const fireStore = db.collection('dashboard')
ここで接続をするコレクション名をセットします。
今回のコレクション名はdashboardなのでこのように記述。

onSnapshotこれがリアルタイム同期をするポイントです。
他にも追加されたものだけ取得、削除されたものだけ取得、とか書き方はありますが
今回のダッシュボードは全体を常に同期していたいのでこう書いてます。

また、firestoreではあまり柔軟にクエリが書けません。
between句もありません><
そこで

.where('report_datetime', '>', payload.from)
.where('report_datetime', '<', payload.to)

このように書くと、between句のように使えます。

次はmutations.js

mutations.js
import {
  GET_REALTIME_INCOMPLETE_LIST_FIRE_STORE,
  UPDATE_FIRE_STORE
} from '~/store/types'

export const mutations = {
  GET_REALTIME_LIST(state, content) {
    state.realtimeFireStore = content.docs.map(res => {
      const fireStore = res.data()
      fireStore.id = res.id
      return fireStore
    })
  },
  UPDATE_FIRE_STORE(state, content) {
    state.updateFireStore = content
  }
}

export default mutations

actionsでmutationsにcommitする際に、res.docsとやってもmutationsでmapで回す前にやっても大丈夫です。

responseではdocsの中に一覧が返ってきます。
そのまま直接参照することは出来ないので、mapで回しながらdata()とやってあげます。

fireStore.id = res.id ここでなぜidを入れているかというと
データを入れる際に、自動生成IDで作成しているので自動生成された一意のIDがデータ内に入っていません。

保持をしておかないとupdateで使用する際に困るので、データ内に突っ込んでます。
自動生成されたIDは、docs内にあります。IDはdata()で参照する中にはなく直接参照できるので、上記のように書きます。

続いてgetters.js

getters.js
export const getters = {
  getRealtimeFireStore: state => state.realtimeFireStore,
  getUpdateFireStore: state => state.updateFireStore
}

export default getters

ここは特にポイントはありません

続いてindex.js

index.js
import Actions from './actions'
import Getters from './getters'
import Mutations from './mutations'

export const state = () => ({
  realtimeFireStore: [],
  updateFireStore: ''
})
export const actions = Actions
export const getters = Getters
export const mutations = Mutations

ここも特にポイントはありません。

これでstoreに準備完了です。

pages/dashboard.vueの作成

さて、やっと画面に取り掛かります。

pages/dashboard.vue
<template>
  <div>
    <div class="contents">
      <h1 class="title">テスト</h1>
    </div>
    <div>
      <table class="dashboard">
        <tr v-for="(dashboardRow, rowIndex) in dashboards" :key="rowIndex">
          <td v-for="(dashboard, dashboardIndex) in dashboardRow" :key="dashboard.id">
            <div class="dashboard-card">
              <input type="text" :value="dashboard.title" @blur="save(rowIndex, dashboardIndex)" />
            </div>
          </td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
import moment from 'moment'
import { mapGetters, mapActions } from 'vuex'
import chunk from 'lodash/chunk'
import cloneDeep from 'lodash/cloneDeep'
export default {
  name: "dashboard",
  computed: {
    ...mapGetters('firestore/', [
      'getUpdateFireStore',
      'getRealtimeFireStore'
    ]),
    dashboards: {
      get(){
        const getFireStore = cloneDeep(this.getRealtimeFireStore)
        const dashboards = chunk(filterData, 3).map((dashboardItems, index) => {
          if (dashboardItems.length !== 3) {
            for (let i = 0; i < 4 - dashboardItems.length; i++) {
              dashboardItems.push({})
            }
          }
          return dashboardItems
        })
        return dashboards
      },
      set(dashboards){
        return dashboards
      }
    }
  },
  created() {
    this.getDashboard()
  },
  methods: {
    ...mapActions('firestore/', [
      'UPDATE_FIRE_STORE',
      'GET_REALTIME_LIST'
    ]),
    async getDashboard() {
      this.GET_REALTIME_LIST({
        from: moment().subtract(3, 'days').format('YYYY-MM-DD 00:00:00'),
        to: moment().format('YYYY-MM-DD 23:59:59')
      })
    },
    async save(rowIndex, dashboardIndex) {
      await this.UPDATE_FIRE_STORE(this.dashboards[rowIndex][dashboardIndex])
    },
  }
}
</script>

<style scoped>
table.dashboard {
  width: 100%;
  margin-top: 40px;
}
table.dashboard td {
  width: 30%;
  padding: 20px 14px;
}
.dashboard-card {
  border: 1px solid #cecece;
  border-radius: 4px;
  padding: 10px 6px;
  background-color: #f7f1f1;
  box-shadow: 0px 0px 9px 0px #afafaf;
}
</style>

直近3日のデータを取得しています。

created() {
  this.getDashboard()
},

createdでリアルタイムでgetするactions: GET_REALTIME_LIST しているメソッドを呼びます。
これで常にリアルタイム通信が可能となります。

computed内で、リアルタイムでgetしているデータを(gettersのgetRealtimeFireStore)を加工してあげます。

横3個ずつズラ〜と出したいのでlodashのchunkでわけてあげます。

カード内のテキストボックスでは、文字firestoreに入れてるデータのtitleを表示しています。
この値を変更してフォーカスを外してタイミングで更新メソッド(save)を呼んでいます。

actions:UPDATE_FIRE_STOREを呼んで、更新処理を行います。

この更新がかかると、今actions:GET_REALTIME_LISTがリアルタイムで取得しているので、
getters: getRealtimeFireStoreの値が変更されるため
computedのdashboardsが動き、画面上のカードがリアルタイムで変更されます。

リアルタイムで動いている確認方法

方法①このlocalhostを2つ開き、画面に分けて表示して
片方で更新かけて、もう1つの方でリロードなしに変更がされれば成功です!

方法②firestoreのコンソールで直接データを変更して、リロードなしにダッシュボード内のカードが更新されれば成功です!

以上でリアルタイムダッシュボードの実装は終わりです!

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

Nuxt.jsとfirestoreでリアルタイムダッシュボードを作った

やりたいこと

Nuxt.jsとfirestoreも使用して、リアルタイムでダッシュボードを更新するシステムを作る。
リロードとかせずサクサクと!

firestoreの作成

コンソールからプロジェクトを作成して設定

※あとで書く

準備

①firebaseをインストール

yarn add firebase or npm install firebase --save

②plugins/firebase.jsの作成

firebaseの初期設定をします!
firebaseのページの設定画面下部にある情報をそのまま貼り付けてます。
開発環境と本番環境でfirebaseのアカウントを使い分けるためにENVにしています。
.envで書いた環境変数名を書きます

plugins/firebase.js
import firebase from 'firebase/app'
import 'firebase/firestore'

firebase.initializeApp({
  apiKey: process.env.FIRE_BASE_API_KEY,
  authDomain: process.env.FIRE_BASE_AUTH_DOMAIN,
  databaseURL: process.env.FIRE_BASE_DATABASE_URL,
  projectId: process.env.FIRE_BASE_PROJECT_ID,
  storageBucket: process.env.FIRE_BASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIRE_BASE_MESSAGING_SENDER_ID,
  appId: process.env.FIRE_BASE_APP_ID,
  measurementId: process.env.FIRE_BASE_MEASUREMENT_ID
})
const db = firebase.firestore()
export default db

③nuxt.config.jsへ追記

nuxt.config.js
...省略

plugins: [
  ...
  { src: '~/plugins/firebase' }
],

...省略

これでfirestoreを使う準備が整いました!

store/firestoreディレクトリを用意する

私はstore配下をAPI毎にディレクトリを切って、actions.js/getters.js/index.js/mutations.js
と分けて書く方法が好きです。
理由は見やすいし追いやすいからです。

今回はfirestoreの通信についてなので、firestoreとディレクトリを切ります。

まずはactions.js

store/actions.js
import db from '~/plugins/firebase'
// コレクション名をセット
const fireStore = db.collection('dashboard')

export const actions = {
  GET_REALTIME_LIST({ commit }, payload) {
    fireStore
      .where('report_datetime', '>', payload.from)
      .where('report_datetime', '<', payload.to)
      .orderBy('report_datetime', 'asc')
      .orderBy('created_at', 'asc')
      .onSnapshot(
        res => {
          commit('GET_REALTIME_LIST', res)
        },
        error => {
          console.error('GET_REALTIME_LIST', error)
        }
      )
  },
  UPDATE_FIRE_STORE({ commit }, payload) {
    fireStore
      .doc(payload.id)
      .set(payload)
      .then(res => {
        commit('UPDATE_FIRE_STORE', res)
      })
      .catch(e => {
        console.error('UPDATE_FIRE_STORE', e)
      })
  }
}

export default actions

const fireStore = db.collection('dashboard')
ここで接続をするコレクション名をセットします。
今回のコレクション名はdashboardなのでこのように記述。

onSnapshotこれがリアルタイム同期をするポイントです。
他にも追加されたものだけ取得、削除されたものだけ取得、とか書き方はありますが
今回のダッシュボードは全体を常に同期していたいのでこう書いてます。

また、firestoreではあまり柔軟にクエリが書けません。
between句もありません><
そこで

.where('report_datetime', '>', payload.from)
.where('report_datetime', '<', payload.to)

このように書くと、between句のように使えます。

次はmutations.js

mutations.js
import {
  GET_REALTIME_INCOMPLETE_LIST_FIRE_STORE,
  UPDATE_FIRE_STORE
} from '~/store/types'

export const mutations = {
  GET_REALTIME_LIST(state, content) {
    state.realtimeFireStore = content.docs.map(res => {
      const fireStore = res.data()
      fireStore.id = res.id
      return fireStore
    })
  },
  UPDATE_FIRE_STORE(state, content) {
    state.updateFireStore = content
  }
}

export default mutations

actionsでmutationsにcommitする際に、res.docsとやってもmutationsでmapで回す前にやっても大丈夫です。

responseではdocsの中に一覧が返ってきます。
そのまま直接参照することは出来ないので、mapで回しながらdata()とやってあげます。

fireStore.id = res.id ここでなぜidを入れているかというと
データを入れる際に、自動生成IDで作成しているので自動生成された一意のIDがデータ内に入っていません。

保持をしておかないとupdateで使用する際に困るので、データ内に突っ込んでます。
自動生成されたIDは、docs内にあります。IDはdata()で参照する中にはなく直接参照できるので、上記のように書きます。

続いてgetters.js

getters.js
export const getters = {
  getRealtimeFireStore: state => state.realtimeFireStore,
  getUpdateFireStore: state => state.updateFireStore
}

export default getters

ここは特にポイントはありません

続いてindex.js

index.js
import Actions from './actions'
import Getters from './getters'
import Mutations from './mutations'

export const state = () => ({
  realtimeFireStore: [],
  updateFireStore: ''
})
export const actions = Actions
export const getters = Getters
export const mutations = Mutations

ここも特にポイントはありません。

これでstoreに準備完了です。

pages/dashboard.vueの作成

さて、やっと画面に取り掛かります。

pages/dashboard.vue
<template>
  <div>
    <div class="contents">
      <h1 class="title">テスト</h1>
    </div>
    <div>
      <table class="dashboard">
        <tr v-for="(dashboardRow, rowIndex) in dashboards" :key="rowIndex">
          <td v-for="(dashboard, dashboardIndex) in dashboardRow" :key="dashboard.id">
            <div class="dashboard-card">
              <input type="text" :value="dashboard.title" @blur="save(rowIndex, dashboardIndex)" />
            </div>
          </td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
import moment from 'moment'
import { mapGetters, mapActions } from 'vuex'
import chunk from 'lodash/chunk'
import cloneDeep from 'lodash/cloneDeep'
export default {
  name: "dashboard",
  computed: {
    ...mapGetters('firestore/', [
      'getUpdateFireStore',
      'getRealtimeFireStore'
    ]),
    dashboards: {
      get(){
        const getFireStore = cloneDeep(this.getRealtimeFireStore)
        const dashboards = chunk(filterData, 3).map((dashboardItems, index) => {
          if (dashboardItems.length !== 3) {
            for (let i = 0; i < 4 - dashboardItems.length; i++) {
              dashboardItems.push({})
            }
          }
          return dashboardItems
        })
        return dashboards
      },
      set(dashboards){
        return dashboards
      }
    }
  },
  created() {
    this.getDashboard()
  },
  methods: {
    ...mapActions('firestore/', [
      'UPDATE_FIRE_STORE',
      'GET_REALTIME_LIST'
    ]),
    async getDashboard() {
      this.GET_REALTIME_LIST({
        from: moment().subtract(3, 'days').format('YYYY-MM-DD 00:00:00'),
        to: moment().format('YYYY-MM-DD 23:59:59')
      })
    },
    async save(rowIndex, dashboardIndex) {
      await this.UPDATE_FIRE_STORE(this.dashboards[rowIndex][dashboardIndex])
    },
  }
}
</script>

<style scoped>
table.dashboard {
  width: 100%;
  margin-top: 40px;
}
table.dashboard td {
  width: 30%;
  padding: 20px 14px;
}
.dashboard-card {
  border: 1px solid #cecece;
  border-radius: 4px;
  padding: 10px 6px;
  background-color: #f7f1f1;
  box-shadow: 0px 0px 9px 0px #afafaf;
}
</style>

直近3日のデータを取得しています。

created() {
  this.getDashboard()
},

createdでリアルタイムでgetするactions: GET_REALTIME_LIST しているメソッドを呼びます。
これで常にリアルタイム通信が可能となります。

computed内で、リアルタイムでgetしているデータを(gettersのgetRealtimeFireStore)を加工してあげます。

横3個ずつズラ〜と出したいのでlodashのchunkでわけてあげます。

カード内のテキストボックスでは、文字firestoreに入れてるデータのtitleを表示しています。
この値を変更してフォーカスを外してタイミングで更新メソッド(save)を呼んでいます。

actions:UPDATE_FIRE_STOREを呼んで、更新処理を行います。

この更新がかかると、今actions:GET_REALTIME_LISTがリアルタイムで取得しているので、
getters: getRealtimeFireStoreの値が変更されるため
computedのdashboardsが動き、画面上のカードがリアルタイムで変更されます。

リアルタイムで動いている確認方法

方法①このlocalhostを2つ開き、画面に分けて表示して
片方で更新かけて、もう1つの方でリロードなしに変更がされれば成功です!

方法②firestoreのコンソールで直接データを変更して、リロードなしにダッシュボード内のカードが更新されれば成功です!

以上でリアルタイムダッシュボードの実装は終わりです!

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

Conteful公式ドキュメント「Integrate Contentful with Nuxt.js」の翻訳

はじめに

Nuxt.jsは、国内でますますの盛り上がりを見せています。特に、ContentfulやMicroCMSといったHeadless CMSと組み合わせたJAMStackな構成が注目されています。
しかし、Headless CMSの中で圧倒的な実績と人気を誇るContentfulはドキュメントが日本語対応していません(※2020年3月24日現在)。Contentfulのドキュメントは(英語とはいえ)この上なく丁寧に解説されています。

そこで、公式ドキュメントの
Integrate Contentful with Nuxt.js
を自分用に日本語に翻訳したものをQiita上で共有させて頂きます。
なるべく原文通りの翻訳を心がけていますが、適宜注をつけています。
参考になれば幸いです。

(※以下本文です)


Integrate Contentful with(ContentfulをNuxt.jsと統合する)

新しいインタラクティブなCLIの開始ガイドを試してみると、おそらく、サーバー側とブラウザーでVue.jsを実行できるユニバーサルJavaScriptアプリケーションであることがわかりました。これは、ビルド時に静的サイトのセット全体を生成し、ユーザーが別の場所に移動したい場合、Vue.jsを使用してサイトが部分的に再レン​​ダリングされることを意味します。かなりクールなものです!

このチュートリアルでは、Contentfulを使用して独自のNuxt.jsプロジェクトをセットアップする方法を示します。

Getting started with Nuxt.js(Nuxt.jsの使用を開始する)

Nuxt.jsアプリケーションで開始する推奨の方法は、vue-cliツールを使用することです。

$ npm install -g vue-cli

vueコマンドが使用可能になったので、nuxt / starterテンプレートを使用して新しいプロジェクトを初期化できます。

$ vue init nuxt / starter <プロジェクト名>

これで、新しく作成したプロジェクトに移動しnpm installを実行するだけで、ユニバーサルJavaScriptアプリケーションをContentfulに対応させることができます。詳細については、Nuxt.jsの入門ガイドをご覧ください。npm run devコマンドは、プロジェクト内での開発に使用できます。実行すると、Nuxt.jsサンプルプロジェクトが表示されます。その後、微調整してContentfulを実装できます。

Setting up the content model using the Contentful CLI(Contentful CLIを使用してコンテンツモデルを設定する)

Contentfulの統合を開始する前にスペースを設定し、コンテンツモデルを作成してデータを入力する必要があります。(あなたにとって)これがまったく新しい場合は、Contentfulデータモデルの紹介と、コンテンツ作成の完全な初心者向けガイドを最初に読むことをお勧めします。

すべてのデータ設定を手動で行うことができますが、CLIを使用して新しいスペースを作成し、データをフィードすることもできます。代わりにこれを行いましょう。

CLIは、提供されている方法のいずれかでインストールできます。

CLIを使用できるようになったので、 contentful loginを介してそれを認証する必要があります。このコマンドは、新しいブラウザウィンドウを開きます。このウィンドウでは、新しいアカウントを作成するか、ログインして新しいアクセストークンを取得できます。

$ contentful login

新しいスペースの作成は、CLIコマンドを1つ使用するだけで済み、app.contentful.comに手動で移動する必要はありません。

注:あなたのプランで使用可能な空きスペースがなくなった場合、スペースを作成すると追加料金が発生する場合があります。

$ contentful space create --name 'Nuxt tutorial

すごい!これで新しいスペースが作成され、コンテンツをシードする準備ができました。 CLIツールはすぐに必要になるスペースIDを表示します。

スペース間でコンテンツを転送するにはContentful CLIからcontentful space exportおよびcontentful space importコマンドを使用します。

seedコマンドは、定義済みの--space-idフラグとともに使用できますが、便宜上、次のコマンドにも特定のスペースを使用するようにCLIを構成できます。

$ contentful space use
? Please select a space:
  ...
❯ Nuxt tutorial (k8iqtj0xib4n)
  ...

いくつかのデータをインポートします!

# after space selection using `contentful space use`
$ contentful space seed --template blog
# without space selection
$ contentful space seed --template blog --space-id <your-new-space-id>

新しいスペースには、人とブログ投稿のコンテンツタイプを含むコンテンツモデルが設定されました。

最後に行うことは、Content Delivery APIアクセストークンを作成することです。これは、WebアプリまたはCLIを使用して実行できます。

$ contentful space accesstoken create --name nuxt-tutorial
✨Successfully created access token nuxt (1234567890xxxxxxxxxxx)

これは開始してNuxt.jsアプリに統合するのに十分です。

Integrating Contentful into Nuxt.js(ContentfulをNuxt.jsに統合する)

この時点でNuxt.jsプロジェクトは、./pages/index.vueで定義されているルートインデックスのみを使用できます。 Contentfulを使用するには、2つのことを行う必要があります。

Install the Contentful CDA JavaScript SDK(Contentful CDA JavaScript SDKをインストールします)

Contentfulを使用する最も簡単な方法は、JavaScript SDKをインストールすることです。これを行うには、npm install contentfulを簡単に呼び出します。 JavaScript SDKはブラウザのJavaScriptバンドルに含まれるので、npm --saveフラグを使用して実稼働依存関係として保存する必要があります。

$ npm install --save contentful

Nuxt.jsは、サーバー(静的な事前レンダリング)およびクライアント側(動的な再レンダリング)でカスタムコードを使用できるようにするプラグイン機能を提供します。幸いなことに、JavaScript SDKはaxiosに基づいており、Node.jsおよびブラウザーコンテキストで使用できます。

これを使用するには、pluginsディレクトリにcontentful.jsという新しいファイルを作成します。このファイルの目的は、ブートストラッププロセス中に設定する定義済みの環境変数を使用してSDKクライアントを作成することです。

// ./plugins/contentful.js

const contentful = require('contentful')
// use default environment config for convenience
// these will be set via `env` property in nuxt.config.js
const config = {
  space: process.env.CTF_SPACE_ID,
  accessToken: process.env.CTF_CDA_ACCESS_TOKEN
}

// export `createClient` to use it in page components
module.exports = {
  createClient () {
    return contentful.createClient(config)
  }
}

次に、使用する環境変数を定義する必要があります。実行中にCLIですべての環境変数を設定する必要性を回避するにはnpm run devでは、.contentful.jsonと呼ばれる新しい構成ファイルをセットアップできます。このファイルには、必要な構成が含まれています。

  • 人のエントリID(ブログの所有者)
  • 投稿データを取得するブログ投稿のコンテンツタイプID
  • あなたのスペースID
  • コンテンツ配信アクセストークン

すでにスペースID(CTF_SPACE_ID)とアクセストークン(CTF_CDA_ACCESS_TOKEN)を使用しています。ブログ投稿のコンテンツタイプIDと、ブログの作成者である1人のエントリIDは、テンプレートデータで既に定義されています。ブログ投稿のIDはblogPostになり、perosnのIDは15jwOBqpxqSAOy2eOO4S0mになります。

{
  「CTF_PERSON_ID」:「15jwOBqpxqSAOy2eOO4S0m」、
  「CTF_BLOG_POST_TYPE_ID」:「blogPost」、
  「CTF_SPACE_ID」:「YOUR_SPACE_ID」、
  「CTF_CDA_ACCESS_TOKEN」:「YOUR_ACCESS_TOKEN」
}

nuxt.config.jsで、構成ファイルを要求し、envプロパティを介してプラグインファイルで使用可能にすることができます。

// ./nuxt.config.js
const config = require('./.contentful.json')

module.exports = {
  // ...
  env: {
    CTF_SPACE_ID: config.CTF_SPACE_ID,
    CTF_CDA_ACCESS_TOKEN: config.CTF_CDA_ACCESS_TOKEN,
    CTF_PERSON_ID: config.CTF_PERSON_ID,
    CTF_BLOG_POST_TYPE_ID: config.CTF_BLOG_POST_TYPE_ID
  }
  // ...
}

envプロパティは、Nuex.jsで、Node.jsコンテキストまたはVue.jsコンポーネントのブラウザーのcontext objectで実行したときにprocess.envを介して使用できる値を定義する方法です。これは本当に便利になり、その理由がすぐにわかります。

Fetch data and render every page(データを取得してすべてのページをレンダリングする)

Nuxt.jsは、使用可能なページとルートを定義する規則を定義します。サンプルテンプレートでは、ファイル./pages/index.vueが既に作成されています。このファイルは、サイトのエントリポイントになります。動的パラメーターを使用してルーターを定義することもできます。詳細については、Nuxt.jsルーティングドキュメントを読むか、完成したサンプルのフォルダー構造を確認することをお勧めします。

Nuxt.jsでは、すべてのページコンポーネントの非同期データを定義できます。このデータは、ビルド時に取得され、その後すべてのページナビゲーションの前に取得されます。 Vue.jsのシングルファイルコンポーネントアプローチに慣れていない場合は、まずファイル構造を理解してください。

<template>
  <div>
    <!-- render data of the person -->
    <h1>{{ person.fields.name }}</h1>
    <!-- render blog posts -->
    <ul>
      <li v-for="post in posts">
        {{ post.fields.title }}
      </li>
    </ul>
  </div>
</template>

<script>
  import {createClient} from '~/plugins/contentful.js'

  const client = createClient()

  export default {
    // `env` is available in the context object
    asyncData ({env}) {
      return Promise.all([
        // fetch the owner of the blog
        client.getEntries({
          'sys.id': env.CTF_PERSON_ID
        }),
        // fetch all blog posts sorted by creation date
        client.getEntries({
          'content_type': env.CTF_BLOG_POST_TYPE_ID,
          order: '-sys.createdAt'
        })
      ]).then(([entries, posts]) => {
        // return data that should be available
        // in the template
        return {
          person: entries.items[0],
          posts: posts.items
        }
      }).catch(console.error)
    }
  }
</script>

これらの数行は作成したばかりのContentfulプラグインをインポートし、新しいSDKクライアントを作成します。 Nuxt.jsには、プラグインディレクトリのショートハンドも用意されているため、Contentfulプラグイン(`〜/ plugins / contentful.js')を簡単にインポートできます。

エクスポートされたオブジェクトのasyncプロパティで、必要なデータを取得し、コンポーネントで使用できるデータで解決されるプロミスを返すことができます。この場合、作成日と1人の人物で並べられたブログ投稿を取得しています。そして、これらの呼び出しの設定値はenvを介して利用できることがわかります。その後、テンプレートのデータにアクセスできます。

サイドノート:理論上は、各ブログ投稿でリンクされているため、1回のAPI呼び出しを保存できますが、わかりやすくするために、ここでは2回呼び出すことにしました。

Nuxt.jsによって事前にレンダリングされた最初のルートを持つために必要なことはこれだけです。 Contentfulからのより多くのデータでより多くのルートを作成できます。これにより、事前にレンダリングされた複数のHTMLページが表示され、ナビゲーション中にブラウザーで自動的に再レン​​ダリングされます。このプロダクションを準備するには、npm run generateを実行してページを静的に生成します。これらのページはデフォルトでdistに書き込まれます。

$ npm run generate
...
                                Asset       Size  Chunks             Chunk Names
0.nuxt.bundle.19d1fc79d53508bafb3c.js    9.02 kB       0  [emitted]  pages/index
1.nuxt.bundle.073636965192d97508b7.js     2.8 kB       1  [emitted]  layouts/default
vendor.bundle.159c28fcc84fd619b9e6.js     213 kB       2  [emitted]  vendor
  nuxt.bundle.4a191451179ff5f8654a.js    24.5 kB       3  [emitted]  app
     manifest.2b2d08aa839a35d40759.js    1.51 kB       4  [emitted]  manifest
                           index.html  132 bytes          [emitted]
                 client-manifest.json    5.73 kB          [emitted]
...

Sum up(まとめる)

これは、インタラクティブな入門CLIガイドを使用して5分で達成できる簡単なウォークスルーでした。

Nuxt.jsとContentfulを使用すると、運用上の大きなオーバーヘッドなしで、かなり迅速にユニバーサルJavaScriptアプリケーションを作成できます。スタックを構築して、選択したCDNにプッシュするだけで、準備完了です。

最終的な実装を確認したい場合は、5分でわかるブログの例をご覧ください。そこには、いくつかのルートと高度なルートの事前レンダリングを含む完全なソリューションがあります。

楽しんでください!

次のステップ


(※以上で翻訳は終了です)

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

初心者によるプログラミング学習ログ 213日目

100日チャレンジの213日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

213日目は

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

Vue.jsを使って簡単なtodoアプリを作ってみた

Vue.jsを使って簡単なtodoアプリを作ってみた

身内用進捗内容報告記事

初めに

Vue.jsが初めてのjsライブラリとなったが苦難することなく作ることができた

デモ

コード

HTML

<div id="app">
  <div>
    <li v-for="(todo, i) in todos">
     {{ todo.text }}
     <button @click="remove(i)">
    削除
  </button>
    </li>
  </div>
  <input v-model="todo" >
  <button @click="add">
    追加
  </button>
</div>

javascript

var app = new Vue({
  el: '#app',
  data: {
    todos: [],
    todo: 'sample',
  },

  methods: {
    add() {
      this.todos.push({ text: this.todo })
    },

    remove(i) {
      this.todos.splice(i, 1) 
    }
  }
}) 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

graphql clientはApolloよりfetchでやった方がいいのではないかという件

リポジトリ

https://github.com/gqlkit-repos/gqlkit-store
graphql client storeプラグインです。
これを、各フレームワークのcontextに突っ込んでやろうという魂胆
reduxはreact、vuexはvueという感じですが
クロスフレームワークでかつgraphqlの為の
client&storeというのを作ってみました。
nuxt.jsでは検証済

resolverファーストアーキテクチャのgraphql client設計

.
├── index.js
└── resolvers
    ├── Mutation
    │   ├── createStaff.js
    │   └── deleteStaff.js
    ├── Query
    │   └── staffs.js
    ├── cache.js
    └── client.js

たったのこれだけ
と言いたいところですが
これを例えば、nuxtで使うなら、nuxtのpluginsディレクトリに突っ込むので
一見、まぁまぁな規模感あるように見えます。

というか、そもそもなのですが
クライアント側のvuexとかreduxだとかのストアが抱えている課題って
もうストアはストアで別個のフレームワークと捉えて設計した方が良いレベルではないかと思うので
これはなんか正解路線なのでは?と思っています。

ちなみにQueryとMutation以下のjsファイルはデモ用のファイルです。

queryのコードの見た目も、mutationのコードの見た目もほぼ同じ

上のディレクトリツリーのstaffs.jsはこんな感じ

staffs.js
import client from '../client'
import cache from '../cache'
import gql from 'graphql-tag'

export const demand = gql`
    query {
        staffs {
            id
            name
            age
            sex
        }
    }
`

export default async variables => {
    let re
    if (!cache.has('staffs').value()) {
        const { staffs } = await client.req(demand)
        cache.set('staffs', staffs).write()
        re = staffs
    } else {
        re = cache.get('staffs').value()
    }

    return re
}

mutationにあたるcreateStaff.jsはこんな感じ
mutationはrefetchしたいモジュールファイルをrefetchという名前で読み込みます。
そんでもってqueryでもmutationでも
クライアントツールはclient.req(demand, variables)(variablesが必要ない場合は省略)

createStaff.js
import client from '../client'
import cache from '../cache'
import gql from 'graphql-tag'
import refetch from "../Query/staffs";

export const demand = gql`
    mutation($name: String!, $age: Int!, $sex: String!) {
        createStaff(name: $name, age: $age, sex: $sex) {
            id
            name
            age
            sex
        }
    }
`

export default async variables => {
    const res = await client.req(demand, variables)
    const staff = res.createStaff
    const staffs = cache.get('staffs').value()
    staffs.push(staff)
    cache.set('staffs', staffs).write()

    return refetch()
}

クライアントツールは21行のコードでqueryもmutationもOK!

fetchで書くとこんなシンプルになるとは正直、驚きました。
ただ、今後subscription導入のことも考えると
もうちょっと大きいファイルになる可能性はあります。
まだ、subscriptionは未経験なので。。。

client.js
const GQLKIT_SERVER_END_POINT = process.env.GQLKIT_SERVER_END_POINT || 'http://localhost:4000/query'
const method = 'POST'
const headers = {
    'Content-Type': 'application/json',
    Accept: 'application/json'
}

export default {
    async req(demand, variables) {
        const res = await fetch(GQLKIT_SERVER_END_POINT, {
            method,
            headers,
            body: JSON.stringify({
                query: demand.loc.source.body,
                variables
            })
        })
        const { data } = await res.json()
        return data
    }
}


キャッシュはどうするか?

キャッシュは完全切り分けの方針です。
Apolloのapiはキャッシュと密結合ですが
resolverファーストなgraphql clientアーキテクチャでは
キャッシュは完全別扱いにして
好きなin memory dbを使えば良いという方針で考えます。
resolverを書いて解決すれば良いじゃないか!という方針です。

僕は今のところlowdbを使っています。
ここはお好みで好きなもの使ってよしです。

cache.js
import low from 'lowdb'
import Memory from 'lowdb/adapters/Memory'

const cache = low(new Memory())

cache.defaults({}).write()

export default cache

まとめ

apolloがなぜ、あれほどまでに複雑なアーキテクチャを取っているのか
僕の知らない何らかの理由でああなっている可能性も大なので

これがベストアンサーだなんて到底言い切る事はできないです。
これはあくまで、1アイデアというものです。

あと、jsのオブジェクトをgormっぽい書き方で読み書き出来るような
ツールをご存知の方おられましたら教えて頂けると嬉しいです。

サーバーとクライアントで違うノウハウというのを最小限に抑えたい
ということで
graphqlサーバーのアーキテクチャを真似たプラグインを作ってみたという話でした。

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

クライアントサイドだけで日本語PDFを出力したときの文字化け・改行不具合をpdfmake最新版で再ビルドして直した話

はじめに

  • 常用文字にサブセット化を行ったものを id:naoa_yさんが作ってくれているのでそれを使っても良い
    • ビルド後のファイル(vfs_fonts.js)で 5MB -> 2.3MBぐらいに減ってる
    • ただしbuildが5年前なのでpdfmakeのバージョンが0.1.20と古い
    • 最新は0.1.63 (released this on 11 Dec 2019)
  • 上記のpdfmake@0.1.20だと日本語化したときの文字改行(word wrap)が適切にうごかない&改行されたとき文字化けする不具合があった
    • 私の場合、可変長の文字を動的に組み込む必要があり、自動改行はどうしても必要だった。1行に収まる短文のみであれば問題ないかと
  • 最新だとなおるかもしれないと、pdfmakeを最新にして使いたかったので、buildし直した。結論、治った!
  • Vueと書いてるけど、フォント差し替えるまでは共通だとおもう
  • @watameさんの記事を参考にしました。ありがとうございます。

TL;DR

  • このリポジトリにある、build/pdfmake.min.js, build/vfs_fonts.jsを使えばよいです。2つ合わせると10MB程度になります
  • https://github.com/yazashin/pdfmake/tree/0.1.63-ja
  • フォントサイズを減らしたい・違うフォントにしたいって人は下記を読みましょう

日本語対応pdfmakeをビルドする

fork&cloneする

cloneした後branchを0.1系に切り替える

  • "This is unstable master branch for version 0.2.x, for stable version 0.1.x see branch 0.1."とのこと

日本語フォントを入手する

  • ここは好み・用途によるとおもいますが、私のケースではこのフォントがマッチしました
  • https://ipafont.ipa.go.jp/#jp
  • IPAexゴシックのみ利用(等幅フォント)
  • メリデメを考えた時、メリットが勝ったのでこれを選択
    • 用途として、珍しい漢字もつかわれるのでサブセット化したとき文字化けの発生が怖い
    • pdfmakeとvfs_fontsで合計して10MBぐらいになる
      • pdfmake.min.js 1.2MB / vfs_fonts.js 8.1MB
      • 印刷ページごとに都度読み込みタイプだと辛い印象がある
      • vueのようなSPAでindex.htmlにscriptタグでglobalに登録してしまえば、大きな問題にはならないって考えて進めた
      • ぶっちゃけ最近はネット回線高速なので、SPAで初回ロード時に1回発生する10MB程度ならほとんど気にならないと思う
        • 安定したらServiceWorkerのキャッシュ対象にしてしまうのもありだとおもう
      • SEO気にする必要がない業務系サービスなのでこの判断ができている。っていう前提があります。
        • むしろSEO気にするサービスで、SPA & PDF生成が必要って、かなりニッチだとはおもうけどw

フォントを入れ替える

  • before
    image.png

  • after
    image.png

vfs_fonts.jsの更新

  • buildFontsを実行
$ yarn install
  ....
success Saved lockfile.
✨  Done in 216.37s.

$ ./node_modules/.bin/gulp buildFonts
[10:45:48] Using gulpfile ~/workspace/pdfmake/gulpfile.js
[10:45:48] Starting 'buildFonts'...
[10:45:48] Finished 'buildFonts' after 189 ms
  • pdfmakeへフォントを反映
$ yarn run build
...
WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
[10:50:16] Finished 'build' after 23 s
✨  Done in 24.49s.
  • warning出てるけど無視して進める

vfs_fonts.jsの確認

  • buildフォルダにあるvfs_fonts.jsがこうなっていれば大丈夫 build.png

Vue(typescript)で確認

  • 今回つくったpdfmake & vfs_fonts は、npmとして登録はせず、publicなフォルダに静的jsとして利用する
    • npmにするとdeploy時にciでビルドするとき、フォントのビルドから必要になるとか、そこらへん面倒でこうした
  • vue-cliのversionによって構成かわりますが、VUE_PROJECT_DIR/public/static-js/配下においた

index.htmlに登録

  • ポイントして、 deferを忘れずにつけましょう。10MB近いjsの読み込みでHTMLのパースが止まってしまいUXに影響がでます
index.html
 ... ~~~ ...
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
  <script defer src="/static-js/pdfmake.min.js"></script>
  <script defer src="/static-js/vfs_fonts.js"></script>
</html>

Vueの実装

  • Vueで使うときはこんなかんじ。手打ちなので間違ってるかもしれません。
pdf.vue
declare var pdfMake: any;
@Component({})
export default class PrintPdf extends Vue {
    createPdf() {
        pdfMake.fonts = {
            IPA_gothic: {
                normal: 'ipaexg.ttf',
                bold: 'ipaexg.ttf',
                italics: 'ipaexg.ttf',
                bolditalics: 'ipaexg.ttf',
            }
        };
        const defaultStyle = 'IPA_gothic';
        const docDefinition = {
            content: 'Hello 日本語!',
            defaultStyle: {
                font: defaultStyle
            }
        }
        pdfMake.createPdf(docDefinition).print();
    }
}
  • pdfmake 0.1.63 & IPAゴシック で生成したPDF
  • なお、文章はジェネレータでつくったので意味をなしてません。
    image.png

  • 文字改行時の文字化け不具合がなおって本当に良かった..

Thanks

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

【Vuetify】<v-avator>を使って背景色を動的に変更するサンプル

はじめに

Vue.jsのUIコンポーネントライブラリであるVuetifyのうち、<v-avator>を使って色を動的に変更するサンプルを残します。

環境

OS: macOS Catalina 10.15.1
Vue: 2.6.10
vuex: 3.1.2
vuetify: 2.1.0

前提:<v-avator>とは?

Avatar component — Vuetify.js

Vuetifyが提供している、以下のようなアバターをお手軽に実装出来るコンポーネントです。
※画像は公式ドキュメントより拝借しました。

image.png
image.png
image.png

今回はこの<v-avator>を使って、中の文字列が

  • 1 -> 黄色
  • 2 -> 青
  • 3 -> 緑

と、それぞれ背景色を変わるというサンプルコードを残します。
Vuetifyのインストールがまだの方は、以下記事の後半等ご参照下さい。
【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順 - Qiita

結論:コード

子コンポーネント

シンプルに<v-avator>だけを表示するようにしています。

SampleChild.vue
<template>
  <div>
    <v-avatar
      :color="avatorColor"
    >
    {{ displayName }}
    </v-avatar>
  </div>
</template>

<script>
  export default {
    name: 'SampleChild',
    //propsでv-avator部分を親コンポーネント側で動的に変更出来るようにする
    props: {
      displayName: '',
      avatorColor: '',
    }
  }
</script>

親コンポーネント

続いて親コンポーネントです。
v-forを使って、先程作成した子コンポーネントをnumbersという配列の要素数に応じて表示するようにしています。

SampleParent.vue
<template>
  <div>
    <!--配列numbersの要素数だけ表示する-->
    <div v-for="number in numbers" :key="number.id">
    <!--子コンポーネントがpropsで渡した2つの変数を親側で使用-->
      <SampleChild
       :avatorColor="color[number.name]"
       :displayName="number.name"
       >
      </SampleChild>
    </div>
  </div>
</template>

<script>
import SampleChild from './SampleChild';

export default {
  name: 'SampleParent',
  components: {
    SampleChild,
  },
  data() {
    return {
      //表示される配列
      numbers: [
        {id:1, name:1},
        {id:2, name:2},
        {id:3, name:3},
      ],
      //number.nameに応じて変わる背景色
      color: {
        1: 'yellow',
        2: 'blue',
        3: 'green',
      }
    }
  },
}
</script>

出力

無事、動的に背景色が変更されていることが確認出来ました!

image.png

確認:number.nを変更した場合の出力

以下のように変更してみます。

SampleParent
<template>
...略
</template>

<script>
//...略
  data() {
    return {
      numbers: [
        {id:1, n:2}, //ここのnを1->2へ変更
        {id:2, n:2},
        {id:3, n:3},
      ],
//...略
      }
    }
  },
}
</script>

変更結果

無事、numbers.nに応じて背景色が変化してくれています!

image.png

この考えを応用すれば、特定のステータスの人だけアバターの背景色を変える、などの表示が可能となります。

おわりに

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

どなたかの参考になれば幸いです:relaxed:

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

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

Nuxt.js firebase GoogleAuth

plugins/firebase.js
import firebase from "firebase";

if (!firebase.apps.length) {
  const firebaseConfig = {
    apiKey: "<apiKey>",
    authDomain: "<authDomain>",
    databaseURL: "<databaseURL>",
    projectId: "<projectId>",
    storageBucket: "<storageBucket>",
    messagingSenderId: "<messagingSenderId>",
    appId: "<appId>",
    measurementId: "<measurementId>"
  };

  firebase.initializeApp(firebaseConfig);
}

export default firebase;
/pages/login.vue
<template>
  <div>
    <button type="button" @click="signIn()" class="btn btn-primary">Sign in with Google</button>
    <div><router-link to="/mypage" >マイページ</router-link></div>
  </div>
</template>

<script>
import firebase from "@/plugins/firebase";

export default {
  created: function() {
    const user = firebase.auth().currentUser;
    console.log('user', user);
    if (user) {
      this.$router.push('/mypage'); 
    }
  },
  methods: {
    signIn() {
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase.auth().signInWithPopup(provider).then((result) => {
        console.log('result', result);
        this.$router.push('/mypage');
      }).catch((error) => {
        console.error(error.code);
      });
    }
  }
}
</script>
pages/mypage.vue
<template>
  <div>
    <div>{{email}}</div>
    <button type="button" @click="signOut()" class="btn btn-primary">ログアウト</button>
    <div><router-link to="/login" >ログイン画面</router-link></div>
  </div>
</template>

<script>

import firebase from "@/plugins/firebase";

export default {
  data: function() {
    return { email: null };
  },  
  created: function() {
    const user = firebase.auth().currentUser;
    console.log('user', user);
    if (user) {
      this.email = user.email; 
    } else {
      this.$router.push('/login');
    }
  },
  methods: {
    async signOut(){
      await firebase.auth().signOut();
      this.$router.push('/login');
    },
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む