20200701のvue.jsに関する記事は11件です。

firestoreのデータをvue-chartでグラフ化する時にハマった

firestoreからデータを引っ張り出しvue-chartでレンダリングしようとした時に躓き時間をだいぶ割いてしまったので同じように悩んでる人が居れば(多分いない)助けになりたいと思い共有

データの流れ

API(openweathermap)→ firestore → vue-chart

絵画してみる

chart.vue
  mounted() {
    this.renderChart(this.chartData, this.options);
  },

グラフは表示されない
92623f11a023e392b4188d019dbcff63.png
タブの大きさ等変更すると表示される
808f10d38bc132c34a312d9cb31f30da.png

???????????状態

データはしかっりと入っている事を確認できる

affefbd363a48674a6fe3b5d1b49c327.png

色々やってみる①

watchで監視

chart.vue
  watch: {
    data: {
      handler() {
        this.renderChart(this.chartData, this.options);
      },
      deep: true,
      immediate: false
    }
  }

mounted()では表示されずwatchで表示される

色々やってみる②

試しに数字べた書きしてみる

chart.vue
  data() {
    return {
      loaded: false,
      Today: null,
      data: {
        labels: [1,2,3],
        datasets: [
          {
            label: "osaka",
            data: [200,300,400,],
          },

a23f5f27e013c93634edab2ee254cb44.png
当然べた書だとグラフ化に成功

原因

データが完全に移動する前にレンダリングしようとしているのが原因でした。
タブの大きさ変更などで表示されていたのは、その時にはデータが移動完了していたからですね

chart.vue
  <allchart v-if="loaded" :data="data" :options="options" />

chart.vue
        snapshot.forEach(doc => {
          this.data.labels.push(doc.data().Timestamp2);
          this.loaded = true;
        }

これで解決できます

おまけ

公式をしっかり読みましょう

API呼び出しが非同期だということです。 この時、データが到着する前にあなたはチャートを表示しようとしてしまいます。
これを防ぐには、単純な v-ifが最善の解決策です。

Qiita読み漁る前に公式をしっかり読みましょう

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

オンライン会議で使えるOSSアプリケーションを作ってみた

auです。

リモートワークが浸透してきて、もっとデジタル化が進んで近未来を題材にしていた漫画のような世界になるのかなととてもワクワクしています。

image.png

今回は、大学生が1人で自由に機能を追加できるオンライン会議のOSSを作ってみました。

今の私の技術力だとできないこともあり、理想的なものというわけにもいかなかったのですが、いろんな人の意見をいただいたり、協力をし合って使いやすいものにできたらなと思います。

お礼

今回の企画のレビューをしていただいた企業様です。
ゲーム開発のメンバーを募集しているので興味がある方はぜひ。

https://www.find-job.net/list/j126586.html?view=view

https://www.find-job.net/list/j126587.html?view=view

興味深い情報を発信をしているので、フォローして確認してみてください。
https://twitter.com/amazing_engine

環境

npm 6.14.5
vue@2.6.11
@vue/cli 4.4.4

なぜ作ったのか

オンライン会議のアプリを使っていて、こんな機能があったら嬉しいなー、簡単なゲームとかを1つの画面でやれたら面白いなーと思ったのをきっかけに、自由に制作できるようなOSSを作ってみました。

利用方法

起動方法


  1. GitHubからレポジトリをクローンする。

    https://github.com/au-niji/snack-meet/tree/master


  2. モジュールをインストール
    npm install
    


  3. サーバを起動する
    npm run serve
    


  4. localhostにアクセスする。
    image.png

  5. リンクマークを押してZoomの招待リンクを入力する。
    image.png

  6. 必要に応じてファイルを作成し、パネル内のプラスマークからページを追加する
    image.png
    サイトバーからパネルの枚数も変更・編集をすることが可能です。
    image.png
  7. ファイルの追加方法

    ファイルを記述して追加することで、自由にパネル内で利用することができます。

    「src/components/pages」の中にファイルを作成し、MainPageでインポートし、必要箇所を編集することで利用することができます。自動で追加するようにした方が圧倒的に使いやすいと感じているので、後日実装したいと思います。

    1. ファイルを作成する

      今回は「TestPage.vue」というファイル名にしました。

      中身は以下の通りです。

      TestPage.vue
      <template>
        <div class='main'>
          <h1>{{ msg }}</h1>
        </div>
      </template>
      
      <script>
      export default {
        name: 'TestPage1',
        data () {
          return {
            msg: 'testpage1'
          }
        }
      }
      </script>
      
      <style scoped>
      </style>
      

    2. MainPage.vueでインポートする

      MainPage.vueで以下の場所にスクリプトを記述します。

      2-1. scriptタグ内でインポート

      MainPage.vue
      // 省略
      </template>
      
      <script>
      import TestPage1 from './../pages/TestPage1.vue'
      // 省略
      

      2-2. export default内のcomponent

      TestPage1.vue
      TestPage1
      

      2-3. export default内のpaneItems

      idとファイル名.nameを辞書型で追加します。

      MainPage.vue
      paneItems: [ 
        { id: 1, page: TestPage1.name }, // ここを追加する
        { id: 2, page: TestPage2.name }
      ]
      

      2-4. export defaulr内のmethods「paneItemClick」

      MainPage.Vue
      paneItemClick (event, item, place, paneIndex) {
            if (place === 'upper') {
              switch (item.page) {
                // caseのブロックをファイル名に置き換える
                case TestPage1.name: 
                  this.upperPane[paneIndex].loadPage = TestPage1.name
                  break
                case TestPage2.name:
                  this.upperPane[paneIndex].loadPage = TestPage2.name
                  break
              }
              this.menu[3].child[0].child[paneIndex].title = 'Pane ID: ' + (paneIndex + 1)
              this.upperPane[paneIndex].icon = false
            } else if (place === 'under') {
              switch (item.page) {
                case TestPage1.name:
                  this.underPane[paneIndex].loadPage = TestPage1.name
                  break
                case TestPage2.name:
                  this.underPane[paneIndex].loadPage = TestPage2.name
                  break
              }
              this.menu[3].child[1].child[paneIndex].title = 'Pane ID: ' + (paneIndex + 1)
              this.underPane[paneIndex].icon = false
            }
            this.closePaneModal()
          },
      

      文章で書いてみて、やっぱり自動で追加するようにした方が絶対にいいと思いました・・・。issueを発行しておこう。

      ビデオ通話機能の実装

      必要な機能

      ビデオ通話の機能で必要なものは以下の通りです。

      1. 複数人でカメラとマイクを通して通信ができる

      これくらいかなと思ったので、自分でも作ろうとしたのですが、P2Pで複数人で通信するとなると、通信量の計算やAPIの利用となり、難しいと判断しました。

      調べてみるとZoomをiframeで埋め込むことができるようです。

      https://devforum.zoom.us/t/embed-zoom-video-to-a-web-server/2882?page=2

      苦労した点

      iframeでページを開いた際に、マイクとカメラをWebサイト側で許可を出さなければいけません。

      日本語だとヒットせずに、英語で調べると該当記事にたどり着くことができました。

      Meeting.Vue
      <iframe
        v-show="iframe.hidden"
        :src="iframe.src"
        :height="height"
        :width="width"
        scrolling="no"
        frameborder="no"
        allow="geolocation;camera;microphone;"
      >
      </iframe>
      

      参考

      https://blog.addpipe.com/camera-and-microphone-access-in-cross-oirigin-iframes-with-feature-policy/

      自由にファイルを変更できるパネルの追加

      このアプリの肝になる「自由にファイルを入れ替えるパネル」を実装しました。ここではパネルと読んでます。

      必要な機能

      1. 自由に入れ替えることができるパネルの追加
      2. 機能を実装したファイルを挿入する
      3. パネルの領域を可変にする

      自由に入れ替えることができるパネルの追加

      まさにこれだというモジュールがありました。

      https://antoniandre.github.io/splitpanes/

      ボタンを押すことでパネルを追加でき、別々にページを埋め込むことができるようになりました。

      機能を実装したファイルを挿入する

      利用方法の説明で、ファイルの追加方法の通りです。

      パネルの領域を可変にする

      これは利用したモジュールに実装されていたので特に苦労はしませんでした。ありがとうございます。

      image.png

      苦労した点

      ファイルを追加するということは、「コンポーネントをページに埋め込む」ということなのですが、方法がいまいち分からず結構調べました。

      以下のように実装しました。

      MainPage.vue
          <pane :size="100-size">
            <splitpanes>
              <pane class="panes" v-for="underPaneN in underPaneNumber" :key="underPaneN">
                <div v-show="underPane[underPaneN-1].icon">
                  <p>{{ underPaneN }}</p>
                  <svg id="icon" @click="openPaneModal(underPane[underPaneN-1].id, 'under')" aria-hidden="true" focusable="false" data-prefix="far" data-icon="plus-square"</svg>
                </div>
                <component :is="underPane[underPaneN-1].loadPage"> </component>// ここを追加
              </pane>
            </splitpanes>
          </pane>
      

      componentタグにページを追加すれば実装することができました。

      https://qiita.com/myLifeAsaDog/items/233f10591be8ff42cf1d

      完成品

      image.png

      確認している問題点

      画面上部に謎の空白がある

      以下の記事通り、脆弱性が確認されているモジュールがあったため、アップデートしました。

      その後この問題が発生しました。

      原因も特定できていないため困っています。

      https://program-shoshinsya.hatenablog.com/entry/2020/06/19/144905

      今後の展望

      今後は、確認している問題点の解決や仲間が集まってくれたらオリジナルのビデオ通話機能を実装したいなと思っています。

      他にもこんな機能があれば使いやすいということがあれば、issueなりコメントを残していただけると幸いです。

      また、一緒にサービスを考えていく、ものづくりに挑戦してみる、プログラミングが好きという方と一緒に進めていきたいと考えています。
      こちらのTwitterアカウントに連絡をください。

      https://twitter.com/au_prog_shoshi

      感想

      Vueを使ったことがなく、「こんなものを作りたい」という想いに技術力が足りているか不安でしたが、パソコンが固まるくらい調べてなんとか形にすることができました。

      完成したものを見て「私以外のアイデアがもっとあればもっとやりやすいものができたのではないか」とも感じたので、今度は1人ではなく複数人で開発をしてみたいです。

      大学1年からプログラミングを学び始め、現在の大学4年の段階で自分が作りたいものをある程度形にすることができるということが分かったので自信につながりました。

      これからも作りたいものを作っていきたいと思いました。

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

Vue CLIでビルド時にメモリ不足になった場合の対処

スタックトレースの例

<--- JS stacktrace --->
...省略...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

対応

Node.jsのオプションでメモリサイズを拡張します。
package.jsonに埋め込んでもいいですし、

package.json
"build": "node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js build ",

環境変数で設定してもOK

set or export NODE_OPTIONS=--max_old_space_size=4096
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで作るLIFF v2アプリ

はじめに

まず、LIFFとはLINEが提供するウェブアプリのプラットフォームです。現在の最新バージョンは2系となっており、LIFF v2と表記されます。
以下は概要の引用ですが、LIFFを使用することでLINEの機能を活かしたWebアプリを開発することが出来ます。
https://developers.line.biz/ja/docs/liff/overview/

LINE Front-end Framework(LIFF)は、LINEが提供するウェブアプリのプラットフォームです。このプラットフォームで動作するウェブアプリを、LIFFアプリと呼びます。
LIFFアプリを使うと、LINEのユーザーIDなどをLINEプラットフォームから取得できます。LIFFアプリではこれらを利用して、ユーザー情報を活用した機能を提供したり、ユーザーの代わりにメッセージを送信したりできます。

本記事ではLIFFアプリをVue.js + TypeScriptで作成します。

手順

本記事では以下の手順でLIFFアプリを作成します。

  1. チャネルを作成する
  2. Vue.jsのプロジェクトを作成する
  3. LIFFアプリを開発する

1.の手順は主に LINE Developers 上の話を、2.の手順では Vue CLI でVue.jsのプロジェクトを作成する話をします。
上記手順は読み飛ばしても大丈夫という方は3.のLIFFアプリを開発する部分だけをお読みいただけると幸いです。

チャネルを作成する

ngrokを起動

LIFFアプリを作成するためにはWebアプリをhttpsで公開する必要があります。
S3などにホスティングしても良いのですが、本記事ではngrokを使用してローカルで動くVue.jsアプリを公開します。
※ngrokのセットアップは主題から外れるため割愛します。

ngrok http 8080

image.png

https://xxx.ngrok.ioはLIFFのエンドポイントURLとして使用するので控えておきます。

プロバイダーを作成する

  1. LINE Developersにログインする
  2. プロバイダーを作成する

image.png

チャネルを作成する

作成したプロバイダーにLINEログインチャネルを追加します。

image.png

LIFFアプリを追加する

チャネルにLIFFアプリを追加します。

image.png

各設定項目については公式のドキュメントに詳しく書かれています。
エンドポイントURLには前述の手順で控えておいたngrokのURL、https://xxx.ngrok.ioを指定します。

ここまでの手順で以下のようなLIFFアプリが作れているかと思います。

image.png

LIFF ID はアプリ開発時に必要になります。ユーザーは LIFF URL にアクセスすることでこのLIFFアプリを開くことが出来ます。

Vue.jsのプロジェクトを作成する

プロジェクトの作成

本記事ではVue CLIを使用してプロジェクトを作成します。
(プロジェクト名は適宜置き換えてください)

vue create liff-app

プロジェクトの設定は任意のもので構いませんが、本記事では以下のような設定で作成しています。

image.png

ngrokからアクセスできるようにする

以下の設定をpackage.jsonに追加します。

package.json
  "vue": {
    "devServer": {
      "disableHostCheck": true
    }
  }

動作確認

以下のコマンドでVue.jsアプリを起動し、https://xxx.ngrok.ioからアクセスできることを確認します。

npm run serve

ngrok経由でVue.jsアプリを開ければ、LIFFアプリとして実行することが出来ます。
前項の手順で作成したLIFF URL、https://liff.line.me/xxxにアクセスすると以下のように表示されると思います。

image.png

LIFFアプリを開発する

LIFF SDKを組み込む

LINE SDKはCDN経由でのみ配布されています。以下のscriptタグをindex.htmlに追加します。

index.html
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>

この状態でLIFF SDKは使用可能な状態となりますが、せっかくTypeScriptで使用するので型の情報が欲しくなります。
幸運なことにliff-typeという素晴らしいパッケージが公開されているのでこちらを使用します。

ドキュメントにある通り以下のコマンドでインストールし、

npm i -D liff-type

tsconfig.jsonファイルに以下の内容を追記します。
※追記内容は環境に応じて適宜読み替えてください

tsconfig.json
{
  "compilerOptions": {
    "types": ["liff-type"]
  }
}

正しく設定が行えればインテリセンスが利いた快適なLIFFアプリ開発環境が作れると思います。
image.png

LIFFアプリを初期化する

LIFF SDKの機能を使用するためには、はじめにliff.init()を実行する必要があります。
作法として正しいのかは分かりませんが、今はアプリ全体でliffの機能を使用したいためApp.vuecreatedのタイミングで実行する処理を入れてみます。

なお、liff.initの実行にはLIFF IDが必要となります。

App.vue
@Component
export default class App extends Vue {
  @Prop({ type: Boolean, default: false })
  loggedIn = false;

  created() {
    liff.init({
      liffId: 'ここにLIFF IDを入れる'
    })
    .then(() => {
      this.loggedIn = liff.isLoggedIn();
    })
  }
}

LIFFアプリが動作している環境を取得する

<template>
  <div class="liff-data">
    <table>
      <tr>
        <th>OS</th>
        <td>{{ os }}</td>
      </tr>
      <tr>
        <th>Language</th>
        <td>{{ language }}</td>
      </tr>
      <tr>
        <th>LIFF SDK Version</th>
        <td>{{ sdkVersion }}</td>
      </tr>
      <tr>
        <th>LINE Version</th>
        <td>{{ lineVersion }}</td>
      </tr>
      <tr>
        <th>isInClient</th>
        <td>{{ isInClient }}</td>
      </tr>
      <tr>
        <th>isLoggedIn</th>
        <td>{{ isLoggedIn }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class LiffData extends Vue {
  get os() {
    return liff.getOS();
  }

  get language() {
    return liff.getLanguage();
  }

  get lineVersion() {
    return liff.getLineVersion();
  }

  get sdkVersion() {
    return liff.getVersion();
  }

  get isInClient() {
    return liff.isInClient();
  }

  get isLoggedIn() {
    return liff.isLoggedIn();
  }
}
</script>

ユーザのプロフィールを取得する

<template>
  <div class="profile">
    <table>
      <tr>
        <th>IDトークンの生成URL</th>
        <td>{{ token.iss }}</td>
      </tr>
      <tr>
        <th>ユーザーID</th>
        <td>{{ token.sub }}</td>
      </tr>
      <tr>
        <th>チャネルID</th>
        <td>{{ token.aud }}</td>
      </tr>
      <tr>
        <th>トークンの有効期限</th>
        <td>{{ token.exp }}</td>
      </tr>
      <tr>
        <th>IDトークンの生成時間</th>
        <td>{{ token.iat }}</td>
      </tr>
      <tr>
        <th>ユーザー認証時間</th>
        <td>{{ token.auth_time }}</td>
      </tr>
      <tr>
        <th>nonce</th>
        <td>{{ token.nonce }}</td>
      </tr>
      <tr>
        <th>認証方法</th>
        <td>{{ token.amr }}</td>
      </tr>
      <tr>
        <th>表示名</th>
        <td>{{ token.name }}</td>
      </tr>
      <tr>
        <th>プロフィールの画像URL</th>
        <td>{{ token.picture }}</td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td>{{ token.email }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Profile extends Vue {
  get token() {
    return liff.getDecodedIDToken();
  }
}
</script>

QRコードリーダを表示する

2020/07/01現在、QRコードリーダーの表示はiOS版LINEバージョン9.19.0以降では動作しないため、関数が存在するかを確認してから使用する必要があります。

参考:https://developers.line.biz/ja/docs/liff/developing-liff-apps/#opening-qr-code-reader

<template>
  <div class="qr">
    {{ scanText }}
    <button v-on:click="qrScan">
      <span>QRコード読み取り</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class QR extends Vue {
  scanText = "";

  qrScan() {
    if (liff.scanCode) {
      liff.scanCode().then(result => {
        const stringifiedResult = JSON.stringify(result);
        this.scanText = stringifiedResult;
      });
    }
  }
}
</script>

メッセージを送信する

2020/07/01現在、LIFFアプリからメッセージを送信する方法は現在のトークに送信する方法とターゲットピッカーを表示して、ユーザが選んだ相手に送信する方法の2種類存在します。
後者のターゲットを選択する方法はLINE 10.3.0以降でサポートされる機能のため、動作環境で使用可能かを確認してから送信します。

<template>
  <div class="message">
    <input v-model="message" />
    <button v-on:click="sendMessage">
      <span>送信</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Message extends Vue {

  message = 'LIFFへようこそ!';

  sendMessage() {
    liff.sendMessages([{
      'type': 'text',
      'text': this.message
    }]).then(function() {
      window.alert('Message sent');
    }).catch(function(error) {
      window.alert('Error sending message: ' + error);
    });
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js + TypeScriptで作るLIFF v2アプリ

2020/07/02追記

記事を公開した当日に公式のnpmパッケージが公開されました。
https://developers.line.biz/ja/news/2020/07/01/published-liff-npm-package/

上記パッケージを使用することでLIFF SDKの導入と同時に型の情報を得ることが出来るので、本記事の内容よりそちらを推奨します。

はじめに

まず、LIFFとはLINEが提供するウェブアプリのプラットフォームです。現在の最新バージョンは2系となっており、LIFF v2と表記されます。
以下は概要の引用ですが、LIFFを使用することでLINEの機能を活かしたWebアプリを開発することが出来ます。
https://developers.line.biz/ja/docs/liff/overview/

LINE Front-end Framework(LIFF)は、LINEが提供するウェブアプリのプラットフォームです。このプラットフォームで動作するウェブアプリを、LIFFアプリと呼びます。
LIFFアプリを使うと、LINEのユーザーIDなどをLINEプラットフォームから取得できます。LIFFアプリではこれらを利用して、ユーザー情報を活用した機能を提供したり、ユーザーの代わりにメッセージを送信したりできます。

本記事ではLIFFアプリをVue.js + TypeScriptで作成します。

手順

本記事では以下の手順でLIFFアプリを作成します。

  1. チャネルを作成する
  2. Vue.jsのプロジェクトを作成する
  3. LIFFアプリを開発する

1.の手順は主に LINE Developers 上の話を、2.の手順では Vue CLI でVue.jsのプロジェクトを作成する話をします。
上記手順は読み飛ばしても大丈夫という方は3.のLIFFアプリを開発する部分だけをお読みいただけると幸いです。

チャネルを作成する

ngrokを起動

LIFFアプリを作成するためにはWebアプリをhttpsで公開する必要があります。
S3などにホスティングしても良いのですが、本記事ではngrokを使用してローカルで動くVue.jsアプリを公開します。
※ngrokのセットアップは主題から外れるため割愛します。

ngrok http 8080

image.png

https://xxx.ngrok.ioはLIFFのエンドポイントURLとして使用するので控えておきます。

プロバイダーを作成する

  1. LINE Developersにログインする
  2. プロバイダーを作成する

image.png

チャネルを作成する

作成したプロバイダーにLINEログインチャネルを追加します。

image.png

LIFFアプリを追加する

チャネルにLIFFアプリを追加します。

image.png

各設定項目については公式のドキュメントに詳しく書かれています。
エンドポイントURLには前述の手順で控えておいたngrokのURL、https://xxx.ngrok.ioを指定します。

ここまでの手順で以下のようなLIFFアプリが作れているかと思います。

image.png

LIFF ID はアプリ開発時に必要になります。ユーザーは LIFF URL にアクセスすることでこのLIFFアプリを開くことが出来ます。

Vue.jsのプロジェクトを作成する

プロジェクトの作成

本記事ではVue CLIを使用してプロジェクトを作成します。
(プロジェクト名は適宜置き換えてください)

vue create liff-app

プロジェクトの設定は任意のもので構いませんが、本記事では以下のような設定で作成しています。

image.png

ngrokからアクセスできるようにする

以下の設定をpackage.jsonに追加します。

package.json
  "vue": {
    "devServer": {
      "disableHostCheck": true
    }
  }

動作確認

以下のコマンドでVue.jsアプリを起動し、https://xxx.ngrok.ioからアクセスできることを確認します。

npm run serve

ngrok経由でVue.jsアプリを開ければ、LIFFアプリとして実行することが出来ます。
前項の手順で作成したLIFF URL、https://liff.line.me/xxxにアクセスすると以下のように表示されると思います。

image.png

LIFFアプリを開発する

LIFF SDKを組み込む

LINE SDKはCDN経由でのみ配布されています。以下のscriptタグをindex.htmlに追加します。

index.html
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>

この状態でLIFF SDKは使用可能な状態となりますが、せっかくTypeScriptで使用するので型の情報が欲しくなります。
幸運なことにliff-typeという素晴らしいパッケージが公開されているのでこちらを使用します。

ドキュメントにある通り以下のコマンドでインストールし、

npm i -D liff-type

tsconfig.jsonファイルに以下の内容を追記します。
※追記内容は環境に応じて適宜読み替えてください

tsconfig.json
{
  "compilerOptions": {
    "types": ["liff-type"]
  }
}

正しく設定が行えればインテリセンスが利いた快適なLIFFアプリ開発環境が作れると思います。
image.png

LIFFアプリを初期化する

LIFF SDKの機能を使用するためには、はじめにliff.init()を実行する必要があります。
作法として正しいのかは分かりませんが、今はアプリ全体でliffの機能を使用したいためApp.vuecreatedのタイミングで実行する処理を入れてみます。

なお、liff.initの実行にはLIFF IDが必要となります。

App.vue
@Component
export default class App extends Vue {
  @Prop({ type: Boolean, default: false })
  loggedIn = false;

  created() {
    liff.init({
      liffId: 'ここにLIFF IDを入れる'
    })
    .then(() => {
      this.loggedIn = liff.isLoggedIn();
    })
  }
}

LIFFアプリが動作している環境を取得する

<template>
  <div class="liff-data">
    <table>
      <tr>
        <th>OS</th>
        <td>{{ os }}</td>
      </tr>
      <tr>
        <th>Language</th>
        <td>{{ language }}</td>
      </tr>
      <tr>
        <th>LIFF SDK Version</th>
        <td>{{ sdkVersion }}</td>
      </tr>
      <tr>
        <th>LINE Version</th>
        <td>{{ lineVersion }}</td>
      </tr>
      <tr>
        <th>isInClient</th>
        <td>{{ isInClient }}</td>
      </tr>
      <tr>
        <th>isLoggedIn</th>
        <td>{{ isLoggedIn }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class LiffData extends Vue {
  get os() {
    return liff.getOS();
  }

  get language() {
    return liff.getLanguage();
  }

  get lineVersion() {
    return liff.getLineVersion();
  }

  get sdkVersion() {
    return liff.getVersion();
  }

  get isInClient() {
    return liff.isInClient();
  }

  get isLoggedIn() {
    return liff.isLoggedIn();
  }
}
</script>

ユーザのプロフィールを取得する

<template>
  <div class="profile">
    <table>
      <tr>
        <th>IDトークンの生成URL</th>
        <td>{{ token.iss }}</td>
      </tr>
      <tr>
        <th>ユーザーID</th>
        <td>{{ token.sub }}</td>
      </tr>
      <tr>
        <th>チャネルID</th>
        <td>{{ token.aud }}</td>
      </tr>
      <tr>
        <th>トークンの有効期限</th>
        <td>{{ token.exp }}</td>
      </tr>
      <tr>
        <th>IDトークンの生成時間</th>
        <td>{{ token.iat }}</td>
      </tr>
      <tr>
        <th>ユーザー認証時間</th>
        <td>{{ token.auth_time }}</td>
      </tr>
      <tr>
        <th>nonce</th>
        <td>{{ token.nonce }}</td>
      </tr>
      <tr>
        <th>認証方法</th>
        <td>{{ token.amr }}</td>
      </tr>
      <tr>
        <th>表示名</th>
        <td>{{ token.name }}</td>
      </tr>
      <tr>
        <th>プロフィールの画像URL</th>
        <td>{{ token.picture }}</td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td>{{ token.email }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Profile extends Vue {
  get token() {
    return liff.getDecodedIDToken();
  }
}
</script>

QRコードリーダを表示する

2020/07/01現在、QRコードリーダーの表示はiOS版LINEバージョン9.19.0以降では動作しないため、関数が存在するかを確認してから使用する必要があります。

参考:https://developers.line.biz/ja/docs/liff/developing-liff-apps/#opening-qr-code-reader

<template>
  <div class="qr">
    {{ scanText }}
    <button v-on:click="qrScan">
      <span>QRコード読み取り</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class QR extends Vue {
  scanText = "";

  qrScan() {
    if (liff.scanCode) {
      liff.scanCode().then(result => {
        const stringifiedResult = JSON.stringify(result);
        this.scanText = stringifiedResult;
      });
    }
  }
}
</script>

メッセージを送信する

2020/07/01現在、LIFFアプリからメッセージを送信する方法は現在のトークに送信する方法とターゲットピッカーを表示して、ユーザが選んだ相手に送信する方法の2種類存在します。
後者のターゲットを選択する方法はLINE 10.3.0以降でサポートされる機能のため、動作環境で使用可能かを確認してから送信します。

<template>
  <div class="message">
    <input v-model="message" />
    <button v-on:click="sendMessage">
      <span>送信</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Message extends Vue {

  message = 'LIFFへようこそ!';

  sendMessage() {
    liff.sendMessages([{
      'type': 'text',
      'text': this.message
    }]).then(function() {
      window.alert('Message sent');
    }).catch(function(error) {
      window.alert('Error sending message: ' + error);
    });
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS S3からファイルを取得したFlask APIがファイルをレスポンスとして返戻し、Vueでダウンロードする

はじめに

忙しいタイトルをしていて、大変申し訳ありません。
現在、以下のアプリ構成でお仕事をしています。
フロントエンド: Vue.js
バックエンド: Flask (python)

Flaskの方については、これまで全く触ったことがなかったので、新鮮さを噛みしめつつ、日々触っています。
そもそもpythonの方も、ファイルを読み込んでcsvを吐き出してくれるというプチツールを作ったことがあるぐらいですので、多くのことを勉強させてもらっています。

本記事で書くこと

タイトルそのままですが、以下を実現します。
1. Flask(Api)が、AWS SDK for Python (Boto3)を使って、AWS S3からファイルを取得する。
2. 取得したファイルデータをレスポンスに詰め、クライアント(Vue.js)に返却する。
3. クライアント(Vue.js)がレスポンスからファイルをダウンロードする。
Lambda + API GateWayで良いのでは・・・
こんなこともできるんだというぐらいの気持ちで読んでください。

本記事で書かないこと

  • Lambda + API GateWayで上記の流れを実現すること
  • Flaskについてのあれこれ
  • pythonについてのあれこれ
  • aws関連についてのあれこれ
  • Vue.jsについてあれこれ
  • fetch APIのあれこれ

おそらく上記は色々な方が実践済みだと思うので。

なぜやろうと思ったのか

そもそも、今回の機能は amplify で実現する予定でした。
が、「Internet Explorer」の場合は、なぜか、aws credentialの認証情報がamplifyにわたってくれず、s3からファイルを取得しようしたときに、403エラーになるという状況に・・・
そのため、このように実装することにしました。
ですので、「Internet Explorer」を対象外とする場合は、素直にamplifyを使うことをオススメします・・・

環境

  • vue: 2.6.11
  • boto3: 1.12.38
  • Flask: 1.1.2
  • python: 3系

事前準備

  • awscli、boto3をインストールしておく
  • aws configureで、awscliを使えるようにしておく

それでは内容へ

1. Flask(Api)が、AWS SDK for Python (Boto3)を使って、AWS S3からファイルを取得する。

s3からファイルを取得する場合に、以下二つの方法があります。

ローカルにダウンロード
boto3.resource('s3').Bucket(BUCKET).download_file(Filename=KEY, Key=KEY)
メモリ上にダウンロード
boto3.client('s3').get_object(Bucket=bucket, Key=key)

Flaskのローカル(アプリケーション上)にファイルがダウンロードされてしまうと、そのファイルを消したりしないといけないのが面倒なので、後者のメモリ上にダウンロードする方法を採用しました。

ソースコード

s3からファイルを取得する
@app.route('/file', methods=['GET'])
def get_file_from_s3():
    s3 = boto3.client('s3')
    bucket = 'YOUR_BUCKET_NAME'
    key = 'YOUR_FILE_NAME'

    obj = s3.get_object(Bucket=bucket, Key=key)

これで、/fileのエンドポイントを叩くことで、s3からファイルオブジェクトを取得することができました。
このobjの内、Bodyのバイト配列を今回は利用します。

2. 取得したファイルデータをレスポンスに詰め、クライアントに返却する。

それでは、取得したファイルオブジェクトからレスポンスをつくりましょう。
レスポンスはmake_response()でつくります。

ソースコード

取得したファイルオブジェクトからレスポンスをつくって返却してあげる
@app.route('/file', methods=['GET'])
def get_file_from_s3():
    s3 = boto3.client('s3')
    bucket = 'YOUR_BUCKET_NAME'
    key = 'YOUR_FILE_NAME'

    obj = s3.get_object(Bucket=bucket, Key=key)

    response = make_response()

    # レスポンスobjectのdataに取得したファイル情報を設定します。
    # read()で、ファイルを読み込みます。
    response.data = obj['Body'].read()

    # ダウンロード時のファイル名を定義します。
    # quoted_filenameでURLエンコードします。また、日本語のファイル名でもできるように、UTF-8エンコーディングもします。
    quoted_filename = urllib.parse.quote(name)
    response.headers['Content-Disposition'] = "attachment; filename='{}'; filename*=UTF-8''{}".format(quoted_filename, quoted_filename)

    return response

3. クライアントがレスポンス情報をダウンロードする。

それでは、クライアント(Vue.js)でレスポンスのファイル情報をダウンロードしましょう。
今回はFetch APIを利用します。

ソースコード

レスポンスからファイルをダウンロードする
    fetch('http://XXXXXX/file', {
      method: 'GET',
    }).then((response) = >{
      // blob()で、Blob形式にします
      return response.blob();
    }).then((blob) => {
      // IEの場合は、msSaveBlobを使います
      if (window.navigator.msSaveBlob) {
        window.navigator.msSaveBlob(blob, 'ファイル名.拡張子');
      } else {
        const a = document.createElement('a');
        a.download = 'ファイル名.拡張子';
        a.href = URL.createObjectURL(blob);
        a.click();
      }
    })

これで、ファイル名.拡張子のファイルをダウンロードできます。
ファイル名.拡張子Content-Dispositionのfilenameとしたい場合は、
response.headers.get('Content-Disposition)Content-Dispositionを取得できるので、そこから抽出して使うと良いと思います。

参考にさせていただいた記事

Flaskでファイルダウンロードを実現する3つの方法
Python の boto3 で S3 とダウンロード/アップロードする
Djnagoメモ Content-Dispositionのfilenameに日本語をセットする

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

IndexedDB + Vue CLI, Chart.js で、測定値の表示 ファイルのインポート可

概要

前と同様、IndexedDB + Vue CLIで
Dexie.js ライブラリを使用した構成となり。
測定値の登録、chart.jsグラフ表示

・ブラウザ内 IndexedDBデータの エクスポート、インポート機能で
jsonファイル経由で可能で、
別PCや、外出先PCのブラウザにインポートできます。

構成

Chrome 83
Vue CLI
dexie : 3.0.1
vue: 2.6.11
vue-router
chart.js
SinglePageApplication / SPA
Progressive Web Apps / PWA


参考

https://vue-chartjs.org/ja/

https://nori-life.com/vue-cli-chart-js/

npm 追加

Vue CLIで、chart.jsだけでは、描画できず。
vue-chartjs ライブラリも。追加しました。

npm install vue-chartjs chart.js --save

package.json

https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/package.json


chart.js sample

・canvas が、動作しないようで
 chart部分を、コンポートネントにする例で。描画できそうでした
親の呼出し側

chart_sample.vue
https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/src/components/IndexMdats/chart_sample.vue

import ChartView from './ChartView'

//
export default {
  name: 'LineSample',
  data () {
    return {
    }
  },
  components: {
    ChartView
  }
}

・子の chart
ChartView.vue
https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/src/components/IndexMdats/ChartView.vue

import { Line } from 'vue-chartjs'

export default {
  extends: Line,
  mounted () {
    this.renderChart({
      labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
      datasets: [
        {
          label: 'Data-1',
          fill: false,
          backgroundColor: '#FF6384',
          borderColor: '#FF6384',
          data: [40, 39, 10, 40, 39, 80, 40]
        }
      ]
    },
    { responsive: true, maintainAspectRatio: false }
    )
  }
}

・上記の、サンプル グラフの画像
ss-chat-sample-0701a.png

画面

・グラフ
ss-mdat-chart-0701a.png

・リスト
 上部分に、エクスポート、インポートの
 ボタンを配置
ss-mdat-index-0701b.png


Vue components

・create
https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/src/components/IndexMdats/new.vue

・index
https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/src/components/IndexMdats/Index.vue

・chart
https://github.com/kuc-arc-f/vue_spa3b_4mdats/blob/master/src/components/IndexMdats/chart.vue


参考のページ

https://knaka0209.hatenablog.com/entry/indexed_db_4mdats

IndexedDB + Dexie.js で CRUDの作成。ファイルインポート可、Vue CLI版
https://qiita.com/knakaqi/items/765a1fb37a53a26278e9


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

Docker で Rails6 + Webpacker + Vue.js + Vue Router + Vuex + axios環境を整える

はじめに

今回は、Docker上でRuby on Rails6の環境構築をし、さらにwebpackerを用いてVue.jsの環境構築をする方法を紹介したいと思います。私が以前書いた、Ruby on Rails6内で、Vue.js + Vuex + Vue Router + axios環境を整えるをDocker上で行ったものになります。
使用PCはMacを想定しています。Windowsの方は、一部コマンドを読み替えてください。

今回の例では、親フォルダ名をrails_vue_mysql_on_dockerとしています。また、DBはMySQLを採用しています。

Ruby on RailsとVue.jsの環境構築

ターミナルを開きましょう。rails_vue_mysql_on_dockerフォルダを作り、その中にDockerfiledocker-compose.ymlGemfileGemfile.lockを作成していきます。

ターミナル
$ mkdir rails_vue_mysql_on_docker
$ cd rails_vue_mysql_on_docker
$ touch Dockerfile
$ touch docker-compose.yml
$ touch Gemfile
$ touch Gemfile.lock

Dockerfileに以下をコピペします。

Dockerfile
FROM ruby:2.6.5
ENV LANG C.UTF-8
ENV TZ=Asia/Tokyo

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update -qq \
    && apt-get install -y nodejs yarn 

ENV APP_HOME /var/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile $APP_HOME/Gemfile
ADD Gemfile.lock $APP_HOME/Gemfile.lock

ENV BUNDLE_DISABLE_SHARED_GEMS 1
RUN bundle install

docker-compose.ymlに以下をコピペします。

docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
  app: &app_base
    build:
      context: .
    volumes:
      - .:/var/src/app
    ports:
      - "3000:3000"
    links:
      - db
    working_dir: /var/src/app
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    cap_add:
      - ALL # Add all privilege
    container_name: app
    tty: true
    stdin_open: true
    privileged: true
    logging:
      driver: "json-file"
      options:
        max-size: "100k"
    dns:
      - 8.8.8.8
  webpack-dev-server:
    build: .
    command: /bin/sh -c "bin/webpack-dev-server --hot --inline"
    ports:
      - "3035:3035"
    environment:
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    volumes:
      - .:/var/src/app
    tty: true
    stdin_open: true
    depends_on:
      - app
volumes:
  mysql-data:
    driver: local

Gemfileに以下をコピペします。Gemfile.lockは空ファイルのままで大丈夫です。

Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 6.0.3'

準備が整ったので、rails new します。

$ docker-compose run --rm app bundle exec rails new . --force --database=mysql --skip-bundle

処理が進んでいき、以下のメッセージが表示された頃には、Gemfileが更新されています。

Could not find gem 'mysql2 (>= 0.4.4)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

DockerfileAddGemfile&Gemfile.lockのキャッシュから更新があったときのみ検知してbundle installを実行します。そのため、このタイミングでdocker-compose buildをします。

$ docker-compose build 

イメージのビルドが終わったら、次に進みます。
rails newで作られた、database.ymlの一部を変更します。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password #変更 docker-compose.ymlに合わせる
  host: db   #変更 docker-compose.ymlに合わせる

続いて、Vueのインストールや、migrationなどをします。

ターミナル
$ docker-compose run --rm app bundle exec rails webpacker:install
$ docker-compose run --rm app bundle exec rails webpacker:install:vue
$ docker-compose run --rm app bundle exec rails db:create db:migrate
$ docker-compose run --rm app bundle exec rails g controller top top

思いつきのタイミングではありますが、ここで邪魔なmarginとpaddingはリセットしておきましょう。

app/assets/stylesheets/application.css
/*省略*/

* {
  margin: 0;
  padding: 0;
}

続いて、top.html.erbに以下を追記します。この記述のおかげで、top.html.erbと、app.vueが紐付きます。

app/views/top/top.html.erb
<%= javascript_pack_tag 'hello_vue' %> <!-- 追記 -->

どのgetのリクエストが来ても、top#topにつながるようにしておきます。こうすることで、routingをVue Routerで行うことができるようになります。

routes.rb
Rails.application.routes.draw do
  get 'top/top'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'top#top'

  namespace :api do
   #API用のルーティングはここに書く
  end

  get '*path', to: 'top#top', format: false   #getのリクエストをまとめる
end

さて、ここで動作確認をしてみましょう。以下のコマンドを入力して、localhostの3000番ポートにアクセスしてください。

$ docker-compose up

以下の画面が表示されればバッチリです。
スクリーンショット 2020-07-01 11.44.14.png

Vueの開発環境を整理

さて、ここからVueの環境を整えていきます。axiosvue-routervuexなどのインストールをし、その後、app/javascriptの直下にviewsフォルダ、componentsフォルダ、storeフォルダ、routesフォルダを作成します。

ターミナル
[rails_vue_mysql_on_docker] $ docker-compose run --rm app yarn install
[rails_vue_mysql_on_docker] $ docker-compose run --rm app yarn add axios vue-axios vue-router vue-template-compiler vuex vue-eslint-parser
[rails_vue_mysql_on_docker] $ cd app/javascript
[javascript] $ mkdir views components store routes
[javascript] $ ls    // views components store routes が作成されたことを確認する。
 app.vue   channels  components   packs   routes    store     views

続いて、以下のファイルを指定のフォルダ内に作成します。(Home.vueと、About.vue、Header.vueは動作確認のために用意するので、環境構築後の開発で不要と判断したら消して頂いて大丈夫です。

viewsフォルダ内 → Home.vue と About.vue
componentsフォルダ内 → Header.vue
storeフォルダ内 → store.js  //Vuexの設定ファイル
routesフォルダ内 → router.js  //vue-routerの設定ファイル

すなわち、以下のコマンドを打ちます。

ターミナル
[javascript] $ cd views
[views] $ touch Home.vue About.vue
[views] $ cd ../components
[components] $ touch Header.vue 
[components] $ cd ../store
[store] $ touch store.js
[store] $ cd ../routes
[routes] $ touch router.js

必要なファイルが揃いました!続いて、ファイルを編集していきます。
まずはapp.vueを以下のように編集します。ルートとなるvueファイルです。このファイルはデフォルトで作られています。

app.vue
<template>
  <div id="app">
    <Header></Header>
    <router-view></router-view>
  </div>
</template>

<script>
  import Header from "./components/Header.vue";
  export default {
    components: {
      Header,
    },
    data: function() {
      return {
        message: "Hello Vue!",
      };
    },
  };
</script>

<style scoped>

</style>

viewsフォルダ内のHome.vueAbout.vueを編集しましょう。

Home.vue
<template>
  <section id="home">
    <h1>{{ title }}</h1>
  </section>
</template>

<script>
export default {
  data() {
    return {
      title: "Homeです"
    };
  }
};
</script>

<style lang="scss" scoped>

</style>
About.vue
<template>
  <section id="about">
    <h1>{{ title }}</h1>
  </section>
</template>

<script>
export default {
  data() {
    return {
      title: "Aboutです"
    };
  }
};
</script>

<style lang="scss" scoped>

</style>

componentsフォルダ内のHeader.vueを編集しましょう。

Header.vue
<template>
  <header>
    <span>{{ message }}</span>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </header>
</template>
<script>
  export default {
    data() {
      return {
        message: "Headerです",
      };
    },
  };
</script>
<style lang="scss" scoped>
  $background-color: skyblue;
  header {
    width: 100%;
    height: 80px;
    background-color: $background-color;
    display: flex;
    flex-direction: row;
    align-items: center;
    a {
      display: inline-block;
      margin: 0 20px;
    }
  }
</style>

Vuexの設定をしておきます。

store/store.js
 import Vue from "vue";
 import Vuex from "vuex";

 Vue.use(Vuex);

 const store = new Vuex.Store({
   state: {},
   getters: {},
   mutations: {},
   actions: {},
 });

 export default store;

Vue Routerの設定をしておきます。

router/router.js
 import Vue from "vue";
 import Router from "vue-router";
 Vue.use(Router);
 import Home from "../views/Home.vue";
 import About from "../views/About.vue";

 const router = new Router({
   mode: "history",
   routes: [
     //ルーティングの設定
     {
       path: "/",
       component: Home,
     },
     {
       path: "/about",
       component: About,
     },
   ],
 });
 export default router;

最後にhello_vue.jsを編集します。hello_vue.jsはVueCLIでいう、src/main.jsです。

packs/hello_vue.js
 import Vue from "vue";
 import Vuex from "vuex"
 import VueRouter from "vue-router";
 import store from "../store/store.js";
 import router from "../routes/router.js";
 import App from "../app.vue";
 import axios from "axios"; 
 import VueAxios from "vue-axios"; 
 Vue.use(Vuex);
 Vue.use(VueRouter);
 Vue.use(VueAxios, axios);

 document.addEventListener("DOMContentLoaded", () => {
   const app = new Vue({
     store,
     router,
     render: (h) => h(App),
   }).$mount();
   document.body.appendChild(app.$el);
 });

完成!!

以上でRails on Dockerプロジェクト内でVue.js、及びVuex、Vue Router、Axiosを利用するための設定が完了しました!
サーバーを起動してみましょう。docker-compose.ymlで、rails sbin/webpack-dev-serverが同時に起動するように、設定してあるので、以下のコマンドだけで、十分です。foremanを使用しません。

[rails_vue_mysql_on_docker]$ docker-compose up 

以下の画面が表示されたらOKです!
vue-rails.gif

お疲れ様でした!

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

Docker上 で Rails6 + Webpacker + Vue.js + Vue Router + Vuex + axios環境を整える

はじめに

今回は、Docker上でRuby on Rails6の環境構築をし、さらにwebpackerを用いてVue.jsの環境構築をする方法を紹介したいと思います。私が以前書いた、Ruby on Rails6内で、Vue.js + Vuex + Vue Router + axios環境を整えるをDocker上で行ったものになります。
使用PCはMacを想定しています。Windowsの方は、一部コマンドを読み替えてください。

今回の例では、親フォルダ名をrails_vue_mysql_on_dockerとしています。また、DBはMySQLを採用しています。

Ruby on RailsとVue.jsの環境構築

ターミナルを開きましょう。rails_vue_mysql_on_dockerフォルダを作り、その中にDockerfiledocker-compose.ymlGemfileGemfile.lockを作成していきます。

ターミナル
$ mkdir rails_vue_mysql_on_docker
$ cd rails_vue_mysql_on_docker
$ touch Dockerfile docker-compose.yml Gemfile Gemfile.lock

Dockerfileに以下をコピペします。

Dockerfile
FROM ruby:2.6.5
ENV LANG C.UTF-8
ENV TZ=Asia/Tokyo

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update -qq \
    && apt-get install -y nodejs yarn 

ENV APP_HOME /var/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile $APP_HOME/Gemfile
ADD Gemfile.lock $APP_HOME/Gemfile.lock

ENV BUNDLE_DISABLE_SHARED_GEMS 1
RUN bundle install

docker-compose.ymlに以下をコピペします。

docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
  app: &app_base
    build:
      context: .
    volumes:
      - .:/var/src/app
    ports:
      - "3000:3000"
    links:
      - db
    working_dir: /var/src/app
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    cap_add:
      - ALL # Add all privilege
    container_name: app
    tty: true
    stdin_open: true
    privileged: true
    logging:
      driver: "json-file"
      options:
        max-size: "100k"
    dns:
      - 8.8.8.8
  webpack-dev-server:
    build: .
    command: /bin/sh -c "bin/webpack-dev-server --hot --inline"
    ports:
      - "3035:3035"
    environment:
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    volumes:
      - .:/var/src/app
    tty: true
    stdin_open: true
    depends_on:
      - app
volumes:
  mysql-data:
    driver: local

Gemfileに以下をコピペします。Gemfile.lockは空ファイルのままで大丈夫です。

Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 6.0.3'

準備が整ったので、rails new します。

$ docker-compose run --rm app bundle exec rails new . --force --database=mysql --skip-bundle

処理が進んでいき、以下のメッセージが表示された頃には、Gemfileが更新されています。

Could not find gem 'mysql2 (>= 0.4.4)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

DockerfileAddGemfile&Gemfile.lockのキャッシュから更新があったときのみ検知してbundle installを実行します。そのため、このタイミングでdocker-compose buildをします。

$ docker-compose build 

イメージのビルドが終わったら、次に進みます。
rails newで作られた、database.ymlの一部を変更します。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password #変更 docker-compose.ymlに合わせる
  host: db   #変更 docker-compose.ymlに合わせる

続いて、Vueのインストールや、migrationなどをします。

ターミナル
$ docker-compose run --rm app bundle exec rails webpacker:install
$ docker-compose run --rm app bundle exec rails webpacker:install:vue
$ docker-compose run --rm app bundle exec rails db:create db:migrate
$ docker-compose run --rm app bundle exec rails g controller top top

思いつきのタイミングではありますが、ここで邪魔なmarginとpaddingはリセットしておきましょう。

app/assets/stylesheets/application.css
/*省略*/

* {
  margin: 0;
  padding: 0;
}

続いて、top.html.erbに以下を追記します。この記述のおかげで、top.html.erbと、app.vueが紐付きます。

app/views/top/top.html.erb
<%= javascript_pack_tag 'hello_vue' %> <!-- 追記 -->

どのgetのリクエストが来ても、top#topにつながるようにしておきます。こうすることで、routingをVue Routerで行うことができるようになります。

routes.rb
Rails.application.routes.draw do
  get 'top/top'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'top#top'

  namespace :api do
   #API用のルーティングはここに書く
  end

  get '*path', to: 'top#top', format: false   #getのリクエストをまとめる
end

さて、ここで動作確認をしてみましょう。以下のコマンドを入力して、localhostの3000番ポートにアクセスしてください。
※初回のコンパイルは平常時より若干時間がかかることがあります

$ docker-compose up

以下の画面が表示されればバッチリです。
スクリーンショット 2020-07-01 11.44.14.png

次のステップに行く前に、Control + C もしくは$ docker-compose stopコマンドを使って、コンテナを停止させておきましょう。
コマンドについて詳しくはこちらをご覧ください。

Vueの開発環境を整理

さて、ここからVueの環境を整えていきます。axiosvue-routervuexなどのインストールをし、その後、app/javascriptの直下にviewsフォルダ、componentsフォルダ、storeフォルダ、routesフォルダを作成します。

ターミナル
[rails_vue_mysql_on_docker] $ docker-compose run --rm app yarn install
[rails_vue_mysql_on_docker] $ docker-compose run --rm app yarn add axios vue-axios vue-router vue-template-compiler vuex vue-eslint-parser
[rails_vue_mysql_on_docker] $ cd app/javascript
[javascript] $ mkdir views components store routes
[javascript] $ ls    // views components store routes が作成されたことを確認する。
 app.vue   channels  components   packs   routes    store     views

続いて、以下のファイルを指定のフォルダ内に作成します。(Home.vueと、About.vue、Header.vueは動作確認のために用意するので、環境構築後の開発で不要と判断したら消して頂いて大丈夫です。

viewsフォルダ内 → Home.vue と About.vue
componentsフォルダ内 → Header.vue
storeフォルダ内 → store.js  //Vuexの設定ファイル
routesフォルダ内 → router.js  //vue-routerの設定ファイル

すなわち、以下のコマンドを打ちます。

ターミナル
[javascript] $ cd views
[views] $ touch Home.vue About.vue
[views] $ cd ../components
[components] $ touch Header.vue 
[components] $ cd ../store
[store] $ touch store.js
[store] $ cd ../routes
[routes] $ touch router.js

必要なファイルが揃いました!続いて、ファイルを編集していきます。
まずはapp.vueを以下のように編集します。ルートとなるvueファイルです。このファイルはデフォルトで作られています。

app.vue
<template>
  <div id="app">
    <Header></Header>
    <router-view></router-view>
  </div>
</template>

<script>
  import Header from "./components/Header.vue";
  export default {
    components: {
      Header,
    },
    data: function() {
      return {
        message: "Hello Vue!",
      };
    },
  };
</script>

<style scoped>

</style>

viewsフォルダ内のHome.vueAbout.vueを編集しましょう。

Home.vue
<template>
  <section id="home">
    <h1>{{ title }}</h1>
  </section>
</template>

<script>
export default {
  data() {
    return {
      title: "Homeです"
    };
  }
};
</script>

<style lang="scss" scoped>

</style>
About.vue
<template>
  <section id="about">
    <h1>{{ title }}</h1>
  </section>
</template>

<script>
export default {
  data() {
    return {
      title: "Aboutです"
    };
  }
};
</script>

<style lang="scss" scoped>

</style>

componentsフォルダ内のHeader.vueを編集しましょう。

Header.vue
<template>
  <header>
    <span>{{ message }}</span>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </header>
</template>
<script>
  export default {
    data() {
      return {
        message: "Headerです",
      };
    },
  };
</script>
<style lang="scss" scoped>
  $background-color: skyblue;
  header {
    width: 100%;
    height: 80px;
    background-color: $background-color;
    display: flex;
    flex-direction: row;
    align-items: center;
    a {
      display: inline-block;
      margin: 0 20px;
    }
  }
</style>

Vuexの設定をしておきます。

store/store.js
 import Vue from "vue";
 import Vuex from "vuex";

 Vue.use(Vuex);

 const store = new Vuex.Store({
   state: {},
   getters: {},
   mutations: {},
   actions: {},
 });

 export default store;

Vue Routerの設定をしておきます。

router/router.js
 import Vue from "vue";
 import Router from "vue-router";
 Vue.use(Router);
 import Home from "../views/Home.vue";
 import About from "../views/About.vue";

 const router = new Router({
   mode: "history",
   routes: [
     //ルーティングの設定
     {
       path: "/",
       component: Home,
     },
     {
       path: "/about",
       component: About,
     },
   ],
 });
 export default router;

最後にhello_vue.jsを編集します。hello_vue.jsはVueCLIでいう、src/main.jsです。

packs/hello_vue.js
 import Vue from "vue";
 import Vuex from "vuex"
 import VueRouter from "vue-router";
 import store from "../store/store.js";
 import router from "../routes/router.js";
 import App from "../app.vue";
 import axios from "axios"; 
 import VueAxios from "vue-axios"; 
 Vue.use(Vuex);
 Vue.use(VueRouter);
 Vue.use(VueAxios, axios);

 document.addEventListener("DOMContentLoaded", () => {
   const app = new Vue({
     store,
     router,
     render: (h) => h(App),
   }).$mount();
   document.body.appendChild(app.$el);
 });

完成!!

以上でRails on Dockerプロジェクト内でVue.js、及びVuex、Vue Router、Axiosを利用するための設定が完了しました!
サーバーを起動してみましょう。docker-compose.ymlで、rails sbin/webpack-dev-serverが同時に起動するように、設定してあるので、以下のコマンドだけで、十分です。foremanを使用しません。

[rails_vue_mysql_on_docker]$ docker-compose up 

以下の画面が表示されたらOKです!
vue-rails.gif

お疲れ様でした!

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

Vuetifyで作った画面のテストをCypressで自動化しようとした時に調べたことや、つまづいたこと

はじめに

Vue.js、Vuetifyで作った画面をCypressでどうにか自動化したいと、いろいろ触って調査して気づいた点を書いていこうと思います。

Cypressについて

Cypressは、E2Eテスティングフレームワークです。

公式サイト

Cypress導入手順

yarn install

yarn install cypress --save-dev

testsフォルダにe2eフォルダを配置

tests/e2e/fixtures
tests/e2e/plugins
tests/e2e/screenshots
tests/e2e/specs
tests/e2e/support
tests/e2e/videos

plugins/index.js

各フォルダ設定

module.exports = (on, config) => {
  return Object.assign({}, config, {
    fixturesFolder: 'tests/e2e/fixtures',
    integrationFolder: 'tests/e2e/specs',
    screenshotsFolder: 'tests/e2e/screenshots',
    videosFolder: 'tests/e2e/videos',
    supportFile: 'tests/e2e/support/index.js'
  })
}

support/index.js

Cypressのカスタムコマンドを追記するファイルの指定

import './commands'

サンプルテストコード

  • tests/e2e/spec/ フォルダ配下に、テストファイルを書く。
    • 表記ルールは特に無い。日本語でもOK(例:検索画面test.js)
describe.skip('検索機能のテスト', () => {
  // テストケース(it関数)の直前で実行される
  beforeEach(() => {
    // Cypressコマンドのタイムアウト値を60秒に設定。(デフォルトは4秒)
    Cypress.config('defaultCommandTimeout', 60000);
    // 画面のサイズ指定
    cy.viewport(1940, 1080);
    // テスト開始時、どのURLにアクセスするかを指定
    cy.visit('http://localhost:8080');
    // ログインをするための、カスタムコマンド(後述)
    cy.login('user');
  });
  // テストケース(it関数)の直後で実行される
  afterEach( () => {
    cy.contains('ログアウト')
      .click();
  });

  // テストケース
  it('検索ができること', () => {
    // 目的の画面へ移動するためのカスタムコマンド(後述)
    cy.gotoTestPage();
    cy.wait(2000);
    cy.contains('検索')
      .click();

  });
});

テスト実行

画面を見ながらのテスト

yarn run cypress open

以下のCypressの画面が開くので、テスト実行したいファイルをクリックします。

image

実行すると、このような画面が開く、テストコードの通りに実行が走ります。
image

Cypressの公式ページ
も参考にして下さい。

コマンドライン上でのテスト

headless オプションを付けることにより、コマンドライン上で実行が可能です。

shell script
yarn run cypress run --browser chrome --headless

image

  • テストが途中で実行終わると、失敗時のスクリーンショットが e2e/screenshots/に保存されます
  • テスト実行の動作は、e2e/videos/ にmp4が保存されます

カスタムコマンド作成

  • AppActionsという、Cypressに独自のコマンド(メソッド)を作成することができる機能。
  • これにより以下の効果が期待できる
    • 内部の複雑なDOM操作などを抽象化して、テストコード自体を見やすくする
    • 特定のDOM操作を抽象化することで、idやinnerTextなどに変更があっても、カスタムコマンド内を修正するだけでよくなる(=保守性向上)

Cypress公式ページ

以下の記事も参考にしてください

サンプルコード

e2e/support/commands.js に以下の様に記載することにより、
テストコード内で、cy.login() cy.gotoTestPage() と使用することができる。

Cypress.Commands.add('login', username => {
  cy.get('[name=USER_ID]')
    .type(username);

  cy.get('[data-test=login]')
    .click();
});

Cypress.Commands.add('gotoTestPage',() => {
  cy.get('.navbar > div > .btn')
    .click();

  cy.contains('テストページ')
    .click();
});

Cypressのテストコードでの注意点とかテクニック

v-selectでselect()が動作しない

type を使わないと駄目っぽい。

内部的にはselectタグでは無くて、別のもので動いているから?

automated tests - How to find element and select by cypress.io with vue.js v-select? - Stack Overflow

  <v-select
    :data-test='test'
    :items="selectItems"
    item-text="name"
    item-value="val"
    dense
    outlined />
cy.get('[data-test=test]').type('item01{enter}', {force: true})

ここの {enter} は特殊文字で、Enterを入力する操作扱いとなる。

参考:type | Cypress Documentation

v-text-field で値を探す時

v-text-fieldは、HTMLにレンダリング後、labelとinputタグが並んで作られる

containsでlabelを探した後に、兄弟要素を探す next、prev でinputタグを取得できる

  cy.contains('label', '取引先コード')
    .next('input')
    .type('001');

面倒だったら、data-* 属性を付与して get でもいける。

Cypress公式のベストプラクティスでは、data-* を付与してテストするほうが、HTML、CSSに依存しないので良いとしている。

Best Practices | Cypress Documentation

ただし、要素内のテキストに変更があった場合にテストが失敗して欲しい時は、cy.contains を使うべきだとも言っている。

  • 送信ボタン → 保存ボタン に変わった場合
    • 振る舞い自体が変わることになるので、テスト失敗して欲しい → contains
    • 失敗しないで欲しい → cy.get([data-cy=hogehoge])

このあたりはどうするかは、プロジェクトの方針にもよるかもしれません。

ちなみにまだ試してませんが、テストだけに使う data-test などの属性をプロダクション環境時に削除する方法はあるそう。

vue-loader 15で、テンプレート内の任意の属性(data-testなど)を除外する - Qiita

v-checkbox でチェックを入れたり外したりする時は、force: trueオプション必要

check、uncheck を使えば良いと書いてあるが、普通にやってもうまくいかない。
forceオプションを入れると、うまくいく。

cy.get('[data-test=hoge]')
  .check({force: true,});

cy.get('[data-test=hoge2]')
  .uncheck({force: true,});
}

途中でテストがこけると、それ以降のテストはスキップされる

  • デフォルトの動作がそういうものらしい

javascript - Cypress: how to mark a single test as failed but continue to run other tests? - Stack Overflow

タイムアウト値の設定を延ばす

Cypressのコマンドのタイムアウト値は、デフォルト4000msです。

SPAで取得したい要素がロードされる前にタイムアウトする場合は、個別にタイムアウト値を設定することができます。

cy.get('button', { timeout: 30000 })

それすらも面倒だなと思った場合は、 defaultCommandTimeout を延ばすことができます。

beforeEach(() => {
    Cypress.config('defaultCommandTimeout', 60000);
});

参考

ボタンのラベルの前後に空白が入っている場合の対処

これはCypressというより、Vue、Vuetifyの仕様の話だと思うけど、

ダイアログの登録ボタンをクリックしたいということで、
cy.contains('登録').click()' を書いたけど、何回も失敗してた。

consoleを確認すると、親ページの方で「登録確認」ボタンがあったので、そちらを取得してしまっていたのが原因だった。

containsは、指定テキストにマッチした一番最初の要素を取得するので、登録ボタンをピンポイントに取得する必要があったので、正規表現を使うことにした。

cy.contains(/^登録$/).click()'

だけど、これだと何も取得できなくてエラーとなる。

そんなわけないだろうと思って、検証ツール上で生成されたHTMLのtextを確認すると、

        登録

なぜか前後に余計な空白が…。

cy.contains(/^ +登録 +$/).click()'

そういうものだと諦め、前後スペース有りの正規表現でなんとか取得できた。

テーブルに出てくる検索結果にアサーションを掛けたい場合

検索画面などで、検索ボタンを推してからテーブルに表示される結果にアサーションをしたい、なんてことがあると思います。

contains、parent、withinの組み合わせでなんとかできます。

取引先コード002で検索したら、検索結果にちゃんと出てきて、テスト株式会社 という文字列が含まれているかどうかを確認したい場合、以下のようなコードでアサーションができます。

// 取引先コード002の要素(td)の、親要素の行(tr)を取得し、withinでそのtr要素内でアサーションします。
cy.contains('002').parent('tr').within(()=> {
  cy.get('td').contains('テスト株式会社').should('be.visible');
});

should('be.visible') は取得した要素が表示されているかどうかをアサーションするコマンドです。

検索結果の任意の列の結果を見たいときもあると思います。
6列目の値が2020-07-01が入っていることを期待する場合は、以下のような感じになります。

cy.contains('002').parent('tr').within(()=> {
  cy.get('td').eq(5).should('eq', '2020-07-01');
});

eqコマンドが、複数要素がある場合の何個目を取得する、といった形になります。

0から始まるので、配列とほぼ一緒と考えて良いと思います。

参考

余談ですが、単体のコマンドの説明だけではなく、こういったやりたいことが公式ドキュメントに整備されているのが、とても好感が持てます。

うまく要素が取得できないな? と思ったら

Cypressを使っていると、各コマンドが充実しており結構使いやすく感じます。

しかし、そもそもE2Eテストのコードを書いた経験が無いとか、HTMLとかにそこそこ詳しくないと、要素の取得とかで普通にハマります。(フロントエンド得意系の人だともっと早く解決できるのかもしれません)

そうなったときによくやるのが、cypress open でブラウザ表示した状態でテストし、Consoleで結果を確認していきます。

cy.get('button') と書いた場合、全てのテストが終わるか、もしくはコマンド終わった辺りでテストをStopし、ブラウザでコマンドが実行されたところをクリック

image

検証ツールのConsoleから、どんな要素が取得できているかを確認できます。

image

そもそもこの時に何も取得できていなかったり、取得したい要素が無かったりしたら、指定が間違っている可能性があります。

おわりに

癖みたいなものはありますけど、基本的に使い勝手は良いと感じています。
v-selectやv-text-fieldの要素取得のあれこれは、カスタムコマンドにまとめてしまえば他の人も扱いやすくなると思うので、本格的に導入を始めたらやっていければいいかなと思っています。

参考記事

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

GithubActionsでVueアプリをS3にデプロイしてみた

はじめに

前回まででGithubActiosでBeanstalkやEC2の自動デプロイを行えるようにしてきました。
今回は、VueCLIで作ったフロントエンドアプリのデプロイをGithubActionsで自動化してみます。

具体的には

  • S3にデプロイ
  • CloudFrontのキャッシュクリア(invalidation)

の作業を自動化します。
CloudFrontはACMを紐付け、S3のリソースをHTTPSで外部にホストするために利用しています。

なお、VueCLIやS3の初期セットアップ方法についてはここでは触れません。
既にVueアプリをS3で運用していて、GithubActionsでデプロイを自動化したい人向けの内容になります。(Vueに限らずnpm run build でビルドできるReactなども同様です)

前提

  • S3でVueアプリをホストしている
  • Githubアカウントがある

開発環境

macOS Catalina
node v12.10.0
npm 6.10.3

大まかな作業の流れ

  1. GithubActionsのworkflowを作成する
  2. Secretsに必要なキー、値を登録する
  3. pushして自動デプロイを確認

GithubActionsのworkflowを作成する

今回作成する workflow のStepsの概要は以下の通りです。

  1. Vueアプリケーションをビルドする
  2. ビルドされたファイル群をS3にデプロイ(Sync)する
  3. CloudFrontのキャッシュをクリア(invalidationを作成)する

実際に作成したファイルは以下になります。
(作成手順は後に説明します)

deploy-s3.yml
name: deploy to S3

on:
  push:
    branches: [ master ]

jobs:
  build:
    name: build and deploy to s3
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test

    - name: s3 sync
      uses: jakejarvis/s3-sync-action@master
      with:
        args: --acl public-read --follow-symlinks --delete
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: 'ap-northeast-1'
        AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
        SOURCE_DIR: 'dist'

    - name: invalidate cloudfront
      uses: chetan/invalidate-cloudfront-action@master
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: 'ap-northeast-1'
        DISTRIBUTION: ${{ secrets.AWS_CF_DISTRIBUTION }}
        PATHS: '/*'

作成手順は以下になります。

雛形を使ってworkflowファイルを作成

workflow は

  • YAML形式で記述する(ファイル名は任意)
  • .github/workflows ディレクトリ内に置く(複数配置可)

ことで実行できるようになります。
手で作成してもよいですが、ブラウザ上からテンプレートを使って作ると間違いもなく簡単です。

今回は最初にVueアプリのビルドを行いたいので、 Node.js の雛形を利用してみます。
(VueプロジェクトがGithubにPushされていない場合はPushしてRepositoryに登録しておきます)

  1. GithubのRepositoryの Actions タブから New workfow をクリックします。
    スクリーンショット 2020-06-30 22.51.22.png

  2. Node.js の雛形を探して Set up this workflw をクリックします。
    スクリーンショット 2020-06-30 22.48.14.png

  3. npm run build を含むテンプレートが表示されるので Start commit をクリックしてコミットコメントを記入して Commit new file をクリックします。
    スクリーンショット 2020-07-01 0.02.10.png

  4. localの開発環境で先ほどコミットした workflow ファイルを master から pull して取得します。

これで雛形となる workflow ファイルを作成できました。
以降は local で workflow ファイルを修正していきます。

Node.jsの雛形をカスタマイズする

Node.js の雛形は、複数のNodeバージョンでのビルド&テストを実施する workflow のため

  • Pull request もトリガとなっている
  • npmのビルドが v12.x だけでなく、 10.x 14.x も実行されてしまう

という点が余計のため、これらを削除して以下のようにしました。

deploy-s3.yml
name: deploy to S3

on:
  push:
    branches: [ master ]

jobs:
  build:
    name: build and deploy to s3
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test

これで master にPushされた時に node v12.x でのクリーンインストール&ビルド&テストのみが動きます。
このビルドでプロジェクト直下の dist というディレクトリ内に、ビルドされたファイル群が出力されます。

S3にデプロイするStepを追記する

S3へのデプロイは marketplace にサードパーティ製の Action があったのでそれを利用します。
https://github.com/marketplace/actions/s3-sync
スクリーンショット 2020-07-01 0.41.27.png

ここの例に従って引数やパラメータを指定したものが以下になります。

    - name: s3 sync
      uses: jakejarvis/s3-sync-action@master
      with:
        args: --acl public-read --follow-symlinks --delete
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: 'ap-northeast-1'
        AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
        SOURCE_DIR: 'dist'

env でよく使うものを以下に抜粋したので参考にしてください。

キー 設定する値 必須 デフォルト値
AWS_ACCESS_KEY_ID IAMのアクセスキーID(取得方法は後述の補足2参照) Y -
AWS_SECRET_ACCESS_KEY IAMのシークレットアクセスキー( 〃 ) Y -
AWS_S3_BUCKET S3のバケット名 Y -
AWS_REGION S3のリージョン名 N us-east-1
SOURCE_DIR S3にアップロードするディレクトリ N ./ (リポジトリのルート)
DEST_DIR アップロード先のS3のディレクトリ N / (S3のバケットのルート)
補足1:セキュアな情報は Secrets に登録して参照する

値の指定で ${{ secrets.〜 }} とある記述は、Repositoryの Secrets に登録した値を参照しています。IAMのアクセスキーなどのセキュアな情報は workflow ファイル内にベタ書きしないようにします。
(Secretsの登録手順は後述します)

補足2:IAMのアクセスキーIDとシークレットアクセスキーの取得方法

IAMのアクセスキーIDとシークレットアクセスキーは、AWSのコンソールにIAMでログイン後、ツールバーのアカウント名のメニューから マイセキュリティ資格情報 > アクセスキー > 新しいアクセスキーの作成 から生成することができます。
スクリーンショット 2020-07-01 1.33.33.png
*これはAWS CLIなどを使ってAWSのリソースを操作する際に使うセキュアな情報です。
*シークレットアクセスキーは、初回に作成時しか表示されないので注意してメモります。

CloudFrontのキャッシュをクリアするStepを追記する

*CloudFrontを使ってない場合はこのStepは省略してよいです。
S3の前にEdgeサーバーとしてCloudFrontを使っている場合は、キャッシュをクリアしないと更新したS3のコンテンツがしばらく配信されません。
いつもは手動で行なっていたキャッシュクリア(invalidation)も自動化します。

例によってmarcketplaceにサードパーティ製の Action があったのでそれを利用します。
https://github.com/marketplace/actions/invalidate-cloudfront
image.png
ここの例に従って引数やパラメータを指定したものが以下になります。

    - name: invalidate cloudfront
      uses: chetan/invalidate-cloudfront-action@master
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: 'ap-northeast-1'
        DISTRIBUTION: ${{ secrets.AWS_CF_DISTRIBUTION }}
        PATHS: '/*'

env で指定しているキーは以下を参考にしてください。

キー 設定する値 必須
AWS_ACCESS_KEY_ID IAMのアクセスキーID(先述と同じ) Y
AWS_SECRET_ACCESS_KEY IAMのシークレットアクセスキー( 〃 ) Y
AWS_REGION リージョン名 Y
DISTRIBUTION 対象のCloudFrontディストリビューションID Y
PATHS キャッシュクリアする1つ以上のパスのリスト(スペース区切りで複数指定可) Y

以上で、冒頭の workflow ファイルの作成が完了しました。

Secretsに必要なキー、値を登録する

workflow 内のパラメータの指定で ${{ secrets.〜 }} とある記述は、Repositoryの Settings > Secrets に登録した値を参照しています。
IAMのアクセスキーなどのセキュアな情報は workflow ファイル内にベタ書きせず、ここに登録して参照するようにします。
image.png
Secrets の値は、仮に echo などで出力してもマスキングされるので安全です。

pushして自動デプロイを確認

Vueアプリのリソースの一部に変更を加えて master にCommit&Pushします。
その後、Githubの Actions タブで自動デプロイの状況を確認してみます。
image.png
このように、オールグリーンなら workflow は成功です!
実際にブラウザからも正常に変更が反映されていることが確認できるはずです。

あとがき

S3へのリソースのアップロード(Sync)、その後のCloudFrontのキャッシュクリアという地味に面倒な作業から開放されました!!
AWSを対象としたCI/CDの工程で必要なStepは、大抵公式かmarketplaceでActionが公開されているので、簡単に workflow が作れるのも手軽でよいですね。

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