初探 script 標籤
September 4, 2022要在瀏覽器中執行 JavaScript,乍看是件簡單的事,只要在HTML檔案中撰寫 script
標籤,並於標籤間撰寫 JavaScript 程式碼。例如:
<script>
let name = prompt('Input your name');
alert(`Hello! ${name}!`);
</script>
HTML 與 JavaScript
嗯?這樣是份完整的 HTML 嗎?若是現代瀏覽器,像這樣沒有自定義 html
、head
、body
等標籤的情況下,也會自動將撰寫的內容,放在 body
標籤之間,也就是相當於:
<html><head><script>
let name = prompt('Input your name');
alert(`Hello! ${name}!`);
</script></head><body></body></html>
prompt
與 alert
是瀏覽器上全域物件提供的函式,會出現提示方塊與顯示方塊,在瀏覽器中,全域物件就是 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>
document
是 window
全域物件上的特性,代表整份 HTML 文件,為 Document
的實例。如果 script
寫在 body
標籤中,執行 document
的 write
方法,會在目前文件解析點輸出指定文字,執行完 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,這邊先簡略說明一下,document
的 getElementById
,可以根據標籤 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
屬性指定檔案名稱,如果 script
的 type
是 "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 的編碼不同本身就是個問題來源;另一方面,script
的 type
屬性可以設定為 "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
標籤增加 async
與 defer
屬性,這兩個屬性只在透過 src
引用 .js 檔案才有作用,傳統內嵌程式碼的 script
標籤,會忽略 async
、defer
屬性,依舊依頁面的出現順序執行。
想要啟用 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
屬性,看看效果是否符合需求。