Edited at

CSS in ERB in JS という実証実験

More than 1 year has passed since last update.


はじめに

この内容は meguro.css#1 での発表を元に内容を整理したものです。

リポジトリ: euxn23/css-in-erb-in-js-poc

スライド: slideshare


動機


大規模な CSS つらい


  • BEM/SMACCS/OOCSS は命名規則で管理しようと言っているけど……


    • 大規模になってきたときに保守できなくなってくる

    • 名前がかぶったり、typo があったり



  • 命名規則ベースでがんばっても結局はグローバルに展開されている


    • 読み込む css ファイルがどれかとかを命名規則だけで運用していくのは厳しい



  • View 側との依存関係が非明示的になる


    • 命名規則に属せられないスタイルが発生して、common とかになっていき……



  • JS で動的にスタイルをいじる時に実装との依存が非明示になる


    • JS で変更することをコメントで残すようになる

    • JS の負債化が進んでいるとそこに巻き込まれる




CSS in JS という成功事例


  • CSS in JS のメリット


    • JS と CSS での変数共有とか動的な処理の見通しがよくなる

    • CSS のスコープ制御を JS の module ベースでできる

    • 使用するスタイルは import するので依存関係が明示化される



  • 実質的に Single Page Application じゃないと使えない


    • 既存の Rails や Laravel で使おうとすると設計レベルで作り直しになる




実証実験


CSS in Ruby の検討


  • テンプレートで css を注入できる?


    • これはできる



  • Ruby 側で依存関係を明示できる?


    • Rails は自動で require される

    • = 明示化できない

    • => スコープ管理が崩壊



  • CSS in Ruby の補完が効かない

  • 動的に CSS 操作できない


CSS in ERB in JS の検討


  • JS で css を注入

  • JS で module 化


    • 両方とも CSS in JS と同様のアプローチで実現できそう




CSS in ERB in JS の実証実験

erb build script

const fs = require('fs')

const { resolve, relative, dirname, basename, extname } = require('path')
const { readdirRecursivelySync } = require('readdir-recursively-sync')

const baseDir = resolve(__dirname, 'app/views')
const erbJsPaths = readdirRecursivelySync(baseDir).filter(
p => extname(p) === '.js'
)

erbJsPaths.forEach(erbJsPath => {
const requirePath = `./${relative(__dirname, erbJsPath)}`
const erbJs = require(requirePath)
const erb = erbJs()
const outputPath = `${dirname(erbJsPath)}/${basename(erbJsPath, '.js')}`
console.log(outputPath)
fs.writeFileSync(outputPath, erb)
})

index.html.erb.js

const commonBorder = require('../../styles/todos/common-border')

module.exports = () => `
<style>
th, td {
${commonBorder}
}
</style>

<p id="notice"><%= notice %></p>

<h1>Todos</h1>

<table style="${commonBorder}">
<thead>
<tr>
<th>Name</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @todos.each do |todo| %>
<tr>
<td><%= todo.name %></td>
<td><%= link_to 'Show', todo %></td>
<td><%= link_to 'Edit', edit_todo_path(todo) %></td>
<td><%= link_to 'Destroy', todo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Todo', new_todo_path %>
`

common-border.js

module.exports = `

border: 1px solid black;
`

index.html.erb (生成後ファイル)


<style>
th, td {

border: 1px solid black;

}
</style>

<p id="notice"><%= notice %></p>

<h1>Todos</h1>

<table style="
border: 1px solid black;
"
>
<thead>
<tr>
<th>Name</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @todos.each do |todo| %>
<tr>
<td><%= todo.name %></td>
<td><%= link_to 'Show', todo %></td>
<td><%= link_to 'Edit', edit_todo_path(todo) %></td>
<td><%= link_to 'Destroy', todo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Todo', new_todo_path %>


考察


功績


  • 概ね CSS in JS と同様の手法で実装できる

  • Template Literal 内で html / css(js) の補完が動作する

  • css の依存を module で管理できる

  • 動的な処理と css 宣言を集約できうる


    • CSS in JS で行うような変数共有はできない

    • グローバル変数を使えばできなくはないが悪手か




懸念点


  • ERB / Rails 変数の補完が効かない


    • IDE 向けプラグインを書けば解決すると思われる



  • ビルドフローの検討


    • Webpacker があることもあり、 Webpack で実行するのが自然か

    • 変わり種だと akameco/s2s の検討も



  • 出力前/後のどちらのインデントに合わせるか


    • そもそも Rails の出力ソースのインデントは大概崩れているが

    • ビルドフローで webpack loader をうまいこと作ればどうにかなりそうではある

    • prettier での整形も検討




Slim => ERB 事前コンパイルのニーズ


  • 新しい Ruby で ERB の実行速度が数倍に速くなったこともあり、事前に slim を erb にコンパイルしたいという考え

  • このビルドフローを予め in JS 可能な形で作ってしまえば、今後普及させられる可能性

  • slm.js 等、slim template を JS で使用するライブラリをうまく使えば、 slim 限定とはなるが簡略化できる可能性


はしがき


  • 本実験の PHP での使用


    • JS の Template Literal は $ を使うが、 PHP も $ を使うため問題は生じないか

    • PHP の方が Template Literal 内での補完が期待できるのでは

    • PHP 界隈での CSS 事情に詳しくないため、詳しい方の情報があればご意見ください




原著: Eutech Blog