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

R Ryan Seddon

Có bao giờ bạn mong muốn rằng mã phía máy khách của bạn có thể đọc đượ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 kích thước mã mà không làm ảnh hưởng đến hiệu suất? Giờ đây, bạn có thể sử dụng sự kỳ diệu của bản đồ nguồn.

Bản đồ nguồn là 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 được tạo. Khi tạo bản dựng cho sản xuất, cùng với việc rút gọn và kết hợp các tệp JavaScript, bạn 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 bạn truy vấn một dòng và số cột nhất định trong JavaScript đã tạo, bạn có thể thực hiện thao tác tra cứu trong bản đồ nguồn sẽ trả về vị trí ban đầu. Các công cụ dành cho nhà phát triển (hiện tại là các bản dựng Truy cập nhanh vào ban đêm, 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à làm cho bản đồ xuất hiện như thể bạn đang chạy các tệp chưa được kết hợp 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 vùng văn bản có 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 số dòng và cột được tạo, đồng thời trả về vị trí trong mã ban đầu. 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 Mozilla JavaScript trong thực tế.

Thế giới thực

Trước khi xem cách triển khai thực tế sau đây của Bản đồ nguồn, hãy đảm bảo rằng bạn đã bật tính năng bản đồ nguồn trong Chrome Canary hoặc ASan hằng đê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ụ dành cho nhà phát triển Tin bài.

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

Cách bật bản đồ nguồn trong công cụ dành cho nhà phát triển của 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/chưa kết hợp, nhưng tương lai rất khả quan với các cuộc thảo luận về các ngôn ngữ biên dịch thành 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ữ đó được hỗ trợ sẵn trong trình duyệt với bản đồ nguồn:

  • CoffeeScript
  • ECMAScript 6 trở lên
  • SASS/LESS và các loại 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 này 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 Bản đồ nguồn. Ray Cromwell của 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 trong thực tế.

Một ví dụ khác mà tôi đã tổng hợp 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 ES3. Trình biên dịch Traceur cũng tạo một bản đồ nguồn. Vui lòng xem bản minh hoạ về trait và lớp ES6 đang được sử dụng theo cách giống như chúng được hỗ trợ sẵn trong trình duyệt, nhờ 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 sẽ được biên dịch nhanh chóng và tạo 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 liên kết nguồn trong thực tế

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 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 sau.) Khi bạn đã kết hợp và rút gọn JavaScript của mình, cùng với nó sẽ có một tệp bản đồ nguồn.

Hiện tại, trình biên dịch Đóng không thêm nhận xét đặc biệt ở cuối cần thiết để 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ó bản đồ nguồn:

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

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

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

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

Thích nhận xét này sẽ cho người 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ợ ghi chú một dòng.

Ví dụ về bản đồ nguồn ở trạng thái 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ác công cụ cho nhà phát triển. Bạn cũng sẽ cần tải tệp gốc lên để các công cụ cho nhà phát triển có thể tham khảo và hiển thị chúng khi cần.

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

Bạn sẽ cần 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. 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

2 cờ lệnh quan trọng là --create_source_map--source_map_format. Đây là yêu cầu bắt buộc vì phiên bản mặc định là V2 và chúng ta 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 lock và trình bày chi tiết hơn về cách hoạt động của phần "ánh xạ". Ví dụ sau đây là một khác biệt nhỏ 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 bản đồ nguồn là một giá trị cố định đối tượng chứa nhiều thông tin thú vị:

  • Số phiên bản của bản đồ nguồn
  • Tên tệp của mã được tạo (Tệp sản xuất được kết hợp/thu nhỏ của bạn)
  • sourceRoot cho phép bạn thêm cấu trúc thư mục vào trước nguồn. Đâ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
  • tên chứa tất cả tên biến/phương thức xuất hiện trong toàn bộ mã của bạn.
  • 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ị Base64 VLQ. Quá trình tiết kiệm dung lượng thực sự đã hoàn tất tại đây.

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

Vậy chúng giảm kích thước như thế nào mà vẫn duy trì được các ánh xạ phức tạp?

VLQ (Số lượng độ dài có thể thay đổi) được dùng cùng với việc mã hoá giá trị thành giá trị Base64. Thuộc tính liên kết 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 được 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 phân đoạn này là 1, 4 hoặc 5 trong các trường có độ dài thay đổi. Một số đoạn mã có thể xuất hiện dài hơn nhưng những đoạn mã này lại chứa các đoạn tiếp theo. 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 đó.

Phân tích 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 thay đổi là 1, 4 hoặc 5. Sơ đồ này được coi là có độ dài thay đổi là 4 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 bản đồ nguồn hoạt động như thế nào đối với vị trí ban đầu.

Các giá trị hiển thị ở trên hoàn toàn là các giá trị đã giải mã Base64. Bạn cần thực hiện thêm một số bước xử lý để nhận được các giá trị đúng. Mỗi phân đoạn thường bao gồm 5 nội dung:

  • 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 phân đoạn nào cũng có tên, tên phương thức hoặc đối số, vì vậy, xuyên suốt các phân đoạn sẽ có độ dài biến đổi từ 4 đến 5 phân đoạn. Giá trị g trong sơ đồ 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ã Base64 VLQ. 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 cần phải lưu trữ số lớn, 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ơ đồ trên AAgBC 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 thuần tuý được giải mã trong Base64 là 1. Vì vậy, các giá trị quan trọng được sử dụng là 0, 0, 16, 1. Điều này sau đó cho chúng ta biết rằng dòng 1 (các dòng được giữ bằng dấu chấm phẩy) cột 0 của tệp được tạo ánh xạ thành tệp 0 (mảng tệp 0 là foo.js), dòng 16 ở cột 1.

Để cho thấy cách giải mã các phân đoạn, tôi sẽ tham chiếu 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ụ dành cho nhà phát triển Tin nhắn web, 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 nắm được kiến thức cơ bản về toán tử bitwise và cách thức hoạt động của thông số kỹ thuật cho việc ánh xạ nguồn. Chữ số trước đó, g, bị gắn cờ là bit tiếp tục bằng cách so sánh chữ số (32) và VLQ_CONTINUATION_BIT (tệp 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

Hàm này trả về 1 ở mỗi vị trí bit mà cả hai đều xuất hiện. Vì vậy, giá trị đã 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 đó, giá trị dịch chuyển của bit sẽ tăng thêm 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, vì vậy dịch chuyển trái 1 (B) bằng 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 dịch chuyển số (32) sang phải một điểm.

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

Vậy là chúng ta có nó: đó là cách bạn chuyển 1 thành 16. Việc này có vẻ là một quá trình phức tạp, nhưng khi con số bắt đầu lớn hơn thì điều 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 đề bao gồm tập lệnh trên 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 dòng đầu tiên của bản đồ nguồn để cố ý vô hiệu hoá JavaScript và xảy ra lỗi cú pháp. Các công cụ dành cho nhà phát triển Tin nhắn theo thời gian thực có thể xử lý vấn đề này.

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

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

Cách hoạt động của sourceURLdisplayName: Hàm Eval và hàm ẩn danh

Mặc dù không thuộc thông số bản đồ nguồn, nhưng 2 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 trường dữ liệu 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 của bản đồ nguồn V3. Bằng cách thêm chú thích đặc biệt sau đây vào mã (sẽ bị đánh giá) này, bạn có thể đặt tên cho các trường hợp để chúng xuất hiện dưới dạng tên hợp lý hơn trong các công cụ cho nhà phát triển của bạn. Hãy xem một bản minh hoạ đơn giản bằ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ông cụ dành cho nhà phát triển

Trình trợ giúp còn lại cho phép bạn đặt tên cho các hàm ẩn danh bằng cách 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.

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ị thuộc tính displayName trong hành động.

Khi phân tích 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ì (anonymous). Tuy nhiên, displayName đã chết khá nhiều và sẽ không được đưa vào Chrome. Tuy nhiên, mọi hy vọng vẫn không mất đi và chúng tôi đã đề xuất một đề xuất tốt hơn nhiều có tên là debugName.

Khi viết tên eval chỉ có sẵn trong trình duyệt Firefox và Metric. Thuộc tính displayName chỉ có trong các phiên bản web tối của tin.

Cùng tập hợp

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 sẽ được thêm vào CoffeeScript. 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 đối với CoffeeScript và những người theo dõi tận tâm của nó.

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

Rất nhiều tools tạo bản đồ nguồn, bao gồm cả trình biên dịch tập lệnh cà phê. Hiện tại, tôi xem đây là vấn đề cần cân nhắc.

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

Chưa hoàn hảo

Một điều bản đồ nguồn hiện không phục vụ cho việc này là biểu thức đồng hồ. 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ề bất cứ 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 sẽ yêu cầu một số loại ánh xạ ngược để tra cứu tên thực của đối số/biến 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 trên bản đồ nguồn, chúng ta có thể bắt đầu thấy một số tính năng thú vị và độ ổn định tốt 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ối bên ngoài CDN chính thức. Hướng dẫn 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 cam kết để 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 rút ra 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 xem:

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 giữ cho ứng dụng web của bạn tinh gọn 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 mạnh mẽ dành cho các nhà phát triển mới hơn để xem cách các nhà phát triển có kinh nghiệm cấu trúc và viết ứng dụng của họ mà không phải xem các đoạn mã rút gọn không đọ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!