العمل باستخدام نموذج جديد لكائنات CSS المكتوبة

إريك بيدلمان

الملخّص

أصبحت لغة CSS الآن واجهة برمجة تطبيقات مناسبة قائمة على الكائنات للعمل مع القيم في 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;

تقرير تشغيلي جديد لصفحات الأنماط المتتالية (CSS)

إنّ نموذج كائن CSS Typed الجديد (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/الإدخالات)، ما يجعلها مرنة عند استخدامها:

// 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')، ولكن يظهر رقم عند إعادة قراءة الخاصية لاحقًا.

المزايا

إذًا، ما هي المشاكل التي يحاول نظام إدارة المحتوى (OM) في كتابة CSS حلّها؟ وبالنظر إلى الأمثلة أعلاه (وباقي هذه المقالة)، يمكنك القول إن CSS Typed OM أكثر مدّة بكثير من نموذج الكائن القديم. أوافقك!

قبل شطب Typed OM، ضع في اعتبارك بعض الميزات الرئيسية التي يضيفها إلى الجدول:

  • أخطاء أقل: على سبيل المثال، يتم عرض قيم رقمية دائمًا كأرقام، وليس كسلاسل.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • العمليات الحسابية وتحويل الوحدات. يتم التحويل بين وحدات الطول المطلق (مثل px -> cm) وإجراء العمليات الحسابية الأساسية.

  • تقريب القيمة وتقريبها: قيم OM مكتوبة على شكل دائري و/أو مثبّت بحيث تكون ضمن النطاقات المقبولة لأحد المواقع.

  • تحسين الأداء: يتعين على المتصفح القيام بأعمال أقل في التسلسل وإلغاء تسلسل قيم السلسلة. أما الآن، فيستخدم المحرّك طريقة فهم مشابهة لقيم CSS على مستوى JavaScript وC++. وقد أظهرت علامة التبويب Akins بعض مقاييس الأداء الأولية التي جعلت إدارة العمليات المكتوبة بـ أسرع بحوالي% 30 في العمليات/العمليات عند مقارنتها باستخدام سلسلة CSSOM القديمة والسلاسل القديمة. ويمكن أن يكون ذلك مهمًا للصور المتحركة السريعة في CSS التي تستخدم requestionAnimationFrame(). يتتبّع crbug.com/808933 أعمال الأداء الإضافية في Blink.

  • حدث الخطأ أثناء المعالجة. تضيف أساليب التحليل الجديدة ميزة التعامل مع الأخطاء إلى لغة CSS.

  • "هل يجب استخدام أسماء أو سلاسل CSS لحالة الجمل؟" ما من داعٍ للتخمين إذا كانت الأسماء مكتوبة على شكل جمل أو سلاسل (مثل el.style.backgroundColor مقابل el.style['background-color']). تكون أسماء سمات CSS في Typed OM دائمًا سلاسل تطابق ما تكتبه فعليًا في CSS :)

دعم المتصفّح ورصد الميزات

وصلت عبارة OM المكتوبة إلى متصفّح Chrome 66 ويجري تنفيذها في Firefox. أظهر متصفّح Edge علامات تدلّ على توفّر ميزات متوافقة، ولكن لم تتم إضافته بعد إلى لوحة البيانات الخاصة به.

لاكتشاف الميزات، يمكنك التحقق من تحديد أحد المصانع الرقمية لـ CSS.*:

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

أساسيات واجهة برمجة التطبيقات

الوصول إلى الأنماط

تكون القيم منفصلة عن الوحدات في CSS Typed 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

الأنماط التي تم احتسابها

تم نقل الأنماط المحسوبة من واجهة برمجة تطبيقات في window إلى طريقة جديدة في HTMLElement computedStyleMap():

أداة CSSOM القديمة

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

إدارة إدارة الطلبات الجديدة

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);

مترجَمة إلى الكتابة بالأحرف اللاتينية الكبيرة:

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))
]);

بالإضافة إلى الإسهاب (ههههه!)، يحتوي CSSTransformValue على بعض الميزات الرائعة. وتحتوي على خاصية منطقية للتفريق بين التحويلات الثنائية الأبعاد والثلاثية الأبعاد وطريقة .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 في Typed 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

تحليل القيم

يقدم OM Typed طرق التحليل إلى النظام الأساسي للويب! وهذا يعني أنّه يمكنك أخيرًا تحليل قيم 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 بأنّها مطوَّلة بعض الشيء، ولكن نأمل أن ينتج عنها أخطاء أقل ورموز برمجية أفضل.