Thiết kế xác thực số ký tự nhập liệu biểu mẫu - Triển khai giới hạn mà không ảnh hưởng UX
Xác thực số ký tự biểu mẫu là thách thức thiết kế cần cân bằng giữa duy trì tính toàn vẹn dữ liệu và bảo toàn trải nghiệm người dùng. Chỉ đặt thuộc tính maxlength là chưa đủ - cần xem xét bộ đếm thời gian thực, xử lý cặp thay thế, xác thực phía máy chủ và phản hồi phù hợp khi xảy ra lỗi. Bài viết này trình bày các mẫu thiết kế xác thực thực tế, bao gồm sự phù hợp với độ dài VARCHAR cơ sở dữ liệu. Hãy nắm vững khái niệm cơ bản về giới hạn ký tự trước khi đọc tiếp.
Cạm bẫy của thuộc tính maxlength
Thuộc tính maxlength của HTML là cách đơn giản nhất để giới hạn số ký tự. Tuy nhiên, nó có một số vấn đề mà nhà phát triển thường bỏ qua.
Vấn đề lớn nhất là maxlength giới hạn theo đơn vị mã UTF-16. Ký tự trong Mặt phẳng đa ngôn ngữ cơ bản (BMP) dùng 1 đơn vị mã, nhưng emoji và ký tự cặp thay thế tiêu thụ 2 đơn vị mã. Nghĩa là trong trường có maxlength="10", người dùng chỉ có thể nhập 5 emoji về mặt trực quan.
| Loại ký tự | Đơn vị mã UTF-16 | Tiêu thụ maxlength | Ví dụ |
|---|---|---|---|
| Ký tự ASCII | 1 | 1 | A, 1, @ |
| Tiếng Nhật (BMP) | 1 | 1 | あ, 漢, カ |
| Emoji cơ bản | 2 | 2 | 😀, 🎉, ❤️ |
| Emoji chuỗi ZWJ | 7-11 | 7-11 | 👨👩👧👦, 🏳️🌈 |
| Emoji cờ quốc gia | 4 | 4 | 🇯🇵, 🇺🇸 |
| CJK Thống nhất Hán tự Mở rộng B | 2 | 2 | 𠮷 (tsuchiyoshi) |
Vấn đề này đặc biệt nổi bật ở trường nhập tên. Một số chữ Hán đăng ký trong hộ tịch Nhật Bản thuộc CJK Thống nhất Hán tự Mở rộng B và tiêu thụ 2 đơn vị mã dưới maxlength. Họ "𠮷田" trông như 2 ký tự nhưng tiêu thụ 3 đơn vị mã. Điều này liên quan chặt chẽ đến vấn đề đếm ký tự toàn chiều rộng/nửa chiều rộng.
Vấn đề khác là maxlength từ chối nhập liệu một cách im lặng. Khi đạt giới hạn, không thể nhập thêm ký tự nhưng không có phản hồi giải thích lý do. Người dùng có thể nghi ngờ bàn phím hỏng hoặc lỗi trình duyệt.
Đếm ký tự chính xác bằng JavaScript
Để tránh vấn đề UTF-16 của maxlength, cần triển khai đếm theo cụm tự vị (Grapheme Cluster) trong JavaScript. Cụm tự vị là đơn vị mà con người nhận thức là "một ký tự", đếm chính xác cặp thay thế, ký tự kết hợp và chuỗi ZWJ là một ký tự đơn.
Phương pháp đáng tin cậy nhất là sử dụng API Intl.Segmenter.
// Đếm cụm tự vị bằng Intl.Segmenter
function countGraphemes(text) {
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
return [...segmenter.segment(text)].length;
}
// Ví dụ sử dụng
countGraphemes('Hello'); // 5
countGraphemes('こんにちは'); // 5
countGraphemes('👨👩👧👦'); // 1
countGraphemes('🇯🇵'); // 1
countGraphemes('𠮷田太郎'); // 4
Intl.Segmenter được hỗ trợ trên các trình duyệt chính tính đến 2024 (Chrome 87+, Firefox 125+, Safari 15.4+). Để hỗ trợ trình duyệt cũ, thư viện grapheme-splitter có thể dùng làm phương án dự phòng.
Tuy nhiên, không phải mọi biểu mẫu đều cần đếm cụm tự vị. Các trường chỉ chấp nhận ký tự ASCII như địa chỉ email hoặc URL hoạt động tốt với maxlength. Đếm cụm tự vị chỉ cần cho các trường người dùng nhập văn bản tự do (tên, bình luận, giới thiệu bản thân, v.v.).
Thiết kế bộ đếm ký tự thời gian thực
Bộ đếm ký tự thời gian thực là thành phần UI phản hồi trực quan số ký tự còn lại cho người dùng. Chỉ báo tiến trình hình tròn được sử dụng trong giới hạn ký tự của X (trước đây là Twitter) được công nhận rộng rãi là thực hành tốt nhất trong lĩnh vực này.
| Yếu tố thiết kế | Mẫu khuyến nghị | Mẫu nên tránh | Lý do |
|---|---|---|---|
| Định dạng hiển thị | "Còn 42 ký tự" hoặc "158/200" | Chỉ "Đã nhập 158 ký tự" | Số còn lại thúc đẩy hành động người dùng tốt hơn |
| Vị trí | Góc dưới bên phải trường nhập | Phía trên trường hoặc xa | Giảm thiểu di chuyển mắt |
| Thay đổi màu | Vàng khi còn 20%, đỏ khi hết | Luôn cùng màu | Truyền tải mức độ khẩn cấp bằng thị giác |
| Hành vi vượt giới hạn | Tô đỏ phần vượt + hiển thị số âm | Chặn nhập im lặng | Cho người dùng không gian chỉnh sửa |
| Khả năng tiếp cận | Thông báo số còn lại bằng aria-live="polite" | Chỉ hiển thị trực quan | Cung cấp thông tin cho người dùng trình đọc màn hình |
Thiết kế của X xuất sắc vì chỉ báo hình tròn đổi màu khi tiến gần giới hạn, và khi vượt quá, số âm hiển thị bằng màu đỏ. Thay vì chặn nhập, nó cho phép tiếp tục chỉnh sửa và vô hiệu hóa nút đăng, cho người dùng thời gian quyết định cắt bỏ phần nào.
Sự cần thiết của xác thực phía máy chủ
Xác thực phía máy khách tồn tại vì mục đích UX và không đảm bảo bảo mật hay tính toàn vẹn dữ liệu. Cả thuộc tính maxlength và bộ đếm JavaScript đều có thể dễ dàng vô hiệu hóa qua công cụ phát triển trình duyệt. Gọi API trực tiếp hoàn toàn bỏ qua xác thực frontend.
Xác thực số ký tự phía máy chủ, giống như bảo mật độ dài mật khẩu, là tuyến phòng thủ cuối cùng. Hãy đảm bảo triển khai các điểm sau:
- Xác minh số byte: Vì VARCHAR cơ sở dữ liệu giới hạn theo byte hoặc ký tự, cũng cần xác minh số byte sau khi mã hóa UTF-8 ở phía máy chủ
- Chuẩn hóa: Áp dụng chuẩn hóa Unicode NFC trước khi đếm. Cùng ký tự trực quan có thể có số ký tự khác nhau tùy thuộc vào dùng ký tự tiền tổ hợp hay chuỗi ký tự kết hợp
- Loại bỏ ký tự điều khiển: Loại bỏ ký tự NULL, backspace và các ký tự điều khiển khác trước khi đếm
- Cắt bỏ: Loại bỏ khoảng trắng đầu và cuối trước khi đếm. Không để khoảng trắng chiếm số ký tự
# Ví dụ xác thực phía máy chủ bằng Python
import unicodedata
def validate_text_length(text, max_chars=200, max_bytes=800):
# Loại bỏ ký tự điều khiển
cleaned = ''.join(c for c in text if unicodedata.category(c) != 'Cc')
# Chuẩn hóa NFC
normalized = unicodedata.normalize('NFC', cleaned.strip())
# Kiểm tra số ký tự
char_count = len(normalized)
# Kiểm tra số byte UTF-8
byte_count = len(normalized.encode('utf-8'))
if char_count > max_chars:
return False, f'Số ký tự vượt giới hạn ({char_count}/{max_chars})'
if byte_count > max_bytes:
return False, f'Kích thước dữ liệu vượt giới hạn'
return True, None
Mẫu triển khai theo framework
| Framework | Thư viện xác thực | Triển khai giới hạn ký tự | Tích hợp bộ đếm thời gian thực |
|---|---|---|---|
| React | React Hook Form + Zod | Định nghĩa schema bằng .max() của Zod | Theo dõi giá trị nhập bằng watch() và hiển thị đếm |
| Vue | VeeValidate + Yup | Định nghĩa schema bằng .max() của Yup | Đếm từ giá trị phản ứng v-model |
| Angular | Reactive Forms | Validators.maxLength() | Đếm qua Observable valueChanges |
| Svelte | Superforms + Zod | Định nghĩa schema bằng .max() của Zod | Đếm bằng khai báo phản ứng $: |
Thiết kế thông báo lỗi cho giới hạn ký tự
| Mẫu | Ví dụ thông báo | Đánh giá |
|---|---|---|
| Chỉ nêu vấn đề | "Vượt giới hạn ký tự" | Trung bình - không rõ vượt bao nhiêu |
| Vấn đề + trạng thái | "523/500 ký tự - vượt 23 ký tự" | Tốt - số vượt rõ ràng |
| Vấn đề + giải pháp | "Vượt 23 ký tự. Vui lòng xóa phần không cần thiết" | Tốt - hành động tiếp theo rõ ràng |
| Cảnh báo tiến triển | Vàng khi còn 50, đỏ khi vượt + thông báo | Xuất sắc - cảnh báo trước và hướng dẫn cụ thể sau khi vượt |
Phương pháp cảnh báo tiến triển hiệu quả nhất. Cung cấp phản hồi trực quan khi tiến gần giới hạn và trình bày số vượt cụ thể cùng giải pháp khi vượt quá.
Bạn cũng có thể tìm sách thiết kế UX trên Amazon để tham khảo thiết kế biểu mẫu.
Textarea tự động thay đổi kích thước và giới hạn ký tự
Textarea tự động thay đổi kích thước mở rộng chiều cao tự động theo nội dung nhập. Khi kết hợp với giới hạn ký tự, cần một số quyết định thiết kế. Không đặt chiều cao tối đa có thể phá vỡ bố cục trang. Với trường giới hạn 500 ký tự, đặt tối đa khoảng 400px là thực tế. CSS field-sizing: content được triển khai trong Chrome 123 năm 2024 cho phép tự động thay đổi kích thước không cần JavaScript, nhưng Firefox và Safari chưa hỗ trợ.
Giới hạn ký tự biểu mẫu di động - Thách thức riêng
- Văn bản đang soạn IME: Trong quá trình chuyển đổi nhập tiếng Nhật, sự kiện
inputkích hoạt nhưng chứa văn bản chưa xác nhận. Theo dõi sự kiệncompositionstart/compositionendvà tạm dừng xác thực trong quá trình soạn - Ảnh hưởng của nhập dự đoán: Chọn từ dự đoán trên iOS hoặc Android chèn nhiều ký tự cùng lúc, có thể vượt
maxlength - Hạn chế kích thước màn hình: Không gian hiển thị bộ đếm hạn chế trên di động - dùng hiển thị ngắn gọn "Còn 42" trên di động
- Hiển thị bàn phím mềm: Bàn phím mềm giảm vùng hiệu dụng khoảng một nửa. Đặt bộ đếm trong hoặc ngay dưới trường
// Kiểm soát xác thực trong quá trình soạn IME
let isComposing = false;
textarea.addEventListener('compositionstart', () => {
isComposing = true;
});
textarea.addEventListener('compositionend', () => {
isComposing = false;
validateLength(textarea.value);
});
textarea.addEventListener('input', () => {
if (!isComposing) {
validateLength(textarea.value);
}
updateCounter(textarea.value);
});
Thiết kế phù hợp với cơ sở dữ liệu
Sự không phù hợp giữa giới hạn ký tự frontend và định nghĩa cột cơ sở dữ liệu gây ra cắt ngắn dữ liệu hoặc lỗi trong môi trường sản xuất. Như trình bày chi tiết trong thiết kế độ dài VARCHAR cơ sở dữ liệu, VARCHAR(255) của MySQL dựa trên ký tự, nhưng tiêu thụ lưu trữ thực tế phụ thuộc vào mã hóa.
| Cơ sở dữ liệu | Đơn vị VARCHAR | 100 ký tự tiếng Nhật | Phù hợp giới hạn frontend |
|---|---|---|---|
| MySQL (utf8mb4) | Ký tự | Vừa VARCHAR(100) | Dễ khớp với giới hạn ký tự frontend |
| PostgreSQL | Ký tự | Vừa VARCHAR(100) | Dễ khớp với giới hạn ký tự frontend |
| SQL Server | Ký tự (NVARCHAR) | Vừa NVARCHAR(100) | Dễ khớp với giới hạn ký tự frontend |
| Oracle | Byte (mặc định) | Cần VARCHAR2(300) | Cần chuyển đổi byte |
| DynamoDB | Kích thước mục (400 KB) | Không giới hạn theo thuộc tính | Đặt giới hạn ở tầng ứng dụng |
Thực hành thiết kế an toàn là đặt giới hạn ký tự frontend nghiêm ngặt hơn định nghĩa cột cơ sở dữ liệu. Ví dụ, nếu cơ sở dữ liệu là VARCHAR(500), đặt giới hạn frontend khoảng 450 ký tự, tạo bộ đệm cho thay đổi số ký tự từ chuẩn hóa Unicode và cắt bỏ.