フロンエンドエンジニアごーです。
CSSを記述するにあたって常に気をつけないといけないのが、すべてのCSSクラスがグローバルネームスペースであるため、副作用の畏れが存在することです。
グローバルネームスペースの衝突を回避する方法としては、OOCSS、SMACSS、BEMといったクラスの命名規約があります。
これらはスタイル実装者が規約を遵守しているうちは問題ないですが、強制するものではないので大規模になるとスタイル定義の秩序は簡単に壊れてしまう可能性があり、非決定論的な解決に思えます。
そこで、クラス名が必ず衝突しない仕組みを実現するツールをいくつか学びCSSクラスの管理コストを軽減してみます。
利用ライブラリ
今回は開発環境のセットアップは割愛します。
以下のライブラリ使った開発環境を前提に説明していきます。
- webpack3.x
- style-loader
- css-loader
- sass-loader
- vue-loader
- Vue.js 2.x
単一ファイルコンポーネント(.vueファイル)を使ったscoped css
単一ファイルコンポーネント.vue
のstyle
要素にscoped
属性を持たせることで、HTML要素にユニークな名前のディレクティブが挿入されます。
要素に適用されるスタイルは、このディレクティブとクラス名で限定されたセレクタとして適用されます。
<template> <div> <button class="btn-red">ボタン</button> <button class="btn-green">ボタン</button> <button class="btn-blue">ボタン</button> </div> </template> <style lang="scss" scoped> .btn-red { background-color: red; } .btn-green { background-color: green; } .btn-blue { background-color: blue; } </style>
vue-loader
によって、以下の様にコンパイルされます。
<div data-v-18683546="" data-v-7ca92acd=""> <button data-v-18683546="" class="btn-red">ボタン</button> <button data-v-18683546="" class="btn-green">ボタン</button> <button data-v-18683546="" class="btn-blue">ボタン</button> </div>
<style type="text/css"> .btn-red[data-v-18683546] { background-color: red; } .btn-green[data-v-18683546] { background-color: green; } .btn-blue[data-v-18683546] { background-color: blue; } </style>
単一ファイルコンポーネント.vue
のstyle
要素で定義されたスタイルは、scoped
属性を持たせると外の要素に対しては追加スタイルの副作用がなく隔離されます。
css-loaderを使ったローカルスコープCSSクラス
css-loader
でmodules
オプションを利用することで、クラス名がBase64でエンコードされた文字列に変換されます。実質的にクラス名の衝突が回避されたCSSクラスになります。
webpack.config.js
は、次の様に記述します。
module.exports = { // 中略 module: { rules: [{ // 中略 { test: /\.scss$/, loaders: [ 'style-loader', 'css-loader?modules', 'sass-loader' ] } ]} } }
単一ファイルコンポーネント.vue
でCSSクラス名を参照出来るように変換されたセレクタを含むmainCss
をリアクティブプロパティdata
で宣言します。
<template> <div> <button :class="mainCss['btn-red']">ボタン</button> <button :class="mainCss['btn-green']">ボタン</button> <button :class="mainCss['btn-blue']">ボタン</button> </div> </template> <script> import mainCss from './main.scss' export default { data: function() { return { mainCss } } } </script>
.btn { &-red { background-color: red; } &-green { background-color: green; } &-blue { background-color: blue; } }
以下の様にコンパイルされます。css-loader
で変換されたCSSクラスがvue-loader
によって指定された各要素へ渡されます。
<div data-v-7ca92acd=""> <button class="_2a8m_DxW1VSCOe2jlEDJtg">ボタン</button> <button class="_1ROlgn2AbMs9c7cjb5HAHy">ボタン</button> <button class="_1BZt8uRXrjTy9iWjcG2Ym5">ボタン</button> </div>
<style type="text/css"> ._2a8m_DxW1VSCOe2jlEDJtg { background-color: red; } ._1ROlgn2AbMs9c7cjb5HAHy { background-color: green; } ._1BZt8uRXrjTy9iWjcG2Ym5 { background-color: blue; } </style>
変換されたクラス名をクラスのバインディングで代入だとマルチクラスに対応出来ないので、単一ファイルコンポーネント.vue
に、次のローカルディレクティブを実装すると複数クラスも代入できます。
directives: { mainCss: { inserted: function(el, binding) { let value = binding.value if (value === undefined) return value = value.split(/\s/) if (value.length === 0) return value.forEach(v => { let className = mainCss[v] let targetClass = className === undefined ? v : className let classes = el.className.split(/\s/) if (classes.some(elm => elm === targetClass)) return el.className += ` ${targetClass}` }) } } }
カスタムディレクティブv-mainCss
は、値を配列にして使います。
<template> <div> <button v-mainCss=['btn-red', 'rect']>矩形の赤ボタン</button> <button v-mainCss=['btn-green', 'circle']>円形の緑ボタン</button> <button :class="mainCss['btn-blue']">ボタン</button> </div> </template>
style-loaderのuseableオプション
style-loader/useable
を使うことで、インポートしたCSSはデフォルトでhead
要素に注入されません。JavaScript側から意図したタイミングでスタイル定義の適用・非適用を行い、根本的にスタイルの有効無効をコントール出来ます。
webpack.config.js
では次の様に記述します。
module.exports = { // 中略 module: { rules: [{ // 中略 { test: /\.scss$/, loaders: [ 'style-loader/useable', 'css-loader', 'sass-loader' ] } ]} } }
import
したSCSSは、use()
,unuse()
を実行することでスタイル定義をhead
要素にstyle
要素の導入と除去が行われます。
<template> <div> <button @click="switchMain">メイン</button> <button @click="switchSub">サブ</button> <button class="btn-red">ボタン</button> <button class="btn-green">ボタン</button> <button class="btn-blue">ボタン</button> </div> </template> <script> import mainCss from "./main.scss" import subCss from "./sub.scss" export default { methods: { // main.scssが有効化 switchMain: function(){ mainCss.use() subCss.unuse() }, // sub.scssが有効化 switchSub: function(){ mainCss.unuse() subCss.use() } } } </script>
おわりに
個人的には、これまでリファクタリングや画面の追加実装を行うことが多かったので、ご紹介したこれらのパターンは、既存スタイルに影響を与えたくない場合に有効だと思います。
特にCSSフレームワークなどを大きいサイズのスタイルを一部の画面に適用して使いたいときに、use()
, unuse()
を用いて動的に導入と除去を行ってフレキシブルに対応すると有効ではないでしょうか。