50
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Nuxt.js × GAS Execution API で同人頒布会向け予約システムをごにょごにょする PWA を作る

Last updated at Posted at 2018-11-10

はじめに

GASでQRコードを使った同人頒布会向け予約システムを作った話」の続きです。

reserview_overview.png

半年ほど前、日本最大級のアナログゲーム頒布会である「ゲームマーケット2018春」で、上記の予約システムを実際に運用してみたところ、

  • (こちらからの声かけ後を含め)QRコードを提示してくれたのは6割弱
    • 0.5割くらいの人がガラケーないしキャリアメールのため、QRコード自体を受信できていない

という問題にぶち当たりました。
QR コードが提示されなかった場合、スマホの Google スプレッドシートアプリから予約番号 or 名前を検索していましたが、いまいち操作性がよろしくない1

というわけで、いっそスプレッドシートを外部 API 化して、スマホアプリから予約情報の検索&購入確定できるようにして、なんならアプリに QR コード読み取り機能も埋め込んじゃおう、というのが今回の趣旨です。

PWA (Progressive Web Apps)

サークル内々で使うアプリのため、アプリストアに上げるようなものでもないので、PWA (Progressive Web Apps) と呼ばれる技術仕様を用いて開発しました。
PWA は(私の理解で)ざっくり言うと「Web ページをローカルに保存してネイティブアプリっぽく使える」技術仕様で、

  • ネイティブアプリと比較すると……ストアの審査が不要、クロスプラットフォーム対応が容易
  • Web アプリと比較すると……キャッシュを用いてオフラインでも動作可、Push 通知が利用可2

のような特長があります(参考:いまさら聞けないPWAとAMP)。

ひと昔前だとキャッシュの管理などをゴリゴリ書くのがとても面倒だったみたいですが、Web フレームワークの一つである Nuxt.js を用いた場合、簡単に Webアプリを PWA 化することができるそうです。

開発手順

せっかくなので備忘録的に Nuxt.js × GAS Execution API で PWA を開発するポイントをまとめました。

1. Nuxt.js プロジェクトの作成

vue-cli を使って、テンプレートから Nuxt.js プロジェクトを作成します。
ここでは、プロジェクト名を reserview-client としました。

$ vue init nuxt/starter reserview-client
$ cd reserview-client
$ npm install

npm run dev 実行時、Nuxt 2.x ではエラーが発生するみたいなので、以下のように nuxt.config.js を修正します。

nuxt.config.js
  module.exports = {
    ...
    build: {
      /*
      ** Run ESLint on save
      */
-     extend (config, { isDev, isClient }) {
-       if (isDev && isClient) {
+     extend (config) {
+       if (process.server && process.browser) {
          config.module.rules.push({
            enforce: 'pre',
            test: /\.(js|vue)$/,
            loader: 'eslint-loader',
            exclude: /(node_modules)/
          })
        }
      }
    }
  }

2. GAS Execution API の有効化

Google スプレッドシートから [ツール] > [スクリプト エディタ] を開き、以下のような scripts/API.gs を作成します。
#実際は、予約一覧を取得するコードをゴリゴリ書いていますが、ここでは割愛します。

前回の記事clasp を導入している場合は、ローカル環境で scripts/API.js を作成し、clasp push しても構いません。

scripts/API.gs
function getReservations () {
    // dummy data
    return JSON.stringify({
        id: '541603',
        reservedAt: '2018-11-07T19:38:26.572Z',
        // purchasedAt: '2018-11-25T11:19:52.398Z'
        name: 'ぶらちょこ'
    });
}

次に [リソース] > [Cloud Platform プロジェクト…] > [このスクリプトが現在関連付けられているプロジェクト:] のリンクから Google Cloud Platform Console を開きます。
[メニュー] > [API とサービス] > [ライブラリ] の検索ボックスから "Apps Script API" を探し、[有効にする] を選択します。
fig2.png

3. OAuth 認証情報の作成

Google Cloud Platform Console の [メニュー] > [API とサービス] > [認証情報] を開き、[認証情報] タブ > [認証情報を作成] > [OAuth クライアント ID] を選択します。

以下のように各項目を設定し、OAuth クライアント ID を[作成] します。
[承認済みの JavaScript 生成元] には、後のステップでホストされた Web ページ の URL を設定しますが、とりあえずローカル環境で試したいというだけであれば、http://localhost:<ポート番号> のみで OK です。
fig3.png

その後、スクリプトエディタに戻り、[公開] > [実行可能 API として導入…] を開きます。
以下のように各項目を設定して [配置] すると、GAS を外部 API として利用できるようになります。
fig1.png

4. GAS Execution API の実行

Web ブラウザ(JavaScript)から GAS Execution API を利用するには、以下の手順を踏む必要があります。
公式ドキュメントのチュートリアルの方がかなり親切に書かれているので、ご参考までに。

  1. OAuth 認証
  2. Google アカウント認証
  3. Apps Script API (gapi.client.script) の読み込み
  4. Apps Script API の実行
pages/index.vue
...
<script>
const CLIENT_ID = '<「3. OAuth 認証情報の作成」で作成した OAuth クライアント ID>'
const SCRIPT_ID = '<[ファイル] > [プロジェクトのプロパティ] > [情報] > [プロジェクト キー]>'
// [ファイル] > [プロジェクトのプロパティ] > [スコープ] に記載されたスコープ
const SCOPES = [
  'https://www.googleapis.com/auth/drive',
  'https://www.googleapis.com/auth/script.external_request',
  'https://www.googleapis.com/auth/script.scriptapp',
  'https://www.googleapis.com/auth/script.send_mail',
  'https://www.googleapis.com/auth/spreadsheets',
  'https://www.googleapis.com/auth/userinfo.email'
]

export default {
  head: () => ({
    script: [
      { src: 'https://apis.google.com/js/client.js' }
    ]
  }),
  async mounted () {
    // 1. Apps Script API の読み込み
    await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/script/v1/rest')

    // 2. OAuth 認証
    gapi.client.init({
      clientId: CLIENT_ID,
      scope: SCOPES.join(' ')
    }).then(async () => {
      // 3. Google アカウント認証
      if (!gapi.auth2.getAuthInstance().isSignedIn.get()) {
        await gapi.auth2.getAuthInstance().signIn()
      }
      
      // 4. Apps Script API の実行
      const result = await gapi.client.script.scripts.run({
        scriptId: SCRIPT_ID,
        resource: {
          function: 'getReservations',
          parameters: []
        }
      })

      // 5. 結果をごにょごにょ
      // JSON.parse(response.result.response.result)
    }, (e) => { console.error(e) })
  }
}
</script>

5. 外部プラグインの追加

Web ページに QR コードの読み取り機能を追加するため、vue-qrcode-reader を導入します3

$ npm install --save vue-qrcode-reader
pages/index.vue
<template>
  ...
  <qrcode-reader
    :track="false"
    @decode="onDecode"
  </qrcode-reader>
  ...
</template>

<script>
...
export default {
  ...
  methods: {
    onDecode (decodedString) {
      // 購入確定する処理とか
    }
  }
}
</script>

Nuxt.js は、デフォルトでは SSR (Server Side Rendering) で動作するため、Web ブラウザ上での動作のみを想定しているライブラリ(document 変数を使っているなど)は正常に動作しません。
公式 FAQ にある通り、クライアントサイドでのみプラグインを使用する設定にしましょう。

plugins/vue-qrcode-reader.js
import Vue from 'vue'
import VueQrcodeReader from 'vue-qrcode-reader'

Vue.use(VueQrcodeReader)
nuxt.config.js
  module.exports = {
    ...
+   plugins: [
+     { src: '~/plugins/vue-qrcode-reader', ssr: false }
+   ]
  }

Web ページの読み込み速度は(おそらく)下がりますが、SSR 自体をやめて SPA (Single Page Application) にしてしまうのも手です。

nuxt.config.js
  module.exports = {
    ...
+   mode: 'spa'
  }

6. Web ページの PWA 化

Nuxt.js プロジェクトを PWA 化するのに便利な PWA Module が提供されているので、これを利用します。

$ npm install --save @nuxtjs/pwa

nuxt.config.js に以下のような manifest を追加し、アプリアイコンとして適当な static/icon.png(推奨サイズは 512×512px)を配置しておけば、PWA として認識されるようになります4

nuxt.config.js
  module.exports = {
    ...
+   modules: [
+     '@nuxtjs/pwa'
+   ],
+   manifest: {
+     name: 'こぐまやん.app',        // スプラッシュ画面に表示されるアプリの名前
+     short_name: 'こぐまやん',      // ホーム画面に追加されるアイコンに付される名前
+     lang: 'ja',
+     theme_color: '#795548',      // ステータスバーの色(Android のみ?)
+     background_color: '#ffffff'  // スプラッシュ画面の背景色
+   }
  }

7. Firebase Hosting へのデプロイ

スマホからアクセスできるように、ホスティングサービスを使って Web ページをインターネットに公開してやります。
無料枠のあるホスティングサービスだけでも、Github Pages やら Netlify やらいくらでも選択肢はありますが、今回は天下の Google 様が提供する Firebase を選びました5

Firebase CLI をインストールしたら、firebase login コマンドで Google アカウント認証を通します。

$ npm install -g firebase-tools
$ firebase login

Google アカウント認証が無事通ったら、Web ブラウザで Firebase Console を開き、[プロジェクトを追加] します。
[プロジェクト名] は任意ですが、ホストされた Web ページの URL は https://<プロジェクト ID>.firebaseapp.com となるため、なるべく覚えやすいものがよさそうです。
fig4.png

次に firebase init コマンドを実行し、以下のように対話形式で Firebase Hosting の設定を行います。

$ firebase init

     🔥🔥🔥🔥🔥🔥🔥🔥 🔥🔥🔥🔥 🔥🔥🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥🔥🔥 🔥🔥🔥🔥🔥🔥🔥🔥     🔥🔥🔥     🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥🔥🔥
     🔥🔥        🔥🔥  🔥🔥     🔥🔥 🔥🔥       🔥🔥     🔥🔥  🔥🔥   🔥🔥  🔥🔥       🔥🔥
     🔥🔥🔥🔥🔥🔥    🔥🔥  🔥🔥🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥   🔥🔥🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥
     🔥🔥        🔥🔥  🔥🔥    🔥🔥  🔥🔥       🔥🔥     🔥🔥 🔥🔥     🔥🔥       🔥🔥 🔥🔥
     🔥🔥       🔥🔥🔥🔥 🔥🔥     🔥🔥 🔥🔥🔥🔥🔥🔥🔥🔥 🔥🔥🔥🔥🔥🔥🔥🔥  🔥🔥     🔥🔥  🔥🔥🔥🔥🔥🔥  🔥🔥🔥🔥🔥🔥🔥🔥

You're about to initialize a Firebase project in this directory:

  *****/reserview-client

? Which Firebase CLI features do you want to setup for this folder? Press Space 
to select features, then Enter to confirm your choices. Hosting: Configure and d
eploy Firebase Hosting sites

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Select a default Firebase project for this directory: <プロジェクト名> (<プロジェクトID>)
i  Using project <プロジェクト名> (<プロジェクト ID>)

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
✔  Wrote dist/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

Firebase initialization complete! と表示されたら Firebase Hosting の設定は完了です。
以下のコマンドを実行し、Web ページを公開しましょう!

$ npm run generate
$ firebase deploy

8. PWA のインストール

スマホの Web ブラウザで https://<プロジェクト ID>.firebaseapp.com を開き、PWA をインストールします。
インストール方法は、ホーム画面にショートカットを作成する方法とまったく同じです。

  • iOS (Safari) の場合:[追加・共有・保存] > [ホーム画面に追加] を選択
  • Android (Chrome) の場合:[メニュー] > [ホーム画面に追加] を選択

インストールが完了すると、ホーム画面にアイコンが追加されます。

できたもの

output.gif
便利っぽい!!!!

補足(+蛇足)

  • 一番ハマったのは、GAS の OAuth 認証、、、困ったら、スクリプトエディタの [公開] > [実行可能 API として導入…] > [更新] してみるのが吉。
  • GAS の実行速度はそこらへんの Web API と比べると見劣りしますが、Google フォーム との親和性の高さなどを踏まえると、選択肢の一つとしては“あり”かなと思いました。
    • Google スプレッドシート(いわゆる Excel 的 UI)が非エンジニアでも閲覧・編集しやすい点も◎。
  • 今回のアプリでは、UI フレームワークに Vuetify を用いました。Material Design 風の画面を簡単に作れます。
  • この記事を見て「面白いことやってんな!」と思ったアナログゲーム愛好家の方は、ぜひ 11/24(土)-25(日) に東京ビッグサイトで開催される「ゲームマーケット2018秋」にお越しください!

参考記事

注意事項

  • 本プロジェクトの利用は、自己責任でお願いします。
  • 「QRコード」は、㈱デンソーウェーブの登録商標です。
  1. “[メニュー] > [検索と置換] > (予約番号 or 名前に応じて IME 切り替え >) 検索クエリを入力 > 該当行の [購入日時] セルにマーク”ってフローがめんどくさいし、他のセルを間違えて上書きしてしまうリスクもありますね。

  2. 2018年10月時点では、iOS では利用不可の模様(なので、クロスプラットフォーム対応が容易、というのもあやしい)です。

  3. 既存 JS ライブラリの Wrapper とはいえ、Vue.js ライブラリには“なさそうであるもの”がそれなりに見つかる印象です(個人の感想です)。

  4. 本来、アプリアイコンとして様々なサイズ(120x120px, 144x144px, 192x192px, ...)の画像ファイルを用意すべきところを、PWA Module では static/icon.png をもとにリサイズしたものを設定しているようです。リサイズによる意図しない文字のつぶれなどが気になる方は、個別に画像ファイルを用意して manifest.icons を設定することもできます(参考:PWA対応をするためにやった最低限のこと)。

  5. 他サービスと比較して、なんとなく可用性が高そうだと思ったため。

50
62
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?