
Tôi đang xây dựng một tiện ích mở rộng trình duyệt cho ChatGPT và gặp phải một trở ngại. Tôi cần cuộn đến một tin nhắn cụ thể trong cuộc trò chuyện, vì vậy tôi đã làm điều mà bất kỳ nhà phát triển nào cũng sẽ làm:
window.scrollY
Luôn luôn là số không.
Tôi đã thử công thức kinh điển:
element.getBoundingClientRect().top + window.scrollY
Vẫn không hoạt động. Tọa độ của tôi bị sai, và tiện ích mở rộng của tôi trở nên vô dụng.
Các SPA hiện đại như ChatGPT không cuộn trang. <body> có chiều cao cố định là 100%. Việc cuộn xảy ra bên trong một container lồng nhau ở đâu đó trong cây DOM. Vì vậy, window.scrollY luôn là 0, và các phép tính của bạn thất bại một cách âm thầm.
Tôi đã mất quá nhiều thời gian để gỡ lỗi cái này. Câu trả lời thực sự đơn giản hơn tôi nghĩ.
Trang không cuộn. Có thứ gì đó bên trong trang cuộn. Bạn chỉ cần tìm nó.
Đây là phương pháp đã hiệu quả với tôi. Đầu tiên, lấy phần tử bạn quan tâm:
const el = document.querySelector("div.whitespace-pre-wrap");
Sau đó đi lên cây DOM cho đến khi bạn tìm thấy phần tử cha thực sự xử lý việc cuộn:
function findScrollParent(element) {
let parent = element.parentElement;
while (parent) {
const style = getComputedStyle(parent);
if (/(auto|scroll)/.test(style.overflowY)) return parent;
parent = parent.parentElement;
}
return document.documentElement;
}
const container = findScrollParent(el);
Hàm này kiểm tra thuộc tính overflowY đã tính toán của từng phần tử cha. Phần tử đầu tiên có auto hoặc scroll là container cuộn của bạn. Trong ChatGPT, đó thường là một <div> với một lớp như .overflow-y-auto hoặc tương tự.
Khi bạn đã có container, việc tính toán vị trí Y thực sự rất đơn giản:
const y = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
Bạn đang trừ offset của container khỏi vị trí của phần tử và cộng lại khoảng cách mà container đã cuộn. Điều đó cho bạn vị trí của phần tử so với đỉnh của container, đó là điều bạn thực sự muốn ngay từ đầu.
Vẽ một đường màu đỏ tại vị trí Y đã tính toán của bạn để đảm bảo nó thẳng hàng hoàn hảo:
const marker = document.createElement("div");
marker.style.cssText = `position: absolute; left: 0; right: 0; background: red; height: 2px; top: ${y}px`;
document.body.appendChild(marker);
Nếu đường thẳng nằm ngay trên phần tử mục tiêu của bạn, bạn đã làm đúng.
Đây không phải là một sự cố. Các framework như React và các thư viện như Tailwind khuyến khích bố cục toàn chiều cao. Mô hình rất đơn giản: phần tử gốc chiếm toàn bộ khung nhìn, và mỗi phần quản lý tràn của riêng nó.
Hãy nghĩ về nó. Nếu toàn bộ trang cuộn, thì thanh điều hướng, thanh bên và modal sẽ chống lại việc cuộn đó. Một thanh bên sẽ cuộn đi khi bạn cuộn xuống. Một tiêu đề cố định cần các thủ thuật CSS bổ sung để giữ nguyên vị trí. Bằng cách lồng các container cuộn, mỗi phần của giao diện người dùng vẫn độc lập.
ChatGPT đưa điều này đến mức cực đoan. Thanh bên cuộn độc lập. Khung trò chuyện cuộn độc lập. Bảng cài đặt cuộn độc lập. Đó là một lựa chọn thiết kế hợp lý, nhưng nó phá vỡ những giả định đã tồn tại từ những ngày đầu của web.
Bạn sẽ gặp phải vấn đề tương tự này trong GitHub, Discord, Linear, Notion và hầu hết mọi SPA dựa trên React. Một khi bạn biết mô hình này, bạn sẽ bắt đầu thấy nó ở khắp mọi nơi.
Tôi đã gặp phải điều này khi xây dựng một tiện ích mở rộng trình duyệt. Nhưng có những trường hợp khác mà bạn có thể cần tọa độ Y chính xác trong một SPA.
Các công cụ tự động hóa giao diện người dùng như Playwright và Cypress thường cần cuộn đến các phần tử trước khi tương tác với chúng. Nếu việc phát hiện cuộn của chúng sử dụng window.scrollY, chúng sẽ thất bại theo cùng một cách.
Định vị tooltip hoặc popover tùy chỉnh bị hỏng khi bạn tính toán vị trí tương đối với container sai. Tôi đã thấy các dropdown hiển thị 2000 pixel phía trên vị trí chúng nên ở.
Logic scroll spy để làm nổi bật phần hiện tại đang hiển thị. Bạn cần container thực sự để biết những gì thực sự hiển thị.
Các công cụ chụp màn hình chụp nội dung toàn trang. Nếu công cụ không biết về các container cuộn lồng nhau, bạn sẽ nhận được một ảnh chụp màn hình của bất cứ thứ gì vừa với khung nhìn và không hơn.
Đây là một kiểm tra nhanh để biết liệu bạn có đang đối phó với vấn đề này không:
// If this returns 0, you've got a nested scroll container
console.log("scrollY:", window.scrollY, "body height:", document.body.scrollHeight);
Tại sao window.scrollY trả về 0 trong các ứng dụng như ChatGPT?
Công thức đúng cho vị trí Y của một phần tử trong một container cuộn lồng nhau là gì?
ChatGPT là một ứng dụng phát trực tuyến. Các tin nhắn mới xuất hiện khi bạn đang xem trang. Các phần tử được thêm, xóa và di chuyển.
Nếu bạn đang theo dõi vị trí của một phần tử cụ thể theo thời gian, đừng tính toán nó một lần và lưu vào bộ nhớ cache. Hãy tính toán lại khi cuộn và khi có thay đổi DOM. Một MutationObserver kết hợp với một trình lắng nghe sự kiện cuộn bao gồm cả hai trường hợp:
const observer = new MutationObserver(() => {
const y = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
// do something with the updated y
});
observer.observe(container, { childList: true, subtree: true });
Một ResizeObserver trên container cũng hữu ích nếu bố cục thay đổi. Tôi đã thấy rằng việc kết hợp cả hai trình quan sát với một throttle trên trình xử lý cuộn bao gồm mọi trường hợp ngoại lệ mà tôi đã gặp.
Điều này quan trọng hơn bạn nghĩ. ChatGPT tải tin nhắn một cách lười biếng khi bạn cuộn lên. Khi nội dung mới tải phía trên phần tử bạn đang theo dõi, vị trí của nó sẽ thay đổi. Nếu không có trình quan sát, bạn sẽ trỏ đến các tọa độ cũ.
MutationObserver kích hoạt rất nhiều. Nếu bạn đang đo vị trí bên trong callback, hãy điều tiết nó. Nếu không, bạn sẽ làm giảm hiệu suất cuộn của giao diện người dùng, và không có gì làm hỏng một bản demo bằng việc cuộn bị giật.
Ba điều cần nhớ:
window.scrollY chỉ hoạt động khi bản thân document cuộn. Trong các SPA, điều đó thường không xảy ra.overflowY.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop.Chỉ vậy thôi. Không có phép thuật. Không có thủ thuật framework. Chỉ là hiểu cách DOM thực sự hoạt động dưới các SPA hiện đại. Một khi tôi ngừng giả định window là mục tiêu cuộn, mọi thứ đã rõ ràng.
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

Tìm hiểu sâu về cách quản lý form trong Next.js 14 bằng Server Actions, useFormState, useFormStatus và Zod để xác thực schema, giúp bạn xây dựng các form hiện đại, nâng cao dần mà không mệt mỏi với JavaScript phía client.
Đọc tiếp
Hướng dẫn toàn diện về việc di chuyển từ Node.js sang Bun 1.2 trong môi trường production: hiệu suất HTTP, SQLite & package full-stack với kiến trúc cấp độ production và các ví dụ code.
Đọc tiếp
Hướng dẫn toàn diện về suppressHydrationWarning trong Next.js: sử dụng an toàn & gỡ lỗi đầy đủ với các ví dụ thực tế đã được kiểm chứng.
Đọc tiếp