การทำงานกับโมเดลออบเจ็กต์ประเภท 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 แสดงว่าคุณจะใช้ URL ดังกล่าว

// 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 ได้อย่างมีประสิทธิภาพ (และสมเหตุสมผล)

คุณจะเข้าถึงรูปแบบผ่านพร็อพเพอร์ตี้ .attributeStyleMap ใหม่สำหรับองค์ประกอบ และพร็อพเพอร์ตี้ .styleMap สำหรับกฎสไตล์ชีตแทน element.style ทั้งคู่แสดงผลออบเจ็กต์ 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.

โปรดทราบว่าในตัวอย่างที่ 2 ตั้งค่า opacity เป็นสตริง ('0.3') แต่จะแสดงตัวเลขกลับมาเมื่อมีการอ่านพร็อพเพอร์ตี้ในภายหลัง

ประโยชน์

CSS Typed OM พยายามแก้ปัญหาอะไร ดูตัวอย่างด้านบน (และตลอดทั้งบทความนี้) คุณอาจโต้แย้งว่า CSS Typed OM มีรายละเอียดมากกว่าโมเดลออบเจ็กต์เดิม ตกลง!

ก่อนที่จะตัด OM ที่พิมพ์ว่า OM ลองพิจารณาคุณลักษณะสำคัญบางส่วนที่นำเสนอในตาราง ดังนี้

  • ข้อบกพร่องน้อยลง เช่น ค่าตัวเลขจะแสดงผลเป็นตัวเลขเสมอ ไม่ใช่สตริง

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • การดำเนินการทางคณิตศาสตร์และการแปลงหน่วย แปลงระหว่างหน่วยความยาวสัมบูรณ์ (เช่น px -> cm) และทำการคำนวณขั้นพื้นฐาน

  • การปรับค่าและการปัดเศษค่า รอบและ/หรือแคลมป์ของ OM ที่พิมพ์อยู่เพื่อให้อยู่ในช่วงที่ยอมรับได้สำหรับพร็อพเพอร์ตี้

  • ประสิทธิภาพที่ดีขึ้น เบราว์เซอร์ไม่ต้องทำงานด้านการเรียงลำดับและดีซีเรียลไลซ์ค่าสตริงน้อยลง ตอนนี้เครื่องมือใช้ความเข้าใจในค่า CSS ใน JS และ C++ ที่คล้ายกัน โดยแท็บ Akins ได้แสดงการเปรียบเทียบประสิทธิภาพในช่วงแรกที่ทำให้ Typed OM ดำเนินการ/วินาทีที่เร็วขึ้นประมาณ 30% เมื่อเทียบกับการใช้ CSSOM และสตริงแบบเก่า ซึ่งเป็นสิ่งสำคัญสำหรับภาพเคลื่อนไหว CSS แบบรวดเร็วที่ใช้ requestionAnimationFrame() crbug.com/808933 ติดตาม งานด้านประสิทธิภาพเพิ่มเติมใน Blink

  • การจัดการข้อผิดพลาด วิธีการแยกวิเคราะห์แบบใหม่นำมาซึ่งการจัดการข้อผิดพลาดในโลกของ CSS

  • "ฉันควรใช้ชื่อหรือสตริง CSS ที่พิมพ์ด้วยอักษร Camel ไหม" ไม่ต้องเดาอีกต่อไปว่าชื่อเป็นอูฐหรือสตริง (เช่น el.style.backgroundColor กับ el.style['background-color']) ชื่อพร็อพเพอร์ตี้ CSS ใน Typed OM มักจะเป็นสตริงซึ่งตรงกับสิ่งที่คุณเขียนใน CSS เสมอ :)

การรองรับเบราว์เซอร์และการตรวจหาฟีเจอร์

OM ที่พิมพ์ว่า OM อยู่ใน Chrome 66 และมีการใช้งานใน Firefox Edge ได้แสดงสัญญาณว่าระบบรองรับแล้ว แต่ยังไม่ได้เพิ่มฟีเจอร์ดังกล่าวลงในแดชบอร์ดของแพลตฟอร์ม

สำหรับการตรวจหาฟีเจอร์ คุณตรวจสอบได้ว่ามีการกำหนดโรงงานตัวเลข CSS.* ไว้หรือไม่

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

ข้อมูลเบื้องต้นเกี่ยวกับ API

การเข้าถึงรูปแบบ

ค่าจะแยกจากหน่วยใน OM ที่มีประเภท CSS การรับรูปแบบจะแสดงผล 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 ที่พิมพ์ใหม่

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 2 ประเภทใน 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 แสดงถึงนิพจน์ทางคณิตศาสตร์และมักมีค่า/หน่วยมากกว่า 1 รายการ ตัวอย่างที่พบบ่อยคือการสร้างนิพจน์ 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))"

Conversion

หน่วยความยาวสัมบูรณ์ สามารถแปลงเป็นความยาวหน่วยอื่นๆ ได้ ดังนี้

// 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

ความเท่าเทียม

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 ที่พิมพ์:

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

นอกจากการพูดรายละเอียดแล้ว (55555)) CSSTransformValue มีฟีเจอร์เจ๋งๆ โดยมีพร็อพเพอร์ตี้บูลีนเพื่อแยกความแตกต่างของการแปลงแบบ 2 มิติและ 3 มิติ และเมธอด .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 ใน 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 มีรายละเอียดเล็กน้อย แต่หวังว่าจะช่วยลดข้อบกพร่องและโค้ดที่มีประสิทธิภาพยิ่งขึ้นในอนาคต