YAMD:JSでモジュールを記述するための別のバイク

最近、JSで多くのことを書き始めました。現在、複雑なアプリケーションとかなり大きなライブラリ(〜5K SLoC)に取り組んでいます。 もちろん、私はモジュール性の問題に遭遇しました。



AMDはアプリケーション最適でした。ライブラリの依存関係で指定し、接続コード、ロジックを追加すると、アプリケーションの準備が整います。 しかし、ライブラリを開発するときに、AMDまたはCommonJSを使用して内部依存関係を管理する問題に遭遇しました。特にライブラリの一部が相互依存している場合、定型文が多すぎます。 したがって、JS- YAMDのモジュールの定義に対する別のアプローチを強調しました



注意! これはAMDやCommonJSの代替品ではなく、AMDを使用してアプリケーションを構築します。接続するライブラリの1つだけがYAMDを使用して構築されます。 したがって、YAMDは、外部の依存関係のない複雑なライブラリをパーツと個別のファイルに分解するためのアプローチであり、これらのファイルを組み立てるためのツールです。



この記事では、アプローチについて説明します。 あなたから、私はあなたが同じタスクに何を使っているかをコメントで知りたいです。



ヤムド



JavaScript用のモジュールを定義する別のアプローチ。 CommonJSやAMDとは異なり:



YAMDは、機能を個別のファイルに分解し、これらのファイルをまとめてアセンブリすることにより、ライブラリを作成するアプローチです。 出力ファイルはIIFEとして実行でき、1つの名前(ライブラリの名前)をCommonJSまたはAMDラッパーとしてグローバルスコープに導入します。



ライブラリを開発する際にYAMDを使用しても、ライブラリユーザーに制限や義務が課されるべきではないという原則を固守しました。



YAMDの使用は、次の場合に意味があります。



私がすでに知っていることとの類似性が引き出されると理解しやすくなります。そのため、YAMDの説明は、AMDおよびCommonJSとの相互比較を通じて提供されます。



YAMD、AMD、およびCommonJSの比較



簡単な例を使用して、YAMD、AMD、およびCommonJSを比較します。 数学ライブラリを書いていると想像してください。



ライブラリ関数は個別のファイルで選択するため、すべてのアプローチのソースディレクトリは同じになります。

> find . ./math ./math/multiply.js ./math/add.js
      
      





ソースを見てみましょう:

AMD Commonjs ヤムド
./math/add.js

 define([], function() { return function(a,b) { return a + b; }; });
      
      



 function add(a, b) { return a + b; } module.exports = add;
      
      



 expose(add); function add(a, b) { return a + b; }
      
      



./math/multiply.js

 define( ["math/adding"], function(adding) { return function(a,b) { var result = 0; for (var i=0;i<a;i++) { result = adding(result, b); } return result; }; });
      
      



 var add = require('./add'); function multiply(a,b) { var result = 0; for (var i=0;i<a;i++) { result = add(result, b); } return result; } module.exports = multiply;
      
      



 expose(multiply); function multiply(a,b) { var result = 0; for (var i=0;i<a;i++) { result = root.add(result, b); } return result; }
      
      



USAGE(すべての依存関係が含まれると仮定)

 require( ["math/add", "math/multiply"], function(add, multiply) { console.info(add(2,7)); console.info(multiply(2,7)); });
      
      



 var multiply = require("./math/multiply"); var add = require("./math/add"); console.info(add(7,2)); console.info(multiply(7,2));
      
      



 console.info(math.add(7,2)); console.info(math.multiply(7,2));
      
      





AMDのアプローチはかなり冗長であることが判明しました。CommonJSは、各ファイルを関数で暗黙的にラップすることでバインディングの数を削減し、YAMDはさらに一歩進んでいます-明示的なインポートなしでその一部にアクセスできるルートライブラリのルートを導入します。



YAMDと連携する



YAMDスタイルで設計されたライブラリを構築するには、 python yamd.py path/to/library



を実行する必要があります。その結果、 nameOfTheLibrary.js



ファイルが現在のディレクトリに表示されます。 ライブラリ名はソースディレクトリの名前によって設定され、さらにこの名前はライブラリをグローバルスコープに追加するために使用されます(もちろん、アセンブリがCommonJSまたはAMDモジュールで指定されている場合を除く)。



ディレクトリの名前、およびすべてのサブディレクトリとjsファイル(「.js」の前)の名前は、JSの変数名の制限に関して有効でなければなりません。



ディレクトリ階層はモジュールの階層を定義し、jsファイルはこれらのモジュールを関数(コンストラクター)で満たします-Javaのパッケージとクラス、またはC#の名前空間とクラスのようなものを取得します。



モジュールに関数を追加するには、このモジュールに対応するディレクトリにjs-fileを作成し(.jsの前のファイル名が関数名を設定します)、たとえばadd



などの任意の名前で関数を定義し、ファイルの先頭で `expose`を呼び出して渡す必要があります関数、たとえば、「expose(add);」。 他のすべてのファイルコンテンツはプライベートであり、エクスポートされた関数に対してのみ表示されます。



関数が宣言の前に使用されているのは奇妙に思えるかもしれません- expose(add);



、しかしこれはYAMDの魔法ではありませんが、JSの法的行動は高揚しています。 それにもかかわらず、ファイルの先頭でgoをexpose



し、一度だけ発生させ、それを呼び出す前にroot



への単一の呼び出しがなかったという要件があります。



アセンブリ後の前の例(数学ライブラリ)は、次のコードとほぼ同等です。

 var math = (function(){ var root = { add: function(a, b) { return a + b; }, multiply: function(a,b) { var result = 0; for (var i=0;i<a;i++) { result = root.add(result, b); } return result; } }; return root; })();
      
      





ライブラリを複雑にし、理論からの分布をライブラリに追加するとします。 それらを別のモジュール(ディレクトリ)に配置するのは論理的です。変更後、ソースディレクトリは次のようになります。

 > find . ./math ./math/multiply.js ./math/add.js ./math/distributions ./math/distributions/normal.js ./math/distributions/bernoulli.js
      
      





次に、アセンブリの後、およそ次のコードを取得します

 var math = (function(){ var root = { add: function(a, b) { return a + b; }, multiply: function(a,b) { var result = 0; for (var i=0;i<a;i++) { result = root.add(result, b); } return result; }, distributions: { normal: function() { throw new Error("TODO"); }, bernoulli: function() { throw new Error("TODO"); } } }; return root; })();
      
      





`expose`に戻ると、関数に加えて、文字列、数値、またはオブジェクトをモジュールにエクスポートできます。 別個のディレクトリを作成するのではなく、ライブラリのルートにある1つのファイルにすべてのディストリビューションを配置することで、前の例を書き換えることができます。

 // FILE ./math/distributions.js expose({normal: normal, bernoulli: bernoulli}); function normal() { throw new Error("TODO"); } function bernoulli() { throw new Error("TODO"); }
      
      





アセンブリ後、ライブラリは完全に同等になります。



相互再帰モジュール



YAMDでは、最初のモジュールである別のモジュールの機能を使用するモジュールに機能を追加できます。 ただし、CommonJSとAMDの場合、これも可能です。違いはコードの数だけです。 たとえば、Collat​​zプロセスのステップ数を計算する関数を作成します 。 最初の例のように、AMD、CommonJS、YAMDの場合、ディレクトリ構造は変更されません。

 > find math/collatz math/collatz math/collatz/steps.js math/collatz/inc.js math/collatz/dec.js
      
      





そして今、コード:

AMD Commonjs ヤムド
./math/collat​​z/steps.js

 define( ["require", "math/collatz/inc", "math/collatz/dec"], function(require, inc, dec) { return function(n) { if (n==1) return 0; if (n%2==0) return require("math/collatz/dec")(n); if (n%2==1) return require("math/collatz/inc")(n); }; });
      
      



 function steps(n) { if (n==1) return 0; if (n%2==0) return require('./dec')(n); if (n%2==1) return require('./inc')(n); } module.exports = steps;
      
      



 expose(steps); function steps(n) { if (n==1) return 0; if (n%2==0) return root.collatz.dec(n); if (n%2==1) return root.collatz.inc(n); }
      
      



./math/collat​​z/inc.js

 define( ["require", "math/collatz/steps"], function(require, steps) { return function(n) { return require("math/collatz/steps")(3*n+1)+1; }; });
      
      



 function inc(n) { return require('./steps')(3*n+1)+1; } module.exports = inc;
      
      



 expose(inc) function inc(n) { return root.collatz.steps(3*n+1)+1; }
      
      



./math/collat​​z/dec.js

 define( ["require", "math/collatz/steps"], function(require, steps) { return function(n) { return require("math/collatz/steps")(n/2)+1; }; });
      
      



 function dec(n) { return require('./steps')(n/2)+1; } module.exports = dec;
      
      



 expose(dec) function dec(n) { return root.collatz.steps(n/2)+1; }
      
      





AMDの場合の相互依存関係については、 このドキュメントに従って説明しました。requireに依存関係を追加し、それを使用して関数内に依存関係を明示的にインポートする必要がありました。



この例は3つのアプローチすべてで処理されましたが、比較的単純です。ユーザーがライブラリ関数を呼び出したときにのみ、再帰的な性質が現れ、この時点でライブラリは既にロードされています。 ライブラリの初期化時にライブラリ自体の機能を使用する必要がある場合、相互再帰の問題が発生します。 この問題は、トムの投稿でよく理解されています。



それに対抗するために、遅延初期化がexpose



に追加されexpose



expose



2番目の引数を使用して関数(モジュールコンストラクター)を渡すことができ、すべてのモジュールが読み込まれた後に呼び出されることが保証されます。



例に戻って、 steps



を高速化し、ライブラリをロードするときにn



のステップ数を計算するとします。 tableLookup



デコレータをtableLookup



てみましょう。



 function tableLookup(table, f) { return function(n) { if (n in table) return table[n]; return f(n); } }
      
      





次に、YAMDアプローチでsteps.jsファイルを次のように変更するだけです。

 // FILE ./math/collatz/steps.js var table = {}; expose(tableLookup(table, steps), ctor); function ctor() { table[3] = steps(3); } function steps(n) { if (n==1) return 0; if (n%2==0) return root.collatz.dec(n); if (n%2==1) return root.collatz.inc(n); }
      
      





CommonJS / AMDを使用する場合、同じことを実装する2つの方法があります。



ひどく判明-CommonJSでは、この問題を解決するために、APIを変更するか、単一の責任原則に違反して、ステップに遅延制御を追加する必要があります。

 // FILE ./math/collatz/steps.js var table = {}; var inited = false; function ctor() { table[3] = steps(3); } function steps(n) { if (!inited) { ctor(); inited = true } if (n==1) return 0; if (n%2==0) return require('./dec')(n); if (n%2==1) return require('./inc')(n); } module.exports = tableLookup(table, steps)
      
      





CommonJSのSRP違反に目を向けて、難しい初期化プロセスを検討する場合、両方のオプションは悪いブレーキです。YAMDの場合はライブラリが接続されているときのブレーキ、CommonJSの場合はsteps



を最初に呼び出すときのブレーキsteps



。 しかし、初期化は常に難しいとは限りません。それでもこのような場合は、YAMDを使用して、ctorから起動されたWebWorkerプロセスに転送を試みて、 `steps`の最初の実行で既に終了することを期待できます。 CommonJSも使用できません。最初のリクエストで初期化を開始する機会があるだけなので、このリクエストは計算する時間がなく、スローダウンが保証されます。



ご清聴ありがとうございました。 また、複雑なJSライブラリを開発する方法をコメントに書くことを忘れないでください。



All Articles