Railsで以下の課題があり、解決のためのコードを試し書きしたので概要を記載する
- ページ単位でCSSのスコープを限定したい
- 不要なCSSファイルの読込みを削減したい
Disclaimer
この記事は、以下の記事を参考に自分用にカスタマイズしたもの。
Rails の CSS にスコープを持たせてファイル分割する
意図通りに動くのは確認したが、実運用に耐えるのかは不明。
ページ単位は細かすぎて割に合わないので、普通にController単位のスコープ限定/呼び出しにしたほうが良さげ。
課題
railsで以下を実現したい。
ページ単位でCSSのスコープを限定したい
常にビューテンプレートのトップレベルに一定のクラス名を設定すれば実現できるが、
もう少し強制力をもたせたい。
不要なCSSファイルの読みこみを削減したい
現状では、「全てのページで全てのCSSファイルを読み込んでいる」ので、
ページ単位で必要なものだけ読み込む形にしたい。
現状はこういう感じ。
application.scss
@import 'bases/_variable';
@import 'bulma/bulma';
@import 'bases/_base';
@import 'partials/*';
@import 'pages/*';
application.html.slim
doctype html
html
head
/...省略
= stylesheet_link_tag 'application', media: 'all'
= javascript_pack_tag 'application'
/...省略
body
/省略
解決策
前提
- SCSS, bulmaを利用
- rails 6.1.3.2
- sprockets 4.0.2
- sprockets-rails 3.2.2
具体的な実装方針/規約
簡単に言うと、"#{controller_name}/#{method_name}"
に対応したcss-scope
属性を
ページのトップレベル要素として記述し、対応する名前のSCSSをファイルのみを呼び出す。
以下詳細。
- ビューテンプレート(例: app/views/articles/run_melos.html.scss)
-
css-scope
属性を各ページテンプレートの最上位の要素に記載 -
#content_for
でcss-scope
をlayoutsファイルに渡す
-
- layoutsファイル(例: app/assets/layouts/application.html.slim)
-
= stylesheet_link_tag yield(:css_scope)
でビューテンプレートに対応するSCSSファイルを呼び出す
-
- SCSSファイル(構成)
- ビューテンプレートに対して、1:1でSCSSファイル(以下「ページSCSS」)を作成
- ページSCSSのfullpathは、
app/assets/stylesheets/pages/{#controller_name}/#{method_name}.scss
- パーシャルに対して、N:1でSCSSファイル(以下パーシャルSCSS)を作成
- SCSSファイル(記載内容)
- ページSCSSから、必要なSCSSファイル(パーシャルSCSS含む)を都度importする
- ページSCSSのトップレベルに、対応するビューテンプレートの
css-scope
を記載
- アセットパイプラインの設定ファイル(config/initializers/assets.rb)
-
app/assets/stylesheets/pages/
以下のファイルをprecompile対象にするよう設定
-
実装のポイント
application.html.slim
html
head
/ 既存のSCSS体系からの移行用
- if content_for?(:css_scope)
/ baseでbulma/bulmaを読み込んでいる
= stylesheet_link_tag 'base', media: 'all'
= stylesheet_link_tag yield(:css_scope), media: 'all'
= stylesheet_link_tag 'partials/_header', media: 'all'
= stylesheet_link_tag 'partials/_footer', media: 'all'
- else
= stylesheet_link_tag 'application', media: 'all'
application_helper.rb
# content_forでcss_scopeをapplication.html.slimに渡しつつ、
# data-css-scopeを設定した(ビューテンプレート内の)トップレベルのHTML要素を作成する
def with_css_scope(tag_name: :div, css_scope:, **options)
content_for :css_scope, css_scope
tag_options = options.deep_merge(data: { 'css-scope': css_scope })
tag.public_send(tag_name, tag_options) { yield }
end
app/views/articles/run_melos.html.slim
= with_css_scope(css_scope: 'pages/articles/run_melos')
h1.title なぜメロスは怒ったのか
/ 以下普通にHTMLを記述
app/assets/stylesheets/pages/articles/run_melos.scss
// 適宜このSCSSで利用しているSCSSファイルを呼ぶ
@import 'bases/_variable';
@import 'bases/_base';
@import 'partials/_helpers';
@import 'partials/_button';
@import 'partials/_qa';
[data-css-scope='pages/articles/run_melos'] {
.title {
font-weight: bold;
}
// 以下省略
}
config/initializers/assets.rb
# 省略
Rails.application.config.assets.precompile += %w(
application.css
partials/_header.css
partials/_footer.css
)
stylesheets_path = 'app/assets/stylesheets/'
Pathname(stylesheets_path + 'pages').
glob('**/**.scss').
map { |e| e.relative_path_from(stylesheets_path).sub('scss', 'css').to_s }.
each { |path| Rails.application.config.assets.precompile << path }
# 省略
# 注記: https://railsguides.jp/asset_pipeline.htmlにはProcを...assets.precompileに渡す方法が書かれているが
# 仕様が変わったのか受け付けてくれないのでPathnameで個別にファイル名を拾って渡している
実装のポイント(+α)
各View Templateから@virtual_path
が参照できるので活用する。
articles/run_melos.html.slim
/ @virtual_pathの値は articles/run_melos
= with_css_scope(css_scope: "pages/#{@virtual_path}")
h1.title 何故メロスは怒りやすいのか
/ 以下普通にHTMLを記述
SCSSファイルからbulmaのクラスや変数を呼び出している場合は、該当するbulmaファイルを個別に読み込む。
sample.scss
// 変数
@import 'bulma/sass/utilities/initial-variables';
// ユーティリティっぽいクラス
@import 'bulma/sass/utilities/_all';
@import 'bulma/sass/base/_all';