ウェブエンジニア問題集
第4章

CommonJSとES Modules — requireとimportの違いと使い分け

8
この章の目次開く

Node.jsのコードを読んでいると、同じ「別ファイルを読み込む」処理なのに、2種類の書き方が出てきます。

const fs = require('node:fs');
js
import fs from 'node:fs';
js

前者は CommonJS、後者は ES Modules です。どちらもモジュールを読み込む仕組みですが、歴史、読み込みのタイミング、書ける場所、Node.jsでの判定方法が違います。

新しく書くNode.jsコードはES Modulesを基本にしつつ、既存コードや設定ファイルを読むためにCommonJSも理解する、というのが実務的な立ち位置です。
学習者学習者

requireimport が混ざっているプロジェクトを見ると、どっちに合わせればいいのか不安になります。エラーも出やすい印象です。

先生先生

まず「そのファイルがCommonJSとして動いているのか、ES Modulesとして動いているのか」を見るのが先だね。Node.jsでは package.jsontype と拡張子が判断材料になる。

モジュールとは

モジュールとは、独立したスコープを持つJavaScriptファイルです。外へ公開したい値だけをexportし、別ファイルからimportします。

// math.js
export function add(a, b) {
  return a + b;
}
js
// app.js
import { add } from './math.js';
 
console.log(add(2, 3)); // 5
js

ブラウザ向けのJavaScriptでもモジュールは使います。基本的な import / export の考え方はモジュールシステムの章でも扱っています。この章では、Node.js特有のCommonJSとの共存に絞って見ていきます。

ES Modules — import / export

ES Modules(ESM)は、JavaScript標準のモジュール方式です。importexport を使います。

名前付きexport

// config.js
export const port = 3000;
 
export function getBaseUrl() {
  return `http://localhost:${port}`;
}
js
// server.js
import { getBaseUrl, port } from './config.js';
 
console.log(port);
console.log(getBaseUrl());
js

構文: import { name } from 'module-specifier'

要素説明
name読み込みたい名前付きexport
module-specifier読み込み先。相対パス、絶対パス、パッケージ名、組み込みモジュール名など

戻り値: import 文そのものは値を返しません。指定したbindingが現在のモジュールスコープに作られます

default export

// logger.js
export default function logger(message) {
  console.log(`[app] ${message}`);
}
js
// app.js
import logger from './logger.js';
 
logger('started');
js

default exportは、読み込む側で好きな名前を付けられます。一方、名前付きexportは {} の中でexport元と同じ名前を指定します。

CommonJS — require / module.exports

CommonJS(CJS)は、Node.jsで古くから使われてきたモジュール方式です。require() で読み込み、module.exports で公開します。

// config.cjs
const port = 3000;
 
function getBaseUrl() {
  return `http://localhost:${port}`;
}
 
module.exports = { port, getBaseUrl };
js
// server.cjs
const { getBaseUrl, port } = require('./config.cjs');
 
console.log(port);
console.log(getBaseUrl());
js

構文: require(id)

引数説明
id読み込むモジュール。相対パス、絶対パス、パッケージ名、組み込みモジュール名など

戻り値: 読み込んだモジュールの module.exports

const path = require('node:path');
console.log(path.join('src', 'app.js'));
js

CommonJSは今でも多くの既存プロジェクト、設定ファイル、古いnpmパッケージで見かけます。新規コードをESMで書くとしても、読める必要はあります。

Node.jsはどちらとして扱うのか

Node.jsは、ファイルの拡張子と package.json"type" を見て、.js ファイルをCommonJSとして扱うかES Modulesとして扱うかを決めます。

条件扱い
拡張子が .mjsES Modules
拡張子が .cjsCommonJS
.js かつ最寄りの package.json"type": "module"ES Modules
.js かつ最寄りの package.json"type": "commonjs" または type なしCommonJS
{
  "type": "module"
}
json

この設定があるプロジェクトでは、通常の .js はESMとして扱われます。

Cannot use import statement outside a module は、ESMの構文を書いたファイルがCommonJSとして扱われているときによく出るエラーです。

ES ModulesとCommonJSの違い

両者の違いを、実務で効く観点に絞って整理します。

観点ES ModulesCommonJS
読み込みimportrequire()
書き出しexportmodule.exports
解析タイミング実行前に静的解析される実行時に読み込まれる
書ける場所原則トップレベルif文や関数内にも書ける
非同期非同期読み込みを前提に設計同期的に読み込む
新規コード基本はこちら既存コード・設定でよく見る

ESMの import は静的です。次のように条件分岐の中へ直接書けません。

// これは構文エラー
if (process.env.NODE_ENV === 'development') {
  import { debug } from './debug.js';
}
js

必要になってから読み込みたい場合は、動的importを使います。

import() — 必要なときに読み込む

動的importは、関数のように import() と書きます。戻り値はPromiseです。

構文: import(moduleSpecifier)

引数説明
moduleSpecifier読み込むモジュールを表す文字列。相対パスやパッケージ名など

戻り値: モジュール名前空間オブジェクトで解決される Promise

if (process.env.NODE_ENV === 'development') {
  const debugModule = await import('./debug.js');
  debugModule.debug();
}
js

戻り値がPromiseなので、非同期処理の基本で学んだ await がそのまま使えます。

組み込みモジュールはnode:プレフィックスを使う

Node.jsの組み込みモジュールを読み込むときは、node: プレフィックスを付ける書き方が推奨されます。

import { readFile } from 'node:fs/promises';
import path from 'node:path';
js
const { readFile } = require('node:fs/promises');
const path = require('node:path');
js

node:fs と書くことで、「これはnpmパッケージではなくNode.js組み込みモジュールだ」と明確になります。

どちらを使えばよいか

新しいNode.jsプロジェクトでは、基本的にES Modulesを選ぶのが自然です。ブラウザやフロントエンドツールとの整合性が高く、標準のJavaScriptとして学びやすいからです。

ただし、次の場面ではCommonJSを読む・使うことがあります。

  • 既存プロジェクトがCommonJSで書かれている
  • 設定ファイルがCommonJSを要求している
  • 古いnpmパッケージやサンプルコードがCommonJSで書かれている
  • .cjs として明示したいファイルがある

この章のまとめ

Node.jsには、ES ModulesとCommonJSという2つのモジュール方式があります。

  • ES Modulesは import / export を使う標準方式
  • CommonJSは require() / module.exports を使うNode.js由来の方式
  • .mjs はESM、.cjs はCommonJSとして扱われる
  • .jspackage.json"type" によって扱いが変わる
  • 新規コードはESMを基本にしつつ、既存コードを読むためにCommonJSも理解する

次章では、Node.jsの非同期処理とイベントループを見ていきます。モジュールで読み込んだ fshttp のAPIも、実務では非同期処理とセットで使います。

参考リンク

Node.jsクイズに挑戦するこの章で学んだNode.jsの知識を、4択クイズでアウトプットして定着させよう