読者です 読者をやめる 読者になる 読者になる

量産型エンジニアの憂鬱

きっと僕は何物にもなれない。

Re:ゼロから始めるフロントエンド開発

JavaScript フロントエンド React

約3週間前・・・。

てことで、フロントエンドの開発に取り掛かりました。
これまでのフロントの開発経験は、

  • テンプレートエンジン(JSP、Velocity)
  • jQuery
  • CSS

StrutsTilesカスタムタグ でコピペを最小限には抑える努力はしていたのですが、ずっとHTMLの部品をコンポーネント化したいと思っていました。

今携わっている案件ではサーバサイドのフレームワークやテスト、CIやGitなどのインフラも一新しているので、フロントエンドの開発環境も整えさせてくれとおねだり。
最初はコンポーネント化を目標としてReactに目をつけ、そこから広げて行きました。

実現したいこととそれを実現するフレームワークなどは以下の通り

これまでフロントエンドは無知だったので、テスティングフレームワークとか全く知りません。
邪道な組み合わせかもしれません。


これらを組み合わせた開発環境と開発の進め方について記録しておきます。

React + ES6 + Flux でコンポーネントを作る

ReactとFluxを組み合わせた構造とデータフローの概要はこちら
f:id:duck8823:20160630204607p:plain

ざっくり自分的にまとめると

  • ActionCreators(Action)
    • Viewからのアクションを受け取る、外部APIとの接続
    • Dispatcherにアクションを渡す
  • Dispatcher
    • アクションを受け取って、コールバックを実行
  • Store
  • React Views(View)
    • データの表示、UIの動作に対するアクションの発行

f:id:duck8823:20160702103222p:plain
APIとの関係はこんな感じかな。
矢印の色は、依存元を示します。
ViewStoreActionに依存しており、子コンポーネント以外からは依存されていません。
多分ここがポイントで、依存されてないからViewの分離がしやすいんだと思います。
renderするViewを変えるだけで簡単に見た目だけ変えたり、分離したViewから同じStoreActionに依存させることで、異なるView同士の連携ができます。

今回作るコンポーネント

今回作りたいのは、ショッピングカートと注文ボタンのようなもの。
f:id:duck8823:20160630202459g:plain

商品ごとに注文ボタンがあって、注文ボタンをクリックすると対応する商品名がカートに追加され、取り消しボタンに変わります。
取り消しボタンを押したらカートから商品が消え、注文ボタンに戻ります。

ディレクトリ構造はこんな感じ。
f:id:duck8823:20160630211952p:plain

どのクラスから書き始めるのが良いかってことになるんですが、
上記の図を見るとDispatcherは他のクラスのAPIに依存していないので、そこから書き始めるのがよさそう。
Dispatcher -> StoreAction -> View の順番で作るのがファイルの行ったり来たりは少なくなりそうです。
※コールバックの順番を制御したい場合はStoreに依存することになりますが、その場合も小さいDispatcherを作ってから複数のStoreを作り、Dispatcherを大きくするのがよいと思いました。

Dispatcher

app-dispatcher.js

import {Dispatcher} from 'flux';
export default new Dispatcher;

シンプルにそのまま使ってます。コールバックを制御したい場合は拡張していきます。

ActionCreators(Action)

Dispatcher.dispath()

static add(item) {
    AppDispatcher.dispatch({
        type: Constants.ADD,
        item: item
    });
}

staticメソッドの中で、アクションをDispatcherに渡しています。
Storeの中で処理を振り分けるために、typeを渡しています。
あとは、Storeで保存などするためのデータ。ここではitemとしてViewから渡される商品オブジェクトになります。

cart-action.js

import AppDispatcher from "../../app-dispatcher";
import {Constants} from "./constants";

/**
 * カートのAction
 */
export default class CartAction {
    
    static add(item) {
        AppDispatcher.dispatch({
            type: Constants.ADD,
            item: item
        });
    }

    static remove(item) {
        AppDispatcher.dispatch({
            type: Constants.REMOVE,
            item: item
        });
    }
}

クラス全体。ここでは、2種類のActionをDispatcherに渡してます。
今回はシンプルなものですが、外部APIと接続する場合はここで実行します。

Store

Dispatcher.register()

constructor() {
    super();
    ...
    AppDispatcher.register(this._onAction.bind(this));
}

Dispatcherからアクションを受け取るために、コンストラクタでDispatcherにアクションを登録します。
ここで登録したアクションで、処理を振り分けています。


EventEmitter.emit()

 add(item) {
    ...
    this.emit(Constants.ADD);
}

Node.jsのEventEmitterを利用し、emit()でViewに変更があったことを通知します。


cart-store.js

import EventEmitter from 'events';
import AppDispatcher from '../../app-dispatcher';
import {Constants} from './constants';

const CONSTANTS = {
    CART : 'CART'
};
/**
 * カートのStore
 */
class CartStore extends EventEmitter {

    constructor() {
        super();
        var items = window.sessionStorage.getItem(CONSTANTS.CART);
        this._items = items ? JSON.parse(items) : {};
        AppDispatcher.register(this._onAction.bind(this));
    }

    /**
    * 商品を追加する
    * @param item
    */
    add(item) {
        if(this.contains(item)){
            throw new Error("already contains: " + item.id);
        }
        this._items[item.id] = item;
        this._save();
        this.emit(Constants.ADD);
    }

    /**
    * 商品を削除する
    * @param item
    */
    remove(item) {
        if(!this.contains(item)){
            throw new Error("not contains: " + item.id);
        }
        delete this._items[item.id];
        this._save();
        this.emit(Constants.REMOVE);
    }

    /**
    * idが含まれているかどうか
    * @param item
    */
    contains(item) {
        return this._items[item.id] ? true : false;
    };

    /**
    * WebStorageに保存する
    * @private
    */
    _save() {
        window.sessionStorage.setItem(CONSTANTS.CART, JSON.stringify(this._items));
    }

    /**
    * 初期化する
    * @private
    */
    _clear() {
        this._items = {};
        window.sessionStorage.removeItem(CONSTANTS.CART);
    }

    _onAction(action) {
        switch (action.type) {
            case Constants.ADD:
                this.add(action.item);
                break;
            case Constants.REMOVE:
                this.remove(action.item);
                break;
            default:
            // nothing to do.
        }
    }

    getItems() {
        var items = [];
        Object.keys(this._items).forEach((key) => items.push(this._items[key]));
        return items;
    }
}
const cartStore = new CartStore();
export default cartStore;

クラス全体です。EventEmitterを継承しています。
最後にインスタンスを生成し、そのインスタンスをexportします。

View

Store.getter()

constructor(props) {
    super(props);
    this.state = {
        items: CartStore.getItems()
    };
}

コンストラクタ内でStoreから値を取得し、Viewコンポーネントの初期値とします。


EventEmitter.on()

componentWillMount() {
    CartStore.on(Constants.ADD, () => {
        this.setState({
            items: CartStore.getItems()
        });
    });
    ...
}

componentWillMountでコンポーネントがマウントされる際(DOMツリーに追加される前)に実行する処理を登録することができます。 EventEmitterを利用してここではConstants.ADDのコールバックにsetState()を指定しています。


Action.do

render() {
   ...
   <div className="ui vertical animated button" ref="button" onClick={this._onAction.bind(this)}>
   ...
}

render()内でボタン部分にonClickを登録しています。

_onAction() {
    const {contains} = this.state;
    const {item} = this.props;
    if(contains){
        CartAction.remove(item);
    } else {
        CartAction.add(item);
    }
    this.setState({contains: contains ? false : true});
}

クリックした際に呼ばれるメソッド内で、Actionのstaticメソッドを実行します。

カートのView

cart-view.js

import React from "react";
import CartStore from "./cart-store";

import {Constants} from "./constants";

import $ from 'jquery';
import 'semantic-ui-modal/modal.css'
$.fn.modal = require('semantic-ui-modal');

import 'semantic-ui-dimmer/dimmer.css';
$.fn.dimmer = require('semantic-ui-dimmer');

import 'semantic-ui-transition/transition.css';
$.fn.transition = require('semantic-ui-transition');

/**
 * カートのView
 */
export default class CartView extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            items: CartStore.getItems()
        };
    }

    componentWillMount() {
        CartStore.on(Constants.ADD, () => {
            this.setState({
                items: CartStore.getItems()
            });
        });
        CartStore.on(Constants.REMOVE, () => {
            this.setState({
                items: CartStore.getItems()
            });
        });
    }

    _open() {
        $(".ui.modal.cart").modal('show');
    }

    render() {
        return (
            <div>
                <div className="ui labeled button modal-button" ref="modal" onClick={this._open.bind(this)}>
                    <div className="ui button">
                        <i className="shop icon"></i> Cart
                    </div>
                    <div className="ui basic left pointing label count">
                        {this.state.items.length}
                    </div>
                </div>
                <div className="ui modal cart">
                    <i className="close icon"></i>
                    <div className="header">
                        Cart
                    </div>
                    <div className="ui description">
                        <div ref="item-list" className="ui middle aligned divided list item-list">
                            {this.state.items.map((item) => {
                                return (
                                    <div key={item.id} className="item">
                                        <div className="content">
                                            {item.name}
                                        </div>
                                    </div>
                                )
                            })}
                        </div>
                    </div>
                    <div className="actions">
                        <div className="ui black deny button">
                            Cancel
                        </div>
                        <div className="ui positive right labeled icon button">
                            OK
                            <i className="checkmark icon"></i>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}
アクションを実行する注文ボタンのView

cart-button.js

import React from 'react';

import CartStore from './cart-store';
import CartAction from './cart-action';

/**
 * ボタンのView
 */
export default class CartButton extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            contains: CartStore.contains(this.props.item)
        };
        
    }

    _onAction() {
        const {contains} = this.state;
        const {item} = this.props;
        if(contains){
            CartAction.remove(item);
        } else {
            CartAction.add(item);
        }
        this.setState({contains: contains ? false : true});
    }

    render() {
        const {contains} = this.state;
        return (
            <div className="ui vertical animated button" ref="button" onClick={this._onAction.bind(this)}>
                <div className="hidden content">
                    {contains ? 'Remove' : 'Add'}
                </div>
                <div className="visible content compact">
                    <i className={contains ? "trash outline icon" : "add to cart icon"}></i>
                </div>
            </div>
        )
    }
}

ViewでjQueryCSSフレームワークを使う

カートのViewではjQuerySemantic UIを利用しています。
jQueryは嫌われているけど、やっぱり便利ですし、デザインセンスが皆無なのでCSSフレームワークに頼りたい。

ES6でjQuery$として扱いたい場合は、

import $ from 'jquery';

cssを扱いたい場合も他のjavascriptと同じようにimport文が使えます。便利。

import 'semantic-ui-modal/modal.css'

この modal のようにCSSフレームワークjQueryの機能をオーバーライドしている場合は、

$.fn.modal = require('semantic-ui-modal');

このように書くことで実現できました。

Webpackによるビルド

ES6はブラウザによって対応状況が異なるので、WebpackBabelを指定してES5形式に変換(ビルド)します。
webpack.conf.js

var webpack = require('webpack');
var path = require('path');

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: {
        bundle: './path/to/src/entry.js'
    },
    output: {
        path: path.join(__dirname, 'path/to/dist'),
        filename: 'bundle.js'
    },
    plugins: [
        new ExtractTextPlugin('bundle.css')
    ],
    module : {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: 'babel'
            },
            {
                test: /\.css(\?.+)?$/,
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            },
            {
                test : /\.(svg|woff2?|eot|ttf|png|gif)$/,
                loader: 'url'
            }
        ]
    }
};

ローダー

ファイルに対してどのようにコンパイルするかを指定します。

loaders: [
    {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel'
    },
    {
        test: /\.css(\?.+)?$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
    },
    {
        test : /\.(svg|woff2?|eot|ttf|png|gif)$/,
        loader: 'url'
    }
]

testで指定した正規表現にマッチするファイル対して、loaderを指定します。
js(x)ファイルはbabel(ES6をES5に変換)
cssファイルはExtractTextPlugin.extract('style-loader', 'css-loader')(実ファイルも作るプラグイン
フォントや画像などはurlbase64エンコード

babelはオプションを.babelrcで指定することができます。
queryとして記述できるのですが、テストランナーでも同じオプションを利用したいのでファイルとして置いておくと便利です。

.babelrc

{
  "presets": ["es2015", "react"]
}

CSSのファイル出力

CSSはテンプレートエンジンでも利用したいので、ファイルとして出力します。
ExtractTextPluginを利用します。

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new ExtractTextPlugin('bundle.css')
    ],
    module : {
        loaders: [
            ...
            {
                test: /\.css(\?.+)?$/,
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            },
        ]
    }
}

pluginsで指定したファイル名bundle.cssに出力されます。

出力先

output

output: {
    path: path.join(__dirname, 'path/to/dist'),
    filename: 'bundle.js'
},

ビルドの起点

entry

entry: {
    bundle: './path/to/src/entry.js'
}

起点となるスクリプトを指定します。
このスクリプトを起点として依存されていないファイルはビルドされないので注意してください。

entry.js

import 'semantic-ui-css/semantic.css'

import React from "react";
import ReactDOM from "react-dom";

import $ from "jquery";

import Cart from "./components/cart/cart-view";
import Button from "./components/cart/cart-button-view";

$(".ui.cart").each(function(){
    ReactDOM.render(
        <Cart />,
        $(this).get(0)
    );
});
$(".ui.cart-button").each(function(){
    var item = {
        id : $(this).data('id'),
        name :$(this).data('name')
    };
    ReactDOM.render(
        <Button item={item} />,
        $(this).get(0)
    );
});

テンプレートエンジンで利用するCSSはここで指定します。
また、自分はCSSフレームワークのようにセレクタコンポーネントレンダリングしたいので、

$(".ui.cart").each(function(){
    ReactDOM.render(
        <Cart />,
        $(this).get(0)
    );
});

て書いてます。これでhtmlでは

<div class="ui cart" ></div>

レンダリングしてくれます。

ビルドの実行

ビルドの実行はコマンドラインから

node_components\.bin\webpack

でビルドを実行します。
webpackはpackage.jsonで依存関係に含まれているので、npm installでインストールできます。

テスト

テストを書きましょう。テストは心を豊かにします。
ES6のテストができて、カバレッジ算出を目指しました。

スティングフレームワーク

Mocha
情報量が多かったので選びました。
describeitでテストケースを区切っていきます。
beforeEach()で各テストケースの前に実行する処理を記述できます。

beforeEach(()=>{
    ...
});

Assertionライブラリ

Chai
これも情報量が多かったので選びました。
色々用意されているみたい。

配列の要素数

expect(cart.state.items).to.have.length(1);

Errorをthrowしているかどうか。

expect(()=>CartAction.add({
        id: '1',
        name: '1'
    })).to.throw(Error);

作成したテスト

import React from "react";
import ReactTestUtils from "react-addons-test-utils";
import {expect} from "chai";

import Cart from "../src/components/cart/cart-view";
import CartButton from "../src/components/cart/cart-button-view";
import CartStore from "../src/components/cart/cart-store";
import CartAction from "../src/components/cart/cart-action";

/**
 * カートコンポーネントのテスト
 */
describe('Cart', ()=>{

    beforeEach(()=>{
        CartStore._clear();
    });

    it('カートの中身は初期値は0件である', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);

        expect(component.state.items).to.have.length(0);
    });

    it('カートに追加すると1件になる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id  : '1',
            name: '1'
        });

        expect(component.state.items).to.have.length(1);
    });

    it('カートに3件追加し、1件削除すると2件になる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id  : '1',
            name: '1'
        });
        CartAction.add({
            id  : '2',
            name: '2'
        });
        CartAction.add({
            id  : '3',
            name: '3'
        });
        CartAction.remove({
            id  : '2',
            name: '2'
        });
        expect(component.state.items).to.have.length(2);
    });

    it('既に含まれているIDを追加しようとするとエラーになって追加されない', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id: '1',
            name: '1'
        });
        expect(component.state.items).to.have.length(1);

        expect(()=>CartAction.add({
            id: '1',
            name: '1'
        })).to.throw(Error);

        expect(component.state.items).to.have.length(1);
    });

    it('含まれていないIDを削除しようとするとエラーになる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id: '1',
            name: '1'
        });
        expect(component.state.items).to.have.length(1);

        expect(()=>CartAction.remove({
            id: '2',
            name: '2'
        })).to.throw(Error);

        expect(component.state.items).to.have.length(1);
    });

    it('リクエストボタンをクリックしたらカートに追加される', ()=>{
        let cart = ReactTestUtils.renderIntoDocument(<Cart />);
        let cartButton = ReactTestUtils.renderIntoDocument(
            <CartButton item={{id : '1', name : '1'}} />
        );
        ReactTestUtils.Simulate.click(cartButton.refs['button']);

        expect(cart.state.items).to.have.length(1);
    });

    it('既に含まれている商品のリクエストボタンをクリックしたらカートから削除される', ()=>{
        var item = {id : '1', name : '1'};
        CartAction.add(item);

        let cart = ReactTestUtils.renderIntoDocument(<Cart />);
        expect(cart.state.items).to.have.length(1);

        let cartButton = ReactTestUtils.renderIntoDocument(
            <CartButton item={item} />
        );
        ReactTestUtils.Simulate.click(cartButton.refs['button']);

        expect(cart.state.items).to.have.length(0);
    });
});


テストランナー

ES6のテストを実行するにはやはりBabelでビルドする必要があります。
ビルドしてテストを実行するためにテストランナーであるKarmaを用いました。

Karmaはkarma.conf.jsで設定します。

ビルド前のソースコードカバレッジ計算

カバレッジ計算ではistanbu-instrumenter-loaderlが使われているようです。
この場合だとwebpackでビルドしたコードに対してカバレッジを計算してしまうので、isparta-loaderを使うといいようです。

出来上がったkarma.conf.js

karma.conf.js

var path = require('path');

module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['phantomjs-shim','mocha'],
        files: [
            'path/to/test/**/*.spec.js'
        ],
        exclude: [
        ],
        plugins: [
            'karma-phantomjs-launcher',
            'karma-phantomjs-shim',
            'karma-mocha',
            'karma-sourcemap-loader',
            'karma-webpack',
            'karma-coverage',
            'karma-mocha-reporter'
        ],
        preprocessors: {
            'path/to/test/**/*.spec.js': ['webpack', "sourcemap"]
        },
        webpack: {
            devtools: 'inline-source-map',
            isparta: {
                embedSource: true,
                noAutoWrap: true
            },
            module: {
                loaders: [
                    {
                        test: /\.jsx?$/,
                        exclude: /node_modules/,
                        loader: 'babel'
                    },
                    {
                        test: /\.css(\?.+)?$/,
                        loader: 'style!css'
                    }
                ],
                preLoaders: [
                    {
                        test: /\.jsx?$/,
                        exclude: [
                            path.resolve('path/to/test/'),
                            path.resolve('node_modules/')
                        ],
                        loader: 'isparta'
                    }
                ]
            }
        },
        webpackMiddleware: {
            noInfo: true
        },
        port: 9876,
        browsers: ['PhantomJS'],
        reporters: ['mocha', 'coverage'],
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: false,
        singleRun: true,
        concurrency: Infinity,
        coverageReporter: {
            dir: 'path/to/test/report/',
            reporters: [
                { type: 'text' },
                { type: 'html', subdir: '.' }
            ]
        }
    })
};

ここで使うbabelの設定は上述の.babelrcで行われるので、ちょっとスッキリしています。

テストの実行は

karma start

f:id:duck8823:20160701005751p:plain
カバレッジを計算してくれました。

レポートでhtmlを指定した場合、どの部分をカバーできていないか教えてくれます。
f:id:duck8823:20160701012639p:plain

また、package.json

"scripts": {
    "test": "karma start"
  }

と設定しているので

npm test

でも実行できます。

フロントエンド開発をちゃんとやろうと思って構築を始めたのですが、こんなにも開発環境は充実していたんですね。
jQueryで頑張っていたのが嘘のよう。

今回作成したコンポーネントGitHubに置いておきました。 github.com

フロントエンドは変化が激しいといわれてますが、それぞれ目的があって見つけてきたものなので、目的が変わらなければこのままでもいいかなと思ったりします。

perlでSlackの雑談Bot作ってみた

perl Slack

前回の記事perlでSlackのbotを作ることができるモジュールを紹介しました。

今回はこれを使って雑談Botを作ります。
雑談用のAPIdocomoさんが提供している雑談対話APIを利用しました。

雑談対話APIを利用するにはここから開発者登録する必要があります。法人情報(会社の情報)は登録しなくても大丈夫でした。
登録時に「雑談対話」を選択します。申請後、API keyが取得できます。

早速、APIを利用した雑談Botを紹介します。

作成したスクリプト全体

#!/usr/bin/perl
use strict;
use warnings FATAL => 'all';

use Slack::RTM::Bot;

use Encode;
use JSON;

use HTTP::Request;
use LWP::UserAgent;

our $ua = LWP::UserAgent->new();

our $api_key = '<docomo Developer supportで取得したAPI key>';
our ($context, $mode) = ('','');

my $bot = Slack::RTM::Bot->new(
    token => '<SlackのAPI Token>'
);

$bot->add_action(
    {
        channel => 'general',
        type => 'message',
        user => '^(?!perl_bot).*$',
        text => '.*'
    }, sub {
        my ($response) = @_;
        my $request = HTTP::Request->new('POST', 'https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=' . $api_key);
        $request->content(JSON::to_json( {
                        utt     => Encode::encode_utf8($response->{text}),
                        mode    => "$mode",
                        context => "$context"
                }));
        my $res = $ua->request($request);
        my $content = JSON::from_json($res->content);
        $context = $content->{context};
        $mode    = $content->{mode};

        $bot->say(
            channel => 'general',
            text    => $content->{utt}
        );
    }
);

$bot->start_RTM;

sleep 300;

$bot->stop_RTM;

コードの一部ずつ

Slack::RTM::Botadd_actionでgeneralに何か書き込まれたときに反応するようにしています。  

{
    channel => 'general',
    type => 'message',
    user => '^(?!perl_bot).*$',
    text => '.*'
}

コールバックではSlackから取得したメッセージをdocomoの雑談会話APIに投げています。
このとき、modecontextを指定することで、会話のラリーが続くようになります。

my ($response) = @_;
my $request = HTTP::Request->new('POST', 'https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=' . $api_key);
$request->content(JSON::to_json( {
                utt     => Encode::encode_utf8($response->{text}),
                mode    => "$mode",
                context => "$context"
        }));
my $res = $ua->request($request);

取得したレスポンスはJSON形式なのでハッシュリファレンスにし、modecontextを取得しています。

my $content = JSON::from_json($res->content);
$context = $content->{context};
$mode    = $content->{mode};

あとは雑談会話APIから取得したメッセージをSlackに流すだけ。Slack::RTM::Botsayが使えます。

$bot->say(
    channel => 'general',
    text    => $content->{utt}
);

コールバックの設定の後は、RTMの開始と終了。ここでは5分間のみにしています。

$bot->start_RTM;

sleep 300;

$bot->stop_RTM;

さて、早速起動。
f:id:duck8823:20160625125543g:plain

とりあえず動いているっぽい。

perlで簡単にSlack Botを作るモジュールを公開した

Slack perl

CPAN authorになりました。

Slack::RTM::Bot - search.cpan.org

GitHubでいくつかperlのモジュールを書いたりはしていたのですが、せっかくなのでCPANに登録することにしました。

SlackのReal Time Messaging (RTM) APIを使ったモジュールです。
公開したモジュールを使うとperlで簡単にSlackのbotが作れます。
CPANに登録したのでインストールも簡単です。

cpanm Slack::RTM::Bot

例えばこのモジュールを利用してチャット内容のログをとる場合。

use Data::Dumper;
use Slack::RTM::Bot;

my $bot = Slack::RTM::Bot->new(
    token => 'Slackで取得したAPIトークン'
);

$bot->add_action(
    { 
        type : 'message',
        text : '.*'
    },
    sub {
        my ($response) = @_;
        print Dumper $response;
    }
);

$bot->start_RTM;

sleep 60;

$bot->stop_RTM;

add_actionメソッドで条件とコールバックを登録します。
条件には正規表現が使えます。Slackからのレスポンス内に条件に合った項目があればコールバックのメソッドを実行します。第一引数を{}にすればすべてのレスポンスに対してコールバックを実施します。
userchannel はIDではなく表示されてる名前で扱えるようにしています。

コールバックの引数にはSlackからのレスポンスが入ってます。
条件やレスポンスはSlack RTM APIで確認できます。

Slackへの通知にはsayメソッドを利用します。

use Slack::RTM::Bot;

my $bot = Slack::RTM::Bot->new(
    token => 'Slackで取得したAPIトークン'
);

$bot->start_RTM;

$bot->say(
    channel => 'general',
    text => 'Hello, World.'
);

$bot->stop_RTM;

ユーザあてにダイレクトメッセージを送りたい場合はchannel@付きのユーザ名で送ることができるようにしました。

$bot->say(
    channel => '@duck8823',
    text => 'Hello, World.'
);

にしても、あっという間にCPANに公開できてしまうんですね。
GitHubにおいてあるいくつかのモジュールも整備してCPANに上げたいなと思います。

Slack::RTM::Botソースコードここにあります。

Slackbotのメッセージをオシャレにしたい

Slack

以前にbotkitを使ってslackbotを作りました。

この通知をオシャレにしたい。
GitLabとかの通知みたいな感じ。
f:id:duck8823:20160530212411p:plain

テスト結果で色とか分けられててわかりやすいですよね。
で、どういう風にやるのかなとかと思ってたんですが、メッセージをJSON形式の添付ファイルにしてやればよいようです。

bot.say({
    channel: 'general',
    text: '進捗どうですか',
    username: 'name',
    icon_url: '',
    attachments:
    [{
        title : "添付のタイトル",
        color : "danger",
        text  : "添付のテキスト"
    }]
});

こんな感じにしてやれば、
f:id:duck8823:20160530213327p:plain

オシャレになりました。
Webページの死活チェックなんかして、ステータスによって色を変えるのもよさそうですね。

SonarQubeでテストカバレッジを表示する

Java テスト

うちのチーム自動テストを導入しようとしてます。
とはいえ、ゼロから始めるには指標がないとしんどいですよね。

JJUG CCCで知見を得たSonarQubeを試すことにしました。

SonarQubeの起動

Docker使ってぱぱっと試します。

docker run -d --name postgres-sonar -e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=sonar postgres
docker run -d --name sonarqube --link postgres-sonar:postgres -p 9000:9000 -p 9092:9092 -e SONARQUBE_JDBC_USERNAME=sonar -e SONARQUBE_JDBC_PASSWORD=sonar -e SONARQUBE_JDBC_URL=jdbc:postgresql://postgres/sonar  sonarqube

PostgresqlもDockerで起動させて、--linkオプションでつなげています。

ブラウザからhttp://<起動させたホスト>:9000で開くことができます。
f:id:duck8823:20160526223352p:plain

JaCoCoの導入

これでプロジェクト上でmavenから起動させればコードの静的解析をしてくれますが、これだけだとカバレッジを表示してくれませんでした。
カバレッジを表示するには、JaCoCoなどであらかじめ計算しておく必要があります。

JaCoCoの導入も簡単。
プロジェクトのpom.xmlに以下を追記します。

<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.7.6.201602180812</version>
    </plugin>
  </plugins>
</build>

mavenで実行

あとはmavenコマンドを実行するだけです。

mvn clean jacoco:prepare-agent test jacoco:report sonar:sonar -Dsonar.host.url=http://<起動させたホスト>:9000 

今回は、個人的に作ってGitHubに上げてるプロジェクトを解析してみました。
コマンドが終了したら再度http://<起動させたホスト>:9000を開きましょう。

f:id:duck8823:20160526224737p:plain

プロジェクト一覧(今回は一つだけ)が表示され、可視化した図が表示されます。
詳しく見るにはプロジェクトをクリックします。
  f:id:duck8823:20160526224905p:plain

解析結果が表示されます。赤色のとこは直した方がよくて、緑はオーケーです。多分。
Dashboardsをみるともうちょっと詳しく数字を出してくれます。

f:id:duck8823:20160526225103p:plain

カバレッジ58.4%でした。網羅できてないですね。
面白いのがTechnical Debt、技術的負債。一日分の負債を抱えてます。

詳しく見ていくと、なんとレビューしてくれてます。
f:id:duck8823:20160526225337p:plain

SonarQubeすごいです。感動しました。 これを指標にテストコードを書いていきたいと思います。

今回2回目の参加ですが、JJUG CCCでは毎回有益な知見をいただけて感謝しかないです。
アウトプット本当に素晴らしい。いつか勉強会とか登壇してみたいな、とか。