Cách RedHat.com xây dựng hệ thống với thiết kế “super” clean cho Front End

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:

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

  1. Layout không áp padding hoặc style cho component con.
  2. Theme không tự thay đổi appearance – chỉ cung cấp context.
  3. Component luôn chạm 4 cạnh của container.
  4. Component KHÔNG có background, float, padding/margin bên ngoài.
  5. Mỗi element có 1 class duy nhất, chỉ dùng trong 1 component.
  6. Không dùng margin-top – element đầu tiên phải dính sát trên.
  7. 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 semanticmodular.
  • 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 PrincipleMỗi class/component chỉ dùng cho 1 mục đích duy nhất
Single Source of TruthStyle chỉ định nghĩa tại 1 nơi (trong Sass component của chính nó)
Opt-in Modifier & ContextTất cả hiệu ứng style phải “chủ động” nhận, không bị áp ngầm
Road Runner RulesBộ 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 AttributesLayout được định nghĩa rõ ràng qua data-*, không cần class phụ
No Top MarginsGiúp các component xếp hàng chuẩn xác khi đặt cạnh nhau
No Global JS BindingJS 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 schemaMô tả biến, kiểu dữ liệu, required/optional
Template file (Twig)Nhận data từ schema → render HTML
Sass partialChứa style của component
Visual regression testsTest UI với nhiều width, nhiều trạng thái
Testing dataData phục vụ test edge cases
Documentation (Markdown)Giải thích cách dùng component
Documentation dataData mẫu để hiển thị trên doc page

JSON schematrá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
typeKiểu dữ liệu tổng thể (object, array, string…)
propertiesCác trường dữ liệu con (field/key)
requiredTrường nào bắt buộc phải có
oneOf / anyOf / allOfRẽ nhánh schema theo biến thể
formatGợi ý UI element (text input, textarea, file upload…)
$refTái sử dụng schema con hoặc định nghĩa chung
definitionsCác schema phụ được định nghĩa dùng lại

💬 Tác động thực tiễn của Schema

  1. 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)
  2. 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”.
  3. Backend Devs:
    • Chỉ cần cung cấp đúng dữ liệu theo schema đã định → auto generate đúng HTML qua template.
  4. Automation & QA:
    • Schema dùng để:
      • Validate dữ liệu trước khi render.
      • Sinh các test cases để kiểm thử input/output.

📌 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 íchMô tả
✅ Content Model chuẩn hóaBiết trước mọi field, kiểu dữ liệu, required/optional
✅ Data validationValidate dữ liệu trước khi render
✅ Template design dễ dàngTemplate chỉ cần bám theo schema
✅ UI editor auto-genCác input field tự động sinh dựa trên schema
✅ Giảm guesswork khi handoffGiao tiếp frontend-backend rõ ràng, không còn suy đoán
✅ Hưởng lợi từ tool ecosystemJSON 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 trangchụ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

  1. Spin up PhantomJS browser.
  2. Dùng CasperJS để:
    • Điều hướng.
    • Tương tác (hover, click, fill form).
    • Chụp ảnh từng trạng thái.
  3. So sánh ảnh mới với ảnh baseline.
  4. 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độ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