その記事は一般に好評でしたが、同じ質問が手紙とTwitterで私に絶えず尋ねられました。 質問の要点:
CSSポリフィルの何がそんなに複雑なのですか? 私は多くのポリフィルCSSを使用していますが、私にとってはうまく機能します。
そして、私は気付きました-もちろん、人々はそのような質問を持っています。 CSSポリフィルを自分で作成したことがない場合は、おそらくこの痛みを経験したことはないでしょう。
したがって、この質問に答える最良の方法-そしてHoudiniが私を喜ばせる理由を説明する-は、なぜCSSポリフィルを使用することがそれほど難しいのかを示すことです。
そしてこれを行う最良の方法は、ポリフィルを自分で書くことです。
注:この記事は、2016年12月2日にdotCSSで読んだ講義のテキスト版です。 この記事ではもう少し詳しく説明しますが、ビデオを見たい場合はここにも挿入しました。
ランダムキーワード
ポリフィルを作成する関数は、(JavaScriptの
Math.random()
ように)
Math.random()
数値を返す新しい(新しいと仮定した)キーワード
random
です。
ランダムの使用例を次に示します。
.foo { color: hsl(calc(random * 360), 50%, 50%); opacity: random; width: calc(random * 100%); }
ご覧のとおり、
random
は無次元の数値を返すため、
calc()
使用してほとんどすべての値に変換できます。 また、任意の値を持つことができるため、任意のプロパティ(
color
、
opacity
、
width
など)で適用できます。
記事の残りの部分では、講義で示したデモページを使用します 。 これは次のようなものです。
random
キーワードを使用したサイトの表示例
これは、4つの
.progress-bar
要素がコンテンツ領域の上部に追加される、Bootstrapスターターテンプレートの「Hello World」ホームページです。
bootstrap.css
に加えて、次のルールを持つ別のCSSファイルが含まれています。
.progress-bar { width: calc(random * 100%); }
進行状況バーの幅の値はデモページで明確に示されていますが、アイデアは、ページがロードされるたびにポリフィルを使用する場合、これらのインジケーターは異なるランダムな幅を持つことです。
ポリフィルの仕組み
JavaScriptでは、言語が非常に動的であり、埋め込みオブジェクトをリアルタイムで変更できるため、ポリフィルの記述は比較的簡単です。
たとえば、
Math.random()
から
Math.random()
を作成する場合は、次のように記述します。
if (typeof Math.random != 'function') { Math.random = function() { // Implement polyfill here... }; }
一方、CSSはそれほど動的ではありません。 (少なくとも今のところ)ネイティブにサポートされていない新しい関数についてブラウザに通知するような方法でランタイムを変更することは不可能です。
つまり、ブラウザーがサポートしていない CSSの関数にポリフィルを適用するには、ブラウザーがサポートしているCSS関数を使用して、CSSを動的に変更して関数の動作を偽造する必要があります 。
つまり、これを有効にする必要があります。
.foo { width: calc(random * 100%); }
ブラウザでのコード実行中にランダムに生成されるようなものに:
.foo { width: calc(0.35746 * 100%); }
CSSの変更
これで、既存のCSSを変更し、ポリフィル関数の動作を模倣する新しいスタイルルールを追加する必要があることがわかりました。
そのようなアクションの可能性を示唆する最も自然な場所は、
document.styleSheets
から利用可能なCSS Object Model(CSSOM)です。 コードは次のようになります。
for (const stylesheet of document.styleSheets) { // Flatten nested rules (@media blocks, etc.) into a single array. const rules = [...stylesheet.rules].reduce((prev, next) => { return prev.concat(next.cssRules ? [...next.cssRules] : [next]); }, []); // Loop through each of the flattened rules and replace the // keyword `random` with a random number. for (const rule of rules) { for (const property of Object.keys(rule.style)) { const value = rule.style[property]; if (value.includes('random')) { rule.style[property] = value.replace('random', Math.random()); } } } }
注:このポリフィルでは、キーワード(URL、プロパティ名、プロパティ内の引用テキストなど)だけでなく、さまざまな形式で存在する可能性があるため、単語
random
検索と置換の単純な機能は使用しません。
content
など)。 デモの最終バージョンの実際のコードは、より信頼性の高い置換メカニズムを使用していますが、簡単にするために、ここでは簡略バージョンを使用しています。
デモ番号2をダウンロードし、上記のコードをJavaScriptコンソールに貼り付けて実行すると、実際に実行されるはずですが、実行後はランダムな幅の進行状況インジケーターは表示されません。
その理由は、CSSOMには
random
キーワードを使用したルールがないためです!
おそらく既にご存知のとおり、ブラウザーが理解できないCSSルールに遭遇した場合、ブラウザーはそれを単に無視します。 ほとんどの場合、この方法でページを壊すことなく古いブラウザーにCSSをロードできるため、これは良いことです。 残念ながら、これは、変更されていない元のCSSにアクセスする必要がある場合は、自分で取得する必要があることも意味します。
ページスタイルを手動で抽出する
<style>
または
<link rel="stylesheet">
要素のいずれかを使用してCSSルールをページに追加できるため、元の変更されていないCSSを取得するには、ドキュメントに
querySelectorAll()
を適用し、
<style>
コンテンツを手動で抽出するか、
fetch()
を適用して、すべての
<link rel="stylesheet">
タグのリソースURLを取得します。
次のコードは、すべてのページスタイルの完全なCSSコードを返す
getPageStyles
関数を定義します。
const getPageStyles = () => { // Query the document for any element that could have styles. var styleElements = [...document.querySelectorAll('style, link[rel="stylesheet"]')]; // Fetch all styles and ensure the results are in document order. // Resolve with a single string of CSS text. return Promise.all(styleElements.map((el) => { if (el.href) { return fetch(el.href).then((response) => response.text()); } else { return el.innerHTML; } })).then((stylesArray) => stylesArray.join('\n')); }
デモ番号3を開き、上記のコードをJavaScriptコンソールに貼り付けて
getPageStyles()
関数を設定すると、以下のコードを実行して完全なCSSテキストログを取得できます。
getPageStyles().then((cssText) => { console.log(cssText); });
抽出されたスタイルの解析
元のCSSテキストを取得したら、解析する必要があります。
ブラウザにすでにパーサーが組み込まれている場合は、何らかの関数を呼び出してCSSを解析できると考えるかもしれません。 残念ながら、これは機能しません。 ブラウザーが
parseCSS()
関数へのアクセスを許可した
parseCSS()
も、ブラウザーが
random
キーワードを理解しないという事実を否定しないため、おそらく
parseCSS()
関数はおそらく動作しません(将来の解析仕様で許可されることを期待しています)既存の構文と互換性のないなじみのないキーワードを処理する)。
いくつかの優れたオープンソースCSSパーサーがあります。このデモの目的のために、 PostCSSを使用します (ブラウザーとして機能し、後で役立つプラグインシステムをサポートするため)。
次のCSSテキストで
postcss.parse()
を実行した場合:
.progress-bar { width: calc(random * 100%); }
次のようになります:
{ "type": "root", "nodes": [ { "type": "rule", "selector": ".progress-bar", "nodes": [ { "type": "decl", "prop": "width", "value": "calc(random * 100%)" } ] } ] }
これは、 Abstract Syntax Tree (ASD)と呼ばれるものであり、独自のバージョンのCSSOMとして想像できます。
これで、CSSの全文を取得するためのユーティリティ関数と、それを解析するための関数ができました。次に、現時点でのポリフィルの外観を次に示します。
import postcss from 'postcss'; import getPageStyles from './get-page-styles'; getPageStyles() .then((css) => postcss.parse(css)) .then((ast) => console.log(ast));
デモ番号4を開いてJavaScriptコンソールを見ると、ページ上のすべてのスタイルのPostCSSの完全なADCを含むオブジェクトログが表示されます。
ポリフィル注入
これまでに多くのコードを作成しましたが、それがポリフィルの実際の機能とはまったく無関係であることは驚くべきことです。 ブラウザが私たちのためにやらなければならなかった多くのことを手動で行うために必要なプラットフォームでした。
ポリフィルロジックの実際の実装には、次のものが必要です。
- CSS SDAを変更し、
random
乱数に置き換えます。 - 変更されたSDAを文字列形式でCSSに貼り付けます。
- 既存のページスタイルを変更されたスタイルに置き換えます。
CSS抽象構文ツリーの変更
PostCSSには、CSS抽象構文ツリーを変更する多くのヘルパー関数を備えた優れたプラグインシステムが付属しています。 これらの関数を使用して、
random
関数を
random
に置き換えることができます。
const randomKeywordPlugin = postcss.plugin('random-keyword', () => { return (css) => { css.walkRules((rule) => { rule.walkDecls((decl, i) => { if (decl.value.includes('random')) { decl.value = decl.value.replace('random', Math.random()); } }); }); }; });
SDAを文字列形式でCSSに挿入します
PostCSSプラグインを使用するもう1つの便利な機能は、ASDを文字列形式でCSSに挿入するためのロジックが既に組み込まれていることです。 必要なのは、PostCSSインスタンスを作成し、使用するプラグインに渡し、
process()
を実行することです。これにより、文字列形式のCSSでオブジェクトが返されます。
postcss([randomKeywordPlugin]).process(css).then((result) => { console.log(result.css); });
ページスタイルの置換
ページスタイルを置き換えるために、すべての
<style>
および
<link rel="stylesheet">
要素を見つけて
getPageStyles()
するユーティリティ関数(
getPageStyles()
類似)を
getPageStyles()
できます。 また、新しい
<style>
を作成し、スタイルコンテンツを、関数に渡されるCSSテキストに設定します。
const replacePageStyles = (css) => { // Get a reference to all existing style elements. const existingStyles = [...document.querySelectorAll('style, link[rel="stylesheet"]')]; // Create a new <style> tag with all the polyfilled styles. const polyfillStyles = document.createElement('style'); polyfillStyles.innerHTML = css; document.head.appendChild(polyfillStyles); // Remove the old styles once the new styles have been added. existingStyles.forEach((el) => el.parentElement.removeChild(el)); };
すべてをまとめる
CSS CSSを変更するためのPostCSSプラグインと、ページスタイルを抽出および更新するための2つのユーティリティ関数を備えたpolyfillコードは、次のようになります。
import postcss from 'postcss'; import getPageStyles from './get-page-styles'; import randomKeywordPlugin from './random-keyword-plugin'; import replacePageStyles from './replace-page-styles'; getPageStyles() .then((css) => postcss([randomKeywordPlugin]).process(css)) .then((result) => replacePageStyles(result.css));
デモ番号5を開くと、実際のデモを見ることができます。 ページを数回更新して、本当の偶然を感じてください!
...うーん、期待通りではありませんか?
何が悪かった
プラグインは技術的には機能しますが、置換機能に対応する各要素に同じランダム値を挿入します。
私たちが何をしたかを考えると、これは完全に論理的です-唯一のプロパティを唯一のルールに置き換えました。
真実は、最も単純なポリフィルCSSでさえ、個々のプロパティ値の書き換え以上のものを必要とするということです。 それらのほとんどは、DOMの知識だけでなく、要件を満たす個々の要素の特定の詳細(サイズ、内容、順序など)も必要とします。 このため、この問題のプリプロセッサとサーバーソリューションだけでは十分ではありません。
しかし、重要な質問は、個々の要素を識別するためにポリフィルをどのように更新するかです。 。
個々の関連要素の特定
私の経験では、個々のDOM要素を定義するための3つのオプションがありますが、それらはすべて十分ではありません。
オプション#1:インラインスタイル
実践が示すように、ほとんどの場合、ポリフィル作成者は、CSSルールセレクターを使用して個々の要素を定義する問題を解決し、ページ上の適切な要素を見つけてインラインスタイルを直接適用します。
このようにしてPostCSSプラグインを変更する方法は次のとおりです。
// ... rule.walkDecls((decl, i) => { if (decl.value.includes('random')) { const elements = document.querySelectorAll(rule.selector); for (const element of elements) { element.style[decl.prop] = decl.value.replace('random', Math.random()); } } }); // ...
デモ番号6は、このコードの動作を示しています。
最初はうまく動作するように見えますが、残念ながら簡単にノックダウンできます。 CSSを更新し、
.progress-bar
ルールの後に別のルールを追加したとし
.progress-bar
。
.progress-bar { width: calc(random * 100%); } #some-container .progress-bar { width: auto; }
上記のコードは、ページ上のすべての読み込みインジケーターの要素がランダムな幅を持つ必要があることを宣言しています。ただし、識別子
#some-container
持つ要素に依存する読み込みインジケーター要素は除きます。 この場合、幅はランダムにしないでください。
もちろん、インラインスタイルを要素に直接適用するため、これは機能しません。 したがって、これらのスタイルは、
#some-container .progress-bar
定義されているスタイルよりも具体的です。
これは、ポリフィルがCSSの操作に関するいくつかの基本的な前提条件を満たさないことを意味します(したがって、個人的には、この方法は受け入れられません)。
オプション番号2:インラインスタイル
2番目のオプションは、実際のアプリケーションの多くの場合、最初のオプションが機能しないことを前提としているため、状況を修正しようとしています。 特に、2番目のオプションでは、次のように実装を更新します。
- CSSの残りの部分で適切なルールを確認し、randomキーワードを乱数に置き換えて、これが最後の一致ルールである場合にのみ、これらの宣言をインラインスタイルとして適用します。
- 待ってください。これは機能しません。なぜなら、特異性を考慮する必要があるため、計算のために各セレクターを手動で解析する必要があるからです。 次に、対応するルールを特異性の昇順でソートし、最も具体的なセレクターからのみ宣言を適用できます。
- ああ、
@media
要素もあるので、ここでもコンプライアンスを手動で確認する必要があります。
- そして、ルールの違反、つまり
@supports
について話す場合、それを忘れないでしょう。
- 最後に、プロパティの継承を考慮する必要があります。したがって、各要素について、DOMツリーを調べてすべての親要素をチェックし、計算されたプロパティの完全なセットを取得する必要があります。
- ああ、申し訳ありませんが、もう1つ、ルールごとではなくプロパティごとに計算される
!important
宣言も考慮する必要があります。 したがって、最終的にどの宣言が勝つかを知るために、彼らのために別のカードを保持する必要があります。
はい、理解できない場合は、カスケードの実装について説明しましたが、その実装にはブラウザへの依存が含まれます。
JavaScriptでこのようなカスケードを再実装することは確かに可能ですが、ここでは多くの作業が行われるので、オプション番号3の内容を必ず確認します。
オプション#3:カスケードの順序を維持しながら、CSSを書き換えて個々の一致する要素を定義する
3番目のオプション-私は最悪の中で最高だと思う-は、CSSを書き直し、多くの要素に一致する1つのセレクターでルールを複数のルールに変換することです。それぞれのルールは、要素の最終セットを変更せずに、1つの要素のみに対応します。
最後の文は完全に意味がないように見えるので、例を挙げて説明します。 ページに含まれ、3つの段落要素を含むCSSファイルを考えます。
* { box-sizing: border-box; } p { /* Will match 3 paragraphs on the page. */ opacity: random; } .foo { opacity: initial; }
DOMの各段落に一意のデータ属性を追加する場合、CSSを次のように書き換えて、独自の個別のルールで各段落を定義できます。
* { box-sizing: border-box; } p[data-pid="1"] { opacity: .23421; } p[data-pid="2"] { opacity: .82305; } p[data-pid="3"] { opacity: .31178; } .foo { opacity: initial; }
もちろん、気付いた場合、このオプションはこれらのセレクタの特異性に影響し、意図しない副作用につながる可能性が高いため、依然としてうまく機能しません。 ただし 、このようなスマートハックを使用して、ページ上の他のすべてのセレクターの特異性を同じ量だけ増やすことにより、正しいカスケード順序が維持されるようにすることができます。
*:not(.z) { box-sizing: border-box; } p[data-pid="1"] { opacity: .23421; } p[data-pid="2"] { opacity: .82305; } p[data-pid="3"] { opacity: .31178; } .foo:not(.z) { opacity: initial; }
上記の変更は、擬似クラスの機能セレクター
:not()
を適用し、DOMには絶対にないクラスの名前を渡します(この場合、
.z;
を選択し
.z;
したがって、DOMで
.z;
クラスを使用する場合は、別の名前を選択する必要があります)。 そして
:not()
は存在しない要素に常に対応するため、対応を変更せずにセレクタの特異性を高めるために使用できます。
デモ番号7は、このような戦略を実装した結果を示しています。 デモのソースコードを調べて、
random-keyword
プラグインのすべての変更を調べることができます。
3番目のオプションの最大の利点は、ブラウザがカスケードを処理し続けることであり、ブラウザは本当に優れています。 これは、メディアクエリを使用できることを意味します
@support
宣言、非標準のプロパティ、
@support
ルール、または任意のCSS関数、およびすべてが正常に機能します。
短所
3番目の方法で、CSSポリフィルのすべての問題を解決したように思えるかもしれませんが、これは真実とはほど遠いものです。 まだ多くの問題があり、そのうちのいくつかは解決できます(多くの余分な時間を費やします)が、他の問題は不可能であるため、避けられません。
未解決の問題
まず、ページに存在する可能性のあるCSSの一部を意図的に無視しましたが、
<style>
および
<link rel="stylesheet">
タグのDOMリクエストには使用できません。
- インラインスタイル
- シャドウドキュメントモデル(シャドウDOM)
これらのケースのポリフィルを更新できますが、これには多くの追加作業が必要になるため、この記事では説明しません。
また、DOMが変更されたときに何が起こるかについても考慮しませんでした。 最後に、DOMの構造に従ってCSSを書き換えます。 これは、DOMが変更されるたびに書き換える必要があることを意味します。
避けられない問題
上記の問題(難しいが解決できる)に加えて、回避できない問題がいくつかあります。
- 膨大な量の追加コードが必要です。
- このメソッドは、クロスオリジンスタイルシート(CORS以外)では機能しません。
- ポリフィルは、変更が必要な場合(DOMの変更、スクロール/サイズ変更ハンドラーなど)にひどく機能します。
random
キーワードのポリフィルは、かなり単純な例です。 しかし、
position: sticky
ようなポリフィルを簡単に想像できると確信しています。ユーザーがページをスクロールするたびに、ここで説明するすべてのロジックを再起動する必要があります。
改善の機会
(限られた時間のために)講義でスキップした1つの解決策は、上記の3つの問題の最初の2つを軽減する可能性があります。 これは、ビルドフェーズ中にサーバー側でCSSを解析および取得します。
次に、スタイルを持つCSSファイルをロードする代わりに、SDAを含むJavaScriptファイルをロードします。 次に、最初に行うことは、ADSを文字列ビューに変換し、ページにスタイルを追加することです。 ユーザーがJavaScriptを無効にしている場合、元のCSSファイルを参照する
<noscript>
含めることもできます。
たとえば、これの代わりに:
<link ref="stylesheet" href="styles.css">
これがあります:
<script src="styles.css.js"></script> <noscript><link ref="stylesheet" href="styles.css"></noscript>
前述したように、これにより、完全なCSSパーサーをJavaScriptバンドルに含める必要があるという問題が解決され、事前にCSS解析が可能になりますが、すべてのパフォーマンスの問題が解決されるわけではありません。
ただし、いずれにしても、変更が必要になったらすぐにCSSを書き換える必要があります。
パフォーマンスへの影響を理解する
ポリフィルのパフォーマンスが非常に低い理由を理解するには、ブラウザーのレンダリングパイプライン、特に開発者としてアクセスできるレンダリング手順を理解する必要があります。
ブラウザーのレンダリングパイプラインへのJavaScriptアクセス
ご覧のとおり、唯一の実際のエントリポイントはDOM
<style>
です。
しかし、ブラウザーのレンダリングパイプラインへのJavaScriptアクセスの現在のメカニズムを考慮すると、これがポリフィルの選択方法です。
ブラウザレンダリングパイプラインへのポリフィルエントリポイント
ご覧のとおり、JavaScriptはDOMを作成した後、元のレンダリングパイプラインを妨げることができないため、ポリフィルによって加えられた変更によりレンダリングプロセスが再び開始されます。
これは、すべての更新が後続のレンダリングに、したがって後続のフレームにつながるため、CSSポリフィルは60 fpsでは機能しません。
まとめ
この記事から、CSSでポリフィルを作成することは特に難しいことを理解してください。開発者としての私たちの仕事はすべて、現代のWebのスタイルとレイアウトの制限を回避することです。
以下は、ポリフィルが単独で行うべきことのリストです-これらはブラウザが既に行うことですが、開発者としてこれらの機能にアクセスすることはできません。
- CSSフェッチ
- CSS解析
- CSSOMの作成
- カスケード処理
- スタイルの無効化
- スタイルの再検証
そして、それこそがHoudiniで私を喜ばせていることです。Houdiniソフトウェアインターフェースがなければ、開発者はハッキングや回避策に頼らなければならず、パフォーマンスとユーザーの利便性が低下します。
そしてこれは、ポリフィラスが必ず次のいずれかになることを意味します。
- 大きすぎる
- 遅すぎる
- 間違っている
残念ながら、3つの欠点すべてを取り除くことはできません。私は選択しなければなりません。
低レベルのスタイリングプリミティブがなければ、イノベーションは最も遅いブラウザーの速度で動きます。
JavaScript開発者は、イノベーションの速度について不満を述べています。しかし、CSSでそれを聞くことは決してないでしょう。また、一部は記事に記載されている制限によるものです。
私たちはそれを変える必要があると思います。#makecssfatigueathingが必要だと思います。