การทำงานกับโมเดลออบเจ็กต์ประเภท CSS ใหม่

TL;DR

ตอนนี้ 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;

Typed OM ของ CSS ใหม่

โมเดลออบเจ็กต์ CSS ที่พิมพ์ (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 เป็นออบเจ็กต์ที่คล้ายกับ Map จึงรองรับฟังก์ชันทั่วไปทั้งหมด (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 พยายามแก้ไขปัญหาใด เมื่อดูตัวอย่างด้านบน (และตลอดทั้งบทความนี้) คุณอาจโต้แย้งว่า Typed OM ของ CSS นั้น มีคำที่ใช้มากกว่าโมเดลออบเจ็กต์แบบเดิมมาก ฉันเห็นด้วย

ก่อนที่จะเลิกใช้ Typed OM ให้พิจารณาฟีเจอร์หลักบางอย่างที่มาพร้อมกับ ฟีเจอร์นี้

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

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

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

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

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

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

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

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

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

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

พื้นฐานเกี่ยวกับ API

การเข้าถึงสไตล์

ค่าจะแยกจากหน่วยใน Typed 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 - ค่าที่มีค่า/หน่วยมากกว่า 1 รายการ เช่น นิพจน์ทางคณิตศาสตร์ (เช่น "calc(56em + 10%)")

ค่าหน่วย

ค่าตัวเลขอย่างง่าย ("50%") จะแสดงด้วยออบเจ็กต์ CSSUnitValue แม้ว่าคุณจะสร้างออบเจ็กต์เหล่านี้ได้โดยตรง (new CSSUnitValue(10, 'px')) แต่ส่วนใหญ่แล้วคุณจะใช้วิธีการของ Factory 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 รายการ ตัวอย่างที่พบบ่อยคือการสร้างนิพจน์ CSS calc() แต่ก็มีวิธีการสำหรับฟังก์ชัน 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))
]);

นอกจากความละเอียด (ฮ่าๆ) 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 Transforms เพื่อสร้างภาพเคลื่อนไหวของลูกบาศก์

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 หรืออ่านค่าในทุกเฟรม (เช่น no 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

การแยกวิเคราะห์ค่า

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 อาจจะยาวไปหน่อย แต่หวังว่า จะช่วยลดข้อบกพร่องและทำให้โค้ดมีประสิทธิภาพมากขึ้นในอนาคต