Giới thiệu về Bản đồ nguồn JavaScript

Ryan Seddon

Bạn có bao giờ muốn giữ cho mã phía máy khách của mình dễ đọc và quan trọng hơn là có thể gỡ lỗi ngay cả sau khi bạn đã kết hợp và rút gọn mã đó mà không ảnh hưởng đến hiệu suất không? Giờ đây, bạn có thể làm được điều đó thông qua bản đồ nguồn.

Bản đồ nguồn là một cách để liên kết một tệp kết hợp/rút gọn trở lại trạng thái chưa tạo. Khi tạo bản dựng cho bản phát hành chính thức, cùng với việc rút gọn và kết hợp các tệp JavaScript, bạn sẽ tạo một bản đồ nguồn chứa thông tin về các tệp gốc. Khi truy vấn một dòng và số cột nhất định trong JavaScript đã tạo, bạn có thể tìm kiếm trong bản đồ nguồn để trả về vị trí ban đầu. Các công cụ dành cho nhà phát triển (hiện là bản dựng hằng đêm của WebKit, Google Chrome hoặc Firefox 23 trở lên) có thể tự động phân tích cú pháp bản đồ nguồn và khiến bản đồ nguồn đó trông như thể bạn đang chạy các tệp chưa được rút gọn và chưa được kết hợp.

Bản minh hoạ cho phép bạn nhấp chuột phải vào vị trí bất kỳ trong textarea chứa nguồn được tạo. Chọn "Tìm vị trí ban đầu" sẽ truy vấn bản đồ nguồn bằng cách truyền số dòng và số cột đã tạo, đồng thời trả về vị trí trong mã gốc. Đảm bảo bảng điều khiển của bạn đang mở để bạn có thể xem kết quả.

Ví dụ về thư viện bản đồ nguồn JavaScript của Mozilla đang hoạt động.

Thế giới thực

Trước khi xem cách triển khai Bản đồ nguồn trong thực tế sau đây, hãy đảm bảo bạn đã bật tính năng bản đồ nguồn trong Chrome Canary hoặc WebKit hằng đêm bằng cách nhấp vào biểu tượng bánh răng cài đặt trong bảng điều khiển công cụ dành cho nhà phát triển rồi đánh dấu vào tuỳ chọn "Bật bản đồ nguồn".

Cách bật bản đồ nguồn trong các công cụ dành cho nhà phát triển WebKit.

Firefox 23 trở lên đã bật bản đồ nguồn theo mặc định trong các công cụ dành cho nhà phát triển tích hợp sẵn.

Cách bật bản đồ nguồn trong công cụ dành cho nhà phát triển Firefox.

Tại sao tôi nên quan tâm đến bản đồ nguồn?

Hiện tại, tính năng ánh xạ nguồn chỉ hoạt động giữa JavaScript không nén/kết hợp với JavaScript nén/không kết hợp, nhưng tương lai sẽ rất tươi sáng với các cuộc thảo luận về các ngôn ngữ được biên dịch sang JavaScript như CoffeeScript và thậm chí có thể hỗ trợ thêm cho các trình xử lý trước CSS như SASS hoặc LESS.

Trong tương lai, chúng ta có thể dễ dàng sử dụng hầu hết ngôn ngữ như thể ngôn ngữ đó được hỗ trợ gốc trong trình duyệt bằng bản đồ nguồn:

  • CoffeeScript
  • ECMAScript 6 trở lên
  • SASS/LESS và các ngôn ngữ khác
  • Hầu hết mọi ngôn ngữ biên dịch sang JavaScript

Hãy xem bản ghi màn hình này về việc gỡ lỗi CoffeeScript trong một bản dựng thử nghiệm của bảng điều khiển Firefox:

Gần đây, Bộ công cụ web của Google (GWT) đã thêm tính năng hỗ trợ Bản đồ nguồn. Ray Cromwell thuộc nhóm GWT đã thực hiện một bản ghi màn hình tuyệt vời cho thấy tính năng hỗ trợ bản đồ nguồn đang hoạt động.

Một ví dụ khác mà tôi đã tổng hợp sử dụng thư viện Traceur của Google, cho phép bạn viết ES6 (ECMAScript 6 hoặc Next) và biên dịch mã đó thành mã tương thích với ES3. Trình biên dịch Traceur cũng tạo một bản đồ nguồn. Hãy xem bản minh hoạ này về các đặc điểm và lớp ES6 đang được sử dụng như được hỗ trợ gốc trong trình duyệt, nhờ bản đồ nguồn.

Textarea trong bản minh hoạ cũng cho phép bạn viết ES6. Mã này sẽ được biên dịch ngay lập tức và tạo bản đồ nguồn cùng với mã ES3 tương đương.

Gỡ lỗi Traceur ES6 bằng cách sử dụng bản đồ nguồn.

Minh hoạ: Viết ES6, gỡ lỗi, xem hoạt động liên kết nguồn

Bản đồ nguồn hoạt động như thế nào?

Hiện tại, trình biên dịch/trình rút gọn JavaScript duy nhất hỗ trợ việc tạo bản đồ nguồn là trình biên dịch Closure. (Tôi sẽ giải thích cách sử dụng sau.) Sau khi bạn kết hợp và rút gọn JavaScript, một tệp bản đồ nguồn sẽ xuất hiện cùng với tệp JavaScript đó.

Hiện tại, trình biên dịch Closure không thêm nhận xét đặc biệt ở cuối cần thiết để cho các công cụ phát triển của trình duyệt biết rằng có bản đồ nguồn:

//# sourceMappingURL=/path/to/file.js.map

Điều này cho phép các công cụ dành cho nhà phát triển liên kết các lệnh gọi trở lại vị trí của chúng trong tệp nguồn ban đầu. Trước đây, pragma nhận xét là //@, nhưng do một số vấn đề với pragma đó và nhận xét biên dịch có điều kiện của IE, nên quyết định đã được đưa ra để thay đổi pragma đó thành //#. Hiện tại, Chrome Canary, WebKit Nightly và Firefox 24 trở lên hỗ trợ pragma nhận xét mới. Thay đổi cú pháp này cũng ảnh hưởng đến sourceURL.

Nếu không thích ý tưởng về nhận xét kỳ lạ, bạn có thể đặt một tiêu đề đặc biệt trên tệp JavaScript đã biên dịch:

X-SourceMap: /path/to/file.js.map

Giống như nhận xét, mã này sẽ cho người dùng bản đồ nguồn biết nơi tìm bản đồ nguồn liên kết với tệp JavaScript. Tiêu đề này cũng giải quyết vấn đề tham chiếu bản đồ nguồn bằng các ngôn ngữ không hỗ trợ nhận xét một dòng.

Ví dụ về công cụ dành cho nhà phát triển WebKit về bản đồ nguồn đang bật và bản đồ nguồn đang tắt.

Tệp bản đồ nguồn sẽ chỉ được tải xuống nếu bạn đã bật bản đồ nguồn và mở công cụ phát triển. Bạn cũng cần tải các tệp gốc lên để các công cụ dành cho nhà phát triển có thể tham chiếu và hiển thị các tệp đó khi cần.

Làm cách nào để tạo bản đồ nguồn?

Bạn cần sử dụng trình biên dịch Closure để rút gọn, nối và tạo bản đồ nguồn cho các tệp JavaScript. Lệnh như sau:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

Hai cờ lệnh quan trọng là --create_source_map--source_map_format. Bạn phải làm việc này vì phiên bản mặc định là V2 và chúng tôi chỉ muốn làm việc với V3.

Cấu trúc của bản đồ nguồn

Để hiểu rõ hơn về bản đồ nguồn, chúng ta sẽ lấy một ví dụ nhỏ về tệp bản đồ nguồn do trình biên dịch Closure tạo ra và tìm hiểu chi tiết hơn về cách hoạt động của phần "mappings" (liên kết). Ví dụ sau đây có một chút khác biệt so với ví dụ về thông số kỹ thuật V3.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Ở trên, bạn có thể thấy rằng bản đồ nguồn là một đối tượng cố định chứa nhiều thông tin hữu ích:

  • Số phiên bản mà bản đồ nguồn dựa trên đó
  • Tên tệp của mã được tạo (Tệp sản xuất được rút gọn/kết hợp)
  • sourceRoot cho phép bạn thêm vào nguồn một cấu trúc thư mục – đây cũng là một kỹ thuật tiết kiệm không gian
  • sources chứa tất cả tên tệp đã được kết hợp
  • names chứa tất cả tên biến/phương thức xuất hiện trong mã của bạn.
  • Cuối cùng, thuộc tính ánh xạ là nơi diễn ra phép thuật bằng cách sử dụng các giá trị VLQ Base64. Việc tiết kiệm không gian thực sự được thực hiện ở đây.

Base64 VLQ và giữ cho bản đồ nguồn nhỏ

Ban đầu, thông số kỹ thuật của bản đồ nguồn có đầu ra rất chi tiết về tất cả các mối liên kết và dẫn đến việc bản đồ nguồn có kích thước lớn hơn khoảng 10 lần so với mã được tạo. Phiên bản 2 giảm khoảng 50% và phiên bản 3 lại giảm thêm 50%, vì vậy, đối với tệp 133 kB, bạn sẽ có một bản đồ nguồn khoảng 300 kB.

Vậy làm cách nào để họ giảm kích thước trong khi vẫn duy trì các mối liên kết phức tạp?

VLQ (Số lượng có độ dài biến đổi) được sử dụng cùng với việc mã hoá giá trị thành giá trị Base64. Thuộc tính ánh xạ là một chuỗi siêu lớn. Trong chuỗi này có dấu chấm phẩy (;) đại diện cho số dòng trong tệp đã tạo. Trong mỗi dòng có dấu phẩy (",") đại diện cho từng đoạn trong dòng đó. Mỗi phân đoạn này có độ dài là 1, 4 hoặc 5 trong các trường có độ dài biến đổi. Một số có thể dài hơn nhưng chứa các bit tiếp tục. Mỗi phân đoạn được xây dựng dựa trên phân đoạn trước đó, giúp giảm kích thước tệp vì mỗi bit tương ứng với các phân đoạn trước đó.

Thông tin chi tiết về một phân đoạn trong tệp JSON của bản đồ nguồn.

Như đã đề cập ở trên, mỗi phân đoạn có thể có độ dài biến đổi là 1, 4 hoặc 5. Sơ đồ này được coi là có độ dài biến thiên là 4 với một bit tiếp tục (g). Chúng ta sẽ phân tích đoạn mã này và cho bạn biết cách bản đồ nguồn xác định vị trí ban đầu.

Các giá trị hiển thị ở trên chỉ là các giá trị được giải mã Base64, cần có thêm một số bước xử lý để có được giá trị thực của các giá trị đó. Mỗi phân khúc thường giải quyết 5 vấn đề:

  • Cột đã tạo
  • Tệp gốc chứa nội dung này
  • Số dòng ban đầu
  • Cột gốc
  • Và tên ban đầu (nếu có)

Không phải phân đoạn nào cũng có tên, tên phương thức hoặc đối số, vì vậy, các phân đoạn trong suốt sẽ chuyển đổi giữa độ dài biến từ 4 đến 5. Giá trị g trong sơ đồ phân đoạn ở trên được gọi là bit tiếp tục, cho phép tối ưu hoá thêm trong giai đoạn giải mã VLQ Base64. Bit tiếp tục cho phép bạn xây dựng trên một giá trị phân đoạn để có thể lưu trữ các số lớn mà không cần lưu trữ một số lớn, một kỹ thuật tiết kiệm không gian rất thông minh bắt nguồn từ định dạng midi.

Sơ đồ AAgBC ở trên sau khi được xử lý thêm sẽ trả về 0, 0, 32, 16, 1 – 32 là bit tiếp tục giúp tạo giá trị 16 sau đây. B được giải mã hoàn toàn trong Base64 là 1. Vì vậy, các giá trị quan trọng được sử dụng là 0, 0, 16, 1. Sau đó, chúng ta biết rằng dòng 1 (các dòng được tính bằng dấu chấm phẩy) cột 0 của tệp được tạo liên kết đến tệp 0 (mảng tệp 0 là foo.js), dòng 16 tại cột 1.

Để minh hoạ cách giải mã các phân đoạn, tôi sẽ tham chiếu đến thư viện JavaScript Bản đồ nguồn của Mozilla. Bạn cũng có thể xem mã ánh xạ nguồn của công cụ phát triển WebKit, cũng được viết bằng JavaScript.

Để hiểu rõ cách lấy giá trị 16 từ B, chúng ta cần có kiến thức cơ bản về toán tử bit và cách hoạt động của thông số kỹ thuật để liên kết nguồn. Số đứng trước, g, được gắn cờ là bit tiếp tục bằng cách so sánh số (32) và VLQ_CONTINUATION_BIT (tập hợp con số nhị phân 100000 hoặc 32) bằng cách sử dụng toán tử AND (&) theo bit.

32 & 32 = 32
// or
100000
|
|
V
100000

Hàm này trả về giá trị 1 ở mỗi vị trí bit mà cả hai đều có giá trị này. Vì vậy, giá trị được giải mã Base64 của 33 & 32 sẽ trả về 32 vì chúng chỉ chia sẻ vị trí 32 bit như bạn có thể thấy trong sơ đồ ở trên. Sau đó, thao tác này sẽ tăng giá trị dịch của bit thêm 5 cho mỗi bit tiếp tục trước đó. Trong trường hợp trên, chỉ dịch chuyển 5 lần, vì vậy, dịch chuyển sang trái 1 (B) 5 lần.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

Sau đó, giá trị đó được chuyển đổi từ giá trị VLQ có dấu bằng cách dịch số (32) sang phải một vị trí.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

Vậy là chúng ta đã có: đó là cách bạn chuyển 1 thành 16. Đây có vẻ là một quy trình phức tạp, nhưng khi các con số bắt đầu lớn hơn, quy trình này sẽ hợp lý hơn.

Các vấn đề tiềm ẩn về XSSI

Thông số kỹ thuật đề cập đến các vấn đề về việc đưa tập lệnh trên nhiều trang web có thể phát sinh do việc sử dụng bản đồ nguồn. Để giảm thiểu vấn đề này, bạn nên thêm ")]}" vào đầu dòng đầu tiên của bản đồ nguồn để cố tình vô hiệu hoá JavaScript, từ đó sẽ gửi một lỗi cú pháp. Các công cụ phát triển WebKit đã có thể xử lý vấn đề này.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

Như minh hoạ ở trên, 3 ký tự đầu tiên được cắt để kiểm tra xem chúng có khớp với lỗi cú pháp trong thông số kỹ thuật hay không. Nếu có, hãy xoá tất cả ký tự dẫn đến thực thể dòng mới đầu tiên (\n).

sourceURLdisplayName trong thực tế: Hàm ẩn danh và hàm Eval

Mặc dù không thuộc quy cách của bản đồ nguồn, nhưng hai quy ước sau đây cho phép bạn phát triển dễ dàng hơn nhiều khi làm việc với các hàm eval và hàm ẩn danh.

Trình trợ giúp đầu tiên trông rất giống với thuộc tính //# sourceMappingURL và thực sự được đề cập trong thông số kỹ thuật bản đồ nguồn V3. Bằng cách đưa nhận xét đặc biệt sau đây vào mã (mã này sẽ được đánh giá), bạn có thể đặt tên cho các eval để chúng xuất hiện dưới dạng tên hợp lý hơn trong các công cụ phát triển. Hãy xem một bản minh hoạ đơn giản bằng cách sử dụng trình biên dịch CoffeeScript:

Minh hoạ: Xem mã đã eval() hiển thị dưới dạng tập lệnh thông qua sourceURL

//# sourceURL=sqrt.coffee
Hình thức của bình luận đặc biệt sourceURL trong công cụ dành cho nhà phát triển

Trợ giúp khác cho phép bạn đặt tên cho các hàm ẩn danh bằng cách sử dụng thuộc tính displayName có sẵn trong ngữ cảnh hiện tại của hàm ẩn danh. Hãy phân tích bản minh hoạ sau để xem thuộc tính displayName hoạt động như thế nào.

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
Hiển thị thuộc tính displayName trong thực tế.

Khi phân tích tài nguyên mã trong các công cụ dành cho nhà phát triển, thuộc tính displayName sẽ xuất hiện thay vì một thuộc tính nào đó như (anonymous). Tuy nhiên, displayName gần như không còn được sử dụng và sẽ không được đưa vào Chrome. Nhưng mọi hy vọng vẫn chưa mất và một đề xuất tốt hơn nhiều đã được đề xuất có tên là debugName.

Tại thời điểm viết, tính năng đặt tên eval chỉ có trong trình duyệt Firefox và WebKit. Thuộc tính displayName chỉ có trong bản phát hành hằng đêm của WebKit.

Hãy cùng nhau đoàn kết

Hiện tại, có một cuộc thảo luận rất dài về việc hỗ trợ bản đồ nguồn được thêm vào CoffeeScript. Hãy kiểm tra vấn đề này và thêm sự hỗ trợ của bạn để tạo bản đồ nguồn được thêm vào trình biên dịch CoffeeScript. Đây sẽ là một thành công lớn cho CoffeeScript và những người theo dõi tận tâm của ngôn ngữ này.

UglifyJS cũng có một vấn đề về bản đồ nguồn mà bạn cũng nên xem xét.

Có rất nhiều công cụ tạo bản đồ nguồn, bao gồm cả trình biên dịch coffeescript. Tôi cho rằng đây là vấn đề không còn quan trọng nữa.

Chúng ta càng có nhiều công cụ có thể tạo bản đồ nguồn thì càng tốt. Vì vậy, hãy tiếp tục yêu cầu hoặc thêm tính năng hỗ trợ bản đồ nguồn vào dự án nguồn mở mà bạn yêu thích.

Không hoàn hảo

Một điều mà bản đồ nguồn hiện không hỗ trợ là biểu thức xem. Vấn đề là việc cố gắng kiểm tra đối số hoặc tên biến trong ngữ cảnh thực thi hiện tại sẽ không trả về giá trị nào vì đối số hoặc tên biến đó không thực sự tồn tại. Điều này đòi hỏi một số loại ánh xạ ngược để tra cứu tên thực của đối số/biến mà bạn muốn kiểm tra so với tên đối số/biến thực tế trong JavaScript đã biên dịch.

Tất nhiên, đây là một vấn đề có thể giải quyết được và nếu chú ý nhiều hơn đến bản đồ nguồn, chúng ta có thể bắt đầu thấy một số tính năng tuyệt vời và độ ổn định cao hơn.

Vấn đề

Gần đây, jQuery 1.9 đã thêm tính năng hỗ trợ cho bản đồ nguồn khi được phân phát qua các CDN chính thức. Báo cáo này cũng chỉ ra một lỗi đặc biệt khi sử dụng các nhận xét biên dịch có điều kiện của IE (//@cc_on) trước khi jQuery tải. Kể từ đó, đã có một commit để giảm thiểu vấn đề này bằng cách gói sourceMappingURL trong một nhận xét nhiều dòng. Bài học cần rút ra là không sử dụng nhận xét có điều kiện.

Kể từ đó, vấn đề này đã được giải quyết bằng cách thay đổi cú pháp thành //#.

Công cụ và tài nguyên

Sau đây là một số tài nguyên và công cụ khác mà bạn nên tham khảo:

  • Nick Fitzgerald có một nhánh của UglifyJS có hỗ trợ bản đồ nguồn
  • Paul Irish có một bản minh hoạ nhỏ gọn và hữu ích về bản đồ nguồn
  • Hãy xem thay đổi WebKit về thời điểm bỏ tính năng này
  • Bản thay đổi này cũng bao gồm một kiểm thử bố cục đã bắt đầu toàn bộ bài viết này
  • Mozilla có một lỗi mà bạn nên theo dõi trạng thái của bản đồ nguồn trong bảng điều khiển tích hợp
  • Conrad Irwin đã viết một mã nguồn ngọc cực kỳ hữu ích cho tất cả người dùng Ruby
  • Một số nội dung đọc thêm về đặt tên evalthuộc tính displayName
  • Bạn có thể xem nguồn Trình biên dịch Closure để tạo bản đồ nguồn
  • Có một số ảnh chụp màn hình và nội dung thảo luận về việc hỗ trợ bản đồ nguồn GWT

Bản đồ nguồn là một tiện ích rất mạnh mẽ trong bộ công cụ của nhà phát triển. Việc có thể giữ cho ứng dụng web của bạn gọn nhẹ nhưng dễ gỡ lỗi là vô cùng hữu ích. Đây cũng là một công cụ học tập rất hữu ích cho các nhà phát triển mới để xem cách các nhà phát triển giàu kinh nghiệm cấu trúc và viết ứng dụng mà không phải tìm hiểu mã rút gọn không đọc được.

Bạn còn chờ gì nữa? Hãy bắt đầu tạo bản đồ nguồn cho tất cả dự án ngay!