script 標籤與模組


如果瀏覽器支援 ES6 模組(可參考〈ECMAScript modules in browsers〉),例如 Chrome 61 之後,可以支援 ES6 模組,那麼可以將 <script>type 屬性設定為 "module",表示這是一個 ES6 模組,例如:

<script type="module" src="js/util.js"></script>

type 設定為 "module" 時,<script> 也會是 defer 的,因此指定的 .js 會以非同步方式下載,並在 DOM 樹生成與其他非 defer 的 .js 執行完後才(依序)執行。

<script type="module"></script> 之間也可以撰寫 JavaScript 程式碼,也才可以撰寫 import 語句,例如,如果有個 hello.js 作為 ES6 模組:

function hello(name) {
    return `Hello! ${name}!`;
}

export {hello};

可以撰寫底下的 HTML,必須注意的是,在瀏覽器中,模組的 import from 中,必須加上 .js 副檔名:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="module">
        import {hello} from './hello.js';
        window.onload = function() {
            let welcome = document.getElementById('welcome');
            welcome.innerHTML = hello(prompt('Input your name'));
        };
    </script>
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

若程式碼中有 import 語句時,瀏覽器以非同步方式下載模組,若有多個 import 語句,全部下載完成後再依 import 的順序執行,然後才執行模組頂層的程式碼。

無論 import 幾次,或者 <script type="module" src="./hello.js"></script> 多次,模組都只會被載入與執行一次。

type 被設為 "module"<script></script> 中,可以使用 type 被設為 "text/javascript"<script></script> 中的程式相關定義:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="text/javascript">
        function hello(name) {
            return `Hello! ${name}!`;
        }
    </script>
    <script type="module">
        window.onload = function() {
            let welcome = document.getElementById('welcome');
            welcome.innerHTML = hello(prompt('Input your name'));
        };
    </script>
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

按此看執行結果

然而,反過來不行:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="module">
        function hello(name) {
            return `Hello! ${name}!`;
        }
    </script>
    <script type="text/javascript">
        window.onload = function() {
            let welcome = document.getElementById('welcome');
            welcome.innerHTML = hello(prompt('Input your name')); // ReferenceError
        };
    </script>
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

兩個 type"module"<script></script>,被視為兩個獨立的模組,因此也沒辦法直接取用定義:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="module">
        function hello(name) {
            return `Hello! ${name}!`;
        }
    </script>
    <script type="module">
        window.onload = function() {
            let welcome = document.getElementById('welcome');
            welcome.innerHTML = hello(prompt('Input your name')); // ReferenceError 
        };
    </script>
</head>
<body>
    <span id="welcome"></span>
</body>
</html>

當然,如果可以使用 type = "module",會有一個主要的模組,在該模組中使用 import,如果真的有需求,必須從 type = "text/javascript"<script></script> 中使用 ES6 的模組的話,是有些 ES6 Module Loader 的 polyfill 程式庫可以使用。

另一個方式是放棄使用 ES6 模組,一個可以處理 type = "module" 的瀏覽器,會忽略 <script nomodule></script>,因此,當這麼撰寫時:

<script type="module" src="js/util.js"></script>
<script nomodule src="js/util_fallback.js"></script>

支援 ES6 模組的瀏覽器會使用 util.js,而不支援的瀏覽器會使用 util_fallback.js。