この記事では、JavaScriptやTypeScriptでコードを書くときの「読み込み方法」や「ファイルの分け方」について解説します。
HTMLとの連携に不可欠な scriptタグの使い方 や、コードを整理するための モジュール構成 の基本を丁寧に紹介します。
実例つきで、実務でも頻繁に使われる defer や type="module" の違い、import / export を使ったファイル分割方法などを詳しく解説していきます。
解説:
JavaScriptやTypeScriptをHTMLで使うためには、scriptタグを正しく記述する必要があります。
ここでは、代表的な2つの指定方法とその違いを整理しておきましょう。
代表的な2つの記述方法は、以下のとおりです。
// defer を使う場合
<script src="main.js" defer></script>
// モジュールとして読み込む場合
<script type="module" src="main.js"></script>どちらもHTMLの <head> の中や <body> の末尾に記述できますが、defer や type="module" を使うことで、HTMLの読み込みが終わってから安全にスクリプトを実行できます。
補足:
type="module" を使うと、自動で defer のような振る舞いになります。つまり、HTMLの構築後にスクリプトが実行されるため、DOMの取得なども安全に行えます。
解説:
プログラムが長くなってくると、すべてを1つのファイルに書いておくのは管理しづらくなります。
そこで登場するのが「モジュール」の考え方です。
JavaScriptでは、type="module" を使うことで、複数のファイルにコードを分割して管理できるようになります。
ファイル間で関数や変数をやり取りできるようになるため、保守性や再利用性が高まります。
※scriptタグの基本構文を参照
まずはシンプルな例で見てみましょう。
// utils.js というファイルに定義
export function hello(name) {
return `こんにちは、${name}さん!`;
}これを別のファイルから使うには、次のように import を書きます:
// main.js 側で読み込む
import { hello } from './utils.js';
console.log(hello('太郎')); // 結果 : こんにちは、太郎さん!実際のプロジェクトでは、次のような構成が一般的です:
project/
├── index.html
├── main.js ← エントリーポイント
├── utils/
│ └── helper.js ← 各種関数の管理
└── modules/
└── user.js ← ユーザー関連処理
このように機能ごとにファイルを分けることで、後からのメンテナンスやチーム開発もスムーズに進められます。
※ちなみに、ファイル名はなんでも大丈夫です。特にルールないです。(←私は、地味に躓きました、笑)
上記のファイル名もあくまで一例です。
モジュール構文( import / export )は、ローカルファイルで直接開くとエラーになることがあります。
正しく動作させるには、ローカルサーバーで実行する必要があります。たとえば:
ブラウザのセキュリティ上、file:// から直接開くと CORSエラー(リソース読み込み制限)になる点に注意しましょう。
解説:
基本的な import / export の使い方に慣れてきたら、もう少し柔軟な書き方も覚えておきましょう。
実務では、デフォルトエクスポート や 別名(エイリアス) を使うケースも多くあります。
ファイル内で1つだけエクスポートする場合は、default キーワードを使うことができます。
// message.js
export default function() {
return 'こんにちは!';
}インポートする側は、波かっこなしで名前を自由につけられます:
// main.js
import showMessage from './message.js';
console.log(showMessage()); // 結果 : こんにちは!エクスポートされた関数名を変更して、自分の好きな名前でインポートすることもできます。
// utils.js
export function sum(a, b) {
return a + b;
}次のように、import時に名前を変更できます:
// main.js
import { sum as add } from './utils.js';
console.log(add(2, 3)); // 結果 : 5
実際には、使う機会はあまり多くないかもしれません。(←私は経験ゼロ、笑)
自作関数ならそのまま使えますし、チーム開発では名前を変更すると可読性が下がることもあるため注意が必要です!笑
1つのファイルで、複数の関数を通常エクスポートしつつ、1つだけをデフォルトエクスポートすることも可能です。
// mathematics.js
export function multiply(a, b) { return a * b; }
export function division(a, b) { return a / b; }
export default function(a, b) { return a + b; }このとき、インポート側では次のように書き分けます:
// main.js
import add, { multiply, division } from './mathematics.js';
console.log(add(3, 4)); // 結果 : 7
console.log(multiply(3, 4)); // 結果 : 12
console.log(division(3, 4)); // 結果 : 0.75モジュール構文では、相対パスの拡張子(.js)を省略できません。
// これはNG(拡張子なし)
import { hello } from './utils'
// これはOK(.jsを明記)
import { hello } from './utils.js'
これらの応用テクニックを使いこなせば、より柔軟で読みやすいコードが書けるようになります。
ただし、チームで開発するときは命名のルールを揃えることも大切です。
少しずつ慣れていけばOKです!
解説:
ファイルを分けて管理できるようになったら、次は「どう分けるか」を考えるステージです。
実務では、モジュールを適切に分割しておくことで、保守性や再利用性が大きく向上します。
例えば、画面に関する処理、API通信、ユーティリティ関数など、役割ごとにファイルを分けるのが基本です。
// src/ui/display.js(画面に関する処理はこれ)
export function renderHeader() { ... }
export function renderFooter() { ... }
// src/api/fetchData.js(API通信に関する処理はこれ)
export async function getUserData() { ... }
// src/utils/calc.js(ユーティリティ関数、、、便利にする処理はこれ)
export function sum(a, b) { return a + b; }複数のファイルがあるときは、それらを集約する index.js を用意すると便利です。
// src/utils/index.js
export { sum } from './calc.js';
export { average } from './average.js';
// 「export { ○○○ } from './○○○.js'」と書くことで、
// 「index.js」へのインポートと他ファイルへエクスポートをまとめて実行できます!上記のようにすると、使う側はindex.jsからまとめでインポートできるので書く量が減ります!
import { sum, average } from './utils/index.js';実は「index.js」はファイル名を省略して書けます(Node.jsやモダン環境では自動的に読み込まれます)。
import { sum, average } from './utils';
import構文では、通常は相対パスでモジュールを読み込みます。
ですが、ファイルの構成が複雑になると、次のような読みにくい深いパスが頻出します
// 深い階層からのインポート例
import { sum } from '../../../lib/utils/math.js';
このようなパスは可読性が悪く、保守やリファクタリングが面倒になります。
そんなときに便利なのが、「パスエイリアス」です。
パスエイリアスを使うと、特定のディレクトリに「短い別名」をつけて、それを使ってインポートできるようになります。
// エイリアスを設定した例
import { sum } from '@utils/math.js';@utils が src/lib/utils などの長いパスを指しているイメージです。
Viteを使っている場合は、vite.config.js で次のように設定します:
// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@utils': path.resolve(__dirname, 'src/lib/utils'),
},
},
});これで、どこからでも「@utils/math.js」でインポートできます。
TypeScriptで型チェックや補完を正しく効かせるには、tsconfig.json にも以下のように書きます:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["src/lib/utils/*"]
}
}
}
エイリアスを使うことで、見通しの良いパス設計ができるようになります。
チーム開発や中〜大規模プロジェクトでは、特に効果を発揮します!
構成や命名に正解はありませんが、「迷わず使える構造か?」を常に意識しましょう。
とくにチーム開発では、命名・配置ルールの統一がスムーズな連携につながります。
※実装例は下記から!
ここまで学んだモジュール構成やファイル分割の内容を活かした実装例を、別記事にまとめています。
実際に動くサンプルも載せているので、ぜひ参考にしてみてください!
≫実装例はこちら≪
解説:
モジュール構成やscriptタグの使い方を理解していても、環境や書き方によって思わぬエラーが発生することがあります。
ここでは、実際によく遭遇する3つのエラーについて、その原因と対処法を紹介します。
モジュールを使うと、ローカルファイル(file://)で開いたときに、次のようなエラーが出ることがあります:
Access to script at 'file:///path/to/module.js' from origin 'null' has been blocked by CORS policy
これはセキュリティ上の制限で、ローカルファイルからモジュールを読み込むことが許可されていないためです。
対策としては、ローカルサーバーを使って起動するのが基本です。
npx serve などの簡易サーバーを起動python -m http.serverimport時のパス指定はとても厳密です。特によくあるのが以下のミスです:
// NG(拡張子がない)
import { hello } from './utils'
// OK(.jsを明記)
import { hello } from './utils.js'
モジュール構文では、必ず「相対パス」と「拡張子」を明記する必要があります。
Node.jsやReactなどのフレームワークと違い、ブラウザは拡張子を自動補完してくれません。
scriptタグでよく使われる defer 属性ですが、type="module" を指定している場合は不要です。
// これはOK
<script type="module" src="main.js"></script>
// これは冗長(deferは付けても無意味)
<script type="module" src="main.js" defer></script>
モジュールは自動的にdeferと同じ挙動(HTMLのパース完了後に実行)になるため、わざわざdeferを付ける必要はありません。
モジュールはすでに defer と同じ挙動を持っているため、defer を付けても意味がなく、基本的には 省略するのが推奨されます。
これらのエラーは、モジュールに慣れていないうちは特につまずきやすいポイントです。
でも大丈夫!原因と対処法を知っておけば、スムーズに開発が進められます。