シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

Irisという超絶キュートなキーボードを作成した話

こんにちは、シンクロ・フードの大久保です。 今回はあまり社内の業務と関係がないのですが、Irisというキーボードを作成したお話しをしようかと思います。

f:id:synchro-food:20180213200721j:plain
これがIrisです

TL;DR

  • IrisというキーボードはコンパクトなErgoDoxで、作って良かった!
  • 組み立て式のキーボード制作は思っているよりも簡単
  • はんだ付けへの抵抗感を無くすのにも良さそう

はじめに

僕はErgoDoxという、左右分割型で親指キーがあるキーボードを使っていて、概ね満足はしていたのですが、ErgoDoxは大きく、使わないキーが沢山あるので、もっとスリムにしたいなあ…と思っていました。 いくつか乗り換え先キーボードの選択肢もあったのですが、色々調べた結果、Irisというキーボードを自作しようという結論に達しました。

f:id:synchro-food:20180213200853j:plain
これがErgoDox。親指キーが特徴です。ErgoDoxEZを使っている人は結構多いのではないでしょうか。

Irisとは

Irisとは、RealForceHHKBのような完成品のキーボードではなく、基礎となる基盤のキットやキースイッチ、キーキャップを自分で購入し、組み立てるタイプのキーボードです。同様のキーボードとしては、PlanckLetsSplit(通称レツプリ)などが有名です。

Irisの特徴としては、以下の点が挙げられます。

  • キー配列がErgoDoxとほぼ同じだが、無駄なキーが削れられている
  • qmk_firmwareが使える
  • コンパクト

ただし、組み立てにははんだ付けなどが必要で、ここは少し心理的な抵抗感があるかと思います。僕も電子工作の経験は少ないので、かなり迷ったのですが、同僚のエンジニアも一緒に作る、ということで、2人で作成していくことにしました。 結論から言うと、思っていたよりも簡単に作ることができました。ただし、Irisの場合は、レツプリのように作成ログの日本語情報があまりWeb上にないので(と思っていたら、2018年になって記事増えてますね…)、以下に僕が作った流れを記載しておきます。

部品集め

まずは組み立てるために必要な部品を購入します。 必要なものは大体4つ。基盤関連、キースイッチ、キーキャップ、工具類です。 2万円くらいあれば、すべて揃うと思います。

基盤関連

Irisはkeebioというサイトで購入可能です。僕はキーボードをLEDで光らせる必要がなかったので、LED関連の機器は購入していません。 必要なものは、PCBの商品詳細にすべて書いてあるのですが、僕が購入したものを以下に記載していきます。

PCB

Iris Keyboard - PCBs for Split Ergonomic Keyboard – Keebio

元となる基盤とダイオード、抵抗、ジャックなどのセットです。

Case

Iris Keyboard - Case/Plates – Keebio

基盤を覆うケースです。midlayerという中間層があるタイプとないタイプがあります。

Pro Micro × 2

Pro Micro - 5V/16MHz - Arduino-compatible ATmega32U4 – Keebio

左右で2つ必要です。

TRRS Cable

TRRS Cable – Keebio

左右のキーボードをつなぐケーブルです。「4極ケーブル」や「TRRS」、という名前で探せば、他のサイトでも購入できます。

MicroUSB Cable

Amazon CAPTCHA

Pro MicroとPCを接続するためのケーブルです。なんでもよいと思います。

キースイッチ

キースイッチは56個必要です。無難にいけば、CherryMX互換のものを買えば良いです。 Cherryの茶軸とか赤軸とか、そういうやつです。 僕はNovelKeyというサイトで購入しました。

https://www.novelkeys.xyz/product/outemu-ice-switches/

僕は上記のOutemuIceSwitchのLight Purple軸を使いました。

市販のキーボードではあまり使えない軸を選べるのは自作キーボードの醍醐味です。ですが、注文してから届くまで時間がかかるので、悩みすぎないようにしましょう。僕は注文してから3週間くらいかかりました…。

キーキャップ

キーキャップも56個必要です。キースイッチをCherryMX互換のものを購入した場合は、CherryMX互換のキーキャップを買えば良いです。

僕はpimpmykeyboardというサイトで購入しました。

Pimpmykeyboard.com

Irisのキーキャップは、すべてが同じ大きさなので、keycapsの1xkeyを56個揃えればよいです。 keysetsで買うと安いですが、ちょうどよい組み合わせがないので、多少は多めに購入する必要があります。 DSAとかSAとかはProfileという、形状の種類なのですが、よくわからない場合は、真っ平らなDSAを買うのが無難だと思います。キーキャップは後でいくらでも変えることができるので、気に入らなければ変更すれば良いと思います。

工具類

これは人それぞれですが、僕が使ったものを書いていきます。

はんだごて

白光 ダイヤル式温度制御はんだこて FX600
https://www.amazon.co.jp/dp/B006MQD7M4/ref=cm_sw_r_cp_ep_dp_4NpDAb0CFJP13

温度調整できるので、これで良いと思います。 他にもはんだごてを置く台なども必要なので、セットみたいなものを買うのが無難かもしれません。

はんだ

これは覚えていないのですが、適当なものを買えば良いと思います…。

練習キット

http://amzn.asia/aRcSdG0

はんだ付けが中学生以来…という方は買って練習すると精神衛生上、良いと思います。僕はRaspberryPiを触っていたときに買ったものが残っていたので、これで軽く練習しました。 実際は、練習せずに一発勝負でも問題はないと思います。

吸い取り線

http://amzn.asia/4KxVzY5

失敗した時用。あると精神衛生上、良いと思います。

他にも言い出せばキリがないのですが、絶縁マットやら固定台とかテスターなど色々あるのですが、必要に応じて購入してください。

あとはひたすら待つ

ここまで完了したら、もう完成したも同然です。そして、注文から到着まで、長いです…。 情報収集しつつ、ワクワクしながら待ちましょう。

組み立て

さて、すべてが揃ったら、いよいよ組み立てです。 一つずつ、作業をご紹介します。

ダイオードをはんだ付けする

f:id:synchro-food:20180124202946j:plain

ひたすらはんだ付けです。スイッチの数だけあるので、全部で56箇所?
もし、はんだ付けを初めてやる、という方であれば、とても良い練習になると思います。

f:id:synchro-food:20180125131741j:plain
両手が付け終わった…。(余計な線はペンチで切る)

Irisだと、ダイオードの向きは黒いほうが下になります。

TRRSジャックとリセットボタン、ProMicro用ピンヘッドをはんだ付けする

f:id:synchro-food:20180212191328j:plain
赤がTRRS,青がProMicro用ピンヘッド,黄がリセットボタン

ひたすらちゅんちゅんやるだけ。抵抗はオプショナルなので、つける必要はないです(僕はつけてしまいましたが…)。

キースイッチをはんだ付けする

f:id:synchro-food:20180126124146j:plain
思いっきり会社のデスクで作業してます(お昼休みに作業してました…)

f:id:synchro-food:20180126130010j:plain

ここもひたすらはんだ付けするだけ。

ProMicroをはんだ付け

ピンヘッドに入れて、はんだ付けしたあと、不要なピンを切っていきます。できる限り薄くしたほうがケースと干渉しなくなります。

f:id:synchro-food:20180126194126j:plain
はんだ付けして、ピンを切った状態。左右でProMicroの向きが違います

これではんだ付け作業は終了。あとはケースの蓋を閉じるだけ。

f:id:synchro-food:20180126222309j:plain

QMKfirmwareをビルドする

ProMicroにfirmwareを書き込みます。 自作キーボードの場合、qmk_firmwareというオープンソースのfirmwareを使うのが一般的で、Irisもそちらを用います。 具体的な作業は、PCでソースをビルドし、コマンドを使ってProMicroに書き込む、という作業を行います。

qmk_firmwareをforkする

以下リポジトリをforkします。
https://github.com/qmk/qmk_firmware

ビルド環境を整える

下記リンクに従って、各種OS毎にビルドする環境を整えてください。 https://docs.qmk.fm/getting_started_build_tools.html

僕はMacでした。Macユーザーの場合はhomebrewでバンバン入れていくだけです。この準備、25分くらいかかります。 なぜかEl Captanだと失敗するときがあったのですが、何度か実行すると、うまくいきました(適当ですいません)。Siera環境だと一発で成功しました。

ビルド実行する

Iris向けのキーマップはすでに準備されているので、まずはREADMEに従ってデフォルトを書き込むのが良いと思います。 https://github.com/qmk/qmk_firmware/tree/master/keyboards/iris

ProMicroにmicroUSBケーブルを接続して、以下コマンドを実行すると、ビルドと書き込みが始まります。

make iris/rev2:default:avrdude

最初だけは、両方のProMicroに書き込む必要があります。 ですので、以下の手順で実行していけば良いです。

  1. 左手のProMicroに書き込み(左手のみ、反応する状態になる)
  2. 右手のProMicroに書き込み(両手が左手のキーマップになる)
  3. 左手のProMicroに書き込み(左手、右手が正しいキーマップになる)

デフォルトのキーマップは、左手側のProMicroにmicroUSBを接続することを前提としているので、上記流れになります。 なお、上記対応をやったあとは、左手だけに書き込むことで、両手のキーマップが反映することになります。 ※このあたりの手順を知らずに対応していたので、両手が左手になったとき、めちゃくちゃ焦りました

完成

f:id:synchro-food:20180127004006j:plain
会社では終わらず、自宅に持ち帰って作業。動いたときはめちゃくちゃ嬉しかったです。

これで完成!

あとはキーマップを自分向けにカスタマイズしていくだけです! 基本的にはdefaultのキーマップをコピーして別名にし、それを変更して、そちらをビルドして書き込む感じです。 例えば、mykeymap、というファイル名でキーマップを作った場合は、以下のコマンドを実行する感じになります。

make iris/rev2:mykeymap:avrdude

尚、ErgoDoxのキーマッピングに慣れている方から見ると、KC_が省略されているキーマッピングになっていて、少し??となると思いますが、sourceを読めば理解できると思います。 一応、僕のキーマップもおいておきます。 https://github.com/shunohkubo/qmk_firmware/blob/develop/keyboards/iris/keymaps/chibikubo/keymap.c

キーマップをあれこれ考えるのも、とても楽しく、沼が待ってます。

参考にしたサイト

非常に詳細なビルドログ
https://imgur.com/a/iQH2W#k3cwV69

作成作業の動画。めっちゃ参考にしました。ProMicroの向きが、ちょっと不思議です。
Iris Split Ergonomic Keyboard Build Log - YouTube

2017の自作キーボードアドベントカレンダーはモチベーションを高める意味でも助けれられました!
https://adventar.org/calendars/2114

終わりに

非常に簡単に作成できますので、興味があれば是非作ってみることをオススメします。
尚、Irisは2018年3月時点で品切れ中…。4月には再度生産してくれるそうなので、手に入ると思います。
他の自作キーボードも、基本的にはIrisと同じ流れで作れると思うので、そういったものを作るのも良いと思います。

シンクロ・フードでは、自作キーボードに興味のあるエンジニアが数名在籍しているので、興味のある方は是非ご応募ください!キーボード談義に花を咲かせましょう。(業務でキーボードを自作することはありませんのでご注意ください…)

www.synchro-food.co.jp

AWS Lambda+API Gateway + S3で格安リアルタイム画像リサイズAPIを作成する

こんにちは、シンクロ・フードの大久保です。

今回は実際に弊社で運用しているAWS Lambdaを使ったリアルタイム画像変換APIについてご紹介したいと思います。 リアルタイム画像変換APIについては、あまり詳しく説明しませんが、画像のサイズ変換等をURLパラメータで指定してリアルタイムに変換することです。 弊社の場合はリサイズしか行わないため、画像変換APIというよりは画像リサイズAPI、というほうが適切かもしれません。

リアルタイム画像変換の方法について

弊社の方法をご紹介する前に、リアルタイム画像変換の一般的な実現手法を挙げてみます。

  1. Nginx, ApacheなどのWebサーバのプラグインを用いる
  2. CDNとして提供されている機能を用いる

1は、クックパッド社のmod_tofuが有名で、色々企業で実現されている手法です。サーバを用意しなければならない点や、冗長化を考えると弊社ではコスト的に取り組めませんでした。 2は、Akamaiやfastlyなど、高機能なCDNサービスを使う方法です。このあたりのサービスは以前は高額なものが多かったのですが、fastlyなどはとてもお得だと思います。

APIドキュメント
https://docs.fastly.com/api/imageopto/

料金
https://www.fastly.com/pricing/

こんなブログポストを書いておいてなんですが、リアルタイム画像変換は自作するよりも、fastlyの利用を検討することをオススメします。弊社も、このリアルタイム変換のシステムを作る前にfastlyで実現できることを知っていたら、fastlyを利用していたのではないかと思います。

ということで、以下はそれでもLambdaを使って自分たちで構築したい、という方向けの記事です。

設計

ベースとなる設計は、以下のAWSブログの記事をベースとしています。

https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/

弊社はこの設計に、さらにCloudFrontをかませており、以下のようなリクエストの流れとなります。元記事よりリクエストの流れを丁寧に説明していきます。

f:id:synchro-food:20180114145809p:plain

前提として、S3のバケットは2つ用意する必要があります。1つはリサイズ元画像を格納するOriginalBucket、もう1つはリサイズ後の画像を格納するResizedBucketです。

  1. ユーザーがCloudFrontにリクエストを送ります。初回リクエストはCloudFront上にキャッシュが無いため、CloudFrontはオリジンとして設定しているS3のResizedBucketにリクエストを流します。
  2. CloudFrontから流れてきたリクエストを受け、S3はBucket内を探しますが、Resizeされた画像が存在しません
  3. 画像が存在しない場合、内部用の画像変換APIへ307レスポンスが投げられます
  4. CloudFrontはS3からの307のレスポンスをそのままユーザーへ戻します
  5. ユーザーは307レスポンスを受け、次は内部用画像変換APIのURLを待ち受ける、APIGatewayにリクエストを投げます
  6. API GatewayよりLambdaの画像変換関数がキックされます
  7. LambdaがS3のOriginalBucketより、画像リサイズ元となる画像を取得します
  8. 7で取得した画像をパラメータに従ってリサイズし、ResizedBucketに保存します
  9. 8で保存した画像へのURLへの301レスポンスをLambdaにて返します
  10. 301レスポンスをそのままユーザーに返します
  11. ユーザーは301レスポンスに従い、もう一度リサイズ画像のリクエストを送ります
  12. CloudFrontからResizedBucketにリクエストが流れ、変換後画像をレスポンスとしてキャッシュしつつ、結果をキャッシュする

大きな流れは以上です。最初に画像を閲覧したユーザーは3回リクエストが飛ぶことになりますが、2回目以降はCloudFrontがキャッシュしているため、高速に戻すことができます。 リクエストが3回も飛ぶ動きなどが一見気持ち悪い手法ですが、この方法で半年以上運用し、特に大きな問題は起きていません。

以降、少し細かい設定方法を説明していきます。

Lambda関数を用意する

流れの図にある、7(S3からリサイズ元画像を取得する)と8(画像をリサイズしResizedBucketに入れる)、という2つの処理を行うLambda関数を作ります。 コードは、元記事も紹介している、以下のコードを改造していくのが良いと思います。

https://github.com/awslabs/serverless-image-resizing

弊社はこのコードを元に、以下の修正を加えて運用しています。

  • 不正なリサイズリクエストを除外するためのハッシュチェック
  • 細かいバリデーション、ログ埋め込みなど
  • serverlessフレームワーク化(まったく必須ではありません)

ハッシュチェックについては、誰かれかまわずリサイズできると困るため、確実に自分たちのサービスからのリクエストであることをチェックするために、アプリケーション側で作成したハッシュ値をLambda側でチェックしています。

API Gatewayの設定

こちらは特に特殊なことをやっているわけではないのですが、API Gatewayは分かりにくいため、記載しておきます。

リソースの作成

まず、API Gatewayにある「APIを作成」というボタンから新しいAPIを作成したあと、「アクション」→「リソースの作成」を選択します。

f:id:synchro-food:20180114161809p:plain

その後、上記画面のような入力画面が表示されるのですが、ここで「プロキシリソースとして設定」にチェックを入れて、リソースを作成してください。進むと以下のような画面が出ます。

f:id:synchro-food:20180118155859p:plain

こちらでは統合タイプを「Lambda関数プロキシ」、Lambdaリージョンは、後述するLambda関数のリージョン、Lambda関数は、作成したLambda関数を指定してください。

デプロイ

「アクション」→「APIのデプロイ」を選択し、ステージを選択します。 最初はステージがないため、「新しいステージ」を選択し、ステージ名を入力してください。 とりあえず、prod、という名称にすることが多いように思います。

デプロイが完了すると、APIGatewayのURLが生成されます。

https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod

これでAPIGatewayの準備は完了です。

S3の設定

上記設計図におけるResizedBucketは、StaticWebHostingを有効にしておく必要があります。それ自体は簡単なのですが、同時にリダイレクトルールを以下のように設定しておく必要があります。

<RoutingRules>
  <RoutingRule>
    <Condition>
      <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
    </Condition>
    <Redirect>
      <Protocol>https</Protocol>
      <HostName>${API Gatewayのホスト名}</HostName>
      <ReplaceKeyPrefixWith>${API Gatewayのステージ名}/resize?key=</ReplaceKeyPrefixWith>
      <HttpRedirectCode>307</HttpRedirectCode>
    </Redirect>
  </RoutingRule>
</RoutingRules>

もう一つ、Lambdaが作成した画像ファイルを都度閲覧可能にする必要があるため、バケットポリシーの設定も必要です。この設定がないと、Lambdaが作成したリサイズ後画像がPrivate権限となっていて、画像参照ができません。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::${S3のバケット名}/*"
        }
    ]
}

CloudFrontの設定

こちらは通常通り作成し、オリジンにはResizedBucketを指定します。

キャッシュ問題について

ただし、このままですと、CloudFrontは1回目の307リクエストの結果をキャッシュしてしまい、変換後の画像を表示してくれません。

この問題は、CloudFrontがキャッシュするかどうかは、S3のオブジェクトが持つCacheControlヘッダを使うようにすれば、解決します。

具体的な操作は以下の通りです。

  • CloudFrontの「Behaviors」→「Edit」→「Object Caching」を、「Use Origin Cache Headers」から「Customize」にする。
  • Lambda側のコードで、リサイズ後画像をS3にPutする際に、CacheControlヘッダを追加する。
      .then(buffer => S3.putObject({
          Body: buffer,
          Bucket: BUCKET,
          ContentType: 'image/png',
          CacheControl: 'max-age=2592000, s-maxage=259200',
          Key: key,
        }).promise()
      )

これでCloudFrontが適切にCacheしてくれるようになります。

基本的な設定の流れは以上です。 この設定を行うことで、以下のようなURLを渡すと、そのサイズにリサイズされた画像が戻ってくるようになります。 呼び出しイメージは以下のような雰囲気です。

https://xxxxxxxxxxx.cloudfront.net/hash/300x100/xxxx.jpg

実際に運用した感想

実際に上記のような設計でリアルタイム画像変換APIを作成し、半年ほど運用をしましたので、所感を列挙します。

  • 安定しています。CloudFront,S3はもちろんですが、lambdaでのトラブルは一切ありません。
  • 画像以外のリクエストが多い。faviconや画像以外のファイルリクエストが沢山きます。リリース後、余計なリクエストを除外する仕組みを入れました。
  • エラー検知は、とりあえずCloudWatchにて、Lambdaの実行エラーをアラームにしています。変換に失敗したときにメールが飛んできますが、エラー内容がメールに記載されていないので、少し面倒です。
  • lambdaのメモリ設定やtimeout設定などは、最初大きめに取り、ログを見ながら最適なメモリサイズやtimeoutに落としていく、という方法が良さそうです。
  • (これはリクエスト数次第ですが)費用は安いです。一度変換した画像は二度と変換が実行されないため、lambdaの呼び出し回数も少ないです。

まとめ

特に手法自体は新規性がないと思いますが、実際にプロダクション環境で運用している事を公開することで、誰かの参考になるかなと思い、ご紹介しました。 リアルタイム変換を今から導入しようと思うのであれば、まずはFastlyを第一に検討すべきかと思いますが、なんらかの問題があった場合などにご検討ください。

シンクロ・フードでは常にエンジニアを募集しています。 ご興味のある方は以下よりエントリーしてみてください!応募ではなく、話を聞いてみたい的なものでも結構ですので、お気軽にお問い合わせください。

Filelint を作って社内プログラムのコーディングスタイルを矯正した話

はじめまして。今年新卒で入社した基盤チームの川井 (@fohte) です。

最近までは、新卒企画研修として開発した wenu という飲食店向け Web サービスの開発基盤を整えたり、フロントエンド (React) のロジック部分を担当していました。

社内の既存プログラムの問題点

新卒研修終了後に配属され、既存プロジェクトの開発に携わったのですが、古くからあるコードが多く残されており、またコーディングスタイルも統一されていませんでした。
具体的には、以下のような問題がありました。

  • 行末にスペースやタブが残されている
  • ファイルの最後に空行が存在したりしなかったりする
  • インデントがソフトタブだったりハードタブだったりと混在している

これらの問題は構文エラーではなく、コンパイルも正常に通るために放置されていました。
しかし、各開発者が編集した際に無駄にスペースが挿入されたり削除されたりと、意味のない変更が差分として上がってしまう問題がありました。
意味のない差分はレビュワーにとっても負担になりますし、これらを強制することでこのような負担を軽減できないかと考えました。

解決手段

上記の問題は ESLintRuboCop といった言語特化型の lint ツールでは対応していないファイルでも見られるため、単純に様々な lint ツールを導入するといった方法では完全に解決することが困難です。
そのため、今回はこれらの lint ツールを使いつつ、また全てのファイルに対してコーディングスタイルを統一できるような手段について検討しました。

EditorConfig

まず、前述の問題を解決するような、言語を問わずコーディングスタイルを統一するツールとして、EditorConfig というツールが存在します。

EditorConfig は、開発者ごとのエディタや IDE の設定によってコーディングスタイルに差異が生まれてしまう問題を解決しようとしているツールの 1 つです。
.editorconfig という ini 形式のファイルをプロジェクトに配置し、それを各エディタや IDE が参照することで、設定内容に従って自動的にコーディングスタイルが統一されるようになります。

各自が自分の利用しているエディタや IDE に EditorConfig プラグインを入れる必要があるため少し抵抗になりますが、それを上回るメリットが存在すると感じたため、今回は開発チーム全体にプラグインを導入してもらうことにしました。

.editorconfig は以下のように設定し、各プロジェクトのルートディレクトリに配置しました。

root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

Filelint

EditorConfig を用いることで新規プログラムではコーディングスタイルが統一されますが、既存プログラムに対して適用することはできません。
そこで、EditorConfig のように言語を問わずコーディングスタイルを統一できる Golang 製の lint ツールを作成しました。

github.com

https://user-images.githubusercontent.com/11088009/27952943-16962632-6345-11e7-896f-f6d43aff084b.gif

Filelint では、以下の一般的なコーディングルールを提供しています。

ルール名 内容
indent (実験的なため不安定) ハードタブ (タブ文字) でのインデントあるいはソフトタブ (半角スペース) でのインデントを強制する
linebreak 改行コードを LF または CRLF に強制する
first-newline ファイルの最初に改行を必須にする / しない
final-newline ファイルの末尾に改行を必須にする / しない
no-bom UTF-8 BOM の挿入を禁止する
no-eol-space 各行の末尾が半角スペースあるいはタブ文字で終わることを禁止する

インデントの強制に関しては、Filelint は構文を解析していないため、うまくインデントが解析できないことがあります。 そのため、ESLint や RuboCop といった構文を解析する lint ツールを併用することを推奨しています。

使い方

ESLint 風のコマンドで、特定のディレクトリやファイルに対してコーディングスタイルに関する lint が実行できます。

# 現在のディレクトリ下のテキストファイルに再帰的に lint をかける
filelint
# README.md に lint をかける
filelint README.md
# 現在のディレクトリ下のテキストファイルに再帰的に lint をかけて自動修正する
filelint --fix
# some/dir 下のテキストファイルに再帰的に lint をかけて自動修正する
filelint some/dir --fix

以下のような YAML 形式の設定ファイルを .filelint.yml というファイル名でプロジェクトに配置することで、各 lint ルールの設定の他、特定のファイルを除外したり、特定のファイルには別のルールを適用することができます。

files:
  exclude:
    - '**/*.min.css'
    - '**/*.min.js'
targets:
  - patterns: ['**/*']
    rules:
      indent:
        enforce: false
      linebreak:
        enforce: true
        style: lf
      first-newline:
        enforce: true
        num: 0
      final-newline:
        enforce: true
        num: 1
      no-bom:
        enforce: true
      no-eol-space:
        enforce: true
  - patterns: ['**/*.md']
    rules:
      no-eol-space:
        enforce: false

インストール方法

GitHub Releases で各 OS 向けのバイナリを含んだパッケージを公開しています。 ここからダウンロードして解凍し、 $PATH に通されているディレクトリ下にバイナリファイルを配置してください。

Golang の環境がある場合は以下のコマンドでインストールできます。

go get github.com/synchro-food/filelint

実際の運用

前述の .filelint.yml はあるプロジェクトで利用しているもので、これを基本として各プロジェクトに配置し、ベースブランチにて filelint --fix コマンドで自動修正しました。

また、すでにベースブランチから切られている開発中ブランチに関しても、コンフリクトを避けるために以下の手順で各開発者の手元で修正して頂きました。

# 適用するブランチ (開発中のブランチ) に移動する
git checkout <branch name>

# .filelint.yml が追加されたコミットを現在のブランチに適用する
git cherry-pick $(git log --reverse origin/master --format="%H" -- .filelint.yml | head -1)

# Filelint を実行する
# (一時的に --rule オプションを用いてインデント修正をしています)
filelint --rule 'indent: {enforce: true, size: 2, style: soft}' --fix $(git diff $(git merge-base origin/master HEAD) --diff-filter=d --name-only)

今後継続的にコーディングスタイルを統一させるため、CI を用いた自動コードレビューシステムを現在構築しています。

最後に

Filelint は社内初の OSS プロダクトです。 今後 OSS 活動を推進していきたいと思っていますので、バグ報告や機能追加要望、PR などお待ちしております。

シンクロ・フードでは OSS 活動に興味のあるエンジニアも募集しています。 ご興味のある方は以下よりご連絡ください。

徳丸浩先生をお招きしてセキュリティ研修を行いました!

こんにちは、シンクロ・フードの大久保です。
弊社では今年の4月に入社した新卒エンジニアを対象に、セキュアスカイ・テクノロジー社(以下SST社)の「eラーニング研修」+「1日オンサイト研修」を行いました。

安全なWebサイト構築のための教育[eラーニング] | Webセキュリティ教育サービス | SST 株式会社セキュアスカイ・テクノロジー

「オンサイト研修」では、あの徳丸本でお馴染み、徳丸浩先生が来ていただけるとのこと!eラーニング研修を受けていない既存エンジニアもオンサイト研修には参加OK、ということだったので、エンジニア全員、徳丸浩先生のオンサイト研修に参加しました。

f:id:synchro-food:20170720170537j:plain SST社の乗口社長もお越しいただきました

f:id:synchro-food:20170720170801j:plain 前半はWebサイトをめぐる状況についてのお話し

f:id:synchro-food:20170720182317j:plain 後半はチームに分かれてディスカッション&発表

私物の徳丸本を持参してサインをもらうエンジニアもちらほら…。

参加者は若いメンバーが多いため、セキュリティを学ぶ動機付けを目的としたオンサイト研修でした。
これからエンジニアとしてキャリアをスタートする新卒メンバーに、セキュリティの大切さや興味を持つきっかけになれば良いと思っています。

徳丸浩先生、SST社乗口社長、ありがとうございました!

尚、シンクロ・フードでは、エンジニアを募集しています。 こういった研修も積極的に実施していく予定ですので、興味があれば是非採用ページをご覧ください!

キャリア採用 | シンクロ・フード採用サイト

ReactでLINE風チャット画面を実装してみた

シンクロ・フードでフロントエンドの開発を担当している四之宮です。
今回は、前回のブログで宣言した通り、ReactでLINE風チャット画面を実装してみたことについてお話ししたいと思います。

ReactとCSSがある程度わかることが前提になります。

作成した機能について

この機能は、弊社が運営している店舗デザイン.COMのサイト内で提供されるサービスになります。

www.tenpodesign.com

店舗デザイン.COMでは、「店舗の出店や改装を考える方と店舗のデザインや施工をおこなうデザイン会社とを結びつける」ということを行っています。
機能の実装をお話しする前に、簡単にではありますが店舗デザイン.COMの説明をしたいと思います。

まず、店舗の出店や改装を考える方(以後施主と表記)は、店舗のイメージや情報を登録します。
その後、この情報をデザイン会社に配信し、興味を持ったデザイン会社がエントリーをします。
施主は、エントリーがあったデザイン会社の過去の作品や情報を見て、もっと詳細な情報を知りたいということになると、実際のやりとりが開始します。

ここで、今回作成したやり取りの機能が使用されます。

実際の画面

まず最初に作成した画面をご紹介したいと思います。
こちらの画面は、施主側のスマホ版のページになります。

f:id:synchro-food:20170427190641p:plain

一覧表示について

簡単な概要は下記の通りです。

  • 画面を開くと、最新の10件が表示される
  • 最下部が最新メッセージで、上にいくほど古い
  • 続きの読み込みはボタンのタップ
  • 前後のメッセージで日付けが変わる場合は、日付を挿入する
  • 送信メッセージ
    • 表示エリアは右寄せで表示
    • 背景は白
    • 相手が読んでいれば「既読」と表示
  • 受信メッセージ
    • 表示エリアは左寄せで表示
    • 相手の名前を表示
    • 背景はグレー

ここからは、ちょっと大変だったポイントを掻い摘んで説明したいと思います。

並び順と日付け挿入について

配列には新しい順でデータを持ちますが、表示では逆順で表示する必要があります。
これは、reverse()をすれば解決です。
当たり前ですが、Immutable.jsなどを使って、propsのmessagesを直接reverse()しないように注意してください。
日付け挿入に関しては、直前のメッセージと比較して、日付が違っていたら日付けを表示するようにします。

下記は、MessageListというコンポーネントのrender部分を一部抜粋したものになります。

<div className="messageList js-messageList">
    {
        MessageList.getReverseMessages(this.props.messages).map((message, index, messages) => {
            let prevMessage = {}
            if (index !== 0) {
                prevMessage = messages[index - 1]
            }
            if (MessageList.isChangeDate(message, prevMessage)) {
                return (
                    <div className="messageList__itemWrap" key={message.keyId}>
                        <div className="messageList__item messageList__item--date">
                            {MessageList.showDate(message.createDate)}
                        </div>
                        <div className="messageList__item">
                            <Message message={message} />
                        </div>
                    </div>
                )
            }
            return (
                <div className="messageList__itemWrap" key={message.keyId}>
                    <div className="messageList__item">
                        <Message message={message} />
                    </div>
                </div>
            )
        })
    }
</div>

日付単位でulで実装したかったのですが、綺麗に日付毎表示されるわけでもないので、渋々div実装になっています。

補足

  • getReverseMessagesは引数のthis.props.messagesをreverseするメソッド
  • isChangeDateは直前のメッセージと日付けが同じか判定するメソッド
  • Messageはこの後に説明します

続きを読むなどの時にスクロール位置を保持する

続きを読みこむと、一番上に続きのメッセージが積まれていきます。
そして、読み込むためのボタンは一番上にあります。
そのため、続きを読みこむとスクロールの位置が一番上になってしまいます。
これを制御するための処理が下記になります。

下記は、MessageListというコンポーネントを一部抜粋したものになります。

constructor(props) {
    super(props)
    this.state = {
        prevHeight: 0,
    }
}

componentDidMount() {
    const height = $('.js-messageList').height()
    const heightDiff = height - this.state.prevHeight
    $('body').scrollTop(heightDiff === 0 ? $('body')[0].scrollHeight : heightDiff)
}

componentWillReceiveProps(nextProps) {
    if (nextProps.messages !== this.props.messages) {
        const prevHeight = $('.js-messageList').height()
        this.setState({ prevHeight })
    }
}

componentDidUpdate(prevProps) {
    if (prevProps.messages !== this.props.messages) {
        const height = $('.js-messageList').height()
        const heightDiff = height - this.state.prevHeight
       $('body').scrollTop(heightDiff === 0 ? $('body')[0].scrollHeight : heightDiff)
    }
}

componentWillReceivePropsを使用しているのは、メッセージの配列を管理しているが、このファイル内ではないためです。
そして、jQueryも使用しています。
賛否両論あるかと思いますが、個人的にそんなに嫌いではないので、Reactの中でも使用することがあります…。
もちろんDOM操作はしませんが。

また、新着メッセージの受信については、setTimeoutなどで受信を監視する必要があります。

受信メッセージの表示について

送受信にはそれぞれに決まったcssをあてる、日付けが変わる場合は日付を挿入するという2点を実現できればいいということです。
そして、先ほど出てきていた、Messageコンポーネントがここにあたります。
下記は、Messageというコンポーネントを一部抜粋したものになります。

const getMessageClass = (message) => {
    if (message.isSend) {
        return 'send'
    }
    return 'receive'
}

const replaceBr = text => (
    text.split('\n').map((line, index) => <p key={index}>{line}</p>)
)

return (
    <div className={`messageBox messageBox--${getMessageClass(props.message)}`}>
        <div className="messageBox__inner">
            {!props.message.isSend && <p className="messageBox__name">{props.message.name}</p>}
            {props.message.message && <p className="messageBox__message">{replaceBr(props.message.message)}</p>}
            {(() => {
                if (props.message.file) {
                    return (
                        <div className="messageBox__file">
                            添付ファイルのアイコンなどを表示する処理
                        </div>
                    )
                }
                return ''
            })()}
        </div>
        {(props.message.isAlreadyRead && props.message.isSend) && <p className="messageBox__alreadyRead">既読</p>}
    </div>
)

ここからはCSS(scss)です。
今更ではありますが、弊社ではMindBEMdingによる命名規約で、クラス名をつけています。
こちらも、必要な箇所を一部抜粋したものになります。

.messageList {
    &__itemWrap {
        &:not(:first-child) {
            margin-top: 20px;
        }
    }

    &__item {
        overflow: hidden;

        &:not(:first-child) {
            margin-top: 20px;
        }

        &--date {
            font-size: 14px;
            color: #666666;
            text-align: center;
            display: flex;
            align-items: center;
        }
    }

    &__item--date {
        // 日付けは中央で左右に線を引く
        &:before, &:after {
            border-top: 2px solid #E6E6E6;
            content: "";
            display: inline;
            flex-grow: 1;
        }

        &:before {
            margin-right: 0.5em;
        }

        &:after {
            margin-left: 0.5em;
        }
    }
}

.messageBox {
    &--send {
        float: right;
    }

    &--receive {
        float: left;
    }

    &__inner {
        border-radius: 16px;
        padding: 10px;
        box-sizing: border-box;
        display: inline-block;

        .messageBox--send & {
            background-color: #fff;
            border: solid 1px #ccc;
        }

        .messageBox--receive & {
            background-color: #f2f2f2;
        }
    }

    &__alreadyRead {
        margin-top: 5px;
        text-align: right;
    }
}

あまり関係ないですが、jsxからだとeCSStractorがそのまま使えないのが個人的にとても悲しいです。
HTMLから自動でCSSのセレクタを生成してくれるので、とても便利です。
しかもBEM記法にも対応しています。
jsxで使うときは、一旦classNameをclassに置換して生成して戻すみたいなことしています…。

https://packagecontrol.io/packages/eCSStractorpackagecontrol.io

まとめ

以上が、「ReactでLINE風チャット画面」の解説になります。
それぞれの分野でお詳しい方からすれば、ReactもCSSも大したことない内容ではあると思いますが、ReactとCSSの知識がそれぞれある程度必要という意味で面白そうでしたので記事にしてみました。

またコード量の問題で、一部抜粋する形での記載になっているので、解り辛い点もあるかと思います。
その際はお気軽にお問合わせいただければと思います。

最後に、シンクロ・フードではエンジニアを募集しています。
少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

www.synchro-food.co.jp

Compassのsprite-mapによるスプライト画像生成を、spritesmithに移行する

シンクロ・フードでフロントエンドの開発を担当している四之宮です。
最近は、ReactでLine風のやりとり機能を実装しました。
近々こちらについても、ご紹介できたらと思います。

では、本題に入っていきたいと思います。
タイトルには入っていないですが、レガシーシリーズもこれが最後だと思います。

Sass+Compassについて

弊社ではSass+Compassでcssのコーディングを行っています。
今では、「Compassは終焉を迎えた」などと言われていますが当時はこれがテッパンだっと思います。
ベンダープレフィックス対応はCompassが提供する各mixin、スプライト画像の対応もCompassが提供するsprite-map、これらを使用してコーディングしました。

その後、ベンダープレフィックス対応はautoprefixerが主流となりました。

www.npmjs.com

スプライト画像に関しては、アイコンフォントを使用することでスプライト画像自体の使用をやめることも増えたのではないでしょうか?

本記事で説明すること

スプライト画像対応に絞って説明していきます。

弊社ではこのベンダープレフィックス対応とスプライト画像の対応が、Compassに依存していました。
autoprefixerへの移行に関しては、記事も多いので割愛させていただきます。

そして、スプライト画像対応に関しては、既存の画像をそのまま使用する方向で対応します。

脱Compassを行った理由

世の中の流れというものありますが、やはり一番の理由はコンパイルの速度向上を目指したためです。
scssファイルの数を増えてからコンパイルに時間がかかり、開発効率が悪いという課題がありました。

そこで、コンパイルの高速化プロジェクトが開始しました。
まず、最初に行ったことですが、@import "compass";の記述をやめました。
必要に応じて、関数名まで指定するという対応を取りました。

例えば、@include border-radius();を使用したいとします。
このとき、ファイルのトップで、@import "compass";を書いてはいけません。
@import "compass/css3/border-radius";と書きます。
この対応だけでも早くなったと体感できるレベルで改善しました。

更に、libsassを使用したgulp-sassにしたいと思ったのですが、こちらはCompass対応していないので、対応見送りとなりました。

しばらくの間は、これで満足していたのですが、慣れというのは怖いですね。この速度では満足できなくなってきました。
コンパイルの時間を見てみると、一番時間がかかっているのはスプライト画像周りの処理でした。
これを解決しない限りはどうしようもないということになり、別の方法でスプライト画像を対応しようとなりました。

スプライト画像対応の移行

gulp.spritesmithを使用します。

www.npmjs.com

npm install --save gulp.spritesmith  

これは、指定したディレクトリに格納された画像を、1つの画像にしてこれに対応するscssファイルを書き出すというものです。
詳しい使用方法はこのあと記述していきます。

gulpfile.jsの対応

本来の使い方から少し拡張して使用しています。
1サイトで1スプライト画像というのが世の中の流れなのかもしれませんが、弊社ではページ単位などで管理していたためこの対応が必要となりました。
元々、gulp.spritesmithはディレクトリ単位でスプライト画像を生成する機能を持たないため、これに対応するための書き方をしています。
これを実現するために、更にglobbyというnpmを使用します。

www.npmjs.com

更に、複数のストリームがある状態で非同期化したいので、merge-streamというnpmを使用します。

www.npmjs.com

1サイトで1スプライト画像の構成になっていれば、この対応は不要になります。

var spritesmith = require("gulp.spritesmith");  
var globby = require('globby');  
var Merged = require('merge-stream');  
  
var root = "src/main/webapp",  
config = {  
   "path" : {  
      "htdocs"    : root,  
      "spriteSass"      : root+"/spriteSass",  
      "spriteImage"    : root+"/spriteImage",  
      "image"     : root+"/image/sprite"  
   }  
};  
  
gulp.task('sprites', ['cleanやcopyなど'], function() {  
    var merged = new Merged();  
  
    globby('src/main/webapp/image/sprite/**/*.png').then(paths => {  
        paths.forEach(function(path) {  
            var filePath = path.match(/^(.+\/)(.+?)(\/.+?\..+?)$/);  
            var spritePath = (filePath[1] + filePath[2]).replace("src/main/webapp/image/sprite", '');  
            var spriteData = gulp.src(filePath[1] + filePath[2] + '/*.png')  
                .pipe(plumber())  
                .pipe(spritesmith({  
                    imgName: filePath[2] + '.png', //生成される画像名を指定します(ここでは各ディレクトリ名になります)  
                    imgPath: '/spriteImage' + spritePath + '/' + filePath[2] + '.png', //生成される画像のパスを指定します  
                    cssName: filePath[2] + '.scss', //生成されるscss名を指定します(ここでは各ディレクトリ名になります)  
                    algorithm: 'top-down', //スプライト画像になるときの画像の並びを指定します  
                    padding: 100 //スプライト画像になるときの、各画像の間隔を指定します  
                }));  
            var imgStram = spriteData.img.pipe(gulp.dest(spriteImage + spritePath));  
            var SassStram = spriteData.css.pipe(gulp.dest("src/main/webapp/spriteSass/" + spritePath));  
  
            merged.add(imgStram);  
            merged.add(SassStram);  
        });  
    });  
    return merged;  
});  

algorithmについては以下が指定できます。
※sprite-mapにあった$position: 100%のような指定はできないようです

https://github.com/twolfson/gulp.spritesmith#algorithms

ここでは使用していないオプションも他にも用意されています。

https://github.com/twolfson/gulp.spritesmith#documentation

scssファイルの対応

  1. sprite-mapでgrep
  2. 対象のscssファイルのsprite-mapの記述を、gulp.spritesmithで生成された対象のscssをimportするように修正
  3. スプライト画像設置用mixinの呼び出しを消す
    • mixin化していなかったらスプライト画像呼び出しの記述を全て消す
  4. 画像を表示するセレクターに@include sprite(○○);のような記述をしていく
    • spriteという関数はgulp.spritesmithで生成されたscss内に宣言されたmixinです
    • ○○にはimportしたスプライト用scss内にある変数を記述します
    • 恐らく$画像名となっているはずです

アイコンなどの設置を空要素で行っていればこの手順でOKです。
もし、空要素ではなくテキストなどが書かれたタグに対して、アイコンを設置していた場合は、アイコン用の要素を設置する必要があります。
弊社ではこの形になっていたので、かなり厳しい対応となりました。
これについては、後ほど実際のコードがでてくるところで説明します。

scssのコンパイル処理の書き換え

Compass依存がなくなったので、gulp-sassを使用してコンパイルするようにします。
gulp-ruby-sassというものもありますが、libsassを使用しているgulp-sassの方が処理が早いので、こちらを選択しました。

www.npmjs.com

npm install --save gulp-sass  

gulpfile.jsの対応

var sass = require('gulp-sass');  
  
gulp.task('sass', ['clean'], function () {  
    return gulp.src('src/main/webapp/sass/**/*scss')  
        .pipe(sass({includePaths: ['src/main/webapp/sass/', 'src/main/webapp/spriteSass/']}).on('error', sass.logError))  
        .pipe(gulp.dest('src/main/webapp/stylesheets/'));  
});  

src/main/webapp/spriteSass/はspritesmithで生成されたscssファイルがあるディレクトリを指定しています。

全体像

gulp.task('sprites', ['cleanやcopyなど'], function() {  
    var merged = new Merged();  
  
    globby('src/main/webapp/image/sprite/**/*.png').then(paths => {  
        paths.forEach(function(path) {  
            var filePath = path.match(/^(.+\/)(.+?)(\/.+?\..+?)$/);  
            var spritePath = (filePath[1] + filePath[2]).replace("src/main/webapp/image/sprite", '');  
            var spriteData = gulp.src(filePath[1] + filePath[2] + '/*.png')  
                .pipe(plumber())  
                .pipe(spritesmith({  
                    imgName: filePath[2] + '.png',  
                    imgPath: '/spriteImage' + spritePath + '/' + filePath[2] + '.png',  
                    cssName: filePath[2] + '.scss',  
                    algorithm: 'top-down',  
                    padding: 100  
                }));  
            var imgStram = spriteData.img.pipe(gulp.dest(spriteImage + spritePath));  
            var SassStram = spriteData.css.pipe(gulp.dest("src/main/webapp/spriteSass/" + spritePath));  
  
            merged.add(imgStram);  
            merged.add(SassStram);  
        });  
    });  
    return merged;  
});  
  
gulp.task('spritesSass', ['sprites'], function () {  
    return gulp.src('src/main/webapp/sass/**/*scss')  
        .pipe(sass({includePaths: ['src/main/webapp/sass/', 'src/main/webapp/spriteSass/']}).on('error', sass.logError))  
        .pipe(gulp.dest(cssPath));  
});  
  
gulp.task('sass', function () {  
    return gulp.src('src/main/webapp/sass/**/*scss')  
        .pipe(sass({includePaths: ['src/main/webapp/sass/', 'src/main/webapp/spriteSass/']}).on('error', sass.logError))  
        .pipe(gulp.dest(cssPath));  
});  
  
gulp.task("spritesAutoprefixer", ['spritesSass'], function() {  
    return gulp.src(cssPathRegexp)  
        .pipe(autoprefixer({  
            browsers: ['last 2 version', 'IE >= 9', 'iOS >= 8.1', 'Android >= 4.4'],  
            cascade: false  
        }))  
        .pipe(gulp.dest(cssPath));  
});  
  
// watchタスク  
gulp.task('watch', ['spritesAutoprefixer'], function () {  
    gulp.watch("src/main/webapp/sass/**/*scss", ['sass']);  
    gulp.watch('src/main/webapp/image/sprite/**/*.png', ['spritesSass']);  
});  

実際のコーディング

  1. スプライト用画像をディレクトリへ入れる
    • 自動でスプライト画像の生成が行われ、これに対応するscssファイルも生成される
    • このscssファイルの中に、スプライト画像を読み込むためのmixinや変数が宣言されている
  2. このscssファイルをスプライト画像でimportする
    • こんな感じで呼び出す @include sprite($next-button);
  3. このmixinによって、画像の縦横幅を指定されるので、空要素を置いて対応する
    • inline要素の場合、blockやinline-blockにするのを忘れないように

画像ディレクトリから以下のようなファイルが出来上がったとします。
対象のディレクトリにはexample_iconがあったということです。

  • sprite.scss
$example-icon-name: 'example_icon';  
$example-icon-x: 0px;  
$example-icon-y: 1591px;  
$example-icon-offset-x: 0px;  
$example-icon-offset-y: -1591px;  
$example-icon-width: 50px;  
$example-icon-height: 50px;  
$example-icon-total-width: 50px;  
$example-icon-total-height: 1641px;  
$example-icon-image: '/spriteImage/top/sprite/sprite.png';  
$example-icon: (0px, 1591px, 0px, -1591px, 50px, 50px, 50px, 1641px, '/spriteImage/top/sprite/sprite.png', 'example_icon', );  
  
@mixin sprite-width($sprite) {  
  width: nth($sprite, 5);  
}  
  
@mixin sprite-height($sprite) {  
  height: nth($sprite, 6);  
}  
  
@mixin sprite-position($sprite) {  
  $sprite-offset-x: nth($sprite, 3);  
  $sprite-offset-y: nth($sprite, 4);  
  background-position: $sprite-offset-x  $sprite-offset-y;  
}  
  
@mixin sprite-image($sprite) {  
  $sprite-image: nth($sprite, 9);  
  background-image: url(#{$sprite-image});  
}  
  
@mixin sprite($sprite) {  
  @include sprite-image($sprite);  
  @include sprite-position($sprite);  
  @include sprite-width($sprite);  
  @include sprite-height($sprite);  
}  
  
@mixin sprites($sprites) {  
  @each $sprite in $sprites {  
    $sprite-name: nth($sprite, 10);  
    .#{$sprite-name} {  
      @include sprite($sprite);  
    }  
  }  
}  

スプライト画像の呼び出しは以下の通り。

@import "sprite";  
  
.exampleIcon {  
    @include sprite($example-icon);  
  
    display: inline-block;  
}  

ここで使用しているspritesという関数ですが、見ての通りですがwidthheightが固定化されます。
これが、スプライト画像を配置する要素を別で用意しなければならない理由です。

また、sprite.scssには変数としていくつか宣言されているので、こちらも使用することができます。
たとえば、$example-icon-widthはスプライト画像として連結される前の画像の横幅です。

retina対応画像の場合

少し対応が変わります。
spritesmithにはretina対応するためのオプションが用意されているのですが、弊社ではこれを使用しませんでした。
使用しないというよりも、弊社では使用できませんでした。
このオプションが使用できるには条件があります。

  • 2x用の画像とそれに対応する1xの画像を用意する
  • この2xと1xの画像は正確に2倍の関係である

弊社では、2x画像のみで1xがないという場合がありました。
1xと2xの両方を用意して対応していたんですが、世の中のスマホが2xが主流となり、1x対応をやめたという背景があります。
更に、2x画像のwidthが奇数値というものもあり、1xの画像を新たに用意することもできませんでした。

そのため、retina対応に関しては、独自の対応をとることにしました。

retina画像用のmixinの作成

自作といっても上記のsprite.scssをベースに作成した形になります。
下記の通り、sprite.scssにあった各種mixinを○○-2xとし、サイズ関係を全て2で割っただけです。
retina画像の場合、以下のscssファイルをimportして、使用するmixinを-2xシリーズにすればOKです。

@charset "UTF-8";  
  
@mixin sprite-width-2x($sprite) {  
    width: nth($sprite, 5) / 2;  
}  
  
@mixin sprite-height-2x($sprite) {  
    height: nth($sprite, 6) / 2;  
}  
  
@mixin sprite-position-2x($sprite) {  
    $sprite-offset-x: nth($sprite, 3) / 2;  
    $sprite-offset-y: nth($sprite, 4) / 2;  
  
    background-position: $sprite-offset-x $sprite-offset-y;  
}  
  
@mixin sprite-image-2x($sprite) {  
    $sprite-image: nth($sprite, 9);  
  
    background-image: url(#{$sprite-image});  
    background-size: (nth($sprite, 7) / 2) (nth($sprite, 8) / 2);  
}  
  
@mixin sprite-2x($sprite) {  
    @include sprite-image-2x($sprite);  
  
    @include sprite-position-2x($sprite);  
  
    @include sprite-width-2x($sprite);  
  
    @include sprite-height-2x($sprite);  
}  

まとめ

以上が脱Compassまでの手順になります。
正直かなり面倒な作業です。

多少時間がかかりますが、無心で対応すればやりきれます。
更に大変なのがテストですが、弊社ではこのタイミングでBackstopJSを導入して、リグレッションテストを自動化しました。
こちらに関しても、詳しく説明された記事も多いので、興味がある方は調べてみてください。簡単に導入できると思います。
もちろんリクエストがあればご説明いたします。

さて、最後に衝撃的な告白です。
ここまで全てやりきったような口ぶりで書いてきましたが、この対応による修正は日の目を見ることはありませんでした。
というのも、この対応をする際、弊社が運営しているサイトの中でも比較的小規模なサイトを選んだのですが、工数がかかり過ぎました。
この工数のほとんどは、スプライト画像を配置するために要素を新たに設置するというものです。
今でこそアイコン関連は空要素に対して配置していますが、記事内でも紹介した通り以前まではテキストなどと同じ要素に対してアイコンを設置していました。
比較的小規模なサイトでも大変だったのに、より大規模なサイトで対応するのは現実的ではなく、サイト間でコーディング方法がずれるのはよくないということで、見送りとなりました。

ただ、一通りテストは行っているので、この対応に関しては問題ないと思います。

といったように既存コードの改修は一旦諦めて、新しいコードは当たり前ですがアイコンフォントを使用する方針で実装しています。
デザイナーさんにもアイコンは可能な限り単色で用意していただくようにしています。

最後に、シンクロ・フードではエンジニアを募集しています。
少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

www.synchro-food.co.jp

Tomcatを高速に起動する

こんにちは、シンクロ・フードの大久保です。最近はAWSのLambdaを触っています。近々、そのネタもブログにしようと思っています。

さて、今回はTomcatの起動高速化のお話しをしようかと思います。Railsもやっていますが、なんだかんだ言って弊社はTomcatとの付き合いが長いので…。

まず、Tomcatの起動高速化する方法を紹介する記事としては、以下のWikiが有名です。
https://cwiki.apache.org/confluence/display/TOMCAT/HowTo+FasterStartUp

今回はこの記事を軽く紹介していく、という、新しいことが特にあるわけでもない記事です…。
とはいえ、元記事を読んだことが無い方には参考になるかもしれませんので、もしこの記事を読んで興味が湧いたら、元記事を読んでみてください。

尚、弊社の本番環境で運用されているTomcatは、サーバを切り替えながら再起動をする仕組みができているため、起動速度を上げたいというニーズがなく、本番環境で適用している設定はありません。弊社では開発環境や、テスト環境などで設定をしています。

高速化手法 その1:web.xmlにmetadata-complete=trueとabsolute-orderingを設定する

Wikiによると、WEB-INF/web.xmlに以下の設定を加えることで、高速化される、とのこと。
1. web-app要素に、metadata-completeという属性を設定し、値をtrueにする
2. absolute-orderingという空要素を追加する

そもそも、Servlet3.0にて追加された、各種便利機能(Servlet開発を楽にするアノテーションなど)のために、Tomcatは起動時にJavaのclassファイルをscanするのですが、これが起動速度に影響します。弊社のようにSeasar2を使っている場合、Servlet3.0で追加された機能が無くても開発に支障はでないため、scanが不要だったりします。
この設定は自身のプロジェクトにあるclassファイルやライブラリをscanしないようにさせる設定です。

適用例はこんな感じ

<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"  
version="2.4" metadata-complete="true">  
  
    <absolute-ordering />  

実際に手元のPC(Intel Corei7-4750HQ、メモリ16GB)にある弊社サイト「飲食店.COM」の起動時間を適用前/後で比較したところ、起動時に大体6600msくらいかかっていたのが、2300msになりました(10回起動した平均値)。
ただし、クラスや依存ライブラリの少ないWebアプリケーションで比較すると、ほとんど効果を感じられませんでした。
当然ながら、クラスやライブラリの数が多ければ多いほど効果があるようです。

高速化手法1~4は、この「jarスキャンを減らすことで速度を上げる」という方向性での高速化を行なっています。

高速化手法 その2:不必要なjarファイルを消す

当たり前ですが、不要なjarファイルが無い方がスキャン対象が減って起動速度が早くなります。

高速化手法 その3:jarファイルをスキャンから除外する

Tomcat7の場合、catalina.propertiesに除外するjarを記載することでスキャンするjarファイルを指定できます。

tomcat.util.scan.DefaultJarScanner.jarsToSkip=\  
bootstrap.jar,commons-daemon.jar,tomcat-juli.jar,\  
...(省略)  
除外したいjarファイル.jar  

Tomcat8ではcatalina.propertiesの他に、context.xmlにある要素の下に、要素を追加して除外もできますが、弊社はcatalina.propertiesに設定をしてしまっています。
この手法は個別にスキャンをスキップするjarファイルを指定することができるので、1の手法よりも少し安心感があります。
当然ですが、除外するjarファイルが多ければ多いほど、起動が早くなります。

高速化手法 その4:WebSocketサポートを切る

TomcatでWebSocketを使わない、という場合は、WebSocketを無効化しましょう、という内容です。
実際には、tomcat/libの下にある、tomcat7-websocket.jar、tomcat-websocket.jarなどを消せば良いです。
こちら、ここに言及した高速化手法をまったくしていない環境に対して実施すると、20%ほど起動が高速化します(6600msが4400msくらい)。
ただし、他の手法でjarファイルのスキャンを無効化していると、WebSocket周りのjarスキャンも無効化されているため、jarファイルを消す意味はないです。
つまり、jarのスキャンは無効化したくないけれども、WebSocketは使わない、という場合に有効な手法ですね。

高速化手法 その5:Entropy Sourceをnon-blockingな/dev/urandamを指定する

Tomcatは起動時にSecureRandamを初期化するために、/dev/randamからホストサーバのノイズを集めて乱数生成をするのですが、/dev/randamは十分なノイズが集まるまではブロックをします。こちらを、ブロックしない/dev/urandamを使うことで高速化する、というのがこちらの内容です。
この2つの違いはTomcat起動とは関係なく、様々なブログなどで言及されているので、あまり詳細には書きません。
具体的な指定としては、以下を起動オプションに加えます。

-Djava.security.egd=file:/dev/./urandom  

高速化手法 その6:プロジェクトを並列起動する

一つのTomcatで複数プロジェクトを起動している場合、デフォルトでは一つ一つ直列に起動するのですが、これを並列起動することで速度を上げることができます。
弊社は一つのTomcatで複数サイトを運営しているため、この設定でかなり起動速度が上がっています。
具体的には、Host要素にstartStopThreads属性を追記し、ここに並列起動数を記載します。0を指定すると、CPUのコア数分起動するので、Tomcat以外のアプリケーションがCPUを常時使っていないのであれば、0で良いと思います。
https://tomcat.apache.org/tomcat-8.0-doc/config/host.html

まとめ

Wikiに記載されているのは以上です。地味ーな内容ですが、Tomcat起動が遅くてイライラ…という方がいらっしゃればお試しください。
少しでも興味があれば、元記事を読んでいただくのが良いと思います。
https://wiki.apache.org/tomcat/HowTo/FasterStartUp

シンクロ・フードではエンジニアを募集しています。Tomcat、好きだなあ…という方、是非ご応募ください! もちろんRubyでもWebアプリケーションを書いているので、Java以外でもWebアプリケーション書きたい、という方も募集中です。

www.synchro-food.co.jp