TypeScriptのmoduleをAMDにしてハマったのでやめたメモ
開発環境構築メモの通り、gulpでTypeScriptを複数であっても1ファイルに集約してコンパイルされるようにしていた
gulpでのコンパイルで複数のファイルを1ファイルに集約するためには以下の方法があり、1つ目を使っていたが、それがハマる原因になった
1️⃣AMD (module: "amd") を使う
define() で出力されるため、RequireJS の読み込みが必須😫
2️⃣Webpack などのバンドラーを使う
import / export をそのまま使える😊
ハマるまで
とりあえず開発中は1ファイルに長々とソースコードを書いていたが、ほぼ完成という段階で「流石にファイルを分けたほうがいいな」と思い、モジュール化に取り掛かる
一通り分割が終わったところで実際にブラウザで確認してみたところ、Uncaught ReferenceError: define is not definedというエラーが出ることに気づく
ChatGPTに教えを仰ぐと、以下を提案されるが、とりあえずそのままAMDを使う方法を選びドハマリすることになる。。。
コンパイルの方法はそのままAMDを使用し、 RequireJSを使う方法
素直にmodule: "esnext" に変える方法
Webpackなどのバンドラーを使う方法
やったこと
ReuireJSを追加
RequireJSをHTMLに追加してみる➔エラーは出なくなるものの、スクリプトが動いていない様子
code:index.html
<script data-main="public/js/script.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
エントリーポイントの明示化とdefine()を正しく解釈しているか確認
script.js の中の define() を正しく解釈しているかを確認
通常、RequireJSの data-main を指定するだけでOKのはずだが、モジュールのエントリーポイントを明示する ことで動作を確認できるとのこと
ブラウザのコンソールで「RequireJS loaded script.js」が表示されればscript.js がロードされているということ
code:index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
require("js/script", function(main) {
console.log("RequireJS loaded script.js");
});
</script>
エントリーポイントの明示化
そもそも、tsconfigを使用していなかったのでtsconfig.jsonを追加し、tsconfig.jsonを読み込むようにする
code:tsconfig.json
gulp.task("typescript", function () {
const tsProject = ts.createProject("tsconfig.json");
return gulp
.src("ts/main.ts") // エントリーポイントを指定
.pipe(tsProject())
.pipe(gulp.dest("public/js"));
});
code:gulpfile.js
...
// TypeScriptのコンパイル
gulp.task("typescript", function () {
console.log("Compiling TypeScript...");
const tsProject = ts.createProject("tsconfig.json");
return (
gulp
.src("ts/main.ts") // エントリーポイント
.pipe(tsProject()) // tsconfig.json を利用
.pipe(gulp.dest(".")) // tsconfigの outFile に従う
);
});
...
tsconfigを使用するようにしてみたものの、コンパイル時にCannot read file '/app/tsconfig.json'というエラーが出る
ts.createProject("tsconfig.json") が tsconfig.json を正しく読み込めていないとのこと。
ただし、これはgulp-typescript の仕様なので、ts.createProject の呼び出しを gulp.task の外に移動することで解消された
code:gulpfile.js
...
const tsProject = ts.createProject("tsconfig.json");
// TypeScriptのコンパイル
gulp.task("typescript", function () {
console.log("Compiling TypeScript...");
return (
gulp
.src("ts/main.ts") // エントリーポイント
.pipe(tsProject()) // tsconfig.json を利用
.pipe(gulp.dest(".")) // tsconfigの outFile に従う
);
});
...
この時点で期待される動作はコンソールに「RequireJS loaded script.js」が表示され、スクリプトが動いていること。だが、「RequireJS loaded script.js」はコンソールに表示されるものの、script.jsの処理は動いていない
➔require()がないとmain.ts の処理は自動で実行されない
HTMLでrequire()する
code:html
<script>
require("js/script", function(main) {
console.log("RequireJS loaded script.js");
console.log(main);
if (main && main.default) {
main.default(); // main.ts のエクスポートされた関数を実行
}
});
</script>
main.tsにexport default function() を追加
document.addEventListener("DOMContentLoaded", function () {...はモジュールとしてロードされても実行されない可能性がある
code:main.ts
export default function main() {
document.addEventListener("DOMContentLoaded", function () {
console.log("start");
...
});
}
上記修正をしたが、console.log(main);の出力結果はundefinedでmain`が正しく読み込めていない。さらに、コンパイル後のscript.jsを見るとモジュール名 "main" で define() されている
code:コンパイルされたscript.js
...
define("main", ..., function (require, exports, ...) {
...
解決策として、require() の引数を "main" に変更してみる
code:html
<script>
require("main", function(main) {
console.log("RequireJS loaded main");
console.log(main); // ここで main の中身を確認
if (main && main.default) {
main.default(); // main.ts のエクスポート関数を実行
}
});
</script>
GET http://localhost/main.js net::ERR_ABORTED 404 (Not Found)と、main.jsを探すようになってしまう。。。
RequireJS baseUrl を指定して script.js を探させる ようにすれば解決できるとのこと
code:html
<script>
require.config({
baseUrl: "js", // script.js があるディレクトリを指定
});
require("script", function(main) {
console.log("RequireJS loaded script.js");
console.log(main); // ここで main の中身を確認
if (main && main.default) {
main.default(); // main.ts のエクスポート関数を実行
}
});
</script>
script.jsを読み込むようにはなったものの、結局console.log(main);の出力結果はundefined(この辺で心がだいぶ折れている)
動く可能性が高いという以下の解決策を提案され、試してみるものの今度はコンパイル時にエラーが出てしまう
tsconfigのoutFileを削除し、outDirを設定することで、各 import が RequireJS に適したdefine()で出力される
code:tsconfig.json
{
"compilerOptions": {
"target": "es2023",
"module": "amd",
"noImplicitAny": true,
"baseUrl": "./ts", // TypeScriptのimportの基準
"outDir": "js" // 出力ディレクトリを変更(outFileを削除)
}
}
各 import の .js ファイルを public/js に出力
code:gulpfile.js
gulp.task("typescript", function () {
console.log("Compiling TypeScript...");
return tsProject()
.src()
.pipe(tsProject())
.pipe(gulp.dest("public/js")); // outDir に合わせる
});
export default function main()をAMDに適したdefine()に変える
code:main.ts
define("require", "exports", function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function main() {
document.addEventListener("DOMContentLoaded", function () {
console.log("start");
...
});
}
return main; // ここで関数をエクスポート
});
default プロパティを使わず、直接 main() を実行する
code:html
<script>
require.config({
baseUrl: "public/js", // outDir に合わせる
});
require("main", function(main) { // "script" ではなく "main" にする
console.log("RequireJS loaded main");
console.log(main); // ここで main の中身を確認
if (main) {
main(); // 直接実行する
}
});
</script>
各解決策を試した結果のコンパイルエラー
code:コンパイルエラー
error TS2304: Cannot find name 'define'.
error TS7006: Parameter 'require' implicitly has an 'any' type.
error TS7006: Parameter 'exports' implicitly has an 'any' type.
AMDを使おうとしたのはファイルを1つにまとめるためだったので、ファイルを1つにまとめないのであればAMDにこだわる必要もなくなるので、AMDは諦めて大人しくmodule: "esnext" を使う方法に切り替えた。
なお、ESNextに変更するにあたってもtsconfig.jsonは使うように変更。
tsconfig.json と gulpfile.js の内容については開発環境構築メモを参照。
import/exportメモ
import / export の使い方に対する知識が浅く、HTMLから起点となるjsファイルを普通に読み込めばよいと思っていたがそうではなかったので一応メモ
起点となるファイル
export default で処理をエクスポート
全ファイルに言えるが、import時はES モジュールとして import を使う(.js拡張子を含める)
code:main.ts
import { changeLogoVisible, changeHeaderColorchange } from "./header.js";
...
function main() {
document.addEventListener("DOMContentLoaded", function () {
...
});
}
export default main;
HTMLから起点となるjsを読み込む際
<script type="module">を使用し、ES モジュールとして読み込む。
また、デフォルトの関数を呼び出す
code:html
<script type="module">
import main from "./public/js/main.js";
if (main) {
main();
}
</script>
ひとまず、AMDからESNextを使うように変更したが、そもそもgulpでコンパイルするのではなく、Webpackを使うのが正解
今回はプロフィールサイトで、規模も大きくないし、自分ひとりのプロジェクトなのでWebpackに変更しなかったが本来なら変更したほうがいいんだろうなぁ。。。という話
#Typescript #gulp #開発環境構築