dackdive's blog

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

package.jsonのパッケージバージョンに記載される ^ (キャレット) とは?どうしてつくのか?

$ npm install --save react

などのコマンドでパッケージをインストールすると、package.json にはインストールした(その時点での最新)バージョンが記載されますが
そのとき

"dependencies": {
  "react": "^15.3.2"
}

というように、バージョン番号の前に ^ がつきます。

これの意味と、どうしてつくのか調べてみたメモ。

前提

Node.js のパッケージのバージョンは Semantic Versioning (semver) というルールに従っている。

semver は X.Y.Z という3桁の数字で表されるバージョンで、

  • X: major version。後方互換性のない変更の場合にこのバージョンを上げる
  • Y: minor version。後方互換性のある変更の場合、このバージョンを上げる
  • Z: patch version。後方互換性のあるバグ修正などの場合、このバージョンを上げる

というルールになっている。


^ (キャレット) とは?

このあたりを読む。
https://docs.npmjs.com/misc/semver#caret-ranges-123-025-004

Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple

パッケージのバージョン番号のうち、一番左の非ゼロの値を変更しないようなアップデートを許容する、という意味のよう。

つまり、

  • バージョンが 1.2.3 の場合、1.2.3 <= n < 2.0.0
  • バージョンが 0.2.3 の場合、0.1.2 <= n < 0.2.0
  • バージョンが 0.0.3 の場合、0.0.3 <= n < 0.0.4

というように、対象のバージョンによって挙動が異なる。

そして、npm install や npm update を行ったとき、許容される範囲の中で最新のバージョンをインストールしたりアップデートしたりする。
そのため、package.json だけだとバージョンを完全に固定できているわけではない


【補足】package.json でバージョンを完全に固定するには?

後述するように ^ をつけないように設定を変更するか、npm shrinkwrap コマンドを使って npm-shrinkwrap.json というファイルを生成しておく。
https://docs.npmjs.com/cli/shrinkwrap

// npm-shrinkwrap.json (例)
{
  "name": "react-es6-tutorial",
  "version": "1.0.0",
  "dependencies": {
    "jquery": {
      "version": "2.2.2",
      "from": "jquery@2.2.2",
      "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.2.tgz"
    },
    "marked": {
      "version": "0.3.5",
      "from": "marked@0.3.5",
      "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz"
    },
    "react": {
      "version": "0.14.7",
      "from": "react@0.14.7",
      "resolved": "https://registry.npmjs.org/react/-/react-0.14.7.tgz",
      "dependencies": {
        "envify": {
          "version": "3.4.1",
          "from": "envify@>=3.0.0 <4.0.0",
          "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.1.tgz",
          "dependencies": {
            "through": {
       ...


【補足】~ (チルダ)

^ と似たような記号として、~ (チルダ) もある。
https://docs.npmjs.com/misc/semver#tilde-ranges-123-12-1

Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.

minor version が明記されていれば patch version のアップデートを許容、明記されていなければ minor version のアップデートを許容。
なので、

  • ~1.2.3 の場合: 1.2.3 <= n < 1.3.0
  • ~1.2 の場合: 1.2 <= n < 1.3.0
  • ~1 の場合: 1.0.0 <= n < 2.0.0


^ (キャレット) はなぜつくのか?

package.json にデフォルトで ^ がつく挙動はどこで設定しているのだろうか?気になって調べてみた。
どうやら npm config に関係する設定があるらしい。


save-prefix

デフォルトでつく prefix を設定する。

$ npm config get save-prefix
^

# デフォルトを ~ に変更
$ npm config set save-prefix "~"
$ npm config get save-prefix
~


save-exact

--save--save-dev したときに prefix をつけるか、あるいは prefix をつけずに package.json に記載してバージョンを完全に固定するかを選べる。
デフォルトは false。なので prefix がつく。

$ npm config get save-exact
false

# デフォルトで完全なバージョン固定になるようにする
$ npm config set save-exact true


【補足】npm config で設定した値

どうやらこの設定は ~/.npmrc に保存されているらしい。
.npmrc はプロジェクトごとに置くことも可能。
https://docs.npmjs.com/misc/config#npmrc-files


^ (キャレット) をつけずにインストールする方法はないのか?

上述したように、npm config で

$ npm config set save-exact true

と設定しておくと、npm install 時の挙動を変更することができる。

あるいは、npm install 時のオプションとしても

--save-exact (略記は -E)

が用意されている。
https://docs.npmjs.com/cli/install

これをつけると、^ なしでインストールできる。

# -SE は --save --save-exact の略
npm i -SE react

Hubotに管理画面のような静的ページを追加する

メモ。
ブラウザから /admin にアクセスしたら管理画面みたいなものが開いて Bot の簡単なカスタマイズができる、みたいなことがやりたくて
特定の URL で静的なページを返すようなことができるのか調べてみました。

結論から言うと robot.router が Express サーバーのインスタンスになっているようなので、この変数に対して通常の Express アプリ開発と同じようなノリでできそうです。

公式ドキュメントのこちらに記載があります。
https://hubot.github.com/docs/scripting/#http-listener

Hubot includes support for the express web framework to serve up HTTP requests. It listens on the port specified by the EXPRESS_PORT or PORT environment variables (preferred in that order) and defaults to 8080.

また、Hubot のソースコードで言うとこのあたり。
https://github.com/github/hubot/blob/master/src/robot.coffee#L411

サンプルコード

CoffeeScript ではなく ES2015 で書いています。
参考:HubotをES2015で書いてHerokuにデプロイする - dackdive's blog

module.exports = (robot) => {
  robot.router.get('/admin', (req, res) => {
    res.end('Hello, World!');
  });
};
結果

f:id:dackdive:20160806113925p:plain

表示できました。

注)
$ ./bin/hubot

を直接実行するとデフォルトでポートは 8080 番が使われますが、この時は

$ heroku local web

# Procfile は以下
web: bin/hubot -a slack --require build

コマンドを使い、Heroku アプリのローカル実行として実行しています。
heroku local コマンドでアプリを起動した時、デフォルトのポートは 5000 番なので キャプチャでは :5000 にアクセスしています。


ejs などのテンプレートエンジンも使ってみる

スクリプト内に HTML をゴリゴリ書いていくのはつらいので、JavaScript のテンプレートエンジンも使えるか試してみます。
Express とテンプレートエンジンの使い方についてはこちらの記事を参考にします。

ゼロからはじめるExpress + Node.jsを使ったアプリ開発 - Qiita

$ npm i --save ejs

でインストールします。

スクリプトとテンプレートファイルのディレクトリ構成は以下のようになっています。

.
├── Procfile
├── README.md
├── bin
│   ├── hubot
│   └── hubot.cmd
├── build ...................... src をトランスパイルしたスクリプト
│   └── index.js
├── external-scripts.json
├── package.json
├── src
│   └── index.js
└── views ...................... テンプレート置き場
    └── index.ejs
スクリプト&テンプレート
// src/index.js
module.exports = (robot) => {
  robot.router.set('view engine', 'ejs');

  robot.router.get('/admin', (req, res) => {
    res.render('index', {});
  });
};
<!-- views/index.ejs -->
<html>
<body>
    <h1>This is Admin Page!</h1>
</body>
</html>
結果

f:id:dackdive:20160806115757p:plain

できました。


今後の予定

まだ軽く試しただけなので、このまま通常の Express アプリ開発と同じように、JS ライブラリや UI フレームワークを使って静的サイトを構築していけるのかはわかりません。
あと、このページで Bot のメッセージ定型文を登録・管理できるように...とかしたくなったら、データの永続化方法とかも調べてみないといけませんね。

Persistence の項を読むと hubot.brain という KVS を提供しているみたいだけど、Heroku で使おうとするとどうなるのか。

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 を使っている。