フロントエンドにESLint、Prettier、stylelintを導入して快適な開発環境を整えました

こんにちは、Webチームの柴山(@shikeapp0909)です。

私たちのプロダクト「ままのて」のフロントエンドはReact + SCSSで開発しています。コード規約は存在するもののインデントやセミコロンのつけ忘れなどをいちいちコーディングの際に気をつけながら開発するのも、コードレビューでチェックして防ぐのも余計な労力で効率が悪いため、リントとコードフォーマッターを入れることにしました。

今回はその導入内容を手順と共にご紹介します。

JSのリントとフォーマッター

JSの方はESLintPrettierでリントとフォーマットを行うようにしました。

JavaScript Standard Style

基本的なJSのコード規約はJavaScript Standard Styleに準拠することとしました。

公式から簡単にルールを抜粋すると、以下のようになっています。

  • インデントは2スペース
  • 文字列はシングルクオート
  • 未使用変数は禁止
  • 文末セミコロンは禁止
  • キーワードの後にスペースを入れる
  • 関数名の後にはスペースを入れる
  • 値の比較には==ではなく===を使う

こちらの記事が詳しく解説してくださっていますので、読んでみてください。

JavaScript Standard Styleのススメ - Qiita

これをESLintのルールとして適用するようにしています。そのためのプラグインが用意されているので、ESLint本体とそれらをインストールします。

$ yarn add -D eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node

※yarnは適宜npmと読み替えてください。

そしたらpackage.jsonと同じディレクトリに.eslintrc.ymlファイルを作成し、以下のように記述します。

extends:
  - standard

これだけでESLintにJavaScript Standard Styleの設定は完了です。

React用のESLint

次にReact用の設定です。こちらもESLint用のプラグインが用意されているので、それをインストールします。

$ yarn add -D eslint-plugin-react eslint-plugin-jsx-a11y

そしたら先ほどの.eslintrc.ymlに追記します。

extends:
  - standard
# ↓追加
  - plugin:react/recommended
plugins:
  - react
settings:
  react:
    version: 16.6 # お使いのReactのバージョン
# ↑追加

これで、Reactのバージョンに応じてDeprecatedになったAPIをしようしていないかなどをチェックしてくれます。

このルールではPropTypesの定義が必須なのですが、既存のコードではPropTypesをまったく記述していなかったので、今回はこのルールをいったん除外することにしました。そのためには.eslintrc.ymlrulesに追加します。

extends:
  - standard
  - plugin:react/recommended
plugins:
  - react
# ↓追加
rules:
  react/prop-types:
    - 0
# ↑追加
settings:
  react:
    version: 16.6 # お使いのReactのバージョン

これでPropTypesを定義していなくても怒られなくなりました。

Prettier

最後にPrettierの設定です。PrettierはJSだけでなくCSSなどさまざまなファイルをフォーマットしてくれる便利なプラグインです。設定できるルールについては、公式ドキュメントで確認してください。

Prettierのデフォルトのルールは先ほどのJavaScript Standard Styleと重複していて相反するものがいくつかあるので、カスタマイズして使用するため、.prettierrc.ymlファイルを.eslintrc.ymlファイルと同じディレクトリに作成し、以下のように記述します。

printWidth: 120
semi: false
singleQuote: true
trailingComma: es5

デフォルトだと80文字を越えると改行されますが、それだと少し短いと感じたので、とりあえず120文字と設定し、配列とobjectには最後のプロパティの後ろにカンマを入れるように設定しました。

これでPrettierの設定はできましたが、こちらに記載されているように、Prettierの設定をESLintに取り込んで、ESLintのルールとして実行することが可能です。そのためのプラグインをインストールします。

$ yarn add -D eslint-plugin-prettier eslint-config-prettier

そしたら.eslintrc.ymlファイルに以下を追加します。

extends:
  - standard
  - plugin:react/recommended
  - prettier # ←追加
plugins:
  - react
  - prettier # ←追加
rules:
  react/prop-types:
    - 0
  # ↓追加
  prettier/prettier:
    - error
  # ↑追加
settings:
  react:
    version: 16.6 # お使いのReactのバージョン

eslint-plugin-prettierの方はESLintのルールとしてPrettierのルールを読み込んで処理するプラグインで、eslint-config-prettierはESLintで設定されているルールとPrettierのルールが重複しないようにするためのプラグインです。

これでESLintとPrettierの設定の統合までできました。

グローバル変数などの対応

このままだと、windowjQuery$などのグローバル変数などが未定義の変数として怒られてしまうので、それらを除外するための設定を.eslintrc.ymlに追加します。

extends:
  - standard
  - plugin:react/recommended
  - prettier
plugins:
  - react
  - prettier
# ↓追加
env:
  browser: true
  jquery: true
globals:
  googletag: true
  ga: true
# ↑追加
rules:
  react/prop-types:
    - 0
  prettier/prettier:
    - error
settings:
  react:
    version: 16.6

envbrowser: truewindowなどが、jquery: truejQuery$が怒られなくなります。 globalsにはその他に除外したいグローバル変数を追加すると怒られなくなります。今回はGoogle AnalyticsGoogle Adsenseなどのグローバル変数が定義されているので、それらを除外しました。

これでESLintはすべての設定が終わりました。

npm scriptを設定する

後はnpm scriptでリントを実行できるようにします。

package.jsonscriptsに以下のように記述します。

"scripts": {
  "eslint": "eslint -c .eslintrc.yml --color './src/**/*.{js,jsx}'",
  "eslint:fix": "eslint -c .eslintrc.yml --color './src/**/*.{js,jsx}' --fix"
}

これでyarn eslintでESLint実行、yarn eslint:fixでESLint実行と自動フォーマットができるようになりました。

webpackビルド時にESLintを実行するようにする

webpackを使っている場合は、ビルド時にESLintを実行させることも可能です。そのためにはeslint-loaderをインストールします。

$ yarn add -D eslint-loader

そしたらwebpackの設定ファイルに追加します。

module.export = {

  ...

  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        options: {
          fix: true,
          configFile: '.eslintrc.yml',
        },
      },
  ...
}

上記のように、loaderの設定を追加するだけで、ビルド時にESLintを実行してくれます。enforce: 'pre'を指定することで、webpackビルド前に実行してくれるようになります。optionsfix: trueを指定して、自動フォーマットまでするようにしたかったのですが、うまく機能してくれてません。。。公式を見る限りではこれだけでやってくれそうな感じなのですが。。。原因がわかる方いらっしゃったら教えていただきたいです。

エディターの設定

後はお使いのエディターのプラグインに、ESLint用のものがおそらくあるかと思いますので、それをインストールすれば、自動で設定ファイルを読み込んでコーディング時にリアルタイムでリントしてくれるようになると思います。ファイル保存時にフォーマットを実行するオプションを使えば、さらに快適にコーディングできると思います。ちなみに私はVS Codeを使用しています。

SCSSのリント

SCSSのリントには、stylelintを選びました。FacebookGitHubWordPressでも使用されており、豊富なルールが用意されていて、導入事例も増えているように思います。何よりもESLint同様自動フォーマットオプションがついているのが大変ありがたいです。

導入

stylelint本体と、公式でも取り上げられているプリセットルール、そしてSCSS用のプラグインを入れます。

$ yarn add -D stylelint stylelint-config-standard stylelint-scss

stylelint-config-standardの他にstylelint-config-recommendedもありますが、すごくざっくり言うと後者は最小限のルールのみで、前者がstylelint-config-recommendedを拡張したもので、よりかっちりしたルールといった感じでしょうか。今回は詳細なルールを決めてより統一感のあるコードにしたかったので、stylelint-config-standardを選択しました。

インストールしたら、ESLint同様、.stylelintrc.ymlファイルを作成し、以下のように記述します。

plugins:
  - stylelint-scss
extends:
  - stylelint-config-standard
syntax:
  - scss
rules:
  at-rule-no-unknown: null
  scss/at-rule-no-unknown: true

SCSSの@extendなどの機能を使う場合、at-rule-no-unknownは対応しておらず怒られてしまうため無効にしないといけません。代わりにscss/at-rule-no-unkwonを有効にすることで対応できます。

次に、プロパティのソート順をリントしてくれるプラグインを入れます。今回はstylelint-config-recess-orderを入れました。これはstylelint-orderをベースに、機能単位でのソート順が定義されています。

$ yarn add -D stylelint-config-recess-order

インストールしたら、.stylelintrc.ymlに追加します。

plugins:
  - stylelint-scss
extends:
  - stylelint-config-standard
  - stylelint-config-recess-order # ←追加
syntax:
  - scss
rules:
  at-rule-no-unknown: null
  scss/at-rule-no-unknown: true

また、ままのてでは上記の他にもまだ定義できるルールがいくつかあるので、そちらも定義しています。

plugins:
  - stylelint-scss
extends:
  - stylelint-config-standard
  - stylelint-config-recess-order
syntax:
  - scss
rules:
  at-rule-no-unknown: null
  scss/at-rule-no-unknown: true
  at-rule-no-vendor-prefix: true # @ルールのベンタープリフィックス禁止
  font-family-name-quotes: always-where-recommended # 'font-family'はスペースで区切られたフォント名の場合クオートで囲む
  font-weight-notation: named-where-possible # 'font-weight'はnormalなどのキーワードが使える場合はそちらを使う(400はNG)
  function-url-quotes: always # 'url()'の引数はクオートで囲む
  media-feature-name-no-vendor-prefix: true # '@media'内のベンダープリフィックス禁止
  no-descending-specificity: null # 詳細度の高いセレクタより後に詳細度の低いセレクタを定義するのを許容
  property-no-vendor-prefix: true # プロパティのベンダープリフィックス禁止
  selector-attribute-quotes: never # '[type=text]'などのセレクタの属性はクオートで囲まない
  selector-no-vendor-prefix: true # セレクタのベンダープリフィックス禁止
  string-quotes: single # 文字列はシングルクオートで囲む
  value-no-vendor-prefix: true # 値のベンダープリフィックス禁止

no-descending-specificityについては、オンにしている場合にSCSSで開発していると、たとえば以下のような場合このルールに引っかかります。

textarea {
  width: 100%;

  &:focus {
    outline: none;
  }
}

どうやらこれはtextarea:focusの方が先に評価されるらしく、このルールに引っかかるようです。そのため、このルールはオフにしています。

ベンダープリフィックス系はautoprefixerを使っているのですべて禁止するようにしています。

ここまでできたら、npm scriptでリント実行できるようにします。package.jsonに以下のように記述します。

"scripts": {
  "stylelint": "stylelint --config .stylelintrc.yml './src/**/*.scss'",
  "stylelint:fix": "stylelint --config .stylelintrc.yml './src/**/*.scss' --fix"
}

これでyarn stylelintでリント実行、yarn stylelint:fixでリント実行と自動フォーマットができるようになります。

webpackビルド時にstylelintを実行するようにする

ESLint同様、stylelintもwebpackビルド時に実行するためのプラグインがあるので、そちらをインストールします。

$ yarn add -D stylelint-webpack-plugin

インストールしたら、webpackの設定ファイルのpluginsに以下のように追加します。

const StyleLintPlugin = require('stylelint-webpack-plugin')

module.export = {

  ...

  plugins: [
    ...

    new StyleLintPlugin({
      configFile: '.stylelintrc.yml',
    }),
  ],

  ...
}

これでwebpackビルド時にstylelintも実行されるようになりました。

エディターの設定

こちらもESLint同様、エディター用のプラグインが用意されているかと思いますので、そちらをインストールすればコーディング時にリアルタイムでリントしてくれるようになります。

まとめ

ESLintとstylelintをまた導入したばかりですが、確実に快適に開発できるようになって、生産性も上がっていると思います。万が一リントのエラーを見逃してコミットしても、ままのてではCIでリントを実行するように設定してあるので、そちらで拾ってくれるようになっています。実装者の負担も減るし、誰が書いてもスタイルの差が生じにくくなったと思います。

やはり快適に気持ちよく開発できる環境は大事だと改めて実感しました!

We're hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さいませ! エンジニアの方、ぜひ情報交換しましょう!

サーバーサイド

www.wantedly.com

ネイティブアプリ

www.wantedly.com

大学職員を半年で辞めて未経験でエンジニア始めました

はじめまして、株式会社Cluexのエンジニアの河原畑です。 昨年11月に入社して2ヶ月ほど経ったところで、自己紹介の記事を書かせていただきます。 入社してまだ日が浅いので、入社に至る経緯をメインに書いていきます。

Cluexで働く前のこと

学生時代は京都の大学の文学部で、西洋古典学を勉強しておりました。西洋古典学というのは2000年以上前の古代ギリシア・ローマの文学を研究対象とする学問で、私は主にラテン語で書かれたラテン詩を専門にしていました。修士課程まで進みましたが、自分はラテン文学を研究するというよりも、ただ読むのが純粋に楽しいという気持ちがあり、博士課程には進みませんでした。修士2回生で就活するに当たって、研究室に自分より優秀な多くの人たちがいたこともあり、優秀な研究者を支えたいという気持ちがあったこと、その上で仕事は早めに帰って趣味を充実させる生活をしたいと思っていたので、大学職員に絞って就活をしました。ラテン語も続けたいし、ドラムも叩きたい。またゲームが好きで、当時UnityとC#を独学して簡単なゲームを作ったりしていました。これも楽しかったので働きながらも続けたい。そのためには、確実に17時で帰れる仕事はとても都合が良かったのです。

運良く私大職員に内定をいただき、昨年4月から働き始めました。

目論見通りに、仕事が9時17時で済む生活が待っていました。初めはとても快適に働き、充実した生活を送っていましたが、徐々にレガシーな環境にもどかしさを感じるようになりました。新しいことを学びたい。最先端のスキルを身に付けたい。できれば自分の手で何か作れる仕事がしたい。気付いた時には、電車の中でも昼休み中も家でも狂ったようにプログラミングを勉強していました。大学の情報学科に入り直そうかと一瞬考えたりして、いろんな人に相談しました。

Cluexに入社するきっかけ

大学職員をやりながら「来世はエンジニアをやりたい」と言っていたのが、だんだん「今世でもいいのかな」と思うようになりました。昨年の夏頃から、第二新卒枠で未経験でエンジニアとして採用してくれる会社を探し始めました。その過程で、学生時代の先輩でエンジニアをやっている方に相談したところ、その先輩のお知り合いのツテでCluexを紹介していただきました。ITによって教育格差を無くすという理念に強く惹かれました。

Cluexの方々とお話をさせていただいた際に、文系卒でも大丈夫か、未経験でも大丈夫かなど、本当に色々な不安をぶちまけましたが、一つ一つ丁寧に不安を解きほぐしていただきました。また、オフィスの雰囲気がとても良いことも印象的でした。ここなら優秀な方々に囲まれて楽しく働き、スキルを身に付けて成長できるのではないかと考え、Cluexで働きたいと思いました。ご縁があって、昨年11月から入社することが決まりました。

Cluexに入社してからのこと

しばらくはインプットに専念することになっていて、高橋征義・後藤裕蔵著『たのしいRuby』(SB Creative)、竹馬力著『たった1日で基本が身に付く!Ruby on Rails 超入門』(技術評論社)、五十嵐邦明・松岡浩平著『ゼロからわかるRuby超入門』(技術評論社)、山田祥寛著『Ruby on Rails 5 アプリケーションプログラミング』(技術評論社)などの書籍や、「Ruby on Railsチュートリアル」を使ってRuby on Railsの学習をしているところです。GitやDockerにも少しずつ慣れてきました。まだまだわからないことがたくさんありますが、毎日疑問点を先輩方に質問できるため、独学よりもはるかに効率的に学べ、モチベーションも高く維持することができています。前職と勤務時間はあまり変わりませんが、今は1日があっという間に感じてしまうほど充実しています。(もちろん趣味の時間も取れています)

まずは業務に関われるレベルになることを直近の目標とし、今はひたすら目の前のことをこなそうと思います。中期的には、プロジェクトを引っ張っていく力のあるエンジニアとなって、より良いプロダクトを作ること、そしてより良いチームを築くことに貢献できるようになることを目指します。長期的には、世の中に良い影響を与えられるもの・面白いものを自分の手でイチから作れるエンジニアになりたいです。

Dependabotを導入して依存パッケージやDockerfileのベースイメージのアップデートを定期実行するようにしました

こんにちは、Webチームの柴山(@shikeapp0909)です。

今回は、普通に開発をしているとついつい後回しになりがちな、Gemなどの依存パッケージのアップデートをどのようにしているのかをご紹介しようと思います。

背景

実のところ数ヶ月前まではスケジュールに余裕ができたら、ある程度工数を確保して誰かがアップデートを実施するという感じでやっていました。 しかし実際そのように工数を確保するのはなかなか難しく、サービス立ち上げ当初からアップデートできていないところも一部あるという状態でした。 そこで定期的にアップデートを実施するようにビジネスサイドと調整して体制を整えるなどしてあまりバージョンが大きくビハインドしないようにしたいと考えていました。

そんな時に、こちらの記事を見つけて、Dependabotを導入して定期的に更新チェックを自動で行い、その都度対応が必要なものはチーム内で調整してスプリントに組み込んで対応すると良さそうだと思いました。 他にもGreenkeeperやTachikomaなどのサービスや、CIで定期的に実行するスクリプトを用意するなどの選択肢があるかと思いますが、Dockerfileのベースイメージの更新まで面倒見てくれて、設定も簡単そうということでDependabotに軍配が上がりました。

更新したいもの

アップデートの対象となるのは以下のものです。

  • Gem
  • npm
  • Dockerfile
    • ruby
    • node
    • nginx
    • go

Dependabotだとこれらすべてを一手に賄えるのでとても良かったです。

導入手順

GitHubを使っている場合はGitHub MarketplaceのDependabotからインストールして、対象のリポジトリを登録します。 ちなみに料金プランはこのようになっています。

f:id:cluex-developers:20181217173555p:plain
料金プラン

インストールしたらこちらの画面から更新チェックする対象を設定します。

f:id:cluex-developers:20181217173652p:plain
Dependabotの設定

対象によって設定できる項目に多少の違いはありますが、基本は以下のような項目が設定できます。

  • Update schedule
    • Daily
    • Weekly
    • Monthly
  • Directory
  • Target branch
  • Automatic PR merging

今回はひとまず月に一度の実施にしてみて様子をみてみようということで、スケジュールはMonthlyにしています。

ここまで設定すれば、あとは自動で登録したリポジトリにPRを作ってくれます。筆者もやっていてとても簡単にできました。

プルリクエスト作成

実際にDependabotが作成したPRはこのようになっています。

f:id:cluex-developers:20181217173729p:plain
GemアップデートPR

f:id:cluex-developers:20181217173755p:plain
npmアップデートPR

f:id:cluex-developers:20181217173816p:plain
DockerfileアップデートPR

ライブラリごとにPRを作成してくれるようになっています。入れてるライブラリの依存パッケージのアップデートまで見てくれます。物によってはリリースノートやアップデートに含まれるコミットログも記載してくれます。

一度に作成するPRの数なども設定で変えられます。また、PRをマージせずに放置しておいても、同じライブラリの新しいバージョンが出ていれば次回の実行時に古いPRは自動でクローズしてくれます。

実際に導入してみて

実際に導入して1か月半くらいになりますが、導入したおかげでRubyは2.3.3から2.5.3に、Nodeは9.11.1から11.1.0に、webpackは3.8.1から4.23.1にアップデートするなど各言語、ライブラリのかなりの部分を最新のバージョンにあげることができました。

何よりPRを作成してくれることによってアップデートしなきゃいけないと意識を高めてくれるので、チームの中でもアップデートに対するモチベーションが高くなったように感じます。

しかしアップデートがすべて終わったわけではなく、自分たちの対応できるリソースよりも作成されるPRの量の方が超過してしまうという問題もあり、一度に作成するPRの量を調整したりしてうまくバランスをとって継続してアップデートを実施していきたいと思います。

We're hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さいませ! エンジニアの方、ぜひ情報交換しましょう!

サーバーサイド

www.wantedly.com

ネイティブアプリ

www.wantedly.com

RailsコードをGoで書き直して、FFIを使ってRailsからGoの関数を実行させて実行時間を5%以下に短縮させた

こんにちは、Webチームの柴山(@shikeapp0909)です。

今回は、弊社Webアプリ「mamanoko」の一部に処理高速化のためにGoを導入したので、どのように導入したかをご紹介しようと思います。

Railsで開発しているWebアプリ内にどのようにGoを取り入れたのか、実際のコード例を交えながら解説していきたいと思います。

なぜRails環境にGoを導入したのか

そもそもなぜGoを導入することになったのかと疑問に思われるかと思います。

mamanokoはメディアサービスのため、ライターさんに文章を書いていただいています。複数のライターさんがおり、文章量も膨大になるため、表記揺れや誤字がどうしても出てきてしまいます。

そのため、文章校正が必要であり、以前からその機能を提供していました。

しかしこの文章校正の機能は記事の文章に対して表記揺れなどチェックしたい単語を総なめする処理をしているため、パフォーマンスがかなり悪かったです。記事の文字数にもよりますが、だいたい1分くらい処理に時間を要していました。また負荷も大きいため、実行中にサーバが落ちてサービスダウンするといったことも懸念されていたため、文章校正機能をピークタイムには実行できず、最大同時実行数は4までという制限も設けていました。

しかしメディアという特性上、少しでも早く、1本でも多く記事を公開してユーザに情報を提供することが事業KPI的にも求められるため、文章校正が使えずライターさんの業務効率が悪くなる、執筆・編集スピードが落ちるといったことは望ましくない状況でした。

そこで文章校正機能の高速化を図るための施策として上がったのが、Goの関数をShared Libraryとしてビルドし、FFIを使ってRailsから実行するというものでした。

追記(2018/07/02)

想像以上に多くの方に読んでいただき、コメントまでたくさんいただきました。当記事を読んで下さった方、コメントして下さった方、ありがとうございました。今後の執筆の励みになります。

ここまでの反響を呼ぶとは思っていなかったため、この記事を新しく読む方に誤解を与えてしまわないように追記を残させていただきます。

今回は実装を行う上でマイクロサービス化や別サーバーを立てるといったことも当初検討していましたが、大掛かりな実装になる可能性があり、時間的な制約がある中で可能な限りスモールな実装でパフォーマンス改善を行いたいと当初考えていたので、やってみて効果が期待できそうならそれで進めようと試験的な取り組みから始めたという経緯があります。結果的にプロトタイプ作成の段階で期待以上の効果があったのでそのまま実装を進め、リリースしました。

しかし、皆さんのコメントから筆者もRubyまたはRailsの範囲内で、アルゴリズムの最適化等で高速化を図れることが理想なのではないかと感じました。また、RubyGolangアルゴリズムに対する理解が少なかったのも事実です。

そういう背景もあり、本記事はあくまで一つの事例として捉えていただけると幸いです。

この記事を公開して、幸いにも多く方からとても勉強になるアドバイスをいただくことが出来ました。 皆様から頂いたアドバイスを元に、今後RubyGolangアルゴリズムの理解を深めて実装を見直すというアプローチを取っていければと考えております。

アドバイスをくださった皆さまに重ねて感謝申し上げます。ありがとうございました。

前置きが長くなりましたが、次項から実際にどのように導入したのかについて解説します。

Rails環境でどのようにGoの関数を実行するか

先にも挙げた通り、FFIという機能を使って実現しています。FFIとは、Foreign Function Interfaceの略です。詳しくはWikipediaをご覧ください。超ざっくり簡単にいうと別の言語で実装された関数を実行する機能です。

RubyにはRuby FFIというGemがあるので、そちらを使用しました。このGemを使って、RubyからCの関数を実行することができます。

簡単なサンプルコードで解説します。

Ruby

まずffiをインストールします。

$ gem install ffi

そして以下のようにしてCの関数を実行します。

require 'ffi'

module Sum
  extend FFI::Library
  ffi_lib 'sum.so'
  attach_function :sum, [:int, :int], :int
end

pp Sum.sum(1, 2)

これでsum.sosum関数を実行するようになります。

Go側

package main

import "C"

// export sum
func sum(a int, b int) int {
    return a + b
}

func main() {}

mainには何も書きません。代わりにFFIで実行したい関数の上に// export 関数名とコメントを書きます。

そして、GoのコードをCのShared Libraryとしてビルドするわけですが、Goのコマンドを以下のように実行するだけです。

$ go build -buildmode=c-shared -o sum.so sum.go

実行するとsum.goコンパイルしてsum.soというファイルがビルドされます。

これでRubyからGoの関数を実行することができます。

以下参考記事です。

c7.se

qiita.com

mamanokoでの実装例

ではここからは実際にmamanokoでどういう実装をしたかをご紹介します。

まず機能的な要求からおさらいすると、以下のようになります。

1. 校正対象の文章をDBから抽出
2. チェックする単語とチェックから除外する単語をDBから抽出
3. 除外単語を除き、校正対象の文章にチェック対象の単語が存在するか精査
4. チェックに引っかかった箇所をハイライトして表示

ここで除外する単語が出てくるのは、例えば「時」という表記は「とき」にするといった場合に、「時間」など「時」が入る単語でチェック対象からは除外したい単語が存在するからです。

ここの3番に該当する処理が相当重たくボトルネックになっていたので、ここをGoでリプレースするのが今回の目的になります。

データはGo側でDBに接続して取得してもよかったのですが、ORMがActiveRecordとの二重管理になるのは避けたかったため、ActiveRecordで取得してシリアライズしてGo側に渡すことにしました。 ここで、Go側で受け取る時の型について1番詰まりました。

結論から言うと、Railsからはstringとして渡すのですが、Go側ではstringでは受け取ることができず、*C.char型で受け取る必要がありました。 いまだになぜstringで受け取れないのかわかってないのですが、Cにコンパイルしているからなんでしょうか。

jsonのstringを渡すわけですが、Go側で*C.charからstringにキャストしなければなりません。それには、C.GoString(articleJSON)とします。

さらに今度はstringにキャストしたJSON文字列をstruct構造体にマッピングする必要があるので、json.Unmarshal([]byte(C.GoString(articleJSON)), &article)となります。

Cのchar型からGoのstringにキャストして、さらにバイトコードにキャストして、構造体にマッピングさせる。ややこしいですね。ここ最近暗黙型変換の世界にずっといたので、明示的にキャストさせるのがとても面倒に思いました・・・

そして色々とごにょごにょした結果を最終的にjsonにしてRails側に戻してやるのですが、ここでもまたキャストが必要になります。構造体 -> []byte -> string -> *C.charという流れになります。

コードにするとこうです。

resultJSON, err := json.Marshal(result)
if err != nil {
    log.Fatalf("result json encode error : %s\n", err)
}

return C.CString(string(resultJSON))

これでFFIRails・Go間の値の受け渡しがひとまずできるようになりました。

それではここまでをコードでまとめます。

まずRails側は、lib配下にlib/ffi/textlint.rbとして作りました。

require 'ffi'

class FFI::Textlint
  extend FFI::Library

  ffi_lib 'lib/ffi/bin/textlint.so'

  attach_function :textlint, [:string, :string], :string
end

そしてGo側です。lib配下にsrcbinディレクトリを切り、GOPATHを設定してRailsと同じプロジェクト内で開発しました。ビルドしたバイナリをbin配下に吐き出すようにしています。

構造体はstructsとパッケージを分けていますが、定義についてはここでは割愛させていただきます。

package main

import (
    "C"
    "encoding/json"
    "ffi/app/struct"
    "log"
    "string"
)

var dictionaries []structs.Dictionary

//export textlint
func textlint(articleJSON *C.char, dictionariesJSON *C.char) *C.char {
    article := new(structs.Article)
    articleErr := json.Unmarshal([]byte(C.GoString(articleJSON)), &article)
    if articleErr != nil {
        log.Fatalf("article json parse error : %s\n", articleErr)
    }

    dictionariesErr := json.Unmarshal([]byte(C.GoString(dictionariesJSON)), &dictionaries)
    if dictionariesErr != nil {
        log.Fatalf("dictionaries json parse error : %s\n", dictionariesErr)
    }

    result := new(structs.Result)

    // lint実施...

    resultJSON, err := json.Marshal(result)
    if err != nil {
        log.Fatalf("result json encode error : %s\n", err)
    }

    return C.CString(string(resultJSON))
}

func main() {}

これでRails側からGoに好きなデータを渡してあげて、あとはGoの方でゴリゴリ処理を実行することができるようになりました。

FFIを使った時のパラメータの型については、以下の資料が参考になりました。

www.slideshare.net

実際のパフォーマンスへの影響

RailsのコードをGoにリプレイスすることでどれだけパフォーマンスが向上したのか気になるかと思います。

記事の文字数に依存するため一概には言えませんが、だいたい以下の通りだと思います。

Before
45~60sec

After
1~3sec

計測はChromeのDevツールのNetworkでサーバリクエストのレスポンスタイムを指標にしました。

めちゃくちゃ速くなりました。サーバへの負荷も軽減され、ピークタイムには使用できなかったものが24時間いつでも使えるようになりました。

今の所リリースから4ヶ月くらい経ちますが、一度もサービスダウンもエラーも発生していません。滞っていたライターさんたちの校正作業も捗るようになったらしく、めちゃくちゃ感謝されました。やはりエンジニアとしてユーザから感謝される、ユーザに喜んでもらうというのが一番嬉しく、モチベーションになりますね。

Mamanoko Tips

最後に文章校正機能の実装の一部をご紹介しようと思います。

文章に対して単語のチェックをする処理からまず見ていきます。

文章のstringから該当する単語のindexのsliceをとれば良さそうと考えましたが、stringパッケージにそれらしい関数が見当たらなかったので、regexパッケージのFindAllStringIndexを使うことにしました。しかしこいつが曲者で、stringのindexではなく、文字のバイトの開始位置と終了位置のsliceのsliceを返します。つまり返り値は[][]byteとなっています。

本当はregexはパフォーマンス的にあまり使わない方が良いらしいので避けたかったのですが、致し方なくこのようにしました。他に良い方法があればご教示いただきたいです。

コードにするとこうです。dictionary.Wordが校正対象単語で、textが文章です。

dictionaryWord := regexp.MustCompile(regexp.QuoteMeta(dictionary.Word))
highlights := dictionaryWord.FindAllStringIndex(text, -1)

さらに、ここで抽出されたものの中から、除外単語に該当する単語を除かなければなりません。

同じ要領で除外単語のindexをとります。そして、highlightsからindexがかぶるものを除外します。

var highlightBytesList [][]int
for _, highlightBytes := range highlights {
    isHighLight := true
    for _, unhighlightBytes := range unhighlights {
        if unhighlightBytes[0] <= highlightBytes[0] && highlightBytes[1] <= unhighlightBytes[1] {
            isHighLight = false
            break
        }
    }
    if isHighLight {
        highlightBytesList = append(highlightBytesList, highlightBytes)
    }
}

ここではチェック単語より除外単語の方が文字数が必ず長くなるという特性を活かして、除外単語のバイトの範囲内にチェック単語のバイトがあれば除外するようにしました。

これで、文章の中のどこに校正すべき単語が存在するかがわかりました。

最後に、校正単語の箇所をハイライト表示させます。該当単語を<span class="hightlight"></span>で囲みます。

var lintedText []string
for index, textRune := range text {
    isHighlight := false
    for _, highlightBytes := range highlightBytesList {
        if highlightBytes[0] <= index && index < highlightBytes[1] {
            isHighlight = true
            break
        }
    }

    if isHighlight {
        lintedText = append(lintedText, "<span class=\"highlight\">"+string([]rune{textRune})+"</span>")
    } else {
        lintedText = append(lintedText, string([]rune{textRune}))
    }
}

return strings.Join(lintedText[:], "")

textはstringですが、stringに対してfor rangeでループを回すと、1文字ごとのバイトの開始位置とrune文字を取得できます。

これを利用して、先ほど取得した校正対象単語のバイト位置が格納されたsliceを比較してハイライト表記するかどうかを判別します。

rune文字は[]rune{textRune}とすることで元の文字列に変換できます。

そして出来上がった[]stringをstringパッケージのJoinを使って繋げて一つの文字列にしてあげれば完成です。

所感

実は今回Go言語を触るのは初めてで、1週間のインプット期間を設けてから本機能の実装に取り掛かりました。

Go言語は言語仕様がかなりシンプルで、実装していてどの関数を使うべきか迷うことがなかったり、標準パッケージがかなり強力で大体それだけでなんとかなるというのがとても良いと思いました。フォーマッタもあるのでフォーマットを気にすることなくガリガリ実装できるのは良い開発体験につながりました。

ポインタのところが自分には難しく、まだ理解しきれていないですが、それでもすんなり入ることができて、Go言語好きになりました。

しかしRubyRailsなど高級多機能な言語、FWから移ってくると「え、この関数ないんか」といった事態に遭遇して自分で実装することになるといったことが多くて大変そうです・・・

ともあれ今回の目的であった文章校正機能の高速化はGoでリプレースすることで見事達成することができました。しかしまさかここまでパフォーマンス向上するとは自分でも驚きです。

We're Hiring!!

弊社ではWeb / ネイティブエンジニアを募集しております。

ご興味がありましたらお気軽にご連絡ください!

www.wantedly.com

React環境にReduxを導入して秩序をもたらした話

初めまして、Webチームのエンジニアの柴山(@shikeapp0909)です。 2017年11月に入社し、Webチームにアサインされ、子育て情報サービスmamanokoの開発をしています。

今回は、今年の2月にmamanokoにReduxを導入した際の考慮したことや設計などについて書きます。

導入前の状態

Reduxを導入する前は素のReactだけで実装していました。 stateの複数コンポーネントでの参照・更新をなるべく避け、stateを渡すのではなくコンポーネントをモノリシックにしてバケツリレーしないようにさせたりすることでなんとか回避している状態でした。

とは言ってもやはりコンポーネントはどんどん肥大化し、コンポーネントの中に複雑な処理が混ざり込み、とてもリーダブル・メンテナブルとは言い難い状態となっていました。

また、実装者によって実装の仕方も違ってきてしまい、どうすべきといった明確な基準がないためコードレビューも難しくなり、品質の担保が困難になっていました。

そこでReduxを導入して複雑化し保守性が低くなっている問題を解決しようということになりました。

導入手順

  • Reduxの公式リファレンスを読む
  • Reduxの事例を調査し、ベストプラクティスを模索し設計
  • 実装してリリース

Reduxの公式リファレンスを読む

まずはやはりReduxについてちゃんと理解しておく必要があるので、公式のリファレンスを熟読しました。 Reduxの元となっているFluxも簡単にではありますが公式を読みました。 Reduxに限った話ではありませんが、このライブラリがどのような問題を解決するために生まれたのか、何をもたらしてくれるのかをしっかり理解しておくことが大事だと思います。

Fluxは、Reactで実装するにあたって問題となる至る所で参照・更新され、管理が煩雑になるstateを、データフローを単一方向にすることで管理しやすくし、処理の見通しをよくするためにFacebookが提唱したアーキテクチャです。 FluxはFacebookが提唱したアーキテクチャの名称で、facebook/fluxはこのアーキテクチャのリファレンス実装です。こちらはReactと併用することを前提にしています。 Fluxと言う時はアーキテクチャのことを基本的に指すことが多いと思います。

ReduxはFluxをシンプルな関数のみで実装したもので、JSアプリケーションの状態管理のためのライブラリです。なので、React以外のフレームワーク/ライブラリでも、もちろん素のJSとも組み合わせて使えます。

詳しいことに関しては、公式を読むか、既に丁寧に解説してくださっている記事がたくさんあるので、そちらを読んでください。

mae.chab.in

qiita.com

Reduxの事例を調査し、ベストプラクティスを模索し設計

Reduxについて理解ができたところで、実際に他社さんのサービスでどのように使われているのかなどの事例を調査し、どうやって実装するのが一番良いのかを模索しました。 特に気になったのは、ディレクトリ構成Middlewareをどうしているかです。

また、弊社アプリはSPAではなく、サーバサイドで生成したビューの一部をReact on RailsでReact化してSSRしています。そのため、SSRの事例を特に調査しました。

ディレクトリ構成

ディレクトリ構成に関しては、actionreducermiddlewareなどをそれぞれディレクトリを分けるか、ドメインでひとまとめにしてしまうかを考えました。 ちなみにドメインactionreducerをひとまとめにするパターンはDucksといいます。基本的にactionは特定のreducerに紐づくことが多く、その場合actionreducerをそれぞれ別のディレクトリ、別のファイルに分ける必要がないため、このパターンは複雑性を回避するには有効な手段だと思います。

しかし、今回は弊社アプリはSPAではなくSSRのまたそのページの一部分でのみReactを使用しているため、既に関心ごとによってディレクトリが分けられている状態でした。そのため、全てをひとまとめにするよりは、どれがactionで、どれがreducerかということが明確に分かった方が良いと判断し、Ducksパターンではなく、ベーシックなディレクトリ構成を取ることにしました。

最終的にこのような構成となりました。

〇〇    // add_todo_formなど部品名が入る
├── components    // 純粋なReactコンポーネント
│   ├── 〇〇.jsx
│   └── △△.jsx
├── containers    // コンポーネントにReduxをつなぐためのクラス
│   ├── 〇〇.jsx    // ファイル名・クラス名はコンポーネントと同じ命名とする
│   └── provider.jsx    // storeとつなぐ一番親となるProviderクラスはprovider.jsxとする
└── stores    // Redux関連のコード
      ├── action_types.js    // Action名の定数群
      ├── actions.js    // Actionの定義
      ├── reducer.js    // Reducerの定義
      ├── middleware.js    // Middlewareの定義
      └── store.js    // Storeの定義

ディレクトリ構成に関してはこちらがとても参考になりました。

speakerdeck.com

Middlewareをどうするか

一番頭を悩ませたのがここです。

Redux自体が副作用を必要とする例えば非同期処理などをする手段を用意はしているものの、ほぼ別のライブラリに頼ることを前提としている感じです。公式ではMiddlewareのところでredux-thunkが登場します。正直Reduxについて理解するだけでお腹いっぱいなのに、また別のMiddlewareのライブラリについて理解しないといけない必要が出てくるのはとても大変だし、それによって初心者の方にとっては参入障壁が高く感じられるポイントではないかと思います。

redux には良い middleware が必須 - Togetterでも議論されていますが、Reduxはシンプルでピュアな反面、実サービスにおいて一番肝心な要素となるMiddlewareが完全に別のライブラリに丸投げされていて、Reduxが果たして最適解なのかという状態を生み出している気がします。ReduxがMiddlewareをもっといい感じに面倒見てくれるようにしてくれればいいのに(いい感じとは?という疑問はありつつ)とつくづく思います・・・おかげでMiddlewareライブラリが乱立していて初学者を混乱させていると思います。

事例を色々見てみると、redux-sagaを導入している方が多かったため、redux-sagaを導入しようかとも考えましたが、ES6のgeneratorを使用して作られており、自分としてはまた理解する概念が増えるのは避けたいと思いました。また、弊社のWebチームのエンジニアは皆Redux初心者でレベルもバラバラだったため、学習コストはなるべく低く抑えたいと思い、redux-sagaの導入は取りやめました。

そこで公式のMiddlewareの章を3、4回くらい熟読し、そもそものMiddlewareに対する理解を深めることにしました。そしてライブラリに頼らず、Middlewareは素の状態で書いたらどうかと思い試してみたところ、思いの外難しくなく、多少記述が多く面倒な程度だったため、これで進めることにしました。

詳しい実装については後述します。

SSR

SSRに関しては、こちらの記事がとても参考になりました。

qiita.com

また、React On Railsの公式ドキュメントを読み、公式のサンプルを参考にして、どのようにRailsからReact + Reduxに繋ぎ込むのかを考えました。

React On RailsがReduxもサポートしてくれているおかげで多少設定のための処理を加えるだけで、Railsで扱うオブジェクトをそのままReduxのStoreに直接渡すことができたのでとても助かりました。

こちらも詳しい実装については後述します。

実装

調査と設計が終わったところで実装に移ります。 ここではReduxの登場人物ごとの基本的な実装方針について紹介します。

※便宜上TODOアプリ風な実装例を紹介します。

Action

まずAction Typeについてはお決まりで例のごとく以下のようにしています。

export const FETCH_TODO_REQUESTED = 'FETCH_TODO_REQUESTED';
export const FETCH_TODO_SUCCESS = 'FETCH_TODO_SUCCESS';
export const FETCH_TODO_FAILURE = 'FETCH_TODO_FAILURE';

そしてActionについては、Flux Standard Action(以下FSA)に則って実装するためにredux-actionsを使用しています。 FSAに則ることで、チームで開発する際に人による実装のズレを最大限無くしています。 FSAでは、Actionは必ず下記のように、typepayloadmeta、そしてerrorの4つのプロパティのみを使用することを要求しています。

{
  type: 'FETCH_TODO_REQUESTED',
  payload: {},
  meta: {},
  error: false
}

redux-actionsを使用することで、下記のように実装することで簡単にFSAに則ったActionを実装できます。

import {createAction}   from 'redux-actions';
import * as actionTypes from './action_types';

export const fetchTodoRequested = createAction(actionTypes.FETCH_TODO_REQUESTED);
export const fetchTodoSuccess = createAction(actionTypes.FETCH_TODO_SUCCESS);
export const fetchTodoFailure = createAction(actionTypes.FETCH_TODO_FAILURE);

これで、アクションを実行した際にredux-actionsが勝手に上述したJSONの形式でActionを生成してくれます。

さらに、このアクションにErrorオブジェクトを引数として渡すことで、redux-actionsが勝手にerrortrueにした状態のActionを生成してくれます。

これによって、例えばaxiosを使用する際に、エラーハンドリングが簡単になります。

axios.get('~~~')
.then(response => store.dispatch(actions.fetchTodoSuccess({todos: response.data})))
.catch(error => store.dispatch(actions.fetchTodoFailure(error)));    // errorはErrorオブジェクトなので、そのままActionに渡すだけ

Reducer

Reducerもredux-actionsを使って実装しています。

import {handleActions} from 'redux-actions';
import * as actionTypes from './action_types';

const initialState = {
  todos: null,
  isLoading: false,
  error: null
};

export default handleActions({
  [actionTypes.FETCH_TODO_REQUESTED]: state => ({
    ...state,
    isLoading: true
  }),
  [actionTypes.FETCH_TODO_SUCCESS]: (state, action) => ({
    ...state,
    todos: action.payload.todos,
    isLoading: false
  }),
  [actionTypes.FETCH_TODO_FAILURE]: state => ({
    ...state,
    isLoading: false,
    error: action.payload.response.data
  })
}, initialState);

switch文で記述するところを、redux-actionsのhandleActionsを使うことでJSONで記述できるようになります。

Middleware

Middlewareは前述の通りで、特にライブラリに頼らず、素の状態で実装しています。

import * as actions     from './actions';
import * as actionTypes from './action_types';

export default store => next => action => {
  switch (action.type) {
    case actionTypes.FETCH_TODO_REQUESTED:
      next(action);

      axios.get('~~~')
      .then(response => store.dispatch(actions.fetchTodoSuccess({todos: response.data})))
      .catch(error => store.dispatch(actions.fetchTodoFailure(error)));

      break;
    case actionTypes.FETCH_TODO_SUCCESS:
      next(action);

      ...

      break;

    ...

    default:
      next(action);

      break;
  }
}

このように、意外と難しくもなく、switch文の記述が多少面倒なくらいなので、素の状態でも十分使えます。

Store

React On RailsでのStoreは通常のReduxと少し実装の仕方が異なります。 React On RailsではStoreをpropsとlocation(option)を引数にとり、storeを返す関数として実装するように要求しています。 React On Railsのドキュメントの「Multiple React Components on a Page with One Store 」の所に書いてあります。 コードは下記の通りです。

import {createStore, combineReducers, compose, applyMiddleware} from 'redux';
import todoReducer from './reducer';
import todoMiddleware from './middleware';

const composeEnhancer = process.env.NODE_ENV === 'production'
  ? compose
  : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export default props => {
  const reducers = combineReducers({
    todoReducer
  });
  const enhancer = composeEnhancer(
    applyMiddleware(todoMiddleware)
  );
  return createStore(reducers, props, enhancer);
};
const composeEnhancer = process.env.NODE_ENV === 'production'
  ? compose
  : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

ここでは本番環境以外ではChromeのDevツールからデバッグが行えるように、Redux DevTools extensionの設定を加えています。

Storeを生成したら、React On Railsへの登録を行います。

import todoStore from '~/store';

ReactOnRails.registerStore({
  todoStore
});

これでサーバ・フロント両方でStoreを使う準備が整いました。

Rails側から直接Storeに値を渡したい時には下記のようにします。

= redux_store 'todoStore', props: { todoReducer: { todos: @todos } }

※弊社アプリではテンプレートエンジンにslimを使用しています。

redux_storeの第一引数は上記でReactOnRails.registerStoreで指定したものと同じ名前をStringで指定します。なのでここではtodoStoreとなります。

第二引数はprops: {}となっており、Storeに登録したい値を渡します。 todoReducer: { todos: @todos } }としている一番上の階層のキーは、StoreでcombineReducersで指定しているReducerと同じ名前を指定します。ここではtodoReducerとなります。 そうすると初期ロード時のStoreの状態は以下のようになります。

{
  todoReducer: {
    todos: [
      'hoge',
      'fuga'
    ]
  }
}

ReactでStoreを使う際に例のごとく登場するProviderでのStoreの渡し方も少し特殊です。

import {Provider} from 'react-redux';

export default () => {
  const store = ReactOnRails.getStore('todoStore');
  return (
    <Provider store={store}>
      <Component />
    </Provider>
  );
}

ReactOnRails.getStore()でstoreを取得して、それをProvideのpropsに渡してあげます。 こちらでもReactOnRails.registerStoreで指定したものと同じ名前を指定する必要があります。なのでtodoStoreとなります。

これでようやくSSRの準備が完了しました。

Component

ComponentはPresentationalとContainerとで分けています。

Presentational Componentは状態を持たず、propsで受け取ったデータの表示とアクションの実行が役割になります。

const Todos = ({todoApp, actions}) => (
  <ul>
    {todoApp.todos.map(todo => (
      <li key={todo.id}>
        <div>
          {todo.text}
        </div>
        <button onClick={() => actions.deleteTodo({ id: todo.id })}>削除</button>
      </li>
    ))}
  </ul>
)

Container ComponentはReduxと繋いでPresentational Componentに必要なデータを流し込むのが役割となります。実装は下記のようになります。

import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import Todos from '../components/todos';
import * as actions from '../stores/actions';

const mapStateToProps = state => ({
  todoApp: state.todoReducer
});

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators(actions, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(Todos);

これで実装については簡単にではありますが全て紹介しました。少しでも実装の助けになればと思います。

その他参考資料

全体的に以下の記事を参考にさせていただきました。かなりよくまとめてくださっているので、React + Reduxで開発する時に大いに助けになると思います。

qiita.com

qiita.com

↓こちらは最近公開された記事で、実装時の参考にはしておりませんが、非常に参考になりました。

qiita.com

所感

私も今回初めてReduxを触ったのですが、やはりReactを使用していてある程度規模が大きくなった場合にReduxを使うと責務がいい感じに分けられるので複雑性が解消され、コードがとても読みやすくなり、保守性も高まると感じました。 チームで開発する際の実装の一貫性も保ちやすくなると思います。

ただ、MobXなども出てきており、Reduxよりも筋の良いライブラリが登場する可能性もまだあるのではないかと思っています。 というより、Reduxがもっと強くなってくれればとつくづく思います・・・

まだ実際にアプリケーションにはほんの一部分にしか導入できていないので、なんとも評価しきれない部分もありますが、Redux導入を進めていく中で問題が生じたらその際にまた考えようかと思います。

We're Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さい! エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

Upgraded Ruby on Rails from v4.2.4 to v5.1.4

こんにちは、エンジニアの神山です。
今年の1月に弊社アプリケーションで使用しているReactをFiber(v16)にアップグレードしました。
そしたら今度はRuby on Railsのアップグレードもすることになり、先日に無事アップグレードが完了したのでそのことについて書きました。

Railsは弊社の基盤フレームワークなので、慎重に確認と修正をしながら進めていきました。

詳細事項

  • Rubyバージョン: 2.3.3
  • Railsバージョン: 4.2.4 -> (5.0.6) -> 5.1.4
  • 実施期間: 2018年2月

手順

手順は以下の通りです。これらを追って説明していきます。

  • テストのカバレッジを上げる
  • すべてのGemのバージョンアップ
  • Railsのアップデート情報の検索
  • Railsのバージョンを5.0.6に上げる
  • Railsのバージョンを5.1.4に上げる
  • リリース

テストのカバレッジをあげる

これはGemやRailsのアップグレードをする際に、現在のコードがアップグレードしてもバグを起こさないか見つけるために行います。特に後方互換性がないアップグレードの時にとても役に立ちます。
今回のようなメジャーバージョンアップだと非推奨や使用不可になったコードが沢山出てくるのですが、テストの時点でほぼ気づくことが出来ました。

弊社は普段からテストを書くことを習慣にしていて、カバレッジは常に約95%あたりをキープしております。今回のアップグレードでも95%あれば十分であると判断したため、テストは追加で書かずにすみました。

カバレッジの目安はアプリケーションやチームによって変わってくると思うので一概には言えませんが、テストケースが十分にあるということは改めて大切だと思いました。

すべてのGemのバージョンアップ

一般的にGemはRubyRailsのバージョンがあがると追随してアップグレードされます。そのためRails5系の環境に沿ったGemを使用するべく、全てのGemのアップグレードも行います。

ただ注意として、そのGemの開発者がアップグレードに追随していないこともあります。するとRails5系で使いたいのに使えない、Rails4系までしか動かないとかも起こります。

そのときは同じ機能を持つGemで開発が活発なものを探すか、自分でパッチをあてないといけません。ただパッチをあてるとその瞬間から自分たちでそのGemを管理していかないといけなくなるので、注意と覚悟が必要です。
そのため信頼できる別のGemを探すほうがアプリケーションの開発に集中できる状態を保てるので、入れ替えのコストはかかりますが適切な選択だと思います。

ちなみにRefileというGemを使用しているのですが、アップグレードに追随していませんでした。この記事の下に詳しく書いておきます。

Railsのアップグレード情報の検索

アップグレードの情報を得るため、ドキュメントを予め読んでおくのは大切です。何かエラーが出たときに解決がスムーズになります。そのため変更点をまとめておくといいと思います。

私は以下のドキュメントを読みました。

Rails公式のリリースノートとアップグレードガイドの2つは絶対に読んでおくべきだと思います。

Railsのバージョンを5.0.6に上げる

今回のアップグレードはRails5.1系がゴールですが、一度Rails5.0系にアップグレードしてからRails5.1系にアップグレードしました。

理由は以下の通りです。

  • メジャーバージョンのアップグレードは一般的に規模の大きいアップグレードで、仕様が大きく変わる可能性があり、アプリケーションが最新のメジャーバージョンに対応できるか確認するため。
  • Rails4.2系からRails5.1系に一気にあげると変更点が多くコードの変更量も多くなるので、エラーが起きたときに原因がわかりにくくなってしまうため。
  • Rails5.1系へのアップグレードが現実的に難しいとわかっても、Rails5.0系へのアップグレードは出来る可能性があるため。

Railsはマイナーバージョンのアップグレードでも大きくコードが変更されることもあるので、段階的にアップグレードすることにしました。特に4系から5系はConfig周りの変更も多いので、慎重を期したほうが良いと思います。

Railsのバージョンを5.1.4に上げる

Rails5.0.6が正常に動いたので、そこからRails5.1.4へのアップグレードを行いました。ちなみにこのアップグレードの最中にRails5.1.5がリリースされたので、一度Rails5.1.5へのアップグレードを試みました。しかし正常に動作しなかったので最終的にはRails5.1.4になりました。こちらも記事の下に起きたことを書いておきます。

Rails5.0系からRails5.1系への変更もなかなかの量があり大変でした。特にActiveRecordまわりのメソッドが非推奨になったり新しく追加されたりしたので、Modelを念入りに確認しました。

アップグレード時に大変だったこと

RefileというGemがRails5系に対応していない

Refileを使っているアプリケーションは注意が必要です。Refileのリリースブランチは2年前のものであるため、Rails4系の仕様になっております。 ただmasterブランチはRails5系の仕様になっているので、そちらを使用すれば問題ありません。

しかしRefileに関連するGemでrefile-s3refile-mini_magickを使用しているのですが、その2つで問題が起きました。
これらはRefileのアドオンのようなもので、内部でRefileを使用しております。しかし使用しているRefileはリリースブランチのバージョン(つまりRails4系)であったため、Rails5系の環境では動きませんでした。

そこで「同じ機能を持つGemで開発が活発なものに変える」か「そのGemのレポジトリをフォークしてパッチをあてて使う」の2つを考えました。

Refileはファイルアップローダーであり、同じ機能を持つGemは有名所だと以下の3つが見つかりました。

上2つは有名なGemですがShrineは初めてしりました。Refileのコアな開発者が作成したGemで、開発も盛んで現在のトレンドのようです。(細かな違いなどは比較記事がネット上にあるのでそちらを参考にしてください。)

ただ今回は「そのGemのレポジトリをフォークしてパッチをあてて使う」にしました。理由はRails5.2系からActiveStorageというファイルアップローダーの機能が搭載されるためです。
そのためRails5.2系にアップデートする段階で再度判断することにし、今回はパッチで対応しました。またあてるパッチの量も少なく済みそうであったことも理由の1つです。

ちなみにあてたパッチはGemfileのrefileの箇所です。

- gem "refile", github: "refile/refile"
+ gem 'refile', git: 'https://github.com/refile/refile', branch: 'master'

ここで注意しなくてはいけないのがRails5系との互換性です。当然ながらGemfileの中身を変更しても、Gemのコード自体が対応していないと使用することはできません。その為パッチをあてるときは、Gemの中身を一通り読んでおくべきだと思います。

Rails5.1.5が正常に動作しない

まだ原因の根源まではわかっていませんが、ActiveRecordで作るクエリの条件式に関連先のテーブルのものを使用するとエラーが起きました。

Hoge.includes(:fuga).where(fugas: { bool: true })

キャッシュの変更に悩まされる

Rails5系からActiveRecord::Relationにも.cache_keyというメソッドが実装されました。しかしActiveRecordで作るクエリの条件式に関連先のものを使用するとエラーが起きました。

Hoge.includes(:fuga).where(fugas: { bool: true}.cache_key
/ => ActiveRecord::StatementInvalid: PG::UndefinedTable

またView上で使用する際も同様のエラーになります。

- cache hoges do …
/ => ActiveRecord::StatementInvalid: PG::UndefinedTable

ちなみにActiveRecord::Relations#cache_keyは、他にも注意点があります。下のドキュメントがよくまとめられているので、一読しておくことをおすすめします。

techracho.bpsinc.jp

所感

今回のアップグレードを通して以下のことを学びました。

  • フレームワークの更新を常に把握する。
  • アップグレードはマイナーバージョンの安定版が出たら即行う。
  • テストは常に書く。

上の項目は普段から習慣付けるといいと思います。そうでないとアップグレードしようとしたときにつらい思いをすることになります。
場合によっては気づいたら最新のバージョンとコードが乖離しすぎてて、アップグレードは現実的に不可能なんてことにもなり兼ねません。

雑感ではありますが、アップグレードはなかなか出来ることでもないのでとてもいい経験になりました。

We’re Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

CSSについて本気出して考えてみた 〜CSS × Atomic Design〜

こんにちは、WEBエンジニアの神山です。

最近シェルをZshからFishに変えました。Fishについて書きたいのですが本題とそれるので一言でまとめます。
「うお!」

さて今回はAtomic Designという考え方を知りまして、それを踏まえてCSSに対する考えを書きました。
今まではBEMやSMACSSを使っていたのですが、そこに感じている悩みやAtomic DesignをもとにしたCSS設計の利点などを自分なりにまとめてみました。

正直CSSについて詳しい人ではないので正確な考察が出来ているかはわかりません。。
もし違っていたり、より良い考え方などありましたらコメントを頂けると非常に助かります。。

そもそもAtomic Designとは

Atomic Designとは、ボタンやフォームなど小さいパーツを組み合わせて、一枚のページを作っていく考え方です。
たとえば検索ページを作る際に、まず入力フォームを作り、その横に「検索」ボタンを作り、フォームの上に「〜〜を検索する」という見出しを作り、検索ページにヘッダーやフッターを配置し、、、みたいに考えていきます。

Atomic Designは、原子や分子など化学から発想を得ております。化学を勉強したことがある人は理解しやすいかもしれません。 まずボタンや入力フォームという原子があり、それらが組み合わさって検索フォームという分子になり、そういう分子がたくさん集まってページという物質が出来上がるというイメージです。

こちらがAtomic Designの考案者が解説したサイトになります。英語ですがわかりやすい図を用いて解説してあり、直感的に理解しやすいです。

bradfrost.com

Atomic Designは具体的に以下の手順でデザインを考えていきます。

  1. Atoms(原子)
    原子は、ボタンや入力フォームにあたります。
  2. Molecules(分子)
    分子は、検索フォームにあたります。
  3. Organisms(有機体)
    有機体は、ヘッダーやメインコンテンツ、サイドバー、フッターにあたります。
  4. Templates
    テンプレートはページのワイヤーフレームにあたります。
  5. Pages
    ページはワイヤーフレームにコンテンツを入れて、文字通りページとして完成したものにあたります。

こちらにそれぞれの具体例がたくさん載っているので、導入の際に参考にしてみてください。

http://demo.patternlab.io/demo.patternlab.io

CSS設計のポイント

CSS設計のポイントは以下の4つで考えております。
当たり前のことなのですが、これらをきちんと守らないと管理しづらいCSSになってしまいます。

  • 予測しやすい
  • 再利用しやすい
  • 保守しやすい
  • 拡張しやすい

これらは下のサイトを参考にさせて頂きました。

article.enja.io

現状のCSS

さて、上にあげたCSSの設計のポイントを実用的にするために、様々な設計手法が存在します。

有名なものだとSMACSSなどOOCSSをもとにしたものが広く使用されていると思います。またBEMのように少し違う側面での設計手法も存在しており、他の設計手法と組み合わせて使うことも多いと思います。

私はBEMとSMACSSをかけ合わせて使っております。どちらも好きな設計手法です。

BEMは命名をBlock、Element、Modifierで分けるというルールがシンプルでわかりやすく、このルールをきちんと守っていれば、管理しやすいCSSができあがると思います。

<div class=”article>
  <div class=”article__heading”>hoge</div>
  <img class=”article__image” />
</div><div class=”article>
  <div class=”article__heading--small”>fuga</div>
  <img class=”article__image--small” />
</div>
.article {
  &__heading {
    ...

    &--small {
      ...
    }
  }
  
  &__image {
    ...

    &--small {
      ...
    }
  }
}

またSMACSSはCSSをカテゴライズして管理する手法です。

  • Layout
    ヘッダー、メインコンテンツ、サイドバーやフッターの配置を整える
  • Module
    リストやボタンなど再利用そうなもののデザインを定義する
  • State
    クリックされたものなど状態により変化したもののデザインを定義する
  • Theme
    ページ単位のデザインを整える
  • Base
    CSSの基幹部分やタグのデフォルト値などを決める

また私はボタンやテキストのデザインを決めるとき、マルチクラスを使用しております。

/modules/button.scss

.button {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 2px;
  background-color: transparent;
  color: #fff;
  font-size: 80%;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;

  &-default {
    border: 1px solid whiten($base-color, 90%);
    background-color: #fff;
    color: whiten($base-color, 20%);
  }

  &-small {
    padding: 8px 12px;
    font-size: 70%;
  }

  &-full {
    width: 100%;
  }
}

これをHTMLでは以下のように使っております。

<div class=”article>
  <div class=”button button-default button-small”>small</div>
  <div class=”button button-default button-full”>full</div>
  ...
</div>

これでボタンのデフォルトのデザインを統一させることができ、いろいろなパターンのボタンを用意することが容易になります。

現状のCSSの悩み

たとえば警告用のボタンを作りたいときに、2通りの方法が考えられます。

一つは、ボタンクラスに要素を増やす方法。
警告用のボタンであれば、button button-warningというCSSを用意します。

もう一つは、Moduleの一部として考える方法。
ログインボタンのデザインであれば、login_form-buttonというCSSを用意します。

どちらも正解ですしチームで方針をしっかり決めれば良いことだと思います。
しかし2通りの方法があると、CSSを設計する上で管理しづらさ、わかりづらさが出てしまうと考えております。
そしてこれを強く問題視している理由は、デザインを考える視点が真逆だからです。

ボタンやテキストなどのパーツを組み合わせて使うという考え方は、小さいものから考えて大きいものを作っていくという考え方です。 ボタンやテキストを予め定めておき、何かのModuleを作るときはそれらを組み合わせてModule全体をデザインしていくイメージです。

一方、フォームの中のボタンやテキストのデザインを変えるという考え方は、大きいものから考えて小さいものを作っていくという考え方です。 何かのModuleを作るときはModule全体のデザインを考えて、それを細かく落とし込んだときにボタンやテキストが作られるイメージです。

多くの設計手法は大きいものから考えていくやり方です。しかしボタンやテキストなどのデザインを統一したいとなると、小さいものから考えていくやり方も必要になります。
つまり両方の考え方が混在してしまいます。

Atomic DesignをもとにCSSを設計

これを解決するために、Atomic Designの考えをもとにCSSを設計すべきではと考えました。上で紹介したように、Atomic Designは全てのデザインを小さいものから作っていくという考え方をしているからです。

考え方を統一すれば、ボタンの変更ならボタンを管理しているCSSを変えれば良いと明確になります。ボタンやテキストのデザインがぶれることもなくなります。 さらに何かのModuleを作るときもデザインの雰囲気が似てきて、組み合わせても違和感のないものが作られるようになると考えております。

またAtomic Designはコンポーネントを組み合わせていくというシンプルな考え方なので、CSSもシンプルになります。

まずAtomsというカテゴリーを作り、そこでボタンやテキストなどのCSSを管理します。
次ぎにMoleculesというカテゴリーを作り、ボタンやテキストなどを組み合わせて作ったコンポーネントを管理します。ここでのCSSはボタンやテキストの間隔を調整するものになります。ボタンを変更したいときはAtomsを変更するようにします。

というようにOrganismsやTemplatesというカテゴリーも作っていきます。PagesはCSSとは関係ないので必要なさそうです。

またSMACSSと比較すると、視点こそは違うものの区分けは似ているので、SMACSSからの移行も難しくはなさそうに思えます。
LayoutはOrganisms、ThemeはTemplatesに相当すると思います。Baseは相当するものがありませんが、Baseというカテゴリーは作ったほうがいいと思います。

結果的にこのようなディレクトリ構成になると思います。

stylesheets/
  |- atoms/
    |- button.scss
    |- heading.scss
    |- text.scss
  |- molecules/
    |- form.scss
    |- navigation.scss
  |- organisms/
    |- header.scss
    |- sidemenu.scss
  |- templates
    |- template.scss
  |- base/
    |- base.scss
    |- mixin.scss
  |- application.scss

APBCSS

Atomic Designを用いたCSSの設計手法にAPBCSSというのがありました。Atomic Designを用いたCSSを考える上でとても参考になりました。

ただディレクトリ構成をAtomic Designのように/atoms/moleculesとしたかったので、導入は見送りました。 ディレクトリ構成はわかりやすさが肝心だと考えております。

まとめ

私としては、Atomic Designを使用するとCSSにおいて、管理が的確になると思います。いわば関心の分離です。

ただこれが全のアプリケーションにおいて最高のCSSの設計になるとは思っておりません。アプリケーションによっては使いづらいこともあると思いますし、SMACSSやBEMでも十分に使いやすいCSSは組めると思います。あくまで一例として捉えていただければと考えております。

また一番大事なのは、アプリケーション全体でデザインが統一されるようにすることであり、それを管理しやすくするためにCSSの設計を考えるということなのかなと思います。

とりとめのない結論になってしまいましたが、CSSが幸せになればいいなと願うばかりです。

We’re Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com