首頁  >  文章  >  web前端  >  大型JavaScript應用程式架構設計模式(進階篇)

大型JavaScript應用程式架構設計模式(進階篇)

亚连
亚连原創
2018-05-21 09:23:512503瀏覽

下面是我要幫大家整理的大型JavaScript應用程式架構設計模式,有興趣的同學可以去看看。

以下是本文的主要章節:

1. 什麼叫「JavaScript大型程式」?

2. 顧目前的程式架構

3. 長遠考慮

#4. 腦力激盪

5. 建議的架構

5.1 設計模式

        5.1.1 模組理論

            5.1.1.1 篇綜述

##      5.1.1.1 綜述

##   #.          5.1.1.3 對象自面量

            5.1.1.4 CommonJS模組

        5.1.2 Facade         5.2 .1 Facade - 核心抽象

            5.2.2 Mediator - 程式核心

       事件

7. Q & A

8. 致謝

什麼叫「JavaScript大型程式」?

在我們開始之前,我們來定義一下什麼叫大型JavaScript站點,很多有經驗的JS開發高手也都被challenge住了,有人說超過10萬行JavaScript程式碼才算大型,也有人說JavaScript程式碼要超過1MB大小才算,其實2者都不能算對,因為不能安裝程式碼量的多少來衡量,很多瑣碎的JS程式碼很容易超過10萬行的。

我對「大」的定義如下,雖然可能不太對,但是應該是比較接近了:

我個人認為,大型JavaScript程式應該是非常重要並且融入了很多卓越開發人員努力,對重量級數據進行處理並且展示給瀏覽器的程式。

回顧目前的程式架構

我不能強調說這個問題有多重要,很多有經驗的開發人員常說:「現有的創意和設計模式在我上一個中型專案上運行得非常好,所以在稍微大型點的程式裡再次使用,應該沒問題,對吧?是大型程序,通常就應該有大的Concerns需要分解關注,我簡短解釋一下要花時間來review當前運行了很長時間的程序架構。大多數情況下,目前的JavaScript程式架構應該是如下這個樣子的(注意,是JS架構,不是大家常說的ASP.NET MVC):

    custom widgets

    models    views#    models

    views

 controllers    templates    libraries/toolkits

    an application core.


#你可能還會將程式單獨封裝成多個modules,或使用其他的設計模式,這很好,但是如果這些結構完全代表你的架構的話,就可能會有一些潛在的問題,我們來看看幾個重要的點:


1.你架構裡的東西,有多少可以立即拿出來重用?
有沒有一些單獨的module不依賴別的程式碼?是自包含麼?如果我到你們正在使用的程式碼庫上去隨即挑選一些模組module程式碼,然後放在一個新頁面,是否能立即就能使用?你可能會說原理通就可以了,我建議你長久打算一下,如果你的公司之前開發很多重要的程序,突然有一天有人說,這個項目裡的聊天模組不錯,我們拿出來放在另外一個項目裡吧,你能直接拿過來不修改程式碼就能用麼?

2.系統裡有多少模組module需要依賴其他模組?

系統的各個模組是不是都很緊耦合?在我將這個問題作為concern之前,我先解釋一下,不是說所有的模組都絕對不能有任何依賴,例如一個細粒度的功能可能是從base功能擴展來的,我的問題和這種情況不一樣,我說的是不同功能模組之前的依賴,理論上,所有的不同功能模組都不應該有太多的依賴。

3.如果你程式的某一部分出錯了,其他部分是否能夠依然運作? 如果你建立一個和Gmail差不多的程序,你可以發現Gmail裡很多模組都是動態加載的,比如聊天chat模組,在初始化頁面的時候是不加載的,而且就算加載以後出錯了,頁面的其他部位也能正常使用。

4.你的各個模組Module能很簡單的進行測試麼? 你的每一個模組都有可能用在數百萬用戶的大型站點上,甚至多個站點都使用它,所以你的模組需要能經得住測試,也就是說,不管是在架構內部還是架構外部,都應該能很簡單的去測試,包括大部分的斷言在不同的環境下都能夠通過。

長遠考慮

架構大型程式的時候,最重要的是要有前瞻性,不能只考慮一個月或一年以後的情況,要考慮更在長久的情況下,有什麼改變的可能性?開發人員經常將DOM操作的程式碼和程式綁定得太緊,儘管有時已經封裝單獨的邏輯到不同的模組裡了,想想一下,長久以後,為什麼不是很好。

我的一個同事曾經說過,一個精確的架構可能不適合未來的情景,有時是正確的,但是當你需要該做的話,你所付出的money那可是相當地多哦。例如,你可能因為某些效能,安全,設計的原因需要在Dojo, jQuery, Zepto, YUI之間需要選擇替換,這時候就有問題了,大部分模組都有依賴,需要錢呀,需要時間啊,需要人呀,對不?

對於一些小型站點沒事,但是大型站點確實需要提供一個更加靈活的機制,而不去擔心各個模組之間的各種問題,這既然節約錢,又能節省時間。

總結一下,現在你能確定你能不重寫整個程式就能取代一些類別庫麼?如果不能,那估計我們下面要講的內容,就比較適合你了。

很多有經驗的JavaScript開發者給了一些關鍵的notes:

JavaScriptMVC的作者Justin Meyer說:

#建立大型程式最大的秘密就是從來不建立大型程序,而是將程式分解成各個小的模組去做,讓每個小模組都可測試,可size化,然後整合到程式裡。

High-performance JavaScript websites作者Nicholas,Zakas:
"The key is to acknowledge from the start that you have no idea how this will grow. When you accept that you don't know everything, you begin to design the system defensively. You identify the key areas that may change, which often is very easy when you put a little bit of time into it. For instance, you should expect that any part of the appates that partic likely change, so you need to abstract that away." -

一大堆文字問題,太麻煩了,總結一句就是,一切皆可變,所以要抽象。

jQuery Fundamentals作者Rebecca Murphey:
各個模組之間聯繫的越密切,重用性越小,改變起來困難越大。

以上這些重要觀點,是建構架構的核心要素,我們需要時時記得。

腦力激盪

我們來腦力激盪一下,我們需要一個鬆散耦合的架構,各模組之間沒有依賴,各個模組和程式進行通信,然後中間層接管和處理回饋相應的訊息。

例如,我們如果有一個JavaScript構建在線麵包店程序,一個模組發出了一個信息可能是“有42個圓麵包需要派件”。我們使用不同的layer層來處理模組發送的訊息,做到如下:

    模組不直接存取程式核心
    模組不會直接呼叫或影響其它的模組

這將防止我們因為某個模組出錯,而導致所有的模組出錯。

另一個問題是安全,真實的情況是,大多數人都不認為內部安全是個問題,我們自己心裡說,程式是我自己建構的,我知道哪些是公開的那些私有的,安全沒問題,但你有沒有辦法定義哪個模組才能權限存取程式核心?例如,有一個chat聊天模組,我不想讓他呼叫admin模組,或不想讓它呼叫有DB寫權限的模組,因為這之間存在很脆弱,很容易導致XSS攻擊。每個模組不應該能做所有的事情,但是目前大多數架構裡的JavaScript程式碼都有這種的問題。提供一個中間層來控制,哪個模組可以存取那個授權的部分,也就是說,這個模組最多只能做到我們所授權的部分。

建議的架構

我們本文的重點來了,這次我們提議的架構使用了我們都很熟知的設計模式:module, facade和mediator。

和傳統的模型不一樣的是,為了了解耦各個模組,我們只讓模組發布一些event事件,mediator模式可以負責從這些模組上訂閱訊息message,然後控制通知的response,facade模式使用者限制各模組的權限。

以下是我們要注意講解的部分:
    1 設計模式
        1.1 模組理論
            1.1.1綜述綜述
.1                1.1.3 對象自面量
            1.1.4 CommonJS模組
        1.2 Facade模式
        1.3 Mediatortorop
       核心抽象
        2.2 Mediator - 核心抽象化
        2.3 緊密聯合運作起來

模組論

大家可能都或多或少地使用了模組化的程式碼,模組是一個完整的強健程式架構的一部分,每個模組都是為了單獨的目的為創建的,回到Gmail,我們來個例子,chat聊天模組看起來是個單獨的一部分,其實它是有很多單獨的子模組來構成,例如裡面的表情模組其實就是單獨的子模組,也被用到了發送郵件的視窗。

另外一個是模組可以動態加載,刪除和替換。

在JavaScript裡,我們又幾種方式來實現模組,大家熟知的是module模式和對象字面量,如果你已經熟悉這些,請忽略此小節,直接跳到CommonJS部分。

Module模式

module模式是比較流行的設計模式,它可以透過大括號封裝私有的變量,方法,狀態的,透過包裝這些內容,一般全域的物件不能直接訪問,在這個設計模式裡,只回傳一個API,其它的內容全部被封裝成私有的了。

另外,這個模式和自執行的函數表達式比較相似,唯一的不同是module回傳的是對象,而自執行函數表達式回傳的是function。

眾所周知, JavaScript不想其它語言一樣有存取修飾符,不能為每個欄位或方法宣告private,public修飾符,那這個模式我們是如何實現的呢?那就是return一個對象,裡麵包括一些公開的方法,這些方法有能力去呼叫內部的對象。

看一下,下面的程式碼,這段程式碼是一個自執行程式碼,宣告裡包含了一個全域的物件basketModule, basket數組是一個私有的,所以你的整個程式是不能存取這個私有數組的,同時我們return了一個對象,其內包含了3個方法(例如addItem,getItemCount,getTotal),這3個方法可以存取私有的basket數組。

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;
  }
 }
}());

同時注意,我們return的物件直接賦值給了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)裡是如何來做呢?

DojoDojo試圖使用dojo.declare來提供class風格的宣告方式,我們可以利用它來實作Module模式,例如如果你想再store命名空間下宣告basket對象,那麼可以這麼做:

//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);
 }
 };

} ();

jQueryjQuery裡有很多Module模式的實現,我們來看一個不同的例子,一個library函數宣告了一個新的library,然後創建該library的時候,在document.ready裡自動執行init方法。

function library(module) {
  $(function() {
    if (module.init) {
      module.init();
    }
  });
  return module;
}
 
var myLibrary = library(function() {
  return {
    init: function() {
      /*implementation*/
      }
  };
}());

物件自面量物件自面量使用大括號聲明,並且使用的時候不需要使用new關鍵字,如果對一個模組裡的屬性欄位的publice/private不是很在意的話,可以使用這種方式,不過請注意這種方式和JSON的不同。物件自面量: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,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碼,我們先載入encoder模組,然後取得他的encodeToASCII方法,理論上代碼應該是如下:

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模式图:

大型JavaScript應用程式架構設計模式(進階篇)

想想一下,各模块是发布者,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(&#39;nameChange&#39;, function(arg){
 console.log(this.name);
 this.name = arg;
 console.log(this.name);
});
 
mediator.publish(&#39;nameChange&#39;, &#39;david&#39;); //tim, david
 
 
//Pub/sub via third party mediator
 
var obj = { name: &#39;sam&#39; };
mediator.installTo(obj);
obj.subscribe(&#39;nameChange&#39;, function(arg){
 console.log(this.name);
 this.name = arg;
 console.log(this.name);
});
 
obj.publish(&#39;nameChange&#39;, &#39;john&#39;); //sam, john

应用Facade: 应用程序核心的抽象

一个facade是作为应用程序核心的一个抽象来工作的,在mediator和模块之间负责通信,各个模块只能通过这个facade来和程序核心进行通信。作为抽象的职责是确保任何时候都能为这些模块提供一个始终如一的接口(consistent interface),和sendbox controller的角色比较类似。所有的模块组件通过它和mediator通信,所以facade需要是可靠的,可信赖的,同时作为为模块提供接口的功能,facade还需要扮演另外一个角色,那就是安全控制,也就是决定程序的哪个部分可以被一个模块访问,模块组件只能调用他们自己的方法,并且不能访问任何未授权的内容。例如,一个模块可能广播dataValidationCompletedWriteToDB,这里的安全检查需要确保该模块拥有数据库的写权限。

总之,mediator只有在facade授权检测以后才能进行信息处理。

应用Mediator:应用程序的核心

Mediator是作為應用程式核心的角色來工作的,我們簡單地說一下他的職責。最核心的工作就是管理模組的生命週期(lifecycle),當這個核心撲捉到任何資訊進來的時候,他需要判斷程式如何來處理——也就是說決定啟動或停止哪一個或一些模組。當一個模組開始啟動的時候,它應該能否自動執行,而不需要應用程式核心來決定是否該執行(例如,是否要在DOM ready的時候才能執行),所以說需要模組自身需要去判定。

你可能還有問題,就是一個模組在什麼情況下才會停止。當程式偵測到一個模組失敗了,或者是出錯了,程式需要做決定來防止繼續執行該模組裡的方法,以便這個元件可以重新啟動,目的主要是提高使用者體驗。

另外,該核心應該可以動態新增或刪除模組,而不影響其他任何功能。常見的例子是,一個模組在頁面加載初期是不可用,但是用戶操作以後,需要動態加載這個模組然後執行,就像Gmail裡的chat聊天功能一樣,從性能優化的目的來看,應該是很好理解的吧。

異常錯誤處理,也是由應用程式核心來處理的,另外各模組在廣播訊息的時候,也廣播任何錯誤到該核心裡,以便程式核心可以根據情況去停止/重啟這些模組。這也是鬆散耦合架構一個很重要的部分,我們不需要手動改變任何模組,透過mediator使用發布/訂閱就可以來做到這一點。

組裝起來

各模組包含了程式裡各種各樣的功能,他們有資訊需要處理的時候,發布資訊通知程式(這是他們的主要職責),下面的QA小節裡提到了,模組可以依賴一些DOM工具操作方法,但是不應該和系統的其它模組有依賴,一個模組不應該關注如下內容:

    1.哪個對象或是模組訂閱了這個模組發佈的資訊
    2.這些物件是客戶端物件還是伺服器端物件
    3.則是多少物件訂閱了你的訊息

大型JavaScript應用程式架構設計模式(進階篇)

大型JavaScript應用程式架構設計模式(進階篇)

大型JavaScript應用程式架構設計模式(進階篇)

# #Facade抽象應用程式的核心,避免各個模組之間直接通信,它從各模組上訂閱信息,也負責授權檢測,確保每個模組有用自己單獨的授權。

Mediator(應用程式核心)使用mediator模式扮演發布/訂閱管理器的角色,負責模組管理以及啟動/停止模組執行,可以動態載入以及重啟有錯誤的模組。

這個架構的結果是:各模組之間沒有依賴,因為鬆散耦合的應用,它們可以很容易地被測試和維護,各模組可以很容易地在其它項目裡被重複使用,也可以在不影響程式的情況下動態添加和刪除。

發布Pub/訂閱Sub的延伸:自動註冊事件(Automatic Event Registration)

關於自動註冊事件,需要遵守一定的命名規範,例如如果一個模組發布了一個名字為messageUpdate的事件,那麼所有帶有messageUpdate方法的模組都會自動執行。有好處也有利弊,具體實作方式,可以看我另外一篇文章:jQuery自訂綁定的魔法升級版。

QA

###1.有可能不使用facade或類似的sandbox模式麼? ######儘管架構的大綱裡提出了facade可以實現授權檢查的功能,其實完全可能由mediator去做,輕型架構要做的事情其實是幾乎一樣的,那就是解耦,確保各模組直接和應用程式核心通訊是沒問題的就行。 ######2.你提高了模組直接不能有依賴,是否意味著不能依賴任何第三方類別庫(例如jQuery)。 ######這其實就是一個兩面性的問題,我們上面說到了,一個模組也許有一些子模組,或者基礎模組,比如基本的DOM操作工具類等,在這個層面上講,我們是可以用第三方類庫的,但是請確保,我們可以輕鬆地能否替換掉他們。 ######3.我喜歡這個架構,並且想開始使用這個架構,有任何程式碼樣本可以參考麼? ######我打算去搞一份程式碼樣本給大家參考,不過在這之前,你可以參考Andrew Burgees的貼文Writing Modular JavaScript 。 ######4.如果模組需要和應用程式核心直接通信,是否可行? ###

技術上來將,沒有理由現在模組不能和應用程式核心直接通信,但是對於大多數應用體驗來說,還是不要。既然你選擇了這個架構,那就要遵守該架構所定義的規則。

上面是我整理給大家的,希望今後對大家有幫助。

相關文章:

JS中事件的詳細解答(圖文教學)

JSON物件轉換為字串(附上程式碼詳細解答)

JS的事件冒泡與事件擷取(圖文教程,簡單暴力)

以上是大型JavaScript應用程式架構設計模式(進階篇)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn