dackdive's blog

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

Reactでフォームの項目をどう扱うか問題

メモ。

なんの話か

  • React でフォーム項目を簡潔に書く方法がわからない
  • 管理されたコンポーネントで書こうとすると、項目の数だけ state と対応するイベントハンドラが必要になる
    • 親に渡す必要がある、とかだとさらにしんどい
// 基本形
class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };

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

  onChangeName(e) {
    console.log(e.target.value);
    this.setState({ name: e.target.value });
  }

  render() {
    return (
      <form>
        <input type="text" value={this.state.name} onChange={this.onChangeName} />
      </form>
    );
  }
}


どうすべきか

いくつか方法はある。
1 と 2 は微妙に違うが、「イベントハンドラを1つにまとめる」という戦略自体は同じ。

1. onChange イベントハンドラに引数で key を渡す
export default class MyFormCmp extends React.Component {

  constructor(props) {
    this.state = {
      name: '',
    };

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

  onChangeField(e, key) {
    console.log(key, e.target.value);
    this.setState({ [key]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          type="text" value={this.state.name} onChange={(e) => this.onChangeField(e, 'name')} />
      </form>
    );
  }
}


2. イベントハンドラ内で event.target.name を参照する

参考:ES2015 以降で React 書くなら form 部品での onChange で setState するのもう全部これでいいんじゃないかなあ - BattleProgrammerShibata

export default class MyFormCmp extends React.Component {
  // 略

  onChangeField(e) {
    console.log(e.target.name, e.target.value);
    this.setState({ [e.target.name]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          name="name"
          type="text"
          value={this.state.name}
          onChange={this.onChangeField}
        />
      </form>
    );
  }
}


3. (deprecated) LinkedStateMixin というアドオンを使う

参考:Two-Way Binding Helpers | React (日本語)

冒頭で

ReactLink is deprecated as of React v15. The recommendation is to explicitly set the value and change handler, instead of using ReactLink.

と書いているので使わないと思うけど。

加えて、ES2015 で書いた React コンポーネントはそのままでは Mixin を使うことができないので
react-mixin というライブラリを使う。

参考:React v0.13から使えるようになったES6のclass構文でmixinを使う - Qiita

import React from 'react';
import reactMixin from 'react-mixin';
import { render } from 'react-dom';
import LinkedStateMixin from 'react-addons-linked-state-mixin';

class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };
  }

  render() {
    return (
      <form>
        <input
          type="text"
          valueLink={this.linkState('name')}
        />
      </form>
    );
  }
}
reactMixin(MyFormCmp.prototype, LinkedStateMixin);


4. (未検証) react-jsonschema-form を使う

元々は JSON Schema という、JSON オブジェクトの値の有無や型のチェックをするための仕様があって
react-jsonschema-form は JSON Schema からフォームを生成できるライブラリっぽい。

以下、JSON Schema の例。properties がフォームの項目に相当。

{
  "title": "A registration form",
  "description": "A simple form example.",
  "type": "object",
  "required": [
    "firstName",
    "lastName"
  ],
  "properties": {
    "firstName": {
      "type": "string",
      "title": "First name"
    },
    "lastName": {
      "type": "string",
      "title": "Last name"
    },
    "age": {
      "type": "integer",
      "title": "Age"
    },
    "bio": {
      "type": "string",
      "title": "Bio"
    },
    "password": {
      "type": "string",
      "title": "Password",
      "minLength": 3
    }
  }
}

実際にはこれとは別に UISchema なるものの定義が必要らしいが、ここから生成されるフォームはこんな感じ。
react-jsonschema-form playground で確認できる)

f:id:dackdive:20161019212542p:plain

いかんせんまだ触ったことがないので、スタイルやバリデーションルール含めどれぐらいきめ細かく設定ができるのか不明。


おわりに

3 は現在推奨されてないので除外。4 が一番気になってる。

1 と 2 については正直差はないと思う。
(JSX 側で state の key を知ってないといけない、という点で)

また、1 ~ 3 の方法は「入門 React」にも書いてた。(p.81 ~ p.84 あたり)

npm installしたパッケージの更新確認とアップデート(npm-check-updates)

タイトルの通り。
npm install --save なり --save-dev なりして package.json に書き込まれたパッケージのバージョン、どうやって定期的にアップデートしていけばいいかわからなかったので。

新しいバージョンがリリースされているかどうかの確認と、実際にどのように新しいバージョンにアップデートすればいいのか調べてみた。

今回サンプルに使う package.json

package.json の例として、以前、React のチュートリアルをやったときのリポジトリを使う。

{
  "name": "react-es6-tutorial",
  "version": "1.0.0",
  "description": "React Tutorial written in ES6",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "lint": "eslint src/**/*.js",
    "webpack": "webpack -w",
    "start": "concurrent \"npm run webpack\" \"npm run lite\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.7.2",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "concurrently": "^2.0.0",
    "eslint": "^2.9.0",
    "eslint-config-airbnb": "^9.0.1",
    "eslint-plugin-import": "^1.8.0",
    "eslint-plugin-jsx-a11y": "^1.2.0",
    "eslint-plugin-react": "^5.1.1",
    "webpack": "^1.12.14"
  },
  "dependencies": {
    "jquery": "^2.2.2",
    "marked": "^0.3.5",
    "react": "^0.14.7",
    "react-dom": "^0.14.7"
  }
}

この package.json に記載された通りのバージョン(babel-core なら 6.7.2)がインストールされた状態、という前提で話を進める。


現在インストールされているバージョンを確認する方法

そもそも、現在インストールされているバージョンを確認するにはどうしたらいいのか。
これは、後述する npm outdated コマンドを使うこともできるが、別の方法として

$ npm list --depth=0

コマンドを使うという方法もある。

$ npm list --depth=0
react-es6-tutorial@1.0.0 /Users/yamazaki/workspace/react/react-tutorial
├── babel-core@6.7.2
├── babel-loader@6.2.4
├── babel-preset-es2015@6.6.0
├── babel-preset-react@6.5.0
├── concurrently@2.0.0
├── eslint@2.9.0
├── eslint-config-airbnb@9.0.1
├── eslint-plugin-import@1.8.0
├── eslint-plugin-jsx-a11y@1.2.0
├── eslint-plugin-react@5.1.1
├── jquery@2.2.2
├── marked@0.3.5
├── react@0.14.7
├── react-dom@0.14.7
└── webpack@1.12.14

後述する npm outdated はバージョンが古くなったパッケージしか表示されないのに対し、
こちらはインストール済みのすべてのパッケージの情報が出力できる。


最新バージョンがあるかどうか確認する方法

インストールしたパッケージに新しいバージョンが存在するかどうか、確認する。
これは、

$ npm outdated

を使う。
https://docs.npmjs.com/cli/outdated

このコマンドを実行すると、以下のような結果が出力される。

f:id:dackdive:20161009200339p:plain

$ npm outdated
Package                 Current  Wanted  Latest  Location
babel-core                6.7.2  6.17.0  6.17.0  babel-core
babel-loader              6.2.4   6.2.5   6.2.5  babel-loader
babel-preset-es2015       6.6.0  6.16.0  6.16.0  babel-preset-es2015
babel-preset-react        6.5.0  6.16.0  6.16.0  babel-preset-react
concurrently              2.0.0   2.2.0   3.1.0  concurrently
eslint                    2.9.0  2.13.1   3.7.1  eslint
eslint-config-airbnb      9.0.1   9.0.1  12.0.0  eslint-config-airbnb
eslint-plugin-import      1.8.0  1.16.0   2.0.0  eslint-plugin-import
eslint-plugin-jsx-a11y    1.2.0   1.5.5   2.2.2  eslint-plugin-jsx-a11y
eslint-plugin-react       5.1.1   5.2.2   6.3.0  eslint-plugin-react
jquery                    2.2.2   2.2.4   3.1.1  jquery
marked                    0.3.5   0.3.6   0.3.6  marked
react                    0.14.7  0.14.8  15.3.2  react
react-dom                0.14.7  0.14.8  15.3.2  react-dom
webpack                 1.12.14  1.13.2  1.13.2  webpack

各パッケージに対し、Current, Wanted, Latest という3つの列が表示されている。
それぞれの列の意味は以下の通り。

Current

現在インストールされているバージョン

Wanted

存在するバージョンのうち、package.json に記載された semver の条件を満たす最新のバージョン。
たとえば、jQuery については

Package                 Current  Wanted  Latest  Location
jquery                    2.2.2   2.2.4   3.1.1  jquery

となっているが、これは 2.x 系の最新は 2.2.4 がリリースされており、それよりメジャーバージョンが1つ上の 3.x 系で 3.1.1 がリリースされている。
ただし、package.json^2.2.2 という記述では

2.2.2 <= n < 3.0.0

という範囲での最新バージョンしか許容されないため、Wanted は 3.x 系ではなく 2.x 系の最新バージョンとなる。

^2.2.2 という記述については以前ブログに書いた。
package.jsonのパッケージバージョンに記載される ^ (キャレット) とは?どうしてつくのか? - dackdive's blog


Latest

npm outdated のドキュメント によると

latest is the version of the package tagged as latest in the registry.

とあるが、そのパッケージの最新バージョンと考えてよさそう。


npm outdated の問題点

さて、この状態で npm update を実行し、再度 npm outdated で結果を確認してみる。

$ npm update
npm WARN peerDependencies The peer dependency react@^0.14.8 included from react-dom will no
npm WARN peerDependencies longer be automatically installed to fulfill the peerDependency
npm WARN peerDependencies in npm 3+. Your application will need to depend on it explicitly.
marked@0.3.6 node_modules/marked
babel-loader@6.2.5 node_modules/babel-loader

...(略)


$ npm outdated
Package                 Current  Wanted  Latest  Location
concurrently              2.2.0   2.2.0   3.1.0  concurrently
eslint                   2.13.1  2.13.1   3.7.1  eslint
eslint-config-airbnb      9.0.1   9.0.1  12.0.0  eslint-config-airbnb
eslint-plugin-import     1.16.0  1.16.0   2.0.0  eslint-plugin-import
eslint-plugin-jsx-a11y    1.5.5   1.5.5   2.2.3  eslint-plugin-jsx-a11y
eslint-plugin-react       5.2.2   5.2.2   6.3.0  eslint-plugin-react
jquery                    2.2.4   2.2.4   3.1.1  jquery
react                    0.14.8  0.14.8  15.3.2  react
react-dom                0.14.8  0.14.8  15.3.2  react-dom

babel-core などのパッケージは最新の 6.17.0 にアップデートされたため、表示されなくなった。

ただし、ESLint などについてはメジャーバージョンが上がったものがリリースされているが、npm update は package.json に記載された semver のルールに従うため、^ つきの記載だとメジャーバージョンのアップデートまでは行ってくれない。

また、npm outdated は パッケージの更新確認はやってくれるが、package.json の更新まではやってくれない という問題もある。
そのため、

  • npm outdated で新しいバージョンがリリースされてないか確認する
  • 新しいバージョンがリリースされていた場合、該当のパッケージを package.json から削除する
  • npm install --save/--save-dev で再度インストールする

という手順になってしまい、ややめんどくさい。
どうするか。


npm-check-updates を使う

というわけで色々調べていると、更新確認とアップデートに便利な npm-check-updates というパッケージを見つけた。

npm-check-updates は

$ npm install -g npm-check-updates

というようにグローバルにインストールする。インストールすると

$ ncu

というコマンドが使えるようになる。

ncu を実行してみる。

$ ncu
⸨░░░░░░░░░░░░░░░░░░⸩ ⠦ :
 jquery                   ^2.2.2  →   ^3.1.1
 react                   ^0.14.7  →  ^15.3.2
 react-dom               ^0.14.7  →  ^15.3.2
 concurrently             ^2.0.0  →   ^3.1.0
 eslint                   ^2.9.0  →   ^3.7.1
 eslint-config-airbnb     ^9.0.1  →  ^12.0.0
 eslint-plugin-import     ^1.8.0  →   ^2.0.0
 eslint-plugin-jsx-a11y   ^1.2.0  →   ^2.2.3
 eslint-plugin-react      ^5.1.1  →   ^6.4.0

The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, use ncu -a/--upgradeAll.

 marked                 ^0.3.5  →   ^0.3.6
 babel-core             ^6.7.2  →  ^6.17.0
 babel-loader           ^6.2.4  →   ^6.2.5
 babel-preset-es2015    ^6.6.0  →  ^6.16.0
 babel-preset-react     ^6.5.0  →  ^6.16.0
 webpack              ^1.12.14  →  ^1.13.2

Run ncu with -u to upgrade package.json

内容としては npm outdated を実行したときと同じ。
上部(The following... より上に列挙されているもの)は、新しいメジャーバージョンがリリースされているもの。
下部はマイナーバージョン以下で新しいバージョンが存在するもの。The following... の文章に書いてあるとおり、こちらは npm update すれば最新のバージョンがインストールできる。

そして、ncu に -u オプションをつけると package.json の更新が行われる。

$ ncu -u

...

Upgraded /Users/yamazaki/workspace/react/react-tutorial/package.json

$ git diff package.json
diff --git a/package.json b/package.json
index 11e9a27..f3e6979 100644
--- a/package.json
+++ b/package.json
@@ -17,18 +17,18 @@
     "babel-loader": "^6.2.4",
     "babel-preset-es2015": "^6.6.0",
     "babel-preset-react": "^6.5.0",
-    "concurrently": "^2.0.0",
-    "eslint": "^2.9.0",
-    "eslint-config-airbnb": "^9.0.1",
-    "eslint-plugin-import": "^1.8.0",
-    "eslint-plugin-jsx-a11y": "^1.2.0",
-    "eslint-plugin-react": "^5.1.1",
+    "concurrently": "^3.1.0",
+    "eslint": "^3.7.1",
+    "eslint-config-airbnb": "^12.0.0",
+    "eslint-plugin-import": "^2.0.0",
+    "eslint-plugin-jsx-a11y": "^2.2.3",
+    "eslint-plugin-react": "^6.4.0",
     "webpack": "^1.12.14"
   },
   "dependencies": {
-    "jquery": "^2.2.2",
+    "jquery": "^3.1.1",
     "marked": "^0.3.5",
-    "react": "^0.14.7",
-    "react-dom": "^0.14.7"
+    "react": "^15.3.2",
+    "react-dom": "^15.3.2"
   }
 }

package.json の更新が正しく行われた。

なお、ncu コマンドは

$ ncu -u [パッケージ名]

というようにパッケージ名を指定することができる。さらに、パッケージ名の部分には正規表現を使用することができる。

# babel-xxx というパッケージのみ対象
$ ncu /babel-/
⸨░░░░░░░░░░░░░░░░░░⸩ ⠏ :
The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, use ncu -a/--upgradeAll.

 babel-core           ^6.7.2  →  ^6.17.0
 babel-loader         ^6.2.4  →   ^6.2.5
 babel-preset-es2015  ^6.6.0  →  ^6.16.0
 babel-preset-react   ^6.5.0  →  ^6.16.0


また、ncu -c だと babel-core などマイナーバージョン以下がアップデートされたパッケージについては package.json は更新されなかったが、

$ ncu -a

というように、-u のかわりに -a オプションをつけて実行すると、これらのパッケージについても package.json が更新される。


注意点

npm-check-updates は package.json の更新のみ行い、実際のアップデートは行われていないので注意。
ncu -u した後 npm update する必要がある。


リファレンス

Dreamforce'16 Developer Keynoteのメモ

今年は行けなかったので Dev Keynote だけざっと見ました。

以下、雑多なメモ。

キーワード

  1. Lightning
  2. Einstein
  3. Salesforce DX


Lightning

Winter'17 以降の新機能についてデモしながら一通り紹介、といった感じ。
(デモは 13:40 頃から)

f:id:dackdive:20161007134941p:plain

Lightning Data Service

Visualforce で言う Standard Controller みたいな感じ。
Apex 側のコントローラーいらずでオブジェクトの参照や更新ができる。

f:id:dackdive:20161007135421p:plain

デモで紹介していたのは <force:recordPreview> というタグだったけど、
開発者ガイド見る限りいくつか種類があるっぽい。

リリースノート:
https://releasenotes.docs.salesforce.com/en-us/winter17/release-notes/rn_lightning_data_service.htm

開発者ガイド:
https://developer.salesforce.com/docs/atlas.en-us.204.0.lightning.meta/lightning/data_service.htm

JS のコントローラーではこのように書ける。
<force:recordPreview> コンポーネントを Id で見つけてきて、saveRecord() で保存)

f:id:dackdive:20161007135512p:plain


Lightning Action

Classic で言うカスタムボタンを追加して、そこから Lightning Component を表示できる。

f:id:dackdive:20161007135054p:plain

わかりづらいけどカーソルのある「Smart Home」がそう。


Lightning Utility Bar

f:id:dackdive:20161007135858p:plain

画面下部からいつでも呼び出すことのできる Lightning Component。かな?

所感


Einstein

機械学習プラットフォームの Einstein。

f:id:dackdive:20161007140033p:plain

デモがよくわからなかったんだけど、Apache Kafka と組み合わせてデータを学習モデルに放り込んでごにょごにょ...という感じ。

Apache Kafka については mokamoto さんの記事を読もう。

http://qiita.com/mokamoto/items/f453275efe6f056c98fb

KafkaはApache Foundationで開発されているpub-sub形式で分散メッセージング処理を取り扱うためのオープンソースミドルウェアです。 Producerと呼ばれるメッセージの送り手が、Kafkaの中にあるTopicに対してメッセージを送ります。Brokerはそのメッセージのハンドリング及び永続化を行い、ConsumerはTopicを購読してそのメッセージを受け取ることができます。

f:id:dackdive:20161007140203p:plain

train と predict だけ用意したら簡単に機械学習ができるのかな。

デモアプリ、これかと思ったけどたぶん違う。
http://www.dreamhouseapp.io/kafka/


Salesforce DX

f:id:dackdive:20161007140735p:plain

ようやく CI/CD 系の機能について発表がありました。
"Open and standard developer experience" とはまさに。

デプロイだけでなく組織の作成やデータのインポートなども行える CLI が提供されるっぽい。
sfdx ?? Force CLI はどうなるんでしょう。

f:id:dackdive:20161007140742p:plain

なんかいっぱいコマンドある。


デモでは、

f:id:dackdive:20161007140751p:plain

組織作成や

f:id:dackdive:20161007140755p:plain

組織にメタデータをプッシュ

のほか、組織からデータをエクスポートするといったこともやってました。

ただ、この機能については既に Twitter 上でいろいろ言われてるようなので
あまり期待しすぎずに詳細を待ちたいと思います。


おわりに:ロードマップ

f:id:dackdive:20161007140800p:plain

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

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