小さなエンドウ豆

まだまだいろいろ勉強中

Slack(Bolt)から Github Actions を実行

Github Actions を Slack から実行

背景

先日 Github Actions を使って Nuxt アプリケーションを S3 にアップロードし CloudFront で配信するワークフローを作りました。

h-piiice16.hatenablog.com

ただ、このワークフローではブランチが固定になっており、固定の内容しかデプロイすることが出来ません。
これだと検証用のブランチをステージング環境にデプロイしたいというシチュエーションで困ります。

ブランチの分だけワークフローを書くのも一つの手ですが、できればワークフロー実行時にブランチ名を指定したい…
と探してたところ、ワークフロー実行のトリガーを deployment にすることでブランチを可変にできるという記事を見つけました。

qiita.com

この記事によると Github API の Create a deployment を叩くとイベントが発火し、ワークフローを実行することができます。
また、Create a deployment のリクエスト時にブランチ名をパラメータに渡すこととで、そのブランチの内容でワークフローが実行されるそう。

これで万事解決するのだが、Slack アプリと組み合わせてより簡単にワークフローを実行できないかと思いつきやってみることにしました。

Slack Bolt

Bolt とは Slack アプリを作るためのフレームワークです。
開発言語は Node.js で Bot や自動返信、Slack コマンドなどを作ることが出来ます。

Bolt の入門ガイドが秀逸で、簡単なアプリを 1 時間もかからずに作れるようになります。

slack.dev

今回作るアプリは Nuxt アプリをデプロイするためのアプリです。
流れとしては以下のような感じです。

  1. /deploy という Slack のコマンドをアプリに向けて打つ
  2. 完成イメージにあったようなセレクトボックスが出くる
  3. ブランチを選ぶと Github API にリクエストする
  4. ワークフローが実行されデプロイ

f:id:h-piiice16:20200125222904p:plain
完成イメージ

Bolt 側のソースコードが以下です。

const { App } = require('@slack/bolt')
const https = require('https')

const githubToken = process.env.GITHUB_TOKEN
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  githubToken: githubToken
})

const url = 'https://api.github.com/repos/hiracky16/nuxt-app-test/branches'
const options = { headers: { 'User-Agent': 'Mozilla/5.0' } }

const deployUrl = "https://api.github.com/repos/hiracky16/nuxt-app-test/deployments"
const deployOptions = {
  method: 'POST',
  headers: { 'User-Agent': 'Mozilla/5.0', 'Authorization': `token ${githubToken}` }
}

app.command('/deploy', async ({ command, ack, say }) => {
  ack()
  https.get(url, options, (response) => {
    let body = '';
    console.log('STATUS: ' + response.statusCode);
    response.setEncoding('utf8');
    response.on('data', (chunk) => { body += chunk })

    response.on('end', (res) => {
      res = JSON.parse(body);
      const branches = res.map(r => { text: r.name, value: r.name })
      say({
        text: 'どのブランチをデプロイしますか?',
        response_type: 'in_channel',
        attachments: [{
          text: 'ブランチを選んでください',
          attachment_type: 'default',
          callback_id: 'select_branch',
          actions: [{
            name: 'branch_list',
            text: 'branches',
            type: 'select',
            options: branches
          }]
        }]
      })
    })
  }).on('error', (e) => {
    say({text: 'ブランチが取得できませんでした。'})
  })
})

app.action({ callback_id: 'select_branch' }, ({ body, ack, say }) => {
  const value = body.actions[0].selected_options[0].value
  ack()
  say(`${value} でデプロイします!`)
  const postData = JSON.stringify({"ref": value})
  const request = https.request(deployUrl, deployOptions, (response) => {
    response.on('data', (chunk) => { console.log(`BODY: ${chunk}`) });
    response.on('end', () => say({text: `${value} でデプロイはじめました。`}));
  }).on('error', (e) => say({text: 'デプロイに失敗しました。'}))
  request.write(postData)
  request.end()
});

(async () => {
  // Start your app
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

メッセージの形式は以下のサイトにテンプレートがたくさんあるので参考にしました。

https://api.slack.com/tools/block-kit-builder

まずはブランチ一覧をリモートリポジトリから取得します。
それをセレクトボックスにセットし、選ばせます。
選ぶと app.action() が実行され、deployment にリクエストが行き、Github Actions が実行されます。

GITHUB_TOKEN は API を使うために必要だったため「Settings > Developer settings > Personal access tokens」から作りました。
API を叩く際に User Agent を設定しないとエラーになっていしまったためブランチ取得時、Deploy 時どちらも設定しています。

Slack のコマンドを作る際は Slack App の設定画面へ行き「Slack Command」で作ることが出来ます。

f:id:h-piiice16:20200125231319p:plain
コマンド作成画面

余談ですが、入門ガイドでもあった ngrok で localhost:3000 を外部公開するやり方だと立ち上げ直すたびに URL が変わってしまい、
コマンドの Webhooks に加えて Event Subscriptions や Interactive Components のリクエスト URL も変えなければならないため大変でした。。

これで完成イメージのようなアプリが出来上がります。

Github Actions 側の修正

これは Qiita を参考に on 句をpush から deployment に変更しました。

on:  on:
+ deployment:
-  push:      deployment:
-    branches:  
-      - master

これにより Slack 側で /deploy コマンドを実行するだけで好きなブランチの内容でデプロイが可能になりました。

まとめ

今回は Slack コマンドから Github Actions を呼び出して Nuxt アプリをデプロイすることに成功しました。
Slack コマンドから Github Actions が呼び出せるということはデプロイだけでなくテストや他のことを Slack を通じて実行できることがわかります。
また Bolt は他のことにも大いに応用できるので今後もネタが付きないと思われます。

Vuex の型定義全部書いてみた

Vuex の型定義全部書いてみた

Vuex + TypeScript での問題点

  • getters の型定義が any
  • mutations / actions の payload が any 型

payload とは以下のような mutation があったとすると、第一引数が state オブジェクトでそれ以外の引数のことを言います。

mutations: {
  increment (state, n) {
    state.count += n
  }
}

これから具体的な例をもとに深ぼっていきます。

getters の定義が any

例えば getter 関数の第2引数 getters を利用することで他の getter 関数への参照を持つことが出来ます。
ただ getters 自身が any 型となってしまうためコンパイル時に型が違う点に気づくことが出来ません。

state: {
  name: null as string | null
},

getters: {
  getName(state) {
    return state.name
  },
  greet(state, getters) {
    return `My name is ${getters.getName.toUpperCase()}.`
    // ここでインラインアサーションを使うとアンチパータン
    // 実装と型が違うケースがあるため
    // 例: return `My name is ${(getters.getName as string).toUpperCase()}.`
  }
}

payload のスキーマ間違え

mutation, action 関数の payload は any 型です。
そのため型アノテーションを付けたとしても実行側はこのアノテーションに関与しないためダウンキャストとなってしまいます。

mutations: {
  setName(state, payload: string) { // ダウンキャスト
    state.name = payload
  }
},
actions: {
  asyncSetName(ctx, payload) {
    ctx.commit('setName', {name: payload}) // スキーマ違い
  }
}

存在しない action 関数の dispatch

これはあるあるかもですね。
commit と dispatch を間違える場合も考えられます。

これらも TypeScript で検知したいところ。

解決へのアプローチ①: 公式型定義を使う

Vuex の types の中に型を見ると以下のようになっている。

export type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;
export type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>
export type Mutation<S> = (state: S, payload: any) => any;

export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>;
}

export MutationTree<S> {
  [key: string]: Mutaion<S>;
}

export ActionTree<S, R> {
  [key: string]: Action<S, R>;
}

state の型が総称型になっているため実装側で指定が可能です。
[key] { process } みたいな形で積み上げることによって store インスタンスの方を表現することが出来ます。
またインデックスシグネチャを使うとすべての関数の第一引数に State 型を付与出来ます。

ただ、依然として getters が any だったり、 mutation, action の payload が any であるため解決したとは言えません。
完全に解決するためには独自に型を作って付与していくしかなさそうです。

getters の型を解決する

予め getters の要件を interface で明示しておきます。
また Getter 型を再定義することで getters の interface を利用します。

interface IGetters {
  double: number
  expo2: number
  expo: (amount: number) => number
}

type Getters<S, G> = {
  [K in keyof G]: (state: S, getters: G) => G[K]
}

ここで in

mutations の型を解決する

Getters と同じように interface を用意します。
ここで用意するのは payload に対する interface です。

interfece IMutations {
  setCount: { amout: number },
  increment: void
}

type Mutations<S, M> = {
  [K in keyof M]: (state: S, payload: M[K]) => void
}

const mutations: Mutations<State, IMutation> = {
  setCount(state, payload) => {...},
  increment(state) => {...}
}

actions の型を解決する

action 関数はこれまで定義した getters と mutations への参照があります。
まず interfece を定義するが、これは mutations と同じように payload に対して定義します。
次に Actions の型を定義するのですが、action 関数では第一引数に ctx が渡されるため Context 型を作成します。
これには store 内のすべてのオブジェクトを型として定義する必要があります。
また commit や dispatch も以下のように表すことができます。

interface IActions {
  asyncSetCount: { amount: number },
  asyncIncrement: void
}

// Promise オブジェクトを返す関数もあるので any とする
type Actions<S, A, G={}, M={}> = {
  [K in keyof A]: (ctx: unknown, payload: A[K]) => any
}

type Context<S, A, G, M, RS, RG> = {
  commit: Commit<M>,
  dispatch: Dispatch<A>,
  state: S,
  getters: G
}

type Commit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void

type Dispatch<A> = <T extends keyof A>(type: T, payload?" A[T]) => any

const actions: Actions<State, IActions, IGetters, IMutations> = {
  asyncSetCount(ctx, payload) {
    ctx.commit('setCount', { amount: payload.amount })
  },
  asyncIncrement(ctx) {
    ctx.commit('increment')
  }

定義してきた型を別ファイルで管理してもいいかもです。

まとめ

Vuex に型をつけたい一心で調べた公式が提供している型だけだと不十分だということがわかりました。
ここまでして型をつけるかは費用対効果を考えなくてはいけませんね。。
Decorator を使って module ライクに Vuex を書き換える方法もあるので次はそれを記して行きたいと思います。

参考: 実践TypeScript ~ BFFとNext.js&Nuxt.jsの型定義~ https://www.amazon.co.jp/%E5%AE%9F%E8%B7%B5TypeScript-BFF%E3%81%A8Next-js-Nuxt-js%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9-%E5%90%89%E4%BA%95-%E5%81%A5%E6%96%87/dp/483996937X/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=typescript&qid=1579435873&sr=8-1

Rails アプリケーションから gRPC サーバー に接続するまで

Rails アプリケーションから gRPC サーバーに接続するまで

そもそも RPC とは

あるコンピュータで動作するソフトウェアから、通信回線やコンピュータネットワークを通じて別のコンピュータ上で動作するソフトウェアへ処理を依頼したり、結果を返したりするための規約。

要するに別アプリにある処理(関数)を呼び出すこと。
API はこれに当たるのかなと思っています。

gRPC とは

gRPCはRPCを実現するためにGoogleが開発したプロトコルの1つで、インターフェイス定義言語のもとになるメッセージ交換形式として Protocol Buffers を利用できます。gRPC上のアプリケーションでは、別マシン上にあるアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができ、分散アプリケーションおよびサービスの作成を簡単にできます。

Protocol Buffers が現時点ではわかりませんが、RPC を実現するためのプロトコルGoogle が定めたものという認識でいます。

Protocol Buffers

インターフェース定義言語(IDL)でデータ構造を定義する通信や永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されています。

Protoc インストール

protoc は protocol buffers で定義されるスキーマから構造体やクラスを生成するコンパイラ
Mac では homebrew でインストールが可能みたいだけどなぜか出来なかったのでソースコード落としてきてビルドしました。

github.com

解凍後、make コマンド実行!

Go で gRPC サーバーを実装

まずは実装例が多い Go 言語で書いてみたいと思います。
Go をあまり書いたことがないため少々時間がかかりましたが、下記の記事を参考に作りました。 記事中ではリクエストで受けた文字列をそのまま返す GetEcho という関数を実装しています。

qiita.com

$ go run client.go
result:&echoService.EchoResponse{Input:"hiracky16", XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
error::<nil>

成功したっぽい。

Rails から gRPC サーバーに問い合わせる

ここからが本題です。 今度は Rails サーバーから GetEcho を呼び出します。

まず Gemfile に 2 つの gem を追加します。 grpc は Ruby で grpc を実現されるための gem、
grpc-tool は参考記事で作成した Protocol Buffers を Ruby のクラスに変換するツールです。

gem 'grpc'
gem 'grpc-tools'

次に以下のコマンドを実行し、

$ bundle exec grpc_tools_ruby_protoc -I ../service --ruby_out=lib --grpc_out=lib ../service/*.proto

これによって Rails プロジェクトの lib 配下に echoService_pb.rbechoService_services_pb.rb が生成されます。
(proto ファイルがキャメルケースだったためファイル名までそうなってしまった。。)

簡単に説明すると echoService_pb.rb では文字列やシンボルベースで proto ファイルの内容からリクエストとレスポンスのクラス(GetEchoMessage, EchoResponse)を作っていた。
要するに Ruby で proto ファイルの内容を表現したものです。
それらのクラスを使って echoService_service_pb.rb では Echo という module の形で get_echo というメソッドを提供しています。

echoService_service_pb.rb の内容は以下です。

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# Source: echoService.proto for package ''

require 'grpc'
require 'echoService_pb'

module Echo
  class Service

    include GRPC::GenericService

    self.marshal_class_method = :encode
    self.unmarshal_class_method = :decode
    self.service_name = 'Echo'

    rpc :GetEcho, GetEchoMessage, EchoResponse
  end

  Stub = Service.rpc_stub_class
end

これをコントローラーから呼び出します。

require 'echoService_services_pb.rb'
require 'echoService_pb.rb'

class ApplicationController < ActionController::API
  def echo
    echo_stub = Echo::Stub.new('localhost:2525', :this_channel_is_insecure)
    res = echo_stub.get_echo(Echo::Service::GetEchoMessage.new(target_echo: 'test'))
    render json: { message: res.input }
  end
end
  1. echoService_service_pb.rb に記述のあった stub のインスタンスを生成
  2. 引数には GetEchoMessage のインスタンスを new して上げて、get_echo(スネークケースなので注意)を呼ぶ
  3. レスポンスに EchoResponse のインスタンスが返ってくるので proto ファイルで指定のあった input というキーの値を取り出して返す

まとめ

やっと gRPC について勉強することができました。 Protocol Buffers を使ってスキーマを定義しておくといろいろなサービスや言語で使い回すことができるので、マイクロサービス設計には欠かせないのかもと思いました。

また、たとえ proto ファイルの内容が変更された場合でも生成し直せば動くことが期待されるため、スキーマの変更に強いのも使われている要因なのかなと思います。

プライベートで gRPC を使うことはそうそうないかもですが、チーム開発では便利なので使える場面があったら試してみたいです。

Github Actions と S3 + CloudFront を使って Nuxt アプリケーションを配信

Github Actions と S3 + CloudFront を使って Nuxt アプリケーションを配信

Nuxt.js を CloudFront で配信する方法が手軽で便利そうなのでやってみました。 Github Actions を組み合わせることによって master への push を検知して S3 へのアップロードを自動化してみます。

Nuxt

Nuxt 側での準備はほとんど必要ありません。 とりあえずプロジェクトを作成し、静的ファイルを生成します。

# プロジェクト作成
$ yarn create nuxt-app sample-app

# 静的ファイル生成
$ cd sample-app
$ yarn generate

これらを Github に push しておきます。

S3

S3 側ではバケットを作成します。 ポリシーはジェネレータを使って作成しました。

awspolicygen.s3.amazonaws.com

{
  "Version": "2012-10-17",
  "Id": "xxxxxxxxxxxxx",
  "Statement": [
    {
      "Sid": "xxxxxxxxxxxxxxxxxx",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": ["arn:aws:s3:::{バケット名}/*"]
    }
  ]
}

Github Actions

Github Actions はワークフローを構築できる Github 上のサービスのことです。 CI / CD 環境を用意する必要がなく Github 上で起きるアクションをフックにビルドやテスト、デプロイを走らせることが出来ます。

ワークフローは Yaml 形式で記述ができ、Github 上にテンプレートがあるため容易に作成が可能です。 このファイルを .github/workflows 配下に置くと Github 側で認識されます。

構文に関しては以下のページに記載されています。

help.github.com

まずはテンプレートを選びます。 ※ AWS に関係ありそうな以下のテンプレートを選んでみましたが ECR にイメージを push するもので今回の題材とはあまり関係ありませんでした。

f:id:h-piiice16:20200104142240p:plain
テンプレートを選択

生成された yml ファイルを Github 上で少し編集します。

on:
  push:
    branches:
      - master

name: Deploy to Amazon S3

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Setup Python 3.7 for awscli
      uses: actions/setup-python@v1
      with:
        version: '3.7'
        architecture: 'x64'

    - name: Setup Python 3.7 for awscli
      uses: actions/setup-python@v1
      with:
        version: '3.7'
        architecture: 'x64'

    - name: Build
      run: |
        yarn install
        yarn generate

    - name: Copy to s3
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: aws s3 cp ./dist ${{ secrets.S3_PATH }} --recursive --acl public-read

on 句の部分で対象とするアクションを指定します。 見たとおり master を push した際に実行されます。

jobs 配下にある run-on では実行するインスタンスを指定することができます。 Windows なんかもあるみたいです。今回はテンプレートと同じ ubuntu で行きます。

env は名前の通り環境変数を定義する場所で、AWS のアクセスキーなどを設定しました。 リポジトリの設定にある screts に AWS_ACCESS_KEY などのキーともに値を設定すると反映されます。

f:id:h-piiice16:20200105134033p:plain
github screts

次に画面右上の緑のボタンから編集した deploy.yml を作成するコミットをします。

f:id:h-piiice16:20200104142235p:plain
作成した yml ファイルを commit

コミットすると早速 Github Actions が実行されます。

f:id:h-piiice16:20200105083524p:plain
github actions の実行風景

S3 を見るとアップロードが完了していました。

CloudFront

CloudFront は AWS 製のコンテンツ配信サービス(CDN)です。 世界中にエッジサーバーがあり、それぞれでキャッシュが効いており高速にコンテンツを配信することが出来ます。

CloudFront は配信データ量による課金制になっており 10TB までなら $0.114 ととても安いです。

それでは「Create Distribution」で作って行きます。

設定したのは Origin の欄だけで S3 のバケットを選択します。(あとはデフォルトのままでいけたはず。。) location がデフォルトだと all になっているため料金を考慮して US だけにしました。

作成後、Error Page の設定が必要です。

なぜかと言うと Nuxt の動的ルーティングするページの場合(_id.vue のようなファイルの場合)html ファイルが実体として存在せず、CloudFront からすると 404 になってしまいます。
これだとまずいので CloudFront 側で 404 のとき /index.html を返すようにすると Nuxt 側で処理して正しいページを返してくれるようになります。

f:id:h-piiice16:20200105135114p:plain
Error page 設定

これで準備完了。

xxxx.cloudfront.net みたいな URL が発行されるのでアクセスすると繋がりました!

参考

tech.actindi.net

nuxt-property-decorator を使って Vue コンポネントをクラス構文に対応させる

nuxt-property-decorator を使って Vue コンポネントをクラス構文に対応させる

Nuxt + TypeScript のプロジェクトを考える際にあると便利な nuxt-property-decorator について調べたのでまとめる。
とりあえず使ってみた感想はクラス構文にすることによって簡潔に書くことができるのが良い点だと思った。

用語の説明や使い方などを記していく。

デコレーターとは

TypeScript で注釈(またはアノテーション)を class や method に付与することのできる宣言のことである。
Vue(Nuxt)の場合、クラスやメソッドに付与することにより、 コンポネントであることや、メソッドが Getter や Action を示すなどの視覚的な情報や そのアノテーション独自の機能を対象に付与することができる。

以下が公式の定義である(英語)

www.typescriptlang.org

日本語もあるよ。

js.studio-kingdom.com

nuxt-class-decorator とは

Nuxt プロジェクトにデコレータを持ち込むためのライブラリである。 もともとの vue-class-decorator という Vue + TypeScript でクラス構文を可能にするライブラリの派生らしい。

そのためシンタックスは Vue のクラス構文になる。

create-nuxt-app をしたあとの pages/index.vue の内容をクラス構文に変えてみる。

before

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
       {{ description }}
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'
export default {
  components: {
    Logo
  },
  data(): {
    return { title: 'test app' }
  },
  computed: {
    description: { `${title} is a My app` }
  }
}
</script>

after

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
        {{ subtitle }}
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

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

@Component({
  components: {
    Logo
  }
})
export default class Index extends Vue {
  public title: string = 'test app'
  public subtitle: string = `${this.title} is My app`
}
</script>

クラス構文に書き直すとこのように変わる。
ここで登場するのが @Component というアノテーションです。 これが「このクラスが Vue コンポーネントですよー」と宣言している。 Component にはいろいろなオプションを与えることができる。(今回だと components がそれに当たる。) アノテーションを付与することによって内部的にはメタプロしているみたいだけどどのようなことしているのかまではわかっていない。

また Vue コンポーネント特有のメソッド data や computed がクラスのインスタンス変数みたいに定義することができる。

次に子コンポネント(components/Child.vue)を新たに追加してみる。

# components/Child.vue
<template>
  <div class="child">
    <p>{{ child.name }}</p>
  </div>
</template>

<script lang="ts">
import { Prop, Component, Vue } from "nuxt-property-decorator";
import ChildType from "../models/child"

@Component
export default class Child extends Vue {
  @Prop()
  child!: ChildType
}
</script>

# pages/index.vue
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
        {{ subtitle }}
      </h2>
      <div v-for="child in childs" :key="child.id">
        <Child :child="child"/>
      </div>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

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

@Component({
  components: {
    Logo,
    Child
  }
})
export default class Index extends Vue {
  public title: string = 'iVoteFront'
  public subtitle: string = `${this.title} is My app`

  get childs() {
    return store.childs
  }
  async asyncData() {
    await store.loadDatas()
  }
}
</script>

コンポーネントの属性には @Prop というアノテーションを付与する。 また Props に Type を指定することでどのような値が親コンポーネントから渡ってくるかがわかりやすい。

ここからは余談ですが、 親コンポーネントでは store から子コンポーネントにわたすデータを取得する例で書いている。

メソッドの前に get というキーワードを書くことによってそのメソッドが算出プロパティ( Vue で言うところの computed)であることを表す。

asyncData は Nuxt 特有の機能で SSR 途中などに外部の API などからデータを得てレンダリングしたい場合などに使われる。 今回の例だと loadData() で childs という state に値をセットしている処理が書かれている。

ここらへんの話は vuex-module-decorator の回でまとめる。

まとめ

デコレータとはなにか nuxt-property-decorator の使い方がわかった。

注意が必要なのはデコレータの機能は実験的らしくこれが今後スタンダードになっていくかがまだわからないということ。

まだ使ったことない注釈もたくさんあり場合によっては便利なシチュエーションがあると思うので今後を追っていきたい。

複数 docker-compose 間でネットワークを共有する方法

複数 docker-compose 間でネットワークを共有する方法

方法としては世の中に出回っているがちょっとつまずいたのでメモ程度にまとめる。

用途としてはサーバーとフロントエンドの環境をそれぞれ docker-compose で組み立てたがその動作確認がしたいときなどに使える。

手順

1. docker network 作成

# 作成
$ docker network create my_network

# 確認
docker network ls
709b835bca33        my_network       bridge              local

2. docker-compose.yml でネットワークを指定

docker-compose.yml その1

version: '3'
services:
  app:
    container_name: app
    build:
        context: ./
        dockerfile: ./Dockerfile
    volumes:
      - ./:/app
      - /node_modules/
    ports:
      - 3333:3333
    environment:
      - HOST=0.0.0.0
    command:
      yarn dev
    networks:
      - my_network

networks:
  my_network:
    external: true

yaml ファイルのトップレベルに networks を指定してあげる。 こうすることによって先程作成してネットワークを使用することができる。

また service にも network を指定しなければならないため記述。(実はここが抜けてて時間を無駄にした…)

この記述を別の docker-compose.yml に記述するとネットワークを共有することができる。

この場合他のコンテナにアクセスする際は host 名がコンテナ名になるため、予め container_name などで指定してあげると良いかも。

Grape + Grape Entity で作る API

Grape + Grape Entity で作る API

Grape をよく使うので基本的な使い方から開発方法などを記す。 あと思いの外 Grape Entity が便利なので小技などをまとめておきたい。

Grape とは

github.com

Rails で RESTful な API を開発するためのフレームワーク。 Rails5 から提供された API モードで普通の Controller で簡単に開発することは可能になったが、 Grape の場合 REST に特化した DSL のためコードが追いやすい。

また Grape Entity と組み合わせることによってエンドポイント特有の結果を提供することが用意になる。

個人的にはコードが簡潔に保てるところが一番の強みだと思っている。

導入方法

「Grape 導入」で調べるといっぱい出てくるので割愛したい。 個人的に気にっているフォルダ構成は以下だ。

├── api
│   ├── base
│   │   └── api.rb
│   └── v1
│       ├── entities
│       │   ├── group_entity.rb
│       │   ├── idol_entity.rb
│       │   └── song_entity.rb
│       ├── groups.rb
│       ├── idols.rb
│       ├── root.rb
│       └── songs.rb

まず Rails プロジェクトの app 配下に api ディレクトリを作る。 そこに base と v1 というディレクトリを作る。 v1 ディレクトリはいわゆる API のバージョニングのためのもの。

api/base/api.rb と routes.rb にはそれぞれ以下が記述されているものとする。

# api/base/api.rb
module Base
  class API < Grape::API
    mount V1::Root
  end
end

# routes.rb
mount Base::API => '/'

こうすることによって API へのリクエストはすべて base/api.rb に流れる。 base の方の記述を見ると v1 に受け流している。 こうすることによって API のバージョンニングを用意にしている(のだと思う)。

v1 配下は Grape を記述していくところで entity ディレクトリは Grape Entity を記述するところである。

それでは Grape の構文を見ていく。

シンタックス

Grape

module V1
  class Idols < Grape::API
    resources :idols do
    desc 'return all idol'
    get '/' do
          @idols = Idol.all
      present @idols, with: V1::Entities::IdolEntity
        end
     end

    desc 'Create an idol'
    params do
      requires :name, type: String
      requires :group_id, type: Integer
    end
    post do
      @idol = Idol.new(name: params[:name], group_id: params[:group_id])
      if @idol.save
        status 201
        present @idol, with: V1::Entities::IdolEntity
      else
        status 400
        present @idol.errors
    end

     route_param :id do
       desc 'returns an idol'
         get do
           @idol = Idol.find(params[:id])
           present @idol, with: V1::Entities::IdolEntity
         end
       end
     end
  end
end

見ただけでどこに何が書いてあるかがわかりやすいと思います。 基本構文は以下のような感じです。

resource :{操作を加えるリソース名(モデル名)do
  desc '説明'
  paras do
    # パラメータを記述
  end
  {HTTP のメソッド} do
    # 処理
  end
end

このように記述することによって以下の操作が可能。

- GET: /idols ... 一覧の取得
- GET: /idols/1 ... 1件取得
- POST: /idols ... 作成

リソースの中にリソースをネストさせることも可能。

resource :groups do
  ... 
  resource :idols do
     ...
  end
end

この場合 idols には /groups/idols という URI でアクセスすることができる。

Grape Entity

上の例にも出てきているが present @idol, with: V1::Entities::IdolEntity の IdolEntity がそれである。 このファイルは以下のように記されている。

module V1
  module Entities
    class IdolEntity < Grape::Entity
      expose :id
      expose :name
      expose :group, using: V1::Entities::GroupEntity
    end
  end
end

expose に指定したフィールドをオブジェクトに問い合わせてその値を返してくれる。 この場合以下のような JSON オブジェクトが返ってくる。

{
  id: 1,
  name: "test",
  group: {
    id: 1,
   name: "test"
  }
}

ここでネストをさせているが Idol モデル belongs_to で Group に紐づくとする。 その場合紐づく group をたどって指定した GroupEntity に当てはめてくれる。

またモデルクラスに定義されているメソッドを返すこともできる。 例えば以下のような例が挙げられる。

module V1
  module Entities
    class IdolEntity < Grape::Entity
      expose :id
      expose :name do |idol|
        idol.full_name
      end
      expose :group, using: V1::Entities::GroupEntity
    end
  end
end

Idol クラスに full_name というメソッドが定義されているとする。 上のように expose :name にブロックを渡してあげることにより full_name の値を name として返すことができる。

まとめ

Grape を使って開発すると Controller に書くのに比べて可読性の高いコードが書けることがわかった。