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

Eric Bidelman

TL;DR

CSS hiện có API dựa trên đối tượng phù hợp để làm việc với 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'

Đã qua rồi thời kỳ để nối các chuỗi và các lỗi tinh vi!

Giới thiệu

CSSOM cũ

CSS đã có một mô hình đối tượng (CSSOM) được 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 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 mới đã nhập CSS

Mô hình đối tượng được nhập CSS (Typed OM) mới, nằm trong nỗ lực Houdini, mở rộng thế giới 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 cho các giá trị CSS. Thay vì chuỗi, các giá trị được hiển thị dưới dạng đối tượng JavaScript để hỗ trợ thao tác CSS hiệu quả (và hợp lý).

Thay vì sử dụng element.style, bạn sẽ truy cập các kiểu thông qua thuộc tính .attributeStyleMap mới cho các phần tử và 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ư bản đồ, nên StylePropertyMap hỗ trợ tất cả các nghi ngờ thông thường (get/set/keys/values/Items), giúp mã này có thể xử lý linh hoạt:

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

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

Lợi ích

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

Trước khi bạn viết tắt OM được nhập, hãy cân nhắc một số tính năng chính mà OM này mang lại cho bảng:

  • Ít lỗi hơn, ví dụ: 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!
    
  • Phép tính số học và chuyển đổi đơn vị: chuyển đổi giữa các đơn vị độ dài tuyệt đối (ví dụ: px -> cm) và thực hiện phép toán cơ bản.

  • Kẹp và làm tròn giá trị. Đã nhập các giá trị vòng và/hoặc kẹp OM để chúng nằm trong phạm vi chấp nhận được cho một cơ sở lưu trú.

  • Hiệu suất tốt hơn. Trình duyệt phải làm ít việc hơn trong việc chuyển đổi tuần tự và giải tuần tự các giá trị chuỗi. Giờ đây, công cụ này áp dụng cách hiểu tương tự về các giá trị CSS trên JS và C++. Tab Akins đã chỉ ra một số điểm chuẩn hiệu suất sớm giúp OM Typed hoạt động nhanh hơn khoảng 30% về số thao tác/giây so với việc sử dụng CSSOM và chuỗi cũ. Điều này có thể có ý nghĩa đối với ảnh động CSS nhanh sử dụng requestionAnimationFrame(). crbug.com/808933 theo dõi thêm công việc hiệu suất trong Blink.

  • Lỗi xử lý. Các phương thức phân tích cú pháp mới mang đến công cụ xử lý lỗi cho CSS.

  • "Tôi nên sử dụng tên hay chuỗi CSS được viết hoa kiểu lạc đà?" Bạn không cần phải đoán nếu tên được viết hoa kiểu lạc đà hoặc chuỗi (ví dụ: el.style.backgroundColor so với el.style['background-color']). Tên thuộc tính CSS trong OM được nhập luôn là các chuỗi, khớp với những gì bạn thực sự viết trong CSS :)

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

OM đã nhập đã được đưa vào Chrome 66 và đang được triển khai trong Firefox. Edge đã cho thấy các 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 về 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 dạng số CSS.* có được xác định hay không:

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

Kiến thức cơ bản về API

Truy cập vào các kiểu

Các giá trị được tách biệt với các đơn vị trong OM được nhập của CSS. Việc lấy 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

Các kiểu điện toán đã chuyển từ API trên window sang 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 đã nhập mới

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

Kẹp / làm tròn giá trị

Một trong những tính năng thú vị 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 đã 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 được chấp nhận, [0, 1]. Nhập OM sẽ kẹp giá trị thành 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 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ị số CSS

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

  1. CSSUnitValue – các giá trị chứa một loại đơn vị (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ể trực tiếp tạo các đối tượng này (new CSSUnitValue(10, 'px')), nhưng trong hầu hết trường hợp bạn sẽ sử dụng phương thức nhà máy 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 thông số của danh sách đầy đủ các phương thức CSS.*.

Giá trị toán học

Đối tượng CSSMathValue đại diện cho 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() của 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 dùng các hàm toán học để tạo các giá trị phức tạp hơn sẽ có đôi chút khó hiểu. Dưới đây là một vài ví dụ để giúp bạn bắt đầu. Tôi đã thêm khoảng thụt đầu dòng bổ sung để dễ đọc hơn.

calc(1px - 2 * 3em) sẽ có cấu trúc như sau:

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

calc(1px + 2px + 3px) sẽ có cấu trúc như sau:

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

calc(calc(1px + 2px) + 3px) sẽ có cấu trúc như sau:

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

Toán tử số học

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

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

Lượt chuyển đổi

Bạn có thể chuyển đổi đơn vị độ dài tuyệt đối sang độ dài đơn vị 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ị biến đổi CSS

Phép biến đổi CSS được tạo bằng CSSTransformValue và truyền một loạt 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);

Đã dịch sang OM đã nhập:

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 độ chi tiết (lolz!), CSSTransformValue có một số tính năng thú vị. Lớp này có một thuộc tính boolean để phân biệt các phép biến đổi 2D và 3D, cũng như một phương thức .toMatrix() để trả về giá trị biểu diễn 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ề việc sử dụng phép biến đổi. Chúng ta sẽ sử dụng các biến đổi JavaScript và CSS để tạo ảnh động cho một hình khối.

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ể trực tiếp tăng góc bằng toán học!
  2. Thay vì chạm vào DOM hoặc đọc lại một giá trị trên mọi khung (ví dụ: không có box.style.transform=`rotate(0,0,1,${newAngle}deg)`), ảnh động sẽ được điều hướng 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 hình khối màu đỏ nếu trình duyệt của bạn hỗ trợ Typed OM. Khối lập phương sẽ bắt đầu xoay khi bạn di chuột lên nó. Ảnh động này sử dụng phần mềm CSS Typed OM! 🤘

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

CSS var() trở thành đối tượng CSSVariableReferenceValue trong OM đã nhập. Giá trị của chúng được phân tích cú pháp thành CSSUnparsedValue vì chúng có thể lấy 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 nhận giá trị của thuộc tính tuỳ chỉnh, bạn cần làm một số việc sau:

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

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

OM đã nhập 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! Chức năng mới này có thể là một giải pháp cứu hộ tiềm năng trong việc phát hiện các lỗi ban đầu và CSS không đúng định dạng.

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ụ – hãy 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

Cuối cùng, đã có mô hình đối tượng cập nhật cho CSS. Tôi cảm thấy việc xử lý chuỗi chưa phù hợp với mình. API OM được nhập của CSS hơi chi tiết, nhưng hy vọng rằng điều này sẽ dẫn đến ít lỗi hơn và mã có hiệu suất tốt hơn về sau.