Help us understand the problem. What is going on with this article?

[WIP] もうFLOCSSで消耗するのはやめよう!単一ファイルコンポーネント時代の新しいCSS設計

More than 1 year has passed since last update.

序章

管理可能で拡張性のあるCSSを構築するために、先人たちは数々のCSS設計や命名規約を生み出しました。
OOCSS、SMACSS、BEM、SuitCSS...
そしてそれらの考えを統合したFLOCSSが登場しました。
FLOCSSは強力なツールです。
しかし、悲しいことに強力すぎて誰も使いこなすことができなかったのです。
「これってComponentとProjectどっちだ?適当でいいか」
「最初に作った人はFLOCSSを理解していたかもしれないが、あとから保守した人が理解していなくてめちゃくちゃになった」
「サーバーエンジニアなのに急にフロントを書かないといけなくなった。FLOCSS? 覚えること多すぎて無理!」

この状況から脱出するための新しい技術が生まれました。
単一ファイルコンポーネントです。

単一ファイルコンポーネントって?

こういうやつです。

Hello.vue
<template>
  <p>{{ greeting }} World!</p>
</template>

<script>
module.export = {
  data: function() {
    return {
      greeting: 'Hello'
    }
  }
}
</script>

<style>
p {
  font-size: 2em;
  text-align: center;
}
</style>

ようは、template(html)、script(js), style(css)を一つのファイルにまとめて、それを使いまわそうということです。

これの何が嬉しいのかはvueの公式から引用させていただきます。

注意すべき重要な点の1つは、関心事項の分離がファイルタイプの分離と等しくないことです。 現代の UI 開発では、コードベースを互いに織り交ぜる3つの巨大なレイヤーに分割するのではなく、それらを疎結合なコンポーネントに分割して構成する方がはるかに理にかなっています。コンポーネントの内部では、そのテンプレート、ロジック、スタイルが本質的に結合されており、実際にそれらを配置することで、コンポーネントがより一貫性と保守性に優れています。

名前が長いので以下、単一ファイルコンポーネントはSFC(Single File Components)と呼びます。

SFCってどうやったら使えるの?

vue.js

vue.jsを使ったプロジェクトであればvue-loaderを使えばすぐに使い始めることができます。
詳しくは公式を参照してください。

Riot.js

RiotはSFCが前提なっているため、普通に使っていけば自然にSFCになるはずです。

React

TODO

フレームワークなし

こちら(webpack-component-loader)が使えそうです。

新しいSFC CSS設計、その名はKaom(仮)

これから説明するSFC CSS設計をKaom(仮)と名付けます。

スタイル言語の選択

Kaom(仮)にはmixin機能と変数機能、import機能が必須です。
なのでスタイル言語にはこれらの機能があるものを選択しましょう。
逆に言えばこれら機能があればlessでもsassでもstylusでもなんでもいいです。
post-cssを駆使してもOKです。
(指定ディレクトリの中をまとめてimportする機能があるstylusがややオススメ。)
以下はstylusのコードで説明していきます。

ディレクトリ構成

src/
├ components/
│├ TOP.vue
│└ ...
├ styles/
│├ bases/
││├ _body.styl
││└ ...
│├ mixins/
││├ _card.styl
││└ ...
│├ _bases.styl
│├ _mixins.styl
│└ _variables.styl
└─ main.js

components

コンポーネントを配置します。
コンポーネント内のstyleをどのように書くかは後述。

styles

スタイルシートを配置します。

bases

サイト全体の設定を行います。
ここではhtmlタグや属性を指定してスタイルを記述していきます。
クラスやIDを指定してはいけません。

例えば以下のように書きます。

_lists.styl
ul, ol
  list-style-type: none
  margin: 0
  padding: 0

dl
  margin-bottom: $small-spacing

  dt
    font-weight: bold
    margin-top: $small-spacing
  dd
    margin: 0

bittersというCSSがあるので、こちらを参考にするといいと思います。
そのままコピーしてきてもいいかも。

mixins

再利用したいスタイルをmixinで定義します。

_shadow.styl
shadow(depth = 1)
  if      depth == 1
    box-shadow: 0  2px  5px 0 rgba(0, 0, 0, .16), 0  2px 10px 0 rgba(0, 0, 0, .12)
  else if depth == 2
    box-shadow: 0  8px 17px 0 rgba(0, 0, 0,  .2), 0  6px 20px 0 rgba(0, 0, 0, .19)
  else if depth == 3
    box-shadow: 0 12px 15px 0 rgba(0, 0, 0, .24), 0 17px 50px 0 rgba(0, 0, 0, .19)
  else if depth == 4
    box-shadow: 0 16px 28px 0 rgba(0, 0, 0, .22), 0 25px 55px 0 rgba(0, 0, 0, .21)
  else if depth == 5
    box-shadow: 0 27px 24px 0 rgba(0, 0, 0,  .2), 0 40px 77px 0 rgba(0, 0, 0, .22)
  else
    box-shadow: none

_bases.styl

basesディレクトリの中を読み込見ます。
stylusの場合、2行だけ、以下のようになります。

_bases.styl
@import './_variables.styl'
@import './bases/*'

_mixins.styl

mixinsディレクトリの中を読み込見ます。
stylusの場合、2行だけ、以下のようになります。

_bases.styl
@import './_variables.styl'
@import './mixins/*'

_basesと同じですね。

_variables.styl

サイト全体で使用する変数を定義していきます。
こちらもbittersを参考にするとどんなことを書くのかわかると思います。

main.js

エンドポイントとなるファイルです。
次の章で詳しく説明します。

質問: Utilityはどこに置くの?

答え: Utilityを作ってはいけません。

.mbs { margin-bottom: 10px; }のような汎用クラスはHTML内にスタイル情報を持ち込む悪しき習慣です。
詳しくはこちら(お前ら今すぐそのCSSフレームワーク使うのやめろ!)に書きました。

再利用したいスタイルはmixinを使いましょう。

各ファイルの読み込み方法

webpackを使った場合の各ファイルの読み込み方法です。

main.js
//
// styles
//
import 'normalize.css'; // 必要な場合
import './styles/_bases.styl';

エンドポイントなるmain.jsではnormalize.cssなどのリセットCSSと先程説明した_bases.stylのみ読み込みます。

_variables.styl_mixins.stylは以下のようにします。

webpack.config.coffee
# webpack 2.0

path = require('path')
webpack = require('webpack')

module.exports =
  entry: './src/main.js'
  output:
    path: path.resolve(__dirname, './dist')
    publicPath: '/dist/'
    filename: 'build.js'
  module:
    rules: [
      {
        test: /\.vue$/
        use:
          loader: 'vue-loader'
          options:
            loaders:
              stylus: [
                {
                  loader: 'style-loader'
                  options: sourceMap: true
                }
                {
                  loader: 'css-loader'
                  options: sourceMap: true
                }
                {
                  loader: 'stylus-loader'
                  options: sourceMap: true
                }
                {
                  loader: 'stylus-resources-loader'
                  options:
                    resources: [
                      path.resolve(__dirname, './src/styles/_variables.styl')
                      path.resolve(__dirname, './src/styles/_mixins.styl')
                    ]
                }
              ]
      }

こうすることですべてのコンポーネントから@importの記述無しで変数とmixinが使えるようになります。

コンポーネントの記述

著者の宗教上の理由により
- framework: vue.js
- template : pug
- script : CoffeeScript2
- style : stylus
をつかって説明させていただきます。

スタイルガイド

Vue.js コンポーネント スタイル ガイドを全面的に採用します。
Riotの場合はRiotJS Style Guideを参照してください。

ファイル名

Vue.js コンポーネント スタイル ガイドに従い、2または3語で命名します。
それをUpperCamelCaseでファイル名にします。例) UserList.vue

コンポーネント内のテンプレートとスタイル

CSS 命名規約

命名の一貫性を保つために命名規約を導入しましょう。
命名規約は色々有りますが、どれを使えばいいでしょうか?

単一ファイルコンポーネントでは、コンポーネントごとにスタイルが独立しているので名前の衝突の危険はありません。
この危険を回避するための意味合いが強いBEMなどは不要です。

rscssがわかりやすく、大げさでなく、使いやすいのでおすすめです。

Kaom's Primacy(仮)

上で「単一ファイルコンポーネントでは、コンポーネントごとにスタイルが独立しているので名前の衝突の危険はありません。」と言いました。
これはscopedオプションによる力です。
どのような力かこちらvue-loaderから引用して説明します。

<template lang="pug">
  .example hi
</template>

<style lang="styl" scoped>
.example
  color: red
</style>

これをコンパイルするとこうなります。

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

ここで追加されたdata-v-f3f3eg9という文字列はコンポーネントごとに固有のものです。
つまり、ランダムな文字列を付与することによって、別のコンポーネントへ影響を与えることを防いでいるのです。

このように自動的にセレクターにランダムな文字列を付与することでスタイルの漏れを防ぐことをKaom's Primacy(仮)と呼ぶことにします。

Kaom's Roots(仮)

templateはコンポーネント名と同じ名前(dash-case)のクラスで全体を囲みましょう。
このクラスを持ったタグをKaom's Roots(仮)と呼ぶことにします。

UserList.vue
<template lang="pug">
.user-list //- <- これがKaom's Roots(仮)
 h1 hello
</template>

<style lang="styl" scoped>
.user-list
  background-color: $theme-color;
</style>

以下のようにKaom's Roots(仮)が複数あるのはNGです。

UserList.vue_NGパターン
<template lang="pug">
.user-list1
  h1 hello
.user-list2
  h1 goodbye
</template>

Kaom's Roots(仮)divである必要はありません。
以下のようにheader, footer, main, form, ulなどを自由に使ってください。

MainHeader.vue
<template lang="pug">
header.main-header
  h1 hello
</template>

<style lang="styl" scoped>
.main-header
  background-color: $theme-color;
</style>

To be continued...

See also

Vue.js命名規則、命名のコツ


TODO

  • 説明加筆
    • 各フレームワークでのSFCの実装方法について
    • マジックナンバーの使用について
  • 用語の統一
  • 例の追加
  • 名前をつける(必要なら)
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away