Node.jsとECMAScript Modules

Node.jsとECMAScript Modules

2018 / 03 / 22

目次
この記事は1年以上更新されていません
この記事は Hatena Blog からの移行記事です

Node.js のバージョン 10 のリリースは 4/25 を予定しています。 また、ECMAScript Modules は Stability1(実験的)でリリースされます。

議論は以下で行われます。

GitHub - nodejs/modules: Node.js Modules Team

Node.js Modules Team. Contribute to nodejs/modules development by creating an account on GitHub.

github.com
GitHub - nodejs/modules: Node.js Modules Team

以下、ECMAScript Modules を ESM、 CommonJS Modules を CJS と略します。

Modules: ECMAScript modules | Node.js v24.1.0 Documentation

nodejs.org

覚えておくべきこと

ESM を使いたい場合は、拡張子を.mjsにする

.jsファイルでimport/exportは使えません。 ブラウザではtype="module"となりますが、Node.js では拡張子で判断します。

.mjsの拡張子は省略可能である

拡張子の探査順は ESM の時、.mjsが優先されます。 しかし、ブラウザでもそのコードを使いたい場合、拡張子の省略は Node 独自の機構であるため省略はしないほうがいいでしょう。

ESM のファイルをトップレベルでは CJS でインポート出来ない

トップレベルで、importを使うことは許されません。 しかし、dynamic importは例外的に CJS でも使えるので一応、CJS から ESM の読み込みは行えます。

CJS のファイルをインポートするのに named import は行えない

import {x} from 'y'の書き方はできません。 CJS のオブジェクトはdefaultで包まれるためです。 なので、もし行いたい場合は、一度defaultに alias をする必要があります。 webpack4 もそのように対応しています。

blog.hiroppy.me

Babel でのトランスパイルはimport { readFile } from 'fs'等の書き方ができてしまうため、そのまま Node へ移すと壊れます。

ESM のパスは whatwg url に準拠している

Node9 で whawg url は Stability2 になり、ブラウザ同様にグローバルに URL オブジェクトが置かれました。 今までは、require('url').URLでした。 もしクエリー(?)が異なる場合は、たとえ同じファイルでも複数回ロードします。

import "./foo?query=1"; // ?query=1としてのfooがロードされる
import "./foo?query=2"; // ?query=2としてのfooがロードされる
import "file:///xxx/foo";
Node の変数である、__dirname__filename等が使えない

stage-3 のimport.metaを使いましょう。

GitHub - tc39/proposal-import-meta: import.meta proposal for JavaScript

import.meta proposal for JavaScript. Contribute to tc39/proposal-import-meta development by creating an account on GitHub.

github.com
GitHub - tc39/proposal-import-meta: import.meta proposal for JavaScript

index.mjs
console.log(import.meta);
// { url: 'file:///xxx/index.mjs' }

.mjsは厳格モード(use strict)になる

仕様です。


その他、挙動が違う部分は下の 早見表 を参照してください。

実行方法

$ node -v
v10.0.0-pre
$ node --experimental-modules index.mjs

このように、今現在は--experimental-modulesというフラグが必要です。

パターンまとめ

インポートされるモジュール

test.mjs
export const a = 1;
export default "dog";

ルートが CJS

index.js
// === 🙅bad ===
// ESMのコードをCJSで呼び出すことはできません
const test = require("./test"); // Must use import to load ES Module
// CJSにESMのSyntaxは存在しません
import { a } from "./test"; // SyntaxError: Unexpected token {
// ---------------------------------------------------------
// === 🙆 good ===
console.log(this); // {}
// dynamic import
// CJS内でもdynamic importの使用は可能です
(async () => {
const test = await import("./test");
console.log(test); // [Module] { a: 1, default: 'dog' }
})();

ルートが ESM

index.mjs
// === 🙅bad ===
// ESMにCJSのSyntaxは存在しません
const test = require("./test"); // ReferenceError: require is not defined
// __dirnameは定義されていないので、エラーとなります
console.log(__dirname);
// fsはNodeのネイティブモジュールであるため、CJSで書かれています
// モジュールがCJSの場合、named importは使えません
import { readdirSync } from "fs"; // syntaxError: The requested module 'fs' does not provide an export named 'readdirSync'
// ---------------------------------------------------------
// === 🙆 good ===
console.log(this); // undefined
import * as t from "./test";
console.log(import.meta); // { url: 'file:///xxxx/index.mjs' }
// whatwg urlに準拠しているので、urlと同様の書き方が可能です
import * as t from "file:///xxx/test";
console.log(t); // [Module] { a: 1, default: 'dog' }
// =========================
// CJSのモジュールをESMで入れる方法は以下のとおりです
import fs from "fs";
console.log(typeof fs.readdirSync);
// CJSはdefaultに包まれる
import * as fs from "fs";
console.log(typeof fs.default.readdirSync);
// defaultをfsにリネームする
import { default as fs } from "fs";
console.log(typeof fs.readdirSync);
// =========================
// dynamic import
(async () => {
// whatwg urlでの指定が可能です
// 今現在、file以外での取得は不可能です
const baseURL = new URL("file://");
baseURL.pathname = `${process.cwd()}/test.mjs`;
const test = await import(baseURL);
console.log(test); // [Module] { a: 1, default: 'dog' }
})();

挙動の早見表

CJS にはデフォルトで厳格モードがつかないので、このテーブルは CJS は厳格モードではない状態での比較です。

モジュール

CodeCJSESM
import('x')okok
import 'x';errorok
export {};errorok

タイミング

CodeTimingHoistedBlocking
require('x');syncnoyes
import 'x';untimed (async generally)yesyes
import('x');asyncnono

ESM では使えないメソッド・変数

以下のメソッド・変数は、CJS でのみ存在し、ESM では存在しないため、エラーになります。

  • __dirname
  • __filename
  • require
  • exports
  • module
  • arguments

予約語への操作

e.g. var let = 1;

CodeCJSESM
argumentsscope::localerror
arguments = []okerror
try {} catch (arguments) {}okerror
evalscope::localerror
eval = evalokerror
try {} catch (eval) {}okerror
implementsokerror
interfaceokerror
packageokerror
privateokerror
protectedokerror
publicokerror
staticokerror
awaitokerror
letokerror
returnerrorerror
yieldokerror
awaitokerror

厳格モード時に変数として使えなくなる予約語です。

  • arguments
  • eval
  • implements
  • interface
  • package
  • private
  • protected
  • public
  • static
  • let
  • yield
index.js
var arguments;
arguments = [];
var eval = 1;
eval = eval;
// constは予約語ですが、letは違います
var implements,
interface,
package,
private,
protected,
public,
static,
let,
yield,
await;

上記以外の許容されない記法

CodeCJSESMSCRIPT
with({}){}okerrorok
<!--\nokerrorok
-->\nokerrorok
0111okerrorok
(function (_, _) {})okerrorok
index.js
// HTMLコメントはCJSでは使えますが、ESMでは使えません
<!--\n
-->\n

# CJS
$
# ESM
$ Module build failed: SyntaxError: Unexpected token (1:0)
> 1 | <!--\n
| ^
2 | -->\n

index.js
0111;

# CJS
$
# ESM
$ SyntaxError: Octal literals are not allowed in strict mode.

評価に関する違い

パース、実行はできるが、評価結果が異なります。

CodeCJSESMSCRIPT
thismodule({})undefinedglobal
(function (){return this}())globalundefinedglobal
(function () {return typeof this;}).call(1)objectnumberobject
var xlocallocalglobal
var x = 0; eval('var x = 1'); x101
console.log(
function () {
return typeof this;
}.call(1),
);

# CJS $ object # ESM $ number

callthisを 1 として束縛し、スコープで包んだfunctionthisを返しその型を評価します。 thisの束縛がない場合は、スコープで包まれているため、typeof this === '[Function]'です。 CJS の場合は、[Number: 1]となり、ESM のときは 1 となります。 つまり、ESM のときはnew Number(1)とならないので、numberとなります。

さいごに

今後、Node 界隈では、.mjsという拡張子が主流になる未来が予想されます。(自分はあまり望んでませんでしたが。。。) このまま順当に行けば、12 には Stability2(安定的)に行ける気がするので、来年ぐらいから本番かなーと思っています:D


前後の記事

関連する記事