White Box技術部

WEB開発のあれこれ(と何か)

Kotlinを使ったWEBアプリケーション開発の始め方

先日、とらのあな主催のKotlin勉強会で、Kotlinを使ったWEBアプリケーション開発の始め方という題で発表してきました。

ちょっと時間が押していたので発表は巻きになってしまったのですが、Kotlinで開発を始める後押しができていたら幸いです。

といっても、Java資産周りの技術解説いりますよね、本当に始めるなら

Kotlinのリフレクション(protected/privateメソッド呼び出し)

protectedメソッドへのアクセス

Kotlinにおいてprotectedのアクセス修飾子は、Javaと異なり同一パッケージからのアクセスを許容しません。 そのため、テストコードなどでprotectedのメソッドを実行したい場合は、リフレクションを使う必要があります。

継承してテストするという方法もありそうですが、今までしなかった発想なのでどうなんですかね。

リフレクションによる実行

例えばTestDataRepositoryにcreateSampleDataというLongのパラメータを1つ取り、SampleDataクラスを返却するprotectedのメソッドあるとした場合、以下が対象のメソッドを実行するコードになっています。

var testSuite = TestDataRepository()

val testMethod = testSuite::class.memberFunctions.find { it.name == "createSampleData" }
val actual = testMethod?.let {
    it.isAccessible = true
    it.call(testSuite, 100L)
}

コード解説

  1. まずは実行対象のメソッドがあるクラスのKClass(testSuite::class)から、
    KFunctionとして対象メソッドを取得(.memberFunctions.find { it.name == "createSampleData" })します
  2. KFunctionのインスタンスが確保できていれば(testMethod?.)、
    呼び出し結果をactualに保持するようにします(val actual = testMethod?.let {
  3. 実行するためにアクセスを許可し(it.isAccessible = true)、
    実行するインスタンスとメソッドに与えるパラメータ(100L)を指定して実行します(it.call(testSuite, 100L)

以上になります。

protectedのテストが面倒になった?

Javaの場合はprotectedメソッドであれば同一パッケージからすんなり呼べたので、テストが楽でよかったのですが、Kotlinの場合はちょっと工夫がいりそうです。 もうちょっと簡単な方法もありそうなのですが(PowerMockのWhiteboxクラス使うとか)、おいおい調べてみないといけないなという感じです。

【Slackアプリ】Botkitのsimple_storageを拡張する

BotkitにはSlackアプリがJSONでユーザ、チャンネル、チームの情報を保持できる簡易DBのような仕組みがあります。

今回はこれを拡張し、保持できる単位を増やしてみました。

simple_storageの拡張

Botkitで利用されている簡易DBは、simple_storage.jsとして実装されているので、そのコードを参考にすることで拡張ができました。

まずは必要となるjfsパッケージをインストールします。

npm i jfs

そしてsimple_storage.jsのstorageに、追加したいDBを足して返す関数を作ります。

ここでは、勤怠情報を保持するスペースとしてkintaiを作っています。ファイル名はstorage.jsの想定です。

'use strict';

import simple_storage from 'botkit/lib/storage/simple_storage.js';
import Store from 'jfs';

export function kintai_storage(config) {

  if (!config) {
    config = {
      path: './',
    };
  }

  let storage = simple_storage(config);
  let kintai_db = new Store(config.path + '/kintai', {saveId: 'id'});

  let objectsToList = (cb) => {
    return (err, data) => {
      if (err) {
        cb(err, data);
      } else {
        cb(err, Object.keys(data).map((key) => data[key]));
      }
    };
  };

  storage.kintai = {
    get: (kintai_id, cb) => {
      kintai_db.get(kintai_id, cb);
    },
    save: (kintai_data, cb) => {
      kintai_db.save(kintai_data.id, kintai_data, cb);
    },
    delete: (kintai_id, cb) => {
      kintai_db.delete(kintai_id, cb);
    },
    all: (cb) => {
      kintai_db.all(objectsToList(cb));
    }
  };

  return storage;
}

そして、この新しいstorageを利用するには、slackbotのパラメータに、storageキーの値を先ほど作成した関数としたオブジェクトを渡してください。

import Botkit from 'botkit';
import { kintai_storage } from './storage';

const controller = Botkit.slackbot({
  storage: kintai_storage({path: './data/'}),
}).configureSlackApp(
...以下省略

このプログラムを起動すると以下のように、保持できるスペースとして新たにkintaiが追加されていることを確認できます。

data
├── channels
├── kintai
├── teams
└── users

本格的に管理するのであれば、Sqliteなどを利用した方がいいと思いますが、一つ増やしたいくらいであれば、このような方法もいいのではないでしょうか。

BotkitでのSlackアプリ開発方法とデプロイ方法

現在、Slackアプリを開発中なのですが、初めてのこともあり色々と学ぶことが多かったです。

Botkitでの開発

Slackアプリを開発するにあたっては、公式フレームワークのBotkitを利用することにしました。

BotkitとBabelの導入

Botkitの導入はSlackアプリのプロジェクトで、npm i botkitをするだけでいいのですが、 ES201Xでコーディングするため、Babelも導入しました。

npm i botkit
npm i -D babel-cli babel-core babel-preset-env

SlackアプリはNode.jsで動くので、.babelrcは以下のようになります。

{
  "presets": [
    ["env", {
      "targets": {
        "node": "current"
      }
    }]
  ]
}

開発用のコードはsrc/main配下に、実行コードはdist/main配下とすることにしたので、 トランスパイルの実行用にpackage.jsonのscriptsに以下の記述を加えました(app.jsが起点のファイルです)。

    "build": "babel src -d dist",
    "start": "node dist/main/app.js",
    "clean": "rm -rf dist",

実行する際は以下のようにして使っています。

npm run clean; npm run build; npm start

開発時の注意点

あとはコーディングするだけなのですが、起動の段で幾つか注意するところがあります。

Botkitの注意点

  • createWebhookEndpointsにverificationの値を渡すと、ブラウザから/loginにアクセスができない
  • configureSlackAppに渡すパラメータにredirectUriが増えている(https://ホスト名:ポート/oauthを渡せばOK)
  • Slackにしているするパスは、Interactive Components、Slash CommandsどちらのRequest URLもhttps://ホスト名:ポート/slack/recieveになる

SlackアプリとBotsの違い

どちらもBotkitで開発できるのですが、以下の点が異なります。

  • Botsにする場合はtokenを、アプリにする場合はclient_idとclient_secretを利用する
  • アプリでは起動したタイミングで毎回/loginにアクセスし、認証しなければいけない
  • interactive_message_callbackを使う場合はアプリでなければいけない(サーバが必要)

今回Slackアプリとして開発したのは、interactive_message_callbackを利用したかったためです。

Slackへの設定

アプリ作成は、slack apiのページで『Create New App』ボタンを押すことで開始できます。

f:id:seri_wb:20180114014342p:plain

ここでアプリの名前とインストールするSlackのチームを入力しましょう。

その後の各項目での作業は以下になります。

Basic Information

  • App CredentialsのClient IDとClient Secretをメモ(アプリ起動に利用するため)
  • Display Informationにアプリの情報を入力する

Install App

  • アプリをSlackにインストールする(最後でもよい)

Interactive Components

  • Request URLにhttps://ホスト名:ポート番号/slack/receiveを設定する

Slash Commands

アプリ用のSlackコマンドを作る場合はここの設定を行います。

  • コマンドの情報を入力する
  • Request URLにhttps://ホスト名:ポート番号/slack/receiveを設定する

OAuth & Permissions

配布アプリを作成する場合は、ここでRedirect URLsにhttps://ホスト名:ポート番号/oauthを設定する必要がありますが、 内部利用だけの場合は設定不要です。

Bot User

利用するBotユーザの表示情報をここで設定します。
注意点として、Slack上に登録されるユーザはここで設定した名前ですが、スラッシュコマンドが応答するアプリの名称はBasic Informationで設定したものになります。

Slackアプリの起動

トランスパイルが終わっていればnpm startで起動するので、SSL対応のサーバでアプリを起動してください。

起動後にブラウザからhttps://ホスト名:ポート番号/loginにアクセスし、認証を行うとRMTがスタートします。

Herokuにデプロイする場合の注意点

最初は開発用サーバとしてHerokuを利用していたのですが、Herokuではできないこともあり、現在は利用していません(ngrokを使っています)。

ただ、利用する際にもいくつかハマリポイントが合ったので残しておきます。

  • ポートが動的に設定されるため、起動ポートにはprocess.env.PORTを利用する
    • 上記の理由で、SlackAppのURL設定項目にはポートを記載しない
  • Herokuではサーバに書き込みができないため、json_file_storeが利用できない(宣言していてもアプリ自体は起動する)
  • 書き込みできないため、babelを利用する場合、トランスパイル後のファイルもコミットする必要がある
  • Procfileを用意して、web: npm startのように起動コマンドを記載する

ngrokの利用

Slackのチュートリアルにもありますが、開発ではngrokでSlackからのリクエストを受けて、ローカルに転送することで、ローカル開発ができます。

Macの場合はHomebrewで導入するのが簡単だと思います。

brew cask install ngrok

起動は以下のようにアプリで利用するポート(ここでは3000)を指定して実行する感じです。

ngrok http 3000

ngrokは起動している間はドメインが変わらない&裏でアプリの再起動をしても問題なく転送してくれるので、開発中はngrokを起動しっぱなしにして利用することになると思います。

ただ、セキュリティリスクがあることをやっているので、会社などで立ち上げていると情シスに怒られる可能性があります。注意しましょう。

まとめ

コーディングに関してはざっくり省きましたが、このようにすることでSlackアプリを自分で作ることができます。

本番利用にはAWSを使おうと思っているので、その手順はまたの機会に。

コーディングのエッセンスはまた別途書きたいと思っています。