技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

Node.jsとECMAScript Modules

2018/03/22の記事です。

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

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

github.com

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

ECMAScript Modules | Node.js v9.9.0 Documentation

覚えておくべきこと

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.com

// 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は厳格モードではない状態での比較です。

モジュール

Code CJS ESM
import('x') ok ok
import 'x'; error ok
export {}; error ok

タイミング

Code Timing Hoisted Blocking
require('x'); sync no yes
import 'x'; untimed (async generally) yes yes
import('x'); async no no

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

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

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

予約語への操作

e.g. var let = 1;

Code CJS ESM
arguments scope::local error
arguments = [] ok error
try {} catch (arguments) {} ok error
eval scope::local error
eval = eval ok error
try {} catch (eval) {} ok error
implements ok error
interface ok error
package ok error
private ok error
protected ok error
public ok error
static ok error
await ok error
let ok error
return error error
yield ok error
await ok error

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

  • 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;

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

Code CJS ESM SCRIPT
with({}){} ok error ok
<!--\n ok error ok
-->\n ok error ok
0111 ok error ok
(function (_, _) {}) ok error ok
// 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.

評価に関する違い

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

Code CJS ESM SCRIPT
this module({}) undefined global
(function (){return this}()) global undefined global
(function () {return typeof this;}).call(1) object number object
var x local local global
var x = 0; eval('var x = 1'); x 1 0 1
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