以下は、私がまとめた大規模な JavaScript アプリケーション アーキテクチャの設計パターンです。興味のある学生は参照してください。
以下はこの記事の主な章です:
1. 「JavaScript ラージ プログラム」とは何ですか?
2. 現在のプログラムのアーキテクチャを検討する
4. ブレインストーミング
5.1 設計パターン
5.1.1. 概要
5.1. 1.2 モジュールモード
5.1.
5.2 アーキテクチャに適用する 5.2.1 ファサード - コアの抽象化 5.2.2 Mediator - プログラムのコア 5.2.3 緊密に連携する 6. Pub を公開する/subscribe Sub 拡張子: 自動登録イベント 7. Q&A8. 「JavaScript ラージ プログラム」とは何ですか?始める前に、大規模な JavaScript サイトとは何かを定義しましょう。多くの経験豊富な JS 開発専門家も、100,000 行を超える JavaScript コードが大規模であるとみなされると言う人もいます。 100,000 行以上であること。サイズが 1MB を超えた場合にのみカウントされます。実際には、インストールされたコードの量では測定できないため、どちらも正しいとは言えません。
私の「大きな」の定義は次のとおりです。正しくないかもしれませんが、比較的近いものであるはずです:
私は個人的に、大規模な JavaScript プログラムは非常に重要であり、多くの優れた開発者の努力が組み込まれているべきだと考えています。ヘビーウェイト データを処理してブラウザに表示するプログラム。 現在のプログラム アーキテクチャを確認するこの質問がどれほど重要であるかを強調することはできませんが、多くの経験豊富な開発者はよく次のように言います。もう少し大きなプログラムで再度使用しても問題ありません。「特定のプログラムではこれが当てはまりますが、大きなプログラムであるため、通常は分解して注意する必要がある大きな懸念事項があることを忘れないでください。」 , 長く稼働しているプログラムのアーキテクチャを見直すには時間がかかることを簡単に説明します。ほとんどの場合、現在の JavaScript プログラム アーキテクチャは次のようになります (これは、私たちがよく呼ぶ ASP.NET MVC ではなく、JS アーキテクチャであることに注意してください): カスタム ウィジェット モデル
ビューコントローラー
テンプレートライブラリ/ツールキット
アプリケーション コア。プログラムを複数のモジュールに個別にカプセル化することも、他の設計パターンを使用することもできます。これは問題ありませんが、これらの構造がアーキテクチャを完全に表す場合、潜在的な問題がいくつかある可能性があります。いくつか見てみましょう。重要なポイント:1. あなたのアーキテクチャ内ですぐに取り出して再利用できるものはいくつありますか?
他のコードに依存しない個別のモジュールはありますか?自己完結型ですか?使用しているコード ベースにアクセスしてモジュール コードをランダムに選択し、新しいページに配置した場合、すぐに使用できるようになりますか?原理を理解するだけで十分だと言うかもしれませんが、あなたの会社がこれまでに多くの重要なプログラムを開発してきた場合、ある日突然、誰かがこのプロジェクトのチャットモジュールが良いと言いました。それを別のプロジェクトに入れてください。コードを変更せずにそのまま使用できますか?2. システム内で他のモジュールに依存しているモジュールはいくつありますか?
システムのさまざまなモジュールは緊密に結合されていますか?この質問を懸念事項として取り上げる前に、すべてのモジュールに依存関係があってはならないという意味ではないことを最初に説明します。たとえば、詳細な関数は基本関数から拡張される可能性があります。私の問題はこの状況とは異なります。理論的には、異なる機能モジュール間の依存関係について話しています。
3. プログラムの特定の部分が失敗しても、他の部分はまだ機能しますか?
Gmail と同様のプログラムを構築すると、Gmail の多くのモジュールが動的に読み込まれることがわかります。たとえば、ページの初期化時にチャット モジュールが読み込まれず、読み込み後にエラーが発生しても、他の部分は読み込まれません。ページは読み込まれませんが、正常に使用できます。
4. 各モジュールは簡単にテストできますか? 各モジュールは、何百万人ものユーザーがいる大規模なサイトで使用されたり、複数のサイトで使用されたりする可能性があるため、モジュールは、アーキテクチャ内であれ、アーキテクチャ外部であれ、テストに耐えられる必要があります。さまざまな環境で通用するほとんどのアサーションを含めて、テストが簡単である必要があります。
長期的な視点で考える
大規模なプログラムを構築するとき、最も重要なことは、1 か月後、1 年後の状況だけを考慮するだけではなく、変更の可能性も考慮することです。長期的には?開発者は、場合によっては別のロジックを別のモジュールにカプセル化する場合でも、DOM 操作コードをプログラムにあまりにも厳密にバインドすることがよくあります。考えてみてください。なぜそれが長期的には良くないのでしょうか。
私の同僚はかつて、正確なアーキテクチャは将来のシナリオには適さないかもしれないと言いました。これは時々真実ですが、それを行う必要がある場合は、多額の費用を支払わなければなりません。たとえば、特定のパフォーマンス、セキュリティ、および設計上の理由から、Dojo、jQuery、Zepto、YUI のいずれかを選択して置き換える必要がある場合があります。このとき、ほとんどのモジュールには依存関係があり、それには費用と時間がかかります。 、人が必要ですよね?
一部の小規模なサイトでは問題ありませんが、大規模なサイトでは、さまざまなモジュール間のさまざまな問題を心配せずに、より柔軟なメカニズムを提供する必要があります。これにより、お金と時間が節約されます。
要約すると、プログラム全体を書き直さずにいくつかのライブラリを置き換えることができると確信できますか?そうでない場合は、これから説明する内容の方が適切だと思います。
多くの経験豊富な JavaScript 開発者がいくつかの重要なメモを提供しました:
JavaScriptMVC の作者である Justin Meyer は次のように述べています:
大規模なプログラムを構築する最大の秘訣は、決して大規模なプログラムを構築せず、プログラムを個々のコンポーネントに分割することです。小さなモジュールを作成して、各小さなモジュールをテストし、サイズ変更し、プログラムに統合できるようにします。
高パフォーマンス JavaScript Web サイトの著者、ニコラス・ザカス氏:
「重要なのは、これがどのように成長するか全く分からないことを最初から認めることです。すべてを知っているわけではないことを受け入れると、システムの設計が始まります。変更される可能性のある主要な領域を特定しますが、これは、たとえば、別のシステムと通信するアプリのどの部分も変更される可能性が高いと想定する必要があるため、これは多くの場合非常に簡単です。 「それを抽象化する必要があります。」 -
文章の問題が多くて面倒です。要約すると、すべては可変であるため、抽象的でなければなりません。
『jQuery Fundamentals』著者、Rebecca Murphey:
各モジュール間の接続が密になるほど、再利用性が低くなり、変更が難しくなります。
上記の重要な点は、アーキテクチャを構築するための核となる要素であり、常に念頭に置く必要があります。
ブレインストーミング
疎結合アーキテクチャが必要です。各モジュールはプログラムと通信し、中間層が対応するフィードバック メッセージを処理します。
たとえば、オンライン ベーカリー プログラムを構築するための JavaScript がある場合、モジュールは「42 個のパンを配送する必要があります」などのメッセージを送信します。次のように、モジュールによって送信されたメッセージを処理するためにさまざまなレイヤーを使用します:
モジュールはプログラムのコアに直接アクセスしません
モジュールは他のモジュールを直接呼び出したり、他のモジュールに影響を与えたりしません
これにより、次のようなエラーが発生するのを防ぎます特定のモジュールエラー すべてのモジュールに障害があります。
もう 1 つの問題はセキュリティです。実際の状況では、プログラムは自分で構築したものであり、どれがプライベートであるかはわかっています。セキュリティ上は問題ありませんが、どのモジュールにプログラムのコアへのアクセス許可があるかを定義する方法はありますか?たとえば、チャット モジュールがある場合、管理モジュールを呼び出したり、DB 書き込み権限を持つモジュールを呼び出したりしたくないのは、それらの間の関係が非常に脆弱であり、簡単に危険な状態に陥る可能性があるためです。 XSS 攻撃に対応します。各モジュールがすべてを実行できるわけではありませんが、これが現在のほとんどのアーキテクチャにおける JavaScript コードの問題です。どのモジュールが許可された部分にアクセスできるかを制御する中間層を提供します。つまり、モジュールは最も許可された部分のみを実行できます。
提案されたアーキテクチャ
今回の記事の焦点はここにあります。モジュール、ファサード、メディエーターという、誰もがよく知っているデザイン パターンが使用されています。
従来のモデルとは異なり、各モジュールを分離するために、メディエーター モードは、これらのモジュールからのメッセージ メッセージをサブスクライブし、ファサード モード ユーザーが通知の応答を制御する役割を担うだけです。各モジュールの権限を制限します。
注目すべき部分は以下のとおりです:
1 デザインパターン
1.1 モジュール理論
1.1.1 概要
1.1.2 モジュールパターン
1.1.3 オブジェクトの自己面
1.1.4 CommonJS モジュール
1.2 ファサードモード
1.3 メディエーター パターン
2 はあなたのアーキテクチャに適用されます
2.1 ファサード - コア抽象化
2.2 メディエーター - プログラム コア
2.3 緊密に連携
モジュール理論
誰もが多かれ少なかれモジュール化されたコードとモジュールを使用する可能性があります完全で堅牢なプログラム アーキテクチャの一部です。各モジュールは個別の目的のために作成されています。Gmail に戻って例を見てみましょう。実際には、チャット モジュールには多くの個別のサブコンポーネントがあります。たとえば、内部の式モジュールは実際には別のサブモジュールであり、電子メールを送信するためのウィンドウでも使用されます。
もう 1 つは、モジュールを動的にロード、削除、置換できることです。
JavaScript では、モジュールを実装する方法がいくつかあります。最もよく知られているのは、モジュール モードとオブジェクト リテラルです。すでにこれらに精通している場合は、このセクションを無視して、直接 CommonJS の部分に進んでください。
モジュール パターン
モジュール パターンは、中括弧を介してプライベート変数、メソッド、および状態をカプセル化できます。このデザイン パターンでは、通常、グローバル オブジェクトに直接アクセスできません。 1 つの API のみが返され、他のすべてのコンテンツはプライベートとしてカプセル化されます。
さらに、このパターンは自己実行関数式と似ていますが、唯一の違いは、モジュールがオブジェクトを返すのに対し、自己実行関数式は関数を返すことです。
ご存知のとおり、JavaScript には他の言語のようなアクセス修飾子がありません。フィールドやメソッドごとにプライベート修飾子とパブリック修飾子を宣言することはできません。つまり、いくつかのパブリック メソッドを含むオブジェクトを返します。これらのメソッドには内部オブジェクトを呼び出す機能があります。
次のコードを見てください。このコードは、グローバル オブジェクト BasketModule を含んでいます。そのため、プログラム全体はこのプライベート配列にアクセスできません。この 3 つのメソッド (addItem、getItemCount、getTotal など) が返され、プライベート バスケット配列にアクセスできます。
var basketModule = (function() { var basket = []; //private return { //exposed to public addItem: function(values) { basket.push(values); }, getItemCount: function() { return basket.length; }, getTotal: function(){ var q = this.getItemCount(),p=0; while(q--){ p+= basket[q].price; } return p; } } }());
また、返すオブジェクトは直接 BasketModule に割り当てられるので、次のように使用できることにも注意してください:
//basketModule is an object with properties which can also be methods basketModule.addItem({item:'bread',price:0.5}); basketModule.addItem({item:'butter',price:0.3}); console.log(basketModule.getItemCount()); console.log(basketModule.getTotal()); //however, the following will not work: console.log(basketModule.basket);// (undefined as not inside the returned object) console.log(basket); //(only exists within the scope of the closure)
さまざまな一般的なライブラリ (Dojo、jQuery など) でそれを行うにはどうすればよいですか?
Dojo
Dojo は、クラススタイルの宣言メソッドを提供するために dojo.declare を使用しようとします。たとえば、ストア名前空間でバスケット オブジェクトを宣言する場合にそれを使用できます。
//traditional way var store = window.store || {}; store.basket = store.basket || {}; //using dojo.setObject dojo.setObject("store.basket.object", (function() { var basket = []; function privateMethod() { console.log(basket); } return { publicMethod: function(){ privateMethod(); } }; }()));
dojo.provide と組み合わせて使用すると、非常に強力です。
YUI
次のコードはYUIの元の実装です:
YAHOO.store.basket = function () { //"private" variables: var myPrivateVar = "I can be accessed only within YAHOO.store.basket ."; //"private" method: var myPrivateMethod = function () { YAHOO.log("I can be accessed only from within YAHOO.store.basket"); } return { myPublicProperty: "I'm a public property.", myPublicMethod: function () { YAHOO.log("I'm a public method."); //Within basket, I can access "private" vars and methods: YAHOO.log(myPrivateVar); YAHOO.log(myPrivateMethod()); //The native scope of myPublicMethod is store so we can //access public members using "this": YAHOO.log(this.myPublicProperty); } }; } ();
jQuery
jQueryにはモジュールパターンの実装がたくさんあります。別の例であるライブラリ関数を見てみましょう。宣言 新しいライブラリを作成し、ライブラリ作成時に document.ready の init メソッドを自動的に実行します。
function library(module) { $(function() { if (module.init) { module.init(); } }); return module; } var myLibrary = library(function() { return { init: function() { /*implementation*/ } }; }());
オブジェクトのセルフファセット
オブジェクトのセルフファセットは中括弧を使用して宣言されており、公開/非公開をあまり気にしない場合は、使用するときに new キーワードを使用する必要はありません。モジュール内の属性フィールドのこのメソッドを使用できますが、このメソッドは JSON とは異なることに注意してください。オブジェクト self-face: var item={name: "tom"、value:123} JSON: var item={"name":"tom"、"value":123}。
var myModule = { myProperty: 'someValue', //object literals can contain properties and methods. //here, another object is defined for configuration //purposes: myConfig: { useCaching: true, language: 'en' }, //a very basic method myMethod: function () { console.log('I can haz functionality?'); }, //output a value based on current configuration myMethod2: function () { console.log('Caching is:' + (this.myConfig.useCaching) ? 'enabled' : 'disabled'); }, //override the current configuration myMethod3: function (newConfig) { if (typeof newConfig == 'object') { this.myConfig = newConfig; console.log(this.myConfig.language); } } }; myModule.myMethod(); //I can haz functionality myModule.myMethod2(); //outputs enabled myModule.myMethod3({ language: 'fr', useCaching: false }); //fr
CommonJS
CommonJS の導入については、これまでの多くの記事で紹介されていますが、ここで言及したいのは、CommonJS 標準には 2 つの重要なパラメータがあるということです。 .exports はロードされるモジュールを表し、require はこれらのロードされたモジュールが他のモジュールに依存する必要があり、またロードされる必要があることを表します。
/* Example of achieving compatibility with AMD and standard CommonJS by putting boilerplate around the standard CommonJS module format: */ (function(define){ define(function(require,exports){ // module contents var dep1 = require("dep1"); exports.someExportedFunction = function(){...}; //... }); })(typeof define=="function"?define:function(factory){factory(require,exports)});
CommonJS の標準モジュールの読み込み実装はたくさんありますが、私が気に入っているのは RequireJS です。画像を ASCII コードに変換するなど、簡単な例を見てみましょう。理論的には、コードは次のようになります:
var encodeToASCII = require("encoder").encodeToASCII; exports.encodeSomeSource = function(){ //其它操作以后,然后调用encodeToASCII }
但是上述代码并没用工作,因为encodeToASCII函数并没用附加到window对象上,所以不能使用,改进以后的代码需要这样才行:
define(function(require, exports, module) { var encodeToASCII = require("encoder").encodeToASCII; exports.encodeSomeSource = function(){ //process then call encodeToASCII } });
CommonJS 潜力很大,但是由于大叔不太熟,所以就不过多地介绍了。
Facade模式
Facade模式在本文架构里占有重要角色,关于这个模式很多JavaScript类库或者框架里都有体现,其中最大的作用,就是包括High level的API,以此来隐藏具体的实现,这就是说,我们只暴露接口,内部的实现我们可以自己做主,也意味着内部实现的代码可以很容易的修改和更新,比如今天你是用jQuery来实现的,明天又想换YUI了,这就非常方便了。
下面这个例子了,可以看到我们提供了很多私有的方法,然后通过暴露一个简单的 API来让外界执行调用内部的方法:
var module = (function () { var _private = { i: 5, get: function () { console.log('current value:' + this.i); }, set: function (val) { this.i = val; }, run: function () { console.log('running'); }, jump: function () { console.log('jumping'); } }; return { facade: function (args) { _private.set(args.val); _private.get(); if (args.run) { _private.run(); } } } } ()); module.facade({run:true, val:10}); //outputs current value: 10, running
Facade和下面我们所说的mediator的区别是,facade只提供现有存在的功能,而mediator可以增加新功能。
Mediator模式
讲modiator之前,我们先来举个例子,机场飞行控制系统,也就是传说中的塔台,具有绝对的权利,他可以控制任何一架飞机的起飞和降落时间以及地方,而飞机和飞机之前不允许通信,也就是说塔台是机场的核心,mediator就相当于这个塔台。
mediator就是用在程序里有多个模块,而你又不想让各个模块有依赖的话,那通过mediator模式可以达到集中控制的目的。实际场景中也是,mediator封装了很多不想干的模块,让他们通过mediator联系在一起,同时也松耦合他们,使得他们之间必须通过mediator才能通信。
那mediator模式的优点是什么?那就是解耦,如果你之前对观察者模式比较了解的话,那理解下面的mediator图就相对简单多了,下图是一个high level的mediator模式图:
想想一下,各模块是发布者,mediator既是发布者又是订阅者。
Module 1向Mediator广播一个实际,说需要做某事
Mediator捕获消息以后,立即启动处理该消息需要使用的Module 2,Module 2处理结束以后返回信息给Mediator
与此同时,Mediator也启动了Module 3,当接受Module 2 返回消息的时候自动记录日志到Module 3里
可以看到,各模块之间并没有通信,另外Mediator也可以实现监控各模块状态的功能,例如如果Module 3出错了,Mediator可以暂时只想其它模块,然后重启Module 3,然后继续执行。
回顾一下,可以看到,Mediator的优点是:松耦合的模块由同一的Mediator来控制,模块只需要广播和监听事件就可以了,而模块之间不需要直接联系,另外,一次信息的处理可以使用多个模块,也方便我们以后统一的添加新的模块到现有的控制逻辑里。
确定是:由于所有的模块直接都不能直接通信,所有相对来说,性能方面可能会有少许下降,但是我认为这是值得的。
我们根据上面的讲解来一个简单的Demo:
var mediator = (function(){ var subscribe = function(channel, fn){ if (!mediator.channels[channel]) mediator.channels[channel] = []; mediator.channels[channel].push({ context: this, callback: fn }); return this; }, publish = function(channel){ if (!mediator.channels[channel]) return false; var args = Array.prototype.slice.call(arguments, 1); for (var i = 0, l = mediator.channels[channel].length; i < l; i++) { var subscription = mediator.channels[channel][i]; subscription.callback.apply(subscription.context, args); } return this; }; return { channels: {}, publish: publish, subscribe: subscribe, installTo: function(obj){ obj.subscribe = subscribe; obj.publish = publish; } }; }());
然后有2个模块分别调用:
//Pub/sub on a centralized mediator mediator.name = "tim"; mediator.subscribe('nameChange', function(arg){ console.log(this.name); this.name = arg; console.log(this.name); }); mediator.publish('nameChange', 'david'); //tim, david //Pub/sub via third party mediator var obj = { name: 'sam' }; mediator.installTo(obj); obj.subscribe('nameChange', function(arg){ console.log(this.name); this.name = arg; console.log(this.name); }); obj.publish('nameChange', 'john'); //sam, john
应用Facade: 应用程序核心的抽象
一个facade是作为应用程序核心的一个抽象来工作的,在mediator和模块之间负责通信,各个模块只能通过这个facade来和程序核心进行通信。作为抽象的职责是确保任何时候都能为这些模块提供一个始终如一的接口(consistent interface),和sendbox controller的角色比较类似。所有的模块组件通过它和mediator通信,所以facade需要是可靠的,可信赖的,同时作为为模块提供接口的功能,facade还需要扮演另外一个角色,那就是安全控制,也就是决定程序的哪个部分可以被一个模块访问,模块组件只能调用他们自己的方法,并且不能访问任何未授权的内容。例如,一个模块可能广播dataValidationCompletedWriteToDB,这里的安全检查需要确保该模块拥有数据库的写权限。
总之,mediator只有在facade授权检测以后才能进行信息处理。
应用Mediator:应用程序的核心
メディエーターはアプリケーションの中核的な役割として機能します。彼の責任について簡単に説明します。コアの仕事は、モジュールのライフサイクルを管理することです。このコアが受信した情報を取得したときに、プログラムがそれをどのように処理するかを決定する必要があります。つまり、どのモジュールを開始または停止するかを決定する必要があります。モジュールが開始されるとき、アプリケーション コアがモジュールを実行するかどうか (たとえば、DOM の準備ができたときに実行するかどうか) を決定することなく自動的に実行できるようにする必要があるため、モジュール自体が決定する必要があります。
どのような状況でモジュールが停止するのかという疑問がまだあるかもしれません。プログラムがモジュールの障害またはエラーの発生を検出した場合、プログラムは、コンポーネントを再起動できるように、モジュール内のメソッドの実行を継続しないように決定する必要があります。主な目的は、ユーザーの操作性を向上させることです。経験。
さらに、コアは他の機能に影響を与えることなく、モジュールを動的に追加または削除できる必要があります。よくある例としては、ページの読み込み開始時にはモジュールが利用できないが、Gmail のチャット機能と同様に、ユーザーの操作後にモジュールが動的に読み込まれて実行される必要があることが、パフォーマンス最適化の観点から挙げられます。とても良いです。
例外エラー処理もアプリケーション コアによって処理されます。また、各モジュールが情報をブロードキャストするときに、すべてのエラーもコアにブロードキャストされるため、プログラム コアは状況に応じてこれらのモジュールを停止/再起動できます。これは、疎結合アーキテクチャの重要な部分でもあり、メディエーターを介してパブリッシュ/サブスクライブを使用することで、モジュールを手動で変更する必要がありません。
アセンブル
以下の QA セクション「モジュール」で説明されているように、各モジュールにはプログラム内にさまざまな関数が含まれており、処理する必要がある情報がある場合、情報をリリースしてプログラムに通知します (これがモジュールの主な役割です)。一部の DOM ツール操作メソッドに依存することはできますが、システムの他のモジュールに依存すべきではありません。モジュールは次の点に焦点を当てるべきではありません:
1. このモジュールによって公開される情報をサブスクライブするオブジェクトまたはモジュール 2. これらのオブジェクトクライアントはエンドオブジェクトまたはサーバーサイドオブジェクトです
3. 情報をサブスクライブするオブジェクトの数
パブリッシュ/サブスクライブサブの発行の拡張: 自動イベント登録
自動登録イベントに関しては、特定の命名規則に従う必要があります。たとえば、モジュールが messageUpdate という名前のイベントを発行する場合、モジュールのすべてのイベントは messageUpdate メソッドを使用します。自動的に実行されます。具体的な実装方法については、私の別の投稿「Magic Upgraded Version of jQuery Custom Binding」を参照してください。QA1. ファサードまたは同様のサンドボックス モードを使用しないことはできますか?
技術的に言えば、現時点でモジュールがアプリケーション コアと直接通信できない理由はありませんが、ほとんどのアプリケーション エクスペリエンスでは、それはまだ必要ありません。このアーキテクチャを選択したので、そのアーキテクチャで定義されたルールに従う必要があります。
上記は私があなたのためにまとめたものです。
関連記事:
JS でのイベントに対する詳細な回答 (グラフィック チュートリアル)
JSON オブジェクトを文字列に変換する (コードが添付された詳細な回答)
JS でのイベント バブリングとイベント キャプチャ (画像とテキストチュートリアル、単純な暴力)
以上が大規模な JavaScript アプリケーション アーキテクチャのデザイン パターン (上級)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。