Meteor Guide 1.4のApplication Structureを勉強がてら、勝手に翻訳(意訳)してみました。
また、前の記事ではCodeStyleについて意訳しています。まず日本語でざっと読んで、原文に行けば良いのかなぁと思います。素人の意訳ですので、ご理解ください。なお、ご指摘は大歓迎です。誤訳などがあれば、教えて下さい。
Application Structure
クライアントやサーバへのコードの受け渡しや複数アプリへのコード分割など、ES2015モジュールを用いたMeteorアプリの構成について
この記事を読むと、以下のことがわかります:
- ファイル構成において、どのようにMeteorアプリケーションが他のアプリケーションと違うのか
- 大小のアプリケーションをどう構成するのか
- 管理しやすくするには、どうコードと名称をフォーマット化するのか
Universal JavaScript
MeteorはJavaScriptアプリケーションを構築するためのフルスタックフレームワークです。これは、Meteorアプリケーションが、他の殆どのアプリケーションと異なることを意味します。それらは、WebブラウザやCordovaモバイル内などのクライアント上で実行されるコードを含み、Node.jsコンテナー内などのサーバ上で実行されるコードを含み、そして、両方の環境で実行される共通コードを含みます。Meteorのビルドツールは、任意のサポートしているUIテンプレートやCSSルールや静的ファイルを含むJavaScriptのコードを、S2015のインポートとエクスポートの組み合わせとMeteorビルドシステムのデフォルトファイルロード順序のルールを使用して、どの環境で実行させるかを簡単に指定することができます。
ES2015 modules
Meteor 1.3では、MeteorはES2015モジュールを完全にサポートするようになりました。標準ES2015モジュールは、JavaScriptモジュールフォーマットとロードシステムで幅広く使用されているCommonJSとAMDに替わるものです。
ES2015では、export
キーワードを使用し、ファイルの外で変数を使用することができます。他のファイルに定義している変数を使用するには、ソースパスを添えてimport
する必要があります。変数をエクスポートしているファイルは、再利用可能なコードユニットとして”モジュール”と呼ばれます。使用するモジュールとパッケージを明確にインポートすることで、グローバルシンボルの紹介や遠隔作用を回避するなどモジュールを用いたコードを書くことができます。
ES2015がMeteor 1.3の新しい機能として発表されて以来、古いバージョンのコードをオンライン上で多く見かけます。それらの古いコードでは、より中央集中的な慣習がグローバルシンボルを定義しているパッケージやアプリの周りに作られています。この古いシステムはまだ機能しており、新しいモジュールシステムコードは、アプリケーションのimports
ディレクトリ内に置く必要があります。近い将来、Meteorでは、デフォルトでモジュールを機能させるようにする予定です。なぜならば、広いJavaScriptコミュニティで開発者のコードの書き方を統一させることができるからです。
モジュールシステムについては、modules
package READMEに記載されています。このパッケージは、自動的に全ての新しいMeteorアプリをecmascript
meta-packageの一部として含んでおり、ほとんどのアプリは直ぐにモジュールを使用するすることができます。
Introduction to using import
and export
Meteorでは、JavaScriptファイルだけではなく、CSSやHTMLファイルも読み込み順序をコントロールするためにimport
させることができます。
インポートスタイルについては、Build Systemの記事に詳細が記載されています。
Meteorでは、標準ES2015モジュールのexport
シンタックスもサポートしています。
Importing from packages
Meteorでは、npmパッケージをクライアントやサーバにロードするためにimport
シンタックスを使用したり、パッケージからエクスポートされたシンボルにアクセスすることが簡単に素直に実現できます。もちろん、Meteor Atmosphereパッケージからのインポートも可能ですが、npmパッケージとのコンフリクトを避けるために、Atmosphereパッケージを使用する際はインポートパスの先頭にmeteor/
を付ける必要があります。例えば、moment
をnpmから、HTTP
をAtmosphereからインポートする場合は、次のようになります。
imports
の使用方法については、Meteor GuideのUsing Packagesに詳しく記載されています。
Using require
Meteorでは、import
ステートメントは、CommonJSのrequire
シンタックスにコンパイルされます。しかし、慣習として、require
ではなく、import
を使用することをオススメします。
そうは言っても、require
を使用しなければならない場合があります。共通ファイルからクライアントかサーバだけのコードが必要な場合です。import
は必ずトップレベルのスコープである必要があり、if
ステートメント内に記載することができません。そのため、次のようなコードにする必要があります。
ちなみに、(要求される名前がランタイム時に変化する可能性がある)require()
の動的呼び出しは、正確に分析されないと、壊れたクライアントバンドルを返す可能性がありますので、注意してください。
もし、default
エクスポート付きでES2015モジュールをrequire
する場合は、require("package").default
でエクスポートをつかむことができます。
require
を使用するその他のケースとしては、CoffeeScriptファイル内です。CoffeeScriptはimport
シンタックスをまだサポートしていないため、require
を使用しなければなりません。
Exporting with CoffeeScript
CoffeeScriptでは、変数のインポートシンタックスが異なるのみならず、エクスポートも異なります。エクスポートしたい変数をexports
オブジェクトに入れる必要があります。
File structure
モジュールシステムを全て使用し、コードを要求した時にのみ実行させるため、全てのアプリケーションコードをimports
ディレクトリ配下に置くことをお勧めします。これは、Meteorのビルドシステムが、import
されているファイルのみを対象に含めることを意味します(これは、”lazy evaluation or loading”と呼ばれます)。
Meteorは、デフォルトのファイルロード順序に従い、imports
ディレクトリ以外のファイルを読み込みます(これは“eager evaluation or loading”と呼ばれます)。クライアントとサーバのエンドポイントを明確にするために、client/main.js
とserver/main.js
の2つのファイルを作成することを推奨します。Meteorでは、server/
ディレクトリ配下のファイルはサーバサイドでしか機能しません。同じように、client/
ディレクトリ配下のファイルはクライアントサイドでしか機能しません。client/
ディレクトリ配下のファイルがサーバサイドでインポートされるのを防ぎ、また逆に、sever/
ディレクトリ配下のファイルがクライアントサイドでインポートされるのを防ぎます。それが、たとえimports/
ディレクトリでネストされていようともです。
これらのmain.js
ファイルは、それら自体では機能しませんが、アプリがロードされると直ぐにクライアントサイドとサーバサイドのそれぞれで起動するstartupモジュールをインポートする必要があります。これらのモジュールでは、アプリで使用するパッケージの必要な設定をさせ、残りのコードをインポートさせる必要があります。
Example directory layout
まずはじめに、TODOサンプルアプリケーションを見てください。このアプリは、あなたがアプリを構成する上でとても良いお手本になります。ここに、ディレクトリ構成の概要を載せます。
Structuring imports
ここではimports/
内の全ファイルに注目し、モジュールを使用したコードをどう整理していくのが良いのかを考えてみましょう。 アプリがスタートする際に機能するコードをimports/startup
に置くのは理解できます。また、データとビジネスロジックをUIレンダリングコードから分けることも良いアイデアです。このアイデアに従って、imports/api
とimports/ui
のディレクトリを使用することを推奨します。
imports/api
ディレクトリにおいて、APIで用いるドメインによってコードをディレクトリで分けることは賢明です。 たとえば、TODOサンプルアプリでは、imports/api/lists
とimports/api/todos
のドメインを持っています。 それぞれのディレクトリでは、関連するドメインデータを操作するためのcollectionやpublicationやmethodを定義しています。
Note: このtodos自体がリストの一部であるような規模の大きなアプリケーションでは、これらのドメインを一つの大きな”list”モジュールに集めることは納得できます。モジュールについて実証するのに、このTodosサンプルは、これ以上分ける必要がない小ささです。
imports/ui
ディレクトリ内では、トップレベルのpages
やラッピングlayouts
や再利用できるcomponents
などで定義できるUIコードのタイプ別でグループ分けするのは理にかなっています。
これまで定義してきたモジュールにおいて、ベースのJavaScriptファイルを使用し、様々な補助ファイルを一箇所に置くことは理にかなっています。 たとえば、BlazeUIテンプレートでは、テンプレートHTMLやJavaScriptロジックやCSSルールを同じディレクトリ内に置くべきです。 ビジネスロジックを含むJavaScriptモジュールはモジュールのユニットテストと一緒に置くべきです。
Startup files
いくつかのコードは、ビジネスロジックやUIコードではなく、アプリを起動する際に必要な設定を記述しているだけのものもあります。 Todosサンプルアプリで言うと、imports/startup/client/useraccounts-configuration.js
は、useraccounts
ロジックテンプレートを設定するファイルであり(useraccounts
についてはAccountsに詳しい記事があります)、imports/startup/client/routes.js
は全てのルートを設定し、かつ、クライアントサイドで必要なコードをインポートしているファイルです。
imports/startup/client/index.js
内でこれらのファイルをインポートしています。
一つのインポートで全てのクライアントのスタートアップコードを、メインのeagerly loadedクライアントエントリポイントclient/main.js
としてインポートする簡単な方法は、次の通りです。
サーバサイドでも同じように全てのスタートアップコードをimports/startup/server/index.js
内でインポートしています。
メインサーバのエントリポイントであるserver/main.js
は、このスタートアップモジュールでインポートします。これらのファイルから変数をインポートしていないことに気付いたでしょうか。これらのファイルをインポートしたら、この順序でそれらは処理されます。
Importing Meteor “pseudo-globals”
後方互換性のためMeteor 1.3では、Meteor coreパッケージのためのグローバル名前空間を提供しています。 前のバージョンのMeteorみたいに、インポートなしでMeteor.publish
関数を直接使用することもできます。 しかし、ベストプラクティスとして、import { Name } from 'meteor/package'
シンタックスを使用してMeteorの”pseudo-globals”(擬似グローバル)を最初にロードすることを推奨します。 たとえば、こんな感じです。
Default file load order
ES2015モジュールとimports
ディレクトリを使用することを推奨しますが、Meteor1.3ではファイルのeager loadingをサポートし続けます。これは、Meteor1.2以前のアプリケーションとの後方互換性を提供するためです。シングルアプリケーションの中で、import
を使用するlazy loadingとeager loadingの両方を使用することができます。ファイルが読み込まれ、ファイルの読み込みルールが評価される際に、インポートのステートメントはファイル内でのリスト順に評価されています。
ファイルの読み込み順序のルールはいくつか存在します。それらは、アプリケーション内で全ての該当ファイルに対して淀みなく適用されます。プライオリティが高い順に以下のようになっています。
- HTMLテンプレートはいつも一番最初に読み込まれます。
main.
で始まるファイルは、最後に読み込まれます。lib/
内のファイルは、次の読み込まれます。- ファイルの階層が深い順に読み込まれます。
- ファイルは、全体パスのアルファベット順に読み込まれます。
例えば、上記のファイルリストは、正しい読み込み順序で並べられています。 main
で始まっているにもかかわらず、main.html
は2番めに読み込まれているのは、このファイルがHTMLテンプレートであり、HTMLテンプレートはいつも1番最初に読み込まれるためです。ルール1はルール2より優先度が高いわけです。 しかし、nav.html
よりあとにmain.html
が読み込まれているのは、ルール2がルール5より優先度が高いためです。
client/lib/style.js
とlib/feature/styles.js
は、同じルール4の読み込み順序に従っていますが、アルファベット順に従ってclient
の方がlib
より前に読み込まれます。
Meteor.startup`を使用し、サーバとクライアントの両サイドで、コードを実行するタイミングを管理することもできます。
Special directories
基本的に、MeteorアプリケーションのJavaScriptファイルはクライアントとサーバサイドでまとめられ、読み込まれます。 しかし、ファイルやディレクトリの名前によって、読み込み順序やどこで読み込まれるのかなどを制御することができます。 Meteorにとって特別な意味を持つファイルとディレクトリのリストを次にあげていきます。
- imorts
imports/
ディレクトリは読み込まれず、それらのファイルはimport
によってインポートされる必要があります。 -
node_modules
node_modules
ディレクトリは読み込まれません。node_modules
にインストールされたnode.jsパッケージはimport
によってインポートされるか、package.js
内でNpm.depends
で使用される必要があります。 -
client
if (Meteor.isClient) { ... }
と同じように、client
ディレクトリはサーバサイドでは読み込まれません。 クライアントサイドで読み込まれたファイルは、プロダクトモードの際、自動的にまとめられ、圧縮されます。 デベロップメントモードの際は、JavaScriptとCSSファイルはデバッグしやすいように圧縮されません。 CSSファイルは、プロダクトモードでもデベロップメントモードでも一つのファイルにまとめられます。なぜならば、CSSファイルのURLが変わることは、URLの処理に影響を及ぼすからです。
MeteorアプリケーションのHTMLファイルは、サーバサイドフレームワークと扱いが異なります。Meteorでは、
と
と“の3つのトップレベルエレメントで全てのHTMLファイルがスキャンされます。headとbodyセクションは、それぞれ1つのheadとbodyにまとめられ、クライアントの最初のページロード時に転送されます。
- server
if (Meteor.isServer) { ... }
と同じように、server/
のディレクトリは、クライアントでは読み込まれません。 パスワードや認証ロジックなどのクライアントに渡したくないコードは、server/
ディレクトリに格納しておくのが良いでしょう。
client
とpublic
とprivate
を除いて、Meteorは全てのJavaScriptフィルをまとめ、それらをNode.jsサーバノードに読み込ませます。 Meteorでは、サーバコードはリクエスト毎にシングルスレッドで実行します。これは、典型的なNodeの非同期コールバックスタイルではありません。
- public
トップディレクトリであるpublic/
ディレクトリ配下の全てのファイルはクライアントに渡されます。 これらのコードを参照する際は、URLにpublic/
を含めてはいけません。 例えば、public/bg.png
をリファレンスする場合、<img src="/bg.png" />
となります。 publicディレクトリは、favicon.ico
やrobots.txt
などを置くのに、最適なディレクトリです。 -
private
private
ディレクトリ配下の全てのファイルは、サーバコード内からのみアクセスでき、Assets
APIを通して読み込み可能です。 外部からアクセスさせたくないようなファイルやプライベートなファイルのために使用できます。 -
client/compability
このフォルダは、グローバル変数に紐づくJavaScriptライブラリとの互換性のためのフォルダです。 このディレクトリ内のファイルは、新しい変数スコープでラッピングされずに実行されます。 これらのファイルは、他のクライアントサイドJavaScriptファイルよりも前に実行されます。
サードパティのJavaScriptライブラリとしてnpmを使用し、
import
で読み込みタイミングを制御することを推奨します。
- tests
tests/
ディレクトリは、読み込まれません。 Meteor’s built-in test tools以外で、テストランナーで実行させたいテストコードを格納するために使用して下さい。
次のディレクトリは、アプリでは読み込まれません。
.meteor
や.git
などのドット”.”で始まるファイルとディレクトリpackages/
: ローカルパッケージのために使用させますcordova-build-override/
: advanced mobile build customizationsで使用されますprograms
: レガシー理由のため
Files outside special directories
特定のディレクトリ外にある全てのJavaScriptファイルはクライアントとサーバの両方で読み込まれます。 Meteorでは、変数Meteor.isClient
とMeteor.isServer
が提供されており、クライアントかサーバのどちらで処理させるのかを制御できます。
特定のディレクトリ外にあるCSSとHTMLファイルは、クライアントでのみ読み込まれ、サーバ側のコードからは使用できません。
Splitting into multiple apps
もし複雑なシステムを書いているのであれば、コードをマルチアプリケーションに分ける良いチャンスです。 例えば、管理者UIを分けるアプリケーションやモバイルとデスクトップアプリを分けるアプリのような場合です。
もう一つのよくあるケースとしては、処理プロセスを分けるケースです。負荷の高いジョブでも、シングルWebサーバを占有せず、ユーザエクスペリエンスに影響しないようにできます。
マルチアプリケーションに分ける利点を載せておきます:
- もし特定のユーザが絶対に使用しないコードを分けた場合、クライアントJavaScriptはとても小さくなります。
- 安全に異なるアプリケーションを異なるスケーリングセットアップを用いてデプロイできます。(例えば、ユーザにファイアーウォールを介した管理アプリケーションへのアクセスを強要するなどです。)
- 異なるチームで、それぞれが異なるアプリケーションを開発させることができます。
しかし、コードを分けるにはチャレンジングな箇所が多々あるため、実行する前によく考えるべきです。
Sharing code
一番のチャレンジは、異なるアプリケーション間で適切にコードを共有することです。 最も単純な解決方法は、異なるsettingsを用いて管理して、同じアプリケーションを異なるWebサーバにデプロイさせる方法です。 この方法は、異なるスケールを持つ異なるバージョンのデプロイを簡単にしますが、上記の利点が十分に活用できなくなってしまいます。
コードを分けてMeteorアプリケーションを作成したいのであれば、共有したいモジュールを用意するべきです。 これらのモジュールを使用するにあたり、パッケージシステムとしての公開方法についてよく考慮する必要があります。
もしコードがプライベート、または他に影響しないのであれば、同じモジュールを両方のアプリケーションで有することは理にかなっています。(これは、private npm modules)で実現可能です。) これを実現する方法はいくつかあります:
- 純粋な方法は、両方のアプリケーションで共通コードをgit submoduleとして単純に含める方法です。
- それ以外では、ひとつのレポジトリで両方のアプリケーションを管理し、両方のアプリ内で共通モジュールを含めるためにシンボリックリンクを使用する方法です。
Sharing data
その他に考慮するべき重要な点は、異なるアプリケーション間でどうやってデータを共有するかです。
最も単純な解決方法は、両方のアプリケーションが同じMONGO_URL
を使用し、両方のアプリケーションからデータベースへのダイレクトな読み込みと書き込みを許可する方法です。 この方法は、データベースを介したリアクティブなMeteorのお陰で上手く動作します。 一方のアプリがMongoDBにあるデータを変更させた際、同じデータベースに接続している他のアプリのユーザは、Meteorライブクエリ(livequery)のお陰で、即座にその変化を見ることができます。
しかし、APIを介し他のアプリケーションにマスタ管理者として、アクセスコントロールさせた方が良い場合があります。 この場合、異なるアプリケーションを異なるタイミングでデプロイさせるときやデータ変更方法の保守性が必要なときに役に立ちます。
サーバからサーバへのAPIを実装する簡単な方法は、直接MeteorのビルトインDDPプロトコルを使用する方法です。これは、Meteorクライアントがサーバからデータを取得する方法と同じで、アプリケーション間の通信にも使用できます。 あるクライアントサーバからマスターサーバに接続するために、DDP.connect()
を使用できます。そして、パブリケーションからの読み込みとメソッドの実行から返される接続オブジェクトを使用できます。
Sharing accounts
同じデータベースに2つのサーバがアクセスしており、認証されたユーザにDDP呼び出しを許可したい場合、一つのログイン接続で、もう一つもログインできるresume tokenを使用できます。
ユーザがサーバAに接続している場合、サーバBにも接続をオープンさせるためDDP.connect()
を使用し、サーバAのresume tokenを認証のためにサーバBに渡せます。 両方のサーバで、同じDBをを使用しているならば、同じサーバトークンは両方で機能します。 認証のコードはこんな感じになります:
この構成のコンセプトの実例は、サンプルレポジトリで確認できます。