עבודה עם מודל ה-CSS החדש מסוג אובייקטים מסוג CSS

אריק בידלמן

אמ;לק

עכשיו יש ל-CSS API מבוסס-אובייקטים שמתאים לעבודה עם ערכים ב-JavaScript.

el.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'

עברו הימים של שרשור המחרוזות והבאגים הקלים!

מבוא

CSSOM ישן

ל-CSS יש מודל אובייקטים (CSSOM) במשך שנים רבות. למעשה, בכל פעם שאתם קוראים או מגדירים את .style ב-JavaScript, אתם משתמשים בו:

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

OM חדש מסוג CSS

ה-CSS Typed Object Model (Typed OM) החדש חלק מהמאמץ של Houdini, מרחיב את תצוגת העולם הזו על ידי הוספת סוגים, שיטות ומודל אובייקטים מתאים לערכי CSS. במקום מחרוזות, הערכים נחשפים כאובייקטים של JavaScript כדי לאפשר מניפולציה הגיונית (והגיונית) של CSS.

במקום להשתמש ב-element.style, הגישה לסגנונות תתבצע דרך נכס .attributeStyleMap חדש לרכיבים ומאפיין .styleMap לכללים של גיליונות סגנונות. שתיהן מחזירות אובייקט StylePropertyMap.

// Element styles.
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number!

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

מכיוון שאובייקטים מסוג StylePropertyMap הם אובייקטים דמויי-מפה, הם תומכים בכל הסימנים הרגילים (get/set/keys/values/entries), ולכן הם גמישים כדי לעבוד עם:

// All 3 of these are equivalent:
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section
// el.attributeStyleMap.get('opacity').value === 0.3

// StylePropertyMaps are iterable.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
}
// → opacity, 0.3

el.attributeStyleMap.has('opacity') // true

el.attributeStyleMap.delete('opacity') // remove opacity.

el.attributeStyleMap.clear(); // remove all styles.

שימו לב שבדוגמה השנייה, השדה opacity מוגדר כמחרוזת ('0.3'), אבל מספר יוצא חזרה כשקוראים את המאפיין מאוחר יותר.

יתרונות

אז אילו בעיות CSS Typed OM מנסה לפתור? אם נסתכל על הדוגמאות שלמעלה (ובהמשך המאמר הזה), אולי תהיה לכם אפשרות לטעון ש-CSS Typed OM הרבה יותר מילולי ממודל האובייקטים הישן. אני מסכים/ה!

לפני שמוחקים את Typed OM, כדאי להביא בחשבון כמה מהתכונות העיקריות שהוא מציג:

  • פחות באגים. למשל, ערכים מספריים תמיד מוחזרים כמספרים, לא מחרוזות.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • פעולות אריתמטיות והמרת יחידות. ניתן להמיר יחידות באורך מוחלט (למשל px -> cm) ולבצע פעולות מתמטיות בסיסיות.

  • עיגול ועיגול של ערך. מוקלד/ה בעיגול או בעיגול של ערכים מסוג OM כדי שהם יהיו בטווחים הקבילים לנכס.

  • ביצועים טובים יותר. הדפדפן צריך פחות עבודה לגבי סידור של ערכי מחרוזת וביצוע פעולת deserialize. עכשיו, המנוע משתמש בהבנה דומה של ערכי CSS ב-JS וב-C++. חברת Tab Akins הציגה כמה אמות השוואה של ביצועים ראשוניים שמציבות את Typed OM כ-30% מהר יותר בפעולות לשנייה, בהשוואה לשימוש ב-CSSOM ובמחרוזות הישנים. זה יכול להיות משמעותי עבור אנימציות CSS מהירות באמצעות requestionAnimationFrame(). crbug.com/808933 עוקב אחר פעולות ביצועים נוספות ב-Blink.

  • טיפול בשגיאות. שיטות ניתוח חדשות מאפשרות טיפול בשגיאות בעולם ה-CSS.

  • "האם כדאי להשתמש בשמות או במחרוזות CSS הכוללים אותיות גמלים?" לא צריך יותר לנחש אם השמות הם באותיות גמלים או מחרוזות (למשל el.style.backgroundColor נגד el.style['background-color']). שמות של מאפייני CSS בסוג OM הם תמיד מחרוזות, שתואמים למה שנכתב בפועל ב-CSS :)

תמיכה בדפדפן וזיהוי תכונות

סוג OM נחתה ב-Chrome 66 ומיושם ב-Firefox. ב-Edge הציגה אותות של תמיכה, אבל עדיין לא הוסיפה אותה למרכז הבקרה של הפלטפורמה.

לצורך זיהוי תכונות, אפשר לבדוק אם אחד מהמפעלים המספריים של CSS.* מוגדר:

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}

יסודות ה-API

גישה לסגנונות

הערכים נפרדים מהיחידות ב-CSS מסוג OM. כשמתקבלת סגנון, מוחזרת CSSUnitValue שמכיל value ו-unit:

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // string arg also works.
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

// Use CSSKeyWorldValue for plain text values:
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

סגנונות מחושבים

סגנונות מחושבים עברו מ-API ב-window לשיטה חדשה בתאריך HTMLElement, computedStyleMap():

CSSOM ישן

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!

OM מסוג OM חדש

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5

אכיפת מינימום / עיגול ערך

אחת התכונות הנחמדות של מודל האובייקטים החדש היא הצמדה אוטומטית ו/או עיגול של ערכי סגנון מחושבים. לדוגמה, נניח שמנסים להגדיר את opacity לערך מחוץ לטווח הקביל, [0, 1]. OM מוקלד מהדק את הערך ל-1 בעת חישוב הסגנון:

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // val not clamped.
el.computedStyleMap().get('opacity').value === 1 // computed style clamps value.

באופן דומה, אם מגדירים z-index:15.4 מעוגל ל-15, כך שהערך יישאר מספר שלם.

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // val not rounded.
el.computedStyleMap().get('z-index').value === 15   // computed style is rounded.

ערכים מספריים של CSS

מספרים מיוצגים על ידי שני סוגים של אובייקטים מסוג CSSNumericValue ב-Typed OM:

  1. CSSUnitValue - ערכים שמכילים סוג יחידה יחיד (למשל "42px").
  2. CSSMathValue - ערכים שמכילים יותר מערך אחד או יחידה אחת, כמו ביטוי מתמטי (למשל "calc(56em + 10%)").

ערכי יחידה

ערכים מספריים פשוטים ("50%") מיוצגים על ידי CSSUnitValue אובייקטים. אפשר ליצור את האובייקטים האלה ישירות (new CSSUnitValue(10, 'px')), אבל ברוב המקרים משתמשים בשיטות המקוריות של CSS.*:

const {value, unit} = CSS.number('10');
// value === 10, unit === 'number'

const {value, unit} = CSS.px(42);
// value === 42, unit === 'px'

const {value, unit} = CSS.vw('100');
// value === 100, unit === 'vw'

const {value, unit} = CSS.percent('10');
// value === 10, unit === 'percent'

const {value, unit} = CSS.deg(45);
// value === 45, unit === 'deg'

const {value, unit} = CSS.ms(300);
// value === 300, unit === 'ms'

לרשימה המלאה של השיטות CSS.*

ערכים מתמטיים

CSSMathValue אובייקטים מייצגים ביטויים מתמטיים ולרוב מכילים יותר מערך אחד/יחידה אחת. הדוגמה הנפוצה היא יצירת ביטוי calc() של CSS, אבל יש שיטות לכל פונקציות ה-CSS: calc(), min(), max().

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)"

new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)"

new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)"

new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"

new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)"

new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)"

ביטויים מקוננים

השימוש בפונקציות המתמטיות כדי ליצור ערכים מורכבים יותר נעשה קצת מבלבל. בהמשך מפורטות כמה דוגמאות שיעזרו לכם להתחיל. הוספתי כניסת פסקה כדי שיהיה קל יותר לקרוא אותן.

calc(1px - 2 * 3em) ייבנה כך:

new CSSMathSum(
  CSS.px(1),
  new CSSMathNegate(
    new CSSMathProduct(2, CSS.em(3))
  )
);

calc(1px + 2px + 3px) ייבנה כך:

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3));

calc(calc(1px + 2px) + 3px) ייבנה כך:

new CSSMathSum(
  new CSSMathSum(CSS.px(1), CSS.px(2)),
  CSS.px(3)
);

פעולות חשבון

אחת התכונות השימושיות ביותר ב-CSS Typed OM היא שאפשר לבצע פעולות מתמטיות באובייקטים של CSSUnitValue.

פעולות בסיסיות

תמיכה בפעולות בסיסיות (add/sub/mul/div/min/max):

CSS.deg(45).mul(2) // {value: 90, unit: "deg"}

CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)"

// Can Pass CSSUnitValue:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

המרה

ניתן להמיר יחידות אורך מוחלט לאורכי יחידות אחרים:

// Convert px to other absolute/physical lengths.
el.attributeStyleMap.set('width', '500px');
const width = el.attributeStyleMap.get('width');
width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"}
width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"}
width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"}

CSS.deg(200).to('rad').value // 3.49066...
CSS.s(2).to('ms').value // 2000

Equality

const width = CSS.px(200);
CSS.px(200).equals(width) // true

const rads = CSS.deg(180).to('rad');
CSS.deg(180).equals(rads.to('deg')) // true

ערכי טרנספורמציה של CSS

המרות CSS נוצרות באמצעות CSSTransformValue ומעבירות מערך של ערכי המרות (למשל CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). לדוגמה, נניח שאתם רוצים ליצור מחדש את ה-CSS הזה:

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

תורגם ל-OM מסוג OM:

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

בנוסף לדרגת המלל (lolz!), ל-CSSTransformValue יש כמה תכונות מגניבות. יש לו מאפיין בוליאני כדי להבחין בין טרנספורמציות דו-ממדיות ותלת-ממדיות ו-method .toMatrix() שמחזירה את הייצוג DOMMatrix של טרנספורמציה:

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

דוגמה: אנימציה של קובייה

בואו נראה דוגמה מעשית לשימוש בטרנספורמציות. אנחנו נשתמש ב-JavaScript ובהמרות של CSS כדי להנפיש קובייה.

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0));
const transform = new CSSTransformValue([rotate]);

const box = document.querySelector('#box');
box.attributeStyleMap.set('transform', transform);

(function draw() {
  requestAnimationFrame(draw);
  transform[0].angle.value += 5; // Update the transform's angle.
  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.
  box.attributeStyleMap.set('transform', transform); // commit it.
})();

שימו לב כי:

  1. המשמעות של ערכים מספריים היא שאפשר להגדיל את הזווית באופן ישיר באמצעות מתמטיקה!
  2. במקום לגעת ב-DOM או לקרוא ערך בכל פריים (למשל, לא box.style.transform=`rotate(0,0,1,${newAngle}deg)`), האנימציה מתבססת על עדכון אובייקט הנתונים ב-CSSTransformValue שבבסיסו, וכך משפר את הביצועים.

הדגמה (דמו)

למטה, תראה קובייה אדומה אם הדפדפן שלך תומך ב-Typed OM. הקובייה מתחילה להסתובב כשמעבירים את העכבר מעליה. האנימציה מופעלת על ידי CSS Typed OM! 🤘

ערכים של מאפיינים מותאמים אישית של CSS

CSS var() הופך לאובייקט CSSVariableReferenceValue ב-Type OM. הערכים שלהם מנותחים אל CSSUnparsedValue כי הם יכולים להיות מכל סוג (px, %, em, rgba() וכו').

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));
// padding.variable === '--default-padding'
// padding.fallback instanceof CSSUnparsedValue === true
// padding.fallback[0] === '8px'

אם אתם רוצים לקבל את הערך של נכס מותאם אישית, יש כמה דברים שצריך לעשות:

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

ערכי מיקום

מאפייני CSS שמופרדים ברווחים עם מיקום x/y, כמו object-position, מיוצגים על ידי אובייקטים מסוג CSSPositionValue.

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);

console.log(position.x.value, position.y.value);
// → 5, 10

ניתוח ערכים

הכלי Typed OM מוסיף שיטות ניתוח לפלטפורמת האינטרנט! במילים אחרות, תוכלו לנתח את ערכי ה-CSS באופן פרוגרמטי לפני שתנסו להשתמש בהם. היכולת החדשה הזו תציל חיים באיתור באגים מוקדמים ובעיות ב-CSS.

ניתוח סגנון מלא:

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

מנתחים את הערכים ל-CSSUnitValue:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

טיפול בשגיאות

דוגמה – צריך לבדוק אם מנתח ה-CSS ישמח לקבל את הערך הזה של transform:

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // use css
} catch (err) {
  console.err(err);
}

סיכום

איזה כיף שיש מודל אובייקטים מעודכן ל-CSS. אף פעם לא נראה לי נכון לעבוד עם מיתרים. ה-CSS Typed OM API קצת ארוך, אבל אנחנו מקווים שהוא יגרום לפחות באגים ולביצועים טובים יותר בהמשך.