クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【TypeScript】モジュール分割で起きたエラーを調べる(CommonJSが原因)

メタ言語の続きです。前回はTypeScriptをモジュール分割しました。EJSやSass(SCSS)と違ってモジュール部分のソースは含まれず、またエラーも起きてうまく動きません。今回はエラーについて掘り下げてみました。*1。それではいきましょう!

【目次】

※参考:前回記事
【TypeScript】おみくじJSをTypeScriptに移植:型付け→モジュール分割(エラーあり) - クモのようにコツコツと

※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

前回のおさらい(モジュール分割→エラー)

「omikuji.ts」のコード

// 運勢
const unsei: string[] = [ 
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];

// 結果
let kekka = (): string => {
    let nmb: number = Math.floor(Math.random() * unsei.length );
    return unsei[nmb]; 
}

// おみくじ
export let omikuji = (): void => {
    let massage: string = 'あなたの運勢は「' + kekka() + '」です。';
    alert(massage);
}

おみくじ部分をモジュールとして分割したファイル。

「script.ts」のコード

import { omikuji } from "./omikuji/omikuji";

// イベント実行
document.addEventListener("DOMContentLoaded", () => {
    let btn: HTMLButtonElement = document.querySelector('.inner__text-btn') as HTMLButtonElement;
    btn!.addEventListener('click', omikuji, false);
}, false);

omikujiモジュールをインポートしている。

コンパイル後の「script.js」

"use strict";
exports.__esModule = true;
var omikuji_1 = require("./omikuji/omikuji");
// イベント実行
document.addEventListener("DOMContentLoaded", function () {
    var btn = document.querySelector('.inner__text-btn');
    btn.addEventListener('click', omikuji_1.omikuji, false);
}, false);

omikujiモジュール部分のコードは含まれない。

冒頭のexports.__esModule = true部分に下記のエラーが…

Uncaught ReferenceError: exports is not defined

詳細は前回の記事を参照。

※参考:【TypeScript】おみくじJSをTypeScriptに移植:型付け→モジュール分割(エラーあり) - クモのようにコツコツと

exports is not definedエラーはCommonJSが原因

エラーコードUncaught ReferenceError: exports is not definedについて調べる。

まずこのX is not definedエラーは下記のような意味合い。

  • 変数が定義されてない
  • または関数のスコープから外れている

※参考:ReferenceError: "x" is not defined - JavaScript | MDN

今回は冒頭にあるexportsの定義が見つからないよ、というエラー。

こちらの記事によるとCommonJSの設定が原因らしい。

tsconfigのmoduleが commonjs になってたのが原因でした。 es2015 に変えたらこのエラーは出なくなりました。

※参考:jsで書いたフロントを徐々にtsに移行する - Qiita

TypeScriptの設定ファイル「tsconfig」を開いてみる。

{
  "compilerOptions": {
   // 中略
    "module": "commonjs",    /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
   // 中略
}

お、確かに。modulecommonjsになっとる。

コメントの和訳

モジュールコードの生成を指定: 'none'、 'commonjs'、 'amd'、 'system'、 'umd'、 'es2015'、 'es2020'、または 'ESNext'。

exportsについて調べると確かにこの書き方はCommonJSの書き方だわ。

CommonJS Export/Import
exports.method = method export { method }

※参考:# CommonJS と ES6の import/export で迷うなら - Qiita

tsconfigのmoduleをes2015に変更してみる(結果変わらず…)

「tsconfig」のmodulees2015に変えてみる。

{
  "compilerOptions": {
   // 中略
    module": "es2015",     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
   // 中略
}
" 

コンパイル結果

exports.__esModule = true;

うーむ、変わらん。。

こちらの記事のようにのimportexportのESモジュールになることを期待していた。

※参考:tsconfigのmoduleの設定値の違い: commonjs, amd, system, umd, es6, es2015, esnext, none - Qiita

ESモジュールの件はひとまず置いておく…。

gulpfile.jsのTypeScript設定を変更

もう一つ、「index.js」がEJSやSass(SCSS)のようにモジュールのコードを含んでいなかったため、モジュールファイルもコンパイルしたい。

前回はindex.jsしかコンパイルされなかった。 f:id:idr_zz:20200627044001j:plain

「gulpfile.js」のTypeScript設定部分を変更する。

まずはコンパイル部分。

// TypeScriptコンパイル
gulp.task('ts',  (done) => {
    gulp.src('./src/ts/**/*.ts')
        .pipe(plumber())
        .pipe(typescript())
        .js
        .pipe(gulp.dest('./dest/js'));
    done();
});

EJSやSass(SCSS)と同様にgulp.src()/**を追記。下層フォルダのファイルも適用範囲にする。

watch(監視)タスクも変更

// 監視ファイル
gulp.task('watch-files', (done) =>  {
    gulp.watch(["./src/ejs/**/*.ejs", "./src/json/**/*.json"], gulp.task('ejs'));
    gulp.watch("./dest/*.html", gulp.task('browser-reload'));
    gulp.watch("./src/scss/**/*.scss", gulp.task('sass'));
    gulp.watch("./dest/css/*.css", gulp.task('browser-reload'));
    gulp.watch("./src/ts/**/*.ts", gulp.task('ts'));
    gulp.watch("./dest/js/*.js", gulp.task('browser-reload'));
    done();
});

こちらもts部分に他と同様に/**を追記する。

モジュールファイルもコンパイル!

変更を適用するためにControl + cでgulpを一旦閉じる。

gulp再起動!

npx gulp

お、下層フォルダのファイルもコンパイルされるようになった! f:id:idr_zz:20200713215017j:plain

「omikuji.js」のコード

"use strict";
exports.__esModule = true;
exports.omikuji = void 0;
// 運勢
var unsei = [
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];
// 結果
var kekka = function () {
    var nmb = Math.floor(Math.random() * unsei.length);
    return unsei[nmb];
};
// おみくじ
exports.omikuji = function () {
    var massage = 'あなたの運勢は「' + kekka() + '」です。';
    alert(massage);
};

こちらもESモジュールではなくCommonJSのexportsにコンパイルされている…。

デベロッパーツールを見ると「script.js」しか認識されてないようだ。 f:id:idr_zz:20200714052342j:plain

「omikuji.js」はファイルとしては存在しているが、リンク状態にはなっていないということだ。

head部分にグローバル変数を書き足す

やはり先ほどのエラーを解決しないとモジュールファイルのコードが認識されない。

※参考:javascript - Uncaught ReferenceError: exports is not defined in filed generated by Typescript - Stack Overflow

上記のQAを参考にhead部分にグローバル変数を追記してみる。

<script> var exports = {}; </script>

「ejs/base/_head.ejs」

<meta charset="UTF-8">
<title><%= main.title %></title>
<meta name="description" content="<%= main.lede %>">
<link rel="stylesheet" href="css/style.css">
<script> var exports = {}; </script>
<script src="js/script.js"></script>

script.jsのリンクの前の行に追記。

そうすると最初のエラーは無くなった!ハックっぽい手段だがこれでええのんか?

他には下記のような回答もあった

  • typescriptのバージョンを下げる
  • コアな設定ファイルを書き換える

今後のメンテナンスを考えるとどちらも好ましくない。

次のエラー:require is not defined

exportsは認識されるようになったが、次に別のエラーが… f:id:idr_zz:20200714053435j:plain omikuji.jsをインポートしている行。

エラーメッセージ

Uncaught ReferenceError: require is not defined

今度はrequireが見つからないよと。

そう、そもそもrequireはサーバサイドであるNode.jsのメソッドのため、クライアントサイド(ブラウザ)では動かない。だからESモジュールにしたかったんだよなー…。

headでモジュールファイルもリンクしてみる(結果変わらず…)

こちらの記事にいくつか対処法があった。

※参考:Uncaught ReferenceError: require is not defined 対処法まとめ - 文系プログラマー

まずインポートファイルである「omikuji.js」もheadからリンクしてみる。

「ejs/base/_head.ejs」

<meta charset="UTF-8">
<title><%= main.title %></title>
<meta name="description" content="<%= main.lede %>">
<link rel="stylesheet" href="css/style.css">
<script> var exports = {}; </script>
<script src="js/script.js"></script>
<script src="js/omikuji/omikuji.js"></script>

デベロッパーツールで見ると「omikuji.js」は認識されるようになった! f:id:idr_zz:20200714080201j:plain

しかし、結局のところまだrequireは認識されない…。

Uncaught ReferenceError: require is not defined

先ほどの参考記事では他には、他に下記のような方法が書いてある。

  • Browserifyでモジュールをバンドルする
  • Webpackでモジュールをバンドルする

EJSやSass(SCSS)もモジュールを統合させた1ファイルのリンクで済んでいた。

JSファイルもやはりバンドルする方向がいいなーと思った。headファイルにリンクをたくさん書く方法ではモジュールの把握が難しくなりそう。

Browserify とWebpackでは今はWebpackの方が主流なのでこちらの方法を試すか。

※参考:JavaScriptのモジュール管理(CommonJSとかAMDとかBrowserifyとかwebpack) | tsuchikazu blog

Webpackは以前も何度か触っている。

※参考:webpackでJSのモジュールファイルをバンドルする - クモのようにコツコツと

最後に:まとめ(次回はWebpack!)

今回やってみたこと、わかったことをまとめます。

  • exports is not definedエラー
    →CommonJSが原因だった
  • tsconfigのmoduleをes2015に変更
    →結果変わらず…
  • gulpfile.jsのTypeScript設定を変更
    /**→モジュールファイルもコンパイルされた!
  • head部分にグローバル変数を書き足す
    exportsが認識されるようになった!
  • require is not definedエラー
    requireはCommonJSなのでブラウザでは動かない
  • headでモジュールファイルもリンク
    →結果変わらず…

結局、CommonJSのをESモジュールに変更することはできなかったため、ブラウザでは認識されない。

また、認識されたとしてもモジュールファイルが分割されたままただとページごとのリンク設定の把握が難しそう。

ということで、次回はWebpackによるモジュールのバンドル(統合)をやってみたく思います。

それではまた!


※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

*1:結論から言うと、エラーは解消されず別アプローチとしてWebpackによるバンドルが必要なこと見えてきました、と言う内容です