zaki work log

作業ログやら生活ログやらなんやら

[ブラウザJavaScript] htmlから<script src>したJavaScriptでさらにimportするときのエラーと対処法

htmlファイルから<script>JavaScriptソースコードを読み込み、そこから更にimportで外部ファイルに実装した処理を読み込みたい、という構成で数日ハマったのでメモ。

html/javascriptコード (before)

ブラウザで表示するhtmlは以下。

<!DOCTYPE html>
<html>
    <head>
        <title>import sample</title>
        <script src="main.js"></script>
    </head>

    <body>
        <h1 id="title">import sample</h1>
        <hr/>
        <button onclick="exec()">method</button>
    </body>
</html>

htmlから<script>で読んでいるJavaScriptコードが以下。

import { Person } from "./library.js";
function exec() {
    console.log("exec");

    const ayumu = new Person('ayumu', 17);
    ayumu.hello();
}

<script>を使って読んでいるソースから、さらにimportで読んでいる外部JavaScriptlibrary.jsが以下。

class Person {
    name;
    age;

    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    hello() {
        console.log('hello, my name is ' + this.name + '.');
        alert('hello, my name is ' + this.name + '.');
    }
}

JavaScriptの理解度は全然だけど多言語(C/C++/Java/Perl/Python...)の経験はそれなりにある…くらいの感覚だと文法エラーはなかったので動きそうな感じのする実装だけど、実際はこの状態でhtmlをブラウザで表示しても、以下のエラーが発生する。

Uncaught SyntaxError: Cannot use import statement outside a module (at main.js:1:1)

エラーと対応

Cannot use import statement outside a module エラー

一般的にこのエラーの原因と対処法は、JavaScriptを読むための<script src="...">type="module"を付与することで回避できる。

<!DOCTYPE html>
<html>
    <head>
        <title>import sample</title>
        <script src="main.js" type="module"></script>
    </head>

    <body>
        <h1 id="title">import sample</h1>
        <hr/>
        <button onclick="exec()">method</button>
    </body>
</html>

これでhtmlを再表示すると、次は以下のエラー

SyntaxError: The requested module './library.js' does not provide an export named 'Person' (at main.js:1:10)

The requested module './***.js' does not provide an export named '***' エラー

これはJavaScriptのルールのようなもの(多分)で、「importして使えるのはexportされている必要がある」というもの。
ここまでのコードでいうと、Personクラスを定義しているlibrary.jsimportしてるけど、Personクラスはexportされていないので、外部からは使えない、という状態になっている。 (プライベートクラスとは別と思うけど、結果的には似た感じ)

これを解決するには、Personクラスをexportしてやる。

export class Person {
    name;
    age;

    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    hello() {
        console.log('hello, my name is ' + this.name + '.');
        alert('hello, my name is ' + this.name + '.');
    }
}

これでhtmlを再読み込みすると、(ページのロード時点では)エラーが無くなる。

ではここで<button>タグで表示しているボタン押下してexec()関数をコール、そこからPersonクラスの操作を行おうとすると…

import/:11 Uncaught ReferenceError: exec is not defined at HTMLButtonElement.onclick (import/:11:34)

exec()関数の未定義エラーが発生する。

<script type="module">で読んでいるソース内の関数がonclickでnot definedエラー

type="module"をはずし、library.jsimportも削除すればもちろん動くが、type="module"を使ったソースファイルの読み込みを行うと、対象ソースファイル(この場合main.js)内の変数や関数は「モジュールスコープ」となり、他のソースファイルからは参照できなくなるのが原因。

これの対処方法は調べた限り2通り。

(対処法A) window.を付与したグローバルスコープで関数実装

window.を明示的に付与してグローバルスコープにすれば、onclickでコールできるようになる。
ただ、前述のようにfunction exec()のままだとwindow.を付けられない(よね?)ので、関数リテラルかアロー関数を使って関数でなく変数を定義してやる。 (しばらくわからなかったが、要はCの関数ポインタとか、Perlでも無名関数っていうやつ)

import { Person } from "./library.js";

window.exec = () => {
    console.log("exec");

    const ayumu = new Person('ayumu', 17);
    ayumu.hello();
}

これでhtmlを再表示しボタン押下すればexec()が実行され歩夢hello()が実行される。

(対処法B) buttonのイベントリスナをJavaScriptで行う

前述の対処法Aのアロー関数を実装するのと異なり、html内の<button>onclickするのでなくボタン押下時のイベントリスナをaddEventListenerでセットし、そこからexec()をコールする、というもの。

htmlとそこから<script type="module">で読むソースは以下の通り。

<!DOCTYPE html>
<html>
    <head>
        <title>import sample</title>
        <script src="main.js" type="module"></script>
    </head>

    <body>
        <h1 id="title">import sample</h1>
        <hr/>
        <button id="button">method</button>
    </body>
</html>
import { Person } from "./library.js";

const exec = () => {
    console.log("exec");

    const ayumu = new Person('ayumu', 17);
    ayumu.hello();
}

document.getElementById('button').addEventListener('click', ()=> {
    exec();
});

これで対処法Aと同様に動作する。

ポイント

  • importする対象はexportされてなければならない
  • importを使ってるJavaScriptをhtmlから読むときはtype="module"が必要
  • type="module"で読んだソースは「モジュールスコープ」になり関数を外からコールできない

その他のアプローチ

(案1) html側で必要なソースの読み込みを全て行う

main.jslibrary.jsを参照するので、まずlibrary.jsから読み込む必要があるので、html内の<script>の記述は以下の通り。(importを使ってないのでtype="module"は不要)

<html>
    <head>
        <title>import sample</title>
        <script src="library.js"></script>
        <script src="main.js"></script>
    </head>
:
:

そしてmain.jsimportが不要になり、単にexec()を実装すればよく、Personクラスも使用可能。
記述内容自体は単純だが、読み込む順序や実装した関数名等の衝突に気を付ける必要がある。

(案2) dynamic import

async/awaitを組み合わせて、関数内で動的にimportすることも可能。

    <head>
        <title>import sample</title>
        <script src="main.js"></script>
    </head>

    <body>
        <h1 id="title">import sample</h1>
        <hr/>
        <button onclick="exec()">method</button>
    </body>
async function exec() {
    console.log("exec");
    const { Person } = await import("./library.js");

    const ayumu = new Person('ayumu', 17);
    ayumu.hello();
}

qiita.com

(案3) scriptエレメントを動的に生成 (2025.05.06追記)

zaki-hmkc.hatenablog.com

参考

elsammit-beginnerblg.hatenablog.com

zenn.dev