UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

メンテナブルなCSS管理

フロンエンドエンジニアごーです。

CSSを記述するにあたって常に気をつけないといけないのが、すべてのCSSクラスがグローバルネームスペースであるため、副作用の畏れが存在することです。

グローバルネームスペースの衝突を回避する方法としては、OOCSSSMACSSBEMといったクラスの命名規約があります。

これらはスタイル実装者が規約を遵守しているうちは問題ないですが、強制するものではないので大規模になるとスタイル定義の秩序は簡単に壊れてしまう可能性があり、非決定論的な解決に思えます。

そこで、クラス名が必ず衝突しない仕組みを実現するツールをいくつか学びCSSクラスの管理コストを軽減してみます。

利用ライブラリ

今回は開発環境のセットアップは割愛します。

以下のライブラリ使った開発環境を前提に説明していきます。

  • webpack3.x
  • style-loader
  • css-loader
  • sass-loader
  • vue-loader
  • Vue.js 2.x

単一ファイルコンポーネント(.vueファイル)を使ったscoped css

単一ファイルコンポーネント.vuestyle要素に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>

単一ファイルコンポーネント.vuestyle要素で定義されたスタイルは、scoped属性を持たせると外の要素に対しては追加スタイルの副作用がなく隔離されます。

css-loaderを使ったローカルスコープCSSクラス

css-loadermodulesオプションを利用することで、クラス名が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() を用いて動的に導入と除去を行ってフレキシブルに対応すると有効ではないでしょうか。