JavaScriptのexportはexport default以外禁止にしてしまった方が楽になる
追記: 2020-12-11
別にそうでもないなと考えを変えました.
なぜ default export を使うべきではないのか? - LINE ENGINEERING
には納得できる論も多いですし, この時やっていたプロジェクトのモジュールがつらいのは単に命名がまずかったからです.
概要
相当遅ればせながらJavaScriptのモジュールについて調べて, 自分なりの付き合い方をまとめました.
結論: export default
のみを使おう. 他のexport
はやめよう.
JavaScriptコードのimportがつらい状況になっている
本質的な問題ではないですが, 以下のようなコードがあってつらい.
import {
actionFoo,
actionBar,
actionBaz,
actionQux,
} from '../redux/action';
本物はもっとひどく, 名前付きimportの名前が50行程度あります. これをまともな形式に修正したい. 数個ならともかく, 50個もimportするならそれを列挙するのはつらすぎる.
JavaScript(Babel, ES2015)のモジュール機構に詳しくないのでどう修正すれば良いのかぱっと見わからない. どれが利用されるようになるかわからない状況なのでまともに学習していなかったので, JavaScriptのモジュールを未だ把握していないので, 基礎的な知識がない. 見に行きます.
import - JavaScript | MDNを見る限り,
修飾無しで全てのexport
をimport
する方法は無いように見えます.
いやまだ諦めたくないです.
ES6 — modules – ECMAScript 2015 – Medium
によると,
import * from 'modules';
という書き方が出来るように見えるのですが,
手元で確かめてみると構文エラーになります.
規格を見ても,
そんな構文は無いようです.
古い提案ですかね?
修正するのは無理っぽいですね. つらい.
JavaScriptで名前で空間構造を分けるのはアンチパターン
どうすれば良かったのか.
actionを纏めるならAction
オブジェクトみたいなものを作って,
そのメンバとしてそれぞれのアクションを参照するようにするべきなんですよね.
export default class Action
に静的メソッドや静的プロパティとしてそれぞれのaction定数を定義するのが良い.
そうしたらAction.foo()
のような構文になります.
もしくはせめて,
それぞれの関数をactionという名前をつけずにexport function
するのが良い.
そしたらimportする側ではActionオブジェクトを作って結果的にAction.foo()
のような構文になります.
camelCaseでもSNAKE_CASEのどちらでもJavaScriptで名前で空間構造を分けるのはアンチパターンです. JavaScriptにはせっかくオブジェクトがあるのですからオブジェクトで名前を分けましょう. C++の名前空間などとは違って構文もスッキリとしていますし.
JavaScriptのimport/export(モジュール)がつらすぎる
この件にぶちあたって初めてJavaScript(ES2015)のモジュールを真面目に調べてやっとまともに学習しました. JavaScriptのモジュールはやはりつらいと再確認しました.
他の言語ではモジュール名に相当するものがJavaScriptではファイル名です.
そして,
JavaScriptは他の言語と違ってモジュール名と識別子の両方をimport
する側が決めなければいけません.
Haskellではimport Data.Ratio
のようにimport
で指定するのはモジュール名だけで十分です.
デフォルトではグローバルな名前空間にデータ型や関数などの識別子が展開されます.
グローバルな名前空間に展開されるので定番のモジュール以外はよく衝突しますが,
衝突したときはas
や名前付きimportで衝突を回避することができます.
衝突した場合はas
で識別子をimport
する側が決めなければいけませんが,
衝突しない場合は問題ありません.
Javaではimport java.lang.Math;
のようにimport
で指定するのはパッケージ名だけで十分です.
Javaでは1つのパッケージで1つのクラスだけが公開できてそれが識別子となり,
メソッドやプロパティなどはクラス以下に存在するので,
衝突することはほぼありません.
衝突したら上位のパッケージ名を指定して回避することが出来ます.
グローバルな名前空間に展開したいときのみimport static
を使うことが出来ます.
import static java.lang.Math.*;
すればMath
以下の識別子が全てグローバルな名前空間に展開されて楽ですね.
JavaScriptでimport
する時はファイル名と識別子の両方を指定する必要があります.
この時点でつらい.
モジュールのオブジェクトがexport default
のみを使ってexport
されていた場合,
Javaのような形式になります.
import Foo from "foo"
のような形式でimport
を行った場合,
識別子はFoo
以下に展開されます.
モジュールが複数export
を行っていた場合,
import * as Foo from "foo"
と書くことでFoo以下に関数をまとめることが出来ます.
グローバルな名前空間に識別子を展開したい場合,
名前付きimport
を使うことでのみそれが可能です.
その際ワイルドカードなどを使って全て展開することは不可能です.
よってグローバルな名前空間で使うことを想定した関数群を含むモジュールをimport
する際,
使う識別子を全て列挙するという地獄が発生します.
要約すると,
- Haskellではデフォルトで全ての識別子をグローバル名前空間に展開できて, 衝突した場合のみ識別子に別名を付けて
import
出来る - Javaではデフォルトではクラス名の識別子しかグローバル名前空間に展開できない,
import static
を使えばグローバル名前空間に展開できる - JavaScriptではデフォルトで
import
側が識別子に別名を付けないとimport
できない, グローバル名前空間に展開する時は一つ一つ識別子を列挙する必要がある
ちょっと辛すぎますね… 動的型付け言語だからというのを考えてもつらいです.
あまりつらくならずにJavaScriptのモジュールと付き合う方法
export
する側は,
export default
のみを使いましょう.
export default class
を使えばJava風になってそこまでつらくないです.
単一の関数のみをexport
したい場合はexport default function
を使いましょう.
import
する側の識別子も小文字初めで付けることが出来るので,
問題ありません.
ただ,
export default
する際もclass
やfunction
に名前はちゃんと付けましょう.
それを参照すれば,
import
側が識別子に悩むことが無くなるためです.
名前をちゃんと付ければ,
静的解析ツールやIDEの助けにもなります.
import
する側はimport Foo from "foo"
の形式のみを使いましょう.
1つのオブジェクトに識別子を纏めることで,
混乱を避けることが出来ます.
複数の識別子をexport
したい場合は,
class
にまとめたりobjectに纏めることでexport default
にしましょう.
複数の関数をオブジェクトにまとめずにexport
したい時は,
本当にそれが必要なのか,
それで設計が破綻しないのかちゃんと考えましょう.
複数の関数をexport
しても,
import
する側はどうせimport * as Foo from "foo"
のように1つのオブジェクトにまとめます.
それなら初めからexport
する側がオブジェクトに纏めたほうが,
import
する側は混乱しません.
グローバル名前空間に展開する関数を複数export
したい?
本当にそれは必要なことなのですか?
関数名に共通するプレフィクスを取り出してclass
に纏めてしまうことで,
回避できませんか?
必要だとしても,
そうするとimport
する側は一つ一つグローバル名前空間に展開する識別子を列挙する必要があって地獄が発生しますが,
本当にそれで良いですか?
今は数個だとしても,
識別子が増えてきて破綻しませんか?
どうしてもimportする方でグローバル名前空間で識別子を使いたいのならば, それだけ変数束縛してもらいましょう.
とにかくexport
はexport default
以外禁止にしてしまえば,
識別子をimport
側にも書かないといけないこと以外は平穏にJavaScriptのモジュールと付き合っていくことが可能です.
Axel Rauschmayer博士もdefault exportを推奨しています. ECMAScript 6 modules: the final syntax
勿論既存のコードとは折り合いを付けなければいけませんが…
つらい
既にとあるプロジェクトの大部分のコードが大量名前付きimportする設計に依存しているので今更修正できない. とてもつらい.
import * as Action from '../redux/action'
とするのも不可です.
既存のコードをぶち壊しますし,
Action.actionFoo()
となって名前が重複するからです.