Cái giá đắt của việc phụ thuộc vào Framework
Phát triển Front-end thường mang lại cảm giác như đang chạy trên một chiếc máy chạy bộ với những lần nâng cấp liên tục. Năm nay nhóm của bạn cam kết sử dụng React; năm sau, một dự án mới lại yêu cầu Vue hoặc Svelte. Mặc dù các hệ sinh thái này cung cấp những sự trừu tượng mạnh mẽ, chúng thường dẫn đến tình trạng bị ràng buộc vào một công nghệ nhất định (vendor lock-in). Nếu bạn dành sáu tháng để xây dựng một thư viện UI mạnh mẽ bằng React, mã nguồn đó thực tế sẽ trở nên vô dụng nếu công ty của bạn mua lại một đội ngũ đang sử dụng Angular.
Web Components phá vỡ chu kỳ này. Chúng là một tập hợp các API trình duyệt gốc cho phép bạn tạo các thẻ HTML tùy chỉnh và được đóng gói. Vì chúng dựa trên các tiêu chuẩn của W3C thay vì một thư viện cụ thể, chúng chạy nguyên bản trên 97% trình duyệt được sử dụng trên toàn thế giới. Tôi đã thấy cách tiếp cận này giúp ổn định hệ thống thiết kế (design system) cho các đội ngũ doanh nghiệp, nơi các bộ phận khác nhau sử dụng các ngăn xếp công nghệ hoàn toàn khác nhau nhưng vẫn chia sẻ chung một logic UI cơ bản.
Ba trụ cột của Native Components
Việc xây dựng các thành phần không phụ thuộc vào framework dựa trên ba công nghệ cốt lõi. Đây không phải là những sự trừu tượng phức tạp. Chúng là những phần mở rộng trực tiếp của HTML và JavaScript mà bạn vẫn sử dụng hàng ngày.
1. Custom Elements
Custom Elements cho phép bạn định nghĩa các thẻ HTML của riêng mình với các hành vi chuyên biệt. Thay vì phải lồng năm thẻ <div> để tạo một thẻ hồ sơ, bạn chỉ cần sử dụng <user-card>. Trình duyệt sẽ xử lý thẻ này với mức độ ưu tiên tương đương như các thẻ <button> hoặc <section> gốc.
2. Shadow DOM
Đóng gói (Encapsulation) là rào cản lớn nhất trong CSS. Trong một tài liệu tiêu chuẩn, một quy tắc p { color: red; } vô tình có thể làm hỏng toàn bộ bố cục của bạn. Shadow DOM giải quyết vấn đề này bằng cách gắn một cây DOM cô lập vào phần tử của bạn. Các kiểu dáng được định nghĩa bên trong “shadow” này không thể rò rỉ ra ngoài và các kiểu dáng toàn cục cũng không thể xâm nhập vào bên trong. Nó cung cấp một phạm vi riêng tư thực sự cho các thành phần nội bộ của component.
3. HTML Templates và Slots
Các phần tử <template> và <slot> cho phép bạn định nghĩa các thẻ markup mà không được render cho đến khi bạn gọi nó. Các Slots đóng vai trò là nơi chứa cho nội dung động. Chúng hoạt động tương tự như props.children trong React, cho phép bạn truyền văn bản hoặc các phần tử tùy chỉnh vào một cấu trúc đã được định nghĩa trước.
Thực hành: Xây dựng một Profile Card
Hãy cùng xây dựng một thẻ hồ sơ người dùng độc lập. Thành phần này sẽ tự xử lý bố cục và kiểu dáng của nó trong khi vẫn đủ linh hoạt để chấp nhận các dữ liệu khác nhau.
Bước 1: Định nghĩa Template
Chúng ta bắt đầu bằng cách định nghĩa cấu trúc và kiểu dáng. Sử dụng biến CSS ở đây là một bước đi thông minh, vì nó cho phép các nhà phát triển bên ngoài có thể thay đổi chủ đề (theme) của component mà không làm hỏng cấu trúc bên trong.
<template id="user-card-template">
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
background: #ffffff;
width: 280px;
border-radius: 12px;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
border: 1px solid #e5e7eb;
margin: 1rem;
}
.container {
padding: 24px;
text-align: center;
}
img {
width: 96px;
height: 96px;
border-radius: 9999px;
object-fit: cover;
border: 4px solid #f3f4f6;
}
h3 {
margin: 16px 0 4px;
color: #111827;
}
p {
color: #6b7280;
font-size: 0.875rem;
margin-bottom: 16px;
}
button {
background: #2563eb;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
button:hover {
background: #1d4ed8;
}
</style>
<div class="container">
<img src="" alt="Ảnh đại diện người dùng" id="avatar" />
<h3><slot name="username">Người dùng ẩn danh</slot></h3>
<p><slot name="role">Người đóng góp</slot></p>
<button id="toggle-info">Xem hồ sơ</button>
</div>
</template>
Bước 2: Tạo Logic cho Component
Tiếp theo, chúng ta tạo một lớp JavaScript kế thừa từ HTMLElement. Đây là nơi chúng ta gắn Shadow DOM và xử lý vòng đời của component.
class UserCard extends HTMLElement {
constructor() {
super();
// Khởi tạo shadow root
this.attachShadow({ mode: 'open' });
const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
this.shadowRoot.appendChild(content);
}
connectedCallback() {
// Thiết lập nguồn ảnh từ thuộc tính hoặc mặc định
const avatarUrl = this.getAttribute('avatar') || 'https://i.pravatar.cc/150?u=default';
this.shadowRoot.querySelector('#avatar').src = avatarUrl;
this.shadowRoot.querySelector('#toggle-info').addEventListener('click', () => {
const name = this.querySelector('[slot="username"]')?.innerText || 'người dùng';
console.log(`Đang điều hướng tới hồ sơ của ${name}...`);
});
}
}
// Đăng ký element với trình duyệt
customElements.define('user-card', UserCard);
Bước 3: Triển khai
Việc sử dụng component giờ đây đơn giản như viết HTML tiêu chuẩn. Bạn có thể thả nó vào một ứng dụng React, một trang WordPress hoặc một tệp index.html thuần túy.
<!-- Cách sử dụng thuần -->
<user-card avatar="https://i.pravatar.cc/150?u=1">
<span slot="username">Alex Rivera</span>
<span slot="role">Kiến trúc sư trưởng</span>
</user-card>
<user-card avatar="https://i.pravatar.cc/150?u=2">
<span slot="username">Sarah Chen</span>
<span slot="role">Nhà thiết kế UX</span>
</user-card>
Lợi thế chiến lược
Hãy chú ý những gì còn thiếu trong quy trình này: không có npm install, không có cấu hình Webpack và không có thư viện runtime dung lượng 30KB. Trình duyệt tự xử lý các công việc nặng nhọc như render và đóng gói. Đối với một hệ thống thiết kế lớn, việc chuyển sang các thành phần gốc có thể giảm kích thước gói JS ban đầu của bạn từ 40-60KB so với một thư viện dựa trên React.
Khả năng tương tác mới là người chiến thắng thực sự ở đây. Nếu tổ chức của bạn chuyển từ React sang Vue vào quý tới, <user-card> của bạn vẫn được giữ nguyên. Bạn chỉ cần nhập script và tiếp tục sử dụng thẻ đó. Điều này giúp bảo vệ UI của bạn trước những thay đổi tất yếu trong hệ sinh thái JavaScript.
Những đánh đổi quan trọng
Web Components rất mạnh mẽ, nhưng chúng không phải là công cụ phù hợp cho mọi tác vụ. Hãy lưu ý những cân nhắc sau:
- SEO: Mặc dù Googlebot render Shadow DOM khá hiệu quả, các nội dung văn bản quan trọng vẫn nên nằm trong “Light DOM” (bên trong các slot) để đảm bảo khả năng hiển thị tối đa cho tất cả các công cụ tìm kiếm.
- Theming: Vì các kiểu dáng đã được đóng gói, bạn không thể chỉ ghi đè chúng từ một tệp CSS toàn cục. Bạn phải sử dụng CSS Parts (
::part) hoặc Biến CSS để để lộ các “móc nối” (hooks) cụ thể cho việc tùy chỉnh. - Quản lý trạng thái (State Management): Đối với các ứng dụng phức tạp với dữ liệu lồng nhau sâu, bạn có thể vẫn muốn một thư viện nhẹ như Lit. Nó cung cấp một cách khai báo hơn để xây dựng giao diện hiệu năng cao không cần Virtual DOM.
Các bước tiếp theo
Đừng cảm thấy áp lực phải viết lại toàn bộ hệ thống của bạn ngay hôm nay. Hãy bắt đầu bằng cách xác định một phần tử UI nhỏ, lặp đi lặp lại—có lẽ là một biểu tượng tải (loading spinner) hoặc một công tắc tùy chỉnh. Hãy xây dựng nó như một Web Component gốc. Một khi bạn trải nghiệm sự dễ dàng khi đưa chính component đó vào ba dự án khác nhau mà không gặp một vấn đề tương thích nào, giá trị của việc Quản lý trạng thái trên nền tảng web sẽ trở nên không thể phủ nhận.

