SPAじゃないプロジェクトのための控えめなJavaScriptフレームワーク「Stimulus」

ReactやAngularのように、アプリ内のすべてのHTMLをJavaScript側で管理して描画するアプローチは大げさすぎる。ほとんどの場合において。

もちろん一定以上の複雑さがあればその辺のJavaScriptフレームワークを使った方が良い。が、それがやり過ぎになるプロジェクトは多い。というかその方が多い。それでも、それらのアプローチではやり過ぎになってしまう場合でも、秩序のあるコードを書き続けられないと当然苦しい状態へ向かう。

この微妙な(そして多数派であるはずの)環境を適度に管理できる枠組みが必要だ。これまでちょうどいい解決策を見つけられずに苦労してきた。Web Componentsは課題を解決するかもしれないがまだ来ない。そこで出会ったのがStimulusだ。

いわく、Stimulusは既存のHTMLのための控えめなJavaScriptフレームワーク。サーバーサイドレンダリングされたHTML(これはIsomorphicとかじゃなくて普通の)に対して、特定の属性が付与されていれば対応するコントローラを初期化する。

まずHTMLにはdata-controller属性、必要に応じてdata-targetdata-action属性を追加する。

<!-- HTML片は好きなところに -->
<div data-controller="hello">
  <input data-target="hello.name" type="text">

  <button data-action="click->hello#greet">
    Greet
  </button>

  <span data-target="hello.output">
  </span>
</div>

対応するコントローラを作成すると、Stimulusはその振る舞いを登録する。

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "name", "output" ]

  greet() {
    this.outputTarget.textContent =
      `Hello, ${this.nameTarget.value}!`
  }
}

これのよくできてるところは、単に読み込み時にだけコントローラの初期化が行われるわけでなく、ドキュメントへコントローラに対応するDOMが挿入されるたびに自動的に初期化される。削除されたら勝手に破棄される。さらにそのためのライフサイクルフックがある(initialize/connect/disconnect)

// 好きなタイミングでDOMを挿入すれば勝手に初期化される
document.body.innerHTML = `<div data-controller="hello">
  <input data-target="hello.name" type="text">

  <button data-action="click->hello#greet">
    Greet
  </button>

  <span data-target="hello.output">
  </span>
</div>`

// 好きなタイミングでDOMを削除すれば勝手に破棄される
document.body.innerHTML = ''

これは、StimulusはそもそもTurbolinksと協調するために作られたからだ。だからTurbolinksと組み合わせて使うこともできる。

これだけ書けば求める人にはグッと来るものがあるはずだ。詳しい使い方は公式ハンドブックのサンプルコードを見れば大体わかる。中途半端にまとめてもすぐに腐るので触らない(APIの変更が多いわけではないが)。思想的な序文だけ訳しておくことにする。

次からStimulusの起源(The Origin of Stimulus)の翻訳。


私たちはBasecampでたくさんのJavaScriptを書いているが、現代的な意味での「JavaScriptアプリケーション」は作成していない。すべてのアプリケーションはサーバーサイドレンダリングされたHTMLをベースにして、それらに付与する形でJavaScriptを追加している。

これが雄大なモノリスのやり方である。Basecampは、Ruby on Railsを使って作成されたコントローラ、ビュー、モデルの単一セットを持ち、ネイティブモバイルアプリを含むいくつかのプラットフォームで動作する。多くのプラットフォームがあるにも関わらず、小さなチームでやっていくためには、1つの共有インターフェイスを1箇所で更新できることが重要だ。

すると昔のように生産性の高いパーティーができる。1人のプログラマーが間接的なシステムや分散システムのレイヤーに縛られることなく、貪欲に進歩できる時代に逆戻りする。誰もにとっての究極の目標は、サーバーサイドアプリケーションがクライアントサイドアプリケーションのためにJSONを生成するだけのものになる前に戻ることだ。

そのようなアプローチに価値がないというわけではない。しかしそれは多くのアプリケーションにとって、そしてBasecampにとって、複雑性と生産性の低下をもたらす。

シングルページのJavaScriptアプリケーションにメリットはある。ページ全体を更新せずにより高速で流動的なインターフェースを実装できることだ。

Basecampもそのようだと感じて欲しかった。まるで流れに乗って、レンダリングをクライアントサイドで行うようにすべて書き直したか、完全なネイティブアプリになったかのように。

この要求が私たちをTurbolinksとStimulusという強力な組み合わせに導いた。

Turbolinkは高く、Stimulusは低く

私たちのこの新しい控えめなJavaScriptフレームワークであるStimulusを紹介する前に、これを使ってTurbolinksの命題を要約しておこう。

TurbolinksはGitHubで開発されたpjaxと呼ばれるアプローチから派生している。基本的な概念は同じままだ。

ページ全体のリフレッシュに時間がかかるのは、ブラウザがサーバーから送信される大量のHTMLを処理しなければならないからではない。それに関してブラウザは非常に優れており、非常に高速だ。そしてほとんどの場合、HTMLペイロードJSONペイロードよりも大きくなる傾向がある事実も問題にはならない。要因は、CSSJavaScriptを再初期化して再度ページに適用する必要があることだ。ファイル自体がキャッシュされているかどうかに関係なく。CSSJavaScriptのサイズによってはかなり遅くなる。

この再初期化を回避するためにTurbolinksは、シングルページアプリケーションと同じように永続的なプロセスを維持する。リンクのクリックイベントを乗っとり、Ajaxによって新しいページをロードする。サーバーは完全な形式のHTMLドキュメントを返す。

この戦略だけでも多くのアプリケーションを非常に速く感じさせられる。Basecampではページからページへの移動が約3倍速くなった。この高速化はアプリケーションに反応性とシングルページアプリケーションの魅力の大部分を占めていた流動性を与える。

しかしTurbolinksだけでは話の半分にしかならない。ページの読み込み以降、1つのページ内にはあらゆる振る舞いが登録される。要素の表示と非表示の切り替え、クリップボードへのコンテンツのコピー、リストへの新しいTodoの追加、および昨今のウェブアプリケーションに関連づけられているその他すべての操作を行うインタラクション。

Stimulus以前、Basecampはこれらをさまざまなスタイルやパターンを使って実装していた。コードの中にはjQueryを使ったちょっとしたものもあれば、同じようなサイズの単純なバニラJavaScriptもあり、また、より大きなオブジェクト指向のサブシステムであるものもあった。これらすべてはいつも、明示的なイベント処理によってdata-behavior属性を無効にしていた。

data-behaviorJavaScriptによる振る舞いをビューに依存せずに管理するための方法論。詳しくはCode Spelunking in the all new Basecamp – Signal v. Noiseを参照)

このような新しいコードを追加するのは簡単だったが、包括的な解決策ではなく、社内のスタイルやパターンが多すぎた。そのためコードの再利用が難しくなり、新しい開発者が一貫したアプローチを習得することが困難だった。

Stimulusの3つの核となるコンセプト

社内の良いパターンから抽出した次の3つが主要な概念となった。

  • コントローラ
  • アクション
  • ターゲット

Stimulusはこれらを控えめで小さなフレームワークとしてまとめたもの。

対応付けられたHTMLを見ると、Stimulusはプログレッシブエンハンスメントとして読み込まれるよう設計されていることがわかる。これにより、どの振る舞いが作用しているのかをテンプレートを見て理解できる。例を示そう。

<div data-controller="clipboard">
  PIN: <input data-target="clipboard.source" type="text" value="1234" readonly>
  <button data-action="clipboard#copy">Copy to Clipboard</button>
</div>

例を読むと何が起きているのかよくわかる。Stimulusについて何も知らなくても、あるいはコントローラのコードを見なくても。これは対応するイベントハンドラーが外部JavaScriptファイルに存在するHTML片を読むことと大きく異なる。また、現代の多くのJavaScriptフレームワークで失われている関心の分離も維持している。

ご覧のように、StimulusはわざわざHTMLを作成するのではなく既存のHTMLドキュメントに取り付けられる。ほとんどの場合、HTMLはページ読み込み時(最初の読み込みまたはTurbolinks経由)に、またはDOMに作用する起点になるAjaxリクエストによってサーバー上でレンダリングされる。

Stimulusはその既存のHTMLを操作することに関心がある。例えば、要素を非表示にしたりアニメーションさせたり、ハイライトさせたりするCSSクラスを追加すること。グループ内の要素を再配置させたりすること。UTCを地方時の日時表記に変換するように要素の中身を操作することなど。

Stimulusに新しいDOM要素を作成させたいこともあるが、それも自由に行える。将来的には簡単にできる方法を提供するかもしれない。しかしこれはごく少数のユースケースだ。焦点は要素の作成でなく操作にある。

Stimulusが主流のJavaScriptフレームワークとどのように違うのか

そのため、Stimulusは現代のJavaScriptフレームワークの大部分とはかなり異なっている。ほとんどのフレームワークは何らかのテンプレート言語を介してJSONをDOM要素に変換することに焦点を当てている。それらのフレームワークでは多くの場合、空のページを作成し、そうしてJSONからレンダリングした要素だけをそのページに追加する。

主流のJavaScriptフレームワークとStimulusでは状態の扱いも異なる。ほとんどのフレームワークには、JavaScriptオブジェクト内に保持した状態に基づいてHTMLをレンダリングする機能がある。Stimulusはその正反対だ。状態はHTMLに格納されるため、ページが変更されるたびにコントローラを破棄できる。そしてキャッシュされたHTMLから再び表示されてすぐの状態から再初期化できる。

これはまったく異なるパラダイムだ。現代のフレームワークに慣れているベテランのJavaScript開発者の多くはこれをあざ笑うに違いない。React+Reduxのような巨大な渦巻きの中で、アプリケーションを維持するために必要な複雑さと労力に満足しているなら、Turbolinks+Stimulusには魅力を感じないだろう。

一方で、現代の技術が示唆するような複雑さとアプリケーションの分離が不要だと感じている場合は、このアプローチに退避することになる。

Stimulusや関連するアイデアは現実世界から抽出された

Basecampではこのアーキテクチャをいくつかの異なるバージョンのBasecampや他のアプリケーションで長年使用してきた。GitHubも同様のアプローチを使って大きな効果を得ている。これは「現代」における、ウェブアプリケーションがどのようなものであるかの主流の理解に対する有効な代替手段である。それも非常に説得力のある。

実際、Stimulusは私たちがRuby on Railsを開発したときにBasecampで使った秘伝のタレに似ている。現代の主流のアプローチは不必要に複雑になっている感覚がある。私たちは本当はより多くのことをより速く、より少ないリソースで実現できるように思う。

あなたはそのいずれかだけを選択する必要はない。StimulusやTurbolinksはよりヘビーな他のアプローチと組み合わせて使うと効果的だ。もしあなたのアプリケーションの80%がヘビーな仕組みを必要としないのであれば、私たちのようにStimulus+Turbolinksを使うことを検討して欲しい。そしてアプリケーションの中の、実際に恩恵を受けられる部分でヘビーな仕組みを展開すると良い。

Basecampでは必要に応じていくつかの強力なアプローチを採用している。例えば、私たちのカレンダーはクライアント側のレンダリングを使用する傾向にある。私たちのテキストエディタTrixという完全な形のテキストプロセッサで、Stimulusコントローラの位置付けとしては意味を成さない。

一連の代替フレームワークはできるだけ重労働を回避することを目的としている。この単純なモデルでうまく機能する多くのインタラクションに対して、リクエスト・レスポンスパラダイムの範囲内に留まる。そして必要性が真に迫っていれば高価なツールに手を伸ばす。

何よりもこれは、より労力がかかる主流のアプローチを使う大きなチームと、忠実さで競争したいと思っている小さなチームのためのツールキットである。

試してみてごらん。

デイヴィッド・ハインマイアー・ハンソン

サーバーサイドのみのテンプレートエンジンとしてのReact

最近の仕事ではJSがあんまりなくてページ数はそこそこあるみたいなサイトを作ってることが多い。作り方として、コンポーネントごとにPugのmixinとかNunjucksのmacroで抽象化してマークアップが壊れないようにしてるんだけど、これらだとコンポーネントを実装するための機能として微妙。具体的には、ノードを挿入できる箇所が1箇所に限定されてることとエディタの補完がない。

mixin Disclosure(params = {})
  -
    const props = Object.assign({
      initialExpanded: false,
      detailsId: ulid(),
    }, params)

  .Disclosure(role="group")&attributes(attributes)
    button.Disclosure__summary(type="button" aria-expanded=String(props.initialExpanded) aria-controls=props.detailsId)!= props.summaryContent
    .Disclosure__details(id=props.detailsId hidden=!props.initialExpanded)
      block

+Disclosure({ summaryContent: '最高の<em>コンテンツ</em>' }).u-mt5
  p 立派なインターネットコンテンツになったなあ。
{% macro Disclosure(params = {}) %}
{% set rootClass = params.rootClass %}
{% set initialExpanded = params.initialExpanded %}
{% set summaryContent = params.summaryContent %}
{% set detailsId = params.detailsId | default(ulid()) %}

<div class="Disclosure {{ rootClass }}" role="group">
  <button class="Disclosure__summary" type="button" aria-expanded="{{ initialExpanded }}" aria-controls="{{ detailsId }}">{{ summaryContent | safe }}</button>
  <div id="{{ detailsId }}" class="Disclosure__details" {% if not initialExpanded %}hidden{% endif %}>
    {{ caller() }}
  </div>
</div>
{% endmacro %}

{% call Disclosure({
  rootClass: 'u-mt5',
  summaryContent: '最高の<em>コンテンツ</em>'
}) %}
<p>立派なインターネットコンテンツになったなあ。</p>
{% endcall %}

例ではsummaryContentの中身が平坦化されたテキストだと限らないので、仕方なくそこだけ生のHTML書くとかになっちゃう。テンプレートエンジンの機能が使えなくなるのでコンポーネント入れ子になったりすると詰む。

VS Codeではシンタックスハイライトしてくれるだけで補完とかはなんも出ない。tsxでReact Componentのpropsの型までわかるのと比べるとだいぶ非効率的になる。Pugのmixinとかはそもそもコンポーネント専用の機能じゃないというのもあると思うし、言語のコミュニティの勢い的にも大きな進歩は望めなそう。自分で拡張書くほどのガッツもない。

じゃあもうtsxそのまま使えばいいじゃんって感じになってきたのでサーバーサイドテンプレートエンジンとしてReactを試した。<body>の中身が空の状態から始めるって話じゃなくて、この場合ではReactはクライアント側に一切介入しない。サーバーでしか動かさない。

似たところだとDocusaurusが同じような発想でReactを使ってる。Facebook製だからだと思うけど。

素直に長いものに巻かれるとNext.jsとかGatsbyJS使えばいいんだけど、あんまりJSを使わないサイトだとやり過ぎになったり、あと納品形態がいろいろなのでいろいろある(ビルド後のHTMLファイルを人間が編集できるようにしておきたい)みたいな理由。

最近Eleventyという静的サイトジェネレータが気に入ってるのでこれを使う。便利なのがテンプレートエンジンの選択肢が豊富なところで、ピュアなJavaScriptでテンプレートを書くこともできる。

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { Layout } from './components/Layout'
import { Disclosure } from './components/Disclosure'

// https://www.11ty.io/docs/data/#eleventy-provided-data-variables
type DefaultProvidedData = {
  pkg: unknown
  page: {
    url: string
  }
  collections: unknown
}

type PageData = {
  title: string
}

module.exports = class {
  data(): PageData {
    return {
      title: 'Home',
    }
  }

  render({ page, title }: DefaultProvidedData & PageData) {
    return (
      '<!doctype html>' +
      ReactDOMServer.renderToStaticMarkup(
        <Layout url={page.url} title={title}>
          <Disclosure
            rootClassName="u-mt5"
            summaryContent={
              <>
                最高の<em>コンテンツ</em>
              </>
            }
          >
            <p>立派なインターネットコンテンツになったなあ。</p>
          </Disclosure>
        </Layout>,
      )
    )
  }
}

こういうのをページごとに書いていく。よさそう。

JSXの代わりにJSXっぽい構文をTagged templatesで書けるdevelopit/htmを使えばプリコンパイルもなくせそうと思ってそれも試した。

const htm = require('htm')
const vhtml = require('vhtml')
const Layout = require('./components/Layout')
const Disclosure = require('./components/Disclosure')

const html = htm.bind(vhtml)

module.exports = class {
  data() {
    return {
      title: 'Home',
    }
  }

  render({ page, title }) {
    return (
      '<!doctype html>' +
      html`
        <${Layout} url=${page.url} title=${title}>
          <${Disclosure}
            rootClass="u-mt5"
            summaryContent=${
              html`
                <span>最高の<em>コンテンツ</em></span>
              `
            }
            ><p>立派なインターネットコンテンツになったなあ。</p><//
          >
        <//>
      `
    )
  }
}

確かにプリコンパイルはなくせたけど、そもそも解決したい補完が弱い。TypeScriptじゃないというのはあるけどもうちょっとがんばって欲しい。lit-htmlの拡張を使ってて進化を期待はできそうではある。

テンプレートの構文の細かい仕様を確認したりするのが地味にめんどい。あとReact.Fragmentみたいなやつが実装されてないとか。そういう風に考えるとしばらくはReactよりいい選択肢はなさそう。

加えて別の視点だとWeb Componentsを使って解決できる線はある気がする。コンポーネントはCustom Elementsで実装して、それをNunjucksとか今あるテンプレートエンジンで普通に使う。クライアントサイドから見ればオーバースペックだけど、JSで実装する部分があればこっちの方が安心できる。

---
layout: base
title: Home
---

<x-disclosure class="u-mt5">
  <span slot="summary">最高の<em>コンテンツ</em></span>
  <p>立派なインターネットコンテンツになったなあ。</p>
</x-disclosure>

Custom Elementsの補完は微妙だけど、開いてるファイルからcustomElements.define(...)を拾っていい感じにしてくれるようになるのを期待できなくもない。というのを書きながら希望的観測過ぎる気はしてきた。

今回試したもののソース全部入りのリポジトリ

完全に問題を解決する場所を間違えている、みたいな意識はない。

なゆくんとのおもいで

ナユコロニー2 Advent Calendar 2018の8日目の記事です。

なゆくんはおぼえていますか? ぼくです、しばらくぶりだね。しらないうちになゆくんはとうきょうではたらくようになっていたんだね。きくとそのわかさでほんをかいたというじゃないか。じつはぼくもきになってとりよせてよんでみたんだけど、せんもんがいのないようだからむずかしくてさっぱりだったよ。しらないうちにたくさんべんきょうしていたんだね。プログラマーというんだっけか? やっぱりいいね、てにしょくつけるというのは。ぼくもそういうしごとをえらべばよかったのかなあとすこしこうかいしています。さて、ぜんだんのはなしはこれくらいでいいかな。ほんだいのまえのはなしがすこしながくなってしまうことがぼくのわるいくせです。

さいしょにおぼえていますかときいたけどわざわざきくまでもないよね。なゆくんはむかしからとてもあたまのいいこだったから、ぼくがこれからなにをつたえるかなんてもうしっているよね。というよりきっと、なんねんもまえから、きょうというひにこのしらせがくるということをいちにちたりともわすれられなかったんじゃないかなあ。でもしかたない、ぼくはわるくないよ、ぜんぶなゆくんがかんがえたことなんだからね。またおしゃべりをしてしまったね、はやくほんだいをだったね。

なゆくんはあしたのしはつでじもとにかえります。ひるすぎにはついてるだろうから、ふるさとのくうきをすってかんがいぶかいきもちになったりせずに、すぐにそうこのまえまできてください。もちろんじもとのしりあいにみつかったりしたらだめだよ。ぼくはうらのあきやにかくれておもてのようすをみてるからついたらすぐにわかるとおもいます。さいきんぼくはずっとなゆくんのためにそうこのようすをしらべてあげてるんだけど、ふたつきにいっかいさとしのおばさんがみにくるだけでそれいがいのひとはよりつきもしてないね。なゆくんはほんとうにラッキーなひとだよ。

わかってるってば。なゆくんがしりたいのはそんなことじゃなくて、あのせいまいきのおくにあるたなをだれもさわってないかってことだよね。だれもあんなところにあるたなをつかおうとするはずないじゃないか。そうこはずっとぎゅうぎゅうずめでせいりするおとこでがたりないってずっとさとしのおばさんがなげいてるよ。このしごとがおわったらてつだってあげるといいんじゃないかな。もちろんぼくはごめんだけどね。どっちにしてもまずはれいのごとく、そうこにたくさんあるガラクタとゆかとのすきまをとおったらたなにかくしてあるかぎとごたいめんだ。あのやせぎすだったなゆくんがとうきょうのおいしいごはんでふとってつっかえてしまわないといいけどね。

かぎがてにはいったらどうするかはわかるよね。なゆくんはあたまがいいからちゃんとやりとげてくれるとしんじてます。ぼくだってまたともだちをなくすのはさみしいからね。

[WIP]CSSの命名について

下書き供養 Advent Calendar 2018の9日目の記事です。

CSS命名規則じゃない命名についての体系的な何かができないかを考えていた。どういう要素に命名するためにどのように言葉を選定するのか、命名という切り口で具体的に説明する文書みたいなものを見たことない気がする。

きっかけはクラス名に使える単語リストみたいなのが流れてきたのを見て感じたなんか違う感。それはそれでわかるんだけど、そもそもどういった場合にどのような種類の言葉を探すのか、要素をどの視点から捉えて説明するのかみたいな話が先にあった方が良さそうに思う。

一旦コンポーネント名の命名に基準を合わせて、UI的な命名ドメイン的な命名に分解できるのではと考えた。有名なところではMCSSのベースとプロジェクト、及びそれに影響を受けたであろうFLOCSSのComponentとProjectのレイヤー分けに近いと思う。ただしこれらはレイヤーとして分離した先にある個々のコンポーネント名についての詳細な説明がない。どちらのレイヤーに属するのかわからないと言われるのもその辺が鍵になってきそう。

  • CardとかModalとかはUI的、NewsとかUserとかはドメイン
  • 特定のドメイン知識に依存しないコンポーネント、そのサイトの中では汎用的に使えるルールのコンポーネントはUI的な命名が妥当
  • デザインの共通化が進むとUI的な命名が増えてくる
  • コンポーネントを小さく砕いて共通化していくとUI的な命名に向かいやすい
  • 規模によってはドメイン的である方がネームスペース的には安全である可能性が高い
    • 同じ機能が出現する場所はだいたい決まっているし、だいたい同じUIで表現される
    • カンプよりワイヤーフレームの方が一貫性があることが多い、表現することの密度の問題
  • ドメイン的な命名はスケールしない、反面捨てやすい
  • UI的な命名ドメイン的な命名を掛け合わせることでより端的な命名に向かうこともある
  • Articleとかどっち側? 記事というUIで記事というデータ型。そのデータ型が特定のドメイン特有のものでないのであればUI的と言っても良い?
  • CardというのかTeaserというのかとかはUI側だけどそれぞれ別の側面から見た視点っぽい。Cardはパターンっぽい、Teaserは用法っぽい。対応する両方はイコールじゃなくて全然違いものを指す可能性があるけどイコールなこともある
  • Primary buttonとBlue buttonとかも違うものだけど、ドメイン的ではないという意味では同じものとして説明できる、Blueっていう名前の商品を買わせるための専用ボタンなのであればドメイン的かもしれない
  • ややこしい用語があったりするとなにがUI的な言葉でなにがドメイン的な言葉なのかプロジェクトごとにはっきりさせとく必要があることもありそう

しばらくはまとまる気配がない。いつか更新するかもしれないし永遠にしないかもしれない。

たまごまぜごはん

TKG Advent Calendar 2018の2日目の記事です。

人が卵かけご飯と言ったとき、僕の頭の中で連想されるのは「卵まぜご飯」と呼んだ方が適切であろうと思われるものだ。幼少期に母親の紹介を経ていわゆる卵かけご飯との出会いを果たしたとき、それは当たり前のように混ぜ合わされた後の状態になっていて、さも混ぜ合わされるという工程を経て初めて卵かけご飯という名を授かるのだと思わされた。

ご飯とおかずは均等な味の比率を保っていなければならない。当時の僕は暗黙的にこのようなルールを遵守していた。特定のおかずにご飯と合う種類の味がどの程度含まれているかによって同時に口に含むべきご飯の量は決まる。これは単なる目安というより、可能な限り守られなければならない規定だった。特定のおかずに対して必要な量のご飯が用意されていないとき、味の総量としてご飯を超えるだけのおかずは食べることができない。そしてそのルールを犯してしまうことに強い罪悪感があった。

チキンライスやチャーハンなどの色付きご飯は例外として扱われた。ルールに則るならば、その味付けの量が対応するご飯の量を超えている場合は均等になるだけの「白いご飯」を増やして中和するというのが筋だ。しかし僕の家庭では色付きご飯の日には炊飯器の釜は「洗い待ち」状態で食事が開始された。これは色付きご飯の日には白いご飯を提供する意思がないということを示す。僕はこの色付きご飯の場合にはルールに当てはまらないとして自分を納得させた。従って色付きご飯の日に白いご飯を食べることができる状態になっていても味を中和させることは必要ではない。

他に少し複雑な対応として、ちょっと茶色いキノコ混ぜ炊き込みご飯のような、色付きご飯ではありながら味の比率としてご飯の方が多いという薄い色付きご飯パターンもあった。これまでのルールに則れば、薄い色付きご飯にはおかずの味の量が不足しているためその分の別のおかずが入り込む余地があるはずだ。つまり特定のおかずを食べるときに、白いご飯の場合に比べれば必要なご飯の量は増えるが、薄い色付きご飯でも味の比率を均等にするルールを守ることができる。しかし当時の僕には別のルールとして、別の種類のおかずの味を混ぜてはならないという決まりがあった。そのため薄い色付きご飯を別のおかずと同時に食べると規定違反になる。

ただそうはいってもこれではその日の食事を食べることができない。これに対する対策として、僕は薄い色付きご飯は白いご飯と同一であるというように解釈した。このご飯には一切の色などなくて、白いご飯とまったく変わりがないのだと。もちろんこの解釈には無理があった。強く念じようとも薄い色付きご飯はやはりおかずとしての佇まいを残しているのだ。僕はこれに対する打開策を講じることができず、薄い味付けご飯の日は「捨て」であるということにした。

こうした背景を振り返って自分はなぜ卵かけご飯は卵混ぜご飯であると捉えているのかが分析できてきた。まず卵の味がご飯全体に均等に浸透していなければ食べることができない。それに塩や醤油が混ざると色付きご飯のパターンとして取り扱われる。そして色付きご飯であるということはこれまでのあらゆるルールの免罪符となり無限の進化の可能性を獲得するのだ。

卵と納豆を混ぜたご飯に目玉焼きを乗せて食べました

CSS in JSはCSSの書き方をどのように変えるのか

CSSの難しさの根源はセレクタにある。CSS設計のための方法論ではどのようにしてセレクタと関わるべきかについて語られる。

その関わり方がCSSのみで実現できなければならないという制約を捨てたのがいわゆるCSS in JSの類(定義的に微妙なやつも全部ひっくるめて)だ。可能性は一気に広がり無数のライブラリが生み出された。

ある程度の期間を経ていくつかの着目すべきアプローチが見えてきた。これから僕はどのようにセレクタと関わっていくべきかという視点で記してみたい。

擬似スコープ

通常CSSセレクタにはスコープはないが、HTMLやCSSハッシュ値を付与して特定のコンテキストを擬似的に閉じてしまおうというアイデア。実装としては、Vue.jsの単一ファイルコンポーネントAngularのコンポーネントスタイルstyled-jsxなど。関連するウェブ標準技術としてShadow DOMがある。

例えば、次のように書かれたスタイル宣言は同一コンポーネント内にしか適用されない。

<template>
  <p>Hello World!</p>
</template>

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

このコンポーネントの外にp要素があったとしても適用範囲外になる。

書き方としては、CSS in JSの類のアプローチの中では最もこれが普通のCSSに近い。スコープがあるということ以外、実質的に変わりがない。セレクタがグローバルであるという難しさを解消するためにスコープを作るということは、発想としてごく普通で受け入れられやすい。

BEMで規約として作っていたブロックというルールを仕組みとして取り入れたとも言える。BEMらしき姿は見えなくなっていても、これはより安全で楽になったBEMだ。

安パイだと思う。

スタイル宣言が付属したHTML要素を作る

styled-componentsを使うと、最初にスタイル宣言が付属したReactコンポーネントを作成し、それを配置していくという形でスタイリングを行うことになる。

import React from 'react'
import styled from 'styled-components'

const Wrapper = styled.button`
  background-color: lightgray;
  border: 1px solid gray;
`

const Icon = styled.img`
  width: 1.25em;
  height: 1.25em;
`

const Text = styled.span`
  margin-left: 1em;
`

const Button = () => {
  return <Wrapper type="button">
    <Icon src="/icon.svg" alt="" />
    <Text>click me</Text>
  </Wrapper>
}

従って、個々の宣言ブロックの中身を実装した後に作成した要素を配置して画面を組み上げていくという流れになるのが自然だ。通常、スタイリングはHTMLを書いた後に行う。対して、styled-componentsは宣言の記述のために個々の要素のReactコンポーネント化を要求するため、全体のHTMLが完成する前に個々の宣言ブロックの中身を実装することに意識を向けなければならない。

しかし実際にそのような流れで実装することには無理がある。そのためstyled-componentsを利用したスタイリングには不自然さが伴う。

宣言ブロックの中身は適用される対象の要素だけを見ても完成させられない。継承させるプロパティの兼ね合いや、兄弟要素とのレイアウト上の関係性など、対象となっている要素以外の要素も意識しなければどのような宣言をするかは決められない。

またこうして作成したReactコンポーネントを配置していく際にも難がある。どこにどの要素を配置するかは、それぞれのHTML要素の種類に依存して決まる。にも関わらずこれらを配置していく際には要素の種類が一目ではわからなくなる。さらには単なるスタイルが付属するコンポーネントなのか別の場所から読み込んだ真っ当なコンポーネントなのかも不明瞭になる。

分解したReactコンポーネントを利用するとき、その中の実装が見えないというゆえの扱いにくさがある。先述したようなHTMLやCSSとしての構造の問題がその一部だが、大抵はそれを上回るだけのコンポーネント化による利点がある。しかしstyled-componentsは全てのHTML要素をReactコンポーネント化することを要求してくる。剥き出しになっていて欲しい部分も覆い隠されてしまう。

最初にHTMLだけを組み上げた後に個々の要素をstyled-componentsに置き換えることもできるが、明らかに非効率的だ。結局は読みにくくもなってしまう。

ごく小さなコンポーネントであればともかく、HTMLとしてそれなりの大きさのコンポーネントを実装しているとまるでもう無理になってしまった。

HTML要素にスタイル宣言をリンクする

宣言ブロックを基にしてそれに対応するユニークなクラス名を生成するというアイデアもある。そのクラス名はHTML要素のclass属性を通してリンクされる。

/* style.css */
.wrapper {
  background-color: lightgray;
  border: 1px solid gray;
}

.icon {
  width: 1.25em;
  height: 1.25em;
}

.text {
  margin-left: 1em;
}
// button.js
import styles from "./style.css"

const Button = () => {
  return <button className={styles.wrapper} type="button">
    <img className={styles.icon} src="/icon.svg" alt="" />
    <span className={styles.text}>click me</span>
  </button>
}

これはCSS Modulesの例。

生成されるクラス名をCSSファイルから読み込み、対象の要素と宣言ブロックを直接繋ぎ合わせる。CSSファイル内のセレクタのようなものはクラス名がマッピングされるオブジェクトのキーに過ぎない。

セレクタをユーザーから隠して宣言ブロックと直接リンクさせるようにしたという意味ではstyled-componentsに近い。このアイデアが優れているのは、単にリンクさせるようにしただけであるというところ。

セレクタというのは書きやすくかつ読みにくいものだ。付属する宣言ブロックがどの要素に適用されるものなのか、実際に実行してみるまで本当に信頼はできない。このアプローチでは、スタイルを適用するためにはクラス名となる文字列を対象の要素に直接繋ぎ合わせるという前提ができることで、コンパイル前の段階でその関係性を確実に保証できる。

ただしCSS Modulesはビルドを複雑にしてしまう。styled-componentsが流行った理由は採用の手軽さにもあるのだろう。

幸い、このようにスタイル宣言をリンクさせるというアプローチが可能なライブラリとしてemotionがある。emotionはstyled-componentsと同等の機能の他に、The css Propという宣言ブロックを基にユニークなクラス名を生成できる機能を備えている。

import React from 'react'
import { css } from 'emotion'

const wrapperClass = css`
  background-color: lightgray;
  border: 1px solid gray;
`

const iconClass = css`
  width: 1.25em;
  height: 1.25em;
`

const textClass = css`
  margin-left: 1em;
`

const Button = () => {
  return <button className={wrapperClass} type="button">
    <img className={iconClass} src="/icon.svg" alt="" />
    <span className={textClass}>click me</span>
  </button>
}

さらにこのようにJavaScriptでスタイル宣言を管理することで、未使用の宣言ブロックを検出できるようになるという利点がある。ESLintやTypeScriptを使用すれば、デザインの変更時などに不要になったスタイルを確実に取り除けるようになる。コードを整理するという観点から、これまでのCSSではとても実現できなかったことだ。

感想

CSS in JSとかそんなものやめてしまえけしからんという気持ちもわかる。けど、これまでのCSSで事足りるというのは大抵、道具として完璧で最高にフィットしてるという感じでなくて、まあまあ不満もあるけど妥協してやっていけるよくらいの温度感のはずだ。

僕自身これ系のものを割と食わず嫌いしていたけどそれなりに学ぶこともあった。ので、とりあえずこれでなにか書いてみればよいのではという感想。

帰ってきた彼

姿を消したのは一夜の夢だったかのように、僕はあのころと何も変わらない彼と話をしていた。慣れというのは恐ろしいものだ。彼は再び当たり前の存在に舞い戻った。

「今でもそれを転がしているし、それなりに好きだよ」という話を聞いた。昔の僕は少し気負い過ぎていたせいか、反射的にそれを否定してしまっていた。とはいえ僕も少しは大人になれたのかもしれない。「君が好きならいいと思うよ」。投げやりになったのではなくて、本心からそんな言葉が出た。

たぶん人は自分の姿を消してしまいたいと思うことがたまにあるのだろう。事情はいろいろあるかもしれないけど、そうしたければそうするしかない。これまでそこにいた人が翌日すっかり影も形もなくなってしまっても、僕たちはそれを受け入れる他ないのだ。

彼はそのことを悪びれる様子もなく、その必要もないと思わされた。僕は日本人の平均以上には自由に生きているという自負があったが、それでも自分にまとわりついている鎖の重さを再確認することになった。

なにはともあれ彼には姿を消すという実績ができた。以降はもうないのかもしれないけど、一度あったことなので二度目もあるかもしれない。きっとそのときも僕にはなにもできないのだろう。僕が彼の失踪から学んだのは、諦めるのは諦めないのと同じくらい重要であるということだ。

それでも彼は今ここにいる。僕は楽しくやれてるし、彼の気持ちはわからないが、再び姿を消すようなことはしばらくしないはずだ。しばらく経ってからのことはわからないが。けれど今が楽しくやれてるということはおそらく素晴らしいことなのだ。今を感じることができるのは今だけなのだから。