Reactでシングルクリックとダブルクリックを区別して使いたい

先に今回の話のソースコードです。 github.com

Reactでは シングルクリックイベントとダブルクリックイベントが用意されています。

シングルクリック

<a href="javascript:void(0)"
   onClick={(e) => this.handleOnSingleClick(e)}>
   Single Click
</a>

onClickイベントが用意されています。

ダブルクリック

<a href="javascript:void(0)"
   onDoubleClick={(e) => this.handleOnDoubleClick(e)}>
   Double Click
</a>

onDoubleClickイベントが用意されています。
※ mobile safari だと動きませんでした。

それぞれを別々に使う分には期待した通りに動作します。

問題はSingleClickとDoubleClickを両方設定したときです。

<a href="javascript:void(0)"
   onClick={(e) => this.handleOnSingleClick(e)}
   onDoubleClick={(e) => this.handleOnDoubleClick(e)}>
   Single Click and Double Click
</a>

こんな形で実装して、ダブルクリックしてみると、onClickイベントが2回とonDoubleClickイベントが1回発火します。
シングルクリックのときは onClickイベントだけ、ダブルクリックの時はonDoubleClickイベントだけと区別して使いたいことがあります。

そういったときどうするのかという話です。

ソースコードはTypescriptで書いてます。

import * as React from 'react';

interface IAppProps {
}
interface IAppState {
}

export class App extends React.Component<IAppProps, IAppState> {

  private clickCount: number;

  constructor(props: IAppProps) {
    super(props);

    this.clickCount = 0;

    this.state= {};
  }

  /**
   * Single or Double Click
   */
  handleOnSingleOrDoubleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    this.clickCount++;

    if (this.clickCount < 2) {
      setTimeout(() => {
        if (this.clickCount > 1) {
          console.log('Double Click');
        } else {
          console.log('Single Click');
        }
        this.clickCount = 0;
      }, 200);
    }
  }

  render() {
    return (
      <div>
        <a href="javascript:void(0)"
          onClick={(e) => this.handleOnSingleOrDoubleClick(e)}>
          Single Click or Double Click
        </a>
      </div>
    );
  }
}

の様に書くとシングルクリックとダブルクリックを区別することができます。

キモになるのはこの部分で、onClickイベントのみを使います。

  /**
   * Single or Double Click
   */
  handleOnSingleOrDoubleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    this.clickCount++;

    if (this.clickCount < 2) {
      setTimeout(() => {
        if (this.clickCount > 1) {
          console.log('Double Click');
        } else {
          console.log('Single Click');
        }
        this.clickCount = 0;
      }, 200);
    }
  }

クリックイベント時に、クリック数をインクリメントして、setTimeoutで後からクリック後の動作を実行してあげることでシングルクリックとダブルクリックを別々に認識させることができます。

クリックして動作が見れるページを作ってみたので、興味がある人はクリックしてみて下さい。 React doubleclick sample

AWSで最初からCloudFormationでインフラを管理する方法を考えてみる

AWSで最初からCloudFormationを使ってインフラを管理していく方法について考えてみました。

個人でサービスを作ろうと思い、あまりお金もかけたくなかったのでサービス選定は

サービス名
クラウドサービス AWS
ソースコード管理 Bitbucket
CI/CD CircleCI

といった感じです。

参考にしたサイトです。

dev.classmethod.jp github.com

CloudFormation と CI の組み合わせをする上で必要な物

今回だとCircleCIからAWSへアクセスする為のIAMユーザーが必要になります。
また、CloudFormationのテンプレートを分割して管理する場合は、S3 Bucketも必要になります。

導入手順

先に今回の内容をコード化したものがコチラです。 github.com

導入手順としては

  1. 全サービスにアクセスできるIAMユーザーを作る
  2. 自分が作成したいサービス向けのIAMユーザーを作る

という流れにしようと思いました。

悩ましいのが、どうしても最初はWebのAWSコンソールからIAMユーザーとS3 bucketの作成が必要になることでした。

いろいろ考えて納得できる方法としては

  1. 手作業でAWSコンソールのCloudFormationから setup/cfn.template.yml を指定して awscli 用のIAMとCloudFormationのテンプレートを一時的に置くS3 bucket を作成します。
  2. 作成されたIAMユーザーのkey, secretを作成してAWS CLIにアカウントをセットアップします。
  3. AWS CLI でcloudformationのスタックを作成して新規作成するサービス向けのインフラを構築するための準備をします。

という手順です。

  1. の作成に使うテンプレートだけは別管理にした方がわかりやすいかなと思いました。

CloudFormationのスタックが2つに分かれてしまいますが、管理しやすくて良いかなと思いました。

  • スタック1(setupディレクトリ配下)
    • awscli用のIAMユーザーとテンプレートなどを保存するためのs3 bucketを管理するテンプレート
  • スタック2(srcディレクトリ配下)
    • 各サービスを管理するためのユーザーを管理するテンプレート

の形にCloudFormationのスタックを分けることでができるので良いかなと思いました。

理想としては1つのスタックで完結させたいです。
ディレクトリツリーで見るとこんな感じです。
要は全てのIAMを src/iam.template.yml で管理したいです。

├── src
│   ├── bucket.template.yml
│   ├── cfn.template.yml
│   └── iam.template.yml

ですが、 最初はWebのAWSコンソールからIAMユーザーとS3 bucketの作成 をしないといけないので

├── setup
│   └── cfn.template.yml
├── src
│   ├── bucket.template.yml
│   ├── cfn.template.yml
│   └── iam.template.yml

と分け

├── setup
│   └── cfn.template.yml

ここで、最初のユーザーを管理します。

AWS CLI からのスタック作成について

デプロイするテンプレートは管理しやすくするために分割してます。

├── src
│   ├── cfn.template.yml
│   └── iam.template.yml

実際のところは下の様に実行すればCloudFormationで実行可能なテンプレートを作成することができる。

$ aws cloudformation package \
    --template-file src/cfn.template.yml \
    --s3-bucket artifact \
    --output-template-file .cfn/packaged.yml \
    --s3-prefix template

このコマンドでしていることは、 --s3-bucket で指定したバケット--s3-prefix で指定したディレクトリ配下に src/cfn.template.yml 以外のテンプレートファイルをアップロードします。 --output-template-file .cfn/packaged.yml で指定している部分に出力されるファイルは、 src/cfn.template.yml の参照している他テンプレートファイルを指定するパスが追加されています。

TemplateURL: /path/to/bucket/fc2e4965584e9309b667bf9a6c1eb6f4.template

の様な物が追加されています。

.cfn/packaged.yml が作成されたら、create-stackupdate-stack を使って更新していきます。

ここまでやるとiam.template.ymlで作成したIAMユーザーをCircleCIに設定することで自動更新ができるようになります。

あとは、新しく作りたいサービスがでたらそのサービス向けに権限を絞ったIAMユーザーを作ることができます。 そうすることで、CIなどを使った際に誤操作で別サービスのデータを消してしまうなどが防げるのではないかと思いました。

課題

今回の構成で自動デプロイできるようにはなってますが、課題もあります。

  • update-stack 実行後のCloudFormationの状態を監視していないのでCircleCIでは Success になるが CloudFormation では失敗してロールバックする可能性がある
  • 連続でデプロイしたとき、現在のスタックの状態を確認していないため、スタックが更新中の場合不具合が起きる可能性がある

備考

テンプレートの検証に cfn-lint を使っていて気づいたけど、 Type: AWS::CloudFormation::Stack を使ってテンプレートを分けた場合は、子テンプレートまでチェックしてくれるわけではないみたい。 あくまで引数に渡したテンプレートファイル単体のテストをしてくれるツールみたい

circleci2.0でawscliを使いたいのでDockerイメージを作ってみた

circleci2.0で awscli を使いたいと思ったとき、毎回 .circleci/config.ymlawscli のインストールコマンドを書くのも面倒に感じます。

dockerhubを使うと特定のイメージの更新をトリガーにして、自分のイメージも更新することができます。

f:id:naught00:20180220004710p:plain

Dockerイメージはdockerhubで公開してます。
https://hub.docker.com/r/naughtldy/circleci-node-awscli/

ソースコードgithubで公開してます。
github.com

phpでlocalstackのs3にファイルをアップロード

AWS周りのものをローカルで開発する場合 localstack を使うと便利だというのを聞いたので試してみました。

github.com

localstackは、GUIはあまり提供されていない用で、主にawscliコマンドを使います。
s3にファイルをアップロードしたい場合は、 --endpoint-url=http://localhost:4572 を付けてあげてアップロード先を変えることができます。

試しに awscli で test というバケットを作ってファイルをアップロードしてみます。

$ aws --endpoint-url=http://localhost:4572 s3 mb s3://test
make_bucket: test
$ aws --endpoint-url=http://localhost:4572 s3 ls s3://
2006-02-04 01:45:09 test
$ touch hoge.txt
$ aws --endpoint-url=http://localhost:4572 s3 cp hoge.txt s3://test/hoge.txt
upload: ./hoge.txt to s3://test/hoge.txt
$ aws --endpoint-url=http://localhost:4572 s3 ls s3://test/
2017-12-08 20:00:00          0 hoge.txt

localstack上のs3にファイルがアップロードされていることが確認できます。

phpを使ってlocalstackのs3にファイルをアップロードしてみます。
awscliのときと同様に、endpointを localstack のs3用エンドポイントを指定してみます。
aws-sdk-php は composer を使って入れてます。

put.php

<?php
require_once 'vendor/autoload.php';
use Aws\S3\S3Client as S3Client;

$s3 = S3Client::factory(array(
    'endpoint' => 'http://localhost:4572',
    'credentials' => array(
        'key' => '',
        'secret' => '',
    ),
    'version' => 'latest'
));
$pamans = array(
    'Bucket' => 'test',
    'Key' => 'hello.txt',
    'Body' => 'Hello World',
    'CacheControl' => 'max-age=1',
);
$s3->putObject($pamans);

実行してみるとエラーが出ます。

Could not resolve host: test.localhost [url] http://test.localhost:4572/hello.txt

hostsが解決できないようなので、/etc/hosts

127.0.0.1 test.localhost

を追記します。

再度実行してみるとエラーが出ずに終了し、ファイルが作られていることが確認できました。

$ php put.php
$ aws --endpoint-url=http://localhost:4572 s3 ls s3://test/
2017-12-08 21:00:00         11 hello.txt

javascriptやgoを使う方法を参考にしていると

  • s3ForcePathStyle
  • endpoint

のオプションを設定してあげればいいと出るのですが、 aws-sdk-php には s3ForcePathStyle というオプションが無いです。
s3ForcePathStyle というオプションを設定しないでアップロードしようとすると、先ほどのエラーにも出てましたが、s3バケット名.localhost:4572 となり、hostsが解決できなくなってしまいます。

php と localstack を使って開発したい場合は hosts を書いてあげて localstack が提供する各URLを指定してあげないといけなさそうです。

追記

コメントで指摘頂いた通り、PHPでは use_path_style_endpoint があり、そちらのオプションを有効にすることで hosts を設定すること無くテストすることができました。
id:imunew さん、ご指摘ありがとうございます。

put.php を下記の様に 'use_path_style_endpoint' => true とオプションを追加することでhostsの設定をすることなくテストが可能になりました。

<?php
require_once 'vendor/autoload.php';
use Aws\S3\S3Client as S3Client;

$s3 = S3Client::factory(array(
    'endpoint' => 'http://localhost:4572',
    'credentials' => array(
        'key' => '',
        'secret' => '',
    ),
    'version' => 'latest',
    'use_path_style_endpoint' => true
));
$pamans = array(
    'Bucket' => 'test',
    'Key' => 'hello.txt',
    'Body' => 'Hello World',
    'CacheControl' => 'max-age=1',
);
$s3->putObject($pamans);

参考

Testable Lambda: Working Effectively with Legacy Lambda // Speaker Deck

Testable Lambda|AWS Summit Tokyo 2017 - YouTube

LaravelでS3への画像アップロードをLocalStackを使って開発 - Qiita

S3互換のストレージソフトMinioを試してみる | モテモテテクノロジーブログ

docker exec で cd する方法

dockerを使っていて、普段は↓のコマンドでコンテナ内に入って作業してます。

$ docker exec -it {container-name} /bin/bash

最近はコンテナ内でお決まりの作業(特定のディレクトリに移動してコマンド実行など)をしたいときに

$ docker exec -i {container-name} "cd /path/to && ls"

の様な形で実行できると思っていたのですが、実際は違っていて上手くいきませんでした。
↓の様なエラーが出ました。

$ docker exec -i {container-name} "cd /var/log && ls"
oci runtime error: exec failed: container_linux.go:265: starting container process caused "exec: \"cd /var/log && ls\": stat cd /var/log && ls: no such file or directory"

cd する方法

$ docker exec -i {container-name} /bin/bash -c "cd /path/to && ls"

という風に、 /bin/bash -c としてその後に実行したいコマンドを書いてあげると動く。

参考

docker-exec failed: "cd": executable file not found in $PATH - Stack Overflow

ISUCON7予選に参加しました

id:Maco_Tasu に、ISUCONに一緒に出ませんかと誘ってもらえたのでいい機会だしと思い参加しました。

LabALICEというチームで、メンバーはid:Maco_Tasu, id:karia です。

macotasu.hatenablog.jp

karia.hatenablog.jp

全員のスコアと順位を出して頂けて運営の方には感謝です。

isucon.net

最終スコアは 79,590 で、43位でした。

当日私は、画像周りの修正をしたのでそのあたりについて記録を残しておこうと思います。

事前準備

チームで3回集まって過去問を解きました。
解いた過去問は

  • isucon6予選
  • isucon4予選
  • pixiv社の社内向けisucon

です。

過去問を解くことで全体的な流れを掴むことができました。
そこで一番感じたのが、競技中にNginxの設定などを動作確認しながら作成してると時間がもったいないということでした。
NginxやMySQLなど、あらかじめ使うであろう物の設定ファイルはひな形を作っておこうとチームで話あって事前準備をしていました。

当日

最初の役割分担は id:Maco_Tasugithubへのソースコード登録、id:karia がインフラの整備となっていました。
私は、DBテーブルの確認をしてからアプリを触って動作確認をしていました。
今回のお題はチャットアプリで、画像の数が多いためNginxから返せれば速くなるのかなと思いながら触ってました。

一通りアプリを触ったので、ソースコードの確認に入りました。
ソースコードを見ると、pixiv社の社内向けisuconと同様に画像をMySQLに入れている部分が目に付きました。

予選の環境はDBサーバー1台とAPサーバー2台の構成でした。
画像はMySQLにいれるのを止めてローカルに保存してNginxから返す様にするというのはすぐに思いつきました。
問題はどのサーバーで保存するかというところで少し考えました。

  1. APサーバーからDBサーバーのMySQLに画像を保存するのを止める
  2. DBサーバーのローカルに画像を保存する

と考えたときに、DBサーバーのローカルに保存したいならDBサーバーをフロントにして、画像の保存はDBサーバーがやってそれ以外の処理は後ろのAPサーバーに流してあげれば良いんじゃないかと考えてインフラの構成を変更しました。

インフラの変更は id:karia にお願いをして、アプリの改修をしました。
改修内容は下の2つです。

  • 画像の登録/更新があったらMySQLには保存せずにローカルに保存する
  • 初期にDBに登録されている画像をローカルに書き出しておく

この後ベンチを回したりして動作を確認していたのですが、どうもDBサーバーの負荷が高くフロントと兼任させるのは良くないという事がわかってきました。
ここで、構成を変更することにしました。

次に試した方法は AP1台をフロントにして、フロント・AP・DBがそれぞれ1台づつの構成です。

  • フロントサーバーは、追加された画像のローカル保存
  • APは、画像処理以外の実行

に担当を分けてみました。

この構成だとDBに余裕ができたので、スコアを上げることができました。
その後は、GET /message GET /history/:channel_id のクエリを直したりということをしていました。

結果は予選敗退でしたが、気づきがとても多く参加して良かったと思います。
運営の皆様ありがとうございました。
id:Maco_Tasu, id:karia チームを組んでくれてありがとうございました。

学び/勘違いしていたこと

  • DBの接続設定

リクエストごとにコネクションが作られる

def db
  return @db_client if defined?(@db_client)

  @db_client = Mysql2::Client.new(

コネクションの使い回しができる

def db
  return Thread.current[:isuconp_db] if Thread.current[:isuconp_db]
  client = Mysql2::Client.new(
  • MySQLのコネクションがちゃんと閉じられていることを確認するのは大切

javascriptのスプレッド演算子(...)

javascriptで関数を呼び出すところで、引数に ... と書いているソースコードを見かけるがこれが何か調べてみた。

ES2015から追加された、スプレッド演算子というらしい。

参考

「…」←これ、ただの省略記号かと思ってました。(Spread operatorのお話)|もっこりJavaScript|ANALOGIC(アナロジック)

スプレッド演算子 - JavaScript | MDN