Làm việc với Mô hình đối tượng được nhập bằng CSS mới

TL;DR

CSS hiện có một API dựa trên đối tượng phù hợp để xử lý các giá trị trong JavaScript.

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

Những ngày nối chuỗi và lỗi nhỏ đã qua!

Giới thiệu

CSSOM cũ

CSS đã có một mô hình đối tượng (CSSOM) trong nhiều năm. Trên thực tế, bất cứ khi nào bạn đọc/đặt .style trong JavaScript, bạn đều đang sử dụng nó:

// 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 được nhập CSS mới

Mô hình đối tượng được nhập CSS (Typed OM) mới, một phần của nỗ lực Houdini, mở rộng thế giới quan này bằng cách thêm các loại, phương thức và mô hình đối tượng phù hợp vào các giá trị CSS. Thay vì các chuỗi, các giá trị được hiển thị dưới dạng đối tượng JavaScript để tạo điều kiện cho việc thao tác hiệu quả (và hợp lý) với CSS.

Thay vì sử dụng element.style, bạn sẽ truy cập vào các kiểu thông qua một thuộc tính .attributeStyleMap mới cho các phần tử và một thuộc tính .styleMap cho các quy tắc biểu định kiểu. Cả hai đều trả về một đối tượng 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 là các đối tượng giống như Map, nên chúng hỗ trợ tất cả các đối tượng thường gặp (get/set/keys/values/entries), giúp bạn linh hoạt khi làm việc với chúng:

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

Xin lưu ý rằng trong ví dụ thứ hai, opacity được đặt thành chuỗi ('0.3') nhưng một số sẽ quay lại khi thuộc tính được đọc lại sau đó.

Lợi ích

Vậy CSS Typed OM đang cố gắng giải quyết những vấn đề gì? Nhìn vào các ví dụ trên (và trong phần còn lại của bài viết này), bạn có thể cho rằng CSS Typed OM chi tiết hơn nhiều so với mô hình đối tượng cũ. Tôi đồng ý!

Trước khi loại bỏ Typed OM, hãy cân nhắc một số tính năng chính mà nó mang lại:

  • Ít lỗi hơn. Ví dụ: các giá trị số luôn được trả về dưới dạng số, chứ không phải chuỗi.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Các phép toán số học và chuyển đổi đơn vị. chuyển đổi giữa các đơn vị chiều dài tuyệt đối (ví dụ: px -> cm) và thực hiện các phép toán cơ bản.

  • Giới hạn và làm tròn giá trị. OM được nhập làm tròn và/hoặc giới hạn các giá trị để chúng nằm trong phạm vi chấp nhận được của một thuộc tính.

  • Hiệu suất cao hơn. Trình duyệt phải thực hiện ít thao tác hơn khi chuyển đổi tuần tự và chuyển đổi không tuần tự các giá trị chuỗi. Giờ đây, công cụ này sử dụng cách hiểu tương tự về các giá trị CSS trên JS và C++. Tab Akins đã cho thấy một số điểm chuẩn hiệu suất ban đầu cho thấy Typed OM nhanh hơn ~30% trong các thao tác/giây khi so sánh với việc sử dụng CSSOM và các chuỗi cũ. Điều này có thể rất quan trọng đối với ảnh động CSS nhanh bằng cách sử dụng requestionAnimationFrame(). crbug.com/808933 theo dõi công việc bổ sung về hiệu suất trong Blink.

  • Xử lý lỗi. Các phương thức phân tích cú pháp mới mang đến khả năng xử lý lỗi trong thế giới CSS.

  • "Tôi có nên sử dụng tên hoặc chuỗi CSS theo quy tắc viết hoa chữ cái đầu của từ thứ hai không?" Bạn không cần phải đoán xem tên có phải là camel-case hay chuỗi (ví dụ: el.style.backgroundColor so với el.style['background-color']). Tên thuộc tính CSS trong Typed OM luôn là chuỗi, khớp với nội dung bạn thực sự viết trong CSS :)

Hỗ trợ trình duyệt và phát hiện tính năng

Typed OM đã có trong Chrome 66 và đang được triển khai trong Firefox. Edge đã thể hiện dấu hiệu hỗ trợ, nhưng vẫn chưa thêm tính năng này vào trang tổng quan nền tảng của họ.

Để phát hiện tính năng, bạn có thể kiểm tra xem một trong các nhà máy số CSS.* có được xác định hay không:

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

Thông tin cơ bản về API

Truy cập vào kiểu

Các giá trị tách biệt với các đơn vị trong CSS Typed OM. Việc nhận một kiểu sẽ trả về CSSUnitValue chứa valueunit:

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

Kiểu đã tính toán

Kiểu được tính toán đã được chuyển từ một API trên window sang một phương thức mới trên HTMLElement, computedStyleMap():

CSSOM cũ

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

OM được nhập mới

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

Giới hạn / làm tròn giá trị

Một trong những tính năng hay của mô hình đối tượng mới là tự động kẹp và/hoặc làm tròn các giá trị kiểu được tính toán. Ví dụ: giả sử bạn cố gắng đặt opacity thành một giá trị nằm ngoài phạm vi chấp nhận được, [0, 1]. Các kẹp OM được nhập sẽ giới hạn giá trị ở 1 khi tính toán kiểu:

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

Tương tự, việc đặt z-index:15.4 sẽ làm tròn thành 15 để giá trị vẫn là một số nguyên.

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.

Giá trị bằng số của CSS

Các số được biểu thị bằng hai loại đối tượng CSSNumericValue trong Typed OM:

  1. CSSUnitValue – các giá trị chứa một loại đơn vị duy nhất (ví dụ: "42px").
  2. CSSMathValue – các giá trị chứa nhiều giá trị/đơn vị, chẳng hạn như biểu thức toán học (ví dụ: "calc(56em + 10%)").

Giá trị đơn vị

Các giá trị số đơn giản ("50%") được biểu thị bằng các đối tượng CSSUnitValue. Mặc dù bạn có thể tạo trực tiếp các đối tượng này (new CSSUnitValue(10, 'px')), nhưng hầu hết thời gian bạn sẽ sử dụng các phương thức tạo 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'

Xem quy cách để biết danh sách đầy đủ các phương thức CSS.*.

Giá trị toán học

Các đối tượng CSSMathValue biểu thị các biểu thức toán học và thường chứa nhiều giá trị/đơn vị. Ví dụ phổ biến là tạo biểu thức calc() CSS, nhưng có các phương thức cho tất cả các hàm 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)"

Biểu thức lồng nhau

Việc sử dụng các hàm toán học để tạo ra các giá trị phức tạp hơn sẽ hơi khó hiểu. Dưới đây là một số ví dụ để giúp bạn bắt đầu. Tôi đã thêm khoảng thụt lề để giúp bạn dễ đọc hơn.

calc(1px - 2 * 3em) sẽ được tạo như sau:

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

calc(1px + 2px + 3px) sẽ được tạo như sau:

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

calc(calc(1px + 2px) + 3px) sẽ được tạo như sau:

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

Phép toán số học

Một trong những tính năng hữu ích nhất của The CSS Typed OM là bạn có thể thực hiện các phép toán trên các đối tượng CSSUnitValue.

Các thao tác cơ bản

Các thao tác cơ bản (add/sub/mul/div/min/max) được hỗ trợ:

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

Chuyển đổi

Đơn vị chiều dài tuyệt đối có thể được chuyển đổi sang các đơn vị chiều dài khác:

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

Bình đẳng

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

Giá trị chuyển đổi CSS

Các phép biến đổi CSS được tạo bằng CSSTransformValue và truyền một mảng các giá trị biến đổi (ví dụ: CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Ví dụ: giả sử bạn muốn tạo lại CSS này:

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

Được dịch thành Typed 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))
]);

Ngoài sự dài dòng (cười lớn!), CSSTransformValue có một số tính năng thú vị. Nó có một thuộc tính boolean để phân biệt các phép biến đổi 2D và 3D, đồng thời có một phương thức .toMatrix() để trả về biểu thị DOMMatrix của một phép biến đổi:

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

Ví dụ: tạo ảnh động cho một khối lập phương

Hãy xem một ví dụ thực tế về cách sử dụng các phép biến đổi. Chúng ta sẽ sử dụng JavaScript và các phép biến đổi CSS để tạo ảnh động cho một khối lập phương.

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.
})();

Lưu ý là:

  1. Giá trị số có nghĩa là chúng ta có thể tăng góc trực tiếp bằng cách sử dụng phép toán!
  2. Thay vì chạm vào DOM hoặc đọc lại một giá trị trên mọi khung hình (ví dụ: không có box.style.transform=`rotate(0,0,1,${newAngle}deg)`), ảnh động được điều khiển bằng cách cập nhật đối tượng dữ liệu CSSTransformValue cơ bản, cải thiện hiệu suất.

Bản minh hoạ

Ở bên dưới, bạn sẽ thấy một khối lập phương màu đỏ nếu trình duyệt của bạn hỗ trợ Typed OM. Khối lập phương bắt đầu xoay khi bạn di chuột lên khối đó. Ảnh động được hỗ trợ bởi CSS Typed OM! 🤘

Giá trị thuộc tính tuỳ chỉnh CSS

var() CSS trở thành một đối tượng CSSVariableReferenceValue trong OM được nhập. Các giá trị của chúng được phân tích cú pháp thành CSSUnparsedValue vì chúng có thể nhận bất kỳ loại nào (px, %, em, rgba(), v.v.).

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'

Nếu muốn lấy giá trị của một thuộc tính tuỳ chỉnh, bạn cần thực hiện một số thao tác:

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

Giá trị vị trí

Các thuộc tính CSS lấy vị trí x/y được phân tách bằng dấu cách, chẳng hạn như object-position, được biểu thị bằng các đối tượng 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

Phân tích cú pháp giá trị

Typed OM giới thiệu các phương thức phân tích cú pháp cho nền tảng web! Điều này có nghĩa là cuối cùng bạn có thể phân tích cú pháp các giá trị CSS theo phương thức lập trình, trước khi cố gắng sử dụng giá trị đó! Khả năng mới này có thể giúp bạn phát hiện sớm các lỗi và CSS bị lỗi.

Phân tích cú pháp một kiểu đầy đủ:

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

Phân tích cú pháp các giá trị thành CSSUnitValue:

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

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

Xử lý lỗi

Ví dụ – kiểm tra xem trình phân tích cú pháp CSS có hài lòng với giá trị transform này hay không:

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

Kết luận

Thật tuyệt khi cuối cùng cũng có một mô hình đối tượng được cập nhật cho CSS. Tôi chưa bao giờ cảm thấy thoải mái khi làm việc với các chuỗi. API CSS Typed OM hơi dài dòng, nhưng hy vọng rằng API này sẽ giúp giảm số lượng lỗi và tăng hiệu suất của mã theo thời gian.