Javascript ES wrapper module tkLibraries.wrapper.mjs による、複数javascriptライブラリの管理


はじめに

Web ページが増えてくると、各 HTML の <head> に多数の JavaScript ライブラリを並べて読み込む構成は管理が難しくなる。

たとえば従来は、次のように自作ライブラリと CDN ライブラリをページごとに列挙していた。

<script src="/js/tkDebug_le.js"></script>
<script src="/js/tkUtils_le.js"></script>
<script src="/js/tkCSS_le.js"></script>
<script src="/js/tkHTMLApplication_le.js"></script>
<script src="/js/tkfunctions/tkInsertHeader.js"></script>
<script src="/js/tkfunctions/tkSearch.js"></script>

<script src="https://cdnjs.cloudflare.com/.../xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/.../papaparse.min.js"></script>
...

この方式には、次の問題がある。

そこで、共通ライブラリの読み込みを 1 本の ES Module に集約する。

<script type="module" src="/js/tkLibraries.wrapper.mjs"></script>

この方法では、HTML 側から見ると共通ライブラリの入口は tkLibraries.wrapper.mjs だけになる。

ただし、現在の /js 以下の自作 JavaScript は本来の ES Module として書かれているわけではなく、従来型の classic script である。そのため今回の wrapper は、単純に

import "/js/tkUtils_le.js";

とするのではなく、従来型 JavaScript を <script> 要素として順番に読み込む「ES Module 製ローダー」として実装している。

この文書では、この構成の考え方、tkLibraries.wrapper.mjs の仕様、HTML 側での利用方法、既存 HTML からの移行方法をまとめる。


全体構成

現在の基本構成は次のようになる。

HTML
 |
 +-- <script type="module"
 |      src="/js/tkLibraries.wrapper.mjs">
 |
 +-- <script nomodule
 |      src="/js/unsupportedBrowser.js">
 |
 +-- ページ固有の HTML
 |
 +-- <div id="..."></div>
     <script>
       tk-libraries-ready を待って
       ページ固有処理を実行
     </script>

tkLibraries.wrapper.mjs の内部では、従来型 JavaScript と CDN ライブラリを順番に読み込む。

tkLibraries.wrapper.mjs
 |
 +-- tkDebug_le.js
 +-- tkUtils_le.js
 +-- XLSX
 +-- PapaParse
 +-- js-beautify
 +-- highlight.js
 +-- encoding-japanese
 +-- tkCSS_le.js
 +-- tkHTMLApplication_le.js
 +-- tkInclude.js
 +-- tkInsertHeader.js
 +-- tkSearch.js
 +-- event_js.js
 |
 +-- DOMContentLoaded を待つ
 |
 +-- 共通 UI を初期化
 |
 +-- "tk-libraries-ready" イベントを発行

ページ固有の JavaScript は、tk-libraries-ready が発生してから実行する。

classic script と ES Module の違い

従来の <script>

通常の <script src="..."> は、HTML を上から解析するときに読み込まれる。

<script src="A.js"></script>
<script src="B.js"></script>
<script src="C.js"></script>

基本的には、

A.js を読み込んで実行
        ↓
B.js を読み込んで実行
        ↓
C.js を読み込んで実行
        ↓
次の HTML を処理

という順番になる。

そのため、後の HTML から A.js、B.js、C.js の関数を直接呼び出す構成が使いやすい。

<script type="module">

ES Module は通常の <script> と実行タイミングが異なる。

<script type="module" src="/js/tkLibraries.wrapper.mjs"></script>

module script は HTML の解析を止めず、defer に近い挙動をする。

したがって、HTML 内で単純に

const app = new tkHTMLApplication(4);

と書くと、その時点では wrapper がまだライブラリを読み終えていない可能性がある。

この問題を避けるため、wrapper が全ライブラリの読み込み完了後に

tk-libraries-ready

という独自イベントを発行する。

ページ側はこのイベントを待ってから、ライブラリを利用する。


今回の wrapper は「完全な ESM 化」ではない

現在の構成では、tkLibraries.wrapper.mjs 自体は ES Module であるが、読み込まれる既存の自作 JavaScript は従来型の classic script のままである。

つまり、

ES Module
  └─ classic script を順番にロードする

という移行用・互換用の構成である。

これは意図的な設計である。

既存の /js/*.js をすべて、

export function ...
import ...

という本来の ES Module に一度に書き換える必要がなく、現在動作しているライブラリをほぼそのまま利用できる。

将来的には、必要に応じて個々のライブラリを本来の ES Module に変更し、

import { ... } from "./tkUtils.mjs";

のような構成へ段階的に移行することも可能である。


tkLibraries.wrapper.mjs の仕様

ライブラリ一覧

現在の wrapper では、LIBRARIES 配列に共通ライブラリを登録している。

const LIBRARIES = [
  // Basic D2MatE utilities
  "/js/tkDebug_le.js",
  "/js/tkUtils_le.js",

  // CDN libraries
  "https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify-css.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify-html.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js",
  "https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js",

  // D2MatE UI / HTML helpers
  "/js/tkCSS_le.js",
  "/js/tkHTMLApplication_le.js",
  "/js/tkInclude.js",
  "/js/tkfunctions/tkInsertHeader.js",
  "/js/tkfunctions/tkSearch.js",
  "/js/tkfunctions/event_js.js",
];

新しい共通ライブラリを追加する場合は、原則としてこの配列に追加する。

重要なのは 依存関係の順番 である。

たとえば B.js が A.js の関数を必要とするなら、

const LIBRARIES = [
  "A.js",
  "B.js",
];

とする。

wrapper はこの順番を維持してロードする。

loadClassicScript()

従来型 JavaScript をロードする関数である。

概略は次のようになっている。

function loadClassicScript(src) {
  const promise = new Promise((resolve, reject) => {
    const script = document.createElement("script");

    script.src = src;
    script.async = false;

    script.addEventListener(
      "load",
      () => resolve(src),
      { once: true }
    );

    script.addEventListener(
      "error",
      () => reject(
        new Error(`Failed to load script: ${src}`)
      ),
      { once: true }
    );

    document.head.appendChild(script);
  });

  return promise;
}

ES Module の import() ではなく、

document.createElement("script")

で通常の <script> 要素を作るところが重要である。

これにより、既存 JavaScript を classic script として実行できる。

同じライブラリの二重読み込み防止

wrapper では、

const loadedScripts = new Map();

を用意している。

既に同じ src が登録されている場合は、新しい <script> を作らず、以前の Promise を返す。

概念的には、

if (loadedScripts.has(src)) {
  return loadedScripts.get(src);
}

である。

これにより、同じライブラリを wrapper 内で誤って複数回要求しても二重読み込みしにくくしている。

ライブラリは逐次読み込みする

loadAllLibraries() では、

for (const src of LIBRARIES) {
  try {
    await loadClassicScript(src);
  } catch (error) {
    ...
  }
}

としている。

await によって、

1 個目のロード完了
       ↓
2 個目をロード
       ↓
3 個目をロード

という順番を保証する。

既存 JavaScript は、前に読み込んだファイルの関数や変数を利用することがあるため、この順序保証が重要である。

windowオブジェクト への公開

function 宣言の場合

classic script で、

function insertHeader(...) {
  ...
}

のように定義された関数は、多くの場合、

window.insertHeader

として利用できる。

HTML 側では、

window.insertHeader(...)

と明示的に書くと、グローバル関数を利用していることが分かりやすい。

class、let、const の場合

一方、

class tkHTMLApplication {
  ...
}

や、

const tkCSS = ...

のような top-level lexical binding は、classic script で実行されても、必ずしも

window.tkHTMLApplication
window.tkCSS

として公開されるとは限らない。

そこで wrapper では、classic script として小さな「橋渡しコード」を実行する。

if (typeof tkCSS !== "undefined") {
  window.tkCSS = tkCSS;
}

if (typeof tkHTMLApplication !== "undefined") {
  window.tkHTMLApplication = tkHTMLApplication;
}

現在の wrapper では、次の名前を window に明示的に公開している。

tkDebug
tkCSS
HTMLApp
tkHTMLApplication

この処理により、HTML では、

window.tkCSS.standard();

あるいは、

window.app = new window.tkHTMLApplication(4);

のように利用できる。


ライブラリの関数を HTML から利用する方法

基本的な確認手順は次のとおりである。

  1. wrapper の LIBRARIES に対象 JavaScript が含まれていることを確認する。
  2. tk-libraries-ready の後で処理を実行する。
  3. window に目的の関数・オブジェクトが存在するか確認する。
  4. 存在すれば直接呼び出す。

たとえば tkCSS.standard() を利用する場合は、

window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      window.tkCSS &&
      typeof window.tkCSS.standard === "function"
    ) {
      window.tkCSS.standard();
    }
  },
  { once: true }
);

とする。

ブラウザの開発者コンソールから、

typeof window.tkCSS
typeof window.tkCSS.standard
typeof window.createSearchForm
typeof window.insertHeader
typeof window.tkHTMLApplication

などを実行すると、公開状況を確認できる。

期待される例は、

"object"
"function"
"function"
"function"
"function"

である。

window に見つからない場合

対象 JavaScript が読み込まれているにもかかわらず、

window.xxx

が undefined の場合は、class、let、const などで定義されている可能性がある。

その場合は wrapper の exposeLegacyLexicalGlobals() に橋渡しを追加する。

たとえば、

const myLibrary = ...

を利用したい場合は、

if (typeof myLibrary !== "undefined") {
  window.myLibrary = myLibrary;
}

を追加する。

tk-libraries-ready イベント

目的

wrapper は、

  1. 全ライブラリのロードを試行する。
  2. legacy global を window に橋渡しする。
  3. DOMContentLoaded を待つ。
  4. 共通 UI を初期化する。
  5. tk-libraries-ready を発行する。

このイベントが、HTML 側のページ固有 JavaScript の実行開始点になる。

基本形

<script>
window.addEventListener(
  "tk-libraries-ready",
  function (event) {
    // ライブラリを使用する処理
  },
  { once: true }
);
</script>

{ once: true } を指定すると、このイベントハンドラは 1 回だけ実行される。

初期化処理の二重実行を避けるため、原則として付けておく。


HTML の <head> の標準形

基本的には次の 2 行でよい。

<script
  type="module"
  src="/js/tkLibraries.wrapper.mjs">
</script>

<script
  nomodule
  src="/js/unsupportedBrowser.js">
</script>

共通ライブラリの <script> タグは各 HTML に列挙しない。


HTTPsa-bano設定: .mjs の MIME type

Apache が .mjs を正しい JavaScript MIME type で返さない場合、ブラウザは ES Module のロードを拒否する。

典型的には、

Expected a JavaScript-or-Wasm module script
but the server responded with a MIME type of "text/plain"

のようなエラーになる。

Apache では、たとえば次の設定を追加する。

AddType text/javascript .mjs

設定変更後は、環境に応じて Apache の設定確認と再読み込みを行う。

例:

sudo apachectl configtest
sudo systemctl reload httpd

.mjs を利用することで、ファイル名だけでも ES Module であることが分かりやすくなる。

今回の命名は、

tkLibraries.wrapper.mjs

としている。

wrapper は、複数の既存ライブラリを 1 つの入口から扱う役割を表している。


古いブラウザへの対応

ES Modules に対応していないブラウザ向けには、

<script nomodule src="/js/unsupportedBrowser.js"></script>

を使用する。

ES Modules 対応ブラウザでは nomodule script は原則として実行されない。

ES Modules 非対応ブラウザでは module script が利用できないため、unsupportedBrowser.js が、

このブラウザには対応していません。
このページを表示するには、ブラウザを最新版にしてください。

というメッセージを表示する。

unsupportedBrowser.js 自体は古いブラウザでも解釈しやすいよう、ES5 相当の記法を中心にしている。


共通 UI の初期化

現在の wrapper は、ライブラリ読み込み後に共通 UI も初期化する。

CopyBox CSS

if (typeof window.injectCopyBoxCSS === "function") {
  window.injectCopyBoxCSS();
}

したがって、多くのページでは以前の、

injectCopyBoxCSS();

を個別に書く必要がない。

if (
  typeof window.initModal === "function" &&
  !document.getElementById("messageDialog")
) {
  window.initModal();
}

initModal() が DOM 要素を作るため、messageDialog が既に存在するときは再作成しない。

これにより modal の二重生成を防ぐ。

highlight.js

if (
  window.hljs &&
  typeof window.hljs.highlightAll === "function"
) {
  window.hljs.highlightAll();
}

したがって、HTML 側で従来の、

<script>
hljs.highlightAll();
</script>

を毎回記述する必要はない。


エラー処理

wrapper 内のロード失敗

各ライブラリのロードに失敗しても、wrapper は直ちに全体を停止せず、

failures.push({ src, error });

として記録する。

全ロード試行終了後、

window.tkLibrariesFailures = failures;

にも保存する。

イベントからエラー情報を取得する

tk-libraries-ready イベントでは、

event.detail.failures

からロード失敗一覧を取得できる。

例:

window.addEventListener(
  "tk-libraries-ready",
  function (event) {
    const failures =
      event.detail && event.detail.failures
        ? event.detail.failures
        : [];

    for (const failure of failures) {
      console.error(
        "Failed to load resource:",
        failure.src,
        failure.error
      );
    }
  },
  { once: true }
);

displayError() が利用できる場合は、ページ内のエラー表示欄へ出すこともできる。

if (typeof window.displayError === "function") {
  window.displayError(
    "Failed to load resource: " + failure.src,
    "#errorMessages"
  );
}

insertErrorHandlers() の実行

従来は、

<script>
insertErrorHandlers("errorMessages");
</script>

のように、ライブラリ読み込み直後に実行していた。

wrapper 化後は、

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.insertErrorHandlers === "function"
    ) {
      window.insertErrorHandlers("errorMessages");
    }
  },
  { once: true }
);
</script>

とする。

tk-libraries-ready は DOMContentLoaded 後に発行されるため、DOM が存在することも保証しやすい。

ヘッダ挿入

従来は、

document.addEventListener(
  "DOMContentLoaded",
  function () {
    insertHeader(
      "/D2MatE/d2mate_header.html"
    ).catch(console.error);
  }
);

としていた。

wrapper は DOMContentLoaded を待ってから tk-libraries-ready を発行するため、ページ側で再び DOMContentLoaded を待つ必要はない。

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.insertHeader === "function"
    ) {
      window.insertHeader(
        "/D2MatE/d2mate_header.html"
      ).catch(console.error);
    }
  },
  { once: true }
);
</script>

tkHTMLApplication の初期化

ページ内の複数箇所から app を使用する場合は、共通初期化部分で、

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.tkHTMLApplication === "function"
    ) {
      window.app =
        new window.tkHTMLApplication(4);

      window.attrs = {
        target: "_blank"
      };

      window.fontAttrs = {};
    }
  },
  { once: true }
);
</script>

のように window.app へ保存する。

別の <script> からは、

const app = window.app;

として取得できる。

<div> の近くに <script> を残す方法

大きな HTML では、すべてのページ固有 JavaScript を 1 箇所にまとめるより、

<div id="target"></div>
<script>
  ...
</script>

のように、内容を挿入する <div> のすぐ近くに JavaScript を置く方が、HTML を読んだときの対応関係が分かりやすい。

wrapper を利用しても、この構造はそのまま維持できる。

例: 1 個のファイルリンクを挿入する

<div id="maze_animation2"></div>

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    const app = window.app;

    if (!app) {
      console.error(
        "tkHTMLApplication is not initialized."
      );
      return;
    }

    const dir =
      "/Lecture/FundamentalsCMS/student2025/";

    const path =
      "maze_animation2.py";

    const attrs =
      window.attrs || { target: "_blank" };

    const content = app.urlContent(
      path,
      dir + path,
      {},
      attrs,
      { target: "_blank" },
      path
    );

    app.append(
      "maze_animation2",
      content
    );
  },
  { once: true }
);
</script>

<div> とそれを埋める JavaScript が隣接するので、ページの保守性が高い。

複数ファイルの一覧を挿入する例

<div id="20250110"></div>

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    const app = window.app;

    if (!app) {
      console.error(
        "tkHTMLApplication is not initialized."
      );
      return;
    }

    const dir =
      "/Lecture/FundamentalsCMS/";

    const paths = [
      "01/data.csv",
      "01/test.ipynb",
      "01/test.py",
      "01/read.ipynb"
    ];

    const attrs =
      window.attrs || { target: "_blank" };

    const fontAttrs =
      window.fontAttrs || {};

    const ulItems = [];

    for (
      let i = 0;
      i < paths.length;
      i++
    ) {
      const path = paths[i];

      ulItems.push(
        app.li([
          app.urlContent(
            path,
            dir + path,
            {},
            attrs,
            { target: "_blank" },
            path
          )
        ])
      );
    }

    const ulList = app.ul(
      ulItems,
      { class: "unordered-list" }
    );

    app.append(
      "20250110",
      app.font(
        ulList,
        fontAttrs
      )
    );
  },
  { once: true }
);
</script>

重要なのは、dir、paths、ulItems などをその script ブロック内のローカル変数にすることである。

従来のように、

dir = ...
path = ...
ulItems = ...

を別の <script> 間で暗黙に共有すると、ページが大きくなったときに変数の上書きが起こりやすい。

wrapper 化を機会に、

const dir = ...
const paths = ...
const ulItems = ...

として局所化すると保守しやすい。

検索フォームの例

tkSearch.js を wrapper から読み込んでいれば、

<div id="searchContainer"></div>

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.createSearchForm ===
      "function"
    ) {
      window.createSearchForm(
        "searchContainer",
        "searchInput",
        "検索",
        "検索キーワードを入力"
      );
    }
  },
  { once: true }
);
</script>

のように利用できる。

tkCSS のページ固有処理

tkCSS 自体の読み込みは wrapper が担当する。

ページごとに必要な CSS 初期化だけを tk-libraries-ready 後に呼ぶ。

例:

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      window.tkCSS &&
      typeof window.tkCSS.standard ===
      "function"
    ) {
      window.tkCSS.standard();
    }
  },
  { once: true }
);
</script>

つまり、

tkCSS_le.js を読み込む

ことと、

tkCSS.standard() を実行する

ことを分離して考える。

前者は wrapper の仕事であり、後者はページ固有処理である。

ページ固有 <script> を分割したまま使うときの注意

同じ tk-libraries-ready イベントを待つ <script> が複数あってもよい。

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    // 共通 app 初期化
  },
  { once: true }
);
</script>

...

<div id="target1"></div>
<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    // target1 を作成
  },
  { once: true }
);
</script>

...

<div id="target2"></div>
<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    // target2 を作成
  },
  { once: true }
);
</script>

HTML の解析中にこれらのイベントハンドラが順番に登録されれば、同じイベントに対して登録順に実行される。

したがって、window.app の初期化 script を後続 script より先に HTML 内へ置く構成が分かりやすい。

イベント発行後に追加されたコードへの対応

wrapper はイベント発行時に、

window.tkLibrariesReady = true;

も設定する。

通常の静的 HTML では、inline script が先にイベントハンドラを登録するため問題にならない。

しかし、AJAX などで後から HTML 断片を読み込み、その中のコードを実行する場合は、既に tk-libraries-ready が発行済みの可能性がある。

そのような場合には、次のような helper を作ると安全である。

function whenTkLibrariesReady(callback) {
  if (window.tkLibrariesReady) {
    callback({
      detail: {
        failures:
          window.tkLibrariesFailures || []
      }
    });
    return;
  }

  window.addEventListener(
    "tk-libraries-ready",
    callback,
    { once: true }
  );
}

利用例:

whenTkLibrariesReady(function () {
  window.insertHeader(
    "/D2MatE/d2mate_header.html"
  );
});

静的 HTML だけで利用する場合は必須ではない。

新しい自作ライブラリを wrapper に追加する手順

たとえば、

/js/tkfunctions/myFunction.js

を追加したい場合を考える。

1. LIBRARIES に追加する

const LIBRARIES = [
  ...
  "/js/tkfunctions/myFunction.js",
];

依存するライブラリがある場合は、その後に配置する。

2. ブラウザで公開状況を確認する

開発者コンソールで、

typeof window.myFunction

を確認する。

"function" なら、そのまま利用できる。

3. window に無い場合

JavaScript 内で、

const myFunction = ...

あるいは、

class MyClass {
  ...
}

として定義されている場合は、wrapper の橋渡しへ追加する。

if (
  typeof MyClass !== "undefined"
) {
  window.MyClass = MyClass;
}

4. HTML から呼び出す

window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.myFunction ===
      "function"
    ) {
      window.myFunction();
    }
  },
  { once: true }
);

CDN ライブラリを追加する手順

CDN の JavaScript も同じ LIBRARIES 配列へ追加できる。

const LIBRARIES = [
  ...
  "https://example.com/library.min.js",
];

ただし、共通 wrapper に入れると、その wrapper を使うすべてのページでロードされる。

したがって、

という使い分けが望ましい。

CDN のバージョン管理

wrapper に CDN URL を集約すると、ライブラリのバージョンを 1 箇所で管理できる。

これは大きな利点であるが、逆に wrapper のバージョンを変更すると、利用するすべてのページへ影響する。

たとえば XLSX を、

0.16.9

から、

0.17.0

へ変更すると、wrapper を利用するすべてのページが新しい版を使う。

したがって、共通 wrapper のライブラリバージョンを変更する場合は、代表的なページで動作確認してから反映する。

wrapper の公開情報

現在 wrapper は、デバッグや後から読み込まれたコードのために次を公開する。

window.tkLibrariesReady
window.tkLibrariesFailures

window.tkLibrariesReady

ライブラリ初期化完了後、

window.tkLibrariesReady = true;

となる。

window.tkLibrariesFailures

ロードに失敗したライブラリを配列として保持する。

console.log(
  window.tkLibrariesFailures
);

で確認できる。

wrapper 自身の export

tkLibraries.wrapper.mjs は ES Module なので、次も export している。

export const ready =
  loadAllLibraries();

export {
  loadClassicScript
};

別の ES Module からは、

import {
  ready,
  loadClassicScript
} from "/js/tkLibraries.wrapper.mjs";

と利用できる。

たとえば、

import {
  ready
} from "/js/tkLibraries.wrapper.mjs";

await ready;

とすれば、ライブラリロード完了を Promise として待つこともできる。

通常の HTML inline script では tk-libraries-ready イベントを利用する方が簡単である。

移行前後の比較

移行前

<head>
  <script src="/js/tkDebug_le.js"></script>
  <script src="/js/tkUtils_le.js"></script>
  <script src="/js/tkCSS_le.js"></script>
  <script src="/js/tkHTMLApplication_le.js"></script>
  <script src="/js/tkfunctions/tkSearch.js"></script>

  <script src="https://cdnjs.cloudflare.com/..."></script>
</head>

<body>

<script>
insertErrorHandlers(
  "errorMessages"
);
</script>

<div id="searchContainer"></div>

<script>
document.addEventListener(
  "DOMContentLoaded",
  function () {
    createSearchForm(...);
  }
);
</script>

</body>

移行後

<head>
  <script
    type="module"
    src="/js/tkLibraries.wrapper.mjs">
  </script>

  <script
    nomodule
    src="/js/unsupportedBrowser.js">
  </script>
</head>

<body>

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.insertErrorHandlers
      === "function"
    ) {
      window.insertErrorHandlers(
        "errorMessages"
      );
    }
  },
  { once: true }
);
</script>

<div id="searchContainer"></div>

<script>
window.addEventListener(
  "tk-libraries-ready",
  function () {
    if (
      typeof window.createSearchForm
      === "function"
    ) {
      window.createSearchForm(...);
    }
  },
  { once: true }
);
</script>

</body>

HTML のページ固有構造は維持したまま、共通ライブラリの管理だけを wrapper へ集約できる。

既存 HTML を修正するときのチェックリスト

  1. <head> に並んでいる共通 JavaScript を確認する。
  2. wrapper の LIBRARIES に必要なライブラリが含まれているか確認する。
  3. 個別の <script src="..."> を削除する。
  4. <head> に次を追加する。
<script
  type="module"
  src="/js/tkLibraries.wrapper.mjs">
</script>
<script
  nomodule
  src="/js/unsupportedBrowser.js">
</script>
  1. ページ固有の初期化処理を tk-libraries-ready の中へ移す。
  2. tkHTMLApplication、tkCSS などは window.xxx から利用する。
  3. typeof ... === "function" などで存在確認する。
  4. <div> を生成する script は、必要なら <div> の近くに残す。
  5. script 間で暗黙に共有していた dir、path、content などは、できるだけ各ブロックの const / let にする。
  6. CDN や自作 JavaScript のロード失敗を Console で確認する。
  7. Apache が .mjs を JavaScript MIME type で返すことを確認する。
  8. ブラウザ Console で window.tkLibrariesReady が true になることを確認する。

よくある問題

.mjs が text/plain として返される

症状:

Expected a JavaScript-or-Wasm module script
but the server responded with a MIME type of "text/plain"

対策:

AddType text/javascript .mjs

を Apache に設定する。

xxx is not defined

原因として、

ことが考えられる。

まず、

window.tkLibrariesReady
window.tkLibrariesFailures
typeof window.xxx

を Console で確認する。

class は読み込まれているのに window.xxx が無い

top-level class や const の可能性がある。

wrapper の exposeLegacyLexicalGlobals() に橋渡しを追加する。

ページ固有処理が二重実行される

イベントハンドラに、

{ once: true }

を付ける。

また、DOM 要素を生成する共通初期化では、既存要素の有無を確認する。

1 つの CDN が落ちるとどうなるか

現在の wrapper はロード失敗を failures に記録し、次のライブラリのロード試行を続ける。

そのため、オプションの CDN が 1 つ取得できなくても、関係のない機能は動作を継続できる可能性がある。

ただし、後続ライブラリがその CDN に依存している場合は、その機能は正常に動かない可能性がある。

設計上の方針

今回の構成では、役割を次のように分ける。

wrapper の役割

HTML の役割

この分離により、

共通ライブラリ管理
        ↓
tkLibraries.wrapper.mjs

ページ固有ロジック
        ↓
各 HTML

という責任分担になる。

今後の発展

現在の方式は、既存資産を活かしながら ES Modules の入口を導入する現実的な移行方法である。

将来的には次の段階へ進められる。

1. ページ種別ごとの wrapper

すべてのページで巨大な共通 wrapper を使うのではなく、

tkLibraries.core.mjs
tkLibraries.graph.mjs
tkLibraries.presentation.mjs
tkLibraries.webapp.mjs

などに分ける方法がある。

ページごとに必要なライブラリだけをロードできる。

2. 自作 JavaScript の本来の ESM 化

たとえば、

export function insertHeader(...) {
  ...
}

として、

import {
  insertHeader
} from "./tkInsertHeader.mjs";

と利用する。

この段階では window を介した global 共有を減らせる。

3. ページ固有 JavaScript の module 化

大きなページでは、

<script type="module">
import ...
</script>

あるいはページ専用 .mjs に分離することもできる。

ただし、HTML 内で <div> と処理する <script> を近くに置く現在の構造は可読性が高いため、無理にすべてを外部ファイルへ移す必要はない。

まとめ

今回の tkLibraries.wrapper.mjs は、

多数の既存 JavaScript
        ↓
1 本の ES Module wrapper
        ↓
tk-libraries-ready
        ↓
ページ固有 JavaScript

という構造を作る。

重要な点は次のとおりである。

この方式を使うと、既存サイトの構造を大きく壊さずに JavaScript の依存関係を整理でき、今後の Web ページ追加・ライブラリ更新・保守が容易になる。

Pandoc による変換

この Markdown は Pandoc で Word および HTML に変換できる。

Word

pandoc ESM_wrapper_guide.md \
  --toc \
  -o ESM_wrapper_guide.docx

既存の Word 書式を使いたい場合は、reference document を指定できる。

pandoc ESM_wrapper_guide.md \
  --toc \
  --reference-doc=reference.docx \
  -o ESM_wrapper_guide.docx

HTML

pandoc ESM_wrapper_guide.md \
  --standalone \
  --toc \
  -o ESM_wrapper_guide.html

必要に応じて CSS を指定する。

pandoc ESM_wrapper_guide.md \
  --standalone \
  --toc \
  --css=style.css \
  -o ESM_wrapper_guide.html

付録: 現在の wrapper の主要 API

名前 種類 用途
window.tkLibrariesReady boolean 共通ライブラリ初期化完了の確認
window.tkLibrariesFailures Array ロード失敗したライブラリ一覧
tk-libraries-ready Window event HTML 側へ初期化完了を通知
event.detail.failures Array イベントから取得するロード失敗一覧
window.tkCSS object tkCSS_le.js の機能
window.tkHTMLApplication class HTML 構築用クラス
window.insertHeader function 共通 header 挿入
window.createSearchForm function 検索フォーム作成
ready ESM export / Promise module 側から全ロード完了を待つ
loadClassicScript() ESM export / function classic script を追加ロードする