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で読んでいる外部JavaScriptのlibrary.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.jsをimportしてるけど、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.jsのimportも削除すればもちろん動くが、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.jsはlibrary.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.jsはimportが不要になり、単に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(); }