Nuxt7(Nuxt.js+Framework7)でモバイルアプリ風のSPAを簡単に作成できるか試してみた

[Nuxt7(Nuxt.js+Framework7)](https://github.com/nuxt-community/nuxt7)について、今回つくったサンプルとともに紹介です。

![demo](https://github.com/hide212131/nuxt7-googlecal-example/wiki/images/readme.gif)

Nuxt7に加え、カレンダーはFullCalendar、Google Signinで認証し、Google CalendarとAPI連携します。静的コンテンツで動きます。

Github: https://github.com/hide212131/nuxt7-googlecal-example/
Demo: https://next7-googlecal-example.netlify.com/

## きっかけ
モバイルアプリやフロントエンドの開発経験ないままは嫌だなあと思ってる中、身内でちょっとしたアプリを作ると喜ばれる機会があり、作ってみることに。

- 機能は少なめ。お金をかけずに短時間で。
- ネイティブアプリの必要はないが、iOS/Androidの操作感はほしい
- プロトタイプを早く作り、感触を知りたい

探してたところ、Framawork7というスマホっぽい動きをするJavaScript・CSSフレームワークを見つけました。Vue.jsとも組み合わせられます。（Framework7-Vue)
Vueも、pwa-moduleやvuexなどの機能を使うなら、統合開発環境としてのNuxt.jsが良さそうです。
さらに、Nuxt.jsとFramework7を組み合わせた、Nuxt7というのがあるのですね。
日本語関連の情報はほどんど無いのですが、面白そうなので試してみました。

## Framework7について
Nuxt7の説明の前に、Framework7から。以前からあるようですがメジャーでなさそうなので、軽く紹介。
https://framework7.io/
OSSで、HTML、CSS、JavaScriptの技術で、iOSとAndroidっぽいルック＆フィールにできます。動作は、上のリンクのトップページのデモを触るのが一番分かりやすいです。
jQueryライクなDOM操作のほか、画面部品をvueのコンポーネントとして使うFramework7-Vueや、React.jsと組み合わせるFramework7-Reactがあります。
ハイブリッドアプリ開発のほか、プロトタイプ作成としての使い方も有効かと思います。

## nuxt7について
Nuxt7は、Nuxt.jsに上記のFramework7-Vueを融合したものです。
https://github.com/nuxt-community/nuxt7
Nuxt.jsが持つPWAなどの機能に加え、Framework7を使える設定が最初から用意されています。

サイトから特長を引用します。
> - Fully compatible with framework7 2.x
> - Development mode with hot reloading
> - Optimized production builds suitable for 100% static hosting
> - Fully PWA compatible out of the box
> - Page based router for Framework7
> - Use vuex store in your apps
> - Familiar nuxt.js development experience with a super easy learning curve

## アプリ
今回作成しようとしたシンプルなカレンダーアプリです。

- iOS風ルック＆フィールのWebアプリ
- Google Signin による認証
- カレンダーを表示し、タップにより、定型スケジュールを登録・削除
- カレンダーデータは、Google Calendarに連携

スターターテンプレートをダウンロードしてから肉付けしていきます。

```console:
> npx sao nuxt-community/nuxt7 nuxt7-app
```

依存パッケージは以下です（無駄に色々入れてる）

```json:package.json
  "dependencies": {
    "framework7": "^2.0.6",
    "framework7-vue": "^2.0.0",
    "nuxt": "next",
    "nuxt7": "latest",
    "store": "^2.0.12",
    "vue-full-calendar": "^2.5.1",
    "vue-google-signin-button": "^1.0.2",
    "vue-i18n": "^7.4.1"
  },
```
以下、作成中にハマったところ、特徴的なところを書いていきます。

### 認証画面
Framework7-Vueは、ログイン画面をダイアログ形式で出すコンポーネント<login-screen>があり、このダイアログの表示・非表示フラグを、Vuexのstoreで管理する方法を使っています。

```html:pages/login.vue
<template>
  <f7-login-screen :opened="!isAuthenticated" >
```
```js:store/index.js
export const state = () => ({
  user: null,
})

export const mutations = {
  SET_USER(state, user) {
    state.user = user || null
  },
}

export const getters = {
  isAuthenticated(state) {
    return !!state.user
  },
}
```

一方で、nuxt.jsでは[middlewareで認証チェックしてリダイレクトする処理例](https://ja.nuxtjs.org/api/pages-middleware)がありますが、なぜかnuxt7ではvueコンポーネントからmiddlewareフォルダ内のjsが呼び出されず、期待した動きができませんでした。Framework7は仕組み的にSPAゴリゴリでページ遷移とは縁遠いので、今のやり方で良い気がします。

同じようなVuexを使った表示／非表示の動きは、実行中ダイアログでも使っています。

### カレンダー
Framework7にはカレンダーのコンポーネントがありますが、カスタマイズが色々できる多機能な[FullCalendar](https://fullcalendar.io/)を使ってみたくて入れました。FullCalendarはjQuery主体ですが、Vueで包んだ[vue-full-calendar](https://github.com/CroudSupport/vue-fullcalendar)というパッケージを使っています。カレンダーのデータをリアクティブに反映でき、良い感じです。

```html:~/pages/calendar.vue
<template>
    <full-calendar :events="events"></full-calendar>
</template>

<script>
export default {
  data() {
    return {
      base: [], // ここにevent情報を格納
    }
  computed: {
    events() { 
      // baseが変化すると呼ばれる。
      // 条件によりイベントの色を変更してreturnすると再描画 
      let events = this.base;
      events.forEach(e => {
        if (…) { 
          e.color = "red";
        } else if (…) {
          e.color = "gray";
        } else {
          e.color = "green";
        }
      });
      return events;
    },
  }
}
</script>
```


### Google認証
Google認証ができるVueコンポーネントとして[vue-google-signin-button](https://github.com/phanan/vue-google-signin-button)を使いました。
vueコンポーネントの認証ボタンが用意されており、サインインのパラメータと、ログイン後のコールバックを実装すれば、ボタン押下でGoogleサイトへの認証リダイレクトし、認証後にコールバック呼び出しを行ってくれます。

```html:~/pages/login.vue
<template>
   ...
   <g-signin-button 
        :params="googleSignInParams"
        @success="onSignInSuccess"
        @error="onSignInError">
        Googleにサインイン
   </g-signin-button>

<script>
export default {
  data() {
    return {
      googleSignInParams
    };
  },
  methods: {
    onSignInSuccess(googleUser) {
       ...
    }
    onSignInError(error) {
       ...
    }
  }
}
</script>
```


scriptタグに必要なjsをロードする必要があるので、nuxt.config.jsに指定します。

```js:~/nuxt.config.js
  head: {
    script: [
      { src: 'https://apis.google.com/js/api:client.js', async: true, defer: true}
    ]
  },
```
ただ、このコンポーネントは、ボタン押下直後のイベント発動APIを用意してないので、二度押し防止の実行中ダイアログを入れにくい。結局強引な方法でイベント登録するはめに。ソースは参考程度にとどめ、直接実装した方がよかったかも。

```html:~/pages/login.vue
<template>
  <g-signin-button ref="gSigninButton">

<script>
export default {
  mounted() {
    this.$refs.gSigninButton.$refs.signinBtn.onclick = () => {
      self.openPreloader(); // 実行中ダイアログ
    };
  },
}
</script>
```
## Google API Client
Calendarを操作する[Google API Client Libraries](https://developers.google.com/api-client-library/javascript/reference/referencedocs
)は非同期でごちゃごちゃしているので、一つのファイル`~/utils/google-api.js`にまとめています。async/awaitを使い、呼び出し側からは同期的な記述をするのがスッキリしますね。

```html:~/pages/login.vue
<script>
export default {
  methods: {
    async onSignInSuccess(googleUser) {
      try {
        await googleInitClientNew(googleUser); // Google Client 初期化
        this.$emit("success"); // 成功
      } catch (err) { // 失敗
        ...
      } finally {
        this.$store.commit("SET_PROGRESS", false); // 終了時にダイアログを消す
      }
    },
  }
}
</script>
```
しかし、Googleのライブラリは何故かasync/awaitには対応しておらず、しかも正常系/異常系のAPIに統一感がない感じです。そんなわけで、async/awaitできれいにするために各種APIをPromiseでラップします。特に[Thenable](https://google.github.io/closure-library/api/goog.Thenable.html)を返してくるAPIについては同じような記述になるので、以下のようなラッパーを作っとくのが良いかと。

```js:~/utils/google-api.js
const gapi_await = async (api, module) => {
    return new Promise((resolve, reject) => {
        api.call(this, module).then((res) => { resolve(res) }, (err) => { reject(err) })
    })
}

const googleInitClient = async (googleUser) => {
    await gapi_await(gapi.client.init, {
        apiKey: API_KEY,
        clientId: CLIENT_ID,
        discoveryDocs: DISCOVERY_DOCS,
        scope: SCOPES
    })
}
```

## 終わりに
vueを含め、ここに出てくるキーワードが全て初めて触るものだったので、正直簡単にとはいかなかったですが、一度テンプレートや実装例を作れば、少ないコード量でやりたいことが実現できそうです。
Nuxt7についてはまだ情報量が少ないので、有益な情報が出てくれば、ここで紹介していきたいと思います。

## 参考
[Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する] (https://qiita.com/potato4d/items/cfddeb8732fec63cb29c) …この記事に感化されました。ありがとうございます。
https://developers.google.com/google-apps/calendar/v3/reference/
https://developers.google.com/api-client-library/javascript/reference/referencedocs