dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Slackのチャンネルのメンバーからランダムで一人選ぶBotを作る

タイトルの通り。

  • GitHub の PR のレビュアーを、開発チームの特定のメンバー数名から一人指名したい
  • 開発チームの一部のメンバーで持ち回りでやらないといけないタスクがあって、毎回誰がやるか決めるのめんどい

といったことがあって、せっかくなので bot に決めてもらえないかなあという気分になりました。

「どの人たちから選ぶか」は目的によってまちまちなので、それなら Slack のチャンネルのメンバーから選ぶようにすれば汎用性が高いんじゃないかと思って作ってみました。

コード

先にコードから。

なお、フレームワークとして Hubot を使ってますが、CoffeeScript を書くのが嫌だったので ES2015 で書いてます。
そのあたりの設定の話はこちらを参考にしてください。

dackdive.hateblo.jp

import request from 'request';

module.exports = (robot) => {
  robot.respond(/.*(random|抽選|選ぶ).*/, (msg) => {
    const url = 'https://slack.com/api/channels.list?token=' + process.env.HUBOT_SLACK_TOKEN;

    // チャンネル一覧を取得
    request(url, (err, res, body) => {
      // msg.message.room で現在の channel 名が取れる
      const channel = findChannel(JSON.parse(body).channels, msg.message.room);
      console.log('Channel found');
      console.log(channel);

      // bot 自身を除外して抽選
      const botId = robot.adapter.self.id;
      const filterdMembers = channel.members.filter((member) => {
        return member !== botId;
      });
      console.log(filterdMembers);
      const member = msg.random(filterdMembers);

      msg.send('選ばれたのは、 <@' + member + '> でした :tea:');
    });
  });
}

function findChannel(channels, targetName) {
  for (const channel of channels) {
    if (channel.name === targetName) {
      return channel;
    }
  }
  return null;
};

GitHub:

(PR は #1


動作の様子

f:id:dackdive:20160728184414p:plain

bot に向かって random/抽選/選ぶ といった単語を含めてメンションすると適当に選んでくれます。


実装上のポイントなど

現在のチャンネル名は msg.message.room で取れる

最初に苦労したのは、自分が今いるチャンネルを取得するという処理です。
以下の記事を読んで msg.message.room で取れるんだーということを知りました。

The Hubot msg Object

自分でも console.log(msg) して中身を確認してみました。
message 以外にも robot に色々情報が入ってそうです。

Response {
  robot:
   Robot {
     name: 'hubot',
     events:
      EventEmitter {
        ...
        _maxListeners: undefined },
     brain:
      Brain {
        ...
        _eventsCount: 3 },
     alias: false,
     adapter:
      SlackBot {
        customMessage: [Function],
        message: [Function],
        ...
  message:
   SlackTextMessage {
     user:
      User {
        id: 'U1R73F7DL',
        name: 'zaki-yama',
        ...
        room: 'bot-channel' },
     text: '@hubot: random',
     rawText: '<@U1R7NPEKU>: random',
     rawMessage:
      Message {
        _client: [Object],
        _onDeleteMessage: [Function],
        deleteMessage: [Function],
        _onUpdateMessage: [Function],
        updateMessage: [Function],
        type: 'message',
        channel: 'C1VTH4FPD',
        user: 'U1R73F7DL',
        text: '<@U1R7NPEKU>: random',
        ts: '1469699396.000062',
        team: 'T1R7BJHV3' },
     id: '1469699396.000062',
     done: false,
     room: 'bot-channel' },


Slack の channel オブジェクトからチャンネルに所属するメンバーを取得する

Slack APIchannels.info でチャンネル一覧を取得し、チャンネル名が msg.message.room と一致するものを探します。

レスポンスの channels プロパティの中にすべてのチャンネルが配列で入ってます。
また、channel オブジェクトの中身はこちらに記載されています。
https://api.slack.com/types/channel

channel.members でメンバーの一覧が取得できますね。


Bot 自身の情報は robot.adapter.self.id で取れる

Bot もチャンネルに含まれているため、ランダムで一人選ぶときには自分を除外しなければなりません。
このためだけに Slack API を叩かないとだめなのかなーと思っていたんですが、なんとなく robot.adapter あたりを console.log() で見てたら見つかりました。

このあたりのドキュメントはどこかにまとまってるんですかね。。。


配列からランダムで1つ選ぶ、は msg.random(array) が使える

らしいです。Hubot の公式ドキュメントに書いてあります。
https://hubot.github.com/docs/scripting/#random

(Hubot のドキュメントは見づらい...!)


メンションは名前でもIDでも送れる

channels.info で取得した channel オブジェクトの members からはユーザーの ID しか取れないので、
てっきり最初は別途 users.info を叩いてユーザー名を取得し直さないといけないんだと思ってました。

が、Slack API のドキュメントをちゃんと読むと、メンションをとばすときは ID で OK ということが書かれています。

https://api.slack.com/docs/message-formatting#linking_to_channels_and_users

むしろ

The readable name can be included after the ID, by separating them with a pipe (|) character.

なので、ID の後ろに name をつけても良いよ、ぐらいでしょうか。
ただ、 ユーザー名だけでも一応メンションはとばせるみたいです。

メンションは @userId でなく <@userId> とする必要があるみたいですね。


TODO

毎回同じ返事だと寂しいので、メッセージにバリエーションを持たせたいです。
→対応しました。GitHub 参照 https://github.com/zaki-yama/hubot-es2015/issues/3


リファレンス

JavaScript(ES2015&React)で画像を扱う:リサイズとプレビュー表示

はじめに

web サイト/アプリケーションで画像のアップロード機能などを実装する場合、
最近のスマホのカメラで撮影した画像はサイズが数 MB にも及ぶので、あらかじめクライアント側で送信可能なサイズまで縮小する必要があります。
今回はそのような画像のリサイズの実装方法を整理しつつ、サンプルコードを React で書いてみます。

冒頭に画像のリサイズについて色々書いてますが、結論としては便利なライブラリでよしなにやってくれるので実装方法だけ知りたい場合は読み飛ばしてください。


JavaScript で画像を扱う際の基本

まず、JavaScript で画像ファイルを扱うにあたっての非常に基本的な話。
<input type="file"> 要素で選択した画像ファイルのデータを取得し、プレビュー表示するには以下のようにします。

class ImagePreviewer extends React.Component {
  constructor(props) {
    super(props);

    this.onImageSelected = this.onImageSelected.bind(this);
  }

  onImageSelected(e) {
    const file = e.target.files[0];
    const fr = new FileReader();

    fr.onload = () => {
      const imgNode = this.refs.image;
      // fr.onload = (event) => { ... } として
      // event.target.result で取る方法もある
      imgNode.src = fr.result;
    };
    fr.readAsDataURL(file);
  }

  render() {
    return (
      <div>
        <input type="file" accept="image/*" onChange={this.onImageSelected} />
        <p>Image will be previewed here!</p>
        <img ref="image" src="" />
      </div>
    );
  }
}

ファイル選択ボタンが押されて画像が選ばれたときに onChange イベントが発火するので、そのイベントハンドラ onImageSelected 内で以下のような処理を行っています。

  • 引数のイベントオブジェクトから対象のファイルを取得
  • FileReader オブジェクトを生成
  • FileReader オブジェクトの onload() を定義
    • onload() は readXXX で画像の読み込みに成功したときに呼び出される関数
  • FileReader.readAsDataURL() を呼び出し、画像を Data URL 形式で読み込む
  • Data URL 形式の画像をそのまま img タグの src 属性に指定する

Data URL とは

data:image/png;base64,iVBORw0KGgoAAAAN...

のような形式の文字列で、画像データを Base64 エンコードしたもので、
RFC2397 で仕様がきちんと定められているそうです。

data:[<mediatype>][;base64],<data>

また、この Data URL を <img src="..."> に指定することで画像のプレビュー表示も可能になります。

このあたりの話は以下の記事が参考になりました。


JavaScript で画像のリサイズ、の難しさ

単純に画像を読み込むだけなら上記の実装でよく、また、この後アップロードする場合も基本は Data URL 形式の画像データをサーバー側に POST してやれば良いです。

ただし、上述したように最近のスマホで撮影した画像などはそのままだと数 MB ものサイズになってしまうため、クライアント側でリサイズしてから送信してやる必要があります。

調べてみると、画像のリサイズには JavaScriptCanvas オブジェクトを使った方法があるそうです。

参考

先ほどのコードの onload メソッド内を以下のように修正します。

fr.onload = () => {
  console.log('before resizing: ', fr.result.length);
  // Image オブジェクトに src を指定すると、元画像の width と height が取れる
  const img = new Image();
  img.src = fr.result;
  const width = img.width;
  const height = img.height;
  console.log('width, height = ', width, height);
                                                                              
  // 縮小後のサイズを計算。ここでは横幅 (width) を指定
  const dstWidth = 1024;
  const scale = dstWidth / width;
  const dstHeight = height * scale;
                                                                              
  // Canvas オブジェクトを使い、縮小後の画像を描画
  const canvas = document.createElement('canvas');
  canvas.width = dstWidth;
  canvas.height = dstHeight;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, dstWidth, dstHeight);
                                                                              
  // Canvas オブジェクトから Data URL を取得
  const resized = canvas.toDataURL('image/jpeg');
  console.log('after resizing: ', resized.length);
  console.log('width, height = ', dstWidth, dstHeight);
                                                                              
  const imgNode = this.refs.image;
  imgNode.src = resized;
};

元画像の width, height を取得するため、一旦 Data URL から Image オブジェクトを作成しています。
src をセットすると width, height が取れるようになります。

Canvas オブジェクトは、Context オブジェクトを取得しそれに対して描画命令を行うことで描画が可能です。
drawImage() メソッドについては、こちらのリンクがわかりやすいです。
http://www.html5.jp/canvas/ref/method/drawImage.html

最後に canvas.toDataURL() メソッドを使って、リサイズ後の画像を Data URL 形式に戻しています。
toDataURL() の引数には MIME タイプ(参考)を指定します。jpg なら image/jpeg です。


リサイズはできた。が、まだ問題が。。。

画像のサイズを小さくすることはできました。
が、これでもまだ問題があります。

上記のコードを使い、スマホで撮影した画像を使ってプレビュー表示すると
以下のように画像が回転して表示されることがあります。

f:id:dackdive:20160717010038p:plain

これは、JPEG 画像の Exif タグに含まれる Orientation 情報を考慮していないからです。
Exif タグとは、ざっくり言うと画像を撮影した時の様々な情報を保持したメタデータです。

参考

また、Orientation は画像の方向を 1 ~ 8 までの数字で表したものですが、数字と方向の対応関係については以下が参考になります。
JPEGのExifタグ情報のOrientaionの定義の早見表 · DQNEO起業日記

Mac の場合、プレビュー.app で ツール > インスペクタを表示 を開き、2 つめのメニューの「一般」から確認することができます。

f:id:dackdive:20160717014101p:plain

というわけで、きちんと画像を扱うためには

  1. 読み込んだ画像から Exif を取得し、さらに Orientation を取得
  2. Orientation を元に、正しく画像を回転

する必要があります。

画像の Exif 情報を扱うためのライブラリとして exif-js なんてのがあるようですが、このあたりで調査は力尽きてしまったので
ES2015 の import/export 形式で読み込めるのかとか Orientation を元に回転するという操作が簡単に行えるのかなどはわかっていません。


JavaScript-Load-Image はそのあたりを解決してくれるライブラリ

というわけで、スマホで撮影した画像を JavaScript で扱うのは色々と面倒であることがわかりました。
さらに調べてみると、上述したリサイズや回転なども含めた画像操作に便利なライブラリがありました。

https://github.com/blueimp/JavaScript-Load-Image

README に明記されてませんが、npm でインストールすることができ、かつ ES2015 の import/export 形式で使用することができます。
(npm のページは https://www.npmjs.com/package/blueimp-load-image

インストールは

npm install --save blueimp-load-image

で行います。

そして、このライブラリを使用して onload メソッドを修正したものがこちらです。

import loadImage from 'blueimp-load-image';

class ImagePreviewer extends React.Component {

  ...

  onImageSelected(e) {
    const file = e.target.files[0];
    loadImage.parseMetaData(file, (data) => {
      const options = {
        maxHeight: 1024,
        maxWidth: 1024,
        canvas: true
      };
      if (data.exif) {
        options.orientation = data.exif.get('Orientation');
      }
      loadImage(file, (canvas) => {
        const dataUri = canvas.toDataURL('image/jpeg');
        const imgNode = this.refs.image;
        imgNode.src = dataUri;
      }, options);
    });
  }

  ...

先に parseMetaData() を使って画像の Exif 情報を取得し、Exif があれば Orientation を取得しておきます。
(ドキュメントにきちんと書かれていませんが、exif.get(key) で Orientation 以外の Exif 情報も得られるようです)

また、loadImage() は

loadImage(file (or blob), callback, options)

というように第一引数に画像データ、第二引数に画像ロード後の処理、第三引数にオプションを指定します。
指定できるオプションは README に書かれています。
https://github.com/blueimp/JavaScript-Load-Image/blob/master/README.md#options

たったこれだけで、画像のサイズ・回転を考慮してロードすることができるようになりました。便利ですね。

HubotをES2015で書いてHerokuにデプロイする

今さらながら Slack bot を作りたくて、フレームワークHubot を選んだ。
Hubot はそのままでは CoffeeScript で書く必要があるんだけど、ES2015 もやっと覚えたばかりなのに CoffeeScript の勉強はしたくない。
ということで、ES2015 で書くための手順をメモ。

デプロイ先は Heroku です。

Hubot のインストール

まずは 公式ドキュメント の通りに Hubot スクリプトのひな形を作成する。

# Node.js はインストール済みとする
$ npm install -g yo generator-hubot
$ mkdir hubot-es2015
$ cd hubot-es2015
$ yo hubot
                     _____________________________
                    /                             \
   //\              |      Extracting input for    |
  ////\    _____    |   self-replication process   |
 //////\  /_____\   \                             /
 ======= |[^_/\_]|   /----------------------------
  |   | _|___@@__|__
  +===+/  ///     \_\
   | |_\ /// HUBOT/\\
   |___/\//      /  \\
         \      /   +---+
          \____/    |   |
           | //|    +===+
            \//      |xx|

? Owner zaki-yama <shingoyamazaki00@gmail.com>
? Bot name hubot-es2015
? Description A simple helpful robot for your Company
? Bot adapter slack
   create bin/hubot
   create bin/hubot.cmd
   create Procfile
   create README.md
   create external-scripts.json
   create hubot-scripts.json
   create .gitignore
   create package.json
   create scripts/example.coffee
   create .editorconfig
                     _____________________________
 _____              /                             \
 \    \             |   Self-replication process   |
 |    |    _____    |          complete...         |
 |__\\|   /_____\   \     Good luck with that.    /
   |//+  |[^_/\_]|   /----------------------------
  |   | _|___@@__|__
  +===+/  ///     \_\
   | |_\ /// HUBOT/\\
   |___/\//      /  \\
         \      /   +---+
          \____/    |   |
           | //|    +===+
            \//      |xx|

なんか色々ファイルが作成されました。

$ ls -al
total 64
drwxr-xr-x   13 yamazaki  staff   442  7 12 18:23 ./
drwxr-xr-x    5 yamazaki  staff   170  7 12 18:22 ../
-rw-r--r--    1 yamazaki  staff   197  5 20 06:45 .editorconfig
drwxr-xr-x   12 yamazaki  staff   408  7 12 18:37 .git/
-rw-r--r--    1 yamazaki  staff    39  5 20 06:45 .gitignore
-rw-r--r--    1 yamazaki  staff    24  7 12 18:23 Procfile
-rw-r--r--    1 yamazaki  staff  7904  7 12 18:23 README.md
drwxr-xr-x    4 yamazaki  staff   136  7 12 18:23 bin/
-rw-r--r--    1 yamazaki  staff   213  7 12 18:23 external-scripts.json
-rw-r--r--    1 yamazaki  staff     2  7 12 18:23 hubot-scripts.json
drwxr-xr-x  113 yamazaki  staff  3842  7 12 18:23 node_modules/
-rw-r--r--    1 yamazaki  staff   662  7 12 18:23 package.json
drwxr-xr-x    3 yamazaki  staff   102  7 12 18:23 scripts/


Babel のインストールと設定

ES2015 で書き、ES5 にトランスパイルするため Babel をインストールする。

$ npm install --save babel-cli babel-preset-es2015

通常、Babel などのアプリケーション本体で使われないパッケージは --save-dev を使ってインストールするが、
Heroku でデプロイしたときに devDependencies は自動的にインストールされない ため、dependencies に保存されるようにしてます。

つづいて、.babelrc を作り、以下を記述。

{
  presets: ["es2015"]
}

さらに package.json に npm script を定義する。

// 抜粋
"scripts": {
  "build": "babel src --out-dir build",
}

src, build のフォルダ名は自由です。


スクリプトの作成とビルド

準備ができたので、スクリプトを書いていく。
src/index.js (ファイル名は任意)に以下のように記述する。

module.exports = (robot) => {
  robot.hear(/すし/, (res) => {
    res.send(':sushi:');
  });
}

「すし」と入力したら Slack 上で寿司アイコンを返すだけの単純なスクリプト

保存したら、先ほどの npm script でビルドする。

$ npm run build
> hubot-es2015@0.0.0 build /Users/yamazaki/workspace/hubot/hubot-es2015
> babel src --out-dir build

src/index.js -> build/index.js

build ディレクトリ以下にファイルが生成されました。


Bot 起動用のコマンドを編集する

デフォルトでは、bot の起動は

$ ./bin/hubot

だけで済むが、今回は --require オプションでスクリプトが格納されているディレクトリを指定する。

$ ./bin/hubot --require build

デプロイ先に Heroku を使う場合は Procfile も編集しておく。

web: bin/hubot -a slack --require build

-a slack は Slack と連携するために必要)

bot の動きを Slack から試してみる場合は、事前に Slack の Hubot 設定ページから HUBOT_SLACK_TOKEN をコピーし

f:id:dackdive:20160713144815p:plain

.env に記述した後、

$ heroku local web

コマンドを実行する。

f:id:dackdive:20160713145028p:plain

ちゃんと動いてるようです。


Heroku にデプロイする

最終的に Heroku にデプロイするときは、以下の作業が必要です。

postinstall という npm script を追加する

参考:https://devcenter.heroku.com/articles/nodejs-support#customizing-the-build-process

postinstall というスクリプトは、Heroku にデプロイ後に自動的に走るスクリプト
デプロイ後に Babel によるビルドが実行されるようにする。

"scripts": {
  "build": "babel src --out-dir build",
  "postinstall": "babel src --out-dir build"
}


本番環境の環境変数にも HUBOT_SLACK_TOKEN を設定する

f:id:dackdive:20160713150224p:plain

画面を開くのが面倒な人は

$ heroku config:set HUBOT_SLACK_TOKEN=...

というコマンドを実行するか、.env ファイルから設定する以下の方法が便利です。
Herokuで本番環境の環境変数(config vars)を.envファイルで設定する - dackdive's blog


環境変数 HUBOT_HEROKU_KEEPALIVE_URL (とタイムゾーン)を設定する

external-scripts.json を見ると、hubot-heroku-keepalive が指定されている。
これは、Free dyno で bot がスリープしないようにするための Hubot スクリプトらしい。

そして、このスクリプトが正しく動作するためには HUBOT_HEROKU_KEEPALIVE_URL という環境変数が設定されている必要があるそうなので、設定する。

# Heroku アプリケーションの URL を指定すれば良い
$ heroku config:set HUBOT_HEROKU_KEEPALIVE_URL=https://[your-heroku-app].herokuapp.com/

# 以下のように、ワンライナーで指定することもできる
$ heroku config:set HUBOT_HEROKU_KEEPALIVE_URL=$(heroku apps:info -s  | grep web-url | cut -d= -f2)

その他の変数については以下の通り。詳しくは README の Configuration 参照。

  • HUBOT_HEROKU_WAKEUP_TIME : Bot を起動する時間。デフォルト 6:00
  • HUBOT_HEROKU_SLEEP_TIME : Bot をスリープする時間。デフォルト 22:00
  • HUBOT_HEROKU_KEEPALIVE_INTERVAL : Bot を定期的に起こす間隔。デフォルト 5 分間隔

おおむねデフォルト通りで良さそうに見えるが、WAKEUP_TIME および SLEEP_TIME は Heroku アプリのタイムゾーン(デフォルト UTC)に従うので、以下のようにタイムゾーンJST に設定しておいた方がよさそう。

$ heroku config:set TZ=Asia/Tokyo


(2016/08/23 追記) Heroku Scheduler というアドオンを追加する

上述した hubot-heroku-keepalive というスクリプトだけでは不十分だった。
スクリプト自体は dyno が スリープしないように 定期的にアクセスするためのものなので、
HUBOT_HEROKU_SLEEP_TIME から HUBOT_HEROKU_WAKEUP_TIME の間で dyno がスリープしてしまうとそれ以降スリープしっぱなしになってしまう。

そのため、HUBOT_HEROKU_WAKEUP_TIME に合わせて dyno をスリープから復旧させる必要がある。
これには hubot-heroku-keepalive の README に書いてあるとおり、Heroku Scheduler というアドオンを使う。

アドオンは管理画面からインストールするか、ローカルで以下のコマンドを実行する。

$ heroku addons:create scheduler:standard

インストール後に heroku addons:open scheduler を開くと設定画面が開くので、新規でジョブを追加して以下のように設定する。

f:id:dackdive:20160823084253p:plain

中央のスクリプトには

$ curl ${HUBOT_HEROKU_KEEPALIVE_URL}heroku/keepalive

を入力する。

また、NEXT DUE は UTC で入れる必要があるので要注意。(キャプチャだと日本時間の 9:00 に起こす)


その他

1) 起動時のログに

INFO /Users/yamazaki/workspace/hubot-es2015/build/index.js is using deprecated documentation syntax

と出力されている。
調べてみると、Description: などの冒頭のコメントがないとこのログが出力されるそう。
参考:Hubotでusing deprecated documentation syntax - Qiita

というわけで、試しにビルド前の JS にコメントを追記してみたけど
ビルドすると冒頭に 'use strict' が追加されてしまい、1 行目からコメントがないとこのログは消せないよう。

ビルド前
// Description:
//   My first slack bot
//
// Commands:
//   hubot すし - return sushi icon
module.exports = (robot) => {
  ...
ビルド後
'use strict';

// Description:
//   My first slack bot
//
// Commands:
//   hubot すし - return sushi icon
module.exports = function (robot) {
  ...

今のところ特に困らないので、とりあえず諦める。


2) (解決) 同じく起動時のログに

ERROR hubot-heroku-alive included, but missing HUBOT_HEROKU_KEEPALIVE_URL. `heroku config:set HUBOT_HEROKU_KEEPALIVE_URL=$(heroku apps:info -s  | grep web-url | cut -d= -f2)`

と出ている
確認したところ external-scripts.jsonhubot-heroku-keepalive というライブラリが指定されていて、README を読む限り HUBOT_HEROKU_KEEPALIVE_URL という環境変数を指定する必要があるみたい。


3) デフォルトで作成される scripts ディレクトリは不要になったので削除した。一緒に、hubot-scripts.json も deprecated のようなので削除。
(起動時に以下のログが出ていた)

WARNING Loading scripts from hubot-scripts.json is deprecated and will be removed in 3.0 (https://github.com/github/hubot-scripts/issues/1113) in favor of packages for each script.


GitHub


リファレンス

Emoji Prefixに学ぶgitのコミットの分け方

こちらの記事を読んで。

http://memo.goodpatch.co/2016/07/beautiful-commits-with-emojis/

この記事では、Emoji Prefix というコミットメッセージに関するルールについて紹介している。
どんなルールかというと、「コミットメッセージの先頭には、コミットの内容に合った Emoji をつけましょう」というものらしい。

Prefix に使える Emoji の種類をルール化し、コミットメッセージにはいずれかの Emoji を必ずつけるように徹底することで
コミットの内容がわかりやすくなるだけでなく、粒度も揃うという効果が期待できるようだ。

以下、記事から引用。

最も期待している効果は、コミットが綺麗になることです。 開発の現場では「インデントの修正と機能の修正を同じコミットにしないでください 😭 」といった悲痛な叫びをよく耳にします。 Emoji Prefixのルールでは「インデントの修正と機能の修正」を同時に表すEmojiが定義されていないので、Emoji Prefixに従ってEmojiをつけるためにはコミットを分ける必要がでてきます。 このように適切に定義されたEmoji Prefixを使うことで、ごった煮コミットが作られにくくなり、ある程度機械的にコミットの粒度を揃えることができます。 コミットが綺麗になればコードレビューの時間を節約できたり、歴史を追いやすくなるのでとても嬉しいことですね🎉

なるほどー面白いなーと思いつつ、そういえば社内でも「コミット分けろっていうけど、じゃあ適切なコミットの粒度ってなんなんだ」という議論がちょうど起きていたので
この Emoji Prefix のきっかけとなった(と、記事では書かれている)Atom のコントリビュートガイド にはどんな種類の Emoji が定義されているのか、気になって確認してみた。


Atom での Emoji Prefix

ref. https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages

英語が怪しいが、おおむねこういう内容だという認識です。

  • 🎨 :コードのフォーマットや構造を改善した
  • 🐎 :パフォーマンスを改善した
  • 🚱 :メモリリークを修正した
  • 📝 :ドキュメントを書いた
  • 🐧 :Linux 環境固有の問題を修正? (fix something on Linux)
  • 🍎 :Mac 環境固有の問題を修正?
  • 🏁 :Windows 環境固有の問題を修正?
  • 🐛 :バグを修正した
  • 🔥 :コードやファイルを削除した
  • 💚 :CI に関する修正
  • ✅ :テストを書いた
  • 🔒 :セキュリティ関連
  • ⬆️ :dependencies (依存ライブラリ?) をアップグレードした
  • ⬇️ :dependencies をダウングレードした
  • 👕 :lint の警告を remove した (lint で警告されていたところを修正した、の意?)

また、Emoji 以外のガイドとしては

  • 現在形を使う(Added feature でなく Add feature。英語のコミットメッセージでよく言われるやつ)
  • 命令形を使う(Moves cursor to... でなく Move cursor to...、のように3単現の s とか不要。これもよく聞く)
  • 1行目は72文字以下
  • issue や PR へのリファレンスはふんだんに (liberally) <- #xxx を多用しなさいってこと?
  • ドキュメントの変更だけの場合、[ci skip] を含める


所感

ぱっと見で思ったこととしては

  • 先頭1文字でコミット内容がだいたいわかる、というのは良いと思う
    • コミット内容に応じて [Tag] をつけようかという話が社内であったので、それより圧倒的にスマートになる
  • OS 固有の修正のためにそれぞれの Emoji を用意してるの面白い。が、自分達のプロダクトでは必要ないだろうなー
  • あくまで個人的な感想だけど、直感的にわかりづらい Emoji がいくつかあるような。。。
  • いずれにしてもこんなに種類あると意味を覚えてらんなさそう

実際には、記事を書いてくださった Goodpatch さんがやられているように
これをそのままマネするのではなく、チームに合った独自ルールを定義することになるんでしょう。

気になったのは、「機能改善」とかでひとつの Emoji にしてしまうと
内容は分けられるけど粒度はものによってはばかでかいコミットになってしまわないかなーというところ。
これも、チームとしてのコミット粒度に対する考え方によるんでしょうね。
(粒度を揃えるというよりも「1つのコミットで1つのことをやる」を徹底させる目的と捉えた方が適切か)

自分のチームに合ったルールを作る、という部分に難しさはあるが、それなりに効果がありそうなので検討したい。
好き嫌いあるけど、メッセージが華やかになるのいいすね。


おまけ

記事に Tips として書いてあるコミットテンプレートという機能が地味にすごい。

reveal.jsで外部Markdownファイルを読み込む

いつも忘れるのでメモ。
index.html 内に直接書くのではなく、別途用意した Markdown ファイルをスライドにしたい。

body に以下のように記述する。

<body>
    <div class="reveal">
      <div class="slides">
        <section data-markdown="./index.md"
                 data-separator="^\n\n\n"
                 data-separator-vertical="^\n--\n"
                 data-separator-notes="^Note:">
        </section>
      </div>
    </div>
    ...
</body>

こうすればスライドの内容は index.md というファイルに書くことができる。

ページの区切りは data-separator で指定することができて、上の例では改行 3 つ分としている。
--- にすると MarkdownGitHub で見たときに区切り線でいっぱいになるので避けた。

また、このように外部ファイルを読み込む場合はそのまま index.html を開いてもだめで
ローカルサーバーを起動する必要がある。

f:id:dackdive:20160629210349p:plain

Failed to get the Markdown file ./index.md. Make sure
that the presentation and the file are served by a HTTP server
and the file can be found there. NetworkError: Failed to
execute 'send' on 'XMLHttpRequest': Failed to load 'file:///
Users/yamazaki/workspace/reveal.js-template/index.md'.

どんな方法でサーバーを起動してもいいんだけど、面倒なのとその他の設定もよく忘れてしまうのでテンプレートを作った。

Node の http-server を使っている。

ES2015時代のJavaScriptテストツールまとめ

最近になって JavaScript を勉強中です。
ES2015 と React にもちょっとずつ慣れてきましたが、テストについては Karma とか mocha とか名前は聞くけど何が何なのかよくわかってなかったので、軽く整理してみます。


JavaScript テストツールの大まかなカテゴリ

JavaScript のテストツールは、だいたい以下のようなカテゴリに分類することができます。

  1. アサーションライブラリ
  2. テストフレームワーク(テスティングフレームワーク
  3. テストランナー

以下、それぞれについての説明と、代表的なライブラリを見ていきます。
★は個人的にこれを使おうかなと思ったやつです。


1. アサーションライブラリ

テストの一番重要な部分である、「A は B である(assert A === B)」を検証する(ためのメソッドを提供する)ライブラリ。
代表的なものとして、以下があります。

Node.js の標準 assert

Node.js 自体にも assert モジュール があるようです。

expect.js
  • Redux の examples のテストで使われているのはこれ
  • メソッドとしては expect() のみ
chai
  • mocha の Introduction で使われているのはこれ
  • メソッドとしては should(), expect(), assert() がある
power-assert
  • テスト界隈で有名な t_wada さんが開発したライブラリ
  • 厳密に言うとアサーションライブラリではないらしい
  • メソッドとしては assert() のみ
  • Node の assert と比べ、テスト結果として出力される情報がわかりやすい
  • 参考:5minで分かるpower-assert


2. テストフレームワーク

テストを実行するためのツール。
テストフレームワークに従った書き方でテストを書き、実行するとテスト結果を確認することができる。

代表的なものは以下。

mocha
  • Redux の examples のテストで使われているのはこれ
  • テストスイートに相当するものを describe で、テストメソッドに相当するものを it で書く
  • サンプル
import { expect } from 'chai';
import add from './my-add-function';

describe('add() の機能', () => {
  it('1 + 1 は 2 になる', () => {
    expect(add(1, 1)).to.equal(2);
  });

  it('負の数も扱うことができる', () => {
    expect(add(-1, 1)).to.equal(0);
  });
});
Jasmine


3. テストランナー

先ほどのテストフレームワークだけでもテストを実行することが可能ですが、
テストランナーを使うと様々なブラウザ環境でテストを実行させたり、テスト結果やカバレッジを見やすい形式で出力することができるそうな。

代表的なものは以下。

Karma
testem
  • 名前だけ聞いた。現在も流行っているのかどうか不明


番外編

上記の3カテゴリにはあてはまりませんが、それ以外に有名なライブラリをいくつか。
これらはいずれも以下の記事で紹介されており、一通り読むとどんなものか知ることができます。React のテストの書き方を学ぶ上でも大変良い記事です。

Sinon.js
(React 限定)Enzyme


まとめと TODO

調べる前はなんでいろんなものを組み合わせて使う必要があるのかわかってませんでしたが、それぞれ役割が異なるんですね。

各ツールについてはなんとなく理解できたので、
次は実際に karma + mocha + power-assert という組み合わせで、ES2015 で書いた React/Redux アプリのテストを書いてみようと思います。

リファレンス

AsciiDocによる継続的ドキュメント開発〜1.インストールと基本的な使い方

はじめに

ドキュメントを書くための言語としておそらく今一番ポピュラーなのは Markdown だと思いますが、
かっちりした仕様書やユーザーマニュアルなど、より本格的なドキュメント作成にはやや機能不足と感じることがあります。

私も以前、ドキュメントを Gitbook で作れないか検討したことがあったんですが
表を書くのがめんどくさかったり、図のキャプションなどに対応していなかったことから断念しました。

そこで、今回は Markdown よりも表現力の高い言語として知られる AsciiDoc を試してみます。
AsciiDoc は O'reilly でも採用されているほか、Pro GitJavaScript Promise の本 などの非常に完成度の高い技術ドキュメントが AsciiDoc で書かれています。

(参考記事)
テクニカルライティングの未来を先取り ー Asciidoc フォーマットと GitHub を使って技術書 Pro Git を共同執筆 — Japanese Official — Medium
JavaScript Promiseの本を書きました | Web Scratch

やりたいことは色々とあるんですが、まずはインストールと基本的な使い方を学ぶところまでやってみます。

参考書籍

この書籍を読みながら勉強中です。
本屋でざっと流し読みした感じ、AsciiDoc で書いたドキュメントを GitHub で管理したり、校正ツールや CI を導入したりと
自分のやりたいことがそのものずばりって感じです。


インストール

Asciidoctor という AsciiDoc の Ruby 実装をインストールします。 AsciiDoc のリポジトリ を見ると HTML などに変換するプロセッサーも用意されているようですが、
実際に AsciiDoc を HTML や pdf に変換するためのプロセッサーは Asciidoctor の方が一般的のようです。

ブログを書いた時点での Ruby のバージョンは以下です。

$ ruby -v
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]
$ gem -v
2.4.5.1
$ bundle -v
Bundler version 1.12.5

インストール方法は、普通に

$ gem install asciidoctor

を実行するか、または Gemfile を用意し

source 'https://rubygems.org'
gem 'asciidoctor'

と記述した後、

$ bundle install

を実行します。

$ bundle install
Fetching gem metadata from https://rubygems.org/
Fetching version metadata from https://rubygems.org/
Resolving dependencies...
Installing asciidoctor 1.5.4
Using bundler 1.12.5
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

インストールすると asciidoctor というコマンドが実行可能になります。


基本的な使い方:HTML, pdf, epub の作成

さっそく何かドキュメントを書いてみます。
拡張子は .adoc または .asciidoc にします。

= はじめての AsciiDoc ドキュメント

これは AsciiDoc で書かれたドキュメントです。

== セクション

``=`` から始まる行はセクションの見出しになります。 +
``==``, ``===``, ... のように `=` を重ねるとレベルが1つ下がります。

== テーブル(表)

.テーブル見出し

|=======================
|Col 1|Col 2      |Col 3
|1    |Item 1     |a
|2    |Item 2     |b
|3    |Item 3     |c
|=======================

CSV 形式で書くこともできます。

[format="csv"]
|======
1,2,3,4
a,b,c,d
A,B,C,D
|======

== ソースコード

[source, javascript]
----
export default class MyComponent extends React.Component {
    render() {
        <div>Hello, World!</div>
    }
}
----
HTML への変換

HTML への変換は、asciidoctor コマンドを実行するだけです。

$ ls
sample-doc.adoc
$ asciidoctor sample-doc.adoc
$ ls
sample-doc.adoc sample-doc.html

生成した HTML はこちら。

f:id:dackdive:20160614013736p:plain

デフォルトのスタイルでもイイ感じですが、ソースコードが色付け(シンタックスハイライト)されてませんね。

ソースコードシンタックスハイライト

http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#source-code を読むと、
ファイルの先頭で source-highlighter という変数を定義する必要があるようです。
また、シンタックスハイライト用のプラグインをいくつかの選択肢の中から選んでインストールする必要があります。

今回は coderay を選択します。

$ gem install coderay

でインストールした後、.adoc ファイルの先頭に以下を追記します。

:source-highlighter: coderay

この状態で HTML に変換すると、シンタックスハイライトが有効になります。

pdf への変換

HTML 以外の形式への変換は、別途プラグインをインストールする必要があります。
Converters の項を参照)

pdf については asciidoctor-pdf をインストールします。

$ gem install asciidoctor-pdf

コマンドが asciidoctor-pdf になること以外は HTML の時と変わりません。

$ ls
sample-doc.adoc sample-doc.html
$ asciidoctor-pdf sample-doc.adoc
$ ls
sample-doc.adoc sample-doc.html sample-doc.pdf

生成した pdf がこれです。

f:id:dackdive:20160614023314p:plain

フォントなど、スタイルで気に入らない部分があるかもしれませんが CSS で調整可能のようです。これについてはまた今度。

epub への変換

epub については asciidoctor-epub3 をインストールします。
参考:How to Convert AsciiDoc to EPUB3 with Asciidoctor | Asciidoctor

まだ試せてないので今後の課題として。


(おまけ)エディタの設定

最後に、AsciiDoc を書きやすくするためにエディタも設定しておきます。
Atom の場合、公式ドキュメント
http://asciidoctor.org/docs/editing-asciidoc-with-live-preview/#atom
にあるように、以下2つのプラグインをインストールすると良さそう。

asciidoc-preview を入れると(Mac の場合)cmd + Shift + A でプレビューを表示させることができます。便利。

f:id:dackdive:20160614204519p:plain

一方、私は普段 Vim を使ってるんですが、Markdown ではできたプレビュー機能を提供するプラグインは見つからず。

参考:[vim]vimでmarkdownをリアルタイムプレビューできるようにする - dackdive's blog


触ってみた感想

Markdown ではネックだった表が CSV 形式で書けたり、ちょっと調べた感じだと図のキャプションに対応していたりと
Markdown では対応できなかったところをカバーしてくれそうな印象。

ただ、以前 Gitbook 調査していて gitbook-cli を使ったときは

$ gitbook serve

みたいなコマンドでローカルサーバーが立ち上がり、以後、ファイルの変更を検知して常に最新の HTML を表示することができていたんですが
Asciidoctor 単独だとそういった機能は提供していないので、gulp タスクなり Makefile なりでそのあたりも自分で作る必要がありそうです。


今後の予定

今後、こういったことをできるようにしていきたいです。

  • epub 形式に変換する
  • ローカルでドキュメントを書くときはローカルサーバーを起動し、自動で HTML 変換する
  • RedPen や textlint などの文法チェックツールを使用する
  • Travis CI などの CI ツールで GitHub にコミット時に自動的に文法チェックを実行する
  • CI の一環で、最新のドキュメントの HTML 版が常にどこかにデプロイされるようにする

とりあえず、今後も色々と機能追加していくと思うので、コードを GitHub に上げておきます。