Railsチュートリアル 第5章 - レイアウトを作成する…Sassとアセットパイプライン

# アセットパイプライン

静的コンテンツの生産性と管理を大幅に強化するRailsの機能です。アセットパイプラインの対象となる静的コンテンツには、以下のようなものがあります。

* CSS
* JavaScript
* 画像

## アセットディレクトリ

Railsの標準的な環境においては、アセットの開発者・適用範囲の違いにより、以下の3つのアセットディレクトリが定義されています。

* `app/assets`…現在のアプリケーション固有のアセット
* `lib/assets`…自開発チームにより作成されたライブラリ用のアセット
* `vendor/assets`…サードパーティのアセット

```
>>> pwd
~/docker/rails_tutorial_test/sample_app

>>> ls -l app/assets/
total 0
drwxr-xr-x  3 ***  ***   96  8  7 07:59 config
drwxr-xr-x  5 ***  ***  160  9 19 07:44 images
drwxr-xr-x  6 ***  ***  192  8 16 09:32 javascripts
drwxr-xr-x  5 ***  ***  160  9 19 08:06 stylesheets
```

上記は`sample_app`の`app/assets`のディレクトリ内容です。`config`はここでは解説されていませんが、他にCSS用の`stylesheets`、JavaScript用の`javascripts`、画像用の`images`というディレクトリが存在することがわかります。

[Railsチュートリアル 第5章 - レイアウトを作成する…構造を追加する](https://qiita.com/rapidliner00/items/7abc5642c04f3eb377aa)で、`custom.scss`というファイルについて言及しました。当該ファイルが`app/assets/stylesheets`以下にあるのは、**アプリケーション固有のCSSであるから**なのです。

## マニフェストファイル

マニフェストファイルは、上記アセットディレクトリに配置された静的ファイル（アセット）をどのように1つのファイルにまとめるのかを定義するためのファイルです。マニフェストファイルの適用可否は以下のとおりです。

* マニフェストファイルが適用される
    * CSS
    * JavaScript
* マニフェストファイルが適用されない
    * 画像

例えば「アプリケーションのCSS用マニフェストファイル」の場合、以下のような内容になります。

```css:app/assets/stylesheets/application.css
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
```

このCSSで重要な部分は、コメント中にある`*=`で始まる2行です。

```css:app/assets/stylesheets/application.css（抜粋）
/*
 ...略
 *= require_tree .
 *= require_self
 */
```

具体的には、以下に示す意味となります。

* `*= require_tree .`という記述は、`app/assets/stylesheets`以下の全てのCSSファイルがアプリケーションCSSに含まれることを意味する
* `*= require_self`という記述は、`application.css`自身もアプリケーションCSSに含まれることを意味する

### Sprockets

Railsのアセットパイプライン機能の根幹をなすライブラリです。マニフェストファイルとの関係では、「マニフェストファイルの記述に基づき、CSSやJavaScriptを実際に1つのファイルにまとめる機能」を提供します。

「Sprocketsとは何か」という疑問の細かな答えとしては、例えば以下のような記事が参考になるかと思われます。

* [Sprocketsについて復習する - ABC Blog](https://linuxserver.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0/ruby/on-rails/sprockets/)
* [Sprocketsの仕組み - Qiita](https://qiita.com/ttaka66/items/991a52081a92cb6c2738) - @ttaka66 さんの記事。

## プリプロセッサエンジン

Railsにおいて、必要なアセットをディレクトリに配置してからサイトテンプレートに埋め込まれるまでには、以下の手順が実行されます。

1. 種々のプリプロセッサエンジンを介し、実行可能なアセットを実行する
2. ブラウザに配信できるように、マニフェストファイルを使ってアセットを結合する
3. サイトテンプレート用に準備する

Railsがどのプリプロセッサエンジンを使うかは、アセットとして配置されたファイルの拡張子によって判断されます。以下はその一例です。

* `.scss`…Sassのプリプロセッサエンジンで実行する
* `.coffee`…CoffeeScriptのプリプロセッサエンジンで実行する
* `.erb`…埋め込みRuby(ERb)のプリプロセッサエンジンで実行する

### プリプロセッサエンジンのチェーン実行

プリプロセッサエンジンは繋げて(chain)実行することができます。例えば、`foober.js.coffee`というアセットは、CoofeeScriptエンジンで実行されます。

プリプロセッサエンジンの実行優先順位は右から左となります。例えば、`foobar.js.erb.coffee`というアセットの場合、先にCoffeeScriptエンジンが実行され、次にERbエンジンが実行されます。

## 本番環境での効率性

アセットパイプラインには、「本番のアプリケーションで効率的になるように最適化されたアセットが自動的に生成される」という特徴があります。この特徴は、「機能を個別のファイルに分割し、読みやすいフォーマットに整えた状態のCSSやJavaScript」の扱いにおいて最大限に活用されます。

「機能を個別のファイルに分割し、読みやすいフォーマットに整えた状態のCSSやJavaScript」というのは、プログラマにとって扱いやすいコードですが、本番環境においては非効率なコードです。なぜなら、「最小化されていないCSSやJavaScriptが複数のファイルに散らばっている」という状態では、読み込み速度が著しく遅くなる（＝ユーザー体験の悪化につながる）からです。

アセットパイプラインを使えば、「開発環境ではプログラマが扱いやすいコードで開発し、本番環境では読み込み時間が最小となるコードで実行する」という要求が実現されます。アセットパイプラインにより行われる具体的な処理の手順は以下のとおりです。

1. 全てのスタイルシートを一つのCSSファイル(`application.css`)にまとめる、または、全てのJavaScriptファイルを一つのJSファイル(`javascripts.js`)にまとめる
2. `application.css`あるいは`javascripts.js`から不要な空白・インデントを除去し、ファイルサイズを最小化する

# 素晴らしい構文を備えたスタイルシート

Sassは、CSSを大幅に強化したスタイルシート記述言語です。Sassが提供する主要な機能は、以下の3つがあります。

* ネスト
* 変数
* ミックスイン

## SCSSフォーマット

Sassが対応する、CSSを拡張したファイルフォーマットです。一般に、拡張子としては`.scss`が用いられます。SCSSはCSSの上位互換であり、すべての有効なCSSはSCSSとしても扱うことができます。

Railsチュートリアルでは、Bootstrapフレームワークの恩恵を得るために、最初からSCSSで記述しています。

Railsのアセットパイプラインは、`.scss`という拡張子を持つファイルに対し、自動的にSassプリプロセッサエンジンで処理を行います。例えば、今回用いる`custom.scss`ファイルもSassプリプロセッサエンジンによって処理されます。

## ネスト

スタイルシート内にある共通のパターンの適用をわかりやすく表現する記法です。

例えば、以下は`.center`と`.center h1`の両方に対してルールが設定されているCSSです。

```css:共通のパターンが存在するCSSの例1
.center {
  text-align: center;
}

.center h1 {
  margin-bottom: 10px;
}
```

SCSSでは、同じルール設定を以下のように記述することができます。ネストの内側にある`h1`というルールは、`.center`のルールを継承しています。

```sass:上述例1のCSSと同じ挙動をするSCSSの例
.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}
```

続いての例は、`#logo`というid属性が含まれるセレクタが2つ定義されたCSSです。`hover`属性は、当該要素の上にマウスポインタをかざしたときの表示を定義します。

```css:#logo属性が含まれるセレクタが2つ定義されたCSS
#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
}

#logo:hover {
  color: #fff;
  text-decoration: none;
}
```

以上のような例でも、SCSSならばネスト記法を適用することが可能です。

でも、この例でネスト記法を使うとなると、親セレクタを参照する必要が出てきますよね。どうやって親属性を参照するのでしょうか？…そんなときに使うのが、`&`という記号です。具体的には、前述の例を以下のように記述することができます。

```scss:&記号を用いたネスト
#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: #fff;
    text-decoration: none;
  }
}
```

SassプリプロセッサエンジンがSCSSをCSSに変換する過程で、`&:hover`は、`#logo:hover`に置換えられます。

### `footer`用のCSSを、ネストを用いたSCSSに書き換えてみる

現時点で、`app/assets/stylesheets/custom.scss`の`footer`要素に関するCSSの記述は、以下のようになっています。ネストは用いられていません。

```css:ネスト不使用のfooterのCSS
/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
}

footer a {
  color: #555;
}

footer a:hover {
  color: #222;
}

footer small {
  float: left;
}

footer ul {
  float: right;
  list-style: none;
}

footer ul li {
  float: left;
  margin-left: 15px;
}
```

上記CSSをネストを用いたSCSSに書き換えると、以下のようになります。

```scss:前述footerのCSSを、ネストを用いたSCSSに書き換えた後
/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
  a {
    color: #555;
    &:hover {
      color: #222;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}
```

## 変数

冗長なコードを排除し、より自由な表現を可能にするために、Sassで採用されている記法です。

```css:同じ色を繰り返し参照しているCSS
h2 {
  /* ...略 */
  color: #777;
}

/* ...略 */

footer {
  /* ...略 */
  color: #777;
}
```

上述のCSSは、同じ色コードを複数回繰り返し使用しています。

色コードのようなリテラルは、使い捨てで使う分には大きな問題はありません。しかしながら、色そのものに何らかの意味をもたせているような場合、色コードをいちいちリテラルで記述するのは、冗長かつ不具合（修正漏れ等）の温床となるものです。そんなときに変数を使うのです。

上述のCSSを、色コードを変数に置き換えたSCSSとして書き換えたのが以下です。

```scss:変数を使用して冗長なコードを排除したSCSSの例
$light-gray: #777;
/* ...略 */
h2 {
  /* ...略 */
  color: $light-gray;
}

/* ...略 */

footer {
  /* ...略 */
  color: $light-gray;
}
```

色コードのみならず、長さに類する尺度に対して変数を定義することも可能です。

### Bootstrapフレームワークにおける変数

Bootstrapでは、多種多様な色や長さに対して変数が定義されています。Railsチュートリアルには、他にも以下のような記述があります。

* Bootstrapでは、CSS拡張書式としてSassではなくLESSを用いて変数を定義している
    * この点については、さらなる確認が必要
* LESSにも変数機能がある
* LESSの変数は、頭文字が`@`で始まる
    * 一方、Sassの変数は、頭文字が`$`で始まる
* `bootstrap-sass`というgemを導入すれば、Bootstrapに定義されている変数をSass環境でも使うことができるようになる

Bootstrapでは、例えば以下のような変数が定義されています。

```
@gray-light: #777;
```

別の言い方をすれば、`bootstrap-sass`を導入すれば、`#777`というコードの色を`$gray-light`と記述することができる、ともいえるのです。それでは、先ほどの`footer`部分のSCSSを、早速`$gray-light`という変数を使うコードに書き換えてみましょう。

```scss:変数を使用して冗長なコードを排除したSCSSの例2
@import "bootstrap-sprockets";
@import "bootstrap";
/* ...略 */
h2 {
  /* ...略 */
  color: $gray-light;
}

/* ...略 */

footer {
  /* ...略 */
  color: $gray-light;
}
```

# 演習 - Sassとアセットパイプライン

## 1. [5.2.2](https://railstutorial.jp/chapters/filling_in_the_layout?version=5.1#sec-sass)で提案したように、`footer`のCSSを手作業で変換してみましょう。具体的には、[リスト 5.17](https://railstutorial.jp/chapters/filling_in_the_layout?version=5.1#code-footer_css)の内容を１つずつ変換していき、[リスト 5.20](https://railstutorial.jp/chapters/filling_in_the_layout?version=5.1#code-refactored_scss)のようにしてみてください。

```css:Before（リスト5.17）
/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
}

footer a {
  color: #555;
}

footer a:hover {
  color: #222;
}

footer small {
  float: left;
}

footer ul {
  float: right;
  list-style: none;
}

footer ul li {
  float: left;
  margin-left: 15px;
}
```

### カラーコードを変数化

前提条件として、Bootstrapでは、カラーコードに対する変数が以下のように定義されています。

* `$gray-light`: `#777`
* `$gray`: `#555`
* `$gray-dark`: `#333`
* `$gray-darker`: `#222`

さらに、`#eaeaea`を`$gray-medium-light`と定義することとします。すると、BeforeのCSSコードは、以下のように書き換えることができます。

```scss:リテラルで記述されていたカラーコードを変数化
$gray-medium-light: #eaeaea;
/* ...略 */

/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid $gray-medium-light;
  color: $gray-light;
}

footer a {
  color: $gray;
}

footer a:hover {
  color: $gray-darker;
}

footer small {
  float: left;
}

footer ul {
  float: right;
  list-style: none;
}

footer ul li {
  float: left;
  margin-left: 15px;
}
```

### ネストによる要素の入れ子関係の明確化

カラーコードを変数化したら、次に、ネストによって要素の入れ子関係を明確化します。「親セレクタを参照する[^親セレクタの参照]には`&`を使う」ことを踏まえた上で、上述のコードには、以下の入れ子関係が存在します。

[^親セレクタの参照]: 例えば`a:hover`に対する`a`を参照する場合に`&:hover`と記述する、`.button-normal`に対する`.button`を参照する場合に`&-normal`と記述するなどです。

```scss:footerの入れ子関係
footer {
  a {
    &:hover {
    }
  }
  small {
  }
  ul {
    li {
    }
  }
}
```

上述の入れ子関係を踏まえた上で、前述「カラーコードの変数化」を適用したコードをネストを用いた形に書き換えると、以下のようになります。

```scss:ネストを用いた形に書き直した結果
/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid $gray-medium-light;
  color: $gray-light;
  a {
    color: $gray;
    &:hover {
      color: $gray-darker;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}
```

これでfooterのCSSを変換できました。