Chuyển đến nội dung
MINH VO Ghi chép từ công việc
của một kỹ sư tại Việt Nam
Cơ bản11 phút đọc

Nâng cấp dần một biểu mẫu tìm kiếm

Làm tìm kiếm GET có thể chia sẻ trước khi thêm kết quả bất đồng bộ, với quy tắc lọc nhất quán, giá trị biểu mẫu được giữ lại và lịch sử điều hướng đúng.

Các bố cục trình duyệt bằng giấy màu ô liu và be mô tả trang máy tính cùng trang di động hẹp hơn

Biểu mẫu tìm kiếm có thể hoạt động bằng yêu cầu GET thông thường trước khi chạy bất kỳ JavaScript nào. Truy vấn nằm trong URL, máy chủ trả nội dung phù hợp và trình duyệt xử lý điều hướng. Sau đó JavaScript có thể cập nhật kết quả ngay trên trang, trong khi biểu mẫu gốc vẫn là đường dự phòng và giao diện có thể chia sẻ.

Nếu đã biết biểu mẫu cơ bản và JavaScript, trang lưu trữ bài viết là nơi phù hợp để áp dụng cách nâng cấp dần này. Người đọc có thể đến từ liên kết, tắt script, dùng kết nối chậm hoặc mở trang đã lọc trong thẻ mới. Kết quả không nên phụ thuộc vào việc họ đã làm đúng một chuỗi thao tác client trước đó.

Để URL biểu đạt yêu cầu

<form action="/vi/blog/" method="get" role="search">
  <label for="journal-query">Tìm bài viết</label>
  <input id="journal-query" name="q" type="search">
  <label for="journal-category">Danh mục</label>
  <select id="journal-category" name="category">
    <option value="">Tất cả danh mục</option>
    <option value="frontend">Frontend</option>
    <option value="ai">AI</option>
  </select>
  <button type="submit">Tìm kiếm</button>
</form>

Tìm “focus styles” trong danh mục frontend tạo tham số tương ứng với các điều khiển được gửi. Trình duyệt xử lý cách mã hóa theo thuật toán gửi biểu mẫu. Hướng dẫn gửi dữ liệu của MDN giải thích khác biệt giữa tham số GET và phần thân yêu cầu POST.

GET phù hợp để lấy kết quả tìm kiếm mà không đổi trạng thái máy chủ. Đừng dùng cùng mẫu cho hành động phá hủy dữ liệu chỉ vì tạo liên kết dễ. Truy vấn có thể xuất hiện trong lịch sử trình duyệt, log và URL được sao chép, nên kho bài viết công khai không nên yêu cầu bí mật trong ô tìm kiếm.

Máy chủ đọc truy vấn, kiểm tra bộ lọc được hỗ trợ và trả lại cả giá trị biểu mẫu đã chọn lẫn kết quả. Trong phản hồi mới, giữ ô nhập đã điền cho người đọc biết trang hiện đại diện yêu cầu nào. Một biểu mẫu trống nằm trên kết quả đã lọc làm quan hệ ấy không rõ.

Kiểm tra URL trước khi chặn thao tác gửi

Với biểu mẫu trên trang, nhập truy vấn và chọn danh mục. Chạy đoạn sau trong console. Nó chỉ tạo URL, không gửi yêu cầu, nên bạn có thể so với địa chỉ tạo ra khi gửi biểu mẫu bình thường.

const form = document.querySelector('form[role="search"]');
const destination = new URL(form.action);
destination.search = new URLSearchParams(new FormData(form)).toString();
console.log(destination.href);

Bộ chuyển đổi nhỏ này giới hạn ở biểu mẫu đã in: điều khiển có giá trị chuỗi và nút gửi không có name. Hàm tạo FormData thu các điều khiển có tên đủ điều kiện gửi. Điều khiển disabled bị bỏ qua; danh mục đang bật với giá trị rỗng vẫn tạo category=. Nếu sau này thêm nút gửi có tên, phần nâng cấp cần tính đến đúng nút đã kích hoạt. Trường tệp cần thiết kế yêu cầu khác, không vô tình chuyển đối tượng File thành chuỗi truy vấn.

Thử truy vấn chứa &, dấu cộng và dấu cách. URLSearchParams thực hiện mã hóa; nối thẳng dữ liệu nhập vào ?q= sẽ nhầm văn bản người dùng với dấu phân cách tham số. Giải mã URL được gửi bằng cùng API và kiểm tra giá trị trở lại đúng. Mã hóa giữ cấu trúc URL, không làm HTML phản chiếu trở nên an toàn.

Phép gán thay toàn bộ query string của action bằng điều khiển hiện tại. Vì vậy, page=3 cũ không đi theo tìm kiếm mới trừ khi chính nó là điều khiển được gửi. Giữ liên kết phân trang tách khỏi trường tìm kiếm nếu truy vấn mới phải về trang một. Đừng vô tình phục hồi số trang cũ bằng cách gộp mọi tham số từ địa chỉ hiện tại.

Tệp tĩnh không tự lọc kết quả ở máy chủ

Hosting tĩnh có thể trả cùng một tệp HTML cho nhiều query string. Nếu không có lớp xử lý tại thời điểm yêu cầu để đọc tham số, trình duyệt vẫn nhận cùng kho bài bất kể truy vấn. Biểu mẫu GET mới là nửa triển khai; route phải thực sự thực hiện điều đã hứa.

Trang Astro có thể dùng route render theo yêu cầu, Worker hoặc lớp máy chủ khác. Lựa chọn kiến trúc phụ thuộc nơi triển khai. Điều người đọc cần là mở trực tiếp URL sẽ nhận đúng nội dung, không phải đợi script chạy rồi giấu các thẻ bài không liên quan.

Lọc phía máy chủ cũng cần đi cùng điều kiện xuất bản. Nếu có bài chưa tới giờ phát hành, tìm kiếm phải dùng tập đủ điều kiện tại thời điểm yêu cầu. Gửi metadata của bài tương lai xuống client rồi giấu bằng giao diện không tương đương với giữ bài chưa công bố.

Biểu mẫu GET tạo URL và nhận kết quả từ máy chủ; JavaScript tùy chọn gửi cùng yêu cầu rồi thay vùng kết quảMở ảnh kích thước đầy đủ ↗

Hai đường trong hình dùng cùng quy tắc lọc. Giữ hai bản triển khai độc lập ở trình duyệt và máy chủ dễ tạo khác biệt về cách đọc truy vấn và điều kiện xuất bản.

Văn bản truy vấn cũng là dữ liệu hiển thị không đáng tin cậy. Tiêu đề kết quả lặp lại truy vấn cần được escape đúng hoặc chèn như văn bản, dù biểu mẫu thuộc chính trang của bạn. URL được sao chép có thể mang giá trị chưa từng đi qua ô nhập nhìn thấy.

Chọn một cách hiểu rõ ràng cho bộ lọc

Hàm JavaScript thuần sau nhận danh mục bài đã đủ điều kiện. Nó bỏ khoảng trắng ở hai đầu truy vấn, xem danh mục không hỗ trợ là “tất cả” và chỉ tìm chuỗi con trong tiêu đề sau khi chuyển thành chữ thường. Ba bản ghi là dữ liệu giả định để thử, không phải số đo kho bài đã triển khai. Các tiêu đề mẫu giữ nguyên tiếng Anh để truy vấn focus và mã bản ghi khớp giữa hai bản bài viết.

function selectPosts(requestUrl, eligiblePosts) {
  const query = (requestUrl.searchParams.get('q') ?? '').trim();
  const requested = requestUrl.searchParams.get('category') ?? '';
  const category = ['frontend', 'ai'].includes(requested) ? requested : '';
  const needle = query.toLowerCase();
  const matches = eligiblePosts.filter(post =>
    (!category || post.category === category) &&
    post.title.toLowerCase().includes(needle)
  );
  return { query, category, matches };
}

const demo = [
  { id: 'focus', title: 'Focus styles for links', category: 'frontend' },
  { id: 'react', title: 'React state and identity', category: 'frontend' },
  { id: 'eval', title: 'Evaluating a text model', category: 'ai' }
];
const state = selectPosts(
  new URL('https://example.test/blog/?q=focus&category=frontend'), demo
);
console.log(state.matches.map(post => post.id)); // ['focus']

Route HTTP vẫn phải gọi hàm, escape truy vấn trả về khi render HTML, giữ danh mục được chọn và render các bài khớp. Hàm không khởi động máy chủ hay quyết định bài nào được xuất bản. Phải cung cấp bản ghi đủ điều kiện trước bước lọc; tên tham số không phải cài đặt kiểm soát truy cập.

Quy tắc chuẩn hóa là lựa chọn có chủ đích. Ứng dụng khác có thể từ chối danh mục lạ, nhưng không nên hiểu một kiểu khi điều hướng thường và kiểu khác khi lấy kết quả bất đồng bộ. Nếu q lặp lại nhiều lần, get() lấy giá trị đầu tiên. Ghi trường hợp ấy vào hợp đồng dùng chung thay vì để mỗi parser chọn một tìm kiếm khác.

Chuyển chữ thường cũng là quy tắc có giới hạn. Nó không rút gọn từ về gốc, bỏ dấu, phân tích từ theo ngôn ngữ hoặc xếp hạng. Truy vấn rỗng khớp mọi tiêu đề được cung cấp; danh mục vẫn có thể thu hẹp tập. Kiểm tra những điều đó trên dữ liệu nhỏ trước khi thay bằng dịch vụ tìm kiếm mạnh hơn. Đổi thuật toán nên là thay đổi hành vi có chủ ý, không phải tác dụng phụ của thêm thư viện client.

Thêm kết quả bất đồng bộ quanh biểu mẫu hiện có

Phần nâng cấp có thể chặn gửi, dựng URL từ điều khiển và yêu cầu một dạng biểu diễn kết quả. Nó nên giữ đường điều hướng thông thường khi không hoàn tất được. Máy chủ có thể trả fragment theo hợp đồng hoặc JSON chứa dữ liệu để render an toàn; client không nên đoán cần thực thi phần nào của HTML tùy ý.

Theo dõi yêu cầu đang chạy để phản hồi cũ không thay kết quả mới. Nếu người đọc gửi “React” rồi “React state”, phản hồi đầu có thể đến sau. Hủy công việc bị thay thế khi có thể và vẫn kiểm tra mã yêu cầu trước khi áp kết quả.

Giữ tiêu đề và số kết quả có nghĩa. Thông báo trạng thái polite có thể báo hoàn tất mà không chuyển focus khỏi ô tìm kiếm. Đừng tự focus bài đầu sau mỗi lần gõ; điều đó ngắt việc sửa truy vấn và làm tìm kiếm lặp khó hơn.

Quyết định thời điểm gửi yêu cầu. Enter hoặc nút tìm kiếm có hành vi dễ đoán. Tìm ngay khi gõ có thể hữu ích cho kho nhỏ nhưng thêm vấn đề tần suất, hủy yêu cầu và thông báo. Debounce giảm lưu lượng đồng thời đổi lúc kết quả xuất hiện; nó không loại yêu cầu xử lý đúng thứ tự.

Lịch sử là một phần của giao diện

Nếu tìm kiếm bất đồng bộ đổi kết quả đang hiển thị, hãy cập nhật URL có chủ đích. Truy vấn mới được gửi có thể tạo một mục lịch sử để Back trở lại truy vấn trước. Những sửa nhỏ chưa gửi không cần tạo hàng chục mục.

Sau đó làm cả chiều ngược: khi Back hoặc Forward đổi URL, khôi phục giá trị biểu mẫu và kết quả tương ứng. Chỉ cập nhật lịch sử mà không xử lý việc quay lại tạo trang có địa chỉ và nội dung mâu thuẫn. Tài liệu popstate giải thích rằng gọi pushState không tự phát popstate. Render kết quả mới thành công khi áp nó, rồi xử lý riêng việc đi qua lịch sử về sau.

Khi đi qua lịch sử, trình duyệt còn có thể khôi phục giá trị điều khiển đã sửa ở tài liệu trước. Vì vậy, URL và tiêu đề kết quả có thể mô tả truy vấn đã gửi, trong khi ô nhập chứa bản nháp được sửa ngay trước lúc rời trang. Đừng mặc định Back luôn khởi tạo lại ô nhập từ HTML máy chủ. Với phần nâng cấp, hãy chọn khôi phục bộ lọc đã áp dụng hay giữ bản nhập dở, đồng thời làm rõ truy vấn nào đang tạo kết quả. Chính sách minh họa dưới đây khôi phục bộ lọc đã áp dụng, không hứa mọi trình duyệt tự phục hồi trạng thái biểu mẫu theo cách đó.

Liên kết phân trang phải giữ bộ lọc hiện tại. Sang trang hai mà mất danh mục là lỗi dễ gặp khi chỉ dựng liên kết từ số trang. Đặt lại trang khi bắt đầu tìm kiếm mới vì trang cũ có thể không tồn tại trong tập kết quả mới.

Phần nâng cấp minh họa bỏ số trang cũ khi dựng tìm kiếm mới và khôi phục bộ lọc đã áp dụng cùng kết quả khi Back kích hoạt URL trướcMở ảnh kích thước đầy đủ ↗

Hàm chọn bài ở trên chưa phân trang; hình bổ sung hợp đồng mà một route có phân trang cần thực hiện. Nó cho thấy URL và trạng thái render cần quan hệ hai chiều. Đặt địa chỉ mới chỉ hoàn thành một nửa. Khi phần nâng cấp thất bại, giữ trang cũ còn nhất quán hoặc điều hướng tới URL yêu cầu, thay vì hiện kết quả một truy vấn dưới địa chỉ của truy vấn khác.

Kiểm tra đường dự phòng trước phần nâng cấp

Tắt JavaScript, gửi truy vấn, tải lại URL kết quả và mở nó trong thẻ mới. Xác nhận giá trị đã chọn còn nhìn thấy và trường hợp không có kết quả chỉ ra bước thử tiếp hữu ích. Sau đó bật phần nâng cấp và lặp hành trình với phản hồi bị trì hoãn hoặc thất bại.

Thử Back sau hai tìm kiếm khác nhau. Kiểm tra focus bàn phím và thông báo khi tải. Đường nâng cấp xứng đáng với độ phức tạp khi giảm chờ hoặc giữ ngữ cảnh hữu ích, đồng thời để hành trình URL thông thường tiếp tục đáng tin cậy.

Nguồn tham khảo & đọc thêm

  1. MDN: sending and retrieving form data
  2. MDN: FormData constructor
  3. MDN: URLSearchParams
  4. MDN: URLSearchParams.get
  5. MDN: popstate event
← Về trang bài viết

Hình minh họa

100%