2014-04-06 15 views
9

我想了解一下Node和異步編程。我閱讀了有關Promises的信息,並試圖在一個小型項目中將它們用於從服務A複製到服務B的用戶。我在理解如何在Promises之間傳遞狀態方面存在一些困難有什麼模式通過JavaScript中的承諾鏈傳遞狀態?

項目已寫入使用我目前的問題Promise library

一個簡單的定義是的NodeJS:

  • 從服務A到服務B用戶複製帖子如果帖子不服務B.
  • 已經存在這兩種服務都提供了http API,令人難忘的用戶標識來查找該用戶的帖子,所以用戶標識必須從用戶名中查找。
  • 所有的http調用都是異步的。

這是一些僞代碼,說明我如何將Promises鏈接在一起。

Promise.from('service_A_username') 
    .then(getServiceAUserIdForUsername) 
    .then(getServiceAPostsForUserId) 
    .then(function(serviceAPosts) { 
    // but what? store globally for access later? 
    doSomethingWith(serviceAPosts); 
    return Promise.from('service_B_username'); 
    }) 
    .then(getServiceBUserIdForUsername) 
    .then(getServiceBPostsForUserId) 
    .done(function(serviceBPosts) { 
    // how do we interact with Service A posts? 
    doSomethingThatInvolvesServiceAPostsWith(serviceBPosts); 
    }); 

有一對夫婦的事情,我也想過做:

  1. 帶上getPostsForUserId函數內getIdForUsername電話。 然而,,我想保持每個功能單元儘可能簡單,遵循'做一件事,做得好'的原則。
  2. 創建一個'context'對象並將其傳遞到整個鏈中,讀取並存儲此對象中的狀態。 但是這種方法使每個功能都非常適合於一個鏈,因此很難單獨使用。

是否還有其他選擇,建議採用哪種方法?

+1

FWIW,上下文對象選項不是全部*那*壞。如果你認爲'getServiceAUserIdForUsername'函數是「接受一個'userName''參數並返回一個'userId''',那麼''接受一個具有'userName'屬性並填充'userId'屬性的對象不是*那個*更具體到一個用例。這裏肯定有更多的耦合(調用代碼和函數需要同意這些名稱,通常他們不會這樣做),但事實是,如果您不僅需要「一個參數導致一個返回值」,您需要有一些方法來識別不同的位。 –

+0

但是如果你想,我可以看到有一個通用的聚合器基礎設施,所以只有調用代碼知道這些屬性名稱。聚合器基礎結構將接受你的「一個arg產生一個結果」的列表,這些啓動函數(因此沒有這些函數已知的名稱)和要使用的名稱,並將調用包裝到它們以填充對象。嗯。我不知道會是什麼樣子。 –

+0

感謝@ t-j-crowder關於通過管道傳遞的上下文對象,您是否知道任何特別使用此模式的JS庫? Java有[Apache Camel](http://camel.apache.org/),它專門用於這種路由,我想知道是否有類似的js。 –

回答

3

我會用Promise.all,這樣

Promise.all([Promise.from('usernameA'), Promise.from('usernameB')]) 
    .then(function(result) { 
     return Promise.all([getUsername(result[0]),getUsername(result[1])]) 
    }) 
    .then(function(result) { 
     return Promise.all([getPosts(result[0]),getPosts(result[1])]); 
    }) 
    .then(function(result) { 
     var postsA = result[0], postsB = result[1]; 
     // Work with both the posts here 
    }); 
+0

我喜歡這個答案@thefourtheye。將數組中的承諾鏈傳遞給Promise.all是合理的,如下所示: https://gist.github.com/andystanton/f9ba6135a523971b6775 對不起50次轉發 - 我沒意識到你可以'把代碼放在回答答案! –

+1

@安迪嗯,我得說,它看起來比我提出的解決方案好:) – thefourtheye

6

首先好的問題。這是我們(至少我)經常處理的承諾。在我看來,這也是一個承諾真正照耀回調的地方。

這是怎麼回事這裏基本上是你真正想要的,你的圖書館裏沒有兩件事情:

  1. .spread這需要返回一個數組,並從一個數組參數參數改變它的承諾。這允許將諸如.then(result) { var postsA = result[0], postsB = result[1];的東西切成.spread(postsA,postsB

  2. .map需要承諾數組並將數組中的每個承諾映射到另一個承諾 - 它就像.then,但是對於數組的每個值。

有兩種選擇,要麼使用已經使用他們喜歡Bluebird因爲我建議的實施是遠遠優於替代現在(更快,更好的堆棧跟蹤,更好的支持,更強的功能集)OR你可以實現它們。

由於這是一個答案,而不是一個圖書館的建議,讓我們做的是:

讓我們開始蔓延,這是比較容易的 - 它的意思是調用Function#apply其傳播的數組變量參數。下面是一個簡單的實現我stole from myself:

if (!Promise.prototype.spread) { 
    Promise.prototype.spread = function (fn) { 
     return this.then(function (args) { 
     //this is always undefined in A+ complaint, but just in case 
      return fn.apply(this, args); 
     }); 

    }; 
} 

接下來,讓我們做映射。 .map上的承諾基本上是數組映射了那麼:

if(!Promise.prototype.map){ 
    Promise.prototype.map = function (mapper) { 
     return this.then(function(arr){ 
      mapping = arr.map(mapper); // map each value 
      return Promise.all(mapping); // wait for all mappings to complete 
     }); 
    } 
} 

爲方便起見,我們可以引入的.map靜態對應啓動鏈:

Promise.map = function(arr,mapping){ 
    return Promise.resolve(arr).map(mapping); 
}; 

現在,我們可以編寫代碼就像我們實際上想要:

var names = ["usernameA","usernameB"]; // can scale to arbitrarily long. 
Promise.map(names, getUsername).map(getPosts).spread(function(postsA,postsB){ 
    // work with postsA,postsB and whatever 
}); 

這是我們一直想要的語法。沒有代碼重複,乾乾淨淨,簡潔明瞭,承諾之美。

請注意,這並不會破壞Bluebird的表面 - 例如,Bluebird會檢測到它是一個地圖鏈,並會將第二個請求的「推送」功能放到第二個請求上,而第一個請求甚至無法完成,因此getUsername第一個用戶不會等待第二個用戶,但如果速度更快,實際上會調用getPosts,因此在這種情況下,它與您自己的要點版本一樣快,同時更清晰地imo。

但是,它工作,很好。

Barebones A +實現更多的是承諾庫之間的互操作性,應該是一個'基準線'。在設計特定平臺小型API時它們非常有用 - IMO幾乎從不。像Bluebird這樣的實體庫可以顯着減少你的代碼。您正在使用的Promise庫,甚至在他們的文檔中說:

它旨在獲得正確的基礎點,以便您可以在其上構建擴展的承諾實現。