Meteor Fan (日本語情報)

JavaScriptフレームワークMeteorに関していろいろ紹介する日本語情報サイト

Meteor 1.3でJavaScriptのmodulesを使う

Meteor 1.3ではmodulesのサポートが追加されます。本記事では、package/modulesのREADME.mdに従ってmodulesの使い方を説明します。

執筆時点のREADME.mdのバージョンはこちらです。

はじめに

Meteor 1.2でES2015のサポートが追加されましたが、期待の大きかったimport/export構文は入りませんでした。Meteor 1.3ではこの標準のモジュールシステムがサポートされるようになります。これはサーバサイドとクライアントサイドの両方で使えることができ、Meteorで長らく懸案事項だったファイルロード順の問題が解決されます。それでいて後方互換性を持ち、今までのコードもそのまま動きます。

モジュールを有効にする

Meteor 1.3ではmodulesパッケージはデフォルトでインストールされています。しかし、このパッケージは必須ではありません。これを既存アプリや既存パッケージに追加するかどうかは開発者の自由です。

アプリの場合は、meteor add modulesとすると有効にできます。もしくは、よりおすすめはmeteor add ecmascriptとすることです。ecmascriptにはmodulesが含まれているからです。

パッケージの場合は、package.jsファイルのPackage.onUsePackage.onTestのセクションにapi.use("modules")を追加すると有効にできます。

ところで、ecmascriptパッケージを使わずにmodulesパッケージを使う意味があるかについて補足します。modulesパッケージのみを使った場合、NodeでおなじみのCommonJSのrequireexportsが提供されます。ecmascriptは単にimportexportの構文をCommonJSに変換しているだけになります。requireexportsを使ってMeteorアプリでNodeモジュールを使うこともできます。modulesを別パッケージに分けておくことで、ecmascriptが使えない場合でもrequireexportsが使えます。実際に、ecmascriptパッケージの実装はそのようになっています。

modulesパッケージ単体でも便利ですが、ecmascriptパッケージを使ってimportexportを使うことがおすすめです(requireexportsではなく)。より説得材料が必要であれば、http://benjamn.github.io/empirenode-2015をご覧ください。

基本文法

importexportの文法には様々な種類がありますが、まずは誰もが知っているべき基本の形式について述べます。

最初に、宣言の前にexportとつけることでその宣言の名前でエクスポートすることができます。

1
2
3
4
5
6
7
// exporter.js
export var a = ...;
export let b = ...;
export const c = ...;
export function d() {...}
export function* e() {...}
export class F {...}

上記の宣言では、a, b, cといった変数がこのexporter.jsモジュールのファイルのスコープだけでなく、exporter.jsimportした他のモジュールからも利用できるようになります。

もしくは、宣言の前につけるのではなく、分けてexportと変数名を書くことでエクスポートもできます。

1
2
3
4
5
6
// exporter.js
function g() {...}
let h = g();
// at the end of the file
export {g, h};

これらすべてのエクスポートは「名前つき」であり、つまり他のモジュールからはその「名前」でインポートできることになります。

1
2
3
// importer.js
import {a, c, F, h} from "./exporter";
new F(a, c).method(h);

もし別の名前を使いたい場合は、exportimportで名前を変えることができます。

1
2
3
// exporter.js
export {g as x};
g(); // same as calling y() in importer.js
1
2
3
// importer.js
import {x as y} from "./exporter";
y(); // same as calling g() in exporter.js

CommonJSのmodule.exportsと同様に、一つのデフォルトのエクスポートを定義することもできます。

1
2
// exporter.js
export default any.arbitrary(expression);

このデフォルトのエクスポートは、波括弧なしてインポートすることができ、その際に名前をつけることになります。

1
2
3
// importer.js
import Value from "./exporter";
// Value is identical to the exported expression

CommonJSのmodule.exportsとは異なり、デフォルトエクスポートを使う場合でも名前つきエクスポートを使うこともできます。下記は両方を混在させた例です。

1
2
// importer.js
import Value, {a, F} from "./exporter";

実は、デフォルトエクスポートは単に”default”と名づけられたエクスポートにすぎません。

1
2
// importer.js
import {default as Value, a, F} from "./exporter";

これらの例をもとに、importexportの文法を使い始めることができるでしょう。より詳しい情報は、Axel Rauschmayerによる詳細な説明に載っている様々なimportexportの文法を参照してください。

モジュールアプリケーション構造

Meteor 1.3がリリースされる前は、アプリケーション内でがファイル間で値を共有するにはグローバル変数を割り当てるか、Sessionのような共有変数(一応、グローバルではない)で伝達するしかありませんでした。モジュールの導入により、あるモジュールから別のモジュールのエクスポートされた値を参照できるようになるため、グローバル変数はもう使わなくてよくなります。

Nodeでのモジュールに慣れていると、モジュールはインポートされた時に初めて評価されるものと期待するかもしれません。しかし、Meteorの以前のバージョンではすべてのコードがアプリケーション開始時に評価されました。そこで後方互換性を維持するため、開始時評価がデフォルトのふるまいとなります。

もしモジュールを遅延評価(Nodeと同じように初回インポート時に評価)したい場合は、そのモジュールをimports/ディレクトリ(アプリ内のルートディレクトリ以外のどこでも)に入れ、インポートするときにそのパスを指定します。例えば、import {stuff} from "./imports/lazy"のようにします。node_modules/内のファイルは常に遅延評価されます(後述)。

Meteorの将来のバージョンでは、遅延評価はデフォルトの動作になる可能性が高いです。もし現時点からその方式を採用したいのであれば、おすすめの方法はすべてのモジュールをclient/imports/ディレクトリやserver/imports/ディレクトリに格納し、それぞれclient/main.jsserver/main.jsに単一のエントリーポイントを作ることです。main.jsファイルはアプリ起動時に最初に評価され、そこからimports/ディレクトリ内のモジュールが評価されます。

モジュールパッケージ構造

パッケージを作成する場合は、package.jsファイルのPackage.onUseapi.use("modules")api.use("ecmascript")を追加するのに加えて、メインエントリポイントを指定するためにapi.mainModuleという新しいAPIを使用することもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Package.describe({
name: "my-modular-package"
});
Npm.depends({
moment: "2.10.6"
});
Package.onUse(function (api) {
api.use("modules");
api.mainModule("server.js", "server");
api.mainModule("client.js", "client");
api.export("Foo");
});

ここでserver.jsclient.jsはパッケージの他のディレクトリにあるファイルをインポートできます。この際、それらのファイルはapi.addFilesによって追加されている必要はありません。

api.mainModuleを使う場合、メインモジュールでエクスポートされたものはPackage["my-modular-package"]でグローバルにアクセス可能になります。加えて、api.exportでエクスポートされたものはそのパッケージをインポートすることで自動でインポートされます。言い換えると、メインモジュールはFooのエクスポートをapi.exportで決めますが、他のプロパティも明示的に指定することでインポートはできる状態になります。

1
2
3
4
5
// In an application that uses my-modular-package:
import {Foo as ExplicitFoo, bar} from "meteor/my-modular-package";
console.log(Foo); // Auto-imported because of api.export.
console.log(ExplicitFoo); // Explicitly imported, but identical to Foo.
console.log(bar); // Exported by server.js or client.js, but not auto-imported.

ここでの注意点は、importfrom "my-modular-package"ではなく、from "meteor/my-modular-package"とすることです。MeteorパッケージはNodeパッケージと区別するために、meteor/...で始まるようにします。

最後に、このパッケージは新しいmodulesパッケージを使っており、Npm.dependsによって、”moment”というNodeパッケージに依存しているため、このパッケージ内ではimport moment from "moment"とすることで、クライアントサイドでもサーバサイドでもこのモジュールをインポートできます。以前のMeteorではNpm.requireでNodeパッケージをサーバサイドでしかインポートできなかったため、これはすばらしいことです。

ローカルのnode_modules

Meteor 1.3より以前では、Meteorアプリケーションのnode_modulesディレクトリの中身は完全に無視されていました。modulesを有効にすると、この無意味だったnode_modulesディレクトリが突然すばらしく意味を持つことになります。

1
2
3
4
5
6
7
meteor create modular-app
cd modular-app
mkdir node_modules
npm install moment
echo 'import moment from "moment";' >> modular-app.js
echo 'console.log(moment().calendar());' >> modular-app.js
meteor

このアプリを実行すると、momentライブラリがクライアントサイドでもサーバサイドでもインポートされ、両方のコンソールでToday at 7:51 PMのようなログが出力さ出るでしょう。このようにnpmモジュールを直接インストールできるようになると、https://atmospherejs.com/momentjs/momentのようなnpmラッパーパッケージを減らせることが期待されます。

Meteor 1.3のベータリリースでは、npmモジュールはクライアントサイドでもサーバサイドでも同じエントリーポイント(package.jsonファイルのmainフィールドか、index.js)でインポートされます。そのため、ブラウザで動くことが分かっているnpmモジュールだけを慎重にクライアントサイドにインポートする必要があります。今後は、package.jsonファイルのbrowserフィールドをサポートすることを検討する予定ですが、ベータリリースには入っていません。

ファイルロード順序

Meteor 1.3以前では、どのファイルが先に評価されるかの順番はStructuring Your ApplicationFile Load Orderに書かれているルールによって決まっていました。このルールは、あるファイルが別のファイルの変数に依存しているのに読み込みの順が逆になる場合など、制御にしくいものでした。

モジュールのおかげで、いかなる依存関係によるロード順もimport文で解決できます。例えば、a.jsb.jsより先に読み込まれる(ファイル名の順序で)としても、a.jsb.jsの何かが必要な場合は、a.jsb.jsの値をimportすればいいだけになります。

1
2
3
// a.js
import {bThing} from "./b";
console.log(bThing, "in a.js");
1
2
3
// b.js
export var bThing = "a thing defined in b.js";
console.log(bThing, "in b.js");

場合によってはモジュールが別のモジュールの値をインポートする必要がなくとも先にロードして欲しいことがあるかもしれません。その場合は、より簡単なimport文を使うこともできます。

1
2
3
// c.js
import "./a";
console.log("in c.js");

この場合、どのモジュールが先にインポートされても、console.logの呼び出し順は常に次のように一定になります。

1
2
3
console.log(bThing, "in b.js");
console.log(bThing, "in a.js");
console.log("in c.js");

まとめ

モジュールのサポートはMeteor 1.3の目玉機能の一つです。これまでのMeteorでは暗黙的だった動作を明確に記述することができ、コード量は増えるもののそれを上回るメリットがあると期待できるでしょう。