CSSポリフィルを使用する暗黒面

昨年、私はSmashing MagazineHoudiniについての記事を書き 「聞いたことのない最も素晴らしいCSSプロジェクト」と呼びました。 この記事では、Houdini APIスイートを使用すると、今日では不可能な方法でポリフィルを介してCSS機能を拡張できることを説明します。



その記事は一般に好評でしたが、同じ質問が手紙と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抽象構文ツリーの変更



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番目のオプションでは、次のように実装を更新します。





はい、理解できない場合は、カスケードの実装について説明しましたが、その実装にはブラウザへの依存が含まれます。



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の構造に従ってCSSを書き換えます。 これは、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のスタイルとレイアウトの制限を回避することです。



以下は、ポリフィルが単独で行うべきことのリストです-これらはブラウザが既に行うことですが、開発者としてこれらの機能にアクセスすることはできません。





そして、それこそがHoudiniで私を喜ばせていることですHoudiniソフトウェアインターフェースがなければ、開発者はハッキングや回避策に頼らなければならず、パフォーマンスとユーザーの利便性が低下します。



そしてこれは、ポリフィラスが必ず次のいずれかになることを意味します。





残念ながら、3つの欠点すべてを取り除くことはできません。私は選択しなければなりません。



低レベルのスタイリングプリミティブがなければ、イノベーションは最も遅いブラウザーの速度で動きます。



JavaScript開発者は、イノベーションの速度について不満を述べています。しかし、CSSでそれを聞くことは決してないでしょう。また、一部は記事に記載されている制限によるものです。



私たちはそれを変える必要があると思います。#makecssfatigueathingが必要だと思います



All Articles