Rails × Mountain View(マウンテンビュー)で作るCSSスタイルガイド&コンポーネント

はじめまして。

# Mountain View導入の経緯

普段 Ruby On Railsでプロダクトを開発しており、gemで完結するCSSのスタイルガイドを探していました。

有名なgemをいくつか素振りしてみたのですが、[LivingStyleGuide](https://github.com/livingstyleguide/livingstyleguide)はスタイルガイドを簡単にカテゴリ分けできず、[Hologram](https://github.com/trulia/hologram)はメンテナンスコストが高そうに感じました。

そんな時に、Railsのビューコンポーネントをそのままスタイルガイド化できる[MountainView](https://github.com/devnacho/mountain_view)と言うgemを知りました。

# Mountain Viewとは
https://github.com/devnacho/mountain_view
> With Mountain View you create reusable components for your Rails frontend, while generating a living style guide.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102943/062b61cf-9c02-d6f9-21c0-3b85b8757767.png)

Mountain Viewを使用すると、スタイルガイドを生成しつつ、Railsフロントエンド用の再利用可能なコンポーネントを作ることができます。

# この記事で紹介すること

紹介しておいてなんですが、私は**パフォーマンス上の理由からMountain Viewのコンポーネント機能をそれほど使っていません。**
(コンポーネントはRailsのパーシャルに任せれば済む話で、どちらかと言うとスタイルガイドとしての機能が欲しい...)

この記事では**私がMountain Viewを導入し、コンポーネントとして使用することを諦め、スタイルガイドとして使用することに落ち着くまでに試行錯誤した内容**をまとめています。

javascriptを使ったモダンなフロントエンド開発からは遠い話になりますのでご容赦ください。

# 導入 ~ スタイルガイドの作成
Rails 5.2.1環境で[公式のREADME](https://github.com/devnacho/mountain_view)だけを参考に導入することができました。

まずは `Gemfile` に `mountain_view` を追加します。

```
# コンポーネント機能を利用する場合はグローバルに読み込みます
gem 'mountain_view'
```

`bundle install`を実行し、`routes.rb` ファイルに次の行を追加します。

```
mount MountainView::Engine => "/mountain_view" if Rails.env.development?
```

下記のコマンドでMountain Viewの新しいコンポーネントを作成します。

```
rails generate mountain_view:component button
```

すると、次のようなディレクトリとファイルが作成されます。

```
app/
  components/
    button/
      _button.html.erb
      button.css
      button.js
      button.yml
```

拡張子にscssやslimなど、Railsで使っているプリプロセッサを使うことができます。

`rails s` でサーバーを立ち上げてみましょう。
ローカル環境からbuttonコンポーネントのスタイルガイドのページが作成されていることが確認できると思います。
http://localhost:3000/mountain_view

# i18n対応
i18n対応で日本語化(default_localeをjaに変更)していた場合、Mountain Viewにja.ymlが存在しないためtitleタグの表示で下記のようなエラー出ているかもしれません。

```
<title>&lt;span class="translation_missing" title="translation missing: ja.mountain_view.layout.styleguide_title"&gt;Styleguide Title&lt;/span&gt;</title>
```

私は[こちらの en.ymlファイル](https://github.com/devnacho/mountain_view/blob/master/config/locales/en.yml)の内容をコピペした `config/locales/mountain_view.ja.yml` のようなファイルを作成しました。

# buttonコンポーネントを作ってみる

ここでは `bootstrap` を使用してコンポーネント作りを試してみます。
`Gemfile` に以下を追加して `bundle install` します。

```
gem 'bootstrap'
```

buttonコンポーネントのscssでbootstrapを読み込みます。

```components/button/button.scss
@import "bootstrap";
```

[bootstrapのbuttonコンポーネント](https://getbootstrap.com/docs/4.2/components/buttons/)をMountainViewに反映させた例です。

```app/components/button/_button.html.erb
<button type="button" class="btn <%= properties[:modifier] %>">
  <%= properties[:title] %>
</button>
```

```app/components/button/button.yml
-
  :modifier: btn-primary
-
  :modifier: btn-secondary
-
  :modifier: btn-success
-
  :modifier: btn-danger
-
  :modifier: btn-warning
-
  :modifier: btn-info
-
  :modifier: btn-light
-
  :modifier: btn-dark
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102943/02c2508a-50f9-6b60-c7a1-d47bb35feac5.png)



# キャッシュの問題
Mountain ViewではCSSの更新やコンポーネントの作成を行った後にキャッシュが残ってしまい、下記のコマンドを実行しないとコンポーネントのデザインがうまく反映されないことがありました。

```
bin/rake tmp:cache:clear
```

# buttonコンポーネントを使ってみる

定義したコンポーネントは`render_component` メソッドを使用することで使うことができます。
第一引数にコンポーネント名、第二引数にハッシュ値を渡して使います。

```
<%= render_component("button", { title: "Btn Primary", modifier: "btn-primary" }) %>
```

コンポーネントにブロックを渡し、`properties[:yield]`で読み込んで使用することもできます。

```app/components/button/_button.html.erb
<button type="button" class="btn <%= properties[:modifier] %>">
  <%= properties[:yield] %>
</button>
```

```使い方.html.erb
<%= render_component("button", {modifier: "btn-primary" }) do %>
  Btn Primary
<% end %>
```

# プレゼンターを使ってみる
コンポーネントの階層に`{コンポーネント名}_component.rb`ファイルを追加してMountainViewコンポーネント用のプレゼンターを定義することができます。

```
app/
  components/
    button/
      _button.html.erb
      button.css
      button.js
      button.yml
      + button_component.rb
```

MountainView::Presenterを継承して使います。
プロパティのデフォルト値なども使用できます。

```app/components/button/button_component.rb
class ButtonComponent < MountainView::Presenter
  properties :modifier, :title
  property :element, default: 'btn'

  def modifier_title
    title || properties[:modifier].titleize
  end
end
```

定義したメソッドやプロパティをコンポーネントのパーシャルで利用することができます。

```app/components/button/_button.html.erb
<button type="button" class="<%= element %> <%= modifier %>">
  <%= title %>
</button>
```

Railsのコンポーネント管理がMountainViewで完結して最高！
...と思ったのですが、このコンポーネント機能には、後述するパフォーマンス上の問題があるようです。

# MountainViewのボトルネック
MountainViewのrender_component機能をeachすると、パフォーマンスがとても残念なことになってしまいます。
これを避けるために公式READMEでは[MountainViewのプレゼンターでrenderメソッドをオーバーライドする方法]((https://github.com/devnacho/mountain_view#improving-performance))を紹介しています。

しかし、個人的にはそれよりも素直にRailsのパーシャルを使用した方がメリットが大きいなのではないかと思います。特にrender_component機能ではパーシャルの[コレクション機能](https://railsguides.jp/layouts_and_rendering.html#%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B)が使えないのが痛いので、私はeachする要素でMountain Viewのrender_component機能は使いません。

例えば、Mountain Viewを

```app/components/list_item/_list_item.html.erb
<%= properties[:list_item].title %>
```

こんな風に定義するよりも、Railsのパーシャルで

```app/views/components/_list_item.html.erb
<%= list_item.title %>
```

このように定義しておけば、renderをキャッシュしてくれますし、collection機能でn+1対策もできます。

```使い方.html.erb
<% @lists = List.all %>

# @listsが100個あったら100回render
<% @lists.each do |list_item| %> 
  <%= render_component("list_item", {list_item: list_item}) %>
<% end %>

# @listsの結果が100個あっても1回のrenderで済む
<%= render partial: 'components/list_item', collection: @lists, as: :list_item %>
```

最高のビューコンポーネントはRailsのパーシャルでした...😂
と、言うわけで私はMountain ViewをRailsコンポーネント管理用のスタイルガイドとして割り切って使うことにしました。

# MountainViewをスタイルガイドとして使用する

MountainViewはデフォルトではスタイルガイドのHTMLタグが表示されません。
デフォルトで `prism.js` が使われているので、[views/mountain_view/styleguide/show.html.erb](https://github.com/globis-org/hodai/blob/88c41f678fe9bdc07baa04d5e6cee6680272a516/app/views/mountain_view/styleguide/show.html.erb)をオーバーライドして、render_componentの記述の上部に下記のコードを追記しただけでスタイルガイドっぽくなってくれます。

```app/views/mountain_view/styleguide/show.html.erb
...
<div class="mv-component__description__properties" style="margin-bottom: 20px;">
  <code class="language-html"><%= CGI::pretty("#{render_component(@component.name, component_stub.properties.clone)}") %></code>
</div>
...
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102943/0bc90f34-c7e5-07d9-ef1d-b2904cb58f91.png)


また、MountainViewのサイドメニューがレスポンシブ表現に邪魔なので、メディアクエリで非表示にしてみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102943/de72dc8a-947b-65b5-3a03-ac379082f8c6.png)

```app/assets/stylesheets/mountain_view/layout.scss
.mv-main {
  @media screen and (max-width: 768px) {
    width: 100%;
    padding: 30px 0;
  }
}

.mv-sidebar {
  @media screen and (max-width: 768px) {
    display: none;
  }
}
```

イニシャライザでMountain Viewでグローバルに読み込みたいCSSを指定することができます。

```config/initializers/mountain_view.rb
MountainView.configure do |config|
  config.included_stylesheets = ["mountain_view/layout"]
end
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102943/dafe3081-f7ab-ae48-bf41-a2882dc0ec9e.png)


# CSS読み込みの問題
MountainViewはディレクトリ名のコンポーネントを[mountain_view.css.erb](https://github.com/devnacho/mountain_view/blob/master/app/assets/stylesheets/mountain_view.css.erb)でディレクトリ名と同名のcssをまとめて読み込んでいるようです。

例えばbuttonコンポーネントでbootstrapをimportした後、別のコンポーネントを作ろうとしてみたところ、button.scssでしかimportしていないbootstrapが既に読み込まれています。

MountainViewの動作がCSS設計の方針と違った場合、私は[マニュフェストファイル](https://github.com/devnacho/mountain_view/blob/master/app/assets/stylesheets/mountain_view.css.erb)や[レイアウトファイル](https://github.com/devnacho/mountain_view/blob/master/app/views/layouts/mountain_view.html.erb)をオーバーライドして調整するようにしています。

# プレゼンターをスタブとして使ってしまう
Tipsとして、私は既存のコードをコンポーネント化する際、一旦Mountain Viewのプレゼンターをスタブとして一旦置いてみる方法をとっています。

```app/components/post/post_component.rb
class PostComponent < MountainView::Presenter
  # コンポーネント作りに必要なデータを取得
  def current_user
    User.find_by(email: 'necessary-user@exapmle.com')
  end
end
```

こんな感じでプレゼンターのメソッドを使うと既存のビューをコンポーネントでリプレースする工程がスムーズにいって便利です。

# 最後に
Railsのgemで完結するスタイルガイドを探したところ、Mountain Viewを改造しながら使っていくと言う道に辿り着きました。
もっとオススメのgemや、Mountain Viewの便利な使い方をご存知の方がいらっしゃいましたらぜひコメント欄で教えてください...🙏