styled-componentsへの最近の感想
今の職場で既に組まれたシステムが既にstyled-componentsにべったり依存していて、別に積極的に入れ替える理由もないので普通に使っているけれども、やっぱりこれ微妙だなと思った話。
そもそもとして、ビルドシステムへの介入が多くて不必要にロックインになったり、提示されてる手法がだいたいイマイチで普通にCSSとかSCSSを書く以上の意義が見出せないので、僕は基本的に「JS中にCSSを書いたり、ES Moduleのimportを使ってCSSを読み込むタイプのアプローチ(以下CSS-in-JS)」がそんなに好きではない・大人しくCSS書く方が好きではあるんだけど。色々あった理由はこちら:
- https://saneyukis.hatenablog.com/entry/2019/02/28/022750
- https://saneyukis.hatenablog.com/entry/2019/03/14/023446
感想
上のリンク先にも色々書いてる微妙に感じる点を全部見なかった事にしても、やっぱり微妙だと思う。
- 何を作るにもstyled-componentsで包まれた(ファクトリ)コンポーネントを作る事になるので、ラムダ(匿名関数)を書いて済ませたい箇所で、古のJavaのCallbackクラスを書くのを強制される気持ちになる
- 具体的には調整用のdivやspanをぺろっと置きたいだけなのに、わざわざ書く事になる
- 結果、包まれた(ファクトリ)コンポーネントは変数定義順の問題があり、function宣言のhoistingなどに頼ることができないので、 BEMのelement定義してからBlock書く感じになる。なので、抽象化されたコンポーネントを定義したいのに、まず具象化された細部から書いていく必要があるし、既存のコードもそのように書かれる
- ここら辺、人によって書き方変わるし、lintなどのしばり方次第で如何様にも書けるだろうけど、temporal dead zoneを考えると宣言してない変数を使うのはありえないのは共通認識とする
- 末節・BEMでいうelement側の定義をファイルを分けてimportすれば解決する問題だが、繰り返すようにラムダ(匿名関数)を書いて済ませたい箇所でCallbackクラスを定義するような事になる
- そもそもテンプレートリテラルで文字列として記述しているのが、非常に中途半端に見える
- JSX記法がDSLとしてセマンティクスを持つ事による教訓が活かされていないのは釈然としない
イマイチだと思ったのはここら辺。他にも色々あるけれども。
かつてのCoffeeScriptを持て囃す論説への拒否感と似ているものの、そんなことをいうとCoffeeScriptに失礼だな。
それとstyled-componentsに限った話題ではないけれども、いわゆる「GUIのTheme実現が可能」みたいな売り文句は実際には十中八九使わないタイプの機能だと思っていて、なぜかというと
- 現実にはダークモード+alphaくらいしかテーマを作らない
- そもそもテーマ機能を真面目に用意してるプロダクトを見た数の方が少ない
- ダークモード対応なら
prefers-color-schemeで解決する
- ダークモード対応なら
- そもそもテーマ機能を真面目に用意してるプロダクトを見た数の方が少ない
- 仮に実装するとしても、起こり得るテーマの変更はCSS custom propertiesで使って色やサイズや
background-imageなどの変数定義で解決するのが大半なので、標準化された方法に依存した方が良い - もっと複雑なことをやりたい場合は、大抵はGUIの実装部分が別になるので「一つのファイル・コードで複数のテーマを実現可能」にお世話になることは(あんまり)ない
- ユーザー定義スタイルを許容する場合、尚更public interfaceとしてclass属性などは必須だし.
から。 ちなみに自分の好きなアプローチは上の方に貼ったリンク先に全部書いてあって、それを超えるものではなかったかな。
Facebookのリデザインに見る、CSS-in-JSの良さそうな方向
色々ぐだぐだ書いているけれども、 https://engineering.fb.com/web/facebook-redesign/ に載っているサンプルコードの方向性は、良さそうに見える。実際にこの通りに書かれてるかはわからないけどね。
なぜかというと、
css-modulesを止めようとしている話(具体的な解決編)
BEMでいいじゃん話の続きその2にして, 具体的な解決編.
多分ここが気になる人が多いと思うのでなるべく箇条書きで済ませることにする.
背景
前回書いてた内容をまとめると以下のようになる.
無駄話が多いので前回は読まなくてもいいです.
追記: 前回読んでもらった方がいい気がしてきた. 時間が無い方はいったん飛ばしてくれて構わないのは変わらず.
状況
- サービスの初期からwebpackのcss-loader(を用いたcss-modules)を使用していた
- サービス開始時の鉄火場の中で, cascadingに基づく暗黙的なスタイルの継承を用いて, UI component treeにおいて祖先側が子孫側のスタイルを上書きしている箇所が多々あった
起きた問題
- リファクタリングの一環でディレクトリ構造を転置し, 併せてimportの順序も変更・整列した結果, css-loaderによるモジュール間依存グラフの構築順序が変わり,
最終成果物たるCSSのcascading順序が変わり, 先述の暗黙的な上書きと相まって, ES Module間の依存構造の先の先に位置するような,
ほぼ全く予期せぬ箇所のスタイルがいきなり崩れるような事故が発生し始めた
- この時点でreftest(visual regression test)は無い
- reftestがあったとしてもいきなり全く関係ない箇所のスタイルが壊れるコードはメンテがしんどい
- リファクタリングを妨げるような道具立ては長期運用するプロダクトにおいて選択する理由がない
- webpackの上にCSSのビルドまで依存させた結果, ビルドパイプラインが複雑化の一途を辿っていた.
解決方法
要求事項
- 最終的に生成されるCSSが予測可能(透過的)なアプローチであること
- 他のツール群(Lintなど)との相互運用性が取れていること
- できればwebpackに依存しないこと
- css-loaderベースの既存コードからのmigrationが容易であること
- css-loaderベースの既存コードのメリットである「UIコンポーネントのコードとそれに関連するCSSファイルの物理的距離が近い(siblingである)」ことが保たれること
具体的な解決法
importの方法
元々, 以下のようなファイル構成になっていた
/ ├ featA/ │ ├ ComponentA.css │ └ ComponentA.jsx └ featB/ ├ ComponentB.css └ ComponentB.jsx
ComponentA.jsxのスタイル指定は原則的にはComponentA.cssに記述され,
css-loaderを用いることでwebpackでのbuild時にimport styles form './ComponentA.css';のように解決される.
これを以下のようにする
/ root.css ├ featA/ │ ├ featA.css │ ├ ComponentA.css │ └ ComponentA.jsx └ featB/ ├ featB.css ├ ComponentB.css └ ComponentB.jsx
/root.cssは, featA.cssとfeatB.cssを任意の順序で@import経由でimportし, featA.cssはComponentA.cssを同様にimportする. featB.css も同じ.
これにより, 既存の記述方式からの緩やかな移行が可能になりつつ, ディレクトリ単位での順序関係の定義がCSS wayで明確に可能になった.
また, 現実的にはUIコンポーネントは粒度の細かいものから大きなものに沿って, 祖先方向(caller)なUIコンポーネントが子孫方向(callee)となり, 従属関係があるように設計されるため, セレクタ名さえユニークであればconflictすることはないし, ディレクトリ構造を少しいじった程度で規則が変わるという問題もない.
これは元々css-loaderを採用していた時から「理想的・本来的にはそうなっているはず」だったのだけど, 現実にはcascading順序が先述の理由から偶発的に変わることがあり, 厳密な保証を取りにくい状況にあった.
それをroot.cssでのimport順序に用いてシンプルなcascading順序解決にすることで最終的なコードにおける記述順序が透過的な形式で記述できるようにできた.
そのため, 万が一に順序の問題が起きたとしても(そしてそれはレガシーコードとの釣り合いの問題や, css-loaderからの移行, リファクタリングの中間過程においては頻繁に発生しうるが), 以前よりは順序問題として容易にコントロールが可能になる.
尚、コードはわかりやすくするとこんな感じになる.
/* desktop/mobileで切り替え可能になっている */ @import './variables.css'; @import './component/atom.css'; @import './component/mole.css'; @import './component/org.css'; /* template */ @import './featA/featA.css'; @import './featB/featB.css'; /* page */ @import './page1/page1.css'; @import './page2/page2.css';
これにより
はなんとかなった.
セレクタの命名規則
前項の前提を達成するためにセレクタをユニークにするにはBEMを下敷きに以下のように設定した.
.アプリケーションドメイン-コンポーネント名__コンポーネント内の要素--修飾子
アプリケーションドメイン の箇所は, 当該cssが配置されているディレクトリまでのパスが-で繋がれる形式になることが多い.
一方で汎用コンポーネントは.com-aや.com-mのように簡略表記を許容していた.
同時に, あとでgrepで検索しやすくするため, アプリケーションドメイン-コンポーネント名の部分をJSコードなどで動的に構築するのは(事実上)禁止することにした.
これは将来的にセレクタ名をrenameするのを容易にするための方針. そもそもcssファイルのsiblingに位置するjsファイルの中でしか使われないので必ずしも重要ではないのだけれども, 原理上, 上位(粒度の粗い側)のコンポーネントからセレクタ経由でdirty hackとして上書きされる可能性を許容してしまっているため, それを「やらざるを得なくなった」場合への対処の余地も含んでいる(現実にはコードレビューで弾かれることの方が多いので存在はしないだろうと思っているが).
結果(中間報告)
一番最初にも述べたけど最終的な移行完了を自分は見届けたわけではないので中間報告になるけれども.
- postcssベースで実現
- node-sassを使わなかったのは, Node.jsのmajor version upへのnode-sassの対応が他に比べて遅れやすく, Node.jsの更新の妨げになるケースが過去多々あったために避けた
- 要求事項は達成
- コード上の使用箇所とスタイル定義が紐づくため, 不要コードの削除も比較的容易なcss-loaderのメリットを引き継げたのは非常に大きい
- 何よりも生成コードの予測がつきやすくなった
- easyではないがsimpleは達成できた
ので結果は上々(と言えると思っている).
Performance Impacts
- minifyしないことで相対的なファイルサイズの増加は起こり得る
- SpeedCurveでの計測を見ていると移行による大規模なperf regressionは起きていない
- 長期的にはむしろ通常の機能追加に基づくコードの増加の影響の方が多い....
未解決または諦めた問題たち
- class属性のmangling
- critical rendering pathに必要なCSSだけの読み込み
- css-loaderべったりの時から, 最終成果物は独立したCSSファイルにしていたため, これ自体はregressionではない
- 将来的にこれが問題になった時に改めて検討する
- plainなCSSにした結果, 必要なスタイル定義だけを切り出してhtml中に埋め込むなどの最適化の余地が広がっているんじゃないかと思っているものの, その調査と最適化を必要とする段階には至っていないので未確認.
- 現代の多くのwebサイト・アプリケーションでは, この問題が現実の問題になるよりも先に, initialで読み込むJSのサイズの方がよっぽどperformance bugになると推測しているし, このとあるアプリケーションA(仮)では事実そうだった
- renameがめんどい
- 変更する箇所が多い
- これはある程度固まったプロダクトでは問題ない
- 立ち上げ期の試行錯誤の続くコードベースではめんどくさいかもしれない
- grep一発で解決可能ではあるが...
余談
- 元々CSS-in-JS嫌いな自分が面白半分で実験していたものを, regression鎮火のために, UIコード周りのオーナーと煮詰め直してworkaroundとして急遽投入したものだけど, 案外うまくいった(と思っている).
- easyではないがcascading style sheetの記述としてsimpleな所に落ち着いたと思うし, 自分としてはBEMで問題が解決していたという屁理屈が実証を伴ったという認識.
- いくつかのunresolved question含め、そのうち同僚の誰かがもう少し詳細に話してくれる....はず. 多分.
教訓・まとめ
- class属性を上書きしてどうにかするのは忙しくても止めた方が良い
- reftestは早期からあると良い
- 壊れたことを検知するには当然テストですよ
- CSSはcascading style sheetsでありES Moduleとは諸々の解決規則が違うので, 道具としては別物として考えるべき
追記: ちょっとした返答・反論とか
CSS Moduleを止めようとしている話(具体的な解決編) - saneyuki_s log“cascadingに基づく暗黙的なスタイルの継承を用いて, UI component treeにおいて祖先側が子孫側のスタイルを上書きしている” コレを食い止めなられなかったことが致命的であって、BEMかCSS Modulesか、という話はあんまり関係な
2019/03/15 03:32
根本的かつ理想的にはそうです. それで解決できていたならば何を採用しても苦労しません.
しかし残念ながら,
- レビューの際の見逃しやby designや時間的な制約による場当たり的な対応という意味の事故も含め, そのような問題が容易に発生しうるのがCSS Scopingのない(WebComponentsに頼れない)世界のCascading Style Sheetsの限界である
- そうした問題が発生していないのであれば, そもそもBEMだstyled-componentsだのという論争も存在しない.
- 現実にそれが当該アプリケーションのコードベースでは発生していた
- 今後の運用の過程で継承や上書きに頼る必要のある箇所が(偶発的であっても)現れるのが予見しうる状況にあったし, 仮にゼロから理想的な設計かつ負債の無いコードベースで構築したとしても, プロダクションにおけるコーナーケースも考慮すると, どこかの層で原理的に担保しない限り「発生しない」という前提に立つのは難しい.
- CSSの継承に頼らず, 関連するpropertyを個別に
unsetまたはinitialで埋めつつ作業をするのも解法としてあり得るが現実的ではない. - そして本文の繰り返しになりますが, リファクタリングなどに際してそこの依存が思わぬ箇所に出てくる結果となっていた. また, css-modulesの枠内で対処を試みると, 取り扱っている対象と記法のセマンティクスのミスマッチから却ってmonkey patchを増やすばかりであった
ため, それらの課題に対して
- そもそも暗黙的な上書きをしうるケースを減らす方向にコーディングスタイルやミクロなUIツリーの設計を変えるようにした
- 最終的に生成されるCSSコードを透過的に扱えるようにすることで, 以下の課題への対処を容易にする
- 1の結果発生しうるコーナーケースの緩和
- 1を志向してもどうしてもうっかり発生しうる将来の問題へのworkaround
- すでに負債と化していたコードベースで発生していた, css-module依存のcascadingに依存したコードからの移行
というのが, 当該アプリケーションで採用した解決案であり, 本エントリで主に言及していたのは2になります.
CSS Moduleを止めようとしている話(具体的な解決編) - saneyuki_s log無秩序なCSS Modulesの使用から、秩序あるBEMにしたら、秩序を得た。CSS Modulesはなにも問題ないのでは?
2019/03/15 20:27
改めて説明すると,
- css-module時代のコードはそれはそれで秩序はあった.
- 付け加えていうのであれば
- リファクタリングを妨げるような道具立ては長期運用するプロダクトにおいて選択する理由がない
- 最終的にcascading style sheetsのセマンティクスで表現されるものを, ESModule(ないしwebpackのloaderセマンティクス)で構築するような, 異なるセマンティクスを道具で構築するのはやり方が悪い
- 最終的に規約や秩序や運用でカバーといった言葉で解決するしかないのであれば, よりsimpleな運用で済む手段で良い
- とりあえずこちら と, 本エントリを改めて読んでいただければと思います
色々書いてなかったこと
Twitterでやりとりした中で、やっぱり書いたほうがいいなと思った話。
込み入りすぎててうまく説明できる気がしなかった初稿で説明していなかったが, className地獄への門の一端として, 現状の課題たるコーナーケースも色々あった(全部覚えていない + 全部列挙する前にCSS Module路線を諦めた)。例えば「本来であれば:nth-child()擬似クラスで解決するのが綺麗なはずだが、実際には適用したい対象がReact Componentかつ孫よりも下位に位置するため, 結局何かしらの上書きが必要になっている」など。この辺りについては「そもそものUI ComponentのAPI設計・粒度がおかしい」と断じてしまえばそれまでなのだが, 目の前にあるコードをmigrationしようとするには避けて通れない問題であった。
また、CSS custom propertiesなどはレガシー環境を考慮するとコンパイル時に解決するものになってしまう。つまり, 実行時制約を転化には処理系でのネイティブサポートが必須になるもののIE11などのサポートを含めている以上はそちらを前提にした設計に切り替えることは難しい(丸々IE11向けとそれ以外のコードベースを用意するか、そこも含めてサポートする互換レイヤーを何かしらで用意する必要があった)。
また、WebComponents一式が使えるようになったとしても, ShadowDOM境界を跨ぐ前提に立つと諸々の設計パターンは変わる可能性があり, WebComponents周りが未だ発展途上気味であるため(form control周りなどは最たる例だし, 残念ながら「今年はWebComponents元年」と年初に叫ぶはもはや年中行事になりつつある), より「モダン」なapproachへ将来的に移行したいことや最も強固なエコシステムであるWeb標準のなるべく近くに位置しておきたいことを考慮すると, メンテナブルかつ保守的(Web標準の近くに寄る)手段を採用するほうが長期的には払うコストが少ないだろうと予想し. ゆえに, シンプルに振ることになった.
ゼロから全てを描き直せるのであれば, おそらく強固な規約を浸透させるなどの運用戦術を取りつつ, 別の手段を考慮する可能性もあるだろう. しかし, 我々にそのような富豪的な選択肢はありえないし, 仮に書き直せるとしても真っ先にレガシー環境を切り捨てる方向に体力を注ぐだろうし, 一連の記事で述べている過程は「書き直しやすくする」ための前段階としての措置であった. そもそも頻繁に書き直しできるのであれば, 長期的にメンテナブルにすべきかなどといった問題はない. 困ったら書き直せばいいのだから.
それとstyled-components/CSS-in-JSに関する話については, 「最終的にCSSというシステムに落ちるのだから」自分は否定的にすぎない(あれで本当に多くのcascadingの課題を難なく解決できるのであればそちらで全く問題ない). ゆえに, React Nativeのように最終形がAndroidやiOSのスタイルシステムに落ちるフレームワークに関しては意見するつもりはない.そっちは今の所よく知らないし.
css-modulesを止めようとしている話(長々とした状況説明編)
BEMでいいじゃん話の続きその1にして, とあるアプリケーションが困っていた話.
書き味は最良ではないけれど, 設計思想を持った上で長期メンテを考えると結構いい感じだと思っているアプローチに至った. しかしながら, 放っておくと誰も試行錯誤の過程を書かないままになってしまいそうだし, それを書きそうな同僚も(多忙のあまり)書きそうな雰囲気はなさそうだし, もしかすると永遠に出てこない気さえするので書いてみる.
多分完遂の暁にはどこかで話として総括が出てくると思うけど, 中間報告ということで.
経緯
とあるアプリケーションは, 元々はCSS Moduleをcssのビルドシステムに用いてAtomic Designとして作っていた. CSS Moduleを選定した理由は自分はよく知らないが, ものの試しで使ってみようと思ったんだと思う. ここまでは別に間違ってはいない. (自分は選ばないが)styled-componentsに比べれば自分の好みではあるし, そこまでガミガミと言うつもりはない.
しかし, 肝心のコードがやらかしていた.
セレクタの黒魔術こそ殆どなかったものの, いたるところに存在するReact製のUI componentがclass属性の上書きを許容する形になっていた. この時点で開かれすぎたcomponent指向なので色々破綻しているのだが, おそらく汎用的なcomponentを作ろうとする麻疹の一種だったのだろうと推察している. そしてそれは同時に, initial release前の炎上の鉄火場のなかでダクトテープを貼り付けてデザインレイアウトとの不整合をどうにか鎮火しようとするプログラマーのメンタルモデルとハマってしまったのだろう(その時自分はその場にいないので、真相は藪の中だけど). 結果, アプリケーションサービスがリリースされて1年以上が経過した頃(自分が関わるようになった頃)には, いたることで上位のcomponentが思い思いにclass属性を上書きすることで帳尻を合わせるコードが散見されるようになっていた.
複雑なセレクタの黒魔術による上書きではなく, class属性の上書きで対処しようとした理由はわからない. だが、おそらくはCSS Moduleを使っている関係上, production buildでclass属性がmangleされてしまい, そのmangle結果もコードの成長に応じて必ずしも一定しないためにセレクタでの解決が不可能だったのではないかと推測する. とはいえセレクタを使っていても問題は後述の問題は回避できなかったと思うけど.
さて, そのような不安定なコードベースの現状に対して, アプリケーションの属するサービスは順調に成長を重ねていた. もし「実は伸びないのでcloseします」みたいなサービスであったら、ガタガタでも何の問題も無かっただろう. どうせ死ぬんだし. だが, それとは真逆でむしろ伸ばす方向にするにはどうするかを考える必要があった. そのような状況で, やばいなーと思いつつもガタガタになっているとはつゆも知らない自分(と同僚たち)はうっかりパンドラの箱を開けてしまうことになる.
発端はコードベース内のディレクトリ構造をリファクタリングしているときに起こった. そもそもディレクトリ構造をリファクタリングしていたのはコードやモジュールの責務境界を明瞭にするために実施したのだが, その過程でES Moduleのimport文の順序関係や格納されるパスのアルファベット順を変えてしまうケースがあった. 変えてもアプリケーションはちゃんと動いているし読み込む側の順序に依存するようなことはないだろうと思っていた.
読み込む側での順序を変えた結果, アプリケーション内で読み込まれる側のモジュールの依存関係に基づくDAG上でトラバースされ評価されるタイミングは変わることになる. しかもその変わった対象は変更したモジュールから遠く離れた箇所であったりもするため, changesetから読み取ることも難しい. これがJSの世界に閉じただけならば問題なかったのだが, 変更前後でグラフ構造の比較が必要になる上, 「変更箇所から10モジュールくらい跨いだ先のJSファイルが読み込んでいるCSSファイルの評価順序が変わった」のを上手く検知しながら他を壊さないように直す方法を探す必要がある. さらに悪いことに, その評価順序の変更の適用される対象が「最終的にはフラットな構造を上から下まで読み込んだ順序に評価する」cascading style sheetsなのであった.
結果を先に述べると, 上の方で述べた「上位のcomponentがclass属性を介して下位のcomponentのclass属性を上書きして微妙なレイアウトを調整する」ために必要なcascading順序が大きく破綻して, 「注意して確認すると壊れている」「大きく破綻するケースもあれば小さくずれているだけも両方ありうる」状況になってしまった.
それでもreftestがあれば早期に問題には気づけたであろうが, 当時の自分たちはreftestを持っておらず, しかも悪いことにリファクタリング作業が半分以上進んだ段階でその問題が発覚したために(それまで気づくことができなかった), 今更ディレクトリ構造の変更を中断して全てをrollbackするのも難しい状態になっていた.
では仮にreftestを持っていた場合は早期に問題が検知できて堅実に進めることができたのだろうか? その場合は先にも述べたようにモジュール間の依存グラフ構造を黒ひげ危機一発的におっかなびっくりと壊さないように変更を進めることになる. それが嫌であれば、コード行数10~15万行の中から, まず安易な上書きをしているところをすべて取り除く必要があり,それは縦横無尽に鎮火を前提として書かれたhackを全て取り除くだけのチームの体力が要求される. どちらにせよ, コードの責務分界点を分けるという目的に到達するまでに年単位の労力が必要になる. そして繰り返すが当時の自分たちにはreftestはない. 事実上, この路線のままでは軽微なcleanupにも等しいリファクタリングさえもロクにできない状況に追い込まれていた. リファクタリングできないコードベースとか敗北宣告にも等しかった.
解決に向けて
話を戻す, 今更rollbackして後にも引けない, かといって放っておくと前進すら難しい自分たちは以下の強行着陸を採用することになる..
- 可能な限りモジュール依存グラフの評価順序を変えないために, JSのimport文のsortを諦める
- CSS Module by css-loaderを使うのではなく, 素のcssをconcatすることでcascadingの評価順序を壊さないようにする
- class属性の上書き順序を保持またはコントロール可能にする
- postcss-loaderを用いる.
1は, 評価順序を変えなければ最終成果物が変わらないという保証が取れたので, 2で払うべきコストを先送りしてディレクトリ構造の変換を継続するための手段. 2は, すでに壊れてしまったものを制御可能な状態に戻すための処置. 手間は増えるしダルいものの, 明示的にcascadingの順序を制御できるために, もっともシンプルな解決方法となる.
この2つを, 最終的に2に持っていく合意のもとに壊れた箇所を修正しながら適用することになった. そもそもclass属性上書きしているのを直すべきなのだが, 影響範囲が大きすぎる + regressionの鎮火が優先であるため, 次のフェーズの課題とした.
結果的に多数のregressionは鎮火し, その後細々と変え続けた結果, 開始から数カ月(機能実装などで一時中断していたので実働としてはもっと短い)ののちにディレクトリ構造の変更は完了したのであった(CSS Module撤廃はside work的に現在も進行中)..
......同僚各位、その節は本当にご迷惑をおかけしました....
なんか話が長くなってきたので具体的にどうしたかは次のエントリで書くことにする.
CSSのcanvasとviewportとposition:fixedとpinch zoom
- specを元にした概念の整理
- 間違いあったら教えて欲しい
CSS 2.1におけるviewport
CSS 2.1におけるviewportを説明するにあたり、以下のterminologyが必要となる
- canvas
-
For instance, user agents rendering to a screen generally impose a minimum width and choose an initial width based on the dimensions of the viewport.
- viewport
-
User agents for continuous media generally offer users a viewport (a window or other viewing area on the screen) through which users consult a document.
-
When the viewport is smaller than the area of the canvas on which the document is rendered, the user agent should offer a scrolling mechanism.
- initial containing block
-
The containing block in which the root element lives is a rectangle called the initial containing block. For continuous media, it has the dimensions of the viewport and is anchored at the canvas origin; it is the page area for paged media. The 'direction' property of the initial containing block is the same as for the root element.
- ブラウザ(=screen mediaのUA)のスクロールバーを伴った描画領域 = viewport
- レイアウトはcanvasに描画される
- initial containing block(デフォルトのcanvas領域)のサイズはviewportの幅と高さに依存する.
viewportが1000pxの場合にhtml要素のwidthを100000pxにしたらどうなるのか?については、overflowプロパティによって定義されていて、
UAs must apply the 'overflow' property set on the root element to the viewport. When the root element is an HTML "HTML" element or an XHTML "html" element, and that element has an HTML "BODY" element or an XHTML "body" element as a child, user agents must instead apply the 'overflow' property from the first such child element to the viewport, if the value on the root element is 'visible'. The 'visible' value when used for the viewport must be interpreted as 'auto'. The element from which the value is propagated must have a used value for 'overflow' of 'visible'.
のように,
- root要素の
overflowプロパティがviewportに対しても適用される - viewportの
overflowプロパティがvisibleである場合、autoとして解釈されるので、スクロール機能の有無はUA依存となる
position:fixedの挙動
In the case of handheld, projection, screen, tty, and tv media types, the box is fixed with respect to the viewport and does not move when scrolled.
For other elements, if the element's position is 'relative' or 'static', the containing block is formed by the content edge of the nearest ancestor box that is a block container or which establishes a formatting context.
要は、対象のボックスがviewportに対しての固定座標に配置されるということ.
CSS Device Adoptationでの拡張
CSS Device Adaptationでは、meta[name="viewport"]でのviewportサイズの指定を可能にしたiOS Safariなどの実装を追認を行う形で仕様が策定されており、ここでは、initial viewportとacutual viewportの二種類が定義されることになった。
- initial viewport
-
This refers to the viewport before any UA or author styles have overridden the viewport given by the window or viewing area of the UA. Note that the initial viewport size will change with the size of the window or viewing area.
- actual viewport
-
This is the viewport you get after the cascaded viewport descriptors, and the following constraining procedure have been applied.
たとえば、meta[name="viewport"][content="width=device-width"]を指定した場合は以下のようになる
- initial viewportがブラウザのウィンドウサイズや描画領域に基づき決定される
UAスタイルおよびユーザースタイルによる
meta[name="viewport"]や@viewportの計算が行われる計算の結果、deviceの幅=ウィンドウの幅を自身の幅としたactual viewportが算出される
- actual viewportを元にinitial containg blockを再度計算する
ここでもUAスタイルがあるために、仮にviewportの指定が行われていない場合は、従来のサイトとの互換性のために、モバイルブラウザでは自動的にviewportの横幅が1000px前後に設定されたりするようになる。
デスクトップブラウザについては@viewportのUAスタイルを持たないと解釈すればいいのだろうが、dbaronによるissue表記があるので、完全に定義が確定しているわけではないのだろう。
では、メディアクエリはどこで計算されるのか? それは以下のように定義されている:
つまり、viewportの計算が全て終わらないとmedia queryをはじめとした計算は起こらない
zoom
ズームについては、まず、CSSOM Viewにて二種類のズームがあることが述べられている:
There are two kinds of zoom, page zoom which affects the size of the initial viewport, and pinch zoom which acts like a magnifying glass and does not affect the initial viewport or actual viewport.
この詳細についてはCSSOM View側では「CSS Device Adaptationを参照」とあるのだが、CSS Device Adaptationには明確なterminologyが設定されているわけではなく、断片的に文章中に記述されているだけである。
When the actual viewport cannot fit inside the window or viewing area, either because the actual viewport is larger than the initial viewport or the zoom factor causes only parts of the actual viewport to be visible, the UA should offer a scrolling or panning mechanism.
This is a magnifying glass type of zoom. Interactively changing the zoom factor from the initial zoom factor does not affect the size of the initial or the actual viewport.
position:fixedとpinch zoomを組み合わせるとどうなるのか
今までに述べた内容をまとめると、
- viewportはブラウザ(=screen mediaのUA)のスクロールバーを伴った描画領域
position: fixedは、対象のボックスをviewportに対する固定座標に配置する- pinch zoomはactual viewportに影響を与えない
となるのだが、pinch zoomをした場合に、Android BrowserやiOS Safariでは常にUAの描画領域の枠の固定位置にposition: fixedが配置されるのは、仕様上正確なのかどうか分かり難い…… pinch zoomではviewportに影響を与えないとあるが、zoomをしている以上は、UAとして表示している領域としてのviewportの横幅は相対比で小さくなっているはず。ちなみにW3C Bugzillaでは特に何も見つからず……
pinch zoomとpostion: fixedへの各UAの対応
IE11 Mobileや最近のChromiumでは、この問題に対して、「pinch zoomを行っても、position: fixedを常にUAとして表示している領域に配置しないようにした」(私の知りうる限り、Firefoxも修正を検討中である)
どのような変更を行ったのかはChromiumの変更を開設したスライドがわかりやすい。
このスライドでは、viewportとされるものは正確には、
- visual viewport: UAとして表示している領域という意味でのviewport
- layout viewport: レイアウト計算に用いられるviewport
の二種類が存在するとしている(正確には、この二種類が存在しているとすることで、問題を解決することに成功した)。
pinch zoomの存在しなかった時代(CSS 2.1での定義)では両者は統一して扱われるものであったのだが、モバイルブラウザ(およびUIとしてのpinch zoom)の登場により、分割して扱う必要が出てきた。しかし、現行のCSS Device Adaptationではそこにまで踏み込んだ定義がされていないために話がややこしくなっている。いや、この一件に限らず、CSS Device Adaptationは出来がいいspecとは思えないですけどね……
ちなみにWindow.scrollX/Yやwindow.innerWidth/Heightなどのviewportのスクロールに絡む座標は、Chrome Beta for Android 40.0.2214.69で確認したところ、visual viewportを基準に算出されるようになる。まあそうするしかないですよねcompat的にも。なので、
The innerWidth attribute must return the viewport width including the size of a rendered scroll bar (if any), or zero if there is no viewport.
The scrollX attribute attribute must return the x-coordinate, relative to the initial containing block origin, of the left of the viewport, or zero if there is no viewport.
以上の定義中にある"viewport"は, visual viewport。ややこしい。
まとめ
図にするとこんな感じ。仕様+実装を元に、viewportというものは、このような概念であると解釈が出来る。
尚、visual viewportとlayout viewportという単語は、Chromiumのスライドのやつが都合がいいから使ってるだけね。
+---------------------+----------+ | | | | *--------* | | | | | | | | | visual viewport | | | | | | | | | | | | | | | | | | *--------* | | | | | | | | | layout viewport | | | | | | | | +---------------------+ | | | | canvas | | | +--------------------------------+
- directionがltrの場合, 左上からactual viewportが始まる
- layout viewport = actual viewport
- layout viewportはinitial contaning blockの大きさを決める
- visual viewportはpinch zoomなどにより虫眼鏡のようにサイズが
可変したり、スクロールしたりする
overflow:hiddenがviewportに適用される場合はinitial containg blockを超えて動けないWindow.scrollX/Y,Window.innerWidth/Heightなどのスクロール関係値は、visual viewportを元に算出されていると解釈できる
- IE11 Mobileや最近のChromiumを除く従来の実装では、
position:fixedはvisual viewportを元にレイアウトされていると考えられる