SlackのBlock Kitをjsxの記法で書ける jsx-slackを試してみた

## TL;DR

- 僕は個人的に datadog_slack_reporterというものを作成して、datadogで監視しているサービスの台数をslackに通知しています
- Slackのメッセージ作成部分を、SlackのBlock Kitをjsxの方式で記述できる [jsx-slack](https://github.com/speee/jsx-slack) に置き換えてみました
- 面倒なjsonの作成部分がReactっぽく書けるので、めちゃくちゃ便利でした
- この資料には`jsx-slack`をTypeScriptで導入する上で必要な設定方法を記載します

## jsx-slackとは何か

Slackのメッセージ作成をjsxのフォーマットで行うことができるnpmのpackageです。これを利用すれば、Reactのcomponentを書くような書き味で複雑なjsonの作成をすることができます。

<img width="1413" alt="スクリーンショット 2019-03-04 12.07.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/23606/17bb9e1f-580f-1e11-b4d2-00b0b9e410e2.png">

上の図は`jsx-slack`内で生成されるjsonが確認できるページです。こちらの右側のjsonがslackが要求するjsonのフォーマットとなっており、左側がjsx-slackを利用して記述するコードになります。`jsx-slack`の動作確認ページは[こちら](https://speee-jsx-slack.netlify.com/)になります。

## 成果物

最初に、今回作成したものの全体像をざっくり説明します。

### Slackへのメッセージ出力

<img width="471" alt="スクリーンショット 2019-03-04 11.22.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/23606/29d6a371-6e02-4346-ca09-b77f1b5e75b3.png">

こちらが、今回僕が作成したbotが実際にslackへ投稿したメッセージです。

### ソースコード (一部抜粋)

jsx-slackを使った slackへのメッセージ送信ボットのプログラムがこちらになります。全体像を掴むために、さらっと眺めてもらえれば大丈夫です。

#### Cloud Functionsのhandler
```typescript

import moment from "moment-timezone";
import "source-map-support/register";
import { DatadogHostMetrics } from "./datadog";
import { DatadogClient } from "./DatadogClient";
import { SlackClient } from "./SlackClient";
import { slackMessageBlock } from "./SlackMessageBlock";

const datadogClient = new DatadogClient();
const slackClient = new SlackClient();

export async function datadog_handler(data: any): Promise<void> {
  const fromTime = moment({ hour: 0, minute: 0, second: 0 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  const toTime = moment({ hour: 23, minute: 59, second: 59 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");
  // datadogのAPIを叩いてデータを取ってくる
  const hostMetrics: DatadogHostMetrics[] = await datadogClient.countHosts(fromTime, toTime);
  // 送信するメッセージを生成する
  const blocks = slackMessageBlock(fromTime, toTime, hostMetrics);
  // メッセージを送信する
  await slackClient.post(blocks);
}
```

#### messageの作成部分 (jsx-slackで書き直された場所)

```tsx

/** @jsx JSXSlack.h */
import JSXSlack, { Block, Section } from "@speee-js/jsx-slack";
import moment from "moment-timezone";
import { DatadogHostMetrics } from "./datadog";
import { ProductMetrics } from "./ProductMetrics";

export function slackMessageBlock(fromTime: string, toTime: string, hostMetrics: DatadogHostMetrics[]) {
  const messages = [];
  for (const metrics of hostMetrics) {
    const productMetrics = new ProductMetrics(metrics);
    const message = (
      <blockquote>
        <b> {productMetrics.name} </b>
        <br />
        min:${productMetrics.minHostCount()} ~ max:${productMetrics.maxHostCount()}
        sum(host*hours):${productMetrics.sum()}
      </blockquote>
    );
    messages.push(message);
  }

  return JSXSlack(
    <Block>
      <Section>
        <p>datadog monitoring daily report</p>
        {moment.unix(parseInt(fromTime, 10)).toString()} ~ {moment.unix(parseInt(toTime, 10)).toString()}
        {messages}
      </Section>
    </Block>
  );
}
```

#### messageの送信部分

```typescript

import { WebClient } from "@slack/client";

export class SlackClient {
  private static username: string = "Datadog按分計算Bot";
  private readonly channelID: string = process.env.CHANNEL_ID as string;
  private readonly token: string = process.env.SlackToken as string;
  private readonly client: WebClient;

  constructor() {
    this.client = new WebClient(this.token);
  }

  public post(blocks: any) {
    return this.client.chat.postMessage({
      channel: this.channelID,
      text: "",
      blocks,
      username: SlackClient.username,
    });
  }
}
```

実際のコードはこちらを参照してください。
https://github.com/selmertsx/datadog_slack_report/pull/4

### 基本的な書き方

- 最初にslack本家の[block-kit-builder](https://api.slack.com/tools/block-kit-builder)で、サンプルを見ながらどのようなblock kitが作りたいのか考えます。
- jsx-slackのリポジトリ内には[サンプル](https://github.com/speee/jsx-slack#jsx-component)がいくつかあるので、そちらを参照しても良いでしょう
- その後、[jsx-slackのサイト](https://speee-jsx-slack.netlify.com/)で目的のjsonが生成できる記法を探します。


## jsx-slackの導入設定

TypeScriptで導入する際は少しだけ設定をする必要があります。公式ドキュメントを見ると下記のような記載があります。

https://github.com/speee/jsx-slack 

> A prgama would work in Babel (@babel/plugin-transform-react-jsx) and TypeScript >= 2.8 with --jsx react.

ということで、jsx-slackをTypeScriptで利用するにはtsconfigの設定が必要であることが分かります。以降、それらの設定を行っていきましょう。

### `@speee-js/jsx-slack`のinstall

```
npm i @speee-js/jsx-slack
```

今回の設定に必要なpackageを上記コマンドで全部installします。

### tsconfigの設定

TypeScriptのjsxに関するドキュメントを読んでみましょう。

https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions

> The exact factory function used by the jsx: react compiler option is configurable. It may be set using either the jsxFactory command line option, or an inline @jsx comment pragma to set it on a per-file basis.

上記コメントで示されているように、TypeScriptでjsxを利用するためには、ファイルごとのpragmaの設定と、コンパイラオプションの指定が必要になります。pragmaの設定とは、サンプルコードで示されていた `/** @jsx JSXSlack.h */` の部分を指します。コンパイラオプションは、下記のように設定します。


```json

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": [
      "es5",
      "es2015",
      "es2016.array.include",
      "esnext.asynciterable"
    ],
    "strict": true,
    "outDir": "./",
    "esModuleInterop": true,
    "noImplicitAny": true,
    "allowJs": true,
    "jsx": "react" // <= 今回追加した設定
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "__tests__",
    "**/__mocks__/*.ts"
  ]
}
```

実際のコードは[こちら](https://github.com/selmertsx/datadog_slack_report/blob/3533f3e8491b82a9a37b3d23f001fe0ff7ea1687/tsconfig.json)

## 最後に

`jsx-slack` 導入の手順は以上になります。今回の例はちょっとシンプルすぎるのであまり有り難みを感じにくいかも知れませんが、もうちょっと複雑になってくると jsxを利用してReactっぽく書ける `jsx-slack`に大きなメリットを感じられるかと思います。