Phân tích chi tiết về thẻ script

Bài viết được dịch từ bài A Detailed Breakdown of the <script> Tag của tác giả Colin Ihrig.


Khi thẻ <script> ban đầu được giới thiệu, nó được dùng để thêm các tương tác cơ bản vào trang web. Nhưng web đã thay đổi rất nhiều, và thẻ <script> đã tiến hóa. Sự phát triển của JavaScript đã khiến cho nó trở thành một trong những thẻ HTML quan trọng nhất. Bài viết này sẽ khám phá chi tiết về thẻ <script>, bao gồm những cách thực hành tốt nhất khi sử dụng nó trong các trang web hiện đại.

Chỉ định ngôn ngữ script

JavaScript là ngôn ngữ script mặc định của web nhưng trình duyệt cũng hỗ trợ các ngôn ngữ khác. Ví dụ, Internet Explorer cũng hỗ trợ một ngôn ngữ script kế thừa từ Visual Basic tên là VBScript. Để chỉ định ngôn ngữ script được sử dụng, thuộc tính "type" của thẻ <script> sẽ được dùng. Về mặt kỹ thuật, "type" chỉ định kiểu MIME của ngôn ngữ. Ví dụ sau sử dụng "type" để chỉ định kiểu MIME của JavaScript, "text/javascript":

<script type="text/javascript">
  // JavaScript code here
</script>

Ở các phiên bản cũ của HTML, giá trị của "type" cần được chỉ định. Tuy nhiên, bắt đầu từ HTML5, "type" sẽ có giá trị mặc định là "text/javscript". Có điều này bởi W3C nhận ra rằng JavsScript là ngôn ngữ script duy nhất được hỗ trợ một cách rộng rãi. Do vậy, thẻ <script> của HTML5 có thể được viết gọn lại như sau:

<script>
  // JavaScript code here
</script>

Thuộc tính "language"

Có một thuộc tính thứ 2, "language", được sử dụng để chỉ định ngôn ngữ script. Không giống như "type", các giá trị được chấp nhận của "language" không bao giờ được chuẩn hóa. Thuộc tính này đã bị bãi bỏ từ lâu, nhưng một số người vẫn dùng nó. Nó không nên được dùng trong bất kỳ hoàn cảnh nào.

Script inline và external

Thẻ <script> cho phép code được nhúng trực tiếp vào HTML hoặc thêm từ file script ngoài. Script inline được tạo ra bằng cách viết code giữa cặp thẻ <script> và </script> trong file HTML. Đoạn code sau mô tả một ví dụ về inline <script>:

<script>
  // Inline JavaScript code here
</script>

Script inline dễ đọc và dễ thêm vào trang web. Tuy nhiên những đoạn script lớn có thể khiến file HTML lộn xộn. Có một giải pháp thay thế là thêm code từ file script ngoài. Các file bên ngoài được tham chiếu đến qua các URL được chỉ định bởi thuộc tính "src". Ví dụ sau môt tả cách thêm file script ngoài. Trong ví dụ này, file script ngoài có tên là external.js và được đặt cùng thư mục với file HTML. Chú ý là bắt buộc phải có thẻ </script>

<script src="external.js"></script>

Tương thích với XHTML

Quy tắc của XHTML nghiêm ngặt hơn HTML rất nhiều. Khi các ký tự đặc biệt (như & và <) được dùng trong script của file XHTML, chúng sẽ gây ra lỗi. Cách giải quyết đơn giản nhất là dùng file script ngoài. Tuy nhiên, nếu bạn buộc phải viết script inline, bạn sẽ phải thêm phần CDATA (character data) vào file của bạn. Với phần CDATA, các kí tự đặc biệt có thể được sử dụng thoải mái. Ví dụ sau sử dụng phần CDATA tương thích với cả XHTML và HTML. Chú ý là XHTML bắt buộc dùng thuộc tính "type":

<script type="text/javascript">
//<![CDATA[
  alert((1 < 2) && (3 > 2));
//]]>
</script>

Chèn thẻ script động

Chèn thẻ script động là kỹ thuật trong đó các thẻ <script> được tạo ra trong lúc trang web hoạt động. Khi thẻ <script> mới được thêm vào trang, file được chỉ định bởi URL trong thuộc tính "src" sẽ được tự động tải và chạy. Code sau mô tả ví dụ về việc chèn thẻ script động. Trong ví dụ này, một thẻ <script> mới được tạo ra bằng hàm document.createElement(). Sau đó, thuộc tính "src" được gán bằng URL của file script. Cuối cùng, thẻ <script> mới này được thêm vào head của document, khiến file script được tải và chạy.

var script = document.createElement("script");

script.setAttribute("src", url);
document.head.appendChild(script);

Ứng dụng

Thẻ <script> không tuân theo chính sách chung origin. Trang web có thể tận dụng điều này để lấy dữ liệu từ trang khác (cross-site). JSONP, một công nghệ giúp tạo các request cross-site kiểu AJAX, sử dụng rộng rãi kỹ thuật chèn thẻ script động. Trong mô hình JSONP, mỗi request AJAX được thay thế bằng một thẻ script được chèn động.

Thuộc tính "async"

Khi gặp thẻ <script>, trình duyệt dừng các công việc đang thực hiện và bắt đầu tải/chạy script đó. Hành vi mặc định này được biết đến là chặn đồng bộ (synchronous blocking). Trong quá trình chặn này, trang web sẽ không có phản ứng hoặc chậm đi. Để giảm thiểu vấn đề này, HTML5 giới thiệu thuộc tính "async" của thẻ <script>. "async" là một thuộc tính Boolean, khi được chỉ định, cho biết script này không chặn việc đọc phần còn lại của trang và chạy bất đồng bộ. Theo đặc tả, "async" nên được dùng với các file script ngoài. Không may là "async" chưa được Opera hỗ trợ. Ví dụ sau cho thấy cách thuộc tính "async" được dùng:

<script src="file.js" async="async"></script>

Thông thường, "async" có giá trị mặc định là false. Tuy nhiên khi xử lý với các thẻ script được chèn động, giá trị này trở thành true. Điều này gây ra vấn đề nếu các script phụ thuộc lẫn nhau được chèn vào cùng lúc. Ví dụ, giả sử trang web chèn một thư viện thứ 3 được lưu ở một server chậm. Cùng lúc, trang web cũng chèn một script chứa lời gọi đến thư viện này. Do script chạy bất đồng bộ, file thứ 2 có khả năng được tải và chạy trước thư viện kia. Giải pháp là đặt giá trị của "async" của cả 2 script thành false. Trong ví dụ sau, trang sẽ chờ script đầu tiên được nạp trước khi tiếp tục với script tiếp theo:

var library = document.createElement("script");
var local = document.createElement("script");

library.async = false;
local.async = false;
library.setAttribute("src", "remote-library-code.js");
local.setAttribute("src", "local-file.js");
document.head.appendChild(library);
document.head.appendChild(local);
// use the library code

Thuộc tính "defer"

Như đã nói ở trên, thẻ <script> khiến trình duyệt chặn phần còn lại của trang khi script được xử lý. Điều này sẽ gây ra vấn đề nếu script tham chiếu đến phần tử trong DOM và các phần tử này chưa được nạp. Trong bối cảnh đó, code động tới DOM thường được đặt trong một event handler như window load hay DOMContentLoaded. Một cách khác là trì hoãn việc chạy script đến khi document đã được trình duyệt phân tích xong bằng cách sử dụng thuộc tính "defer". Giống như "async", "defer" là thuộc tính Boolean chỉ nên sử dụng với các file script ngoài. Ví dụ sau mô tả cách "defer" hoạt động. Ví dụ này cần một file HTML và một file JavaScript riêng tên là defer.js. Mã HTML được hiển thị bên dưới:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>defer Example</title>
    <meta charset="UTF-8" />
    <script src="defer.js" defer="defer"></script>
  </head>
  <body>
    <span id="message"></span>
  </body>
</html>

Code của defer.js được hiển thị bên dưới. Nếu không có thuộc tính "defer", code sẽ lỗi do DOM chưa sẵn sàng khi thẻ <span> được truy cập đến.

var span = document.getElementById("message");

span.textContent = "The DOM is ready for scripting!";

Cân nhắc về hiệu năng

Tất cả các ví dụ trong bài viết này đều đặt thẻ <script> trong thẻ head của document. Điều này có thể làm giảm hiệu năng vì phần còn lại của trang sẽ bị chặn trong khi script được xử lý. Chắc chắn các thuộc tính "defer" và "async" có thể được sử dụng. Không may là chúng chưa được hỗ trợ ở tất cả các trình duyệt. Cách thứ 2 là chuyển các thẻ script xuống dưới cuối cùng của thẻ <body> khi có thể - điều này ổn chừng nào script không gọi hàm document.write().

Dùng file script ngoài thay vì script inline là một kỹ thuật khác giúp cải thiện hiệu năng. Việc này khiến file HTML nhỏ hơn, nhưng làm tăng chi phí do tạo thêm request HTTP. Tuy nhiên, các file script thường được sử dụng chung ở nhiều trang HTML và có thể được lưu đệm bởi trình duyệt. Kết quả cuối cùng là file HTML nhỏ hơn và không có thêm request đến server. Và cũng nên nhớ rằng script có thể được tải về sau khi trang đã tải xong.

Những điều cần nhớ

  • Thẻ <script> luôn phải có thẻ đóng </script>
  • Ngôn ngữ script được chỉ định bởi thuộc tính "type". Không sử dụng thuộc tính "language". Thuộc tính "type" có giá trị mặc định là "text/javascript" ở HTML5.
  • Các file XHTML nên dùng file script ngoài hoặc phần CDATA để sử dụng ký tự đặc biệt.
  • Các thẻ script được chèn động có thể request các tài nguyên từ trang khác (cross-site)
  • Thuộc tính "async" khiến cho script chạy bất đồng bộ. Nó cũng có thể được dùng để bảo toàn thứ tự của các script được chèn động.
  • Thuộc tính "defer" có thể trì hoãn việc chạy script cho đến khi document sẵn sàng.
  • Chuyển các thẻ <script> xuống cuối cùng của thẻ <body> khi có thể.
  • Sử dụng các file script ngoài, các file này có thể được lưu đệm và sử dụng ở nhiều trang.