1. Modular design là gì? Và vì sao thất bại?
Ban đầu, team xây dựng website RedHat.com không hề có khái niệm “modular design”. Mỗi “band” (khối nội dung – content block) được viết theo kiểu top-down – nghĩa là style CSS được gắn rất cụ thể vào từng cấu trúc markup, ví dụ:
.about-contact .hero1 .container > section.features-quarter > section.f-contact h3
Hệ quả:
- Không thể tái sử dụng một phần tử ở nơi khác.
- Cần viết selector dài và phức tạp.
- Markup phụ thuộc vị trí, không linh hoạt.
🎯 Khi được hỏi “Thiết kế của mình có modular không?”, câu trả lời duy nhất là: PHẢI VIẾT LẠI TOÀN BỘ.
2. Debug: Phân rã Layout và Component

➤ Layout:
- Mô tả bố cục: chia cột, canh lề, lưới.
- Không áp đặt padding hay style lên component con.

➤ Component:
- Là mảnh nhỏ nội dung như: quote, icon, blog preview.
- “Dumb”: không chứa padding, margin, background.
- Tự mở rộng theo bố cục cha.
- Có thể đặt cạnh nhau và vẫn canh hàng hoàn hảo (xem hình).
🎯 Từ đây, bất kỳ layout nào cũng có thể tái sử dụng → giảm trùng lặp code cực mạnh.
3. Hệ thống hóa với “Road Runner Rules” 🏁
📐 Triết lý:
Lấy cảm hứng từ 9 quy tắc làm phim Road Runner (Chuck Jones), nhóm viết ra bộ quy tắc bất biến cho hệ thống thiết kế:
🔒 Danh sách Road Runner Rules:
- Layout không áp padding hoặc style cho component con.
- Theme không tự thay đổi appearance – chỉ cung cấp context.
- Component luôn chạm 4 cạnh của container.
- Component KHÔNG có background, float, padding/margin bên ngoài.
- Mỗi element có 1 class duy nhất, chỉ dùng trong 1 component.
- Không dùng margin-top – element đầu tiên phải dính sát trên.
- JavaScript không gắn vào class – chỉ sử dụng qua data-attributes.
⚠️ Đặc biệt: Mỗi rule phải bắt đầu bằng các từ: always
, never
, only
, every
, do
, don’t
.
4. Triết lý: Single Responsibility + Single Source of Truth
- Mỗi class chỉ dùng 1 mục đích duy nhất, ví dụ
.rh-standard-band-title
. - Nếu component bị deprecated, có thể xóa toàn bộ CSS liên quan mà không ảnh hưởng nơi khác.
☝️ Single Source of Truth:
- Style của một element (ví dụ
.rh-standard-band-title
) chỉ được định nghĩa duy nhất trong file Sass của component đó. - Kể cả các context hoặc modifier cũng phải định nghĩa tại chỗ, ví dụ:
.some-context .rh-standard-band-title { ... } // phải nằm trong file Sass của .rh-standard-band
5. Modifier & Context: Chỉ hoạt động khi được “opt-in”
Không dùng modifier như .is-dark
theo kiểu BEM. Thay vào đó dùng data-attributes
:
<div class="rh-card--layout" data-rh-theme="dark" data-rh-justify="center">
📦 Lợi ích của data-attribute:
- Có cấu trúc rõ ràng (
data-rh-theme
,data-rh-align
,data-rh-layout
) - Có thể truyền giá trị (ví dụ
"dark"
,"center"
) thay vì chỉ bật/tắt class.
📌 Ví dụ:
.rh-card--layout { &[data-rh-theme="light"] { background: white; } &[data-rh-theme="dark"] { background: black; } &[data-rh-justify="center"] { text-align: center; } }
6. Context thông minh: component “biết mình ở đâu”
🔄 Một component sẽ thay đổi có kiểm soát khi nằm trong context nhất định.
📌 Ví dụ:
- Khi
data-rh-theme="dark"
được gắn vào card, icon bên trong đổi màu trắng. - Khi theme chuyển thành
"light"
, icon đổi màu đen.
.rh-icon { &[data-rh-theme="dark"] & { color: white; } &[data-rh-theme="light"] & { color: black; } }
⚠️ Chỉ những thành phần nào opt-in mới bị ảnh hưởng → kiểm soát chặt, không rò rỉ style.
7. Semantic Grids: Grid layout đơn giản hóa
❌ Không còn .row .col-4
như Bootstrap.
✅ Thay vào đó: dùng data-rh-layout="gallery5"
để định nghĩa layout.

<div class="band-content" data-rh-layout="gallery5"> <div class="card">...</div> <div class="card">...</div> </div>
🎯 Khi muốn đổi layout:
- data-rh-layout="gallery5"
+ data-rh-layout="gallery4"
📌 Lợi ích:
- Không cần thêm class hay container thừa.
- Tăng khả năng semantic và modular.
- Dễ kiểm thử và tạo visual regression test.
📊 Tổng kết bảng quy tắc & triết lý chính
Quy tắc / Triết lý | Ý nghĩa chính |
---|---|
Single Responsibility Principle | Mỗi class/component chỉ dùng cho 1 mục đích duy nhất |
Single Source of Truth | Style chỉ định nghĩa tại 1 nơi (trong Sass component của chính nó) |
Opt-in Modifier & Context | Tất cả hiệu ứng style phải “chủ động” nhận, không bị áp ngầm |
Road Runner Rules | Bộ quy tắc bất biến, ngắn gọn, dùng để kiểm soát toàn bộ hệ thống |
Semantic Grid via Data Attributes | Layout được định nghĩa rõ ràng qua data-* , không cần class phụ |
No Top Margins | Giúp các component xếp hàng chuẩn xác khi đặt cạnh nhau |
No Global JS Binding | JS dùng qua data-* , không gắn trực tiếp vào class component |
8. Red Hat Process
📌 Bối cảnh: “Last Mile Problem” trong phát triển frontend
- Trước đây:
- Frontend team làm prototype riêng (Mustache, Twig templates).
- Backend team (PHP, Ruby, Angular, React, Ember…) cố gắng copy markup theo prototype.
- Vấn đề:
- Markup thực tế thường không đồng bộ với prototype.
- Prototype và CMS code dần lệch nhau → Technical debt tăng mạnh → Prototype mất giá trị.
📌 Giải pháp: Chung một engine render
- Đột phá:
- Dùng Twig templates cho cả prototyping tool lẫn CMS (Drupal, WordPress).
- Prototype và production dùng chung 1 template → không còn dịch markup từ hệ thống này sang hệ thống khác.
- Kết quả:
- Backend chỉ cần đổ đúng data vào template.
- Xây dựng được Design System API:
“Đưa đúng data → Trả về HTML chuẩn xác bất kể platform nào.”
📌 Standard Deliverables: Bộ profile chuẩn cho mỗi component
Mỗi component/layout đều có:
Tài liệu | Ý nghĩa |
---|---|
JSON schema | Mô tả biến, kiểu dữ liệu, required/optional |
Template file (Twig) | Nhận data từ schema → render HTML |
Sass partial | Chứa style của component |
Visual regression tests | Test UI với nhiều width, nhiều trạng thái |
Testing data | Data phục vụ test edge cases |
Documentation (Markdown) | Giải thích cách dùng component |
Documentation data | Data mẫu để hiển thị trên doc page |
✅ JSON schema là trái tim của hệ thống: mọi thay đổi bắt đầu từ schema → lan ra các file còn lại.
📌 Schema-Driven Design System: Xây dựng từ dữ liệu, không từ giao diện
🎯 Tư duy cốt lõi: Schema trước, giao diện sau
- Test-Driven Development: viết test trước → code để pass test.
- Schema-Driven Design: viết schema mô tả dữ liệu trước → giao diện và code phục vụ đúng dữ liệu đó.
Điểm mạnh triết lý này:
- Tách biệt hoàn toàn hình thức (markup, style) khỏi nội dung và cấu trúc dữ liệu.
- Thiết kế UX, frontend, backend đều thống nhất ngay từ dòng dữ liệu đầu tiên, không cần phỏng đoán.
👉 Trước khi hỏi: “HTML này trông ra sao?”, chúng ta phải trả lời: “Người dùng có những dữ liệu gì? Cần nhập gì? Ràng buộc ra sao?”
- Tương tự như Test-Driven Development:
- Test-Driven: Viết test trước, code sau.
- Schema-Driven: Viết schema mô tả dữ liệu trước, giao diện code sau.
Lợi ích:
- Xác định rõ content model ngay từ đầu.
- Giảm mơ hồ khi giao tiếp giữa design & development.
- Tăng khả năng mở rộng, tự động hóa UI editor.
🛠️ Thành phần quan trọng nhất: JSON Schema
Mỗi component (ví dụ logo-wall) có file .schema.json
mô tả:
Thành phần | Ý nghĩa |
---|---|
type | Kiểu dữ liệu tổng thể (object , array , string …) |
properties | Các trường dữ liệu con (field/key) |
required | Trường nào bắt buộc phải có |
oneOf / anyOf / allOf | Rẽ nhánh schema theo biến thể |
format | Gợi ý UI element (text input, textarea, file upload…) |
$ref | Tái sử dụng schema con hoặc định nghĩa chung |
definitions | Các schema phụ được định nghĩa dùng lại |
💬 Tác động thực tiễn của Schema
- Frontend Devs:
- Không cần hỏi PM/Designer: “Ở chỗ này nhập text hay upload file?”
- Đọc schema là biết:
- Input type
- Field required hay optional
- Dùng layout nào (ví dụ 2up vs 3up)
- UI/UX Team:
- Tạo JSON-editor auto-generate form nhập liệu từ schema.
- Không còn nguy cơ “UI builder hiểu sai yêu cầu”.
- Backend Devs:
- Chỉ cần cung cấp đúng dữ liệu theo schema đã định → auto generate đúng HTML qua template.
- Automation & QA:
- Schema dùng để:
- Validate dữ liệu trước khi render.
- Sinh các test cases để kiểm thử input/output.
- Schema dùng để:
📌 Ví dụ: Schema cho logo-wall component
🧩 JSON Schema mẫu
- Headline: optional, kiểu text.
- Body: object, 2 lựa chọn:
- “Two Up” → 2 logos.
- “Three Up” → 3 logos.
- Logo:
- Upload file hoặc paste URL.
🧠 Điểm hay:
- Dùng oneOf để định nghĩa 2 biến thể.
- Dùng $ref để tái sử dụng định nghĩa logo cho cả 2 layouts.
- Tùy chỉnh UI editor bằng cách thêm
"format": "tabs"
,"hidden": true
, etc.
📌 Cách chuyển schema thành template Twig
Dữ liệu input:
{ "headline": "This is my headline", "body": { "layout": "2up", "logos": [ { "url": "http://example.com/logo1.png" }, { "file": "path/to/logo2.png" } ] } }
Twig template tương ứng:
<div class="logo-wall"> {% if headline %} <h1 class="logo-wall-headline">{{ headline }}</h1> {% endif %} <div class="logo-wall-logos" data-layout="{{ body.layout }}"> {% for logo in body.logos %} <img class="logo-wall-logo" src="{{ logo.file ?: logo.url }}" /> {% endfor %} </div> </div>
→ Mỗi giá trị đều được bảo vệ và kiểm tra đúng kiểu như đã mô tả trong schema.

📌 Tổng hợp lợi ích từ việc dùng Schema
Lợi ích | Mô tả |
---|---|
✅ Content Model chuẩn hóa | Biết trước mọi field, kiểu dữ liệu, required/optional |
✅ Data validation | Validate dữ liệu trước khi render |
✅ Template design dễ dàng | Template chỉ cần bám theo schema |
✅ UI editor auto-gen | Các input field tự động sinh dựa trên schema |
✅ Giảm guesswork khi handoff | Giao tiếp frontend-backend rõ ràng, không còn suy đoán |
✅ Hưởng lợi từ tool ecosystem | JSON Schema có rất nhiều tools hỗ trợ tự động hóa |
⚡ Yếu điểm của Schema-Driven Design System
📌 1. Quá phụ thuộc vào cấu trúc dữ liệu tĩnh
Vấn đề:
- Schema mô tả một cấu trúc dữ liệu tĩnh (JSON Schema là declarative, không procedural).
- Khi business logic phức tạp (ví dụ: “nếu user A thì hiện field X, nếu user B thì hiện field Y” hoặc “layout thay đổi theo nhiều điều kiện runtime”) → Schema khó lòng mô tả hết.
Hệ quả:
- Bắt buộc phải:
- Bẻ schema thành nhiều phiên bản.
- Hoặc build thêm logic ngoài (JS, backend rules…) → phá vỡ sự thuần nhất của hệ thống.
Kết luận:
Schema-driven rất mạnh khi UI/Data đơn giản hoặc trung bình. Khi workflow business nhiều conditional logic, nó không đủ và cần bổ sung code logic.
📌 2. Maintenance nặng nề nếu schema bị thay đổi thường xuyên
Vấn đề:
- Schema là gốc rễ: tất cả template, test, validation, UI… đều bám theo nó.
- Nếu thay đổi nhỏ trong schema (ví dụ đổi 1 field optional thành required), có thể kéo theo:
- Update lại template Twig/Angular.
- Update lại test data, visual regression test.
- Update lại form editor.
Hệ quả:
- Chi phí bảo trì cao nếu:
- Design System thay đổi liên tục.
- Yêu cầu dự án nhiều lần pivot hoặc thử nghiệm.
Kết luận:
Schema-driven rất mạnh cho hệ thống ổn định, nhưng với môi trường phát triển nhanh, hay thay đổi lớn thì dễ bị đuối.
📌 3. Độ phức tạp cao ban đầu
Vấn đề:
- Để xây dựng đúng:
- Cần skill tốt về JSON Schema, Template Engines, Data Validation, Frontend Architecture.
- Người mới vào team sẽ:
- Cần học 2-3 layers (Schema → Template → Validation) trước khi viết được một component đơn giản.
Hệ quả:
- Onboarding cost tăng: Người mới mất nhiều thời gian hiểu hệ thống.
- Team training phải bài bản, nếu không dễ làm sai (viết schema lệch, template không theo chuẩn…).
Kết luận:
Schema-driven là “hệ thống dành cho chuyên nghiệp” — team yếu hoặc thiếu quy trình sẽ không gánh nổi, và biến thành chaos.
📌 4. Khó tối ưu cho tương tác động real-time
Vấn đề:
- Schema describe static structure.
- Nhưng UI/UX hiện đại (như Google Docs, Figma, Editor.js…) cần tương tác dynamic:
- Thêm field mới ngay trên giao diện.
- Biến đổi cấu trúc form khi người dùng thao tác.
Hệ quả:
- Schema-driven khó xử lý:
- Các field động (dynamic form fields).
- Các mối quan hệ phức tạp nhiều cấp độ runtime.
Kết luận:
Nếu dự án đòi hỏi real-time dynamic form building, schema-driven không phải lựa chọn lý tưởng, hoặc phải cấu trúc lại rất nhiều.
📌 5. Overhead nếu dùng cho component quá nhỏ
Vấn đề:
- Không phải component nào cũng cần Schema.
- Ví dụ: Một Badge component chỉ render một dòng text đơn giản.
Hệ quả:
- Nếu ép tất cả components phải theo schema:
- Sinh dư thừa: 10 dòng code component → kèm thêm 100 dòng JSON schema → nặng nề, khó đọc.
- Maintenance mất thời gian cho những thứ không đáng.
Kết luận:
Nên áp dụng selective: chỉ những component thực sự có phức tạp data structure mới cần schema-driven.
🧪 9. Red Hat Testing: Visual Regression in Action
🛠️ Công cụ Testing chính
PhantomCSS = Kết hợp sức mạnh của 3 công cụ:
- PhantomJS: Trình duyệt headless WebKit để render trang và chụp ảnh.
- CasperJS: Công cụ điều khiển tương tác (click, hover, điền form…).
- ResembleJS: Công cụ so sánh hình ảnh pixel-by-pixel.
🌟 Tất cả được tích hợp vào Grunt để tự động hóa quy trình test.
⚙️ Cài đặt PhantomCSS với Grunt
- ❗ Cảnh báo: Gói
grunt-phantomcss
chính chủ trên npm đã cũ ➔ Nên dùng bản từ Anselm Hannemann.
🌟 Cài đặt:
npm i --save-dev git://github.com/anselmh/grunt-phantomcss.git
🌟 Gruntfile.js cấu hình:
grunt.loadNpmTasks('grunt-phantomcss'); phantomcss: { options: { mismatchTolerance: 0.05, // Ngưỡng khác biệt cho phép screenshots: 'baselines', // Folder ảnh baseline results: 'results', // Folder ảnh so sánh kết quả viewportSize: [1280, 800] // Kích thước cửa sổ trình duyệt }, src: ['phantomcss.js'] // File test chính }
🖼️ Viết file test: phantomcss.js
🌟 Cách hoạt động:
casper.start('http://localhost:9001/cta-link.html') .then(function() { phantomcss.screenshot('.cta-link', 'cta-link'); // Chụp trạng thái bình thường }) .then(function() { this.mouse.move('.cta-button'); phantomcss.screenshot('.cta-link', 'cta-link-hover'); // Chụp trạng thái hover });
- casper.start: Điều hướng tới URL.
- phantomcss.screenshot: Chụp ảnh selector chỉ định.
- this.mouse.move: Mô phỏng hover.
🧪 Quy trình test hoàn chỉnh với $ grunt phantomcss
- Spin up PhantomJS browser.
- Dùng CasperJS để:
- Điều hướng.
- Tương tác (hover, click, fill form).
- Chụp ảnh từng trạng thái.
- So sánh ảnh mới với ảnh baseline.
- Báo cáo:
- Nếu giống ➔ PASS ✅
- Nếu khác ➔ FAIL ❌
🔥 Xử lý khi test fail
- Nếu thay đổi dự kiến (ví dụ: đổi style button):
- Xóa ảnh baseline cũ.
- Commit baseline mới cùng với feature branch.
- Nếu thay đổi không dự kiến:
- Phải kiểm tra lại ➔ Có thể lỗi do thay đổi quá rộng/global.
- Hoặc cần xác nhận với Designer nếu muốn áp dụng thay đổi đó lên các component khác.
🌟 Kết quả:
- Mỗi component luôn có ảnh chuẩn “golden image” để kiểm tra.
- Các nhánh khác nhau đều có thể chạy lại bộ test mà vẫn đảm bảo chính xác.
🛠️ Tuỳ chỉnh thêm cho workflow tại Red Hat
🗂️ 1. Baseline nằm trong thư mục Component
- Thay vì gom baseline chung một chỗ ➔ Baseline ảnh được đặt cùng thư mục với code component.
- Mục đích:
- Dễ tìm.
- Dễ review khi merge request.
🌟 Cấu trúc ví dụ:

🧩 2. Chạy test từng Component riêng lẻ
- Thay vì chạy toàn bộ 100+ tests ➔ Chạy từng bộ test theo component.
- Ưu điểm:
- Nhanh hơn.
- Dễ khoanh vùng lỗi.
🌟 Hiển thị kết quả chi tiết pass/fail theo từng component.
📦 3. Làm cho tests portable và độc lập
- Vấn đề cũ:
casper.start()
bắt buộc chỉ ở file đầu ➔ Gây lỗi nếu thay đổi thứ tự file test.
- Giải pháp mới:
casper.start()
ngay khi Grunt task bắt đầu.- Các file test chỉ cần
casper.thenOpen(...)
.
🌟 Ví dụ:
// cta.tests.js casper.thenOpen('http://localhost:9001/cta.html') .then(function () { this.viewport(600, 1000); phantomcss.screenshot('.rh-cta-link', 'cta-link'); }) .then(function () { this.mouse.move(".rh-cta-link"); phantomcss.screenshot('.rh-cta-link', 'cta-link-hover'); }); // quote.tests.js casper.thenOpen('http://localhost:9001/quote') .then(function () { this.viewport(600, 1000); phantomcss.screenshot('.rh-quote', 'quote-600'); }) .then(function () { this.viewport(350, 1000); phantomcss.screenshot('.rh-quote', 'quote-350'); });
🏆 Kết quả đạt được
- Hệ thống component tại Red Hat:
- Dễ mở rộng.
- Dễ bảo trì.
- Giảm thiểu rủi ro khi thêm/tối ưu hóa tính năng.
- Bất kỳ bug nào xuất hiện ➔ Viết thêm test ➔ Ngăn không cho lặp lại.
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.