Các kỹ thuật gỡ lỗi dùng trong trình duyệt
(alan.norbauer.com)- Nếu có thể sử dụng công cụ dành cho nhà phát triển của trình duyệt ở mức trung cấp trở lên, bạn có thể mở rộng breakpoint có điều kiện không chỉ như điều kiện dừng, mà còn thành công cụ ghi log, truy vết, thay đổi trạng thái và đo thời gian
- Khi đưa các Console API như
console.log,console.count,console.trace,console.timevào breakpoint hoặc Watch Pane, bạn có thể ghi lại luồng thực thi và trạng thái mà không dừng chương trình - Bằng cách kết hợp số lượng đối số hàm, thời gian, giá trị CSS đã tính toán, số lần thực thi, lấy mẫu ngẫu nhiên và boolean toàn cục, bạn có thể kiểm soát thời điểm dừng tinh vi hơn nhiều
monitor,debug,copy,$0,getEventListeners,monitorEventscủa Chrome giúp xử lý nhanh lời gọi hàm, DOM, sự kiện và thao tác clipboard từ console- Trên các màn hình mà DOM liên tục thay đổi do JS hoặc sự kiện người dùng, tạm dừng JS,
setTimeout(debugger), snapshot DOM và giám sát focus đặc biệt hữu ích cho gỡ lỗi HTML/CSS
Mở rộng breakpoint có điều kiện thành công cụ gỡ lỗi
-
Trong breakpoint có điều kiện, ngoài điều kiện đúng/sai, bạn cũng có thể đưa vào biểu thức JavaScript có side effect
-
Tận dụng điều này, bạn có thể dùng breakpoint cơ bản như công cụ ghi log, truy vết, thay đổi trạng thái hoặc đo thời gian
-
Logpoint và tracepoint
- Khi gọi
console.logtrong breakpoint có điều kiện, nó hoạt động như một logpoint, ghi giá trị ra console mà không dừng thực thi - Dùng
console.countcũng có thể đếm kèm số lần dòng đó đã được thực thi - Tính đến bản cập nhật tháng 5/2020, các trình duyệt lớn đã hỗ trợ trực tiếp logpoint/tracepoint
- Khi gọi
-
Dùng Console API trong Watch Pane
- Bạn cũng có thể đưa các biểu thức như
console.logvào Watch Pane - Mỗi lần ứng dụng dừng trong debugger, có thể chụp snapshot localStorage bằng
console.table(localStorage) - Khi dùng breakpoint thay đổi DOM cùng với biểu thức Watch, có thể lưu snapshot vào mảng sau khi subtree DOM thay đổi
(window.doms = window.doms || []).push(document.documentElement.outerHTML)- Không thể làm cho breakpoint thay đổi DOM không dừng thực thi
- Bạn cũng có thể đưa các biểu thức như
-
Truy vết call stack
- Khi cần tìm vấn đề có hàm hiển thị loading spinner và hàm ẩn nó, nhưng chỉ còn lời gọi hiển thị, có thể đưa
console.tracevào breakpoint có điều kiện - Từ stack trace cuối cùng của hàm hiển thị, nhấp vào caller để chuyển tới vị trí gọi tương ứng
- Khi cần tìm vấn đề có hàm hiển thị loading spinner và hàm ẩn nó, nhưng chỉ còn lời gọi hiển thị, có thể đưa
-
Thay đổi hành vi chương trình khi đang chạy
- Nếu ghi đè đối số hàm trong biểu thức breakpoint có điều kiện, bạn có thể thay đổi tức thì hành vi chương trình ngay trong trình duyệt
- Ví dụ, có thể thay đổi giá trị
idcủa hàmgetPerson - Biểu thức được đánh giá là đúng như
id=1sẽ làm debugger dừng lại, nên nếu không muốn dừng, có thể thêm, false
-
Đo hiệu năng nhanh
- Vì có lẫn thời gian đánh giá breakpoint có điều kiện, cách này không phù hợp để profiling hiệu năng chính xác
- Khi cần đo nhanh, có thể đặt
console.time('label')ở điều kiện tại điểm bắt đầu vàconsole.timeEnd('label')ở điều kiện tại điểm kết thúc để ghi thời gian thực thi ra console
Làm điều kiện dừng tinh vi hơn
-
Tận dụng số lượng đối số hàm
- Nếu chỉ muốn dừng khi hàm hiện tại được gọi với 3 đối số, có thể dùng điều kiện sau
arguments.callee.length === 3- Hữu ích với hàm overload có đối số tùy chọn
- Nếu chỉ muốn dừng khi số đối số được định nghĩa khác với số đối số thực tế được truyền vào, có thể dùng điều kiện sau
(arguments.callee.length) != arguments.length- Giúp tìm lỗi ở vị trí gọi hàm
-
Điều kiện dựa trên thời gian
- Nếu không muốn dừng trước khi đã qua 5 giây sau khi tải trang, có thể dùng điều kiện sau
performance.now() > 5000- Phù hợp khi chỉ quan tâm đến breakpoint sau giai đoạn tải trang ban đầu
- Nếu muốn sau khi breakpoint được đặt thì không dừng trong 5 giây tiếp theo, rồi sau đó mới dừng, có thể lưu thời điểm mốc vào biến toàn cục
window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000- Có thể khởi động lại bộ đếm từ console bằng
window.baseline = Date.now()
-
Điều kiện dựa trên trạng thái CSS
- Có thể dừng dựa trên giá trị CSS đã tính toán làm điều kiện
- Ví dụ, nếu chỉ muốn dừng khi màu nền của
document.bodylà màu đỏ, có thể dùng điều kiện sau
window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)" -
Số lần thực thi và lấy mẫu
- Có thể tăng bộ đếm mỗi khi dòng được thực thi để chỉ dừng một lần trong mỗi hai lần
window.counter = (window.counter || 0) + 1, window.counter % 2 === 0- Nếu muốn dừng ngẫu nhiên 1 lần trong 10 lần bằng lấy mẫu ngẫu nhiên, có thể dùng điều kiện sau
Math.random() < 0.1 -
Không bao giờ dừng ở một vị trí cụ thể
- Trong Chrome, nếu nhấp chuột phải vào gutter và chọn “Never Pause Here”, một breakpoint có điều kiện luôn là
falsesẽ được tạo, nên không dừng ở dòng đó - Có thể dùng khi loại trừ một dòng cụ thể khỏi breakpoint XHR hoặc bỏ qua một ngoại lệ cụ thể
- Trong Chrome, nếu nhấp chuột phải vào gutter và chọn “Never Pause Here”, một breakpoint có điều kiện luôn là
-
Tự động gán ID cho instance
- Có thể đặt breakpoint có điều kiện trong constructor của class để lưu từng instance vào mảng
(window.instances = window.instances || []).push(this)- ID duy nhất của một instance cụ thể có thể được xác định bằng chỉ mục mảng
window.instances.indexOf(instance) -
Bật/tắt breakpoint toàn cục
- Có thể điều khiển một hoặc nhiều breakpoint có điều kiện bằng boolean toàn cục
- Có thể thay đổi trực tiếp từ console, thay đổi từ breakpoint khác, hoặc thay đổi bằng timer
window.enableBreakpoints = true setTimeout(() => (window.enableBreakpoints = true), 5000)
Truy vết lời gọi hàm và class
-
Giám sát lời gọi method của class
- Dùng method dòng lệnh
monitorcủa Chrome có thể dễ dàng truy vết lời gọi method của class - Nếu muốn xem lời gọi method cho mọi instance của class
Dog, có thể áp dụngmonitorcho mọi property của prototype
var p = Dog.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- Console sẽ xuất thông tin lời gọi như
function bark called with arguments: 2 - Nếu muốn dừng thực thi khi được gọi thay vì chỉ ghi log, có thể dùng
debugthay chomonitor
- Dùng method dòng lệnh
-
Truy vết class từ một instance cụ thể
- Nếu không biết tên class nhưng có instance, có thể giám sát theo cùng cách thông qua prototype của constructor
var p = instance.constructor.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- Hữu ích khi tạo hàm hoạt động với instance bất kỳ mà không cố định vào một class cụ thể
-
Gỡ lỗi ngay trước khi gọi hàm từ console
- Trước khi gọi hàm muốn gỡ lỗi từ console, chạy
debugger, rồi Step into vào lời gọi hàm tiếp theo để đi vào phần triển khai hàm
debugger; fn(1);- Hữu ích khi ngại tìm vị trí định nghĩa hàm hoặc khi
fnđược bind động nên không biết vị trí source - Trong Chrome, cũng có thể gọi
debug(fn)để mỗi lầnfnđược gọi thì dừng bên trong hàm
- Trước khi gọi hàm muốn gỡ lỗi từ console, chạy
Gỡ lỗi thay đổi URL và đọc property
-
Dừng trước khi SPA routing
- Để dừng ngay trước khi ứng dụng một trang thay đổi URL, có thể gắn hàm có
debuggervàohistory.pushState,history.replaceState,window.onhashchange,window.onpopstate
const dbg = () => { debugger; }; history.pushState = dbg; history.replaceState = dbg; window.onhashchange = dbg; window.onpopstate = dbg;- Phiên bản
dbgnày không bao gồm phần triển khai vừa dừng vừa không làm hỏng điều hướng - Trường hợp gọi trực tiếp
window.location.replacehoặcwindow.location.assign, trang sẽ unload ngay sau khi gán nên không thể xử lý bằng cách này - Trong Chrome, có thể đặt
debuglên các method liên quan để xem source redirect và trạng thái tại thời điểm đó
debug(window.location.replace); debug(window.location.assign); - Để dừng ngay trước khi ứng dụng một trang thay đổi URL, có thể gắn hàm có
-
Tìm nơi property được đọc
- Nếu muốn biết thời điểm một property cụ thể của object được đọc, có thể đặt
debuggertrong getter
{ get configOption() { debugger; return true; } }- Hữu ích khi kiểm tra đoạn code nào sử dụng option cấu hình và sử dụng như thế nào
- Có thể áp dụng bằng cách sửa trong source gốc hoặc dùng breakpoint có điều kiện
- Nếu muốn biết thời điểm một property cụ thể của object được đọc, có thể đặt
Sao chép dữ liệu từ console
- Dùng Console API
copy()có thể sao chép thông tin trong trình duyệt vào clipboard mà không bị cắt ngắn chuỗi - Có thể dùng khi sao chép snapshot DOM hiện tại, metadata tài nguyên như hình ảnh, JSON blob lớn hoặc dump localStorage
copy(document.documentElement.outerHTML)
copy(performance.getEntriesByType("resource"))
copy(JSON.parse(blob))
copy(localStorage)
Gỡ lỗi HTML/CSS
-
Kiểm tra DOM khi JS đang tạm dừng
- Trong DOM inspector, trên Chrome/Windows, nhấn
ctrl+\để tạm dừng thực thi JS bất cứ lúc nào - Ngay cả khi JS thay đổi DOM hoặc sự kiện như mouseover làm thay đổi DOM, bạn vẫn có thể kiểm tra ổn định snapshot DOM hiện tại
- Trong DOM inspector, trên Chrome/Windows, nhấn
-
Kiểm tra phần tử biến mất
- Với phần tử DOM có điều kiện biến mất khi di chuyển chuột, có thể đặt timer 5 giây trong console để chạy debugger rồi kiểm tra
setTimeout(function() { debugger; }, 5000);- Sau khi hiển thị UI trong 5 giây, nếu thực thi JS dừng lại thì phần tử sẽ không biến mất, nên có thể chuyển sang công cụ dành cho nhà phát triển để kiểm tra
- Trong khi JS đang dừng, có thể kiểm tra phần tử, chỉnh sửa CSS và chạy lệnh JS console
- Hiệu quả khi kiểm tra DOM phụ thuộc vào vị trí con trỏ hoặc focus
-
Ghi lại snapshot DOM
- Để sao chép DOM hiện tại, có thể dùng lệnh sau
copy(document.documentElement.outerHTML);- Để lưu chuỗi DOM vào mảng mỗi 1 giây, có thể dùng
setIntervalnhư sau
doms = []; setInterval(() => { const domStr = document.documentElement.outerHTML; doms.push(domStr); }, 1000);- Cũng có thể liên tục in ra console mà không lưu vào mảng
-
Giám sát phần tử đang được focus
- Có thể định kỳ so sánh
document.activeElementđể in thay đổi focus ra console
(function () { let last = document.activeElement; setInterval(() => { if (document.activeElement !== last) { last = document.activeElement; console.log("Focus changed to: ", last); } }, 100); })(); - Có thể định kỳ so sánh
-
Tìm phần tử được hiển thị in đậm
- Có thể tìm mọi phần tử có
fontWeightđã tính toán là"bold"hoặc"700"
const isBold = (e) => { let w = window.getComputedStyle(e).fontWeight; return w === "bold" || w === "700"; }; Array.from(document.querySelectorAll("*")).filter(isBold);- Nếu chỉ muốn tìm trong các phần tử con của phần tử hiện được chọn trong inspector, có thể tìm dựa trên
$0
Array.from($0.querySelectorAll("*")).filter(isBold); - Có thể tìm mọi phần tử có
-
Xử lý phần tử được chọn và sự kiện
$0trong console là tham chiếu tự động trỏ tới phần tử hiện được chọn trong Element Inspector- Trong Chrome và Edge, có thể truy cập các phần tử đã kiểm tra trước đó bằng
$1, phần tử trước nữa bằng$2, v.v. - Trong Chrome, có thể xem event listener của phần tử hiện được chọn
getEventListeners($0)- Để gỡ lỗi mọi sự kiện của phần tử được chọn, có thể dùng
monitorEvents($0) - Nếu chỉ muốn xem một nhóm sự kiện cụ thể, có thể truyền vào mảng
monitorEvents($0, ["control", "key"])
1 bình luận
Các ý kiến trên Hacker News
Trong vài thập kỷ qua, các công cụ gỡ lỗi tích hợp trong trình duyệt đã tiến bộ thật sự rất nhiều
Với tư cách là người đã dùng JS lâu năm, tôi rất biết ơn những người đã làm cho việc gỡ lỗi mã ngay trong trình duyệt trở nên trực quan đến vậy
Khi chuyển sang các mảng phát triển khác như backend hay các ngôn ngữ khác, tôi lại nhớ hệ sinh thái công cụ gỡ lỗi mà các trình duyệt hiện đại cung cấp sẵn
Tôi đã gỡ lỗi nhiều năm với Python, Java, C++ nhưng chưa từng cảm thấy thiếu công cụ
Tất nhiên tôi từng thấy khá nhiều người không biết những lựa chọn có thể dùng trong lĩnh vực này
So với địa ngục đó thì tôi chọn console
setTimeout(function() { debugger; }, 5000);thật thông minhRốt cuộc cách duy nhất để thắng được chồng “rùa đệ quy” khi Chrome debugger tự gỡ lỗi chính nó chính là câu lệnh
debuggersam.pl của sâu Myspace Sammy khét tiếng cũng từng dùng bẫy gỡ lỗi để ngăn khách truy cập đào bới trang chủ HTML đã bị làm rối
Nó chạy vòng lặp chính, chèn các câu lệnh
debugger;phiền phức khắp nơi, rồi thực thi ở 30FPS / mỗi 32ms để khiến DevTools trở nên vô dụngVì không có cách nào để “bỏ qua” câu lệnh
debuggercmd-\Trang sẽ dừng ở dòng JS đang chạy hiện tại, và bạn có thể kiểm tra phần tử gây vấn đề như bình thường
debuggerPhần lớn trường hợp dùng
debuggerhợp lệ đều có thể thay bằng breakpoint đơn giản nếu đang làm việc trên mã của chính mìnhViệc thiếu
queryObjectskhá nổi bậtĐây là một API khá điên rồ, trả về danh sách tất cả đối tượng được tạo bằng một constructor cụ thể
Ví dụ có thể dùng
queryObjects(Function)để lấy danh sách mọi hàm trong heapNó trả về cả các hàm “private” nằm bên trong một module nào đó
Đây là một tính năng thật sự điên rồ, và có lẽ chỉ có trên Chromium
Tôi không tài nào dùng watch variable cho ra hồn được
Scope và quy tắc cập nhật quá khó hiểu; tưởng như chỉ watch được biến toàn cục, nhưng ngay cả vậy cũng không hoạt động như mong đợi, nên khi test rốt cuộc tôi lại phải rải log giá trị khắp nơi
Từ nhiều năm nay tôi vẫn nghĩ nên đưa một UI kiểu Data.gui [1] vào console để có thể xem và thử biến cùng giá trị cấu hình
Có thể xem cách nó hoạt động thực tế trong CodePen này [2]
Trình duyệt về lý thuyết có nhiều tính năng gỡ lỗi tuyệt vời, nhưng thực tế dường như chúng không hoạt động ổn định
Ngay cả breakpoint tôi cũng không làm cho tất cả bắt được một cách ổn định
Khi mã còn được bung ra thì ổn, nhưng chỉ cần được bundle, dù chưa minify, thì nhiều tính năng debugger dường như đã hỏng
Tôi không phải kỹ sư frontend nên có lẽ tôi đang làm sai gì đó
Rất có thể đó là nguyên nhân gây bực bội, và tôi vẫn không hiểu vì sao nó được bật mặc định trong khi trải nghiệm gỡ lỗi chủ động khi bật nó lại tệ đến vậy
Sẽ thật tốt nếu có cách truy cập biến cục bộ của IIFE mà không cần dừng thực thi mã bên trong phạm vi IIFE
Có cách nào thuyết phục trình gỡ lỗi làm như vậy không?
https://en.m.wikipedia.org/wiki/Immediately_invoked_function...
Nếu đang hoàn toàn ở bên ngoài IIFE thì tôi nghĩ là không thể
Biến bạn tìm có thể không tồn tại trong bộ nhớ trước hoặc sau khi thực thi
JavaScript không có biến tĩnh như các ngôn ngữ khác, nên mỗi lần IIFE được gọi, các biến bên trong sẽ bị bỏ đi
JavaScript có mô hình đơn luồng khá mạnh, trừ khi dùng những thứ như web worker, nên nếu không cố tình tạo race condition trực tiếp bằng lệnh gọi
async/awaitthì bản thân khái niệm một biến như vậy còn nằm trong bộ nhớ bên ngoài phạm vi IIFE đã khó thành lậpNếu lạm dụng
varđể khởi tạo biến bên ngoài phạm vi IIFE, bạn có thể thấy giá trị do lần gọi IIFE cuối cùng tạo raCần chỉnh sửa một chút, nhưng hiệu quả so với công sức bỏ ra là rất lớn
https://github.com/kristopolous/_inject
Bạn có thể đi qua gần như bất kỳ ngữ cảnh hàm nào tại một thời điểm tùy ý để xem chuyện gì đã xảy ra
Cách làm là lợi dụng bộ đếm tham chiếu để ngăn ngữ cảnh bị hủy
Hồi còn làm nhiều JS phía client, nó thật sự hữu ích
Phiên bản killer app được làm tử tế của công cụ này sẽ là mở REPL trong bất kỳ ngữ cảnh nào
Ngay cả ở trạng thái hiện tại, muốn dùng tốt cũng cần khá nhiều kỹ năng
https://firefox-source-docs.mozilla.org/devtools-user/debugg...
Sửa: Tôi vừa nhận ra đó đúng là nội dung đầu tiên được nói tới trong bài được liên kết
[[Scopes]]Có vẻ Firefox không có cách nào
Để đầy đủ, có thể khuyên dùng Werkzeug
Tôi đang dùng nó để phát triển backend Django và nó cực kỳ hữu ích
Ở bất kỳ đâu, bất cứ lúc nào có exception xảy ra, nó cho phép dùng shell “PDB” ngay trong trình duyệt
1/0vào nơi bạn muốn Werkzeug hiện lên trong trình duyệtRất đơn giản và trực quan
django-extension/runserver_pluskhông?Một mẹo tôi luôn dùng là tìm kiếm các script đã tải bằng chuỗi UI để gỡ lỗi
Vào Network panel, bắt đầu ghi lại các network request, mở sidebar bên trái rồi tìm kiếm đoạn mã hoặc chuỗi UI muốn tìm
Thường nó sẽ được tìm thấy trong một file chunk JS bundle kỳ lạ nào đó; khi bấm vào kết quả, network request của file đó sẽ mở ra
Nhấp chuột phải ở bất kỳ đâu trong file đó và chọn mục kiểu “Open in Sources” để chuyển sang debugger
Giờ chỉ cần đặt câu lệnh
debugger; trong quá trình này source map có lẽ cũng sẽ được tảiLà lập trình viên Python/Elixir lâu năm, tôi đã dùng
pdb.set_trace()vàIEx.pry()rất nhiềuGần đây tôi thừa hưởng một backend NodeJS cực kỳ lộn xộn, và đang vò nốt chỗ tóc còn lại vì không có công cụ gỡ lỗi tử tế
Tôi đã quay lại gỡ lỗi bằng
console.log, nhưng cảm giác như trở thành người nguyên thủyTôi không thể tin là một hệ sinh thái phổ biến như vậy lại không có REPL gỡ lỗi đúng nghĩa; ai có thể chỉ tôi nên đi đâu không?
Hãy dùng
node --inspect-brk, rồi kết nối bằng VS Code hoặc Chrome DevToolsPhần rắc rối là liệu có công cụ build như biến đổi TypeScript đứng trước lệnh
nodejshay khôngNếu chỉ chạy
nodethì khá dễREPL gỡ lỗi chính là JavaScript console
Thêm nó làm tùy chọn gỡ lỗi là có thể đặt breakpoint và step vào trong hàm
Vì phần lớn được đóng gói trong object, JavaScript khá dễ gỡ lỗi
--inspecthoặc--inspect-brkTrong VSCode cũng có thể dùng lệnh “open JavaScript debug terminal”
Lệnh này mở một terminal mà bất kỳ lệnh
nodenào cũng sẽ tự động được khởi chạy với debugger đã gắn vàohttps://pypi.org/project/snoop/
Đây là decorator để trace sâu
Tôi chưa thấy tính năng như vậy trong hệ sinh thái Node JS
Ở phần “Debugging Property Reads”, tôi tò mò bằng cách nào dùng breakpoint có điều kiện để đổi
{configOption: true}thành{get configOption() { debugger; return true; }}Bạn chỉ cần ghi đè giá trị bằng getter trong console; nếu có quyền ghi, cũng có thể sửa trực tiếp trong mã nguồn
Có vẻ đây là thứ nhất định phải học, nên tôi thắc mắc liệu có sách chuyên biệt hay tài liệu học tập nào không
Hay là chỉ còn cách đào sâu mãi vào phát triển web và frontend?
Khi đang bận làm việc, việc bấm vào liên kết để đọc release notes có thể không hấp dẫn lắm, nhưng mỗi khi có phiên bản mới, chỉ cần lướt qua 5 phút cũng rất đáng
Cũng có những video ngắn vài phút, dễ tiếp thu, giúp nắm được tổng quan nhanh
Mục đích là giới thiệu tính năng mới, nhưng theo kinh nghiệm, bạn cũng thường hiểu được các giới hạn hiện tại của công cụ