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

Ryan Seddon

Bạn đã bao giờ ước mình có thể 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à giảm thiểu mã mà không ảnh hưởng đến hiệu suất không? Giờ đây, bạn có thể nhờ sự kỳ diệu của bản đồ nguồn.

Bản đồ nguồn là một cách để ánh xạ một tệp kết hợp/đã rút gọn trở lại trạng thái chưa xây dựng. Khi tạo bản dựng cho phiên bản chính thức, cùng với việc giảm kích thước 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 của mình. Khi truy vấn một số dòng và cột nhất định trong JavaScript đã tạo, bạn có thể thực hiện tra cứu trong bản đồ nguồn để trả về vị trí ban đầu. Công cụ dành cho nhà phát triển (hiện tại là bản dựng hàng đêm WebKit, Google Chrome hoặc Firefox 23+) có thể tự động phân tích cú pháp bản đồ nguồn và làm cho bản đồ xuất hiện như thể bạn đang chạy các tệp đơn giản và không kết hợp.

Bản minh hoạ cho phép bạn nhấp chuột phải vào bất cứ đâu trong vùng văn bản chứa nguồn được tạo. Chọn "Lấy vị trí gốc" sẽ truy vấn bản đồ nguồn bằng cách chuyển vào dòng và số cột được tạo và trả về vị trí trong mã gốc. Hãy đảm bảo bảng điều khiể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 trong thực tế.

Thế giới thực

Trước khi bạn xem triển khai thực tế sau đây của Bản đồ nguồn, đảm bảo bạn đã bật tính năng bản đồ nguồn trong Chrome Canary hoặc WebKit ban đêm bằng cách nhấp vào 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 và chọn tuỳ chọn "Bật bản đồ nguồn".

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

Firefox 23+ có bản đồ nguồn được bật theo mặc định trong 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. Tuy nhiên, tương lai sẽ rất tươi sáng với các cuộc thảo luận về ngôn ngữ biên dịch sang JavaScript như CoffeeScript và thậm chí là khả năng hỗ trợ thêm các bộ tiền xử lý CSS như SASS hoặc LESS.

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

  • CoffeeScript
  • ECMAScript 6 trở lên
  • SASS/ÍT và các nguồn khác
  • Khá nhiều ngôn ngữ biên dịch thành JavaScript

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

Bộ công cụ web của Google (GWT) gần đây đã thêm hỗ trợ cho Source Maps. Ray Cromwell của nhóm GWT đã thực hiện một bản ghi màn hình tuyệt vời thể hiện sự hỗ trợ của bản đồ nguồn trong thực tế.

Một ví dụ khác mà tôi đã tổng hợp lại là sử dụng thư viện Traceur của Google. Thư viện này cho phép bạn viết ES6 (ECMAScript 6 hoặc Next) và biên dịch 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 giống như chúng được hỗ trợ vốn có trong trình duyệt, nhờ vào bản đồ nguồn.

Vùng văn bản trong bản minh hoạ cũng cho phép bạn viết ES6, mã này sẽ được biên dịch nhanh chóng và tạo một bản đồ nguồn cùng với mã ES3 tương đương.

Gỡ lỗi Traceur ES6 bằng bản đồ nguồn.

Bản minh hoạ: Viết ES6, gỡ lỗi, xem quá trình ánh xạ nguồn trong thực tế

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

Trình biên dịch/trình rút gọn JavaScript duy nhất hiện có hỗ trợ việc tạo bản đồ nguồn là trình biên dịch Đóng. (Tôi sẽ giải thích cách sử dụng tính năng này ở phần sau.) Khi bạn đã kết hợp và giảm thiểu JavaScript của mình, cùng với JavaScript sẽ tồn tại một tệp bản đồ nguồn.

Hiện tại, trình biên dịch Packaging không thêm nhận xét đặc biệt vào cuối, điều này là bắt buộc để biểu thị cho các công cụ dành cho nhà phát triển của trình duyệt rằng có sẵn bản đồ nguồn:

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

Điều này cho phép nhà phát triển các công cụ ánh xạ các lệnh gọi về vị trí của họ 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ều đó và nhận xét biên dịch có điều kiện của IE, quyết định đã được đưa ra thay đổi thành //#. Hiện tại, Chrome Canary, WebKit Nightly và Firefox 24 trở lên hỗ trợ tính năng nhận xét mới. Việc thay đổi cú pháp này cũng ảnh hưởng đến sourceURL.

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

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

Giống như nhận xét, thao tác này sẽ cho người tiêu dùng bản đồ nguồn của bạn biết nơi cần tìm bản đồ nguồn được liên kết với tệp JavaScript. Tiêu đề này cũng xoay quanh 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ụ của WebKit Devtools 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ụ cho nhà phát triển của mình. Bạn cũng sẽ cần phải tải lên các tệp gốc của mình để công cụ cho nhà phát triển có thể tham khảo và hiển thị chúng khi cần thiết.

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

Bạn sẽ cần phải sử dụng trình biên dịch đóng để rút gọn, nối và tạo bản đồ nguồn cho các tệp JavaScript của mình. Lệnh này có dạng 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. Điều này là bắt buộc 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.

Phân tích thành phần 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 sẽ được tạo bởi trình biên dịch Đóng, đồng thời tìm hiểu chi tiết hơn về cách hoạt động của mục "ánh xạ". Ví dụ sau đây là một sự khác biệt nhỏ so với ví dụ về quy cách của phiên bản 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 chứa nhiều thông tin hấp dẫn:

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

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

Ban đầu, thông số bản đồ nguồn có kết quả 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 gấp khoảng 10 lần kích thước của mã được tạo. Phiên bản hai đã giảm khoảng 50% và phiên bản ba lại giảm thêm 50%, vì vậy, đối với tệp 133kB, bạn sẽ có bản đồ nguồn ~ 300kB.

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 độ dài thay đổ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 là 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 phân đoạn trong dòng đó. Mỗi đoạn trong số này là 1, 4 hoặc 5 trong các trường độ dài thay đổi. Một số URL có thể xuất hiện dài hơn nhưng các mã này 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 có liên quan đến các phân đoạn trước đó.

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

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

Các giá trị được trình bày ở trên chỉ là các giá trị được giải mã Base64, có một số quy trình xử lý khác để có được giá trị thực của chúng. Mỗi phân đoạn thường bao gồm năm điều:

  • Cột đã tạo
  • Tệp gốc xuất hiện trong tệp này
  • Số dòng ban đầu
  • Cột ban đầu
  • Và tên gốc, nếu có

Không phải mọi phân đoạn đều có tên, tên phương thức hoặc đối số, do đó các phân đoạn sẽ chuyển từ độ dài 4 đến 5 biến. Giá trị g trong biểu đồ phân đoạn ở trên được gọi là bit tiếp tục, bit này cho phép tối ưu hoá hơn nữa trong giai đoạn giải mã VLQ Base64. Bit tiếp tục cho phép bạn tạo dựa trên giá trị phân đoạn để có thể lưu trữ các số lớn mà không phải lưu trữ một số lớn. Đây là một kỹ thuật tiết kiệm không gian rất thông minh có nguồn gốc 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ị sau là 16. B được giải mã đơn thuần trong Base64 là 1. Vậy các giá trị quan trọng thường dùng là 0, 0, 16, 1. Sau đó, điều này cho 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 ánh xạ đến tệp 0 (mảng tệp 0 là foo.js), dòng 16 ở cột 1.

Để hiển thị cách các phân đoạn được giải mã, tôi sẽ tham khảo 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ụ nhà phát triển WebKit, cũng được viết bằng JavaScript.

Để hiểu đúng cách nhận giá trị 16 từ B, chúng ta cần có hiểu biết cơ bản về các toán tử bitwise và cách dùng thông số kỹ thuật để ánh xạ nguồn. Chữ số đứng trước, g, được gắn cờ là bit tiếp tục bằng cách so sánh chữ số (32) và VLQ_CONTINUATION_BIT (nhị phân 100000 hoặc 32) bằng cách sử dụng toán tử bitwise AND (&).

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

Thao tác này sẽ trả về giá trị 1 ở mỗi vị trí bit nơi cả hai cùng xuất hiện. 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ị chuyển dịch bit lên 5 cho mỗi bit tiếp tục trước đó. Trong trường hợp trên, nó chỉ dịch chuyển 5 lần một lần, do đó dịch chuyển sang trái 1 (B) 5.

1 <<../ 5 // 32

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

Sau đó, giá trị đó được chuyển đổi từ giá trị đã ký VLQ bằng cách di chuyển sang phải số (32) một điểm.

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

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

Các vấn đề có thể xảy ra với XSSI

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

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

Như đã trình bày ở trên, ba 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 và nếu có thì sẽ xóa tất cả các ký tự dẫn đến thực thể dòng mới đầu tiên (\n).

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

Mặc dù không phải là một phần của thông số bản đồ nguồn, nhưng hai quy ước sau 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 ẩn danh và evals.

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 của bản đồ nguồn V3. Bằng cách bao gồm nhận xét đặc biệt sau đây trong mã của bạn (sẽ bị loại bỏ), bạn có thể đặt tên cho evals để chúng xuất hiện dưới dạng tên hợp lý hơn trong công cụ cho nhà phát triển của bạn. Hãy xem bản minh hoạ đơn giản về cách sử dụng trình biên dịch CoffeeScript:

Bản minh hoạ: Xem mã của eval() hiển thị dưới dạng tập lệnh qua sourceURL

//# sourceURL=sqrt.coffee
Nhận xét đặc biệt sourceURL trông như thế nào trong các công cụ cho nhà phát triển

Trình 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 đó. Phân tích tài nguyên bản minh hoạ sau đây để 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);
Đang hiển thị tài sản displayName đang hoạt động.

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

Khi viết tên eval, chỉ có sẵn trong trình duyệt Firefox và WebKit. Thuộc tính displayName chỉ nằm trong các đêm WebKit.

Hãy cùng nhau tập hợp

Hiện tại, việc hỗ trợ bản đồ nguồn đang được thêm vào CoffeeScript rất dài. Hãy kiểm tra vấn đề và hỗ trợ thêm tính năng tạo bản đồ nguồn 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 nhiệt thành của dịch vụ này.

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

Rất nhiều tools tạo bản đồ nguồn, bao gồm cả trình biên dịch cà phê espresso. Tôi coi đây là một vấn đề đang gây tranh cãi.

Càng có nhiều công cụ có sẵn cho chúng tôi để có thể tạo bản đồ nguồn, chúng tôi sẽ càng tốt hơn, vì vậy hãy tiếp tục và yêu cầu hoặc thêm hỗ trợ bản đồ nguồn vào dự án nguồn mở yêu thích của bạn.

Không hoàn hảo

Một thứ hiện tại của bản đồ nguồn chưa được hỗ trợ là biểu thức theo dõi. Vấn đề là việc cố gắng kiểm tra một đối số hoặc tên biến trong ngữ cảnh thực thi hiện tại sẽ không trả về kết quả nào vì nó không thực sự tồn tại. Điều này sẽ yêu cầu một số kiểu á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ú ý hơn về bản đồ nguồn, chúng ta có thể bắt đầu thấy được một số tính năng thú vị và cải thiện độ ổn định.

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 từ các CDN chính thức. Điều này cũng chỉ ra lỗi đặc biệt khi nhận xét biên dịch có điều kiện của IE (//@cc_on) được sử dụng trước khi jQuery tải. Kể từ đó, chúng tôi đã có một cam kết nhằm giảm thiểu điều này bằng cách gói sourceMappingURL trong một ghi chú nhiều dòng. Bài học cần học không sử dụng nhận xét có điều kiện.

Vấn đề này đã được giải quyết bằng việc thay đổi cú pháp thành //#.

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

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

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 gọn nhẹ nhưng dễ gỡ lỗi sẽ cực kỳ hữu ích. Đây cũng là một công cụ học tập rất mạnh mẽ dành cho các nhà phát triển mới để tìm hiểu cấu trúc và cách viết ứng dụng của những nhà phát triển có kinh nghiệm mà không cần phải tìm hiểu về các đoạn mã rút gọn không thể đọc được.

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