初探 script 標籤

September 4, 2022

要在瀏覽器中執行 JavaScript,乍看是件簡單的事,只要在HTML檔案中撰寫 script 標籤,並於標籤間撰寫 JavaScript 程式碼。例如:

<script>
    let name = prompt('Input your name');
    alert(`Hello! ${name}!`);
</script>

HTML 與 JavaScript

嗯?這樣是份完整的 HTML 嗎?若是現代瀏覽器,像這樣沒有自定義 htmlheadbody 等標籤的情況下,也會自動將撰寫的內容,放在 body 標籤之間,也就是相當於:

<html><head><script>
    let name = prompt('Input your name');
    alert(`Hello! ${name}!`);
</script></head><body></body></html>

promptalert 是瀏覽器上全域物件提供的函式,會出現提示方塊與顯示方塊,在瀏覽器中,全域物件就是 window 物件,代表瀏覽器視窗,為 Window 的實例,這類 API 不是由 JavaScript 規範,而是由瀏覽器提供。

如果想在 JavaScript 程式碼中撰寫中文呢?現代的Web應用程式,建議使用 UTF-8 編碼,若要撰寫中文,HTML 檔案只要儲存為 UTF-8,例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <script>
        let name = prompt('輸入你的名稱');
        alert(`哈囉!${name}!`);
    </script>
</body>
</html>

如果使用 UTF-8 作為 HTML 檔案編碼,<meta charset="utf-8"> 不是必要的,因為現代瀏覽器預設會使用 UTF-8 來讀取檔案。

瀏覽器也會假設 script 標籤中使用的是 JavaScript 語言,不過也可以使用 <meta> 來指定:

<meta http-equiv="Content-Script-Type" content="text/javascript">

HTML4 規範了 script 標籤的 type 屬性,必須設定它的值;然而,type 在 HTML5 為選用,若沒有指定,預設為 "text/javascript",若要指定 type 可以如下:

<script type="text/javascript">
    // 你的程式碼
</script>

script 標籤的 type 屬性,可以視需求指定其他值。例如:

  • "module"
  • "text/jscript"
  • "text/vbscript"
  • 其他自訂值

指定為 "module" 時,表示 JavaScript 程式碼內容為模組,type 屬性可以指定其他自訂值,通常這是為了嵌入自定義的腳本語言。

文件解析與 script 標籤

在瀏覽器載入 HTML 檔案後,必須先解析所有 HTML 標籤,過程中會按照頁面中定義的資訊下載資源,建立各標籤對應的 DOM(Document Object Model)物件並組成 DOM 樹,最後依 DOM 樹結構來呈現畫面。

預設情況下,瀏覽器在遇到 script 標籤時,會停止文件解析,先執行完標籤間定義的 JavaScript,再繼續之後的文件解析。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
      執行 JavaScript 前 … <br>
      <script>
          let name = prompt('輸入你的名稱');
          document.write(`哈囉!${name}!`);
      </script><br>
      執行 JavaScript 後 … 
</body>
</html>

documentwindow 全域物件上的特性,代表整份 HTML 文件,為 Document 的實例。如果 script 寫在 body 標籤中,執行 documentwrite 方法,會在目前文件解析點輸出指定文字,執行完 JavaScript 後,才繼續之後的文件解析,因此若你輸入了「Justin」,結果相當於產生了以下的 HTML:

<!DOCTYPE html>
<html><head>
    <meta charset="utf-8">
</head>
<body>
      執行 JavaScript 前 … <br>
      <script>
          let name = prompt('輸入你的名稱');
          document.write(`哈囉!${name}!`);
      </script>哈囉!Justin!<br>
      執行 JavaScript 後 … 
</body>
</html>

粗體字就是 document.write(...) 執行的輸出結果,就瀏覽器最後呈現的畫面來說,會看到以下的文字順序:

執行 JavaScript 前 … 
哈囉!良葛格!
執行 JavaScript 後 …

在過去,JavaScript 程式碼經常將 script 放在 head 標籤之間。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script>
        let name = prompt('輸入你的名稱');
        document.write(`哈囉!${name}!`);
    </script>
</head>
<body>
</body>
</html>

如果 JavaScript 程式碼沒有操作任何 DOM 物件,這種寫法是沒有問題,然而務必記得,瀏覽器處理 head 間的 JavaScript 時,還沒有解析 body 標籤,試圖操作 body 中標籤對應的 DOM 物件就會出錯。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script>
        let welcome = document.getElementById('welcome');
        let name = prompt('輸入你的名稱');
        welcome.innerHTML = `哈囉!${name}!`;
    </script>
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

範例中出現了 DOM API,這邊先簡略說明一下,documentgetElementById,可以根據標籤 id 屬性值取得對應的 DOM 物件,innerHTML 可用來設定實例的 HTML 內容。

在執行上例的 JavaScript 時,body 間的標籤還沒開始解析,span 對應的 DOM 物件還不存在,因此 welcome 變數的值會是 null,也就無從設定 innerHTML,程式因此出錯而沒有顯示任何內容。

如果 JavaScript 程式碼與操作DOM有關,想確定DOM樹已生成,可以將 script 標籤放在整份HTML文件之後,</body> 之前,因為 HTML 文件此時已經載入、解析完成,標籤對應的 DOM 物件已經產生,若 JavaScript 必須操作 DOM 元素,此時可以放心地進行操作。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <span id="welcome"></span>
    <script>
        let welcome = document.getElementById('welcome');
        let name = prompt('輸入你的名稱');
        welcome.innerHTML = `哈囉!${name}!`;
    </script>
</body>
</html>

HTML 網頁藉由瀏覽器呈現畫面,使用者操作畫面,操作畫面往往與事件處理有關,事件處理後續會討論,然而這邊要先談談 window.onload 事件,在 HTML 網頁的「全部資源」載入後會發生此事件,過去想在 head 間放 script,又要能操作 body 間對應標籤的 DOM 物件,經常可見以下的模式:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script>
        window.onload = function() {
            var welcome = document.getElementById('welcome');
            var name = prompt('輸入你的名稱');
            welcome.innerHTML = `哈囉!${name}!`;
        };
    </script>    
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

在解析 body 標籤前,script 標籤間的程式碼確實先執行了,只不過任務是在 window.onload 特性指定函式,這是傳統事件註冊方式;在 HTML 網頁定義的「全部資源」載入後會發生 onload 事件,這時就會呼叫 window.onload 指定的函式。

「全部資源」載入不單只是 DOM 樹建立完成,還包含了 HTML 頁面鏈結的 CSS、圖片等都載入完成,此時操作 DOM 物件當然就沒問題;不過,若需要下載的資源很多,或者是網路速度不佳,在使用者可以操作畫面前,等待的時間可能過長,這會造成使用者對應用程式的體驗不佳。

HTML5 標準化了 DOMContentLoaded 事件,會在 DOM 樹建立之後就觸發。

引用 .js 原始碼

JavaScript 程式碼可寫在 .js 檔案,若想在瀏覽器中執行這些檔案,可以在 HTML 檔案中使用 script 標籤的 src 屬性指定檔案名稱,如果 scripttype"text/javascript" 的話,src 引用的 .js 檔案來源也可以是其他網站。例如寫個 hello.js:

let welcome = document.getElementById('welcome');
let name = prompt('輸入你的名稱');
welcome.innerHTML = `哈囉!${name}!`;

如果 hello.js檔案放在 js 子資料夾中,可以在 HTML 網頁如下引用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <span id="welcome"></span>
    <script src="js/hello8.js"></script>
</body>
</html>

雖然程式都在 .js 中,但 <script></script> 還是要成對出現,對於具有 src 屬性的 script 標籤來說, <script></script> 間的程式碼會被忽略。

瀏覽器會載入 .js 檔案,若 src 是相對路徑,瀏覽器從 HTTP 伺服器下載 HTML,遇到 script 標籤時,也會從同一來源下載 js資料夾中 hello.js。

如果不想在網頁中遇到亂碼或其他編碼問題,請保持 .js 原始碼編碼與 HTML 檔案的編碼一致,如果 HTML 是 UTF-8,那麼 .js 檔案也應該設為 UTF-8,瀏覽器會假設 HTML 與 .js 編碼兩者是一致的。

在過去,確實是有個 charset 可以作為 script 標籤的屬性,用來決定載入 .js 時使用的編碼,然而,現在不建議使用 charset,HTML 與 .js 的編碼不同本身就是個問題來源;另一方面,scripttype 屬性可以設定為 "module",這個時候 charset 屬性是沒作用的。

noscript標籤

雖然說現今不支援 JavaScript 的瀏覽器幾乎不存在,不過瀏覽器上的 JavaScript 仍可能因一些原因無法使用(例如防毒軟體、安全機制等級、使用者禁用JavaScript等)。

若想在不支援 JavaScript 的情境下,仍可呈現基本畫面或資訊,可以使用 noscript 標籤,在 <noscript></noscript> 的內容,會在無法執行 JavaScript 時出現,提供替代的頁面內容。

不過,在不支援 JavaScript 的情境下,script 標籤的內容,可能會直接呈現出來,此時會使用 <!--與--> 註解,讓瀏覽器看不到 JavaScript 程式碼:

<script>
<!--

    // 你的程式...

//-->
</script>

對瀏覽器來說,script 標籤中的 <!-- 作用如同 JavaScript的 // 單行註解,因而 <!-- 不會發生執行錯誤,而 --> 前的 // 因為是單行註解,瀏覽器也就看不到之後的 --> 了。

async 與 defer

對於內嵌在 <script></script> 的原始碼,瀏覽器遇上 script 後,就是暫停頁面解析,執行程式碼後再繼續頁面解析,若頁面中有多個 script 標籤,就是依遇上的順序來暫停頁面解析、執行完程式碼後再繼續頁面解析。

如果 script 透過 src 引用外部 .js 檔案,瀏覽器會暫停頁面解析、「同步地」下載 .js 檔案,下載完成後執行程式碼,執行完成後再繼續頁面解析,也就是說,後續的程式執行、頁面解析、資源下載就會被阻斷,若網路速度不良,就會造成使用者的體驗不佳。

為了能進一步控制 .js 的下載與執行順序,HTML5為 script 標籤增加 asyncdefer 屬性,這兩個屬性只在透過 src 引用 .js 檔案才有作用,傳統內嵌程式碼的 script 標籤,會忽略 asyncdefer 屬性,依舊依頁面的出現順序執行。

想要啟用 async 的功能,不用為 async 加上任何值,只要出現 async 屬性就可以了。例如:

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

script 標籤加上 async 屬性,瀏覽器遇到 script 時,會「非同步地」下載 .js 檔案(使用瀏覽器的執行緒),下載完成前,不會阻斷後續資源的下載與頁面剖析;然而一旦被標示為 async 的 .js 下載完成,瀏覽器就會暫停頁面剖析,JavaScript 引擎執行 .js 的程式碼後,再繼續處理頁面剖析。

如果有多個 async 屬性的 .js,先下載完的就會先執行,然而因為檔案大小不一,網路狀況也不同,.js 下載完成的順序是無法預測的,因此 async 屬性的 script 在執行順序也就無法預測。

如果程式庫檔案容量比較大,然而與初始頁面處理沒有立即相關性,頂層程式碼不多或執行迅速,與其他程式庫也沒有相依性,就可以試著在 script 標籤上設置 async 屬性,看看效果是否符合需求。

想要啟用 defer 的功能,不用為 defer 加上任何值,只要出現這個屬性就可以了。例如:

<script defer src="xxx.js"></script>

script 標籤加上 defer 屬性,瀏覽器會「非同步地」下載 .js 檔案,不會阻斷後續資源下載與頁面剖析,下載完成後也不會馬上執行程式碼,而是在 DOM 樹生成、其他非 defer 的 .js 執行完後,才執行被加上 defer 屬性的 .js,如果有多個 defer 屬性的 .js,會按照「頁面上出現的順序」執行。

如果程式庫檔案容量比較大,必須在剖析完 DOM 之後執行,與其他程式碼間有順序上的相依性,(在缺少模組管理程式下)必須親自安排順序來依序執行,就可以試著在 script 標籤上設置 defer 屬性,看看效果是否符合需求。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter