Bài viết “Styling cho Svelte Việt Nam” được chia làm ba phần, liệt kê sau đây. Bạn đang đọc phần cuối cùng.
Trong hai phần trước, ta xác định rằng TailwindCSS là một công cụ tuyệt vời để xây dựng hệ thống thiết kế, và rằng CSS Component là giải pháp tối giản để đóng gói các thành phần giao diện cơ bản. Trong phần này, ta sẽ tìm hiểu cách kết nối những lý thuyết đó trong một dự án thực tế.
Tailwind @layer
Trước hết, ta cần biết rằng TailwindCSS sắp xếp CSS vào ba layer (tầng hay lớp). Ta thường thấy ba layer này thông qua phần khai báo của Tailwind:
@tailwind base;
@tailwind components;
@tailwind utilities;
Theo đó:
base
là tầng thấp nhất, chứa những quy luật CSS với mục đích đặt lại một chuẩn mặc định cho các phần tử HTML (thường được gọi là “reset CSS”),components
là nơi ta sẽ thêm các quy luật tùy chỉnh, trong bài viết này chính là các CSS component,- cuối cùng
utilities
chứa các quy luật phổ biến của Tailwind như.bg-red-500
hay.text-center
.
Mặc dù cú pháp @tailwind
là đặc hữu từ Tailwind, CSS @layer là một tính năng hợp lệ của CSS - được chuẩn hóa từ năm 2022. Hãy chú ý trình tự khai báo của các layer này: quy luật CSS trong layer sau có thể ghi đè lên quy luật trong layer trước bất kể tính specificity. Có nghĩa là, ví dụ như ta có một CSS component .c-btn
trong layer components
:
@layer components {
.c-btn {
/* ... */
text-align: center;
}
}
Giả sử trong một tình huống đặc biệt nào đó, ta cần quy định cho c-btn
thuộc tính text-align
với giá trị là left
, ta hoàn toàn có thể sử dụng kèm lớp tương ứng trong layer utilities
:
<button class="c-btn text-left"></button>
Dù .c-btn
và .text-left
ngang hàng nhau ở tính specificity, .text-left
được khai báo ở layer sau nên sẽ được áp dụng.
Bạn có thể thấy rằng CSS Component ở trên được thêm tiền tố c-
. Đây là quy ước của dự án sveltevietnam.dev và không bắt buộc. Tuy nhiên, nó giúp ta dễ dàng nhận diện một CSS Component và cung cấp một số lợi ích cho trình soạn thảo mà ta sẽ bàn tới ở phần sau.
Khai báo CSS Component bằng @layer components
Trong phần trước, ta thấy một cách khai báo CSS component là sử dụng @layer components
:
/* app.css */
@layer components {
.c-btn {
text-align: center;
background-color: theme('colors.blue.500');
color: white;
/* ... */
}
}
Đây là giải pháp đơn giản nhất và nếu bạn mới sử dụng Tailwind thì nên bắt đầu từ cách này. Nhược điểm của giải pháp trên là nó không tương thích với TaiwindCSS language server và các plugin hỗ trợ Tailwind cho các trình soạn thảo. Khi rê chuột vào c-component
trong phần markup, plugin không thể nhận diện được c-btn
giống như các lớp Tailwind tiêu biểu khác.
Vì sao ta quan tâm đến việc một lớp có được nhận diện bởi TailwindCSS language server hay không? Để đảm bảo rằng mã nguồn có thể dễ dàng được “khám phá” một cách tự nhiên trong quá trình làm việc của lập trình viên. Ta chỉ cần gõ c-
và kích hoạt trình soạn thảo để nhận diện hoặc đề xuất các CSS component trong hệ thống thiết kế của dự án, tiết kiệm thời gian và giảm thiểu sai sót.
Khai báo CSS Component bằng Tailwind plugin
Tailwind có cung cấp API để viết plugin. Đây là giải pháp rất hiệu quả nếu ta cần linh hoạt trong cấu hình và tương thích với TailwindCSS language server. Vì API này sử dụng Javscript và cung cấp quyền truy cập đến PostCSS, ta có thể mở rộng hầu hết mọi khía cạnh của Tailwind mà không bị giới hạn bởi cú pháp CSS như giải pháp CSS thuần túy ở phần trước.
import definePlugin from 'tailwindcss/plugin';
const myplugin = definePlugin(function ({ addComponents }) {
addComponents({
'.c-btn': {
textAlign: 'center',
backgroundColor: `theme('colors.blue.500')`,
color: 'white',
},
});
});
/** @type {import("tailwindcss").Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts,md}', 'svelte.config.js'],
plugins: [myplugin],
};
Chú ý rằng mặc dù đoạn code tailwind.config.js
trên được chạy trong ngữ cảnh NodeJS, ta đang sử dụng cú pháp ESM. Thường thì cú pháp này đòi hỏi thiết lập package.json
với thuộc tính "type": "module"
:
{
"type": "module",
// ...
}
ESM đang dần được chuẩn hóa làm mặc định trong hệ sinh thái NodeJS. Trong các phần tiếp theo sau đây, ta sẽ tiếp tục sử dụng cú pháp này.
Ta còn có thể sử dụng plugin API để thêm vào các tầng base, utilities, thay đổi bảng màu và hệ thống đo lường, hoặc khai báo variant mới. Tất cả tùy chỉnh vừa nêu sẽ được TailwindCSS language server nhận diện. Tuy nhiên, có thể bạn đã nhận ra rằng phương thức này đòi hỏi ta phải sử dụng “CSS-in-JS”:
addComponents({
'.c-btn': {
textAlign: 'center',
backgroundColor: `theme('colors.blue.500')`,
color: 'white',
},
});
Đây là điểm mình hoàn toàn muốn tránh, vì nó trộn lẫn hai cú pháp với nhau và lấy mất đi tất cả các lợi ích của trình soạn thảo và công cụ hỗ trợ mã nguồn cho ngôn ngữ CSS (syntax highlighting, linting). Để giải quyết vấn đề này, ta cần thiết lập thêm một bước trung gian để chuyển đổi mã nguồn CSS sang CSS-in-JS tương ứng.
Chuyển đổi CSS sang CSS-in-JS
(1) Mã nguồn cho từng CSS component được đặt trong tệp CSS riêng biệt. Ví dụ với component c-btn
trên:
.c-btn {
/* ... */
}
(2) Sử dụng postcss và postcss-js để chuyển đổi các tệp CSS ở bước trước sang cấu trúc phù hợp trong Javscript:
import { readFileSync } from 'fs';
import postcss from 'postcss';
import postcssCustomSelectors from 'postcss-custom-selectors';
import postcssJs from 'postcss-js';
import postcssMixins from 'postcss-mixins';
// https://github.com/postcss/postcss-mixins
/** @type {Record<string, import('postcss-mixins').Mixin> } */
const mixins = {};
/**
* @param {string} filename
*/
function jssLoader(filename) {
const css = readFileSync(filename, 'utf8');
const root = postcss.parse(css);
// áp dụng các postcss plugin thích hợp, ví dụ mixins và custom selectors
// để tailwind intellisense thể hiện đúng CSS mong muốn
const jss = postcssJs.sync([postcssMixins({ mixins }), postcssCustomSelectors])(
postcssJs.objectify(root),
);
return jss;
}
(3) Áp dụng đầu ra của bước trước vào cấu hình plugin:
import definePlugin from 'tailwindcss/plugin';
import path from 'path';
import { fileURLToPath } from 'url';
import { jssLoader } from './jss-node-loader';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const myplugin = definePlugin(function ({ addComponents }) {
addComponents({
'.c-btn': {
textAlign: 'center',
backgroundColor: `theme('colors.blue.500')`,
color: 'white',
},
});
addComponents(jssLoader(path.resolve(__dirname, './c-btn.css')));
});
// ...
Tối ưu hóa bằng bước build riêng
Như vậy, ta đã có một giải pháp tương đối cân bằng, vừa đảm bảo TailwindCSS language server nhận diện được các CSS component, vừa có thể sử dụng được cú pháp CSS tiêu chuẩn. Giải pháp vừa nêu có thể đáp ứng được hầu hết nhu cầu phổ biến, đặc biệt là đối với các dự án quy mô nhỏ với số lượng component không đáng kể.
Tuy nhiên, khi số lượng component tăng lên, trải nghiệm của lập trình viên sẽ giảm dần cùng với tốc độ phản hồi của build tool (chẳng hạn như Vite) và TailwindCSS language server, do quá trình đọc tệp và xử lý của hàm jssLoader
là tương đối phức tạp và lặp lại thường xuyên (được kích hoạt bởi file watcher hoặc hot-module-replacement (HMR)). Để khắc phục điều này, ta có thể thực hiện các tác vụ jssLoader
trước, xuất thành tệp cố định, và chỉ cần sử dụng lại trong cấu hình plugin tại runtime. Nói cách khác, ta sẽ tạo ra một bước build riêng cho CSS component.
Có nhiều cách để thực hiện bước build này. Bạn có thể tham khảo mã nguồn của Daisy UI, hoặc từ chính dự án sveltevietnam.dev. Điểm chung của các giải pháp này là sử dụng hệ sinh thái PostCSS tương tự như ta đã thấy tại hàm jssLoader
đã giới thiệu ở phần trước. Sau đây là một ví dụ đơn giản cho build script:
import { writeFile } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { jssLoader } from './jss-node-loader';
const __dirname = dirname(fileURLToPath(import.meta.url));
const components = {
...jssLoader(resolve(__dirname, './c-btn.css')),
...jssLoader(resolve(__dirname, './c-input.css')),
// ...jssLoader(resolve(__dirname, './c-*.css')),
};
writeFile(
resolve(__dirname, './components.dist.json'),
JSON.stringify(components),
'utf-8',
(e) => {
if (e) console.error(e);
},
);
Ta cũng có thể thiết lập npm script để chạy bước build này:
{
// ...
"scripts": {
"build:css": "node ./build.js",
},
// ...
}
Và sử dụng kết quả build trong cấu hình plugin:
import definePlugin from 'tailwindcss/plugin';
import path from 'path';
import { fileURLToPath } from 'url';
import { jssLoader } from './jss-node-loader';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import components from './components.dist.json';
const myplugin = definePlugin(function ({ addComponents }) {
addComponents(jssLoader(path.resolve(__dirname, './c-btn.css')));
addComponents(components);
});
// ...
Như đã đề cập, bước build riêng giúp cái thiện tốc độ phản hồi trong quá trình phát triển. Tuy nhiên, ta không thể phủ nhận rằng nó cũng làm dự án thêm phần phức tạp và cồng kềnh. Bạn có thể bắt đầu mà không cần bước build, sau đó tái cấu trúc khi dự án đã đủ nhu cầu. Các đặc điểm sau đây có thể giúp bạn nhận biết khi nào nên thêm vào bước build riêng:
- dev server tốn hơn ba giây để phản hồi mỗi lần bạn thay đổi các thành phần liên quan đến cấu hình Tailwind,
- hệ thống thiết kế đã được phát triển tương đối đầy đủ, không cần quá nhiều thay đổi,
- hệ thống thiết kế cần được đóng gói để tái sử dụng cho nhiều dự án khác nhau.
Đóng gói và tái sử dụng
Đối với mình, lợi ích lớn nhất của giải pháp sử dụng plugin API từ Tailwind là tính độc lập với các logic khác của ứng dụng, giúp ta dễ dàng đóng gói hệ thống thiết kế để tái sử dụng cho nhiều dự án sử dụng chung thiết kế hoặc thành phần giao diện.
Vì hệ thống thiết kế này chỉ bao gồm các lớp abstraction cơ bản, xây dựng trên CSS và Javascript, ta có thể sử dụng trong nhiều ngữ cảnh khác nhau, với bất cứ framework nào có hỗ trợ PostCSS và TailwindCSS. Trong các ứng dụng nâng cao, ta cũng có thể tích hợp plugin Tailwind trên vào các thư viện giao diện chứa Javascript component, tùy theo nhu cầu và framework mà bạn đang sử dụng.
import { Header, Footer, ArticleCard } from '@company/design-system/svelte';
import { Playground } from '@company/design-system/react';
// ...
Chi tiết để đóng gói mã nguồn trên khá dài dòng và nằm ngoài phạm vi của bài viết này. Nếu bạn đã từng làm việc với các thư viện nội bộ trong một monorepo hoặc xuất bản một thư viện tại npm thì cách thực hiện ở đây cũng tương tự. Bạn cũng có thể tham khảo mã nguồn của @sveltevietnam/ui - chính là hệ thống thiết kế của sveltevietnam.dev, phát triển dựa trên các lý thuyết đã trình bày.
Tổng kết
Qua ba phần của bài viết “Styling cho Svelte Việt Nam”, mình đã giới thiệu sơ bộ về nguyên nhân và cách thức dự án sveltevietnam.dev sử dụng TailwindCSS, kết hợp với ý tưởng “CSS component” để xây dựng một hệ thống thiết kế tối giản, đảm bảo tính đóng gói và tái sử dụng, giúp mã nguồn dễ dàng làm quen và khám phá đối cho lập trình viên nhờ sự tương thích với các công cụ hỗ trợ của trình soạn thảo.
Còn rất nhiều khía cạnh của các công cụ trong hệ sinh thái CSS mà ta có thể mở rộng và khai thác. Hy vọng bài viết này không chỉ giúp bạn hiểu thêm về sveltevietnam.dev mà còn cung cấp nguồn tham khảo và ý tưởng cho những dự án của riêng bạn. Hệ thống thiết kế của sveltevietnam.dev không phải là hoàn hảo và luôn được cải tiến mỗi ngày; những chi tiết thực hiện được giới thiệu trong bài viết có thể sẽ trở nên lỗi thời với tốc độ phát triển của công nghệ ngày nay, nhưng mình tin là những lý thuyết cốt lỗi vẫn luôn có giá trị.
Để thảo luận thêm về chủ đề này, bạn có thể tham gia Discord của Svelte Việt Nam. Cảm ơn bạn đã theo dõi!
Bạn tìm thấy lỗi chính tả hay cần đính chính nội dung? Sửa trang này tại Github