blogこの広告は、90日以上更新していないブログに表示しています。v12でESM周りの仕様が大幅に変更されました。この記事の内容は既に古くなっているため、最新の情報は以下の公式ブログを参考にしてください。Medium追記終了この話を今日のNode学園で話すので、ご興味あればどうぞ。Modules(ESM)の仕様が策定されたものの実装がなかなか進まない、という話を1年前に発表した。Modulesネイティブサポートにも7月に入って動きがあり、draftというステータスながらも仕様がマージされ、初期実装のプルリクが投げられた。Modules仕様と実装を眺めてみたので紹介する。実はのんびり書いている間に下記のブログが公開されました。blog.jxck.io重複部分は省略してるので、上の記事と前回のスライドを合わせてご参照ください。Modulesのシンタックスは決めたが、それが実際どう動くかはホスト側が決めることになっていた。Modulesの周辺仕様を策定していく必要があるため、実装方針だけ決めれば良い他の機能に比べて時間がかかっていた。論点になったのは以下3点。いうモジュール機構を持っているNode.jsでは相互互換性という面倒な要素を考える必要があった。制限付きで互換ブラウザについては前述のスライドや記事を参照してもらうとして、以下ではNode.jsについて見ていく。今どうすればいいの?長くなるので、つべこべ言わずに今どんなコードを書けば良いのか教えて!という人のために、先に今押さえておくべきことだけピックアップ。Modulesを多くの人にとって、まだ仕様が不安定なESMでコードを今書くメリットは「かっこいい」「キマル」ということ以外にほとんどない。世間に踊らされずrequire()を書いていれば、あとでNode.jsにESMが来たときでも互換性が確保される予定なので、これまで書いたパッケージをESMからimportできる。Modulesを書きたい人は以下2点に気をつける。importを使わない。importだけを使う。__filenameは利用できなくなるので、できるだけ使わない、または後で変更する覚悟をしておく。して議論が分かれている.mjs関連に比べて、この辺りはコンセンサスが取れているようなのでそのまま通りそう。今から気をつけておくと修正リスクが減らせる。世界では普通に利用されているので要注意。ModulesがサポートされたNode.jsではエラーになる。importで書き直す必要がある。fs;readFile(...);拡張子.mjsについては議論も実装もまだ揺れているし、機械的にリネームするのも難しくないので、今はとりあえずスルーしておくのが良さそう。Modulesとして認識させるには、ファイル拡張子を.mjsにする必要がある。.jsファイルは従来のCJS扱いになるため、.jsの中でimportやexportを使うとシンタックスエラーになる。.mjsは新しい拡張子のため、ローカル開発環境のエディタ、IDE、ビルドツールやlintツールなどの設定変更が必要になる。ブラウザは拡張子は全くチェックしない。nosniffと同等にcontent-typeをチェックすることになったため、application/octet-stream等ではJavaScriptとして実行してくれない)。よって、.mjsファイルをブラウザでもJSとして実行させたい場合、配信時にcontent-typeヘッダをapplication/javascriptなどJavaScriptとして適切なものにする必要がある。そのために、例えばnginx/apacheなどのサーバー設定で.mjsファイルに対する設定を変更しなければいけない。Modulesの識別子(下記でいう'./foo'の部分)をどう扱うかについて。Node.jsのESMでもこれを追随して、URLとして扱うことになった。%などの扱いが従来のCJSの識別子とは異なる。通常の用途ではまあ問題ないと思う。systemを参照)探索方法は基本的には旧来のNode.jsの方式を継承している。従来と異なるのは、自動で付与される拡張子に.mjsが追加されていること。優先順位は.jsより高い。'./foo';と書かれた場合、以下の優先順位になる。package.jsonの探索」では、例えばmainが./barだった場合、./foo/bar./foo/bar.mjs./foo/bar.js./foo/bar.json./foo/bar.node./foo/bar/index(拡張子を変えて探索)のように探索される。これを利用して、npmパッケージをESM/CJS両対応でリリースすることができる(後述)。これを踏まえて、Node.jsでESMからESMを呼び出すのは以下のようになる。'foo!'上述の通り、識別子の拡張子は自動で補われるため省略して./fooと書ける。このままブラウザに配信しても動作しない。拡張子自動付与はNode.js独自の仕様であるため。変更/変換なしでそのままブラウザでも動かしたい場合、以下のように自分で拡張子を書いてあげる必要がある。'./foo.mjs';ただ、この書き方をすると後述するESM/CJS両対応パッケージを作りにくくなるのが悩ましいところ。Modulesでは従来のscriptに比べて以下のような違いがある。Modulesでは、従来のCJSでは使えた以下のようなグローバル変数が削除されている。requiremoduleexports__filename__dirnamearguments最初の3つが意味することは、ESMの中でrequire()を使ってCJSを読み込んだり、ESMとCJSの両方の方式でexportしたりすることはできないということ(CJSを呼び出す方法は後述)。__dirnameについては、URLベースになったため単純な置き換えが不可能として削除された。よってファイルのURLが取得可能にすることが検討されている。詳細は未定だが、こんな感じになりそう。file:///foo/bar.mjsimport.metaは、言語仕様に入らないホスト依存のメタ情報をモジュール内部に渡す役割をする特殊なプロパティ。他にはブラウザではモジュール内部からscript要素の各種属性へのアクセスなんかもこれを使うことが検討されている。Modulesを読み込むESMからは、CJSを直接importで読み込むことができる。module.exports}のようなESMとして扱われる。exportは利用できない。繰り返しになるが、以下のように書くとエラーになる。'readFile'つまり、インポート先のパッケージがESMなのかCJSなのか把握しておく必要がある。従来のrequireように、宣言的ではなく命令的に、かつ同期的にCJSを読み込みたい場合はどうするの?という要望に対しては、import.meta.requireとして提供するということも検討されているようだ。(ついさっき実装完了した模様)でだけ使えるもの。8等で実行したCJSファイルからESMを読み込めるというわけではない(もちろんpolyfill等は出てくるだろうけど)。Importimport()はPromiseを返すので、そのまま.then()でつなぐか、awaitで受ける。Import自体は、CJSとESMのどちらの中でも使える。ESMでは条件別ロードや遅延ロードで利用される想定。importしか扱えない。handは用意されていないため、上述のように.defaultにプロパティアクセスするか、以下のようにDestructuringを使うしかない。index.jsという挙動になるので、Babel等でimport/export文をCJSにトランスパイルすると同時に拡張子を.mjsから.jsに変換し、両方をnpmパッケージにバンドルすると、ESM/CJS両対応パッケージとなる。サンプルリポジトリを用意したので詳細はこちらを参照。#60どれもpros/cons、トレードオフがあって、完璧な解決策はない。module関連のissueはbikeshedなため数百のレスがついてしまいもはやオープンな議論はほぼ不可能なんじゃないかという印象なので、個人的にはとにかく最初の仕様と実装を進めることが大事だと思う。その最初の実装として.mjsは仕様も実装もシンプルなので進めやすい。Modulesの議論をリードしているbmeckも、.mjs以外の案について排他ではなくadditionalな仕様として検討したいと発言している。Modulesがどんどん広がって本当に収拾がつかなくなってしまうので。module";については、先月のTC39でECMAScript仕様に入れるか議論されたが、ネガティブな意見が多く進まなかった。ES仕様に入れるためにはNodeだけでなくブラウザでも有益であることを示すことが求められた。module";を採用することもできる(ECMAScript仕様に準拠した拡張として可能)が、Node陣営としてはそれは行わない方針らしいので、このまま行くとこの提案はお蔵入りになりそう。.mjsの不安なところは、マネージドサービスのcontent-typeが対応してくれるかどうか。Pagesから.mjsファイルをscript要素で読み込んでも実行できずエラーになる。.mjsというNode.js特有仕様なので(Web/ブラウザの仕様ではない)、それを各種サービスのWebサーバー設定にどれだけ反映してくれるのだろうか。apacheでのデフォルト設定の改善などに向けて動いているとのこと。h2oにもプルリクが来ている。Codeの対応はマージ済み。NodeのCLIで標準入力や-eから実行するときはどうするの?拡張子を指定するためのフラグが検討中。Modulesでは以下の様なモジュールラッパーによって特殊な変数を提供していた。here});https://nodejs.org/api/modules.html#modules_the_module_wrapperESMの提供というタイミングで、識別子がURLベースになったこと、非標準なグローバル変数を外したい、といった要因で削除された。今後、こういったホストから提供される情報はimport.metaを経由するのが標準になっていくはず。その方がbrowserify的な変換もしやすいし。いつ使えるようになるの?現時点でプルリクはまだレビュー中。Modulesを学んでいるという印象。プルリクでは--harmony-modulesフラグ付きで実行することになっている(フラグの名称は議論中なので変わるかも)。さっき--experimental-modulesに変更された。freezeしてbetaリリース、10月にRCをリリース予定としている。Modulesのフラグ付き実装がリリースされそう。v10でフラグを外すのは難しいと予想する。v12でLTSデビューという流れか。いうポリフィルパッケージがリリースされたので試せる。importのポリフィルを当てるライブラリ。パフォーマンスへの配慮か、tar.gzで固めるなどパッケージングの仕方もおもしろい。JavaScriptは実現しますか?識別子の仕様の違いから、素の静的なHTTPサーバーだけで実現することは難しい。識別子の拡張子を省略するとブラウザは解釈できない。node_modules以下のnpmパッケージを参照する方法がない。'./foo'では動かないので./foo.mjsにする必要があるが、そうするとNode.js側でのフォールバックが素直にはできなくなって辛い。'foo'のような、絶対URLでもなく、.または/から始まらない文字列は認めていないのでエラーになる。Workerで変換を挟むなどして頑張る必要がある。Babelは分かったが、TypeScriptはどうなの?まだちゃんと議論は始まってないけど、おそらくちゃんとコードを書いてればTypeScript側のトランスパイルで吸収してくれそう。TypeScriptには二つのモジュール構文がある。使うというルールがある(と思う)。それが徹底されている限りはTypeScriptのトランスパイル側で吸収してくれるはず。この辺りは、型の整合性を保つ必要があるので、Babelに比べると厳格に検討されていたことが結果的に良かった。(1)はてなブログをはじめよう!teppeisさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。