翻訳:Rich Harris「形而上学とJavaScript」に関する見解(ReactによるDOMの抽象化の不完全性について)
この記事は「Thoughts on Rich Harris’ “Metaphysics and JavaScript”」の日本語訳です。Svelteのコーディングスタイルや記事として言及している講演のスライドも併せて参照しなければ理解しづらい内容です。
公開にあたっては著者のJim Nielsen氏に許諾をいただいています。
要旨としては、ユーザーにとってSvelteは実際のDOMとのメンタルモデルのギャップが少なく感じられるデザインであって、結果的に、Reactより理解しやすいプログラミングモデルになっているという話です。
Svelteの考案者であるRich Harris氏は、Reactのプログラミングの側面を批判する「形而上学とJavaScript」というタイトルの最近の講演のスライドを共有しました。この講演には非常に説得力があり、Reactを利用した私の経験にも当てはまるとわかりました。
この記事では彼の論点のいくつかを反芻して、Reactでのプログラミングに関する私自身の感情をさらに解明し、明確にするために役立てたいと思います。
はじめに:私はReactが大好きで毎日使っています。以降の話はそれを変えてしまうものではありません。
UIは状態からなる関数であるか?
Richは講演の冒頭で、「神聖な牛の中でもっとも神聖なもの」(訳注:ヒンドゥー教では牛は神聖な存在であり、原文のsacred cows
は神聖なるものという慣用句)を批判する計画であることを明らかにしました。UIはアプリケーションの状態からなる関数であるということについてです。
UI = F(STATE)
この公式化こそがReactのプログラミングモデルの核心ですが、しかしこれが間違いであると主張するつもりはありません。私はこれが不完全であると主張します。Reactアプリを作ったときに実際になにが起こっているかの説明というよりは、イデオロギー的な主張であるということです。そしてこれは私たちがコードについてどう考えるかについて、より深い真実を曖昧にしてしまう思考様式を表していると私は主張します。
Gut punch.
では公式UI=F(STATE)
はどのように間違っているのでしょうか? ああ、でも彼はそれが「間違い」だとは言わずに「不完全」だと言いました。彼は「UIは状態からなる関数である」というのはReactアプリを作ったときになにが起こっているかを正確に説明するものではないと言います。
私はRich氏の言葉を引用していますが、彼が言ったのは、UIがアプリケーションの状態からなる関数であるという考え方は抽象化であり、うまく機能してかなりあなたを助けますが、それでもただの抽象化だということ。抽象化は便利ですが、それでも素材の上にあるレイヤーであることに変わりはありません。下層の素材の上をきれいに塗装できないとわかったときには非常に厄介になることがあります。抽象化の欠点を克服するための一貫した戦術を見つけられなければ、火事が起こり、フラストレーションで燃え尽きるまで絶えず軋轢が生じます。
Julio Biason氏が書いた記事「私が苦労しながら学んだ物事(ソフトウェア開発の30年で)」を思い出します。彼は、デザインパターンは解決策を見つけるためではなく、解決策を説明するために使うべきであると述べています。
(繰り返しますが個人的な意見では)デザインパターンが適用されているのを見ると、ほとんどの場合、解決策を見つけるための方法として適用されていました。そのため最終的には解決策をひねって、時には問題の方をパターンに適合させることになるでしょう。
彼は特に「デザインパターン」について語っていますが、UIは状態からなる関数であるというイデオロギーをめぐるRich氏の主張にもかなりよく当てはまるようです。言い換えれば、UIは状態からなる関数であるという考え方は非常に有用であることがわかります。複雑さを乗り切るのに役立ちます。しかしながらそれは、現実の世界でウェブアプリケーションを実際に構築する方法、つまりDOMという基礎の上にあるイデオロギー的な抽象概念です。Rich氏はこれについて時間をかけて説明します。
「UI = F(STATE)」はイデオロギー的な声明だと思いますが、私が言いたいのは、資本主義イデオロギーや共産主義イデオロギーのような他のイデオロギーと同じように、個人やグループが純粋に認識論的な理由以外で保持している規範的な信念や価値観の集合であり、大まかに言うと、「人々が実際に生きている世界ではなく、人々が想像したいと思っている世界を表している」ということです。
私は「イデオロギー」という言葉を、否定的なものではなく記述的ラベルとして使用しています。
しかしイデオロギーは危険を伴う場合があります。なぜなら彼らがいつもしているように、現実に逆らって苛立ち始めると、イデオロギーの衝動は常に、理論を修正することよりも、現実を作り直すことにあるからです。
「UIは状態からなる関数」というのは複雑なウェブアプリケーションを構築するための適切な解決策に近い理想論であり、私たちはReactのプログラミングモデルを通じてそのアイデアを活用しています。ですが今日では、私たちが実際に構築する素材(DOM)に合うように、イデオロギー的な解決策に多くのものを加えなければならないという不可避的な欠陥があります。どちらかというと、むしろトレードオフになると言うべきかもしれません。それは、私たちが生きているイデオロギーが現実のミスマッチと摩擦するときに軋轢を生みます。
私が本当に称賛したいのは、フレームワークの背後にあるイデオロギーと、ウェブアプリケーション構築の現実とのミスマッチに対処しようとするRich氏の試みです。素晴らしいことです。これはReact対Vueのような話をするときに聞きたいものです。単なる「ReactはJSXを使い、VueはHTMLテンプレートを使っている」という話でなく。
話が脱線しました。Rich氏が中断したところから続けましょう。
では関数型UIに対して非常に素朴なアプローチを取ったとしたらどうなるでしょう。たとえばある状態からなにかしらのUIを生成する関数を作成します。これには、その状態が変化したときに関数を再び呼び出すイベントリスナーが含まれます。
const render = state => { document.body.innerHTML = ` <h1>Hello ${state.name}</h1> <input value="${state.name}"> `; const input = document.querySelector('input'); input.oninput = () => { render({ name: input.value }); }; }; render({ name: 'world' });
ここでなにが起こっているのかがわかりますか? 「状態からなる関数としてのUI」というアイデアを示す非常に単純でインタラクティブな体験を実装しようとしています。「Hello XXX」というテキストがありますが、ここではXXXは<input>
の値で埋められます。JavaScriptのコードはinputの変更を監視するだけに過ぎず、変更が行われるたびにDOMにアクセスして<body>
のすべてを消去し、inputの新しいステートフルな値に基づいてUIを再描画します。
この例は、UIはアプリケーションの状態からなる関数であるという考え方をよく表しています。ではここでの問題はなんでしょうか。なぜこれが機能しないのでしょうか? UIには、name
というアプリケーションの状態だけでなく、より多くの状態があります。Rich氏は言う。
古いinputにはフォーカスがあり、新しいinputにはフォーカスがないため、機能しないことがわかります。
コードが実行されるたびにDOMにアクセスし、<body>
のすべてを新しいHTMLに置き換えます。しかしおそらく気がついていなかったのは、DOMにはすでになにかしらの状態、つまりinputにフォーカスがあるということです。新しい状態で再描画するために<body>
のすべてを削除すると、特定のinputにフォーカスがあるというDOMにあらかじめ含まれている暗黙の状態が失われます。Reactは制御された(controlled)inputとともにこの問題を処理(解決)してくれますが、もしかするとあなたはご存知なかったでしょうか? Rich氏の指摘に沿って簡単な例を作るまで、私はそれを完全には理解していませんでした。「クソッ、ブラウザがデフォルトでどの状態を処理するか把握していなかった」。したがってUIは必ずしもアプリケーションの状態からなる関数ではないようです。
Rich氏は、DOMの暗黙的な状態をアプリケーションのコードに引き上げることでこの問題を解決できることを示しています。なにが起こるのでしょう? 特にアプリとそのインタラクションが複雑になるにつれて、同じ種類の問題が次々と発生します。
つまりDOMはステートフルであることがわかります。しかし私たちがReactのようなフレームワークから逃れようとしているのはそれだと思いました。そしてRich氏が指摘しているミスマッチがあります。フレームワークには必ずしも現実と一致しないイデオロギー的な傾向があります。このようにして、私たちは現実に合わせてイデオロギーを再構築するのではなく、イデオロギーに合わせて現実を再構築しようとすることがよくあるのです。
Rich氏が続けて指摘しているように、DOMの暗黙の状態(inputのフォーカス、要素がマウントされた時間など)は、ユーザー(開発者)が画面になにを表示するかを決定する上でいつでも重要な要素です。この暗黙の状態の多くは簡単にはアプリケーションに持ち込めません。
純粋な関数を使ってUIを表現したいという願望はDOMの本質と真っ向から対立しています。状態→ピクセル変換を記述するには最適な方法であり、ゲームレンダリングやジェネレーティブアートには最適です。しかしウェブ上でアプリを構築していると、そのアイデアはステートフルなメディアの実際に逆らってしまいます。
あるレベルでは、ReactはDOMの多くの面倒を見てくれていることをご存知でしょう。知っています。しかし「UI=F(STATE)」という基本的なアイデアを取り入れたアプリをJavaScriptで(Rich氏が提供した例のように)構築しようとしたとき、Reactが私のためにどれだけのことをしているのかが分かり始めました。
そしてそれが、私がRich氏の議論の頂点として見いだしたものへと導いてくれるのです。
もちろんReactが「DOMの暗黙的な状態の競合」をどのように処理するかはわかっています。Reactは仮想DOMツリーの新しいノードをDOMの既存のノードにマップします。つまり、Reactは明らかに機能していない基盤を機能的に抽象化したものです。
私の経験では、抽象化と抽象化の間のギャップが大きければ大きいほど、プログラマーが「インピーダンスミスマッチ」と呼ぶものに悩まされる可能性が高く、Reactでもそれを経験しているように思うのです。
つまりもっとも基本的なレベルでは、Reactのプログラミングモデルとウェブの命令型APIとの間にミスマッチがあります。ウェブには性質があり、Rich氏は「UIは状態からなる関数」という考えがそれに反することをうまく説明しているように思います。
ではここからどこへ向かうのでしょうか。私にはわかりません。しかしこれを書くことで、私が頻繁に感じる摩擦をよりよく理解できるようになりました。そして「Svelteが存在する理由」という講演のための完璧な準備です。
Eleventyとwebpackの連携
Eleventyは静的サイトジェネレーターとして柔軟かつ欲しい機能も揃っていて、僕が携わるほぼすべての静的HTMLベースのプロジェクトで採用している。一方でアセットのビルドなどは責務外なので別途webpack等と組み合わせる必要がある。
そこで困るのがCache bustingをどのように実装するかだ。たとえばHTMLファイルが1枚しかないクライアントサイドレンダリング前提のSPAであれば、HtmlWebpackPluginでHTMLファイルも丸ごとwebpackで生成してしまえるので、パスの解決についてユーザーがケアする必要がない。
しかしEleventyをベースにする場合では、HTMLの生成はEleventy側で行うため、何かしらのやり方でEleventyにwebpackのManifestを渡してやらないといけない。この受け渡しのためには、ManifestをJSONファイルとして出力するWebpack Manifest Pluginを使うのが一般的であるようだ。このJSONファイルには、MiniCssExtractPluginが出力するCSSやfile-loaderによって読み込まれた画像などのパスも含まれるので、一通りのアセット類はwebpackでビルドするようにすればCache bustingの対象にできる。
EleventyではDataディレクトリにJSONファイルなどを配置するとHTMLテンプレートから参照できるようになるので、Manifestをまずはそこに出力してみる。説明を単純にするためにかなり簡略化したビルド。
. ├── dist/ │ ├── build/ │ │ ├── assets/ │ │ │ └── background.d41d8cd9.svg │ │ ├── main.095d67a3.css │ │ └── main.3a6b57da.js │ └── index.html ├── src/ │ ├── assets/ │ │ ├── background.svg │ │ └── logo.svg │ ├── scripts/ │ │ └── main.js │ ├── site/ │ │ ├── _data/ │ │ │ └── assetManifest.json │ │ ├── build/ │ │ │ ├── assets/ │ │ │ │ └── background.d41d8cd9.svg │ │ │ ├── main.095d67a3.css │ │ │ └── main.3a6b57da.js │ │ └── index.njk │ └── styles/ │ └── main.css ├── gulpfile.js ├── package-lock.json ├── package.json └── webpack.config.js
.eleventy.js
:
module.exports = (eleventyConfig) => { eleventyConfig.addPassthroughCopy('src/site/build') eleventyConfig.setUseGitIgnore(false) return { dir: { input: 'src/site', output: 'dist', }, } }
webpack.config.js
:
const path = require('path') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const ManifestPlugin = require('webpack-manifest-plugin') const srcDir = path.join(__dirname, 'src') module.exports = { mode: 'production', context: srcDir, entry: ['./scripts/main.js', './styles/main.css'], output: { path: path.join(srcDir, 'site/build'), filename: '[name].[contenthash:8].js', publicPath: '/build/', }, module: { rules: [ { oneOf: [ { test: /\.css$/i, use: [ { loader: MiniCssExtractPlugin.loader }, { loader: 'css-loader' }, ], }, { exclude: [/\.js$/, /\.json$/], use: { loader: 'file-loader', options: { name: '[path][name].[contenthash:8].[ext]', }, }, }, ], }, ], }, resolve: { alias: { '~assets': path.join(srcDir, 'assets'), assets: path.join(srcDir, 'assets'), }, }, plugins: [ new MiniCssExtractPlugin({ filename: 'main.[contenthash:8].css' }), new ManifestPlugin({ fileName: path.join(srcDir, 'site/_data/assetManifest.json'), }), ], }
gulpfile.js
:
const spawn = require('cross-spawn') const gulp = require('gulp') const del = require('del') const webpack = require('webpack') const webpackConfig = require('./webpack.config') const clean = () => { return del(['dist', 'src/site/_data/assetManifest.json', 'src/site/build']) } const assets = (done) => { webpack(webpackConfig).run((err, stats) => { if (err) { console.error(err.stack || err) if (err.details) { console.error(err.details) } done(err) return } console.log(stats.toString('minimal')) if (stats.hasErrors()) { done(new Error('webpack compilation errors')) return } done() }) } const eleventy = () => { return spawn('./node_modules/.bin/eleventy', [], { stdio: 'inherit', }) } const build = gulp.series(clean, assets, eleventy) exports.build = build
src/site/index.njk
:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello</title> <link rel="stylesheet" href="{{ assetManifest['main.css'] }}"> <script src="{{ assetManifest['main.js'] }}"></script> </head> <body> <h1><img src="{{ assetManifest['assets/logo.svg'] }}" alt="Hello"></h1> </body> </html>
src/styles/main.css
:
body { background-image: url("~assets/background.svg"); }
src/scripts/main.js
:
console.log(require('~assets/background.svg'))
gulp build
を実行すると次のファイルが出力されて、HTMLテンプレートはそれを読み込んでパスを解決する。
src/site/_data/assetManifest.json
:
{ "main.css": "/build/main.095d67a3.css", "main.js": "/build/main.3a6b57da.js", "assets/background.svg": "/build/assets/background.d41d8cd9.svg" }
ただしこれだとsrc/assets/logo.svg
がManifestに含まれておらず、EleventyをビルドしてもassetManifest['assets/logo.svg']
の部分が空文字列になってしまう。これはwebpack側ではsrc/assets/logo.svg
を読み込んでいないからだ。src/scripts/main.js
やsrc/styles/main.css
からファイルを指定するとwebpackのManifestに入りつつパス解決をしているが、Eleventyはその結果を参照しているだけなので、HTML側でファイルを指定していることはwebpackに関係がない。なのでHTML側だけで参照するファイルもManifestに含めるために、それらのファイルをあらかじめwebpackに読み込ませておく。
src/assets/_importer.js
:
require.context('.', true)
webpack.config.js
:
- entry: ['./scripts/main.js', './styles/main.css'], + entry: ['./assets/_importer.js', './scripts/main.js', './styles/main.css'],
これでsrc/assets
ディレクトリに配置されたすべてのファイルがManifestに入るようになる。
しかしまだHTMLテンプレートに{{ assetManifest['assets/logo.svg'] }}"
のように記述しないといけないのが冗長でめんどくさく、もう少し書きやすくしたい。NuxtJSではVueテンプレートが次のように変換される。
<template> <img src="~/assets/image.png"> </template>
<img src="/_nuxt/img/image.0c61159.png">
これを参考にEleventyでも同じようにパスが変換されるようにしてみた。
.eleventy.js
:
const { JSDOM } = require('jsdom') const importFresh = require('import-fresh') const transformAssetAttrs = { video: ['src', 'poster'], source: ['src'], img: ['src'], image: ['xlink:href', 'href'], use: ['xlink:href', 'href'], } const transformAssetSelector = Object.entries(transformAssetAttrs) .map(([element, attrs]) => { return attrs.map((attr) => `${element}[${attr}]`).join(',') }) .join(',') module.exports = (eleventyConfig) => { eleventyConfig.addPassthroughCopy('src/site/build') eleventyConfig.addTransform('assetPath', (content, outputPath) => { if (!outputPath.endsWith('.html')) { return content } const assetManifest = importFresh('./src/site/_data/assetManifest.json') const dom = new JSDOM(content) dom.window.document .querySelectorAll(transformAssetSelector) .forEach((element) => { transformAssetAttrs[element.tagName.toLowerCase()].forEach((attr) => { const value = element.getAttribute(attr) if (value && value.startsWith('~assets/')) { const id = value.replace('~assets/', 'assets/') const resolved = assetManifest[id] if (!resolved) { throw new Error(`Can't resolve '${value}' for '${outputPath}'`) } element.setAttribute(attr, resolved) } }) }) return dom.serialize() }) eleventyConfig.setUseGitIgnore(false) return { dir: { input: 'src/site', output: 'dist', }, } }
これによりHTMLテンプレートに次のように書いたパスは自動的に解決されるようになった。
<img src="~assets/logo.svg" alt="Hello">
存在しないファイルを指定するとエラーが出てくれるようにもなったので安心できる。
そういうわけでここまででビルドする方法に書いてきたが、実際にはwatchできないと使いものにならない。いい感じにwatchする方法についてもメモしておこうかと思ったが、解説量が膨大になりそうなのでリポジトリだけ置いておく。
「404 Not Found」は実装側の言葉であってユーザーに見せる言葉ではない
いわゆる404ページを作っていて、ふと「ページが見つかりません」という表現に違和感を覚えた。ユーザーになにの話をしているんだ?
URLに対応するリソースを見つけたり見つけられなかったりするのはあくまでサーバー、つまり実装の話で、そういった仕組みの話はどうでもいい。重要なのは実装を通してユーザーにどのような意味を感じさせたいかだ。
リンクをクリックしてページを開くとき、リンクは別の場所に移動するためのボタンに思える。移動先にはページ(またはその他のリソース)が存在していたりしなかったりして、ほぼ確実な存在を期待するが、まれにそうでない場合に出くわす。このときユーザーに関係があるのはページが存在しているか否かで、サーバーの処理内容に意識はない。「見つからなかった」と知らされてもわけがわからずトンチンカンな印象を受けてしまう。
「お探しのページは見つかりませんでした」と言っても同じ話だ。ページを探すのはサーバーの方で、ユーザーはリンクを選んだ時点で移動先を確定させている。強いて言うならユーザーが探すのはリンクであり、ページの先にあるものだ。
では「404 Not Found」をどのように表現するか。伝えるべきはページの存在についてだが、ややこしいのは404ページ自身もまたページであることだ。404ページが「ページが存在しない」と言うとき、「ページ」が指すのは「そこに存在するはずのページ」である。「この場所にあるはずのページは存在しません」とかだと冗長すぎるし、却って誤解を招きそうだ。
「HTTPエラーページに意味を持たせよう」によると、以前Googleのエラーページには次のような文言があったらしい。
The page - www.google.com/dkjfhsd - does not exist.
The page
はなかなかいいと思える表現だ。エラーページが表示される瞬間にまさにユーザーの意識がある対象のページを指していて、かつ「404ページ自身が存在しない」という矛盾した表現になっていない。
しかし、訳しにくい。「そのページは存在しません」としても、「そのページ」がユーザー自身が意識していた「存在するはずのページ」を指しているとは理解されず、不自然に「その」と明言されていることで別のなにかを意図していると誤解されそうだ。「ページは存在しません」でもどのページを指しているのか一瞬理解しづらい気がする。
では「このページは存在しません」ではどうか。404ページ自身を指しているようにも取れるが、その場所に存在するはずの「存在しないページ」を指して「このページ」と呼んでいるように思えないだろうか。あるいはこれは訪問先の住所にある空っぽの土地に「売地」という看板が立っているようなイメージでも理解できるかもしれない。
そんな訳で作った404ページでは「このページは存在しません」を採用した。
ひと通り書いてみて想像してたほどうまく説明できなかったなと思った。「ページが見つかりません」的な表現はもはや記号化しているので今さらどうでもいい気もするが、それでも気持ち悪さはあったので書いてみた。
雑な日記
割と時間をかけて書いた記事を先週に公開した。特に最近はいろんな考えを言葉にしようと意識的にしていて、なんとなくぼやっとしてるものの存在を見えるところに持ってきたいみたいな意図がある。詳しく説明しようとすると疲れるのでこの記事には書かない。こっちじゃない方のブログになにか書くときは、といっても記事数的にはかなり少ないけど、僕としてはかなり丁寧に書いてるんだけど、その分煮え切ってない話を半端なまま書くとか、真面目に編集するほどでもないと自分では思うような話を雑に出すのがやりにくい。記事自体の出来がどうこうっていうか書く側のハードル的な意味で。少し前からツイッターのタイムラインで「ツイートはできるのにブログは書けない」みたいなつぶやきをいろんな人が方々でしてて、これって今までブログに書いてた雑なやつはツイートで発散されちゃってるんだろうなって思ってて、ブログってだいぶ「わざわざ」書くものになったなって。ツイートって必要以上に雑にできちゃうなって感じるときがあって、外が暑いときにアッッッッツとか書くのは別に鼻クソみたいなもんだからどうでもいいんだけど、ちょっと時間かけて連ツイしちゃうくらいの何かがあったときに、ツイッターで終わると自分の中でもそっからあんまり考えが繋がっていかないんだよね。これ前にも同じようなツイしたなって思って過去のやつ漁るとそっから大して話が進んでない。程度問題だとは思うんだけどブログだとある程度メモを一通りひとつの記事に押し込めるし忘れたら塊として取りに行ける。ツイだと検索できる程度記憶がないと拾いに行けないしいちいち連ツイにしなくて分散してるやつもあるしただテキストを放流しただけみたいなもんなんだよね。だから雑なままでもとりあえずブログにまとめる習慣を復活させないと困るなと僕としては思ってて。ブログ記事書くのに何日も掛けるとかありえなかったのにアレとか書いてなかった期間も含めてひと月くらい掛けてたような。たぶんそうやって作り込んでいくのに慣れると即席なものを出すのに躊躇するようにもなるのかもしれない。一方でとりあえずでも書いた昔の記事がいまだに読まれてる通知もたまに来たりしてやっぱり記事化の意味を感じられたりするし、逆にどんだけ丁寧に書いても興味ないテーマだと読まれないのは同じだし。そういえばこれを記事にしようと思ったのは今accrefsを作ってるからかもしれない。これはいろんな記事とかを大量に集めてるサイトで、既存のページを集めてるので当然人気があるテーマばっかりの記事が増えたりとか偏りがあって、粒度とかバラバラで体系的にまとまってるとは言えないんだけど、それでもここを見ればあるというだけで意味がある。ちなみにこれはいろんな人の協力でできてるサイトで、僕はサイト作るとかを手伝わさせてもらってる。インブラウザデザインで作ったんだけどやっぱりこうやってみると仕事で書いてるのって同じCSSなのかとか思ってしまう。ちょうど最近Every Layoutをよく読んでて本当にCSSは奥深いなとか今さらになって改めて思わされたんだけど、トレースするようにCSS書くのはCSSのあり方とまったく真逆を行ってるなとインブラウザの感覚を通して確信した。なんだったらインブラウザデザインは本当にもう10年近くやってるかもしれないんだけど、これまでに今回ほどうまくできたことがないんだよね。まさにこれもツイートしたんだけど、The Stackが革新的で、だいぶ前から他所でも氏がこのスニペットを共有してたんだけどパッと見だとなにがいいんだって感じでスルーしてたんだけど、言葉の通り騙されたと思って使ったら自分のCSS観が大幅に変わってしまったくらいで。僕は要素型セレクタが教条的と言ってもいいくらい好きで、インブラウザデザインするときはあらゆるセレクタ設計の中心にしてたんだけど、まあ実際にはあんまりうまくいかない。余白は要素同士の関係性によって決まるもので、グローバルなスタイルとして要素の余白を決めていても変更しないといけない場合がかなり多い。こうした場合の手として、すべての要素の余白はmargin-top
で統一して、要素の組み合わせによるバリエーションは隣接セレクタで上書きするというのがあるんだけど、要素同士の組み合わせなんて無限にあるのでいちいちセレクタを書いていくのにはかなり無理がある。特になにかの要素に入れ子になったりとかすると……。そんなわけで余白だけを責務にするクラスを作ったりしてHTML側でコントロールできるようにするのが現実的な解だった。ただこれも方々で余白のサイズが違ってくるなら効果的とは言えるけど、特定のコンテキストにおいて一貫した余白を設けたいみたいな場合では無駄が多い。The Stackはいくつか余白のバリエーションを作っておくと、accrefsでは3つだけ、それで囲ってやるだけでいい。理屈として特定のコンテキストでは余白を一貫させるというのは理にかなっているように思えて合理的な仕組みだと思う。同じ意味でそういったコンテキストが存在しない場合はspacerクラスみたいなのを作る方が向いている。最近読んだ本で話すように書くのは作文技術としてありえないみたいなことを言ってたんだけどこの記事は雑に書くためにまったく気を遣わずに話すように書いた。
[翻訳]Web Componentsに祝杯を?
Any Holy Grail for Web Components?の翻訳。
Web Componentsの歴史は輝かしいものではなかった:
- それらの宣言的な性質は静的なHTMLの世界に限定されている
- スロットとShadow DOMを介したJSカウンターの宣言はぎこちない
- Shadow DOMは重く、確実にポリフィルするのが困難
- Shadow DOMはすべての問題は解決できず、必須の設定が必要であり、SSR(そうすべきではないとわかっているが…)を介して配信できない
- HTML Modulesはまだどこにも実装がないが、Vue.jsや他のフレームワークが何年も前から提供しているものとあまり変わらない
Shadow DOMを必要としない、または使用しない場合、これまでに成功してきたライブラリやプロジェクトで採用されてきたように、コンポーネントが広く採用される可能性が高くなる。しかしまだ他の弱みが残る:
- 宣言はまだ静的なHTMLの世界に限定されている
- CSSはあらゆる場所にも存在し、Shadow DOM内のものより脆弱である
- 冗長なノードの数があまりにも多くなるため、複雑なページではダウンロードと処理の両方に負荷がかかる
- 古き良き標準DOMとテーマの統合はまだぎこちないだろう
DOMの冗長性について
ホスティングしているページからテーマを継承する可能性のあるParagraphを表現したいとしよう。そして、JSを経由して魔法のようなことができる「Special paragraph」として配信したいとすれば、おそらく次のようにするだろう:
// the JS side class SpecialP extends HTMLElement { constructor() { /* setup it once */ } } customElements.define('special-p', SpecialP); // the HTML side <special-p> <p> The actual content </p> </special-p>
代償として、どのような形であれShadow DOMを使用すると、ホスティングしているサイトのテーマを反映できる<p>
タグをつけなくてもロジックとレイアウトの両方が肥大化する。
グレースフルエンハンスメントについて言うと、カスタム要素をサポートしていないブラウザには<spacial-p>
タグをどう処理すればいいのかさえわからない。
私たちが望んでいたのは、Paragraphを簡単に設定する方法だけだった。
標準ベースのアプローチ
2KBの小さなWicked Elementsライブラリは、すでに説明したような問題を解決する素晴らしいソリューションになっている。それはひとつのポリフィルも必要とせずにIE9までサポートする。カスタム要素をまったく使用せずに、DOMに到達するあらゆる要素に、カスタム要素のようなイベントを別のAPIを介して公開する。
// the JS side wickedElements.define('p.special', { init() { /* setup it once */ } }); // the HTML side <p class="special"> The actual content </p>
頭を使う必要がないほど簡単なので、ぜひこの小さなライブラリを試してみてほしい。しかしまだいくつかの問題点が残っている:
- wickedElementsライブラリはそのスタックをより理解している人にはうまく機能するが、初心者には少し混乱を招くかもしれない(普通のパラダイムとはまったく異なるので)
- 要素はライブになるとその場でアップグレードされるが、DOMはアップグレードされるまでこれらの要素について何も知らない。これはカスタム要素にも当てはまるが、内部または将来のブラウザパフォーマンス最適化に関しては、カスタム要素などのネイティブのプリミティブを使用する方が常に安全である
- HTMLコンテンツの宣言はデフォルトではまだHTMLの世界に限定されている。これはSSRとグレースフルエンハンスメントについては素晴らしいことだが、Reactエコシステムは多くの開発者がロジックをひとつの場所——つまりJSファイルとJSX(または似たような解決策)だけに限定したがることを示している
簡単なまとめ:React開発のエクスペリエンス
Web標準についてさらに説明する前に、私が考えるReactの人気を高めるために役立ったことを要約したい:
- 素晴らしいドキュメント
- great tooling around
- 依存関係をインポートし、1つのファイルでコンポーネントの意図を宣言する
Web標準もこのリストの最初の2つの点を、より良くはないにしても確実にカバーしている。しかし3つ目の点を超えることはできない:
// the React JS side class Special extends React.Component { render() { return <p>{this.props.text}</p>; } } // still the React JS side ... const el = <Special text="The actual content" />;
そしてstyled-componentsを追加する……。標準技術を通して私たちがそのような経験に近づき、楽になる可能性はあるだろうか?
カスタム要素に組み込まれているアプローチ
残念ながらまだすべてのブラウザで利用できるわけではないが、すでにカスタム要素をサポートしている場合(例:Safari)ならたった1Kのポリフィルを使用すれば、このプリミティブはカスタム要素の標準的な表現力だけでwickedElementの能力を得るための最善の妥協策になる。
// the JS side class SpecialP extends HTMLParagraphElement { constructor() { /* setup it once */ } } customElements.define('special-p', SpecialP, {extends: 'p'}); // the HTML side <p is="special-p"> The actual content </p>
結果として、これまでに説明したいくつかの課題が解決された:
- DOMはまったく膨張しない
- ページはグレースフルエンハンスメントされる
- ページは今すぐSSRできる
- カスタム要素のすべてのライフサイクルを利用できる
したがって、不足しているのは次の通り:
- JSファイル内でも内容を宣言する方法
- JSファイル内でもコンポーネントのスタイルを定義する方法
- 素晴らしいアプリを簡単に開発するために役立ついつもの特別な魔法
簡単に言うと、これまで押し戻されてきた組み込みの拡張とユーザーランドの標準ベースのソリューションを組み合わせてJSで宣言的レイアウトを作成することは、標準的な世界でReactエクスペリエンスをシミュレートするための完璧な組み合わせかもしれないということだ。
Heresyとは何か
このライブラリ名(和訳すると異教、異端、異説)は、一般に受け入れられている概念とは大きく異なる概念を組み合わせているため、これ以上適切なものはないと思う。
Special P
コンポーネントの最も基本的な例を次に示す:
// the JS side import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static tagName = 'p'; }); // the HTML side (SSR ready) <p is="special-heresy"> The actual content </p> // or ... the JS side render(document.body, () => html` <Special> The actual content </Special> `);
🤯
待って……。何が起こっている?
- 静的クラス
.name
を使用してレジストリ名を定義し、末尾に-heresy
を付けて、カスタム要素レジストリに対して常に有効な名前にする - 静的クラス
.tagName
を使用して、表すべき実際のDOM要素の種類を指定する。組み込みのカスタム要素であればどんな要素でも構わない(option
、tr
、td
、select
、li
、input
、label
……) - もしDOMに
is=
属性が検出されればすぐにアップグレードされる - lighterhtmlを使用してクラス
.name
を記述した場合、テンプレートリテラルでレイアウトを宣言するために使用されるエンジンは、<p is="...">
として自動的に変換されて膨張しない。宣言されたノードは実際にはDOM上に存在し(これらを参照することもできる)、仮想的なものは存在しない。つまりカスタム要素はユーザーがハンドリングできるインスタンスである
その結果、JSXと非常によく似た方法で宣言することができる。
the best part of `heresy` is that layout can look exactly like in JSX, but there's no indirection whatsoever to what you write and what you get/target 🎉 pic.twitter.com/tKZpLrvsKE
— Andrea Giammarchi (@WebReflection) April 27, 2019
CSSについてはどうか?
import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static style = selector => ` ${selector} { transition: background 300ms; } ${selector}:hover { background-color: silver; } `; static tagName = 'p'; });
このCSSは、クラスの静的style
メソッドを介してクラス宣言ごとに1回だけ注入され、スタイル設定に使用される名前を含む文字列であるセレクタを引数として渡す。
この例では、引数はp[is="special-heresy"]
になる。
style
は単なる静的メソッドであるため、実行時にSASSやLESSのようなトランスフォームを介して変換された文字列を返すこともできる。
propsについてはどうか?
import {render, html, define} from 'heresy'; define(class Special extends HTMLParagraphElement { static tagName = 'p'; #props = {}; get props() { return this.#props; } set props(props) { this.#props = props; this.render(); } render() { this.html`${this.props.text}`; } }); render(document.body, () => html` <Special props=${{text: 'The actual content'}}/> `);
lighterhtmlのおかげで、カスタム要素には任意のゲッターまたはセッターを指定できるので、任意の種類の値を即座に渡すことができる。
互換性についてはどうか?
ポリフィルとツールを適切に組み合わせることで、HeresyはIE9でも動く。
その方法を理解するには、基本的なライブデモまたはそのソースコードを参考にできる。
では…?
私に休憩させてください😂
このプロジェクトはまだ初期の段階で実験的なものだが、すでにいくつか有望な成果が出ている。たとえばこの古典的なTodoデモ(ソース)は現在Chrome Canaryでしか動作しないが、これはトランスパイルを行わずに最新のJSの機能をベースにしているからだ。
次のようなもの:
// ... render() { this.html` <input placeholder="type item" onkeydown=${this}> <Hide onchange=${this}/> <ul>${this.items.map( data => html`<Item data=${data}/>` )}</ul>`; }
このカスタム要素ベースのアプローチを使うのはとても楽しいが、私はまだこれを使って具体的なアプリを作っていないことを覚えておくように。そしてlightterhtmlよりもhyperHTMLの方が、その.bind
メカニズムのおかげでより適切なのではないかと考えている。
しかしプロトタイプ作成にはlighterhtmlが最も簡単なツールだ。当面私が理解したいのは、コミュニティがこのようなソリューションを気に入って、興味を持っているかどうか。そうすればおそらくいつかSafariの開発者もネイティブの組み込み機能を提供して、誰もがポリフィルをドロップできるようになるだろう。
特定のコンテンツのためのコンポーネントとそうでないコンポーネント
CSS Utility Classes and "Separation of Concerns"(日本語訳)というTailwind CSS作者の記事を読んだ。そこでは「セマンティック」なコンポーネントと「コンテンツに依存しない」コンポーネントについて言及されていた。
セマンティックなコンポーネントは、
author-bio
article-preview
のように取り扱うコンテンツの種類をそのまま命名に反映させたものだ。コンポーネントの用途は特定のコンテンツのためだけに限定される。これを特定のコンテンツのためのコンポーネントと呼ぼう。
コンテンツに依存しないコンポーネントは、
media-card
card-list
button
のようにUIパターンを区別するだけの命名がされる。コンポーネントの用途として取り扱うコンテンツの種類を制限しない。これはUIパターンとしてのコンポーネントと呼ぼう。
以下私見。
普通は特定のコンテンツのためのコンポーネントの方が専用的だ。同じ種類のコンテンツの表示を別々のコンポーネントで行うことはあまりない。というかそうなるように分ける。たとえば商品詳細と商品属性概要だと、product
とproduct-teaser
のようにそれぞれ別コンポーネントにする。
デザインの個別性が高いサイトほど特定のコンテンツのためのコンポーネントが増える。それは何かしらの理由により共通化が進められていない状態とも言える。だから影響範囲が予測しやすく捨てやすい。反面新しいページで再利用できるコンポーネントは少なくなる。言うまでもなく、実情を見ずにUIパターンとしてのコンポーネントを増やしてしまったりすると、結果的には名前通りの使い方ができない嘘の設計になってしまう。
一方でUIパターンとしてのコンポーネントは汎用的だ。media-card
を使えば、商品でも著者でも記事でも好きなコンテンツを表示できる。
デザインの共通化が進むとUIパターンとしてのコンポーネントが増える。すると再利用できるコンポーネントが増える一方で、個別の違いを持たせるのが難しくなる。汎用性を目的としたUIフレームワークはUIパターンとしてのコンポーネントを中心に構成されていることが多く、スケールさせやすい。逆に特定のプロダクトに特化したPrimerのようなUIフレームワークは特定のコンテンツのためのコンポーネントも多く含む(ただしできるだけ避けるべきと言っている)。
両者には優劣があるわけではなく、必要に応じて併有するものだ。そうしてコンポーネントの役割を明らかにする。やがてサイトが成長するにつれて(あるいは実装が進むにつれて)、それぞれのコンポーネントが本当のところはどちら側なのかが浮き彫りになってくる。その段階で改めて命名し直す。この作業によりガイドが適切に引き直される。最初に戻る。
多くの場合、早すぎる共通化は失敗する。つまり特定のコンポーネントが、UIパターンとしてのコンポーネントであるかは慎重に判断すべきだ。
間違った共通化は意図しない再利用を生む。意図しない再利用は非合理的な拡張のきっかけになり、不必要に複雑性を高める。さらには使用箇所を把握しにくくなる。
共通化はすぐにできても、個別化は適切な影響範囲の理解と構成要素の分解を要する。それなら安直な共通化は避けるべきだ。
だから僕は必要と思えるタイミングまで、UIパターンとしてのコンポーネントの作成を見送ることが多い。
SPAじゃないプロジェクトのための控えめなJavaScriptフレームワーク「Stimulus」
ReactやAngularのように、アプリ内のすべてのHTMLをJavaScript側で管理して描画するアプローチは大げさすぎる。ほとんどの場合において。
もちろん一定以上の複雑さがあればその辺のJavaScriptフレームワークを使った方が良い。が、それがやり過ぎになるプロジェクトは多い。というかその方が多い。それでも、それらのアプローチではやり過ぎになってしまう場合でも、秩序のあるコードを書き続けられないと当然苦しい状態へ向かう。
この微妙な(そして多数派であるはずの)環境を適度に管理できる枠組みが必要だ。これまでちょうどいい解決策を見つけられずに苦労してきた。Web Componentsは課題を解決するかもしれないがまだ来ない。そこで出会ったのがStimulusだ。
いわく、Stimulusは既存のHTMLのための控えめなJavaScriptフレームワーク。サーバーサイドレンダリングされたHTML(これはIsomorphicとかじゃなくて普通の)に対して、特定の属性が付与されていれば対応するコントローラを初期化する。
まずHTMLにはdata-controller
属性、必要に応じてdata-target
、data-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ペイロードよりも大きくなる傾向がある事実も問題にはならない。要因は、CSSとJavaScriptを再初期化して再度ページに適用する必要があることだ。ファイル自体がキャッシュされているかどうかに関係なく。CSSとJavaScriptのサイズによってはかなり遅くなる。
この再初期化を回避するためにTurbolinksは、シングルページアプリケーションと同じように永続的なプロセスを維持する。リンクのクリックイベントを乗っとり、Ajaxによって新しいページをロードする。サーバーは完全な形式のHTMLドキュメントを返す。
この戦略だけでも多くのアプリケーションを非常に速く感じさせられる。Basecampではページからページへの移動が約3倍速くなった。この高速化はアプリケーションに反応性とシングルページアプリケーションの魅力の大部分を占めていた流動性を与える。
しかしTurbolinksだけでは話の半分にしかならない。ページの読み込み以降、1つのページ内にはあらゆる振る舞いが登録される。要素の表示と非表示の切り替え、クリップボードへのコンテンツのコピー、リストへの新しいTodoの追加、および昨今のウェブアプリケーションに関連づけられているその他すべての操作を行うインタラクション。
Stimulus以前、Basecampはこれらをさまざまなスタイルやパターンを使って実装していた。コードの中にはjQueryを使ったちょっとしたものもあれば、同じようなサイズの単純なバニラJavaScriptもあり、また、より大きなオブジェクト指向のサブシステムであるものもあった。これらすべてはいつも、明示的なイベント処理によってdata-behavior
属性を無効にしていた。
(data-behavior
はJavaScriptによる振る舞いをビューに依存せずに管理するための方法論。詳しくは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コントローラの位置付けとしては意味を成さない。
一連の代替フレームワークはできるだけ重労働を回避することを目的としている。この単純なモデルでうまく機能する多くのインタラクションに対して、リクエスト・レスポンスパラダイムの範囲内に留まる。そして必要性が真に迫っていれば高価なツールに手を伸ばす。
何よりもこれは、より労力がかかる主流のアプローチを使う大きなチームと、忠実さで競争したいと思っている小さなチームのためのツールキットである。
試してみてごらん。
デイヴィッド・ハインマイアー・ハンソン