フロントエンドにESLint、Prettier、stylelintを導入して快適な開発環境を整えました
こんにちは、Webチームの柴山(@shikeapp0909)です。
私たちのプロダクト「ままのて」のフロントエンドはReact + SCSSで開発しています。コード規約は存在するもののインデントやセミコロンのつけ忘れなどをいちいちコーディングの際に気をつけながら開発するのも、コードレビューでチェックして防ぐのも余計な労力で効率が悪いため、リントとコードフォーマッターを入れることにしました。
今回はその導入内容を手順と共にご紹介します。
JSのリントとフォーマッター
JSの方はESLintとPrettierでリントとフォーマットを行うようにしました。
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.yml
のrules
に追加します。
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の設定の統合までできました。
グローバル変数などの対応
このままだと、window
やjQueryの$
などのグローバル変数などが未定義の変数として怒られてしまうので、それらを除外するための設定を.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
env
のbrowser: true
でwindow
などが、jquery: true
でjQueryの$
が怒られなくなります。
globals
にはその他に除外したいグローバル変数を追加すると怒られなくなります。今回はGoogle AnalyticsやGoogle Adsenseなどのグローバル変数が定義されているので、それらを除外しました。
これでESLintはすべての設定が終わりました。
npm scriptを設定する
後はnpm scriptでリントを実行できるようにします。
package.json
のscripts
に以下のように記述します。
"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ビルド前に実行してくれるようになります。options
にfix: true
を指定して、自動フォーマットまでするようにしたかったのですが、うまく機能してくれてません。。。公式を見る限りではこれだけでやってくれそうな感じなのですが。。。原因がわかる方いらっしゃったら教えていただきたいです。
エディターの設定
後はお使いのエディターのプラグインに、ESLint用のものがおそらくあるかと思いますので、それをインストールすれば、自動で設定ファイルを読み込んでコーディング時にリアルタイムでリントしてくれるようになると思います。ファイル保存時にフォーマットを実行するオプションを使えば、さらに快適にコーディングできると思います。ちなみに私はVS Codeを使用しています。
SCSSのリント
SCSSのリントには、stylelintを選びました。FacebookやGitHub、WordPressでも使用されており、豊富なルールが用意されていて、導入事例も増えているように思います。何よりも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 / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さいませ! エンジニアの方、ぜひ情報交換しましょう!
サーバーサイド
ネイティブアプリ
大学職員を半年で辞めて未経験でエンジニア始めました
はじめまして、株式会社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からインストールして、対象のリポジトリを登録します。 ちなみに料金プランはこのようになっています。
インストールしたらこちらの画面から更新チェックする対象を設定します。
対象によって設定できる項目に多少の違いはありますが、基本は以下のような項目が設定できます。
- Update schedule
- Daily
- Weekly
- Monthly
- Directory
- Target branch
- Automatic PR merging
今回はひとまず月に一度の実施にしてみて様子をみてみようということで、スケジュールはMonthlyにしています。
ここまで設定すれば、あとは自動で登録したリポジトリにPRを作ってくれます。筆者もやっていてとても簡単にできました。
プルリクエスト作成
実際にDependabotが作成した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 / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さいませ! エンジニアの方、ぜひ情報交換しましょう!
サーバーサイド
ネイティブアプリ
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の範囲内で、アルゴリズムの最適化等で高速化を図れることが理想なのではないかと感じました。また、RubyやGolang、アルゴリズムに対する理解が少なかったのも事実です。
そういう背景もあり、本記事はあくまで一つの事例として捉えていただけると幸いです。
この記事を公開して、幸いにも多く方からとても勉強になるアドバイスをいただくことが出来ました。 皆様から頂いたアドバイスを元に、今後Ruby・Golang・アルゴリズムの理解を深めて実装を見直すというアプローチを取っていければと考えております。
アドバイスをくださった皆さまに重ねて感謝申し上げます。ありがとうございました。
前置きが長くなりましたが、次項から実際にどのように導入したのかについて解説します。
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.so
のsum
関数を実行するようになります。
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の関数を実行することができます。
以下参考記事です。
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))
これでFFIでRails・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配下にsrc
とbin
ディレクトリを切り、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言語好きになりました。
しかしRubyやRailsなど高級多機能な言語、FWから移ってくると「え、この関数ないんか」といった事態に遭遇して自分で実装することになるといったことが多くて大変そうです・・・
ともあれ今回の目的であった文章校正機能の高速化はGoでリプレースすることで見事達成することができました。しかしまさかここまでパフォーマンス向上するとは自分でも驚きです。
We're Hiring!!
弊社ではWeb / ネイティブエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡ください!
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とも組み合わせて使えます。
詳しいことに関しては、公式を読むか、既に丁寧に解説してくださっている記事がたくさんあるので、そちらを読んでください。
Reduxの事例を調査し、ベストプラクティスを模索し設計
Reduxについて理解ができたところで、実際に他社さんのサービスでどのように使われているのかなどの事例を調査し、どうやって実装するのが一番良いのかを模索しました。 特に気になったのは、ディレクトリ構成とMiddlewareをどうしているかです。
また、弊社アプリはSPAではなく、サーバサイドで生成したビューの一部をReact on RailsでReact化してSSRしています。そのため、SSRの事例を特に調査しました。
ディレクトリ構成
ディレクトリ構成に関しては、action
やreducer
、middleware
などをそれぞれディレクトリを分けるか、ドメインでひとまとめにしてしまうかを考えました。
ちなみにドメインでaction
やreducer
をひとまとめにするパターンはDucksといいます。基本的にaction
は特定のreducer
に紐づくことが多く、その場合action
やreducer
をそれぞれ別のディレクトリ、別のファイルに分ける必要がないため、このパターンは複雑性を回避するには有効な手段だと思います。
しかし、今回は弊社アプリは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の定義
ディレクトリ構成に関してはこちらがとても参考になりました。
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に関しては、こちらの記事がとても参考になりました。
また、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は必ず下記のように、type
、payload
、meta
、そして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が勝手にerror
をtrue
にした状態の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で開発する時に大いに助けになると思います。
↓こちらは最近公開された記事で、実装時の参考にはしておりませんが、非常に参考になりました。
所感
私も今回初めてReduxを触ったのですが、やはりReactを使用していてある程度規模が大きくなった場合にReduxを使うと責務がいい感じに分けられるので複雑性が解消され、コードがとても読みやすくなり、保守性も高まると感じました。 チームで開発する際の実装の一貫性も保ちやすくなると思います。
ただ、MobXなども出てきており、Reduxよりも筋の良いライブラリが登場する可能性もまだあるのではないかと思っています。 というより、Reduxがもっと強くなってくれればとつくづく思います・・・
まだ実際にアプリケーションにはほんの一部分にしか導入できていないので、なんとも評価しきれない部分もありますが、Redux導入を進めていく中で問題が生じたらその際にまた考えようかと思います。
We're Hiring
弊社ではWeb / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さい! エンジニアの方、ぜひ情報交換しましょう!
Upgraded Ruby on Rails from v4.2.4 to v5.1.4
こんにちは、エンジニアの神山です。
今年の1月に弊社アプリケーションで使用しているReactをFiber(v16)にアップグレードしました。
そしたら今度はRuby on Railsのアップグレードもすることになり、先日に無事アップグレードが完了したのでそのことについて書きました。
Railsは弊社の基盤フレームワークなので、慎重に確認と修正をしながら進めていきました。
詳細事項
手順
手順は以下の通りです。これらを追って説明していきます。
テストのカバレッジをあげる
これはGemやRailsのアップグレードをする際に、現在のコードがアップグレードしてもバグを起こさないか見つけるために行います。特に後方互換性がないアップグレードの時にとても役に立ちます。
今回のようなメジャーバージョンアップだと非推奨や使用不可になったコードが沢山出てくるのですが、テストの時点でほぼ気づくことが出来ました。
弊社は普段からテストを書くことを習慣にしていて、カバレッジは常に約95%あたりをキープしております。今回のアップグレードでも95%あれば十分であると判断したため、テストは追加で書かずにすみました。
カバレッジの目安はアプリケーションやチームによって変わってくると思うので一概には言えませんが、テストケースが十分にあるということは改めて大切だと思いました。
すべてのGemのバージョンアップ
一般的にGemはRubyやRailsのバージョンがあがると追随してアップグレードされます。そのためRails5系の環境に沿ったGemを使用するべく、全てのGemのアップグレードも行います。
ただ注意として、そのGemの開発者がアップグレードに追随していないこともあります。するとRails5系で使いたいのに使えない、Rails4系までしか動かないとかも起こります。
そのときは同じ機能を持つGemで開発が活発なものを探すか、自分でパッチをあてないといけません。ただパッチをあてるとその瞬間から自分たちでそのGemを管理していかないといけなくなるので、注意と覚悟が必要です。
そのため信頼できる別のGemを探すほうがアプリケーションの開発に集中できる状態を保てるので、入れ替えのコストはかかりますが適切な選択だと思います。
ちなみにRefileというGemを使用しているのですが、アップグレードに追随していませんでした。この記事の下に詳しく書いておきます。
Railsのアップグレード情報の検索
アップグレードの情報を得るため、ドキュメントを予め読んでおくのは大切です。何かエラーが出たときに解決がスムーズになります。そのため変更点をまとめておくといいと思います。
私は以下のドキュメントを読みました。
- Ruby on Rails 5.0 リリースノート | Rails ガイド
- Ruby on Rails 5.1リリースノート | Rails ガイド
- Rails アップグレードガイド | Rails ガイド
- 「Rails 5.0 update」で検索したときの上位約20記事
- 「Rails 5.1 update」で検索したときの上位約20記事
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-s3
とrefile-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
は、他にも注意点があります。下のドキュメントがよくまとめられているので、一読しておくことをおすすめします。
所感
今回のアップグレードを通して以下のことを学びました。
- フレームワークの更新を常に把握する。
- アップグレードはマイナーバージョンの安定版が出たら即行う。
- テストは常に書く。
上の項目は普段から習慣付けるといいと思います。そうでないとアップグレードしようとしたときにつらい思いをすることになります。
場合によっては気づいたら最新のバージョンとコードが乖離しすぎてて、アップグレードは現実的に不可能なんてことにもなり兼ねません。
雑感ではありますが、アップグレードはなかなか出来ることでもないのでとてもいい経験になりました。
We’re Hiring
弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!
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の考案者が解説したサイトになります。英語ですがわかりやすい図を用いて解説してあり、直感的に理解しやすいです。
Atomic Designは具体的に以下の手順でデザインを考えていきます。
- Atoms(原子)
原子は、ボタンや入力フォームにあたります。 - Molecules(分子)
分子は、検索フォームにあたります。 - Organisms(有機体)
有機体は、ヘッダーやメインコンテンツ、サイドバー、フッターにあたります。 - Templates
テンプレートはページのワイヤーフレームにあたります。 - Pages
ページはワイヤーフレームにコンテンツを入れて、文字通りページとして完成したものにあたります。
こちらにそれぞれの具体例がたくさん載っているので、導入の際に参考にしてみてください。
http://demo.patternlab.io/demo.patternlab.io
CSS設計のポイント
CSS設計のポイントは以下の4つで考えております。
当たり前のことなのですが、これらをきちんと守らないと管理しづらいCSSになってしまいます。
- 予測しやすい
- 再利用しやすい
- 保守しやすい
- 拡張しやすい
これらは下のサイトを参考にさせて頂きました。
現状の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 / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!